feat: use threads instead of GoRoutines (#6)

* feat: use threads instead of GoRoutines

* many improvements

* fix some bugs
This commit is contained in:
Kévin Dunglas
2022-10-04 14:36:03 +02:00
parent 84273ec395
commit 796476d537
21 changed files with 1049 additions and 890 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.vscode/
/caddy/frankenphp/frankenphp
/internal/testserver/testserver

4
.gitmodules vendored Normal file
View 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

Submodule C-Thread-Pool added at d42cc5a2f7

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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
)

File diff suppressed because it is too large Load Diff

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View 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
View 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
View 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
View File

@@ -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)
}

View File

@@ -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))
}