From fb63099a8852f54ca0350e513d2e30b5fbef80ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 15 Sep 2023 12:59:43 +0200 Subject: [PATCH] feat: allow passing env vars to workers (#210) --- caddy/caddy.go | 70 +++++++++++++++++++++++++++++++++++++-------- caddy/caddy_test.go | 66 ++++++++++++++++++++++++++++++++++++++++++ docs/config.md | 52 +++++++++++++++++++++++++++++++++ frankenphp.c | 2 +- frankenphp_test.go | 3 +- options.go | 5 ++-- testdata/env.php | 11 +++++++ worker.go | 12 +++++--- worker_test.go | 17 +++++++++-- 9 files changed, 216 insertions(+), 22 deletions(-) create mode 100644 testdata/env.php diff --git a/caddy/caddy.go b/caddy/caddy.go index 7a1cbd1..b36c711 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -4,6 +4,7 @@ package caddy import ( + "errors" "net/http" "strconv" @@ -38,13 +39,19 @@ func (phpInterpreterDestructor) Destruct() error { } type workerConfig struct { + // FileName sets the path to the worker script. FileName string `json:"file_name,omitempty"` - Num int `json:"num,omitempty"` + // Num sets the number of workers to start. + Num int `json:"num,omitempty"` + // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. + Env map[string]string `json:"env,omitempty"` } type FrankenPHPApp struct { - NumThreads int `json:"num_threads,omitempty"` - Workers []workerConfig `json:"workers,omitempty"` + // NumThreads sets the number of PHP threads to start. Default: 2x the number of available CPUs. + NumThreads int `json:"num_threads,omitempty"` + // Workers configures the worker scripts to start. + Workers []workerConfig `json:"workers,omitempty"` } // CaddyModule returns the Caddy module information. @@ -61,7 +68,7 @@ func (f *FrankenPHPApp) Start() error { opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger)} for _, w := range f.Workers { - opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num)) + opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env)) } _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) { @@ -111,11 +118,11 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { f.NumThreads = v case "worker": - if !d.NextArg() { - return d.ArgErr() + wc := workerConfig{} + if d.NextArg() { + wc.FileName = d.Val() } - wc := workerConfig{FileName: d.Val()} if d.NextArg() { v, err := strconv.Atoi(d.Val()) if err != nil { @@ -125,6 +132,41 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { wc.Num = v } + for d.NextBlock(1) { + v := d.Val() + switch v { + case "file": + if !d.NextArg() { + return d.ArgErr() + } + wc.FileName = d.Val() + case "num": + if !d.NextArg() { + return d.ArgErr() + } + + v, err := strconv.Atoi(d.Val()) + if err != nil { + return err + } + + wc.Num = v + case "env": + args := d.RemainingArgs() + if len(args) != 2 { + return d.ArgErr() + } + if wc.Env == nil { + wc.Env = make(map[string]string) + } + wc.Env[args[0]] = args[1] + } + + if wc.FileName == "" { + return errors.New(`The "file" argument must be specified`) + } + } + f.Workers = append(f.Workers, wc) } } @@ -147,11 +189,15 @@ func parseGlobalOption(d *caddyfile.Dispenser, _ interface{}) (interface{}, erro } type FrankenPHPModule struct { - Root string `json:"root,omitempty"` - SplitPath []string `json:"split_path,omitempty"` - ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"` - Env map[string]string `json:"env,omitempty"` - logger *zap.Logger + // Root sets the root folder to the site. Default: `root` directive. + Root string `json:"root,omitempty"` + // SplitPath sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php`. + SplitPath []string `json:"split_path,omitempty"` + // ResolveRootSymlink enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists. + ResolveRootSymlink bool `json:"resolve_root_symlink,omitempty"` + // Env sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. + Env map[string]string `json:"env,omitempty"` + logger *zap.Logger } // CaddyModule returns the Caddy module information. diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index 49d2ffc..e8b8013 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -73,3 +73,69 @@ func TestLargeRequest(t *testing.T) { "Request body size: 1048576 (unknown)", ) } + +func TestWorker(t *testing.T) { + var wg sync.WaitGroup + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + + frankenphp { + worker ../testdata/index.php 2 + } + } + + localhost:9080 { + route { + php { + root ../testdata + } + } + } + `, "caddyfile") + + for i := 0; i < 100; i++ { + wg.Add(1) + + go func(i int) { + tester.AssertGetResponse(fmt.Sprintf("http://localhost:9080/index.php?i=%d", i), http.StatusOK, fmt.Sprintf("I am by birth a Genevese (%d)", i)) + wg.Done() + }(i) + } + wg.Wait() +} + +func TestEnv(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + + frankenphp { + worker { + file ../testdata/env.php + num 1 + env FOO bar + } + } + } + + localhost:9080 { + route { + php { + root ../testdata + env FOO baz + } + } + } + `, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080/env.php", http.StatusOK, "bazbar") +} diff --git a/docs/config.md b/docs/config.md index 435bafe..87ee203 100644 --- a/docs/config.md +++ b/docs/config.md @@ -8,6 +8,58 @@ You can also configure PHP using `php.ini` as usual. In the Docker image, the `php.ini` file is located at `/usr/local/lib/php.ini`. +## Caddy Directives + +To register the FrankenPHP executor, the `frankenphp` directive must be set in Caddy global options, then the `php` HTTP directive must be set under routes serving PHP scripts: + + +Then, you can use the `php` HTTP directive to execute PHP scripts: + +```caddyfile +{ + frankenphp +} + +localhost { + route { + php { + root # Sets the root folder to the site. Default: `root` directive. + split_path # Sets the substrings for splitting the URI into two parts. The first matching substring will be used to split the "path info" from the path. The first piece is suffixed with the matching substring and will be assumed as the actual resource (CGI script) name. The second piece will be set to PATH_INFO for the CGI script to use. Default: `.php` + resolve_root_symlink # Enables resolving the `root` directory to its actual value by evaluating a symbolic link, if one exists. + env # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. + } + } +} +``` + +Optionnaly, the number of threads to create and [worker scripts](worker.md) to start with the server can be specified under the global directive. + +```caddyfile +{ + frankenphp { + num_threads # Sets the number of PHP threads to start. Default: 2x the number of available CPUs. + worker { + file # Sets the path to the worker script. + num # Sets the number of PHP threads to start, defaults to 2x the number of available CPUs. + env # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. + } + } +} + +# ... +``` + +Alternatively, the short form of the `worker` directive can also be used: + +```caddyfile +{ + frankenphp { + worker + } +} + +# ... +``` ## Environment Variables diff --git a/frankenphp.c b/frankenphp.c index 81c66a1..b9bb70f 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -563,7 +563,7 @@ sapi_module_struct frankenphp_sapi_module = { static void *manager_thread(void *arg) { #ifdef ZTS - // TODO: use tsrm_startup() directly as we now the number of expected threads + // TODO: use tsrm_startup() directly as we know the number of expected threads php_tsrm_startup(); /*tsrm_error_set(TSRM_ERROR_LEVEL_INFO, NULL);*/ # ifdef PHP_WIN32 diff --git a/frankenphp_test.go b/frankenphp_test.go index 0f01748..2008ab3 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -28,6 +28,7 @@ import ( type testOptions struct { workerScript string nbWorkers int + env map[string]string nbParrallelRequests int realServer bool logger *zap.Logger @@ -51,7 +52,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), * initOpts := []frankenphp.Option{frankenphp.WithLogger(opts.logger)} if opts.workerScript != "" { - initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers)) + initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env)) } initOpts = append(initOpts, opts.initOpts...) diff --git a/options.go b/options.go index beeb85c..13c22c9 100644 --- a/options.go +++ b/options.go @@ -19,6 +19,7 @@ type opt struct { type workerOpt struct { fileName string num int + env map[string]string } // WithNumThreads configures the number of PHP threads to start. @@ -31,9 +32,9 @@ func WithNumThreads(numThreads int) Option { } // WithWorkers configures the PHP workers to start. -func WithWorkers(fileName string, num int) Option { +func WithWorkers(fileName string, num int, env map[string]string) Option { return func(o *opt) error { - o.workers = append(o.workers, workerOpt{fileName, num}) + o.workers = append(o.workers, workerOpt{fileName, num, env}) return nil } diff --git a/testdata/env.php b/testdata/env.php new file mode 100644 index 0000000..4e0683c --- /dev/null +++ b/testdata/env.php @@ -0,0 +1,11 @@ +