mirror of
https://github.com/dunglas/frankenphp.git
synced 2025-12-24 13:38:11 +08:00
feat: use threads instead of GoRoutines (#6)
* feat: use threads instead of GoRoutines * many improvements * fix some bugs
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.vscode/
|
||||
/caddy/frankenphp/frankenphp
|
||||
/internal/testserver/testserver
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "C-Thread-Pool"]
|
||||
path = C-Thread-Pool
|
||||
url = https://github.com/dunglas/C-Thread-Pool
|
||||
branch = feat/mac-os-compat
|
||||
1
C-Thread-Pool
Submodule
1
C-Thread-Pool
Submodule
Submodule C-Thread-Pool added at d42cc5a2f7
@@ -1,5 +1,59 @@
|
||||
# Contributing
|
||||
|
||||
## Compiling PHP
|
||||
## Running the test suite
|
||||
|
||||
Pass the `--enable-debug` flag to compile PHP with debugging symbols.
|
||||
go test -race -v ./...
|
||||
|
||||
## Testing in live
|
||||
### With Docker (Linux)
|
||||
|
||||
Prepare a dev Docker image:
|
||||
|
||||
docker build -t frankenphp .
|
||||
docker run -p 8080:8080 -p 443:443 -v $PWD:/go/src/app -it frankenphp bash
|
||||
|
||||
#### Caddy module
|
||||
|
||||
Build Caddy with the FrankenPHP Caddy module:
|
||||
|
||||
cd /go/src/app/caddy/frankenphp/
|
||||
go build
|
||||
|
||||
Run the Caddy with the FrankenPHP Caddy module:
|
||||
|
||||
cd /go/src/app/testdata/
|
||||
../caddy/frankenphp/frankenphp run
|
||||
|
||||
#### Minimal test server
|
||||
|
||||
Build the minimal test server:
|
||||
|
||||
cd /go/src/app/internal/testserver/
|
||||
go build
|
||||
|
||||
Run the test server:
|
||||
|
||||
cd /go/src/app/testdata/
|
||||
../internal/testserver/testserver
|
||||
|
||||
The server is listening on `127.0.0.1:8080`:
|
||||
|
||||
curl http://127.0.0.1:8080/phpinfo.php
|
||||
|
||||
### Without Docker (Linux and macOS)
|
||||
|
||||
Compile PHP:
|
||||
|
||||
./configure --enable-debug --enable-zts
|
||||
make -j6
|
||||
sudo make install
|
||||
|
||||
Build the minimal test server:
|
||||
|
||||
cd internal/testserver/
|
||||
go build
|
||||
|
||||
Run the test app:
|
||||
|
||||
cd ../../testdata/
|
||||
../internal/testserver/testserver
|
||||
|
||||
70
Dockerfile
70
Dockerfile
@@ -1,38 +1,56 @@
|
||||
FROM golang
|
||||
|
||||
ARG PHP_VERSION=8.1.5
|
||||
ARG LIBICONV_VERSION=1.17
|
||||
ENV PHPIZE_DEPS \
|
||||
autoconf \
|
||||
dpkg-dev \
|
||||
file \
|
||||
g++ \
|
||||
gcc \
|
||||
libc-dev \
|
||||
make \
|
||||
pkg-config \
|
||||
re2c
|
||||
|
||||
# Sury doesn't provide ZTS builds for now
|
||||
#RUN apt-get update && \
|
||||
# apt-get -y --no-install-recommends install apt-transport-https lsb-release&& \
|
||||
# wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg && \
|
||||
# sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' && \
|
||||
# apt-get update && \
|
||||
# apt-get -y --no-install-recommends install php8.1-dev && \
|
||||
# apt-get -y remove apt-transport-https lsb-release && \
|
||||
# apt-get clean all
|
||||
#ENV CGO_CFLAGS="-I /usr/include/php/20200930 -I /usr/include/php/20200930/Zend -I /usr/include/php/20200930/TSRM -I /usr/include/php/20200930/main -I /usr/include/php/20200930/sapi/embed"
|
||||
|
||||
# TODO: check the downloaded package using the provided GPG signatures
|
||||
RUN apt-get update && \
|
||||
apt-get -y --no-install-recommends install libxml2 libxml2-dev sqlite3 libsqlite3-dev && \
|
||||
apt-get clean && \
|
||||
curl -s -o php-${PHP_VERSION}.tar.gz https://www.php.net/distributions/php-${PHP_VERSION}.tar.gz && \
|
||||
tar -xf php-${PHP_VERSION}.tar.gz && \
|
||||
cd php-${PHP_VERSION}/ && \
|
||||
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 \
|
||||
# Dev tools \
|
||||
git \
|
||||
gdb \
|
||||
valgrind \
|
||||
neovim && \
|
||||
echo 'set auto-load safe-path /' > /root/.gdbinit && \
|
||||
echo '* soft core unlimited' >> /etc/security/limits.conf \
|
||||
&& \
|
||||
apt-get clean
|
||||
|
||||
RUN git clone https://github.com/dunglas/php-src.git && \
|
||||
cd php-src && \
|
||||
git checkout frankenphp-8.2 && \
|
||||
# --enable-embed is only necessary to generate libphp.so, we don't use this SAPI directly
|
||||
./configure --enable-zts --enable-embed --enable-debug && \
|
||||
make && \
|
||||
./buildconf && \
|
||||
./configure --enable-embed=static --enable-zts --disable-zend-signals --enable-static --enable-debug && \
|
||||
make -j6 && \
|
||||
make install && \
|
||||
rm -Rf php-${PHP_VERSION}/ php-${PHP_VERSION}.tar.gz
|
||||
ENV LD_LIBRARY_PATH=/usr/local/lib/
|
||||
#rm -Rf php-src/ && \
|
||||
ldconfig && \
|
||||
php --version
|
||||
|
||||
RUN echo "zend_extension=opcache.so\nopcache.enable=1" > /usr/local/lib/php.ini
|
||||
|
||||
WORKDIR /go/src/app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go get -d -v ./...
|
||||
RUN go build -v
|
||||
#RUN cd cmd/frankenphp && go install -v ./...
|
||||
|
||||
#CMD ["frankenphp"]
|
||||
|
||||
58
README.md
58
README.md
@@ -15,45 +15,69 @@ docker build -t frankenphp .
|
||||
|
||||
#### Install PHP
|
||||
|
||||
Most distributions don't provide packages containing ZTS builds of PHP.
|
||||
Because the Go HTTP server uses goroutines, a ZTS build is needed.
|
||||
To use FrankenPHP, you currently need to compile a fork of PHP.
|
||||
Patches have been contributed upstream, and some have already
|
||||
been merged. It will be possible to use the vanilla version of PHP
|
||||
starting with version 8.3.
|
||||
|
||||
Start by [downloading the latest version of PHP](https://www.php.net/downloads.php),
|
||||
then follow the instructions according to your operating system.
|
||||
First, get our PHP fork and prepare it:
|
||||
|
||||
##### Linux
|
||||
```
|
||||
git clone https://github.com/dunglas/php-src.git
|
||||
cd php-src
|
||||
git checkout frankenphp-8.2
|
||||
./buildconf
|
||||
```
|
||||
|
||||
Then, configure PHP for your platform:
|
||||
|
||||
**Linux**:
|
||||
|
||||
```
|
||||
./configure \
|
||||
--enable-embed=static \
|
||||
--enable-zts
|
||||
make -j6
|
||||
make install
|
||||
--enable-embed \
|
||||
--enable-zts \
|
||||
--disable-zend-signals
|
||||
```
|
||||
|
||||
##### Mac
|
||||
**Mac**:
|
||||
|
||||
The instructions to build on Mac and Linux are similar.
|
||||
However, on Mac, you have to use the [Homebrew](https://brew.sh/) package manager to install `libiconv` and `bison`.
|
||||
You also need to slightly tweak the configuration.
|
||||
Use the [Homebrew](https://brew.sh/) package manager to install
|
||||
`libiconv` and `bison`:
|
||||
|
||||
```
|
||||
brew install libiconv bison
|
||||
echo 'export PATH="/opt/homebrew/opt/bison/bin:$PATH"' >> ~/.zshrc
|
||||
```
|
||||
|
||||
Then run the configure script:
|
||||
|
||||
```
|
||||
./configure \
|
||||
--enable-embed=static \
|
||||
--enable-zts \
|
||||
--with-iconv=/opt/homebrew/opt/libiconv/ \
|
||||
--without-pcre-jit
|
||||
--disable-zend-signals \
|
||||
--disable-opcache-jit \
|
||||
--with-iconv=/opt/homebrew/opt/libiconv/
|
||||
```
|
||||
|
||||
These flags are required, but you can add other flags (extra extensions...)
|
||||
if needed.
|
||||
|
||||
Finally, compile PHP:
|
||||
|
||||
```
|
||||
make -j6
|
||||
make install
|
||||
```
|
||||
|
||||
#### Compile the Go App
|
||||
|
||||
You can now use the Go lib and compile our Caddy build:
|
||||
|
||||
```
|
||||
go get -d -v ./...
|
||||
go build -v
|
||||
cd caddy/frankenphp
|
||||
go build
|
||||
```
|
||||
|
||||
## Misc Dev Resources
|
||||
|
||||
127
caddy/caddy.go
127
caddy/caddy.go
@@ -4,10 +4,8 @@
|
||||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
@@ -20,15 +18,36 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
frankenphp.Startup()
|
||||
|
||||
caddy.RegisterModule(&FrankenPHPApp{})
|
||||
caddy.RegisterModule(FrankenPHPModule{})
|
||||
httpcaddyfile.RegisterGlobalOption("frankenphp", parseGlobalOption)
|
||||
httpcaddyfile.RegisterHandlerDirective("php", parseCaddyfile)
|
||||
}
|
||||
|
||||
type FrankenPHPApp struct{}
|
||||
type mainPHPinterpreterKeyType int
|
||||
|
||||
var mainPHPInterpreterKey mainPHPinterpreterKeyType
|
||||
|
||||
var phpInterpreter = caddy.NewUsagePool()
|
||||
|
||||
type phpInterpreterDestructor struct{}
|
||||
|
||||
func (phpInterpreterDestructor) Destruct() error {
|
||||
log.Print("Destructor called")
|
||||
frankenphp.Shutdown()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type workerConfig struct {
|
||||
FileName string `json:"file_name,omitempty"`
|
||||
Num int `json:"num,omitempty"`
|
||||
}
|
||||
|
||||
type FrankenPHPApp struct {
|
||||
NumThreads int `json:"num_threads,omitempty"`
|
||||
Workers []workerConfig `json:"workers,omitempty"`
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (a *FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
|
||||
@@ -38,30 +57,96 @@ func (a *FrankenPHPApp) CaddyModule() caddy.ModuleInfo {
|
||||
}
|
||||
}
|
||||
|
||||
func getGID() uint64 {
|
||||
b := make([]byte, 64)
|
||||
b = b[:runtime.Stack(b, false)]
|
||||
b = bytes.TrimPrefix(b, []byte("goroutine "))
|
||||
b = b[:bytes.IndexByte(b, ' ')]
|
||||
n, _ := strconv.ParseUint(string(b), 10, 64)
|
||||
return n
|
||||
}
|
||||
func (f *FrankenPHPApp) Start() error {
|
||||
var opts []frankenphp.Option
|
||||
if f.NumThreads != 0 {
|
||||
opts = append(opts, frankenphp.WithNumThreads(f.NumThreads))
|
||||
}
|
||||
|
||||
func (*FrankenPHPApp) Start() error {
|
||||
log.Printf("started! %d", getGID())
|
||||
return frankenphp.Startup()
|
||||
for _, w := range f.Workers {
|
||||
num := 1
|
||||
if w.Num > 1 {
|
||||
num = w.Num
|
||||
}
|
||||
|
||||
opts = append(opts, frankenphp.WithWorkers(w.FileName, num))
|
||||
}
|
||||
|
||||
_, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) {
|
||||
if err := frankenphp.Init(opts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return phpInterpreterDestructor{}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if loaded {
|
||||
frankenphp.Shutdown()
|
||||
if err := frankenphp.Init(opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Print("FrankenPHP started")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*FrankenPHPApp) Stop() error {
|
||||
log.Printf("stoped!")
|
||||
//frankenphp.Shutdown()
|
||||
log.Print("FrankenPHP stopped")
|
||||
|
||||
frankenphp.Shutdown()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
for d.Next() {
|
||||
for d.NextBlock(0) {
|
||||
switch d.Val() {
|
||||
case "num_threads":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f.NumThreads = v
|
||||
|
||||
case "worker":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
wc := workerConfig{FileName: d.Val()}
|
||||
if d.NextArg() {
|
||||
v, err := strconv.Atoi(d.Val())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wc.Num = v
|
||||
}
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, error) {
|
||||
app := &FrankenPHPApp{}
|
||||
if err := app.UnmarshalCaddyfile(d); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// tell Caddyfile adapter that this is the JSON for an app
|
||||
return httpcaddyfile.App{
|
||||
@@ -117,11 +202,7 @@ func (f FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, next
|
||||
fc.Env[k] = repl.ReplaceKnown(v, "")
|
||||
}
|
||||
|
||||
if err := frankenphp.ExecuteScript(w, fr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
return frankenphp.ServeHTTP(w, fr)
|
||||
}
|
||||
|
||||
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
|
||||
|
||||
@@ -11,37 +11,17 @@ import (
|
||||
|
||||
func TestPHP(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
caddytest.Default.AdminPort = 2019
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
|
||||
#frankenphp
|
||||
admin localhost:2999
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
}
|
||||
|
||||
localhost:9080 {
|
||||
route {
|
||||
root * {env.PWD}/../testdata
|
||||
# Add trailing slash for directory requests
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
}
|
||||
redir @canonicalPath {path}/ 308
|
||||
|
||||
# If the requested file does not exist, try index files
|
||||
@indexFiles file {
|
||||
try_files {path} {path}/index.php index.php
|
||||
split_path .php
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
|
||||
# Handle PHP files with FrankenPHP
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
|
||||
respond 404
|
||||
}
|
||||
respond "Hello"
|
||||
}
|
||||
`, "caddyfile")
|
||||
|
||||
@@ -54,3 +34,17 @@ func TestPHP(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestAutoHTTPtoHTTPSRedirectsImplicitPort(t *testing.T) {
|
||||
tester := caddytest.NewTester(t)
|
||||
tester.InitServer(`
|
||||
{
|
||||
http_port 9080
|
||||
https_port 9443
|
||||
}
|
||||
localhost
|
||||
respond "Yahaha! You found me!"
|
||||
`, "caddyfile")
|
||||
|
||||
tester.AssertRedirect("http://localhost:9080/", "https://localhost/", http.StatusPermanentRedirect)
|
||||
}
|
||||
|
||||
152
caddy/go.mod
152
caddy/go.mod
@@ -1,138 +1,144 @@
|
||||
module github.com/dunglas/frankenphp/caddy
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
replace github.com/dunglas/frankenphp => ../
|
||||
|
||||
replace github.com/caddyserver/caddy/v2 => ../../caddy
|
||||
|
||||
require (
|
||||
github.com/caddyserver/caddy/v2 v2.5.1
|
||||
github.com/caddyserver/caddy/v2 v2.6.1
|
||||
github.com/dunglas/frankenphp v0.0.0-00010101000000-000000000000
|
||||
go.uber.org/zap v1.21.0
|
||||
go.uber.org/zap v1.23.0
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/BurntSushi/toml v1.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.2.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
|
||||
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||
github.com/antlr/antlr4 v0.0.0-20200503195918-621b933c7a7f // indirect
|
||||
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
|
||||
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b // indirect
|
||||
github.com/benesch/cgosymbolizer v0.0.0-20190515212042-bec6fe6e597b // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/certmagic v0.16.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.2 // indirect
|
||||
github.com/caddyserver/certmagic v0.17.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/chzyer/readline v1.5.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/dgraph-io/badger v1.6.2 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.0.4-0.20200906165740-41ebdbffecfd // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect
|
||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-kit/kit v0.10.0 // indirect
|
||||
github.com/go-kit/kit v0.12.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-logr/logr v1.2.2 // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/cel-go v0.7.3 // indirect
|
||||
github.com/google/cel-go v0.12.5 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20220405231054-a1ae3e4bba26 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.10.1 // indirect
|
||||
github.com/jackc/pgconn v1.13.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.9.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.14.0 // indirect
|
||||
github.com/klauspost/compress v1.15.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||
github.com/jackc/pgtype v1.12.0 // indirect
|
||||
github.com/jackc/pgx/v4 v4.17.2 // indirect
|
||||
github.com/klauspost/compress v1.15.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.1 // indirect
|
||||
github.com/libdns/libdns v0.2.1 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.27.1 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.29.1 // indirect
|
||||
github.com/manifoldco/promptui v0.9.0 // indirect
|
||||
github.com/marten-seemann/qpack v0.2.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
||||
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/mholt/acmez v1.0.2 // indirect
|
||||
github.com/mholt/acmez v1.0.4 // indirect
|
||||
github.com/micromdm/scep/v2 v2.1.0 // indirect
|
||||
github.com/miekg/dns v1.1.49 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.12.2 // indirect
|
||||
github.com/prometheus/client_golang v1.13.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.34.0 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/rs/xid v1.2.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.0.1 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/slackhq/nebula v1.5.2 // indirect
|
||||
github.com/smallstep/certificates v0.19.0 // indirect
|
||||
github.com/smallstep/cli v0.18.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/slackhq/nebula v1.6.1 // indirect
|
||||
github.com/smallstep/certificates v0.22.1 // indirect
|
||||
github.com/smallstep/cli v0.22.0 // indirect
|
||||
github.com/smallstep/nosql v0.4.0 // indirect
|
||||
github.com/smallstep/truststore v0.11.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/smallstep/truststore v0.12.0 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.5.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stoewer/go-strcase v1.2.0 // indirect
|
||||
github.com/tailscale/tscert v0.0.0-20220316030059-54bbcb9f74e2 // indirect
|
||||
github.com/urfave/cli v1.22.5 // indirect
|
||||
github.com/yuin/goldmark v1.4.12 // indirect
|
||||
github.com/urfave/cli v1.22.10 // indirect
|
||||
github.com/yuin/goldmark v1.5.2 // indirect
|
||||
github.com/yuin/goldmark-highlighting v0.0.0-20220208100518-594be1970594 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.29.0 // indirect
|
||||
go.opentelemetry.io/otel v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/internal/metric v0.27.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.27.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.4.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.4.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.12.0 // indirect
|
||||
go.step.sm/cli-utils v0.7.0 // indirect
|
||||
go.step.sm/crypto v0.16.1 // indirect
|
||||
go.step.sm/linkedca v0.15.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.36.1 // indirect
|
||||
go.opentelemetry.io/otel v1.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.10.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.10.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.32.1 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.10.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.10.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
|
||||
go.step.sm/cli-utils v0.7.5 // indirect
|
||||
go.step.sm/crypto v0.19.0 // indirect
|
||||
go.step.sm/linkedca v0.18.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 // indirect
|
||||
golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e // indirect
|
||||
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect
|
||||
golang.org/x/exp v0.0.0-20220929160808-de9c53c655b9 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.0.0-20220927171203-f486391704dc // indirect
|
||||
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
|
||||
golang.org/x/term v0.0.0-20220919170432-7a66f970e087 // indirect
|
||||
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
|
||||
golang.org/x/tools v0.1.10 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df // indirect
|
||||
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220929141241-1ce7b20da813 // indirect
|
||||
google.golang.org/grpc v1.49.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
730
caddy/go.sum
730
caddy/go.sum
File diff suppressed because it is too large
Load Diff
120
frankenphp.c
120
frankenphp.c
@@ -9,6 +9,8 @@
|
||||
#include <php_variables.h>
|
||||
#include <php_output.h>
|
||||
#include <Zend/zend_alloc.h>
|
||||
#include "C-Thread-Pool/thpool.h"
|
||||
#include "C-Thread-Pool/thpool.c"
|
||||
#include "_cgo_export.h"
|
||||
|
||||
#if defined(PHP_WIN32) && defined(ZTS)
|
||||
@@ -70,7 +72,7 @@ int frankenphp_check_version() {
|
||||
return -1;
|
||||
#endif
|
||||
|
||||
if (PHP_VERSION_ID <= 80100 || PHP_VERSION_ID >= 80200) {
|
||||
if (PHP_VERSION_ID < 80200) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
@@ -78,14 +80,13 @@ int frankenphp_check_version() {
|
||||
}
|
||||
|
||||
typedef struct frankenphp_server_context {
|
||||
uintptr_t request;
|
||||
uintptr_t requests_chan;
|
||||
char *worker_filename;
|
||||
uintptr_t current_request;
|
||||
uintptr_t main_request; // Only available during worker initialization
|
||||
char *cookie_data;
|
||||
} frankenphp_server_context;
|
||||
|
||||
// Adapted from php_request_shutdown
|
||||
void frankenphp_worker_request_shutdown(uintptr_t request) {
|
||||
void frankenphp_worker_request_shutdown(uintptr_t current_request) {
|
||||
/* 0. skipped: Call any open observer end handlers that are still open after a zend_bailout */
|
||||
/* 1. skipped: Call all possible shutdown functions registered with register_shutdown_function() */
|
||||
/* 2. skipped: Call all possible __destruct() functions */
|
||||
@@ -151,7 +152,7 @@ void frankenphp_worker_request_shutdown(uintptr_t request) {
|
||||
sapi_deactivate();
|
||||
} zend_end_try();
|
||||
|
||||
if (request != 0) go_frankenphp_worker_handle_request_end(request);
|
||||
if (current_request != 0) go_frankenphp_worker_handle_request_end(current_request);
|
||||
|
||||
/* 14. Destroy stream hashes */
|
||||
// todo: check if it's a good idea
|
||||
@@ -250,13 +251,19 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
|
||||
if (ctx->request == 0) {
|
||||
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);
|
||||
frankenphp_worker_request_shutdown(0);
|
||||
|
||||
previous_request = ctx->main_request;
|
||||
|
||||
// Mark the worker as ready to handle requests
|
||||
go_frankenphp_worker_ready();
|
||||
}
|
||||
|
||||
uintptr_t request = go_frankenphp_worker_handle_request_start(ctx->requests_chan);
|
||||
if (!request) {
|
||||
uintptr_t next_request = go_frankenphp_worker_handle_request_start(previous_request);
|
||||
if (!next_request) {
|
||||
// Shutting down, re-create a dummy request to make the real php_request_shutdown() function happy
|
||||
frankenphp_worker_request_startup();
|
||||
|
||||
@@ -273,7 +280,7 @@ PHP_FUNCTION(frankenphp_handle_request) {
|
||||
fci.retval = &retval;
|
||||
zend_call_function(&fci, &fcc);
|
||||
|
||||
frankenphp_worker_request_shutdown(request);
|
||||
frankenphp_worker_request_shutdown(next_request);
|
||||
|
||||
RETURN_TRUE;
|
||||
}
|
||||
@@ -321,7 +328,7 @@ uintptr_t frankenphp_clean_server_context() {
|
||||
free(SG(request_info.request_uri));
|
||||
SG(request_info.request_uri) = NULL;
|
||||
|
||||
return ctx->request;
|
||||
return ctx->current_request;
|
||||
}
|
||||
|
||||
uintptr_t frankenphp_request_shutdown()
|
||||
@@ -338,13 +345,14 @@ uintptr_t frankenphp_request_shutdown()
|
||||
free(ctx);
|
||||
SG(server_context) = NULL;
|
||||
|
||||
#if defined(ZTS)
|
||||
ts_free_thread();
|
||||
#endif
|
||||
|
||||
return rh;
|
||||
}
|
||||
|
||||
// set worker to 0 if not in worker mode
|
||||
int frankenphp_create_server_context(uintptr_t requests_chan, char* worker_filename)
|
||||
int frankenphp_create_server_context()
|
||||
{
|
||||
#ifdef ZTS
|
||||
/* initial resource fetch */
|
||||
@@ -358,9 +366,8 @@ int frankenphp_create_server_context(uintptr_t requests_chan, char* worker_filen
|
||||
frankenphp_server_context *ctx = calloc(1, sizeof(frankenphp_server_context));
|
||||
if (ctx == NULL) return FAILURE;
|
||||
|
||||
ctx->request = 0;
|
||||
ctx->requests_chan = requests_chan;
|
||||
ctx->worker_filename = worker_filename;
|
||||
ctx->current_request = 0;
|
||||
ctx->main_request = 0;
|
||||
ctx->cookie_data = NULL;
|
||||
|
||||
SG(server_context) = ctx;
|
||||
@@ -369,7 +376,8 @@ int frankenphp_create_server_context(uintptr_t requests_chan, char* worker_filen
|
||||
}
|
||||
|
||||
void frankenphp_update_server_context(
|
||||
uintptr_t request,
|
||||
uintptr_t current_request,
|
||||
uintptr_t main_request,
|
||||
|
||||
const char *request_method,
|
||||
char *query_string,
|
||||
@@ -381,7 +389,8 @@ void frankenphp_update_server_context(
|
||||
char *auth_password,
|
||||
int proto_num
|
||||
) {
|
||||
((frankenphp_server_context*) SG(server_context))->request = request;
|
||||
((frankenphp_server_context*) SG(server_context))->main_request = main_request;
|
||||
((frankenphp_server_context*) SG(server_context))->current_request = current_request;
|
||||
|
||||
SG(request_info).auth_password = auth_password;
|
||||
SG(request_info).auth_user = auth_user;
|
||||
@@ -396,7 +405,7 @@ void frankenphp_update_server_context(
|
||||
|
||||
static int frankenphp_startup(sapi_module_struct *sapi_module)
|
||||
{
|
||||
return php_module_startup(sapi_module, &frankenphp_module, 1);
|
||||
return php_module_startup(sapi_module, &frankenphp_module);
|
||||
}
|
||||
|
||||
static int frankenphp_deactivate(void)
|
||||
@@ -409,9 +418,7 @@ static size_t frankenphp_ub_write(const char *str, size_t str_length)
|
||||
{
|
||||
frankenphp_server_context* ctx = SG(server_context);
|
||||
|
||||
if (ctx->request == 0) return 0; // TODO: write on stdout?
|
||||
|
||||
return go_ub_write(ctx->request, (char *) str, str_length);
|
||||
return go_ub_write(ctx->current_request ? ctx->current_request : ctx->main_request, (char *) str, str_length);
|
||||
}
|
||||
|
||||
static int frankenphp_send_headers(sapi_headers_struct *sapi_headers)
|
||||
@@ -425,11 +432,11 @@ static int frankenphp_send_headers(sapi_headers_struct *sapi_headers)
|
||||
int status;
|
||||
frankenphp_server_context* ctx = SG(server_context);
|
||||
|
||||
if (ctx->request == 0) return SAPI_HEADER_SEND_FAILED;
|
||||
if (ctx->current_request == 0) return SAPI_HEADER_SEND_FAILED;
|
||||
|
||||
h = zend_llist_get_first_ex(&sapi_headers->headers, &pos);
|
||||
while (h) {
|
||||
go_add_header(ctx->request, h->header, h->header_len);
|
||||
go_add_header(ctx->current_request, h->header, h->header_len);
|
||||
h = zend_llist_get_next_ex(&sapi_headers->headers, &pos);
|
||||
}
|
||||
|
||||
@@ -440,7 +447,7 @@ static int frankenphp_send_headers(sapi_headers_struct *sapi_headers)
|
||||
status = atoi((SG(sapi_headers).http_status_line) + 9);
|
||||
}
|
||||
|
||||
go_write_header(ctx->request, status);
|
||||
go_write_header(ctx->current_request, status);
|
||||
|
||||
return SAPI_HEADER_SENT_SUCCESSFULLY;
|
||||
}
|
||||
@@ -449,18 +456,18 @@ static size_t frankenphp_read_post(char *buffer, size_t count_bytes)
|
||||
{
|
||||
frankenphp_server_context* ctx = SG(server_context);
|
||||
|
||||
if (ctx->request == 0) return 0;
|
||||
if (ctx->current_request == 0) return 0;
|
||||
|
||||
return go_read_post(ctx->request, buffer, count_bytes);
|
||||
return go_read_post(ctx->current_request, buffer, count_bytes);
|
||||
}
|
||||
|
||||
static char* frankenphp_read_cookies(void)
|
||||
{
|
||||
frankenphp_server_context* ctx = SG(server_context);
|
||||
|
||||
if (ctx->request == 0) return "";
|
||||
if (ctx->current_request == 0) return "";
|
||||
|
||||
ctx->cookie_data = go_read_cookies(ctx->request);
|
||||
ctx->cookie_data = go_read_cookies(ctx->current_request);
|
||||
|
||||
return ctx->cookie_data;
|
||||
}
|
||||
@@ -470,15 +477,16 @@ static void frankenphp_register_variables(zval *track_vars_array)
|
||||
// https://www.php.net/manual/en/reserved.variables.server.php
|
||||
frankenphp_server_context* ctx = SG(server_context);
|
||||
|
||||
if (ctx->request == 0 && ctx->worker_filename != NULL) {
|
||||
// todo: remove this
|
||||
/*if (ctx->current_request == 0 && ctx->worker_filename != NULL) {
|
||||
// todo: also register PHP_SELF etc
|
||||
php_register_variable_safe("SCRIPT_FILENAME", ctx->worker_filename, strlen(ctx->worker_filename), track_vars_array);
|
||||
}
|
||||
}*/
|
||||
|
||||
// todo: import or not environment variables set in the parent process?
|
||||
//php_import_environment_variables(track_vars_array);
|
||||
|
||||
go_register_variables(ctx->request, track_vars_array);
|
||||
go_register_variables(ctx->current_request ? ctx->current_request : ctx->main_request, track_vars_array);
|
||||
}
|
||||
|
||||
static void frankenphp_log_message(const char *message, int syslog_type_int)
|
||||
@@ -518,28 +526,59 @@ sapi_module_struct frankenphp_sapi_module = {
|
||||
STANDARD_SAPI_MODULE_PROPERTIES
|
||||
};
|
||||
|
||||
int frankenphp_init() {
|
||||
void *manager_thread(void *arg) {
|
||||
#ifdef ZTS
|
||||
php_tsrm_startup();
|
||||
//tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);
|
||||
# ifdef PHP_WIN32
|
||||
ZEND_TSRMLS_CACHE_UPDATE();
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef ZEND_SIGNALS
|
||||
zend_signal_startup();
|
||||
#endif
|
||||
sapi_startup(&frankenphp_sapi_module);
|
||||
|
||||
return frankenphp_sapi_module.startup(&frankenphp_sapi_module);
|
||||
}
|
||||
frankenphp_sapi_module.startup(&frankenphp_sapi_module);
|
||||
|
||||
threadpool thpool = thpool_init(*((int *) arg));
|
||||
free(arg);
|
||||
|
||||
uintptr_t rh;
|
||||
while ((rh = go_fetch_request())) {
|
||||
thpool_add_work(thpool, go_execute_script, (void *) rh);
|
||||
}
|
||||
|
||||
// channel closed, shutdown gracefully
|
||||
thpool_wait(thpool);
|
||||
thpool_destroy(thpool);
|
||||
|
||||
void frankenphp_shutdown()
|
||||
{
|
||||
frankenphp_sapi_module.shutdown(&frankenphp_sapi_module);
|
||||
|
||||
sapi_shutdown();
|
||||
#ifdef ZTS
|
||||
tsrm_shutdown();
|
||||
#endif
|
||||
|
||||
go_shutdown();
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int frankenphp_init(int num_threads) {
|
||||
pthread_t thread;
|
||||
|
||||
int *num_threads_ptr = calloc(1, sizeof(int));
|
||||
*num_threads_ptr = num_threads;
|
||||
|
||||
if (pthread_create(&thread, NULL, *manager_thread, (void *) num_threads_ptr) != 0) {
|
||||
go_shutdown();
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
return pthread_detach(thread);
|
||||
}
|
||||
|
||||
int frankenphp_request_startup()
|
||||
@@ -548,8 +587,6 @@ int frankenphp_request_startup()
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
fprintf(stderr, "problem in php_request_startup\n");
|
||||
|
||||
frankenphp_server_context *ctx = SG(server_context);
|
||||
SG(server_context) = NULL;
|
||||
free(ctx);
|
||||
@@ -565,6 +602,7 @@ int frankenphp_execute_script(const char* file_name)
|
||||
|
||||
zend_file_handle file_handle;
|
||||
zend_stream_init_filename(&file_handle, file_name);
|
||||
file_handle.primary_script = 1;
|
||||
|
||||
zend_first_try {
|
||||
status = php_execute_script(&file_handle);
|
||||
@@ -572,5 +610,7 @@ int frankenphp_execute_script(const char* file_name)
|
||||
/* int exit_status = EG(exit_status); */
|
||||
} zend_end_try();
|
||||
|
||||
zend_destroy_file_handle(&file_handle);
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
207
frankenphp.go
207
frankenphp.go
@@ -1,8 +1,9 @@
|
||||
package frankenphp
|
||||
|
||||
// #cgo CFLAGS: -Wall -Wno-unused-variable
|
||||
// #cgo CFLAGS: -Wall
|
||||
// #cgo CFLAGS: -I/usr/local/include/php -I/usr/local/include/php/Zend -I/usr/local/include/php/TSRM -I/usr/local/include/php/main
|
||||
// #cgo LDFLAGS: -L/usr/local/lib -L/opt/homebrew/opt/libiconv/lib -L/usr/lib -lphp -lxml2 -liconv -lresolv -lsqlite3
|
||||
// #cgo LDFLAGS: -L/usr/local/lib -L/opt/homebrew/opt/libiconv/lib -L/usr/lib -lphp -lxml2 -lresolv -lsqlite3 -ldl -lm -lutil
|
||||
// #cgo darwin LDFLAGS: -liconv
|
||||
// #include <stdlib.h>
|
||||
// #include <stdint.h>
|
||||
// #include <php_variables.h>
|
||||
@@ -19,8 +20,9 @@ import (
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unsafe"
|
||||
// debug
|
||||
// debug on Linux
|
||||
//_ "github.com/ianlancetaylor/cgosymbolizer"
|
||||
)
|
||||
|
||||
@@ -28,10 +30,20 @@ type key int
|
||||
|
||||
var contextKey key
|
||||
|
||||
func init() {
|
||||
// Make sure the main goroutine is bound to the main thread.
|
||||
runtime.LockOSThread()
|
||||
var (
|
||||
InvalidRequestError = errors.New("not a FrankenPHP request")
|
||||
AlreaydStartedError = errors.New("FrankenPHP is already started")
|
||||
InvalidPHPVersionError = errors.New("FrankenPHP is only compatible with PHP 8.2+")
|
||||
MainThreadCreationError = errors.New("error creating the main thread")
|
||||
RequestContextCreationError = errors.New("error during request context creation")
|
||||
RequestStartupError = errors.New("error during PHP request startup")
|
||||
ScriptExecutionError = errors.New("error during PHP script execution")
|
||||
|
||||
requestChan chan *http.Request
|
||||
shutdownWG sync.WaitGroup
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile) // TODO: switch to Zap
|
||||
}
|
||||
|
||||
@@ -84,43 +96,67 @@ func FromContext(ctx context.Context) (fctx *FrankenPHPContext, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
// Init initializes the PHP engine.
|
||||
// Init and Shutdown must be called in the main function.
|
||||
func Init() error {
|
||||
switch C.frankenphp_check_version() {
|
||||
case -1:
|
||||
return errors.New(`ZTS is not enabled, recompile PHP using the "--enable-zts" configuration option`)
|
||||
|
||||
case -2:
|
||||
return errors.New(`FrankenPHP is only compatible with PHP 8.1`)
|
||||
func Init(options ...Option) error {
|
||||
if requestChan != nil {
|
||||
return AlreaydStartedError
|
||||
}
|
||||
|
||||
if C.frankenphp_init() < 0 {
|
||||
return errors.New("error initializing PHP")
|
||||
opt := &opt{numThreads: runtime.NumCPU()}
|
||||
for _, o := range options {
|
||||
if err := o(opt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opt.numThreads == 0 {
|
||||
opt.numThreads = 1
|
||||
}
|
||||
|
||||
switch C.frankenphp_check_version() {
|
||||
case -1:
|
||||
if opt.numThreads != 1 {
|
||||
opt.numThreads = 1
|
||||
log.Print(`ZTS is not enabled, only 1 thread will be available, recompile PHP using the "--enable-zts" configuration option or performance will be degraded`)
|
||||
}
|
||||
|
||||
case -2:
|
||||
return InvalidPHPVersionError
|
||||
}
|
||||
|
||||
shutdownWG.Add(1)
|
||||
requestChan = make(chan *http.Request)
|
||||
|
||||
if C.frankenphp_init(C.int(opt.numThreads)) != 0 {
|
||||
return MainThreadCreationError
|
||||
}
|
||||
|
||||
for _, w := range opt.workers {
|
||||
// TODO: start all the worker in parallell to reduce the boot time
|
||||
if err := startWorkers(w.fileName, w.num); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown stops the PHP engine.
|
||||
// Init and Shutdown must be called in the main function.
|
||||
func Shutdown() {
|
||||
//if atomic.LoadInt32(&started) < 1 {
|
||||
// return
|
||||
//}
|
||||
//atomic.StoreInt32(&started, 0)
|
||||
log.Printf("Shutdown called ")
|
||||
stopWorkers()
|
||||
close(requestChan)
|
||||
shutdownWG.Wait()
|
||||
requestChan = nil
|
||||
}
|
||||
|
||||
C.frankenphp_shutdown()
|
||||
//export go_shutdown
|
||||
func go_shutdown() {
|
||||
shutdownWG.Done()
|
||||
}
|
||||
|
||||
func updateServerContext(request *http.Request) error {
|
||||
if err := populateEnv(request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fc, ok := FromContext(request.Context())
|
||||
if !ok {
|
||||
panic("not a FrankenPHP request")
|
||||
return InvalidRequestError
|
||||
}
|
||||
|
||||
var cAuthUser, cAuthPassword *C.char
|
||||
@@ -132,14 +168,16 @@ func updateServerContext(request *http.Request) error {
|
||||
cAuthUser = C.CString(authUser)
|
||||
}
|
||||
|
||||
rh := cgo.NewHandle(request)
|
||||
|
||||
cMethod := C.CString(request.Method)
|
||||
cQueryString := C.CString(request.URL.RawQuery)
|
||||
contentLengthStr := request.Header.Get("Content-Length")
|
||||
contentLength := 0
|
||||
if contentLengthStr != "" {
|
||||
contentLength, _ = strconv.Atoi(contentLengthStr)
|
||||
var err error
|
||||
contentLength, err = strconv.Atoi(contentLengthStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid Content-Length header: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
contentType := request.Header.Get("Content-Type")
|
||||
@@ -155,8 +193,16 @@ func updateServerContext(request *http.Request) error {
|
||||
|
||||
cRequestUri := C.CString(request.URL.RequestURI())
|
||||
|
||||
var rh, mwrh cgo.Handle
|
||||
if fc.responseWriter == nil {
|
||||
mwrh = cgo.NewHandle(request)
|
||||
} else {
|
||||
rh = cgo.NewHandle(request)
|
||||
}
|
||||
|
||||
C.frankenphp_update_server_context(
|
||||
C.uintptr_t(rh),
|
||||
C.uintptr_t(mwrh),
|
||||
|
||||
cMethod,
|
||||
cQueryString,
|
||||
@@ -172,39 +218,80 @@ func updateServerContext(request *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExecuteScript(responseWriter http.ResponseWriter, request *http.Request) error {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
// todo: check if it's ok or not to call runtime.UnlockOSThread() to reuse this thread
|
||||
func ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) error {
|
||||
shutdownWG.Add(1)
|
||||
defer shutdownWG.Done()
|
||||
|
||||
if C.frankenphp_create_server_context(0, nil) < 0 {
|
||||
return fmt.Errorf("error during request context creation")
|
||||
fc, ok := FromContext(request.Context())
|
||||
if !ok {
|
||||
return InvalidRequestError
|
||||
}
|
||||
|
||||
if err := updateServerContext(request); err != nil {
|
||||
if err := populateEnv(request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if C.frankenphp_request_startup() < 0 {
|
||||
return fmt.Errorf("error during PHP request startup")
|
||||
fc.responseWriter = responseWriter
|
||||
fc.done = make(chan interface{})
|
||||
|
||||
rc := requestChan
|
||||
// Detect if a worker is available to handle this request
|
||||
if nil == fc.responseWriter {
|
||||
fc.Env["FRANKENPHP_WORKER"] = "1"
|
||||
} else if v, ok := workersRequestChans.Load(fc.Env["SCRIPT_FILENAME"]); ok {
|
||||
fc.Env["FRANKENPHP_WORKER"] = "1"
|
||||
rc = v.(chan *http.Request)
|
||||
}
|
||||
|
||||
fc := request.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
fc.responseWriter = responseWriter
|
||||
rc <- request
|
||||
<-fc.done
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//export go_fetch_request
|
||||
func go_fetch_request() C.uintptr_t {
|
||||
r, ok := <-requestChan
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
return C.uintptr_t(cgo.NewHandle(r))
|
||||
}
|
||||
|
||||
//export go_execute_script
|
||||
func go_execute_script(rh unsafe.Pointer) {
|
||||
handle := cgo.Handle(rh)
|
||||
defer handle.Delete()
|
||||
|
||||
request := handle.Value().(*http.Request)
|
||||
fc, ok := FromContext(request.Context())
|
||||
if !ok {
|
||||
panic(InvalidRequestError)
|
||||
}
|
||||
defer close(fc.done)
|
||||
|
||||
if C.frankenphp_create_server_context() < 0 {
|
||||
panic(RequestContextCreationError)
|
||||
}
|
||||
|
||||
if err := updateServerContext(request); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if C.frankenphp_request_startup() < 0 {
|
||||
panic(RequestStartupError)
|
||||
}
|
||||
|
||||
cFileName := C.CString(fc.Env["SCRIPT_FILENAME"])
|
||||
defer C.free(unsafe.Pointer(cFileName))
|
||||
|
||||
if C.frankenphp_execute_script(cFileName) < 0 {
|
||||
return fmt.Errorf("error during PHP script execution")
|
||||
panic(ScriptExecutionError)
|
||||
}
|
||||
|
||||
rh := C.frankenphp_clean_server_context()
|
||||
C.frankenphp_clean_server_context()
|
||||
C.frankenphp_request_shutdown()
|
||||
|
||||
cgo.Handle(rh).Delete()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//export go_ub_write
|
||||
@@ -212,7 +299,15 @@ func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) C.size_t {
|
||||
r := cgo.Handle(rh).Value().(*http.Request)
|
||||
fc, _ := FromContext(r.Context())
|
||||
|
||||
i, _ := fc.responseWriter.Write([]byte(C.GoStringN(cString, length)))
|
||||
var writer io.Writer
|
||||
if fc.responseWriter == nil {
|
||||
// log the output of the
|
||||
writer = log.Writer()
|
||||
} else {
|
||||
writer = fc.responseWriter
|
||||
}
|
||||
|
||||
i, _ := writer.Write([]byte(C.GoStringN(cString, length)))
|
||||
|
||||
return C.size_t(i)
|
||||
}
|
||||
@@ -220,15 +315,11 @@ func go_ub_write(rh C.uintptr_t, cString *C.char, length C.int) C.size_t {
|
||||
//export go_register_variables
|
||||
func go_register_variables(rh C.uintptr_t, trackVarsArray *C.zval) {
|
||||
var env map[string]string
|
||||
if rh == 0 {
|
||||
// Worker mode, waiting for a request, initialize some useful variables
|
||||
env = map[string]string{"FRANKENPHP_WORKER": "1"}
|
||||
} else {
|
||||
r := cgo.Handle(rh).Value().(*http.Request)
|
||||
env = r.Context().Value(contextKey).(*FrankenPHPContext).Env
|
||||
}
|
||||
r := cgo.Handle(rh).Value().(*http.Request)
|
||||
env = r.Context().Value(contextKey).(*FrankenPHPContext).Env
|
||||
|
||||
env[fmt.Sprintf("REQUEST_%d", rh)] = "on"
|
||||
// FIXME: remove this debug statement
|
||||
env[fmt.Sprintf("REQUEST_%d", rh)] = "1"
|
||||
|
||||
for k, v := range env {
|
||||
ck := C.CString(k)
|
||||
|
||||
@@ -4,13 +4,12 @@
|
||||
#include <stdint.h>
|
||||
|
||||
int frankenphp_check_version();
|
||||
int frankenphp_init(int num_threads);
|
||||
|
||||
int frankenphp_init();
|
||||
void frankenphp_shutdown();
|
||||
|
||||
int frankenphp_create_server_context(uintptr_t requests_chan, char *worker_filename);
|
||||
int frankenphp_create_server_context();
|
||||
void frankenphp_update_server_context(
|
||||
uintptr_t request,
|
||||
uintptr_t current_request,
|
||||
uintptr_t main_request,
|
||||
|
||||
const char *request_method,
|
||||
char *query_string,
|
||||
|
||||
@@ -9,13 +9,13 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testOptions struct {
|
||||
@@ -25,51 +25,34 @@ type testOptions struct {
|
||||
realServer bool
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
runtime.LockOSThread()
|
||||
if err := frankenphp.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
code := m.Run()
|
||||
|
||||
frankenphp.Shutdown()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *httptest.Server, int), opts *testOptions) {
|
||||
if opts == nil {
|
||||
opts = &testOptions{}
|
||||
}
|
||||
if opts.workerScript != "" {
|
||||
t.SkipNow()
|
||||
}
|
||||
if opts.nbWorkers == 0 {
|
||||
opts.nbWorkers = 2
|
||||
}
|
||||
if opts.nbParrallelRequests == 0 {
|
||||
opts.nbParrallelRequests = 10
|
||||
opts.nbParrallelRequests = 100
|
||||
}
|
||||
|
||||
cwd, _ := os.Getwd()
|
||||
testDataDir := cwd + "/testdata/"
|
||||
|
||||
initOpts := make([]frankenphp.Option, 0, 1)
|
||||
if opts.workerScript != "" {
|
||||
frankenphp.StartWorkers(testDataDir+opts.workerScript, opts.nbWorkers)
|
||||
defer frankenphp.StopWorkers()
|
||||
initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers))
|
||||
}
|
||||
|
||||
err := frankenphp.Init(initOpts...)
|
||||
require.Nil(t, err)
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
handler := func(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
|
||||
req := frankenphp.NewRequestWithContext(r, testDataDir)
|
||||
if opts.workerScript == "" {
|
||||
err = frankenphp.ExecuteScript(w, req)
|
||||
} else {
|
||||
err = frankenphp.WorkerHandleRequest(w, req)
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
var ts *httptest.Server
|
||||
@@ -100,7 +83,6 @@ func testHelloWorld(t *testing.T, opts *testOptions) {
|
||||
|
||||
resp := w.Result()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
|
||||
assert.Equal(t, fmt.Sprintf("I am by birth a Genevese (%d)", i), string(body))
|
||||
}, opts)
|
||||
}
|
||||
@@ -159,16 +141,13 @@ func testPathInfo(t *testing.T, opts *testOptions) {
|
||||
cwd, _ := os.Getwd()
|
||||
testDataDir := cwd + "/testdata/"
|
||||
|
||||
requestURI := r.URL.RequestURI()
|
||||
rewriteRequest := frankenphp.NewRequestWithContext(r, testDataDir)
|
||||
rewriteRequest.URL.Path = "/server-variable.php/pathinfo"
|
||||
fc, _ := frankenphp.FromContext(rewriteRequest.Context())
|
||||
fc.Env["REQUEST_URI"] = r.URL.RequestURI()
|
||||
fc.Env["REQUEST_URI"] = requestURI
|
||||
|
||||
if opts == nil {
|
||||
assert.Nil(t, frankenphp.ExecuteScript(w, rewriteRequest))
|
||||
} else {
|
||||
assert.Nil(t, frankenphp.WorkerHandleRequest(w, rewriteRequest))
|
||||
}
|
||||
frankenphp.ServeHTTP(w, rewriteRequest)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest("GET", fmt.Sprintf("http://example.com/pathinfo/%d", i), nil)
|
||||
@@ -316,15 +295,16 @@ func testPhpInfo(t *testing.T, opts *testOptions) {
|
||||
}
|
||||
|
||||
func ExampleExecuteScript() {
|
||||
frankenphp.Init()
|
||||
if err := frankenphp.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
phpHandler := func(w http.ResponseWriter, req *http.Request) {
|
||||
if err := frankenphp.ExecuteScript(w, req); err != nil {
|
||||
log.Print(fmt.Errorf("error executing PHP script: %w", err))
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
req := frankenphp.NewRequestWithContext(r, "/path/to/document/root")
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
http.HandleFunc("/", phpHandler)
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
7
go.mod
7
go.mod
@@ -1,8 +1,11 @@
|
||||
module github.com/dunglas/frankenphp
|
||||
|
||||
go 1.18
|
||||
go 1.19
|
||||
|
||||
require github.com/stretchr/testify v1.7.1
|
||||
require (
|
||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20220405231054-a1ae3e4bba26
|
||||
github.com/stretchr/testify v1.7.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -2,6 +2,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20220405231054-a1ae3e4bba26 h1:UT3hQ6+5hwqUT83cKhKlY5I0W/kqsl6lpn3iFb3Gtqs=
|
||||
github.com/ianlancetaylor/cgosymbolizer v0.0.0-20220405231054-a1ae3e4bba26/go.mod h1:DvXTE/K/RtHehxU8/GtDs4vFtfw64jJ3PaCnFri8CRg=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
|
||||
35
internal/testserver/main.go
Normal file
35
internal/testserver/main.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/dunglas/frankenphp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := frankenphp.Init(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
req := frankenphp.NewRequestWithContext(r, cwd)
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
}
|
||||
38
options.go
Normal file
38
options.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package frankenphp
|
||||
|
||||
import "runtime"
|
||||
|
||||
// Option instances allow to configure the SAP.
|
||||
type Option func(h *opt) error
|
||||
|
||||
// opt contains the available options.
|
||||
//
|
||||
// If you change this, also update the Caddy module and the documentation.
|
||||
type opt struct {
|
||||
numThreads int
|
||||
workers []workerOpt
|
||||
}
|
||||
|
||||
type workerOpt struct {
|
||||
fileName string
|
||||
num int
|
||||
}
|
||||
|
||||
// WithNumThreads allows to configure the number of PHP threads to start (worker threads excluded).
|
||||
func WithNumThreads(numThreads int) Option {
|
||||
return func(o *opt) error {
|
||||
o.numThreads += -runtime.NumCPU() + numThreads
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithWorkers allow to start worker goroutines.
|
||||
func WithWorkers(fileName string, num int) Option {
|
||||
return func(o *opt) error {
|
||||
o.workers = append(o.workers, workerOpt{fileName, num})
|
||||
o.numThreads += num
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
33
testdata/Caddyfile
vendored
Normal file
33
testdata/Caddyfile
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
frankenphp {
|
||||
worker /Users/dunglas/workspace/frankenphp/testdata/index.php
|
||||
}
|
||||
}
|
||||
|
||||
localhost {
|
||||
log
|
||||
route {
|
||||
root * .
|
||||
# Add trailing slash for directory requests
|
||||
@canonicalPath {
|
||||
file {path}/index.php
|
||||
not path */
|
||||
}
|
||||
redir @canonicalPath {path}/ 308
|
||||
|
||||
# If the requested file does not exist, try index files
|
||||
@indexFiles file {
|
||||
try_files {path} {path}/index.php index.php
|
||||
split_path .php
|
||||
}
|
||||
rewrite @indexFiles {http.matchers.file.relative}
|
||||
|
||||
# FrankenPHP!
|
||||
@phpFiles path *.php
|
||||
php @phpFiles
|
||||
encode zstd gzip
|
||||
file_server
|
||||
|
||||
respond 404
|
||||
}
|
||||
}
|
||||
160
worker.go
160
worker.go
@@ -4,119 +4,119 @@ package frankenphp
|
||||
// #include "frankenphp.h"
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var requestsChans sync.Map // map[fileName]cgo.NewHandle(chan *http.Request)
|
||||
var workersWaitGroup sync.WaitGroup
|
||||
var (
|
||||
workersRequestChans sync.Map // map[fileName]chan *http.Request
|
||||
workersReadyWG sync.WaitGroup
|
||||
workersWG sync.WaitGroup
|
||||
)
|
||||
|
||||
func WorkerHandleRequest(responseWriter http.ResponseWriter, request *http.Request) error {
|
||||
if err := populateEnv(request); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fc, _ := FromContext(request.Context())
|
||||
v, ok := requestsChans.Load(fc.Env["SCRIPT_FILENAME"])
|
||||
if !ok {
|
||||
panic(fmt.Errorf("No worker started for script %s", fc.Env["SCRIPT_FILENAME"]))
|
||||
}
|
||||
rch := v.(cgo.Handle)
|
||||
rc := rch.Value().(chan *http.Request)
|
||||
|
||||
fc.responseWriter = responseWriter
|
||||
fc.done = make(chan interface{})
|
||||
fc.Env["FRANKENPHP_WORKER"] = "1"
|
||||
|
||||
rc <- request
|
||||
<-fc.done
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func StartWorkers(fileName string, nbWorkers int) {
|
||||
if _, ok := requestsChans.Load(fileName); ok {
|
||||
func startWorkers(fileName string, nbWorkers int) error {
|
||||
if _, ok := workersRequestChans.Load(fileName); ok {
|
||||
panic(fmt.Errorf("workers %q: already started", fileName))
|
||||
}
|
||||
|
||||
rc := make(chan *http.Request)
|
||||
rch := cgo.NewHandle(rc)
|
||||
|
||||
requestsChans.Store(fileName, rch)
|
||||
workersRequestChans.Store(fileName, make(chan *http.Request))
|
||||
shutdownWG.Add(nbWorkers)
|
||||
workersReadyWG.Add(nbWorkers)
|
||||
|
||||
var (
|
||||
m sync.Mutex
|
||||
errors []error
|
||||
)
|
||||
for i := 0; i < nbWorkers; i++ {
|
||||
newWorker(fileName, rch)
|
||||
go func() {
|
||||
defer shutdownWG.Done()
|
||||
|
||||
// Create main dummy request
|
||||
r, err := http.NewRequest("GET", "", nil)
|
||||
if err != nil {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
errors = append(errors, fmt.Errorf("workers %q: unable to create main worker request: %w", fileName, err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(
|
||||
r.Context(),
|
||||
contextKey,
|
||||
&FrankenPHPContext{
|
||||
Env: map[string]string{"SCRIPT_FILENAME": fileName},
|
||||
},
|
||||
)
|
||||
|
||||
log.Printf("worker %q: starting", fileName)
|
||||
if err := ServeHTTP(nil, r.WithContext(ctx)); err != nil {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
errors = append(errors, fmt.Errorf("workers %q: unable to start: %w", fileName, err))
|
||||
|
||||
return
|
||||
}
|
||||
log.Printf("worker %q: terminated", fileName)
|
||||
}()
|
||||
}
|
||||
|
||||
workersReadyWG.Wait()
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
if len(errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Wrapping multiple errors will be available in Go 1.20: https://github.com/golang/go/issues/53435
|
||||
return fmt.Errorf("workers %q: error while starting: #%v", fileName, errors)
|
||||
}
|
||||
|
||||
func StopWorkers() {
|
||||
requestsChans.Range(func(k, v any) bool {
|
||||
close(v.(cgo.Handle).Value().(chan *http.Request))
|
||||
requestsChans.Delete(k)
|
||||
func stopWorkers() {
|
||||
workersRequestChans.Range(func(k, v any) bool {
|
||||
close(v.(chan *http.Request))
|
||||
workersRequestChans.Delete(k)
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
workersWaitGroup.Wait()
|
||||
}
|
||||
|
||||
func newWorker(fileName string, requestsChanHandle cgo.Handle) {
|
||||
go func() {
|
||||
workersWaitGroup.Add(1)
|
||||
runtime.LockOSThread()
|
||||
|
||||
cFileName := C.CString(fileName)
|
||||
defer C.free(unsafe.Pointer(cFileName))
|
||||
|
||||
if C.frankenphp_create_server_context(C.uintptr_t(requestsChanHandle), cFileName) < 0 {
|
||||
panic(fmt.Errorf("error during request context creation"))
|
||||
}
|
||||
|
||||
if C.frankenphp_request_startup() < 0 {
|
||||
panic("error during PHP request startup")
|
||||
}
|
||||
|
||||
log.Printf("worker %q: started", fileName)
|
||||
|
||||
if C.frankenphp_execute_script(cFileName) < 0 {
|
||||
panic("error during PHP script execution")
|
||||
}
|
||||
|
||||
C.frankenphp_request_shutdown()
|
||||
|
||||
log.Printf("worker %q: shutting down", fileName)
|
||||
workersWaitGroup.Done()
|
||||
}()
|
||||
//export go_frankenphp_worker_ready
|
||||
func go_frankenphp_worker_ready() {
|
||||
workersReadyWG.Done()
|
||||
}
|
||||
|
||||
//export go_frankenphp_worker_handle_request_start
|
||||
func go_frankenphp_worker_handle_request_start(rch C.uintptr_t) C.uintptr_t {
|
||||
rc := cgo.Handle(rch).Value().(chan *http.Request)
|
||||
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)
|
||||
|
||||
log.Print("worker: waiting for request")
|
||||
v, ok := workersRequestChans.Load(previousFc.Env["SCRIPT_FILENAME"])
|
||||
if !ok {
|
||||
// Probably shutting down
|
||||
return 0
|
||||
}
|
||||
|
||||
rc := v.(chan *http.Request)
|
||||
|
||||
log.Printf("worker %q: waiting for request", previousFc.Env["SCRIPT_FILENAME"])
|
||||
r, ok := <-rc
|
||||
if !ok {
|
||||
// channel closed, server is shutting down
|
||||
log.Print("worker: breaking from the main loop")
|
||||
log.Printf("worker %q: shutting down", previousFc.Env["SCRIPT_FILENAME"])
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
log.Printf("worker: handling request %#v", r)
|
||||
|
||||
fc := r.Context().Value(contextKey).(*FrankenPHPContext)
|
||||
if fc == nil || fc.responseWriter == nil {
|
||||
panic("worker: not a valid worker request")
|
||||
}
|
||||
|
||||
log.Printf("worker %q: handling request %#v", previousFc.Env["SCRIPT_FILENAME"], r)
|
||||
if err := updateServerContext(r); err != nil {
|
||||
// Unexpected error
|
||||
log.Print(err)
|
||||
log.Printf("worker %q: unexpected error: %s", previousFc.Env["SCRIPT_FILENAME"], err)
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -133,4 +133,6 @@ func go_frankenphp_worker_handle_request_end(rh C.uintptr_t) {
|
||||
cgo.Handle(rh).Delete()
|
||||
|
||||
close(fc.done)
|
||||
|
||||
log.Printf("worker %q: finished handling request %#v", fc.Env["SCRIPT_FILENAME"], r)
|
||||
}
|
||||
|
||||
@@ -42,14 +42,19 @@ func TestWorker(t *testing.T) {
|
||||
}
|
||||
|
||||
func ExampleWorkerHandleRequest() {
|
||||
frankenphp.StartWorkers("worker.php", 5)
|
||||
|
||||
phpHandler := func(w http.ResponseWriter, req *http.Request) {
|
||||
if err := frankenphp.WorkerHandleRequest(w, req); err != nil {
|
||||
log.Print(fmt.Errorf("error executing PHP script: %w", err))
|
||||
}
|
||||
if err := frankenphp.Init(
|
||||
frankenphp.WithWorkers("worker1.php", 4),
|
||||
frankenphp.WithWorkers("worker2.php", 2),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer frankenphp.Shutdown()
|
||||
|
||||
http.HandleFunc("/", phpHandler)
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
req := frankenphp.NewRequestWithContext(r, "/path/to/document/root")
|
||||
if err := frankenphp.ServeHTTP(w, req); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user