diff --git a/docs/hot-reload.md b/docs/hot-reload.md index 628377e643..d1eff0b49d 100644 --- a/docs/hot-reload.md +++ b/docs/hot-reload.md @@ -47,6 +47,9 @@ php_server { By default, FrankenPHP will watch all files in the current working directory matching this glob pattern: `./**/*.{css,env,gif,htm,html,jpg,jpeg,js,mjs,php,png,svg,twig,webp,xml,yaml,yml}` +When hot reload is enabled, `opcache.revalidate_freq` defaults to `0`, so OPcache checks on every request whether a script changed. +Setting it in a `php.ini` file or through the `php_ini` directive takes precedence. + It's possible to set the files to watch using the glob syntax explicitly: ```caddyfile diff --git a/frankenphp.c b/frankenphp.c index 12a30abc75..ea72e5ecca 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1522,6 +1522,17 @@ static char *frankenphp_getenv(const char *name, size_t name_len) { return NULL; } +/* php.ini files and php_ini directives override these defaults. */ +static void frankenphp_ini_defaults(HashTable *configuration_hash) { + if (go_is_hot_reload_enabled()) { + /* Otherwise, a page reloaded right after a change may run the old code. */ + zval tmp; + ZVAL_NEW_STR(&tmp, zend_string_init("0", sizeof("0") - 1, 1)); + zend_hash_str_update(configuration_hash, "opcache.revalidate_freq", + sizeof("opcache.revalidate_freq") - 1, &tmp); + } +} + sapi_module_struct frankenphp_sapi_module = { "frankenphp", /* name */ "FrankenPHP", /* pretty name */ @@ -1815,6 +1826,8 @@ static void *php_main(void *arg) { frankenphp_sapi_module.ini_entries = php_ini_overrides; } + frankenphp_sapi_module.ini_defaults = frankenphp_ini_defaults; + frankenphp_init_interned_strings(); /* take a snapshot of the environment for sandboxing */ diff --git a/frankenphp.go b/frankenphp.go index fa645f9c1a..229eb6b2a0 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -71,6 +71,8 @@ var ( // atomic: read by in-flight requests while a reload may rewrite it maxWaitTime atomic.Int64 maxRequestsPerThread int + + hotReloadEnabled bool ) type ErrRejected struct { @@ -427,6 +429,8 @@ func Init(options ...Option) error { } } + hotReloadEnabled = opt.hasHotReload() + mainThread, err := initPHPThreads(opt.numThreads, opt.maxThreads, opt.phpIni) if err != nil { shutdown() @@ -962,6 +966,7 @@ func resetGlobals() { globalWorkersByPath = nil servers = nil watcherIsEnabled = false + hotReloadEnabled = false maxIdleTime = defaultMaxIdleTime maxRequestsPerThread = 0 } diff --git a/hotreload_test.go b/hotreload_test.go new file mode 100644 index 0000000000..92358b8645 --- /dev/null +++ b/hotreload_test.go @@ -0,0 +1,47 @@ +//go:build !nomercure && !nowatcher + +package frankenphp_test + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/dunglas/frankenphp" + "github.com/dunglas/mercure" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHotReloadOpcacheRevalidateFreq(t *testing.T) { + for name, tc := range map[string]struct { + hotReload bool + phpIni map[string]string + expected string + }{ + "hot reload": {hotReload: true, expected: "0"}, + "hot reload with php_ini": {hotReload: true, phpIni: map[string]string{"opcache.revalidate_freq": "5"}, expected: "5"}, + "without hot reload": {expected: "2"}, + "without hot reload php_ini": {phpIni: map[string]string{"opcache.revalidate_freq": "5"}, expected: "5"}, + } { + t.Run(name, func(t *testing.T) { + opts := &testOptions{nbParallelRequests: 1, phpIni: tc.phpIni} + if tc.hotReload { + h, err := mercure.NewHub(t.Context(), mercure.WithTransport(mercure.NewLocalTransport(mercure.NewSubscriberList(0)))) + require.NoError(t, err) + + opts.initOpts = []frankenphp.Option{frankenphp.WithHotReload("hot-reload", h, []string{"./testdata/**/*.none"})} + } + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) { + body, _ := testGet("http://example.com/ini.php?key=opcache.revalidate_freq", handler, t) + if strings.HasSuffix(body, ":") { + t.Skip("OPcache is not loaded") + } + + assert.Equal(t, "opcache.revalidate_freq:"+tc.expected, body) + }, opts) + }) + } +} diff --git a/phpmainthread.go b/phpmainthread.go index 5e19c0fc75..23a544ced8 100644 --- a/phpmainthread.go +++ b/phpmainthread.go @@ -285,6 +285,11 @@ func go_frankenphp_shutdown_main_thread() { mainThread.state.Set(state.Reserved) } +//export go_is_hot_reload_enabled +func go_is_hot_reload_enabled() C.bool { + return C.bool(hotReloadEnabled) +} + //export go_get_custom_php_ini func go_get_custom_php_ini(disableTimeouts C.bool) *C.char { if mainThread.phpIni == nil { diff --git a/watcher-skip.go b/watcher-skip.go index b6413c58d2..9f1db3c949 100644 --- a/watcher-skip.go +++ b/watcher-skip.go @@ -7,6 +7,10 @@ import "errors" type hotReloadOpt struct { } +func (*hotReloadOpt) hasHotReload() bool { + return false +} + var errWatcherNotEnabled = errors.New("watcher support is not enabled") // validateWatchers reports what initWatchers() would refuse, so Validate() diff --git a/watcher.go b/watcher.go index 6b78688074..63864b9ef6 100644 --- a/watcher.go +++ b/watcher.go @@ -21,6 +21,10 @@ func validateWatchers(*opt) error { return nil } +func (o *hotReloadOpt) hasHotReload() bool { + return len(o.hotReload) != 0 +} + func initWatchers(o *opt) error { watchPatterns := make([]*watcher.PatternGroup, 0, len(o.hotReload))