From 2e151231fa358b01ea89b92a079481e5503f7192 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sun, 1 Sep 2024 23:53:10 +0200 Subject: [PATCH 001/155] Adds filesystem watcher with tests. --- caddy/caddy.go | 10 ++ docs/config.md | 31 ++++++ frankenphp.go | 5 + frankenphp_test.go | 4 + go.mod | 2 + go.sum | 4 + options.go | 14 +++ testdata/files/.gitignore | 2 + testdata/worker-with-watcher.php | 13 +++ watcher.go | 165 +++++++++++++++++++++++++++++++ watcher_options.go | 37 +++++++ watcher_options_test.go | 115 +++++++++++++++++++++ watcher_test.go | 55 +++++++++++ worker.go | 12 ++- 14 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 testdata/files/.gitignore create mode 100644 testdata/worker-with-watcher.php create mode 100644 watcher.go create mode 100644 watcher_options.go create mode 100644 watcher_options_test.go create mode 100644 watcher_test.go diff --git a/caddy/caddy.go b/caddy/caddy.go index 493ab585b..1684e0b45 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -64,6 +64,8 @@ type FrankenPHPApp struct { NumThreads int `json:"num_threads,omitempty"` // Workers configures the worker scripts to start. Workers []workerConfig `json:"workers,omitempty"` + // Directories to watch for changes + Watch []string `json:"watch,omitempty"` } // CaddyModule returns the Caddy module information. @@ -82,6 +84,9 @@ func (f *FrankenPHPApp) Start() error { for _, w := range f.Workers { opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env)) } + for _, fileName := range f.Watch { + opts = append(opts, frankenphp.WithFileWatcher(fileName)) + } _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) { if err := frankenphp.Init(opts...); err != nil { @@ -126,6 +131,11 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } f.NumThreads = v + case "watch": + if !d.NextArg() { + return d.ArgErr() + } + f.Watch = append(f.Watch, d.Val()) case "worker": wc := workerConfig{} diff --git a/docs/config.md b/docs/config.md index c114ee0da..7c00405b6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -130,6 +130,37 @@ php_server [] { } ``` +### Watching for file changes +Since workers won't restart automatically on file changes you can also +define a number of directories that should be watched. This is useful for +development environments. + +```caddyfile +{ + frankenphp { + worker /path/to/app/public/worker.php + watch /path/to/app/sourcefiles + } +} +``` + +The configuration above will watch the `/path/to/app/sourcefiles` directory recursively. +You can also add multiple `watch` directives + +```caddyfile +{ + frankenphp { + watch /path/to/app/folder1 # watches all subdirectories + watch /path/to/app/folder2/*.php # watches only php files in the app directory + watch /path/to/app/folder3/**/*.php # watches only php files in the app directory and subdirectories + } +} +``` +Be sure not to include files that are created at runtime (like logs) into you watcher, since they might cause unwanted +worker restarts. +The file watcher is based on [fsnotify](https://github.com/fsnotify/fsnotify). + + ### Full Duplex (HTTP/1) When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body diff --git a/frankenphp.go b/frankenphp.go index eca530868..a88a56b82 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -319,6 +319,10 @@ func Init(options ...Option) error { return err } + if err := initWatcher(opt.watch, opt.workers); err != nil { + return err + } + logger.Info("FrankenPHP started 🐘", zap.String("php_version", Version().Version), zap.Int("num_threads", opt.numThreads)) if EmbeddedAppPath != "" { logger.Info("embedded PHP app 📦", zap.String("path", EmbeddedAppPath)) @@ -330,6 +334,7 @@ func Init(options ...Option) error { // Shutdown stops the workers and the PHP runtime. func Shutdown() { stopWorkers() + stopWatcher() close(done) shutdownWG.Wait() requestChan = nil diff --git a/frankenphp_test.go b/frankenphp_test.go index eee4e0970..219e32bc5 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -34,6 +34,7 @@ import ( type testOptions struct { workerScript string + watch string nbWorkers int env map[string]string nbParrallelRequests int @@ -61,6 +62,9 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), * if opts.workerScript != "" { initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env)) } + if(opts.watch != "") { + initOpts = append(initOpts, frankenphp.WithFileWatcher(opts.watch)) + } initOpts = append(initOpts, opts.initOpts...) err := frankenphp.Init(initOpts...) diff --git a/go.mod b/go.mod index 3fec57523..638b535ce 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ toolchain go1.22.0 retract v1.0.0-rc.1 // Human error require ( + github.com/fsnotify/fsnotify v1.7.0 github.com/maypok86/otter v1.2.1 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 @@ -21,6 +22,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 89ae67004..f02a56108 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ 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/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -30,6 +32,8 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/options.go b/options.go index 0c5622b8b..f35c2edda 100644 --- a/options.go +++ b/options.go @@ -13,6 +13,7 @@ type Option func(h *opt) error type opt struct { numThreads int workers []workerOpt + watch []watchOpt logger *zap.Logger } @@ -40,6 +41,19 @@ func WithWorkers(fileName string, num int, env map[string]string) Option { } } +// WithFileWatcher configures filesystem watching. +func WithFileWatcher(fileName string) Option { + return func(o *opt) error { + watchOpt, err := fileNameToWatchOption(fileName) + + if(err == nil) { + o.watch = append(o.watch, watchOpt) + } + + return nil + } +} + // WithLogger configures the global logger to use. func WithLogger(l *zap.Logger) Option { return func(o *opt) error { diff --git a/testdata/files/.gitignore b/testdata/files/.gitignore new file mode 100644 index 000000000..88428ddd0 --- /dev/null +++ b/testdata/files/.gitignore @@ -0,0 +1,2 @@ +*.json +*.txt \ No newline at end of file diff --git a/testdata/worker-with-watcher.php b/testdata/worker-with-watcher.php new file mode 100644 index 000000000..fee3e266a --- /dev/null +++ b/testdata/worker-with-watcher.php @@ -0,0 +1,13 @@ + Date: Mon, 2 Sep 2024 09:21:24 +0200 Subject: [PATCH 002/155] Refactoring. --- options.go | 2 +- testdata/worker-with-watcher.php | 4 +--- watcher.go | 37 ++++++-------------------------- watcher_options.go | 26 +++++++++++++++++++++- watcher_options_test.go | 22 +++++++++---------- watcher_test.go | 34 +++++++++++++++++++---------- 6 files changed, 68 insertions(+), 57 deletions(-) diff --git a/options.go b/options.go index f35c2edda..21abac368 100644 --- a/options.go +++ b/options.go @@ -44,7 +44,7 @@ func WithWorkers(fileName string, num int, env map[string]string) Option { // WithFileWatcher configures filesystem watching. func WithFileWatcher(fileName string) Option { return func(o *opt) error { - watchOpt, err := fileNameToWatchOption(fileName) + watchOpt, err := createWatchOption(fileName) if(err == nil) { o.watch = append(o.watch, watchOpt) diff --git a/testdata/worker-with-watcher.php b/testdata/worker-with-watcher.php index fee3e266a..f6807a54d 100644 --- a/testdata/worker-with-watcher.php +++ b/testdata/worker-with-watcher.php @@ -1,10 +1,8 @@ Date: Mon, 2 Sep 2024 10:10:45 +0200 Subject: [PATCH 003/155] Formatting. --- watcher.go | 49 +++++++++++++++++++++++----------------------- watcher_options.go | 42 +++++++++++++++++++-------------------- worker.go | 9 ++++----- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/watcher.go b/watcher.go index b296eeb67..2bcf7693a 100644 --- a/watcher.go +++ b/watcher.go @@ -7,12 +7,13 @@ import ( "strings" "os" "time" + "sync/atomic" ) // sometimes multiple fs events fire at once so we'll wait a few ms before reloading const debounceDuration = 300 var watcher *fsnotify.Watcher -var isReloadingWorkers bool = false +var isReloadingWorkers atomic.Bool func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { if(len(watchOpts) == 0) { @@ -42,27 +43,27 @@ func stopWatcher() { func listenForFileChanges(watchOpts []watchOpt, workerOpts []workerOpt) { for { - select { - case event, ok := <-watcher.Events: - if !ok { - logger.Error("unexpected watcher event") - return - } - watchCreatedDirectories(event, watchOpts) - if isReloadingWorkers || !fileMatchesPattern(event.Name, watchOpts) { - continue - } - isReloadingWorkers = true - logger.Info("filesystem change detected", zap.String("event", event.Name)) - go reloadWorkers(workerOpts) - - case err, ok := <-watcher.Errors: - if !ok { - return - } - logger.Error("watcher: error:", zap.Error(err)) - } - } + select { + case event, ok := <-watcher.Events: + if !ok { + logger.Error("unexpected watcher event") + return + } + watchCreatedDirectories(event, watchOpts) + if isReloadingWorkers.Load() || !fileMatchesPattern(event.Name, watchOpts) { + continue + } + isReloadingWorkers.Store(true) + logger.Info("filesystem change detected", zap.String("event", event.Name)) + go reloadWorkers(workerOpts) + + case err, ok := <-watcher.Errors: + if !ok { + return + } + logger.Error("watcher: error:", zap.Error(err)) + } + } } @@ -112,7 +113,7 @@ func watchCreatedDirectories(event fsnotify.Event, watchOpts []watchOpt) { for _, watchOpt := range watchOpts { if(watchOpt.isRecursive && strings.HasPrefix(event.Name, watchOpt.dirName)) { logger.Debug("watching new dir", zap.String("dir", event.Name)) - watcher.Add(event.Name) + watcher.Add(event.Name) } } @@ -137,6 +138,6 @@ func reloadWorkers(workerOpts []workerOpt) { } logger.Info("workers restarted successfully") - isReloadingWorkers = false + isReloadingWorkers.Store(false) } diff --git a/watcher_options.go b/watcher_options.go index 531415932..92251e3a5 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -7,33 +7,33 @@ import ( ) type watchOpt struct { - pattern string - dirName string + pattern string + dirName string isRecursive bool } func createWatchOption(fileName string) (watchOpt, error) { watchOpt := watchOpt{pattern: "", dirName: fileName, isRecursive: true} - dirName, baseName := filepath.Split(watchOpt.dirName) - if(strings.Contains(baseName, "*") || strings.Contains(baseName, ".")) { - watchOpt.dirName = dirName - watchOpt.pattern = baseName - watchOpt.isRecursive = false - } + dirName, baseName := filepath.Split(watchOpt.dirName) + if(strings.Contains(baseName, "*") || strings.Contains(baseName, ".")) { + watchOpt.dirName = dirName + watchOpt.pattern = baseName + watchOpt.isRecursive = false + } - if(strings.Contains(fileName, "/**")) { - watchOpt.dirName = strings.Split(fileName, "/**")[0] - watchOpt.isRecursive = true - } + if(strings.Contains(fileName, "/**")) { + watchOpt.dirName = strings.Split(fileName, "/**")[0] + watchOpt.isRecursive = true + } - absName, err := filepath.Abs(watchOpt.dirName) - if err != nil { - logger.Error("directory could not be watched", zap.String("dir", watchOpt.dirName), zap.Error(err)) - return watchOpt, err - } - watchOpt.dirName = absName + absName, err := filepath.Abs(watchOpt.dirName) + if err != nil { + logger.Error("directory could not be watched", zap.String("dir", watchOpt.dirName), zap.Error(err)) + return watchOpt, err + } + watchOpt.dirName = absName - return watchOpt, nil + return watchOpt, nil } func fileMatchesPattern(fileName string, watchOpts []watchOpt) bool { @@ -42,8 +42,8 @@ func fileMatchesPattern(fileName string, watchOpts []watchOpt) bool { continue } if(watchOpt.isRecursive == false && filepath.Dir(fileName) != watchOpt.dirName) { - continue - } + continue + } if watchOpt.pattern == "" { return true } diff --git a/worker.go b/worker.go index 9d5f9c15e..155556144 100644 --- a/worker.go +++ b/worker.go @@ -19,13 +19,12 @@ var ( workersRequestChans sync.Map // map[fileName]chan *http.Request workersReadyWG sync.WaitGroup workerShutdownWG sync.WaitGroup - workersDone chan interface{} + workersDone chan interface{} ) // TODO: start all the worker in parallell to reduce the boot time func initWorkers(opt []workerOpt) error { workersDone = make(chan interface{}) - workersReadyWG = sync.WaitGroup{} for _, w := range opt { if err := startWorkers(w.fileName, w.num, w.env); err != nil { return err @@ -50,7 +49,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { workersReadyWG.Add(nbWorkers) var ( - m sync.RWMutex + m sync.RWMutex errs []error ) @@ -137,8 +136,8 @@ func stopWorkers() { return true }) if(workersDone != nil) { - close(workersDone) - } + close(workersDone) + } workerShutdownWG.Wait() } From bf4e72fec6e9f6d3180b606f15d92756465fdc84 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 10:22:59 +0200 Subject: [PATCH 004/155] Formatting. --- watcher_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watcher_test.go b/watcher_test.go index 56f915aa1..53adc55b3 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -61,7 +61,7 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt func updateTestFile(fileName string, content string){ bytes := []byte(content) err := os.WriteFile(fileName, bytes, 0644) - if(err != nil) { - panic(err) - } + if(err != nil) { + panic(err) + } } \ No newline at end of file From 4920767eebc023d3709f60045719fa9aa26bb002 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 11:29:12 +0200 Subject: [PATCH 005/155] Switches to absolute path in tests. --- go.mod | 2 +- watcher_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index a2f6d0b8c..7db757599 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.22.0 retract v1.0.0-rc.1 // Human error require ( - github.com/fsnotify/fsnotify v1.7.0 + github.com/fsnotify/fsnotify v1.7.0 github.com/maypok86/otter v1.2.2 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 diff --git a/watcher_test.go b/watcher_test.go index 53adc55b3..6076a6539 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -12,8 +12,8 @@ import ( func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.txt" - updateTestFile("./testdata/files/test.txt", "version1") + const filePattern = "/go/src/app/testdata/**/*.txt" + updateTestFile("/go/src/app/testdata/files/test.txt", "version1") runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -21,7 +21,7 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { assert.Equal(t, "version1", body) // now we verify that updating a .txt file does not cause a reload - updateTestFile("./testdata/files/test.txt", "version2") + updateTestFile("/go/src/app/testdata/files/test.txt", "version2") time.Sleep(1000 * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) assert.Equal(t, "version2", body) @@ -30,8 +30,8 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.json" - updateTestFile("./testdata/files/test.txt", "version1") + const filePattern = "/go/src/app/testdata/**/*.json" + updateTestFile("/go/src/app/testdata/files/test.txt", "version1") runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -39,7 +39,7 @@ func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { assert.Equal(t, "version1", body) // now we verify that updating a .txt file does not cause a reload - updateTestFile("./testdata/files/test.txt", "version2") + updateTestFile("/go/src/app/testdata/files/test.txt", "version2") time.Sleep(1000 * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) assert.Equal(t, "version1", body) From c834a020b79260493cfb3b0c7f72c534fb461e15 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 14:56:36 +0200 Subject: [PATCH 006/155] Fixes race condition from merge conflict. --- watcher_test.go | 12 ++++++------ worker.go | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/watcher_test.go b/watcher_test.go index 6076a6539..53adc55b3 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -12,8 +12,8 @@ import ( func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { - const filePattern = "/go/src/app/testdata/**/*.txt" - updateTestFile("/go/src/app/testdata/files/test.txt", "version1") + const filePattern = "./testdata/**/*.txt" + updateTestFile("./testdata/files/test.txt", "version1") runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -21,7 +21,7 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { assert.Equal(t, "version1", body) // now we verify that updating a .txt file does not cause a reload - updateTestFile("/go/src/app/testdata/files/test.txt", "version2") + updateTestFile("./testdata/files/test.txt", "version2") time.Sleep(1000 * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) assert.Equal(t, "version2", body) @@ -30,8 +30,8 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { - const filePattern = "/go/src/app/testdata/**/*.json" - updateTestFile("/go/src/app/testdata/files/test.txt", "version1") + const filePattern = "./testdata/**/*.json" + updateTestFile("./testdata/files/test.txt", "version1") runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -39,7 +39,7 @@ func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { assert.Equal(t, "version1", body) // now we verify that updating a .txt file does not cause a reload - updateTestFile("/go/src/app/testdata/files/test.txt", "version2") + updateTestFile("./testdata/files/test.txt", "version2") time.Sleep(1000 * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) assert.Equal(t, "version1", body) diff --git a/worker.go b/worker.go index 155556144..c1a377236 100644 --- a/worker.go +++ b/worker.go @@ -46,6 +46,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { workersRequestChans.Store(absFileName, make(chan *http.Request)) shutdownWG.Add(nbWorkers) + workerShutdownWG.Add(nbWorkers) workersReadyWG.Add(nbWorkers) var ( @@ -63,6 +64,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { for i := 0; i < nbWorkers; i++ { go func() { defer shutdownWG.Done() + defer workerShutdownWG.Done() for { // Create main dummy request r, err := http.NewRequest(http.MethodGet, filepath.Base(absFileName), nil) From 4273035e81ab4ec6dd23c71978e3fffd2ba20327 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 15:49:03 +0200 Subject: [PATCH 007/155] Fixes race condition. --- watcher.go | 27 +++++++++++++++++++++++---- watcher_test.go | 25 +++++++++++++------------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/watcher.go b/watcher.go index 2bcf7693a..20b030ecb 100644 --- a/watcher.go +++ b/watcher.go @@ -8,11 +8,13 @@ import ( "os" "time" "sync/atomic" + "sync" ) // sometimes multiple fs events fire at once so we'll wait a few ms before reloading const debounceDuration = 300 var watcher *fsnotify.Watcher +var watcherMu sync.RWMutex var isReloadingWorkers atomic.Bool func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { @@ -23,28 +25,36 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { if err != nil { return err } + watcherMu.Lock() watcher = fsWatcher; - - go listenForFileChanges(watchOpts, workerOpts) + watcherMu.Unlock() if err := addWatchedDirectories(watchOpts); err != nil { logger.Error("failed to watch directories") return err } + go listenForFileChanges(watchOpts, workerOpts) + return nil; } func stopWatcher() { if(watcher != nil) { + watcherMu.RLock() watcher.Close() + watcherMu.RUnlock() } } func listenForFileChanges(watchOpts []watchOpt, workerOpts []workerOpt) { + watcherMu.RLock() + events := watcher.Events + errors := watcher.Errors + watcherMu.RUnlock() for { select { - case event, ok := <-watcher.Events: + case event, ok := <-events: if !ok { logger.Error("unexpected watcher event") return @@ -57,7 +67,7 @@ func listenForFileChanges(watchOpts []watchOpt, workerOpts []workerOpt) { logger.Info("filesystem change detected", zap.String("event", event.Name)) go reloadWorkers(workerOpts) - case err, ok := <-watcher.Errors: + case err, ok := <-errors: if !ok { return } @@ -71,7 +81,9 @@ func addWatchedDirectories(watchOpts []watchOpt) error { for _, watchOpt := range watchOpts { logger.Debug("watching for changes", zap.String("dir", watchOpt.dirName), zap.String("pattern", watchOpt.pattern), zap.Bool("recursive", watchOpt.isRecursive)) if(watchOpt.isRecursive == false) { + watcherMu.RLock() watcher.Add(watchOpt.dirName) + watcherMu.RUnlock() continue } if err := watchRecursively(watchOpt.dirName); err != nil { @@ -88,7 +100,9 @@ func watchRecursively(dir string) error { return err } if !fileInfo.IsDir() { + watcherMu.RLock() watcher.Add(dir) + watcherMu.RUnlock() return nil } if err := filepath.Walk(dir, watchFile); err != nil { @@ -113,7 +127,9 @@ func watchCreatedDirectories(event fsnotify.Event, watchOpts []watchOpt) { for _, watchOpt := range watchOpts { if(watchOpt.isRecursive && strings.HasPrefix(event.Name, watchOpt.dirName)) { logger.Debug("watching new dir", zap.String("dir", event.Name)) + watcherMu.RLock() watcher.Add(event.Name) + watcherMu.RLock() } } @@ -122,6 +138,8 @@ func watchCreatedDirectories(event fsnotify.Event, watchOpts []watchOpt) { func watchFile(path string, fi os.FileInfo, err error) error { // ignore paths that start with a dot (like .git) if fi.Mode().IsDir() && !strings.HasPrefix(filepath.Base(fi.Name()), ".") { + watcherMu.RLock() + defer watcherMu.RUnlock() return watcher.Add(path) } return nil @@ -141,3 +159,4 @@ func reloadWorkers(workerOpts []workerOpt) { isReloadingWorkers.Store(false) } + diff --git a/watcher_test.go b/watcher_test.go index 53adc55b3..589f3df34 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -10,39 +10,40 @@ import ( "github.com/stretchr/testify/assert" ) +// we have to wait a few milliseconds for the worker debounce to take effect +const debounceMilliseconds = 500 + func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { const filePattern = "./testdata/**/*.txt" - updateTestFile("./testdata/files/test.txt", "version1") runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Equal(t, "version1", body) + assert.Equal(t, "requests:1", body) // now we verify that updating a .txt file does not cause a reload - updateTestFile("./testdata/files/test.txt", "version2") - time.Sleep(1000 * time.Millisecond) + updateTestFile("./testdata/files/test.txt", "updated") + time.Sleep(debounceMilliseconds * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Equal(t, "version2", body) + assert.Equal(t, "requests:1", body) }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) } func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.json" - updateTestFile("./testdata/files/test.txt", "version1") + const filePattern = "./testdata/**/*.txt" runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Equal(t, "version1", body) + assert.Equal(t, "requests:1", body) - // now we verify that updating a .txt file does not cause a reload - updateTestFile("./testdata/files/test.txt", "version2") - time.Sleep(1000 * time.Millisecond) + // now we verify that updating a .json file does not cause a reload + updateTestFile("./testdata/files/test.json", "{updated:true}") + time.Sleep(debounceMilliseconds * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Equal(t, "version1", body) + assert.Equal(t, "requests:2", body) }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) } From d5d86d5a31a741790af756eba1536953839e160c Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 16:03:58 +0200 Subject: [PATCH 008/155] Fixes tests. --- testdata/worker-with-watcher.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testdata/worker-with-watcher.php b/testdata/worker-with-watcher.php index f6807a54d..7db14b496 100644 --- a/testdata/worker-with-watcher.php +++ b/testdata/worker-with-watcher.php @@ -1,9 +1,9 @@ Date: Mon, 2 Sep 2024 16:08:46 +0200 Subject: [PATCH 009/155] Fixes markdown lint errors. --- docs/config.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index f66d561b2..c876512ad 100644 --- a/docs/config.md +++ b/docs/config.md @@ -132,6 +132,7 @@ php_server [] { ``` ### Watching for file changes + Since workers won't restart automatically on file changes you can also define a number of directories that should be watched. This is useful for development environments. @@ -157,11 +158,11 @@ You can also add multiple `watch` directives } } ``` + Be sure not to include files that are created at runtime (like logs) into you watcher, since they might cause unwanted worker restarts. The file watcher is based on [fsnotify](https://github.com/fsnotify/fsnotify). - ### Full Duplex (HTTP/1) When using HTTP/1.x, it may be desirable to enable full-duplex mode to allow writing a response before the entire body From ad3c7fc917119f42918d3ed141ab14a57cff98bd Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 16:29:20 +0200 Subject: [PATCH 010/155] Switches back to absolute paths. --- watcher_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/watcher_test.go b/watcher_test.go index 589f3df34..5bfb650ba 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -7,6 +7,7 @@ import ( "testing" "os" "time" + "path/filepath" "github.com/stretchr/testify/assert" ) @@ -15,7 +16,7 @@ const debounceMilliseconds = 500 func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.txt" + const filePattern = "/go/src/app/testdata/**/*.txt" runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -23,7 +24,7 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .txt file does not cause a reload - updateTestFile("./testdata/files/test.txt", "updated") + updateTestFile("/go/src/app/testdata/files/test.txt", "updated") time.Sleep(debounceMilliseconds * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) assert.Equal(t, "requests:1", body) @@ -32,7 +33,7 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.txt" + const filePattern = "/go/src/app/testdata/**/*.txt" runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -40,7 +41,7 @@ func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .json file does not cause a reload - updateTestFile("./testdata/files/test.json", "{updated:true}") + updateTestFile("/go/src/app/testdata/files/test.json", "{updated:true}") time.Sleep(debounceMilliseconds * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) assert.Equal(t, "requests:2", body) @@ -60,6 +61,10 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt } func updateTestFile(fileName string, content string){ + dirName := filepath.Dir(fileName) + if _, err := os.Stat(dirName); os.IsNotExist(err) { + os.MkdirAll(dirName, 0700) + } bytes := []byte(content) err := os.WriteFile(fileName, bytes, 0644) if(err != nil) { From 9b403622c67b78490e86e30fae5499f0da69facf Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 16:44:33 +0200 Subject: [PATCH 011/155] Reverts back to relative file paths. --- watcher_test.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/watcher_test.go b/watcher_test.go index 5bfb650ba..8dcf05f77 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -16,7 +16,7 @@ const debounceMilliseconds = 500 func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { - const filePattern = "/go/src/app/testdata/**/*.txt" + const filePattern = "./testdata/**/*.txt" runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -24,16 +24,18 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .txt file does not cause a reload - updateTestFile("/go/src/app/testdata/files/test.txt", "updated") + absPath, err := filepath.Abs("./testdata/files/test.txt") + updateTestFile(absPath, "updated") time.Sleep(debounceMilliseconds * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) + assert.Nil(t, err) assert.Equal(t, "requests:1", body) }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) } func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { - const filePattern = "/go/src/app/testdata/**/*.txt" + const filePattern = "./testdata/**/*.txt" runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -41,9 +43,11 @@ func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .json file does not cause a reload - updateTestFile("/go/src/app/testdata/files/test.json", "{updated:true}") + absPath, err := filepath.Abs("./testdata/files/test.json") + updateTestFile(absPath, "{updated:true}") time.Sleep(debounceMilliseconds * time.Millisecond) body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) + assert.Nil(t, err) assert.Equal(t, "requests:2", body) }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) @@ -63,8 +67,8 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt func updateTestFile(fileName string, content string){ dirName := filepath.Dir(fileName) if _, err := os.Stat(dirName); os.IsNotExist(err) { - os.MkdirAll(dirName, 0700) - } + os.MkdirAll(dirName, 0700) + } bytes := []byte(content) err := os.WriteFile(fileName, bytes, 0644) if(err != nil) { From 97b468837ee6758006b2a4d5fc9202daf1d13f5e Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 17:33:38 +0200 Subject: [PATCH 012/155] Fixes golangci-lint issues. --- watcher.go | 19 ++++++++++++------- watcher_options.go | 2 +- watcher_test.go | 5 ++++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/watcher.go b/watcher.go index 20b030ecb..cafb58976 100644 --- a/watcher.go +++ b/watcher.go @@ -80,10 +80,13 @@ func listenForFileChanges(watchOpts []watchOpt, workerOpts []workerOpt) { func addWatchedDirectories(watchOpts []watchOpt) error { for _, watchOpt := range watchOpts { logger.Debug("watching for changes", zap.String("dir", watchOpt.dirName), zap.String("pattern", watchOpt.pattern), zap.Bool("recursive", watchOpt.isRecursive)) - if(watchOpt.isRecursive == false) { + if(!watchOpt.isRecursive) { watcherMu.RLock() - watcher.Add(watchOpt.dirName) + err := watcher.Add(watchOpt.dirName) watcherMu.RUnlock() + if(err != nil) { + return err + } continue } if err := watchRecursively(watchOpt.dirName); err != nil { @@ -101,9 +104,8 @@ func watchRecursively(dir string) error { } if !fileInfo.IsDir() { watcherMu.RLock() - watcher.Add(dir) - watcherMu.RUnlock() - return nil + defer watcherMu.RUnlock() + return watcher.Add(dir) } if err := filepath.Walk(dir, watchFile); err != nil { return err; @@ -121,14 +123,17 @@ func watchCreatedDirectories(event fsnotify.Event, watchOpts []watchOpt) { logger.Error("unable to stat file", zap.Error(err)) return } - if fileInfo.IsDir() != true { + if !fileInfo.IsDir() { return } for _, watchOpt := range watchOpts { if(watchOpt.isRecursive && strings.HasPrefix(event.Name, watchOpt.dirName)) { logger.Debug("watching new dir", zap.String("dir", event.Name)) watcherMu.RLock() - watcher.Add(event.Name) + err := watcher.Add(event.Name) + if(err != nil) { + logger.Error("failed to watch new dir", zap.Error(err)) + } watcherMu.RLock() } } diff --git a/watcher_options.go b/watcher_options.go index 92251e3a5..0c03feb84 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -41,7 +41,7 @@ func fileMatchesPattern(fileName string, watchOpts []watchOpt) bool { if !strings.HasPrefix(fileName, watchOpt.dirName) { continue } - if(watchOpt.isRecursive == false && filepath.Dir(fileName) != watchOpt.dirName) { + if(!watchOpt.isRecursive && filepath.Dir(fileName) != watchOpt.dirName) { continue } if watchOpt.pattern == "" { diff --git a/watcher_test.go b/watcher_test.go index 8dcf05f77..623e01774 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -67,7 +67,10 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt func updateTestFile(fileName string, content string){ dirName := filepath.Dir(fileName) if _, err := os.Stat(dirName); os.IsNotExist(err) { - os.MkdirAll(dirName, 0700) + err = os.MkdirAll(dirName, 0700) + if(err != nil) { + panic(err) + } } bytes := []byte(content) err := os.WriteFile(fileName, bytes, 0644) From b528c23451233bb0e7070091c3b9992376cfb608 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 2 Sep 2024 21:51:20 +0200 Subject: [PATCH 013/155] Uses github.com/dunglas/go-fswatch instead. --- caddy/go.mod | 1 + caddy/go.sum | 2 + dev.Dockerfile | 8 ++ go.mod | 7 +- go.sum | 6 +- watcher.go | 168 ++++++++++++++-------------------------- watcher_options.go | 32 +++----- watcher_options_test.go | 52 +++++-------- watcher_test.go | 2 +- 9 files changed, 107 insertions(+), 171 deletions(-) diff --git a/caddy/go.mod b/caddy/go.mod index be9b993b5..289e8cc7b 100644 --- a/caddy/go.mod +++ b/caddy/go.mod @@ -48,6 +48,7 @@ require ( github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dolthub/maphash v0.1.0 // indirect + github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 // indirect github.com/dunglas/httpsfv v1.0.2 // indirect github.com/dunglas/mercure v0.16.3 // indirect github.com/dunglas/vulcain v1.0.5 // indirect diff --git a/caddy/go.sum b/caddy/go.sum index 5a1b21201..c3c820600 100644 --- a/caddy/go.sum +++ b/caddy/go.sum @@ -141,6 +141,8 @@ github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/dunglas/caddy-cbrotli v1.0.0 h1:+WNqXBkWyMcIpXB2rVZ3nwcElUbuAzf0kPxNXU4D+u0= github.com/dunglas/caddy-cbrotli v1.0.0/go.mod h1:KZsUu3fnQBgO0o3YDoQuO3Z61dFgUncr1F8rg8acwQw= +github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 h1:F5jAHx1qL6uxK0NCyBZ5N3Agrf+HZITjtWW1Jwrdz84= +github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15/go.mod h1:7Yj67KBnUukcR0gbP9HPCmyUKGEQ2mzDI3rs2fLZl0s= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dunglas/mercure v0.16.3 h1:zDEBFpvV61SlJnJYhFM87GKB4c2F4zdOKfs/xnrw/7Y= diff --git a/dev.Dockerfile b/dev.Dockerfile index 3e7142fed..a782916d2 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -63,6 +63,14 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ echo "opcache.enable=1" >> /usr/local/lib/php.ini && \ php --version +RUN wget https://github.com/emcrisostomo/fswatch/releases/download/1.17.1/fswatch-1.17.1.tar.gz && \ + tar xzf fswatch-1.17.1.tar.gz && \ + cd fswatch-* && \ + ./configure && \ + make && \ + make install && \ + ldconfig + WORKDIR /go/src/app COPY . . diff --git a/go.mod b/go.mod index 7db757599..d17b6154a 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,13 @@ module github.com/dunglas/frankenphp -go 1.21 +go 1.22.0 -toolchain go1.22.0 +toolchain go1.22.6 retract v1.0.0-rc.1 // Human error require ( - github.com/fsnotify/fsnotify v1.7.0 + github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 github.com/maypok86/otter v1.2.2 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 @@ -22,7 +22,6 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 80a706244..74b1a598f 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ 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/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 h1:F5jAHx1qL6uxK0NCyBZ5N3Agrf+HZITjtWW1Jwrdz84= +github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15/go.mod h1:7Yj67KBnUukcR0gbP9HPCmyUKGEQ2mzDI3rs2fLZl0s= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -32,8 +32,6 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/watcher.go b/watcher.go index cafb58976..37bda26cb 100644 --- a/watcher.go +++ b/watcher.go @@ -1,37 +1,43 @@ package frankenphp import ( - "github.com/fsnotify/fsnotify" + fswatch "github.com/dunglas/go-fswatch" "go.uber.org/zap" - "path/filepath" - "strings" - "os" "time" "sync/atomic" "sync" ) -// sometimes multiple fs events fire at once so we'll wait a few ms before reloading -const debounceDuration = 300 -var watcher *fsnotify.Watcher -var watcherMu sync.RWMutex -var isReloadingWorkers atomic.Bool +type watchEvent struct { + events []fswatch.Event + watchOpt watchOpt +} + +// sometimes multiple events fire at once so we'll wait a few ms before reloading +const debounceDuration = 150 +// latency of the watcher in seconds +const watcherLatency = 0.1 + +var ( + watchSessions []*fswatch.Session + watcherMu sync.RWMutex + isReloadingWorkers atomic.Bool + fileEventChannel = make(chan watchEvent) +) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { if(len(watchOpts) == 0) { return nil } - fsWatcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - watcherMu.Lock() - watcher = fsWatcher; - watcherMu.Unlock() - if err := addWatchedDirectories(watchOpts); err != nil { - logger.Error("failed to watch directories") - return err + watchSessions := make([]*fswatch.Session, len(watchOpts)) + for i, watchOpt := range watchOpts { + session, err := createSession(watchOpt) + if(err != nil) { + return err + } + watchSessions[i] = session + go session.Start() } go listenForFileChanges(watchOpts, workerOpts) @@ -39,116 +45,60 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { return nil; } +func createSession(watchOpt watchOpt) (*fswatch.Session, error) { + eventTypeFilters := []fswatch.EventType{ + fswatch.Created, + fswatch.Updated, + fswatch.Renamed, + fswatch.Removed, + } + // Todo: allow more fine grained control over the options + opts := []fswatch.Option{ + fswatch.WithRecursive(watchOpt.isRecursive), + fswatch.WithFollowSymlinks(false), + fswatch.WithEventTypeFilters(eventTypeFilters), + fswatch.WithLatency(0.01), + } + return fswatch.NewSession([]string{watchOpt.dirName}, registerFileEvent(watchOpt), opts...) +} + func stopWatcher() { - if(watcher != nil) { - watcherMu.RLock() - watcher.Close() - watcherMu.RUnlock() + logger.Info("stopping watcher") + for _, session := range watchSessions { + session.Destroy() } } func listenForFileChanges(watchOpts []watchOpt, workerOpts []workerOpt) { - watcherMu.RLock() - events := watcher.Events - errors := watcher.Errors - watcherMu.RUnlock() for { select { - case event, ok := <-events: - if !ok { - logger.Error("unexpected watcher event") - return - } - watchCreatedDirectories(event, watchOpts) - if isReloadingWorkers.Load() || !fileMatchesPattern(event.Name, watchOpts) { - continue - } - isReloadingWorkers.Store(true) - logger.Info("filesystem change detected", zap.String("event", event.Name)) - go reloadWorkers(workerOpts) - - case err, ok := <-errors: - if !ok { - return + case watchEvent := <-fileEventChannel: + for _, event := range watchEvent.events { + handleFileEvent(event, watchEvent.watchOpt, workerOpts) } - logger.Error("watcher: error:", zap.Error(err)) + case <-done: + logger.Info("stopping watcher") + return } } } -func addWatchedDirectories(watchOpts []watchOpt) error { - for _, watchOpt := range watchOpts { - logger.Debug("watching for changes", zap.String("dir", watchOpt.dirName), zap.String("pattern", watchOpt.pattern), zap.Bool("recursive", watchOpt.isRecursive)) - if(!watchOpt.isRecursive) { - watcherMu.RLock() - err := watcher.Add(watchOpt.dirName) - watcherMu.RUnlock() - if(err != nil) { - return err - } - continue - } - if err := watchRecursively(watchOpt.dirName); err != nil { - return err - } +func registerFileEvent(watchOpt watchOpt) func([]fswatch.Event) { + return func(events []fswatch.Event) { + fileEventChannel <- watchEvent{events,watchOpt} } - - return nil } -func watchRecursively(dir string) error { - fileInfo, err := os.Stat(dir) - if err != nil { - return err - } - if !fileInfo.IsDir() { - watcherMu.RLock() - defer watcherMu.RUnlock() - return watcher.Add(dir) - } - if err := filepath.Walk(dir, watchFile); err != nil { - return err; - } - - return nil -} - -func watchCreatedDirectories(event fsnotify.Event, watchOpts []watchOpt) { - if !event.Has(fsnotify.Create) { - return - } - fileInfo, err := os.Stat(event.Name) - if err != nil { - logger.Error("unable to stat file", zap.Error(err)) +func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []workerOpt) { + if isReloadingWorkers.Load() || !fileMatchesPattern(event.Path, watchOpt) { return } - if !fileInfo.IsDir() { - return - } - for _, watchOpt := range watchOpts { - if(watchOpt.isRecursive && strings.HasPrefix(event.Name, watchOpt.dirName)) { - logger.Debug("watching new dir", zap.String("dir", event.Name)) - watcherMu.RLock() - err := watcher.Add(event.Name) - if(err != nil) { - logger.Error("failed to watch new dir", zap.Error(err)) - } - watcherMu.RLock() - } - } - + isReloadingWorkers.Store(true) + logger.Info("filesystem change detected", zap.String("path", event.Path)) + go reloadWorkers(workerOpts) } -func watchFile(path string, fi os.FileInfo, err error) error { - // ignore paths that start with a dot (like .git) - if fi.Mode().IsDir() && !strings.HasPrefix(filepath.Base(fi.Name()), ".") { - watcherMu.RLock() - defer watcherMu.RUnlock() - return watcher.Add(path) - } - return nil -} func reloadWorkers(workerOpts []workerOpt) { <-time.After(time.Millisecond * time.Duration(debounceDuration)) diff --git a/watcher_options.go b/watcher_options.go index 0c03feb84..ea01b0f77 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -36,26 +36,18 @@ func createWatchOption(fileName string) (watchOpt, error) { return watchOpt, nil } -func fileMatchesPattern(fileName string, watchOpts []watchOpt) bool { - for _, watchOpt := range watchOpts { - if !strings.HasPrefix(fileName, watchOpt.dirName) { - continue - } - if(!watchOpt.isRecursive && filepath.Dir(fileName) != watchOpt.dirName) { - continue - } - if watchOpt.pattern == "" { - return true - } - baseName := filepath.Base(fileName) - patternMatches, err := filepath.Match(watchOpt.pattern, baseName) - if(err != nil) { - logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) - continue - } - if(patternMatches){ - return true - } +func fileMatchesPattern(fileName string, watchOpt watchOpt) bool { + if watchOpt.pattern == "" { + return true + } + baseName := filepath.Base(fileName) + patternMatches, err := filepath.Match(watchOpt.pattern, baseName) + if(err != nil) { + logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) + return false + } + if(patternMatches){ + return true } return false } \ No newline at end of file diff --git a/watcher_options_test.go b/watcher_options_test.go index b8ba7b7d5..70a1907ff 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -63,53 +63,39 @@ func TestRelativePathname(t *testing.T) { assert.True(t, watchOpt.isRecursive) } -func TestShouldWatchWithoutPattern(t *testing.T) { +func TestPatternShouldMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" - wOpt := watchOpt{pattern: "", dirName: "/some/path", isRecursive: false} - watchOpts := []watchOpt{wOpt} + wOpt := watchOpt{pattern: "*.php", dirName: "/some/path", isRecursive: true} - assert.True(t, fileMatchesPattern(fileName, watchOpts)) + assert.True(t, fileMatchesPattern(fileName, wOpt)) } -func TestShouldNotWatchBecauseOfNoRecursion(t *testing.T) { - const fileName = "/some/path/sub-path/watch-me.php" - wOpt := watchOpt{pattern: ".php", dirName: "/some/path", isRecursive: false} - watchOpts := []watchOpt{wOpt} +func TestPatternShouldMatchExactly(t *testing.T) { + const fileName = "/some/path/watch-me.php" + wOpt := watchOpt{pattern: "watch-me.php", dirName: "/some/path", isRecursive: true} - assert.False(t, fileMatchesPattern(fileName, watchOpts)) + assert.True(t, fileMatchesPattern(fileName, wOpt)) } -func TestShouldWatchBecauseOfRecursion(t *testing.T) { - const fileName = "/some/path/sub-path/watch-me.php" - wOpt := watchOpt{pattern: "", dirName: "/some/path", isRecursive: true} - watchOpts := []watchOpt{wOpt} +func TestPatternShouldNotMatch(t *testing.T) { + const fileName = "/some/path/watch-me.php" + wOpt := watchOpt{pattern: "*.json", dirName: "/some/path", isRecursive: true} - assert.True(t, fileMatchesPattern(fileName, watchOpts)) + assert.False(t, fileMatchesPattern(fileName, wOpt)) } -func TestShouldWatchBecauseOfPatters(t *testing.T) { - const fileName = "/some/path/sub-path/watch-me.php" - wOpt := watchOpt{pattern: "*.php", dirName: "/some/path", isRecursive: true} - watchOpts := []watchOpt{wOpt} +func TestPatternShouldNotMatchExactly(t *testing.T) { + const fileName = "/some/path/watch-me.php" + wOpt := watchOpt{pattern: "watch-me-too.php", dirName: "/some/path", isRecursive: true} - assert.True(t, fileMatchesPattern(fileName, watchOpts)) + assert.False(t, fileMatchesPattern(fileName, wOpt)) } -func TestShouldNotWatchBecauseOfPattern(t *testing.T) { - const fileName = "/some/path/sub-path/watch-me.php" - wOpt := watchOpt{pattern: "*.json", dirName: "/some/path", isRecursive: true} - watchOpts := []watchOpt{wOpt} +func TestEmptyPatternShouldAlwaysMatch(t *testing.T) { + const fileName = "/some/path/watch-me.php" + wOpt := watchOpt{pattern: "", dirName: "/some/path", isRecursive: true} - assert.False(t, fileMatchesPattern(fileName, watchOpts)) + assert.True(t, fileMatchesPattern(fileName, wOpt)) } -func TestShouldMatchWithMultipleWatchOptions(t *testing.T) { - const fileName = "/third/path/watch-me.php" - wOpt1 := watchOpt{pattern: "*.php", dirName: "/first/path", isRecursive: true} - wOpt2 := watchOpt{pattern: "*.php", dirName: "/second/path", isRecursive: true} - wOpt3 := watchOpt{pattern: "*.php", dirName: "/third/path", isRecursive: true} - watchOpts := []watchOpt{wOpt1,wOpt2,wOpt3} - - assert.True(t, fileMatchesPattern(fileName, watchOpts)) -} diff --git a/watcher_test.go b/watcher_test.go index 623e01774..66ab89d5a 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -12,7 +12,7 @@ import ( ) // we have to wait a few milliseconds for the worker debounce to take effect -const debounceMilliseconds = 500 +const debounceMilliseconds = 1000 func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { From 8cac13b8824cea38d5b6e29da5874411abee42ba Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 3 Sep 2024 10:08:27 +0200 Subject: [PATCH 014/155] Stops watcher before stopping workers. --- frankenphp.go | 5 +---- worker.go | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index bbc5ce6c8..0ecc02efe 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -338,15 +338,12 @@ func Init(options ...Option) error { // Shutdown stops the workers and the PHP runtime. func Shutdown() { - stopWorkers() stopWatcher() + stopWorkers() close(done) shutdownWG.Wait() requestChan = nil - // Always reset the WaitGroup to ensure we're in a clean state - workersReadyWG = sync.WaitGroup{} - // Remove the installed app if EmbeddedAppPath != "" { os.RemoveAll(EmbeddedAppPath) diff --git a/worker.go b/worker.go index c1a377236..1c26e3a8d 100644 --- a/worker.go +++ b/worker.go @@ -141,6 +141,9 @@ func stopWorkers() { close(workersDone) } workerShutdownWG.Wait() + // Always reset the WaitGroup to ensure we're in a clean state + workersReadyWG = sync.WaitGroup{} + workerShutdownWG = sync.WaitGroup{} } //export go_frankenphp_worker_ready From 8a66165f7496c7fa3b1e5922a5814187fb422390 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 3 Sep 2024 10:08:35 +0200 Subject: [PATCH 015/155] Updates docs. --- docs/config.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index c876512ad..1556ae12a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -161,7 +161,8 @@ You can also add multiple `watch` directives Be sure not to include files that are created at runtime (like logs) into you watcher, since they might cause unwanted worker restarts. -The file watcher is based on [fsnotify](https://github.com/fsnotify/fsnotify). +The file watcher is based on [go-fswatch](https://github.com/dunglas/go-fswatch) and shares the limitations of the +underlying fswatch library. ### Full Duplex (HTTP/1) From d8de3ee766e3158c59bacad01ac041f4f9004b18 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 3 Sep 2024 10:09:16 +0200 Subject: [PATCH 016/155] Avoids segfault in tests. --- watcher.go | 40 ++++++++++++++++++++++++++-------------- watcher_options.go | 5 +---- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/watcher.go b/watcher.go index 37bda26cb..655a190df 100644 --- a/watcher.go +++ b/watcher.go @@ -5,7 +5,6 @@ import ( "go.uber.org/zap" "time" "sync/atomic" - "sync" ) type watchEvent struct { @@ -14,21 +13,22 @@ type watchEvent struct { } // sometimes multiple events fire at once so we'll wait a few ms before reloading -const debounceDuration = 150 +const debounceDuration = 100 // latency of the watcher in seconds const watcherLatency = 0.1 var ( - watchSessions []*fswatch.Session - watcherMu sync.RWMutex - isReloadingWorkers atomic.Bool - fileEventChannel = make(chan watchEvent) + watchSessions []*fswatch.Session + blockReloading atomic.Bool + fileEventChannel chan watchEvent + watcherDone chan interface{} ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { - if(len(watchOpts) == 0) { + if(len(watchOpts) == 0 || len(workerOpts) == 0) { return nil } + blockReloading.Store(true) watchSessions := make([]*fswatch.Session, len(watchOpts)) for i, watchOpt := range watchOpts { @@ -39,9 +39,14 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { watchSessions[i] = session go session.Start() } + watcherDone = make(chan interface{} ) + fileEventChannel = make(chan watchEvent) + for _, session := range watchSessions { + go session.Start() + } go listenForFileChanges(watchOpts, workerOpts) - + blockReloading.Store(false) return nil; } @@ -63,9 +68,17 @@ func createSession(watchOpt watchOpt) (*fswatch.Session, error) { } func stopWatcher() { + if watcherDone == nil { + return + } logger.Info("stopping watcher") + blockReloading.Store(true) + close(watcherDone) + watcherDone = nil for _, session := range watchSessions { - session.Destroy() + if err := session.Destroy(); err != nil { + panic(err) + } } } @@ -76,8 +89,7 @@ func listenForFileChanges(watchOpts []watchOpt, workerOpts []workerOpt) { for _, event := range watchEvent.events { handleFileEvent(event, watchEvent.watchOpt, workerOpts) } - case <-done: - logger.Info("stopping watcher") + case <-watcherDone: return } } @@ -91,10 +103,10 @@ func registerFileEvent(watchOpt watchOpt) func([]fswatch.Event) { } func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []workerOpt) { - if isReloadingWorkers.Load() || !fileMatchesPattern(event.Path, watchOpt) { + if blockReloading.Load() || !fileMatchesPattern(event.Path, watchOpt) { return } - isReloadingWorkers.Store(true) + blockReloading.Store(true) logger.Info("filesystem change detected", zap.String("path", event.Path)) go reloadWorkers(workerOpts) } @@ -111,7 +123,7 @@ func reloadWorkers(workerOpts []workerOpt) { } logger.Info("workers restarted successfully") - isReloadingWorkers.Store(false) + blockReloading.Store(false) } diff --git a/watcher_options.go b/watcher_options.go index ea01b0f77..d89763a71 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -46,8 +46,5 @@ func fileMatchesPattern(fileName string, watchOpt watchOpt) bool { logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) return false } - if(patternMatches){ - return true - } - return false + return patternMatches } \ No newline at end of file From 3d7d8496d9f94f6dc6ade3998f0855d15ef8eb62 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 3 Sep 2024 10:52:17 +0200 Subject: [PATCH 017/155] Fixes watcher segmentation violations on shutdown. --- testdata/worker-with-watcher.php | 4 +- watcher.go | 71 +++++++++++--------------------- watcher_test.go | 8 ++-- 3 files changed, 30 insertions(+), 53 deletions(-) diff --git a/testdata/worker-with-watcher.php b/testdata/worker-with-watcher.php index 7db14b496..248cf469f 100644 --- a/testdata/worker-with-watcher.php +++ b/testdata/worker-with-watcher.php @@ -1,11 +1,11 @@ Date: Tue, 3 Sep 2024 13:21:56 +0200 Subject: [PATCH 018/155] Adjusts watcher latencies and tests. --- watcher.go | 12 +++++++----- watcher_options.go | 5 +++-- watcher_options_test.go | 11 +++++++++++ watcher_test.go | 15 +++++---------- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/watcher.go b/watcher.go index da73506a3..dbd2167b0 100644 --- a/watcher.go +++ b/watcher.go @@ -5,10 +5,11 @@ import ( "go.uber.org/zap" "sync/atomic" "sync" + "time" ) -// latency of the watcher in seconds -const watcherLatency = 0.15 +// latency of the watcher in milliseconds +const watcherLatency = 150 var ( watchSessions []*fswatch.Session @@ -52,7 +53,7 @@ func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, fswatch.WithRecursive(watchOpt.isRecursive), fswatch.WithFollowSymlinks(false), fswatch.WithEventTypeFilters(eventTypeFilters), - fswatch.WithLatency(0.01), + fswatch.WithLatency(watcherLatency / 1000), } return fswatch.NewSession([]string{watchOpt.dirName}, registerFileEvent(watchOpt, workerOpts), opts...) } @@ -79,10 +80,9 @@ func registerFileEvent(watchOpt watchOpt, workerOpts []workerOpt) func([]fswatch } func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []workerOpt) bool { - if !fileMatchesPattern(event.Path, watchOpt) || blockReloading.Load() { + if !fileMatchesPattern(event.Path, watchOpt) || !blockReloading.CompareAndSwap(false, true) { return false } - blockReloading.Store(true) reloadWaitGroup.Add(1) logger.Info("filesystem change detected", zap.String("path", event.Path)) go reloadWorkers(workerOpts) @@ -92,6 +92,8 @@ func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []worker func reloadWorkers(workerOpts []workerOpt) { logger.Info("restarting workers due to file changes...") + // we'll be giving the reload process a grace period + time.Sleep(watcherLatency * time.Millisecond) stopWorkers() if err := initWorkers(workerOpts); err != nil { logger.Error("failed to restart workers when watching files") diff --git a/watcher_options.go b/watcher_options.go index d89763a71..1d7891591 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -21,8 +21,9 @@ func createWatchOption(fileName string) (watchOpt, error) { watchOpt.isRecursive = false } - if(strings.Contains(fileName, "/**")) { - watchOpt.dirName = strings.Split(fileName, "/**")[0] + if(strings.Contains(fileName, "/**/")) { + watchOpt.dirName = strings.Split(fileName, "/**/")[0] + watchOpt.pattern = strings.Split(fileName, "/**/")[1] watchOpt.isRecursive = true } diff --git a/watcher_options_test.go b/watcher_options_test.go index 70a1907ff..d23b1e65c 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -63,6 +63,17 @@ func TestRelativePathname(t *testing.T) { assert.True(t, watchOpt.isRecursive) } +func TestMatchLiteralFilePattern(t *testing.T) { + const fileName = "/some/path/**/fileName" + + watchOpt, err := createWatchOption(fileName) + + assert.Nil(t, err) + assert.Equal(t, "fileName", watchOpt.pattern) + assert.Equal(t, "/some/path", watchOpt.dirName) + assert.True(t, watchOpt.isRecursive) +} + func TestPatternShouldMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" wOpt := watchOpt{pattern: "*.php", dirName: "/some/path", isRecursive: true} diff --git a/watcher_test.go b/watcher_test.go index c2bac5f65..569e6d6a0 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -12,7 +12,7 @@ import ( ) // we have to wait a few milliseconds for the watcher debounce to take effect -const timeToWaitForChanges = 600 +const timeToWaitForChanges = 1500 func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { @@ -65,16 +65,11 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt } func updateTestFile(fileName string, content string){ - dirName := filepath.Dir(fileName) - if _, err := os.Stat(dirName); os.IsNotExist(err) { - err = os.MkdirAll(dirName, 0700) - if(err != nil) { - panic(err) - } - } bytes := []byte(content) - err := os.WriteFile(fileName, bytes, 0644) - if(err != nil) { + if err := os.WriteFile(fileName, bytes, 0644); err != nil { panic(err) } + if err := os.Remove(fileName); err != nil { + panic(err) + } } \ No newline at end of file From 41d337b1708ade76fd796cb40784c3ade88822f6 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 3 Sep 2024 14:03:42 +0200 Subject: [PATCH 019/155] Adds fswatch to dockerfiles --- Dockerfile | 11 +++++++++++ alpine.Dockerfile | 10 ++++++++++ dev-alpine.Dockerfile | 8 ++++++++ dev.Dockerfile | 14 +++++++------- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2e70cb9d2..12c1f18af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -86,6 +86,14 @@ COPY --link caddy caddy COPY --link internal internal COPY --link testdata testdata +# install fswatch (necessary for file watching) +ARG FSWATCH_VERSION='1.17.1' +WORKDIR /usr/local/src/fswatch +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ + tar xzf fswatch.tar.gz +WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION +RUN ./configure && make && make install && ldconfig + # See https://github.com/docker-library/php/blob/master/8.3/bookworm/zts/Dockerfile#L57-L59 for PHP values ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" ENV CGO_CPPFLAGS=$PHP_CPPFLAGS @@ -104,6 +112,9 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 +COPY --from=builder /usr/local/lib/libfswatch.so* /usr/local/lib/ +RUN ldconfig + COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \ frankenphp version diff --git a/alpine.Dockerfile b/alpine.Dockerfile index d9d4f02b2..d1eadd7fd 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -104,6 +104,14 @@ COPY --link caddy caddy COPY --link internal internal COPY --link testdata testdata +# install fswatch (necessary for file watching) +ARG FSWATCH_VERSION='1.17.1' +WORKDIR /usr/local/src/fswatch +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ + tar xzf fswatch.tar.gz +WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION +RUN ./configure && make && make install && ldconfig /usr/local/lib && fswatch --version + # See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55 ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" ENV CGO_CPPFLAGS=$PHP_CPPFLAGS @@ -122,6 +130,8 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 +COPY --from=builder /usr/local/lib/libfswatch.so* /usr/local/lib/ + COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \ frankenphp version diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index f630ef68f..ace2b1395 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -58,6 +58,14 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ echo "opcache.enable=1" >> /usr/local/lib/php.ini && \ php --version +# install fswatch (necessary for file watching) +ARG FSWATCH_VERSION='1.17.1' +WORKDIR /usr/local/src/fswatch +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ + tar xzf fswatch.tar.gz +WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION +RUN ./configure && make && make install && ldconfig /usr/local/lib && fswatch --version + WORKDIR /go/src/app COPY . . diff --git a/dev.Dockerfile b/dev.Dockerfile index a782916d2..d37b193bf 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -63,13 +63,13 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ echo "opcache.enable=1" >> /usr/local/lib/php.ini && \ php --version -RUN wget https://github.com/emcrisostomo/fswatch/releases/download/1.17.1/fswatch-1.17.1.tar.gz && \ - tar xzf fswatch-1.17.1.tar.gz && \ - cd fswatch-* && \ - ./configure && \ - make && \ - make install && \ - ldconfig +# install fswatch (necessary for file watching) +ARG FSWATCH_VERSION='1.17.1' +WORKDIR /usr/local/src/fswatch +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ + tar xzf fswatch.tar.gz +WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION +RUN ./configure && make && make install && ldconfig && fswatch --version WORKDIR /go/src/app COPY . . From 639c07260840a1f87cd2c76384b7e5e65b6bf11e Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 4 Sep 2024 22:53:59 +0200 Subject: [PATCH 020/155] Fixes fswatch in alpine. --- alpine.Dockerfile | 6 ++++-- dev-alpine.Dockerfile | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/alpine.Dockerfile b/alpine.Dockerfile index d1eadd7fd..9f479d5fb 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -2,7 +2,7 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 #checkov:skip=CKV_DOCKER_7 -FROM php-base AS common +FROM php:zts-alpine AS common ARG TARGETARCH @@ -50,7 +50,7 @@ FROM common AS builder ARG FRANKENPHP_VERSION='dev' SHELL ["/bin/ash", "-eo", "pipefail", "-c"] -COPY --link --from=golang-base /usr/local/go /usr/local/go +COPY --link --from=golang:1.22-alpine /usr/local/go /usr/local/go ENV PATH=/usr/local/go/bin:$PATH @@ -131,6 +131,8 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 COPY --from=builder /usr/local/lib/libfswatch.so* /usr/local/lib/ +COPY --from=builder /usr/local/bin/fswatch /usr/local/bin/fswatch +RUN apk add libstdc++ #required for fswatch to work COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \ diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index ace2b1395..d80c23e39 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -61,8 +61,8 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ # install fswatch (necessary for file watching) ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ - tar xzf fswatch.tar.gz +RUN wget https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz && \ + tar xzf fswatch-$FSWATCH_VERSION.tar.gz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION RUN ./configure && make && make install && ldconfig /usr/local/lib && fswatch --version From f6ba8d70cf5c0e26aa24468fd6cbfc4503643bf8 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 00:11:46 +0200 Subject: [PATCH 021/155] Fixes segfault (this time for real). --- watcher.go | 11 +++++------ watcher_test.go | 7 +++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/watcher.go b/watcher.go index dbd2167b0..401729baf 100644 --- a/watcher.go +++ b/watcher.go @@ -5,7 +5,6 @@ import ( "go.uber.org/zap" "sync/atomic" "sync" - "time" ) // latency of the watcher in milliseconds @@ -29,7 +28,6 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { return err } watchSessions[i] = session - go session.Start() } for _, session := range watchSessions { @@ -61,12 +59,15 @@ func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, func stopWatcher() { logger.Info("stopping watcher") blockReloading.Store(true) - reloadWaitGroup.Wait() for _, session := range watchSessions { + if err := session.Stop(); err != nil { + logger.Error("failed to stop watcher") + } if err := session.Destroy(); err != nil { - panic(err) + logger.Error("failed to destroy watcher") } } + reloadWaitGroup.Wait() } func registerFileEvent(watchOpt watchOpt, workerOpts []workerOpt) func([]fswatch.Event) { @@ -92,8 +93,6 @@ func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []worker func reloadWorkers(workerOpts []workerOpt) { logger.Info("restarting workers due to file changes...") - // we'll be giving the reload process a grace period - time.Sleep(watcherLatency * time.Millisecond) stopWorkers() if err := initWorkers(workerOpts); err != nil { logger.Error("failed to restart workers when watching files") diff --git a/watcher_test.go b/watcher_test.go index 569e6d6a0..f06ba8ed9 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -65,6 +65,13 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt } func updateTestFile(fileName string, content string){ + dirName := filepath.Dir(fileName) + if _, err := os.Stat(dirName); os.IsNotExist(err) { + err = os.MkdirAll(dirName, 0700) + if(err != nil) { + panic(err) + } + } bytes := []byte(content) if err := os.WriteFile(fileName, bytes, 0644); err != nil { panic(err) From f6f4a9c5d0ff1cb33ff02f8bcb04d7eed68f3d76 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 10:17:45 +0200 Subject: [PATCH 022/155] Allows queueing new reload if file changes while workers are reloading. --- watcher.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/watcher.go b/watcher.go index 401729baf..1c1c0b06e 100644 --- a/watcher.go +++ b/watcher.go @@ -57,6 +57,9 @@ func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, } func stopWatcher() { + if(len(watchSessions) == 0) { + return + } logger.Info("stopping watcher") blockReloading.Store(true) for _, session := range watchSessions { @@ -84,6 +87,7 @@ func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []worker if !fileMatchesPattern(event.Path, watchOpt) || !blockReloading.CompareAndSwap(false, true) { return false } + reloadWaitGroup.Wait() reloadWaitGroup.Add(1) logger.Info("filesystem change detected", zap.String("path", event.Path)) go reloadWorkers(workerOpts) @@ -94,13 +98,13 @@ func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []worker func reloadWorkers(workerOpts []workerOpt) { logger.Info("restarting workers due to file changes...") stopWorkers() + blockReloading.Store(false) if err := initWorkers(workerOpts); err != nil { logger.Error("failed to restart workers when watching files") panic(err) } logger.Info("workers restarted successfully") - blockReloading.Store(false) reloadWaitGroup.Done() } From 5e345f1880151496b86001fd3359aebf4c497320 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 10:18:05 +0200 Subject: [PATCH 023/155] Makes tests more consistent. --- watcher_test.go | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/watcher_test.go b/watcher_test.go index f06ba8ed9..a254299f6 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -12,7 +12,9 @@ import ( ) // we have to wait a few milliseconds for the watcher debounce to take effect -const timeToWaitForChanges = 1500 +const pollingTime = 150 +const minTimesToPollForChanges = 5 +const maxTimesToPollForChanges = 100 // we will poll a maximum of 100x150ms = 15s func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { @@ -24,18 +26,14 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .txt file does not cause a reload - absPath, err := filepath.Abs("./testdata/files/test.txt") - updateTestFile(absPath, "updated") - time.Sleep(timeToWaitForChanges * time.Millisecond) - body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Nil(t, err) - assert.Equal(t, "requests:1", body) + requestBodyHasReset := pollForWorkerReset(handler, maxTimesToPollForChanges) + assert.True(t, requestBodyHasReset) }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) } func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.txt" + const filePattern = "./testdata/**/*.php" runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { // first we verify that the worker is working correctly @@ -43,12 +41,9 @@ func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .json file does not cause a reload - absPath, err := filepath.Abs("./testdata/files/test.json") - updateTestFile(absPath, "{updated:true}") - time.Sleep(timeToWaitForChanges * time.Millisecond) - body = fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Nil(t, err) - assert.Equal(t, "requests:2", body) + requestBodyHasReset := pollForWorkerReset(handler, minTimesToPollForChanges) + + assert.False(t, requestBodyHasReset) }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) } @@ -64,8 +59,24 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt return string(body) } +func pollForWorkerReset(handler func(http.ResponseWriter, *http.Request), limit int) bool{ + for i := 0; i < limit; i++ { + updateTestFile("./testdata/files/test.txt", "updated") + time.Sleep(pollingTime * time.Millisecond) + body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) + if(body == "requests:1") { + return true; + } + } + return false; +} + func updateTestFile(fileName string, content string){ - dirName := filepath.Dir(fileName) + absFileName, err := filepath.Abs(fileName) + if err != nil { + panic(err) + } + dirName := filepath.Dir(absFileName) if _, err := os.Stat(dirName); os.IsNotExist(err) { err = os.MkdirAll(dirName, 0700) if(err != nil) { @@ -73,10 +84,7 @@ func updateTestFile(fileName string, content string){ } } bytes := []byte(content) - if err := os.WriteFile(fileName, bytes, 0644); err != nil { + if err := os.WriteFile(absFileName, bytes, 0644); err != nil { panic(err) } - if err := os.Remove(fileName); err != nil { - panic(err) - } } \ No newline at end of file From 0423bb9de11803f9b06a9bc9d807774855eed68f Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 10:40:20 +0200 Subject: [PATCH 024/155] Prevents the watcher from getting stuck if there is an error in the worker file itself. --- worker.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worker.go b/worker.go index 1c26e3a8d..9e684dbfc 100644 --- a/worker.go +++ b/worker.go @@ -10,6 +10,7 @@ import ( "path/filepath" "runtime/cgo" "sync" + "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -98,12 +99,14 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { // TODO: make the max restart configurable if _, ok := workersRequestChans.Load(absFileName); ok { - workersReadyWG.Add(1) if fc.exitStatus == 0 { + workersReadyWG.Add(1) if c := l.Check(zapcore.InfoLevel, "restarting"); c != nil { c.Write(zap.String("worker", absFileName)) } } else { + // we will wait a few milliseconds to not overwhelm the logger in case of repeated unexpected terminations + time.Sleep(50 * time.Millisecond) if c := l.Check(zapcore.ErrorLevel, "unexpected termination, restarting"); c != nil { c.Write(zap.String("worker", absFileName), zap.Int("exit_status", int(fc.exitStatus))) } From 1db309b9b32d57a4924f456dc0e7a3b445ca7481 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 10:44:57 +0200 Subject: [PATCH 025/155] Reverts changing the image. --- alpine.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 9f479d5fb..68e2b1c82 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -2,7 +2,7 @@ #checkov:skip=CKV_DOCKER_2 #checkov:skip=CKV_DOCKER_3 #checkov:skip=CKV_DOCKER_7 -FROM php:zts-alpine AS common +FROM php-base AS common ARG TARGETARCH @@ -50,7 +50,7 @@ FROM common AS builder ARG FRANKENPHP_VERSION='dev' SHELL ["/bin/ash", "-eo", "pipefail", "-c"] -COPY --link --from=golang:1.22-alpine /usr/local/go /usr/local/go +COPY --link --from=golang-base /usr/local/go /usr/local/go ENV PATH=/usr/local/go/bin:$PATH From c6591f08d34cdb2c2a3d7d04b32ce56bf6e1824f Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 11:24:24 +0200 Subject: [PATCH 026/155] Puts fswatch version into docker-bake.hcl. --- Dockerfile | 2 +- alpine.Dockerfile | 2 +- docker-bake.hcl | 6 ++++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 12c1f18af..76108226e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -87,7 +87,7 @@ COPY --link internal internal COPY --link testdata testdata # install fswatch (necessary for file watching) -ARG FSWATCH_VERSION='1.17.1' +ARG FSWATCH_VERSION WORKDIR /usr/local/src/fswatch RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ tar xzf fswatch.tar.gz diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 68e2b1c82..86b719cf5 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -105,7 +105,7 @@ COPY --link internal internal COPY --link testdata testdata # install fswatch (necessary for file watching) -ARG FSWATCH_VERSION='1.17.1' +ARG FSWATCH_VERSION WORKDIR /usr/local/src/fswatch RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ tar xzf fswatch.tar.gz diff --git a/docker-bake.hcl b/docker-bake.hcl index 693b45c24..15bc175fa 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -14,6 +14,10 @@ variable "GO_VERSION" { default = "1.22" } +variable FSWATCH_VERSION { + default = "1.17.1" +} + variable "SHA" {} variable "LATEST" { @@ -115,6 +119,7 @@ target "default" { } args = { FRANKENPHP_VERSION = VERSION + FSWATCH_VERSION = FSWATCH_VERSION } } @@ -140,6 +145,7 @@ target "static-builder" { } args = { FRANKENPHP_VERSION = VERSION + FSWATCH_VERSION = FSWATCH_VERSION } secret = ["id=github-token,env=GITHUB_TOKEN"] } From c946de956c59a372e746bd5918f96020062ffa60 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 11:24:47 +0200 Subject: [PATCH 027/155] Asserts instead of panicking. --- watcher_test.go | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/watcher_test.go b/watcher_test.go index a254299f6..b06705075 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -26,7 +26,7 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .txt file does not cause a reload - requestBodyHasReset := pollForWorkerReset(handler, maxTimesToPollForChanges) + requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) assert.True(t, requestBodyHasReset) }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) @@ -41,7 +41,7 @@ func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { assert.Equal(t, "requests:1", body) // now we verify that updating a .json file does not cause a reload - requestBodyHasReset := pollForWorkerReset(handler, minTimesToPollForChanges) + requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) assert.False(t, requestBodyHasReset) @@ -59,9 +59,9 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt return string(body) } -func pollForWorkerReset(handler func(http.ResponseWriter, *http.Request), limit int) bool{ +func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool{ for i := 0; i < limit; i++ { - updateTestFile("./testdata/files/test.txt", "updated") + updateTestFile("./testdata/files/test.txt", "updated", t) time.Sleep(pollingTime * time.Millisecond) body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) if(body == "requests:1") { @@ -71,20 +71,15 @@ func pollForWorkerReset(handler func(http.ResponseWriter, *http.Request), limit return false; } -func updateTestFile(fileName string, content string){ +func updateTestFile(fileName string, content string, t *testing.T){ absFileName, err := filepath.Abs(fileName) - if err != nil { - panic(err) - } + assert.NoError(t, err) dirName := filepath.Dir(absFileName) if _, err := os.Stat(dirName); os.IsNotExist(err) { err = os.MkdirAll(dirName, 0700) - if(err != nil) { - panic(err) - } + assert.NoError(t, err) } bytes := []byte(content) - if err := os.WriteFile(absFileName, bytes, 0644); err != nil { - panic(err) - } + err = os.WriteFile(absFileName, bytes, 0644) + assert.NoError(t, err) } \ No newline at end of file From 0d4327d3f7ee216ded12c2e1de067da942133928 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:30:51 +0200 Subject: [PATCH 028/155] Adds notice MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 12c1f18af..e792c6b02 100644 --- a/Dockerfile +++ b/Dockerfile @@ -88,6 +88,7 @@ COPY --link testdata testdata # install fswatch (necessary for file watching) ARG FSWATCH_VERSION='1.17.1' +# in the future, we may replace this custom compilation by the installation of https://packages.debian.org/bookworm/fswatch, which provides the library and headers, but the version currently shipped by Debian is too old WORKDIR /usr/local/src/fswatch RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ tar xzf fswatch.tar.gz From 70461de3fa938b2b9f7070ebbcceccc58ace62a2 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:33:21 +0200 Subject: [PATCH 029/155] Update dev.Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- dev.Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev.Dockerfile b/dev.Dockerfile index d37b193bf..52d6b7e11 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -66,8 +66,7 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ # install fswatch (necessary for file watching) ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ - tar xzf fswatch.tar.gz +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION RUN ./configure && make && make install && ldconfig && fswatch --version From 6fcedfb4ca3714f688a358f71ef2bec7e41feae3 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:33:46 +0200 Subject: [PATCH 030/155] Update Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e792c6b02..037db70d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,8 +90,7 @@ COPY --link testdata testdata ARG FSWATCH_VERSION='1.17.1' # in the future, we may replace this custom compilation by the installation of https://packages.debian.org/bookworm/fswatch, which provides the library and headers, but the version currently shipped by Debian is too old WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ - tar xzf fswatch.tar.gz +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION RUN ./configure && make && make install && ldconfig From d8e0a2ce0eca0798b8c69842d748191a8979283a Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:34:13 +0200 Subject: [PATCH 031/155] Update Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- Dockerfile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 037db70d6..d0047862d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -92,7 +92,10 @@ ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && make && make install && ldconfig +RUN ./configure && \ + make -j"$(nproc)" && \ + make install && \ + ldconfig # See https://github.com/docker-library/php/blob/master/8.3/bookworm/zts/Dockerfile#L57-L59 for PHP values ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" From 9df79f3baf00c401113a4f277a0e78752adfd31f Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:34:28 +0200 Subject: [PATCH 032/155] Update alpine.Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- alpine.Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 9f479d5fb..713f11a70 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -107,8 +107,7 @@ COPY --link testdata testdata # install fswatch (necessary for file watching) ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz > fswatch.tar.gz && \ - tar xzf fswatch.tar.gz +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION RUN ./configure && make && make install && ldconfig /usr/local/lib && fswatch --version From d8fb28c6a73671ac5814a91ad603153f19ec9a33 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:34:57 +0200 Subject: [PATCH 033/155] Update alpine.Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- alpine.Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 713f11a70..85792e5f2 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -109,7 +109,11 @@ ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && make && make install && ldconfig /usr/local/lib && fswatch --version +RUN ./configure && \ + make -j"$(nproc)" && \ + make install && \ + ldconfig /usr/local/lib && \ + fswatch --version # See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55 ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" From d66588d790bace02b1b350a58007ed4ab892123a Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:35:09 +0200 Subject: [PATCH 034/155] Update dev-alpine.Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- dev-alpine.Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index d80c23e39..df771c3ff 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -61,8 +61,7 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ # install fswatch (necessary for file watching) ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch -RUN wget https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz && \ - tar xzf fswatch-$FSWATCH_VERSION.tar.gz +RUN wget -o https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION RUN ./configure && make && make install && ldconfig /usr/local/lib && fswatch --version From 53456f7c30e8ae2cacef2a83e26a7723e623b302 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:35:23 +0200 Subject: [PATCH 035/155] Update dev-alpine.Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- dev-alpine.Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index df771c3ff..9ded73d5a 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -63,7 +63,11 @@ ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch RUN wget -o https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && make && make install && ldconfig /usr/local/lib && fswatch --version +RUN ./configure && \ + make -j"$(nproc)" && \ + make install && \ + ldconfig /usr/local/lib && \ + fswatch --version WORKDIR /go/src/app COPY . . From c61e42cb65c5094136cf067372f95d92f94e65eb Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:36:27 +0200 Subject: [PATCH 036/155] Update dev.Dockerfile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- dev.Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dev.Dockerfile b/dev.Dockerfile index 52d6b7e11..5b608ece2 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -68,7 +68,11 @@ ARG FSWATCH_VERSION='1.17.1' WORKDIR /usr/local/src/fswatch RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && make && make install && ldconfig && fswatch --version +RUN ./configure && \ + make -j"$(nproc)" && \ + make install && \ + ldconfig && \ + fswatch --version WORKDIR /go/src/app COPY . . From c0836b975c7005864e1eddb90872eeaa8ba5a5e5 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:36:58 +0200 Subject: [PATCH 037/155] Update docs/config.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 1556ae12a..23b5e657f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -131,7 +131,7 @@ php_server [] { } ``` -### Watching for file changes +### Watching for File Changes Since workers won't restart automatically on file changes you can also define a number of directories that should be watched. This is useful for From d87f3ff4d4309819af1302bd0cf2f527d57523a2 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 11:39:03 +0200 Subject: [PATCH 038/155] Runs fswatch version. --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 944fdbce2..020f153a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -95,7 +95,8 @@ WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION RUN ./configure && \ make -j"$(nproc)" && \ make install && \ - ldconfig + ldconfig && \ + fswatch --version # See https://github.com/docker-library/php/blob/master/8.3/bookworm/zts/Dockerfile#L57-L59 for PHP values ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" From 0254b33ce2ad34a93cc7f449faca78f8e1f63950 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 11:39:55 +0200 Subject: [PATCH 039/155] Removes .json. --- testdata/files/.gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/testdata/files/.gitignore b/testdata/files/.gitignore index 88428ddd0..314f02b1b 100644 --- a/testdata/files/.gitignore +++ b/testdata/files/.gitignore @@ -1,2 +1 @@ -*.json *.txt \ No newline at end of file From 1fb4bf2c537172b08c8906f360ccd42378db37e5 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 11:44:22 +0200 Subject: [PATCH 040/155] Replaces ms with s. --- watcher.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/watcher.go b/watcher.go index 1c1c0b06e..a91910baf 100644 --- a/watcher.go +++ b/watcher.go @@ -7,8 +7,8 @@ import ( "sync" ) -// latency of the watcher in milliseconds -const watcherLatency = 150 +// latency of the watcher in seconds +const watcherLatency = 0.15 var ( watchSessions []*fswatch.Session @@ -51,7 +51,7 @@ func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, fswatch.WithRecursive(watchOpt.isRecursive), fswatch.WithFollowSymlinks(false), fswatch.WithEventTypeFilters(eventTypeFilters), - fswatch.WithLatency(watcherLatency / 1000), + fswatch.WithLatency(watcherLatency), } return fswatch.NewSession([]string{watchOpt.dirName}, registerFileEvent(watchOpt, workerOpts), opts...) } From 665fa285c62370d235812fdbd6406fb9f730c3c1 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 11:44:59 +0200 Subject: [PATCH 041/155] Resets the channel after closing it. --- worker.go | 1 + 1 file changed, 1 insertion(+) diff --git a/worker.go b/worker.go index 9e684dbfc..6f3267e8d 100644 --- a/worker.go +++ b/worker.go @@ -142,6 +142,7 @@ func stopWorkers() { }) if(workersDone != nil) { close(workersDone) + workersDone = nil; } workerShutdownWG.Wait() // Always reset the WaitGroup to ensure we're in a clean state From 255c1c537b9a10f707d531fae80dbff2fbe8bb6e Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:48:30 +0200 Subject: [PATCH 042/155] Update watcher_options.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- watcher_options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher_options.go b/watcher_options.go index 1d7891591..4b5060a8c 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -48,4 +48,4 @@ func fileMatchesPattern(fileName string, watchOpt watchOpt) bool { return false } return patternMatches -} \ No newline at end of file +} From 9ec89dc1a53a5a3b164f068f5d6b448fadfb6c06 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:48:55 +0200 Subject: [PATCH 043/155] Update watcher_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- watcher_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher_test.go b/watcher_test.go index a254299f6..73e593cbf 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -87,4 +87,4 @@ func updateTestFile(fileName string, content string){ if err := os.WriteFile(absFileName, bytes, 0644); err != nil { panic(err) } -} \ No newline at end of file +} From 420b125333c13076ab08266ada9a6bad02d56ee1 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 11:51:01 +0200 Subject: [PATCH 044/155] Asserts no error instead. --- watcher_options_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/watcher_options_test.go b/watcher_options_test.go index d23b1e65c..7a8534c9a 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -11,7 +11,7 @@ func TestSimpleRecursiveWatchOption(t *testing.T) { watchOpt, err := createWatchOption(fileName) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "", watchOpt.pattern) assert.Equal(t, "/some/path", watchOpt.dirName) assert.True(t, watchOpt.isRecursive) @@ -22,7 +22,7 @@ func TestSingleFileWatchOption(t *testing.T) { watchOpt, err := createWatchOption(fileName) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "watch-me.json", watchOpt.pattern) assert.Equal(t, "/some/path", watchOpt.dirName) assert.False(t, watchOpt.isRecursive) @@ -33,7 +33,7 @@ func TestNonRecursivePatternWatchOption(t *testing.T) { watchOpt, err := createWatchOption(fileName) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "*.json", watchOpt.pattern) assert.Equal(t, "/some/path", watchOpt.dirName) assert.False(t, watchOpt.isRecursive) @@ -44,7 +44,7 @@ func TestRecursivePatternWatchOption(t *testing.T) { watchOpt, err := createWatchOption(fileName) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "*.json", watchOpt.pattern) assert.Equal(t, "/some/path", watchOpt.dirName) assert.True(t, watchOpt.isRecursive) @@ -56,8 +56,8 @@ func TestRelativePathname(t *testing.T) { watchOpt, err2 := createWatchOption(fileName) - assert.Nil(t, err1) - assert.Nil(t, err2) + assert.NoError(t, err1) + assert.NoError(t, err2) assert.Equal(t, "*.txt", watchOpt.pattern) assert.Equal(t, absPath, watchOpt.dirName) assert.True(t, watchOpt.isRecursive) @@ -68,7 +68,7 @@ func TestMatchLiteralFilePattern(t *testing.T) { watchOpt, err := createWatchOption(fileName) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, "fileName", watchOpt.pattern) assert.Equal(t, "/some/path", watchOpt.dirName) assert.True(t, watchOpt.isRecursive) From e0e4bea99742329b2b08b63bbdd38ae18a5619d0 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 13:22:39 +0200 Subject: [PATCH 045/155] Fixes a race condition where events are fired after frankenphp has stopped. --- watcher.go | 21 +++++++++++++++------ worker.go | 1 - 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/watcher.go b/watcher.go index a91910baf..dd98949f6 100644 --- a/watcher.go +++ b/watcher.go @@ -12,8 +12,12 @@ const watcherLatency = 0.15 var ( watchSessions []*fswatch.Session + // we block reloading until workers have stopped blockReloading atomic.Bool + // when stopping the watcher we need to wait for reloading to finish reloadWaitGroup sync.WaitGroup + // the integrity ensures rouge events are ignored + watchIntegrity atomic.Int32 ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { @@ -21,6 +25,7 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { return nil } + watchIntegrity.Store(watchIntegrity.Load() + 1) watchSessions := make([]*fswatch.Session, len(watchOpts)) for i, watchOpt := range watchOpts { session, err := createSession(watchOpt, workerOpts) @@ -34,8 +39,8 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { go session.Start() } - blockReloading.Store(false) reloadWaitGroup = sync.WaitGroup{} + blockReloading.Store(false) return nil; } @@ -53,7 +58,8 @@ func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, fswatch.WithEventTypeFilters(eventTypeFilters), fswatch.WithLatency(watcherLatency), } - return fswatch.NewSession([]string{watchOpt.dirName}, registerFileEvent(watchOpt, workerOpts), opts...) + handleFileEvent := registerFileEvent(watchOpt, workerOpts, watchIntegrity.Load()) + return fswatch.NewSession([]string{watchOpt.dirName}, handleFileEvent, opts...) } func stopWatcher() { @@ -62,6 +68,7 @@ func stopWatcher() { } logger.Info("stopping watcher") blockReloading.Store(true) + watchIntegrity.Store(watchIntegrity.Load() + 1) for _, session := range watchSessions { if err := session.Stop(); err != nil { logger.Error("failed to stop watcher") @@ -73,21 +80,23 @@ func stopWatcher() { reloadWaitGroup.Wait() } -func registerFileEvent(watchOpt watchOpt, workerOpts []workerOpt) func([]fswatch.Event) { +func registerFileEvent(watchOpt watchOpt, workerOpts []workerOpt, integrity int32) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { - if (handleFileEvent(event, watchOpt, workerOpts)){ + if (handleFileEvent(event, watchOpt, workerOpts, integrity)){ return } } } } -func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []workerOpt) bool { +func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []workerOpt, integrity int32) bool { if !fileMatchesPattern(event.Path, watchOpt) || !blockReloading.CompareAndSwap(false, true) { return false } - reloadWaitGroup.Wait() + if(integrity != watchIntegrity.Load()) { + return false + } reloadWaitGroup.Add(1) logger.Info("filesystem change detected", zap.String("path", event.Path)) go reloadWorkers(workerOpts) diff --git a/worker.go b/worker.go index 6f3267e8d..9e684dbfc 100644 --- a/worker.go +++ b/worker.go @@ -142,7 +142,6 @@ func stopWorkers() { }) if(workersDone != nil) { close(workersDone) - workersDone = nil; } workerShutdownWG.Wait() // Always reset the WaitGroup to ensure we're in a clean state From 77013f5e4d38334deb8a26a91599ccfa1f1aea52 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 13:27:33 +0200 Subject: [PATCH 046/155] Updates docs. --- docs/config.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/config.md b/docs/config.md index 23b5e657f..f0c9a4d9f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -161,8 +161,7 @@ You can also add multiple `watch` directives Be sure not to include files that are created at runtime (like logs) into you watcher, since they might cause unwanted worker restarts. -The file watcher is based on [go-fswatch](https://github.com/dunglas/go-fswatch) and shares the limitations of the -underlying fswatch library. +The file watcher is based on [fswatch](https://github.com/emcrisostomo/fswatch). ### Full Duplex (HTTP/1) From a03bd81ac725e753ea96c4487ed3edb90f24eeeb Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Thu, 5 Sep 2024 13:40:00 +0200 Subject: [PATCH 047/155] Update watcher_options_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- watcher_options_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher_options_test.go b/watcher_options_test.go index 7a8534c9a..f2fb3e70d 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -12,7 +12,7 @@ func TestSimpleRecursiveWatchOption(t *testing.T) { watchOpt, err := createWatchOption(fileName) assert.NoError(t, err) - assert.Equal(t, "", watchOpt.pattern) + assert.Empty(t, watchOpt.pattern) assert.Equal(t, "/some/path", watchOpt.dirName) assert.True(t, watchOpt.isRecursive) } From 054dd1f45c0e54cc2666dca41c76b75f0d6a825e Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 16:11:26 +0200 Subject: [PATCH 048/155] Allows queuing events while watchers are reloading. --- watcher.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/watcher.go b/watcher.go index dd98949f6..fe52e382a 100644 --- a/watcher.go +++ b/watcher.go @@ -71,8 +71,8 @@ func stopWatcher() { watchIntegrity.Store(watchIntegrity.Load() + 1) for _, session := range watchSessions { if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher") - } + logger.Error("failed to stop watcher") + } if err := session.Destroy(); err != nil { logger.Error("failed to destroy watcher") } @@ -94,19 +94,20 @@ func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []worker if !fileMatchesPattern(event.Path, watchOpt) || !blockReloading.CompareAndSwap(false, true) { return false } + reloadWaitGroup.Wait() if(integrity != watchIntegrity.Load()) { return false } reloadWaitGroup.Add(1) - logger.Info("filesystem change detected", zap.String("path", event.Path)) + logger.Info("filesystem change detected, restarting workers...", zap.String("path", event.Path)) go reloadWorkers(workerOpts) return true } func reloadWorkers(workerOpts []workerOpt) { - logger.Info("restarting workers due to file changes...") stopWorkers() + // reloads will be blocked until workers are stopped and queued while they are starting blockReloading.Store(false) if err := initWorkers(workerOpts); err != nil { logger.Error("failed to restart workers when watching files") From 36cab2460de2ef36b513044bd802340975d5adb2 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 20:12:40 +0200 Subject: [PATCH 049/155] go fmt --- frankenphp_test.go | 2 +- watcher_options.go | 10 +++++----- watcher_options_test.go | 4 +--- watcher_test.go | 22 ++++++++++------------ 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/frankenphp_test.go b/frankenphp_test.go index 0e553773e..4dcfcdef4 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -63,7 +63,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), * if opts.workerScript != "" { initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env)) } - if(opts.watch != "") { + if opts.watch != "" { initOpts = append(initOpts, frankenphp.WithFileWatcher(opts.watch)) } initOpts = append(initOpts, opts.initOpts...) diff --git a/watcher_options.go b/watcher_options.go index 4b5060a8c..170e03d66 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -7,21 +7,21 @@ import ( ) type watchOpt struct { - pattern string - dirName string + pattern string + dirName string isRecursive bool } func createWatchOption(fileName string) (watchOpt, error) { watchOpt := watchOpt{pattern: "", dirName: fileName, isRecursive: true} dirName, baseName := filepath.Split(watchOpt.dirName) - if(strings.Contains(baseName, "*") || strings.Contains(baseName, ".")) { + if strings.Contains(baseName, "*") || strings.Contains(baseName, ".") { watchOpt.dirName = dirName watchOpt.pattern = baseName watchOpt.isRecursive = false } - if(strings.Contains(fileName, "/**/")) { + if strings.Contains(fileName, "/**/") { watchOpt.dirName = strings.Split(fileName, "/**/")[0] watchOpt.pattern = strings.Split(fileName, "/**/")[1] watchOpt.isRecursive = true @@ -43,7 +43,7 @@ func fileMatchesPattern(fileName string, watchOpt watchOpt) bool { } baseName := filepath.Base(fileName) patternMatches, err := filepath.Match(watchOpt.pattern, baseName) - if(err != nil) { + if err != nil { logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) return false } diff --git a/watcher_options_test.go b/watcher_options_test.go index f2fb3e70d..649768857 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -2,8 +2,8 @@ package frankenphp import ( "github.com/stretchr/testify/assert" - "testing" "path/filepath" + "testing" ) func TestSimpleRecursiveWatchOption(t *testing.T) { @@ -108,5 +108,3 @@ func TestEmptyPatternShouldAlwaysMatch(t *testing.T) { assert.True(t, fileMatchesPattern(fileName, wOpt)) } - - diff --git a/watcher_test.go b/watcher_test.go index 356ff8e7b..928f7e9dd 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -1,14 +1,14 @@ package frankenphp_test import ( + "github.com/stretchr/testify/assert" "io" "net/http" "net/http/httptest" - "testing" "os" - "time" "path/filepath" - "github.com/stretchr/testify/assert" + "testing" + "time" ) // we have to wait a few milliseconds for the watcher debounce to take effect @@ -16,7 +16,6 @@ const pollingTime = 150 const minTimesToPollForChanges = 5 const maxTimesToPollForChanges = 100 // we will poll a maximum of 100x150ms = 15s - func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { const filePattern = "./testdata/**/*.txt" @@ -29,7 +28,7 @@ func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) assert.True(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch: filePattern}) } func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { @@ -45,10 +44,9 @@ func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { assert.False(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch:filePattern}) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch: filePattern}) } - func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string { req := httptest.NewRequest(method, url, nil) w := httptest.NewRecorder() @@ -59,19 +57,19 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt return string(body) } -func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool{ +func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool { for i := 0; i < limit; i++ { updateTestFile("./testdata/files/test.txt", "updated", t) time.Sleep(pollingTime * time.Millisecond) body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - if(body == "requests:1") { - return true; + if body == "requests:1" { + return true } } - return false; + return false } -func updateTestFile(fileName string, content string, t *testing.T){ +func updateTestFile(fileName string, content string, t *testing.T) { absFileName, err := filepath.Abs(fileName) assert.NoError(t, err) dirName := filepath.Dir(absFileName) From 94b2723efb59b390b2a63120d0c355a8d610dc8c Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 5 Sep 2024 20:23:16 +0200 Subject: [PATCH 050/155] Refactors stopping and draining logic. --- frankenphp.go | 12 ++++++++---- watcher.go | 53 +++++++++++++++++++++++---------------------------- worker.go | 25 +++++++++++++++++------- 3 files changed, 50 insertions(+), 40 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 0ecc02efe..5260da583 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -338,10 +338,9 @@ func Init(options ...Option) error { // Shutdown stops the workers and the PHP runtime. func Shutdown() { - stopWatcher() - stopWorkers() - close(done) - shutdownWG.Wait() + drainWatcher() + drainWorkers() + drainThreads() requestChan = nil // Remove the installed app @@ -357,6 +356,11 @@ func go_shutdown() { shutdownWG.Done() } +func drainThreads() { + close(done) + shutdownWG.Wait() +} + func getLogger() *zap.Logger { loggerMu.RLock() defer loggerMu.RUnlock() diff --git a/watcher.go b/watcher.go index fe52e382a..1524644cf 100644 --- a/watcher.go +++ b/watcher.go @@ -3,25 +3,25 @@ package frankenphp import ( fswatch "github.com/dunglas/go-fswatch" "go.uber.org/zap" - "sync/atomic" "sync" + "sync/atomic" ) // latency of the watcher in seconds const watcherLatency = 0.15 var ( - watchSessions []*fswatch.Session + watchSessions []*fswatch.Session // we block reloading until workers have stopped - blockReloading atomic.Bool + blockReloading atomic.Bool // when stopping the watcher we need to wait for reloading to finish - reloadWaitGroup sync.WaitGroup + reloadWaitGroup sync.WaitGroup // the integrity ensures rouge events are ignored - watchIntegrity atomic.Int32 + watchIntegrity atomic.Int32 ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { - if(len(watchOpts) == 0 || len(workerOpts) == 0) { + if len(watchOpts) == 0 || len(workerOpts) == 0 { return nil } @@ -29,7 +29,7 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { watchSessions := make([]*fswatch.Session, len(watchOpts)) for i, watchOpt := range watchOpts { session, err := createSession(watchOpt, workerOpts) - if(err != nil) { + if err != nil { return err } watchSessions[i] = session @@ -41,7 +41,7 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { reloadWaitGroup = sync.WaitGroup{} blockReloading.Store(false) - return nil; + return nil } func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, error) { @@ -62,11 +62,16 @@ func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, return fswatch.NewSession([]string{watchOpt.dirName}, handleFileEvent, opts...) } -func stopWatcher() { - if(len(watchSessions) == 0) { +func drainWatcher() { + if len(watchSessions) == 0 { return } logger.Info("stopping watcher") + stopWatcher() + reloadWaitGroup.Wait() +} + +func stopWatcher() { blockReloading.Store(true) watchIntegrity.Store(watchIntegrity.Load() + 1) for _, session := range watchSessions { @@ -77,14 +82,13 @@ func stopWatcher() { logger.Error("failed to destroy watcher") } } - reloadWaitGroup.Wait() } func registerFileEvent(watchOpt watchOpt, workerOpts []workerOpt, integrity int32) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { - if (handleFileEvent(event, watchOpt, workerOpts, integrity)){ - return + if handleFileEvent(event, watchOpt, workerOpts, integrity) { + break } } } @@ -95,27 +99,18 @@ func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []worker return false } reloadWaitGroup.Wait() - if(integrity != watchIntegrity.Load()) { + if integrity != watchIntegrity.Load() { return false } - reloadWaitGroup.Add(1) + logger.Info("filesystem change detected, restarting workers...", zap.String("path", event.Path)) - go reloadWorkers(workerOpts) + go triggerWorkerReload(workerOpts) return true } - -func reloadWorkers(workerOpts []workerOpt) { - stopWorkers() - // reloads will be blocked until workers are stopped and queued while they are starting - blockReloading.Store(false) - if err := initWorkers(workerOpts); err != nil { - logger.Error("failed to restart workers when watching files") - panic(err) - } - - logger.Info("workers restarted successfully") +func triggerWorkerReload(workerOpts []workerOpt) { + reloadWaitGroup.Add(1) + restartWorkers(workerOpts) reloadWaitGroup.Done() + blockReloading.Store(false) } - - diff --git a/worker.go b/worker.go index 9e684dbfc..09669806c 100644 --- a/worker.go +++ b/worker.go @@ -20,7 +20,7 @@ var ( workersRequestChans sync.Map // map[fileName]chan *http.Request workersReadyWG sync.WaitGroup workerShutdownWG sync.WaitGroup - workersDone chan interface{} + workersDone chan interface{} ) // TODO: start all the worker in parallell to reduce the boot time @@ -51,7 +51,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { workersReadyWG.Add(nbWorkers) var ( - m sync.RWMutex + m sync.RWMutex errs []error ) @@ -140,13 +140,24 @@ func stopWorkers() { return true }) - if(workersDone != nil) { - close(workersDone) - } + close(workersDone) +} + +func drainWorkers() { + stopWorkers() workerShutdownWG.Wait() // Always reset the WaitGroup to ensure we're in a clean state - workersReadyWG = sync.WaitGroup{} - workerShutdownWG = sync.WaitGroup{} + workersReadyWG = sync.WaitGroup{} + workerShutdownWG = sync.WaitGroup{} +} + +func restartWorkers(workerOpts []workerOpt) { + drainWorkers() + if err := initWorkers(workerOpts); err != nil { + logger.Error("failed to restart workers when watching files") + panic(err) + } + logger.Info("workers restarted successfully") } //export go_frankenphp_worker_ready From 8044848ff3555b2c007809c5738143ee36b1b757 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 18:09:32 +0200 Subject: [PATCH 051/155] Allows extended watcher configuration with dirs, recursion, symlinks, case-sensitivity, latency, monitor types and regex. --- caddy/caddy.go | 12 ++- caddy/watch_config.go | 169 +++++++++++++++++++++++++++++++++++ frankenphp_test.go | 6 +- options.go | 14 +-- watcher.go | 65 ++++++-------- watcher_options.go | 188 ++++++++++++++++++++++++++++++++++----- watcher_options_test.go | 189 ++++++++++++++++++++++++++++++---------- watcher_test.go | 65 ++++++++++---- 8 files changed, 567 insertions(+), 141 deletions(-) create mode 100644 caddy/watch_config.go diff --git a/caddy/caddy.go b/caddy/caddy.go index 2b31a917c..9e9489941 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -67,7 +67,7 @@ type FrankenPHPApp struct { // Workers configures the worker scripts to start. Workers []workerConfig `json:"workers,omitempty"` // Directories to watch for changes - Watch []string `json:"watch,omitempty"` + Watch []watchConfig `json:"watch,omitempty"` } // CaddyModule returns the Caddy module information. @@ -86,8 +86,8 @@ func (f *FrankenPHPApp) Start() error { for _, w := range f.Workers { opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env)) } - for _, fileName := range f.Watch { - opts = append(opts, frankenphp.WithFileWatcher(fileName)) + for _, watchConfig := range f.Watch { + opts = applyWatchConfig(opts, watchConfig) } _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) { @@ -134,11 +134,9 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { f.NumThreads = v case "watch": - if !d.NextArg() { - return d.ArgErr() + if err:= parseWatchDirective(f, d); err != nil { + return err } - f.Watch = append(f.Watch, d.Val()) - case "worker": wc := workerConfig{} if d.NextArg() { diff --git a/caddy/watch_config.go b/caddy/watch_config.go new file mode 100644 index 000000000..ec4e2584e --- /dev/null +++ b/caddy/watch_config.go @@ -0,0 +1,169 @@ +package caddy + +import ( + "strconv" + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "github.com/dunglas/frankenphp" +) + +type watchConfig struct { + // FileName sets the path to the worker script. + Dirs []string `json:"dir,omitempty"` + // Determines whether the watcher should be recursive. + Recursive bool `json:"recursive,omitempty"` + // Determines whether the watcher should follow symlinks. + FollowSymlinks bool `json:"follow_symlinks,omitempty"` + // Determines whether the regex should be case sensitive. + CaseSensitive bool `json:"case_sensitive,omitempty"` + // Determines whether the regex should be extended. + ExtendedRegex bool `json:"extended_regex,omitempty"` + // Latency of the watcher in ms. + Latency int `json:"latency,omitempty"` + // Include only files matching this regex + IncludeFiles string `json:"include,omitempty"` + // Exclude files matching this regex (will exclude all if empty) + ExcludeFiles string `json:"exclude,omitempty"` + // Allowed: "default", "fsevents", "kqueue", "inotify", "windows", "poll", "fen" + MonitorType string `json:"monitor_type,omitempty"` + // Determines weather to use the wildcard shortform + IsShortForm bool `json:"shortform,omitempty"` +} + +func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { + if watchConfig.IsShortForm { + return append(opts, frankenphp.WithFileWatcher( + frankenphp.WithWatcherShortForm(watchConfig.Dirs[0]), + frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), + )) + } + return append(opts, frankenphp.WithFileWatcher( + frankenphp.WithWatcherDirs(watchConfig.Dirs), + frankenphp.WithWatcherRecursion(watchConfig.Recursive), + frankenphp.WithWatcherSymlinks(watchConfig.FollowSymlinks), + frankenphp.WithWatcherFilters(watchConfig.IncludeFiles, watchConfig.ExcludeFiles, watchConfig.CaseSensitive, watchConfig.ExtendedRegex), + frankenphp.WithWatcherLatency(watchConfig.Latency), + frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), + )) +} + +func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error{ + watchConfig := watchConfig{ + Recursive: true, + Latency: 150, + } + if d.NextArg() { + watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) + watchConfig.IsShortForm = true + } + + if d.NextArg() { + if err := verifyMonitorType(d.Val(), d); err != nil { + return err + } + watchConfig.MonitorType = d.Val() + } + + for d.NextBlock(1) { + v := d.Val() + switch v { + case "directory", "dir": + if !d.NextArg() { + return d.ArgErr() + } + watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) + case "recursive": + if !d.NextArg() { + watchConfig.Recursive = true + continue + } + v, err := strconv.ParseBool(d.Val()) + if err != nil { + return err + } + watchConfig.Recursive = v + case "follow_symlinks": + if !d.NextArg() { + watchConfig.FollowSymlinks = true + continue + } + v, err := strconv.ParseBool(d.Val()) + if err != nil { + return err + } + watchConfig.FollowSymlinks = v + case "latency": + if !d.NextArg() { + return d.ArgErr() + } + v, err := strconv.Atoi(d.Val()) + if err != nil { + return err + } + watchConfig.Latency = v + case "include", "include_files": + if !d.NextArg() { + return d.ArgErr() + } + watchConfig.IncludeFiles = d.Val() + case "exclude", "exclude_files": + if !d.NextArg() { + return d.ArgErr() + } + watchConfig.ExcludeFiles = d.Val() + case "case_sensitive": + if !d.NextArg() { + watchConfig.CaseSensitive = true + continue + } + v, err := strconv.ParseBool(d.Val()) + if err != nil { + return err + } + watchConfig.CaseSensitive = v + case "shortform": + if !d.NextArg() { + watchConfig.IsShortForm = true + continue + } + v, err := strconv.ParseBool(d.Val()) + if err != nil { + return err + } + watchConfig.IsShortForm = v + case "extended_regex": + if !d.NextArg() { + watchConfig.ExtendedRegex = true + continue + } + v, err := strconv.ParseBool(d.Val()) + if err != nil { + return err + } + watchConfig.ExtendedRegex = v + case "monitor_type", "monitor": + if !d.NextArg() { + return d.ArgErr() + } + if err := verifyMonitorType(d.Val(), d); err != nil { + return err + } + watchConfig.MonitorType = d.Val() + default: + return d.Errf("unknown watcher subdirective '%s'", v) + } + } + if(len(watchConfig.Dirs) == 0) { + return d.Err("The 'dir' argument must be specified for the watch directive") + } + f.Watch = append(f.Watch, watchConfig) + return nil +} + +func verifyMonitorType(monitorType string, d *caddyfile.Dispenser) error { + switch monitorType { + case "default", "system", "fsevents", "kqueue", "inotify", "windows", "poll", "fen": + return nil + default: + return d.Errf("unknown watcher monitor type '%s'", monitorType) + } +} \ No newline at end of file diff --git a/frankenphp_test.go b/frankenphp_test.go index 4dcfcdef4..9d1aad339 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -35,7 +35,7 @@ import ( type testOptions struct { workerScript string - watch string + watchOptions []frankenphp.WatchOption nbWorkers int env map[string]string nbParrallelRequests int @@ -63,8 +63,8 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), * if opts.workerScript != "" { initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env)) } - if opts.watch != "" { - initOpts = append(initOpts, frankenphp.WithFileWatcher(opts.watch)) + if len(opts.watchOptions) != 0 { + initOpts = append(initOpts, frankenphp.WithFileWatcher(opts.watchOptions...)) } initOpts = append(initOpts, opts.initOpts...) diff --git a/options.go b/options.go index 21abac368..6bbd94d8d 100644 --- a/options.go +++ b/options.go @@ -41,14 +41,16 @@ func WithWorkers(fileName string, num int, env map[string]string) Option { } } -// WithFileWatcher configures filesystem watching. -func WithFileWatcher(fileName string) Option { +// WithFileWatcher configures filesystem watchers. +func WithFileWatcher(wo ...WatchOption) Option { return func(o *opt) error { - watchOpt, err := createWatchOption(fileName) - - if(err == nil) { - o.watch = append(o.watch, watchOpt) + watchOpt := getDefaultWatchOpt() + for _, option := range wo { + if err := option(&watchOpt); err != nil { + return err + } } + o.watch = append(o.watch, watchOpt) return nil } diff --git a/watcher.go b/watcher.go index 1524644cf..905f260ed 100644 --- a/watcher.go +++ b/watcher.go @@ -7,17 +7,14 @@ import ( "sync/atomic" ) -// latency of the watcher in seconds -const watcherLatency = 0.15 - var ( watchSessions []*fswatch.Session // we block reloading until workers have stopped blockReloading atomic.Bool // when stopping the watcher we need to wait for reloading to finish reloadWaitGroup sync.WaitGroup - // the integrity ensures rouge events are ignored - watchIntegrity atomic.Int32 + // active watch options that need to be disabled on shutdown + activeWatchOpts []*watchOpt ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { @@ -25,17 +22,16 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { return nil } - watchIntegrity.Store(watchIntegrity.Load() + 1) watchSessions := make([]*fswatch.Session, len(watchOpts)) + activeWatchOpts = make([]*watchOpt, len(watchOpts)) for i, watchOpt := range watchOpts { - session, err := createSession(watchOpt, workerOpts) + session, err := createSession(&watchOpt, workerOpts) if err != nil { + logger.Error("unable to start watcher", zap.Strings("dirs", watchOpt.dirs)) return err } watchSessions[i] = session - } - - for _, session := range watchSessions { + activeWatchOpts[i] = &watchOpt go session.Start() } @@ -44,67 +40,58 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { return nil } -func createSession(watchOpt watchOpt, workerOpts []workerOpt) (*fswatch.Session, error) { - eventTypeFilters := []fswatch.EventType{ - fswatch.Created, - fswatch.Updated, - fswatch.Renamed, - fswatch.Removed, - } - // Todo: allow more fine grained control over the options +func createSession(watchOpt *watchOpt, workerOpts []workerOpt) (*fswatch.Session, error) { opts := []fswatch.Option{ fswatch.WithRecursive(watchOpt.isRecursive), - fswatch.WithFollowSymlinks(false), - fswatch.WithEventTypeFilters(eventTypeFilters), - fswatch.WithLatency(watcherLatency), + fswatch.WithFollowSymlinks(watchOpt.followSymlinks), + fswatch.WithEventTypeFilters(watchOpt.eventTypes), + fswatch.WithLatency(watchOpt.latency), + fswatch.WithMonitorType((fswatch.MonitorType)(watchOpt.monitorType)), + fswatch.WithFilters(watchOpt.filters), } - handleFileEvent := registerFileEvent(watchOpt, workerOpts, watchIntegrity.Load()) - return fswatch.NewSession([]string{watchOpt.dirName}, handleFileEvent, opts...) + handleFileEvent := registerFileEvent(watchOpt, workerOpts) + return fswatch.NewSession(watchOpt.dirs, handleFileEvent, opts...) } func drainWatcher() { - if len(watchSessions) == 0 { - return - } - logger.Info("stopping watcher") stopWatcher() reloadWaitGroup.Wait() } func stopWatcher() { + logger.Info("stopping watcher...") blockReloading.Store(true) - watchIntegrity.Store(watchIntegrity.Load() + 1) for _, session := range watchSessions { if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher") + logger.Error("failed to stop watcher", zap.Error(err)) } if err := session.Destroy(); err != nil { - logger.Error("failed to destroy watcher") + logger.Error("failed to destroy watcher", zap.Error(err)) } } + // we also need to deactivate the watchOpts to avoid a race condition in tests + for _, watchOpt := range activeWatchOpts { + watchOpt.isActive = false + } } -func registerFileEvent(watchOpt watchOpt, workerOpts []workerOpt, integrity int32) func([]fswatch.Event) { +func registerFileEvent(watchOpt *watchOpt, workerOpts []workerOpt) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { - if handleFileEvent(event, watchOpt, workerOpts, integrity) { + if handleFileEvent(event, watchOpt, workerOpts) { break } } } } -func handleFileEvent(event fswatch.Event, watchOpt watchOpt, workerOpts []workerOpt, integrity int32) bool { - if !fileMatchesPattern(event.Path, watchOpt) || !blockReloading.CompareAndSwap(false, true) { - return false - } - reloadWaitGroup.Wait() - if integrity != watchIntegrity.Load() { +func handleFileEvent(event fswatch.Event, watchOpt *watchOpt, workerOpts []workerOpt) bool { + if !watchOpt.allowReload(event.Path) || !blockReloading.CompareAndSwap(false, true) { return false } - logger.Info("filesystem change detected, restarting workers...", zap.String("path", event.Path)) go triggerWorkerReload(workerOpts) + return true } diff --git a/watcher_options.go b/watcher_options.go index 170e03d66..a7ab5a050 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -1,48 +1,190 @@ package frankenphp import ( + fswatch "github.com/dunglas/go-fswatch" "go.uber.org/zap" "path/filepath" "strings" + "time" ) +type WatchOption func(o *watchOpt) error + type watchOpt struct { - pattern string - dirName string - isRecursive bool + dirs []string + isRecursive bool + followSymlinks bool + isActive bool + latency float64 + wildCardPattern string + filters []fswatch.Filter + monitorType fswatch.MonitorType + eventTypes []fswatch.EventType } -func createWatchOption(fileName string) (watchOpt, error) { - watchOpt := watchOpt{pattern: "", dirName: fileName, isRecursive: true} - dirName, baseName := filepath.Split(watchOpt.dirName) - if strings.Contains(baseName, "*") || strings.Contains(baseName, ".") { - watchOpt.dirName = dirName - watchOpt.pattern = baseName - watchOpt.isRecursive = false +func getDefaultWatchOpt() watchOpt { + return watchOpt{ + isActive: true, + isRecursive: true, + latency: 0.15, + monitorType: fswatch.SystemDefaultMonitor, + eventTypes: parseEventTypes(), } +} - if strings.Contains(fileName, "/**/") { - watchOpt.dirName = strings.Split(fileName, "/**/")[0] - watchOpt.pattern = strings.Split(fileName, "/**/")[1] - watchOpt.isRecursive = true +func WithWatcherShortForm(fileName string) WatchOption { + return func(o *watchOpt) error { + return parseShortForm(o, fileName) } +} - absName, err := filepath.Abs(watchOpt.dirName) - if err != nil { - logger.Error("directory could not be watched", zap.String("dir", watchOpt.dirName), zap.Error(err)) - return watchOpt, err +func WithWatcherDirs(dirs []string) WatchOption { + return func(o *watchOpt) error { + for _, dir := range dirs { + absDir, err := parseAbsPath(dir) + if err != nil { + return err + } + o.dirs = append(o.dirs, absDir) + } + return nil + } +} + +func WithWatcherFilters(includeFiles string, excludeFiles string, caseSensitive bool, extendedRegex bool) WatchOption { + return func(o *watchOpt) error { + o.filters = parseFilters(includeFiles, excludeFiles, caseSensitive, extendedRegex) + return nil + } +} + +func WithWatcherLatency(latency int) WatchOption { + return func(o *watchOpt) error { + o.latency = (float64)(latency) * time.Millisecond.Seconds() + return nil + } +} + +func WithWatcherMonitorType(monitorType string) WatchOption { + return func(o *watchOpt) error { + o.monitorType = parseMonitorType(monitorType) + return nil + } +} + +func WithWatcherRecursion(withRecursion bool) WatchOption { + return func(o *watchOpt) error { + o.isRecursive = withRecursion + return nil + } +} + +func WithWatcherSymlinks(withSymlinks bool) WatchOption { + return func(o *watchOpt) error { + o.followSymlinks = withSymlinks + return nil } - watchOpt.dirName = absName +} + +func parseShortForm(watchOpt *watchOpt, fileName string) error { + watchOpt.isRecursive = true + dirName := fileName + splitDirName, baseName := filepath.Split(fileName) + if fileName != "." && fileName != ".." && strings.ContainsAny(baseName, "*.") { + dirName = splitDirName + watchOpt.wildCardPattern = baseName + watchOpt.isRecursive = false + } + + if strings.Contains(fileName, "/**/") { + dirName = strings.Split(fileName, "/**/")[0] + watchOpt.wildCardPattern = strings.Split(fileName, "/**/")[1] + watchOpt.isRecursive = true + } + + absDir, err := parseAbsPath(dirName) + if err != nil { + return err + } + watchOpt.dirs = []string{absDir} + return nil +} - return watchOpt, nil +func parseFilters(include string, exclude string, caseSensitive bool, extended bool) []fswatch.Filter { + filters := []fswatch.Filter{} + + if(include != "" && exclude == "") { + exclude = "\\." + } + + if(include != "") { + includeFilter := fswatch.Filter { + Text: include, + FilterType: fswatch.FilterInclude, + CaseSensitive: caseSensitive, + Extended: extended, + } + filters = append(filters, includeFilter) + } + + if(exclude != "") { + excludeFilter := fswatch.Filter { + Text: exclude, + FilterType: fswatch.FilterExclude, + CaseSensitive: caseSensitive, + Extended: extended, + } + filters = append(filters, excludeFilter) + } + return filters } -func fileMatchesPattern(fileName string, watchOpt watchOpt) bool { - if watchOpt.pattern == "" { +func parseMonitorType(monitorType string) fswatch.MonitorType { + switch monitorType { + case "fsevents": + return fswatch.FseventsMonitor + case "kqueue": + return fswatch.KqueueMonitor + case "inotify": + return fswatch.InotifyMonitor + case "windows": + return fswatch.WindowsMonitor + case "poll": + return fswatch.PollMonitor + case "fen": + return fswatch.FenMonitor + default: + return fswatch.SystemDefaultMonitor + } +} + +func parseEventTypes() []fswatch.EventType { + return []fswatch.EventType { + fswatch.Created, + fswatch.Updated, + fswatch.Renamed, + fswatch.Removed, + } +} + +func parseAbsPath(path string) (string, error) { + absDir, err := filepath.Abs(path) + if err != nil { + logger.Error("path could not be watched", zap.String("path", path), zap.Error(err)) + return "", err + } + return absDir, nil +} + +func (watchOpt *watchOpt) allowReload(fileName string) bool { + if(!watchOpt.isActive){ + return false + } + if watchOpt.wildCardPattern == "" { return true } baseName := filepath.Base(fileName) - patternMatches, err := filepath.Match(watchOpt.pattern, baseName) + patternMatches, err := filepath.Match(watchOpt.wildCardPattern, baseName) if err != nil { logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) return false diff --git a/watcher_options_test.go b/watcher_options_test.go index 649768857..76e856a95 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -2,109 +2,202 @@ package frankenphp import ( "github.com/stretchr/testify/assert" - "path/filepath" "testing" + fswatch "github.com/dunglas/go-fswatch" + "path/filepath" ) func TestSimpleRecursiveWatchOption(t *testing.T) { - const fileName = "/some/path" + const shortForm = "/some/path" - watchOpt, err := createWatchOption(fileName) + watchOpt := createFromShortForm(shortForm, t) - assert.NoError(t, err) - assert.Empty(t, watchOpt.pattern) - assert.Equal(t, "/some/path", watchOpt.dirName) + assert.Empty(t, watchOpt.wildCardPattern) + assert.Equal(t, "/some/path", watchOpt.dirs[0]) assert.True(t, watchOpt.isRecursive) } func TestSingleFileWatchOption(t *testing.T) { - const fileName = "/some/path/watch-me.json" + const shortForm = "/some/path/watch-me.json" - watchOpt, err := createWatchOption(fileName) + watchOpt := createFromShortForm(shortForm, t) - assert.NoError(t, err) - assert.Equal(t, "watch-me.json", watchOpt.pattern) - assert.Equal(t, "/some/path", watchOpt.dirName) + assert.Equal(t, "watch-me.json", watchOpt.wildCardPattern) + assert.Equal(t, "/some/path", watchOpt.dirs[0]) assert.False(t, watchOpt.isRecursive) } func TestNonRecursivePatternWatchOption(t *testing.T) { - const fileName = "/some/path/*.json" + const shortForm = "/some/path/*.json" - watchOpt, err := createWatchOption(fileName) + watchOpt := createFromShortForm(shortForm, t) - assert.NoError(t, err) - assert.Equal(t, "*.json", watchOpt.pattern) - assert.Equal(t, "/some/path", watchOpt.dirName) + assert.Equal(t, "*.json", watchOpt.wildCardPattern) + assert.Equal(t, "/some/path", watchOpt.dirs[0]) assert.False(t, watchOpt.isRecursive) } func TestRecursivePatternWatchOption(t *testing.T) { - const fileName = "/some/path/**/*.json" + const shortForm = "/some/path/**/*.json" - watchOpt, err := createWatchOption(fileName) + watchOpt := createFromShortForm(shortForm, t) - assert.NoError(t, err) - assert.Equal(t, "*.json", watchOpt.pattern) - assert.Equal(t, "/some/path", watchOpt.dirName) + assert.Equal(t, "*.json", watchOpt.wildCardPattern) + assert.Equal(t, "/some/path", watchOpt.dirs[0]) assert.True(t, watchOpt.isRecursive) } func TestRelativePathname(t *testing.T) { - const fileName = "../testdata/**/*.txt" - absPath, err1 := filepath.Abs("../testdata") + const shortForm = "../testdata/**/*.txt" + absPath, err := filepath.Abs("../testdata") - watchOpt, err2 := createWatchOption(fileName) + watchOpt := createFromShortForm(shortForm, t) - assert.NoError(t, err1) - assert.NoError(t, err2) - assert.Equal(t, "*.txt", watchOpt.pattern) - assert.Equal(t, absPath, watchOpt.dirName) + assert.NoError(t, err) + assert.Equal(t, "*.txt", watchOpt.wildCardPattern) + assert.Equal(t, absPath, watchOpt.dirs[0]) assert.True(t, watchOpt.isRecursive) } -func TestMatchLiteralFilePattern(t *testing.T) { - const fileName = "/some/path/**/fileName" +func TestCurrentRelativePath(t *testing.T) { + const shortForm = "." + absPath, err := filepath.Abs(shortForm) - watchOpt, err := createWatchOption(fileName) + watchOpt := createFromShortForm(shortForm, t) assert.NoError(t, err) - assert.Equal(t, "fileName", watchOpt.pattern) - assert.Equal(t, "/some/path", watchOpt.dirName) + assert.Equal(t, "", watchOpt.wildCardPattern) + assert.Equal(t, absPath, watchOpt.dirs[0]) assert.True(t, watchOpt.isRecursive) } -func TestPatternShouldMatch(t *testing.T) { +func TestMatchPatternWithoutExtension(t *testing.T) { + const shortForm = "/some/path/**/fileName" + + watchOpt := createFromShortForm(shortForm, t) + + assert.Equal(t, "fileName", watchOpt.wildCardPattern) + assert.Equal(t, "/some/path", watchOpt.dirs[0]) + assert.True(t, watchOpt.isRecursive) +} + +func TestAddingTwoFilePaths(t *testing.T) { + watchOpt := getDefaultWatchOpt() + applyFirstPath := WithWatcherDirs([]string{"/first/path"}) + applySecondPath := WithWatcherDirs([]string{"/second/path"}) + + err1 := applyFirstPath(&watchOpt) + err2 := applySecondPath(&watchOpt) + + assert.NoError(t, err1) + assert.NoError(t, err2) + assert.Equal(t, []string{"/first/path","/second/path"}, watchOpt.dirs) +} + +func TestAddingAnInclusionFilterWithDefaultForExclusion(t *testing.T) { + expectedInclusionFilter := fswatch.Filter{ + Text: "\\.php$", + FilterType: fswatch.FilterInclude, + CaseSensitive: true, + Extended: true, + } + expectedExclusionFilter := fswatch.Filter{ + Text: "\\.", + FilterType: fswatch.FilterExclude, + CaseSensitive: true, + Extended: true, + } + + watchOpt := createWithOption(WithWatcherFilters("\\.php$", "", true, true), t) + + assert.Equal(t, []fswatch.Filter{expectedInclusionFilter, expectedExclusionFilter}, watchOpt.filters) +} + +func TestWithExclusionFilter(t *testing.T) { + expectedExclusionFilter := fswatch.Filter{ + Text: "\\.php$", + FilterType: fswatch.FilterExclude, + CaseSensitive: false, + Extended: false, + } + + watchOpt := createWithOption(WithWatcherFilters("", "\\.php$", false, false), t) + + assert.Equal(t, []fswatch.Filter{expectedExclusionFilter}, watchOpt.filters) +} + +func TestWithPollMonitor(t *testing.T) { + watchOpt := createWithOption(WithWatcherMonitorType("poll"), t) + + assert.Equal(t, (int)(fswatch.PollMonitor), (int)(watchOpt.monitorType)) +} + +func TestWithSymlinks(t *testing.T) { + watchOpt := createWithOption(WithWatcherSymlinks(true), t) + + assert.True(t, watchOpt.followSymlinks) +} + +func TestWithoutRecursion(t *testing.T) { + watchOpt := createWithOption(WithWatcherRecursion(false), t) + + assert.False(t, watchOpt.isRecursive) +} + +func TestWithLatency(t *testing.T) { + watchOpt := createWithOption(WithWatcherLatency(500), t) + + assert.Equal(t, 0.5, watchOpt.latency) +} + +func TestAllowReloadOnMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" - wOpt := watchOpt{pattern: "*.php", dirName: "/some/path", isRecursive: true} + watchOpt := createFromShortForm("/some/path/**/*.php", t) - assert.True(t, fileMatchesPattern(fileName, wOpt)) + assert.True(t, watchOpt.allowReload(fileName)) } -func TestPatternShouldMatchExactly(t *testing.T) { +func TestAllowReloadOnExactMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" - wOpt := watchOpt{pattern: "watch-me.php", dirName: "/some/path", isRecursive: true} + watchOpt := createFromShortForm("/some/path/watch-me.php", t) - assert.True(t, fileMatchesPattern(fileName, wOpt)) + assert.True(t, watchOpt.allowReload(fileName)) } -func TestPatternShouldNotMatch(t *testing.T) { +func TestDisallowReload(t *testing.T) { const fileName = "/some/path/watch-me.php" - wOpt := watchOpt{pattern: "*.json", dirName: "/some/path", isRecursive: true} + watchOpt := createFromShortForm("/some/path/dont-watch.php", t) - assert.False(t, fileMatchesPattern(fileName, wOpt)) + assert.False(t, watchOpt.allowReload(fileName)) } -func TestPatternShouldNotMatchExactly(t *testing.T) { +func TestAllowReloadOnRecursiveDirectory(t *testing.T) { const fileName = "/some/path/watch-me.php" - wOpt := watchOpt{pattern: "watch-me-too.php", dirName: "/some/path", isRecursive: true} + watchOpt := createFromShortForm("/some", t) - assert.False(t, fileMatchesPattern(fileName, wOpt)) + assert.True(t, watchOpt.allowReload(fileName)) } -func TestEmptyPatternShouldAlwaysMatch(t *testing.T) { +func TestAllowReloadIfOptionIsNotAWildcard(t *testing.T) { const fileName = "/some/path/watch-me.php" - wOpt := watchOpt{pattern: "", dirName: "/some/path", isRecursive: true} + watchOpt := getDefaultWatchOpt() - assert.True(t, fileMatchesPattern(fileName, wOpt)) + assert.True(t, watchOpt.allowReload(fileName)) } + +func createFromShortForm(shortForm string, t *testing.T) watchOpt { + watchOpt := getDefaultWatchOpt() + applyOptions := WithWatcherShortForm(shortForm) + err := applyOptions(&watchOpt) + assert.NoError(t, err) + return watchOpt +} + +func createWithOption(applyOptions WatchOption, t *testing.T) watchOpt { + watchOpt := getDefaultWatchOpt() + + err := applyOptions(&watchOpt) + + assert.NoError(t, err) + return watchOpt +} \ No newline at end of file diff --git a/watcher_test.go b/watcher_test.go index 928f7e9dd..486c8040b 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "testing" "time" + "github.com/dunglas/frankenphp" ) // we have to wait a few milliseconds for the watcher debounce to take effect @@ -16,35 +17,64 @@ const pollingTime = 150 const minTimesToPollForChanges = 5 const maxTimesToPollForChanges = 100 // we will poll a maximum of 100x150ms = 15s -func TestWorkerShouldReloadOnMatchingPattern(t *testing.T) { +func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { const filePattern = "./testdata/**/*.txt" + watchOptions := []frankenphp.WatchOption{frankenphp.WithWatcherShortForm(filePattern)} runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { - // first we verify that the worker is working correctly - body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Equal(t, "requests:1", body) - - // now we verify that updating a .txt file does not cause a reload requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) - assert.True(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch: filePattern}) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) } -func TestWorkerShouldNotReloadOnNonMatchingPattern(t *testing.T) { +func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { const filePattern = "./testdata/**/*.php" + watchOptions := []frankenphp.WatchOption{frankenphp.WithWatcherShortForm(filePattern)} runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { - // first we verify that the worker is working correctly - body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Equal(t, "requests:1", body) - - // now we verify that updating a .json file does not cause a reload requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) + assert.False(t, requestBodyHasReset) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) +} +func TestWorkersReloadOnMatchingIncludedRegex(t *testing.T) { + const include = "\\.txt$" + watchOptions := []frankenphp.WatchOption{ + frankenphp.WithWatcherDirs([]string{"./testdata"}), + frankenphp.WithWatcherRecursion(true), + frankenphp.WithWatcherFilters(include, "", true, false), + } + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { + requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) + assert.True(t, requestBodyHasReset) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) +} + +func TestWorkersDoNotReloadOnExcludingRegex(t *testing.T) { + const exclude ="\\.txt$" + watchOptions := []frankenphp.WatchOption{ + frankenphp.WithWatcherDirs([]string{"./testdata"}), + frankenphp.WithWatcherRecursion(true), + frankenphp.WithWatcherFilters("", exclude, false, false), + } + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { + requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) assert.False(t, requestBodyHasReset) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) +} - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch: filePattern}) +func TestWorkerShouldReloadUsingPolling(t *testing.T) { + watchOptions := []frankenphp.WatchOption{ + frankenphp.WithWatcherDirs([]string{"./testdata/files"}), + frankenphp.WithWatcherMonitorType("poll"), + } + + runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { + requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) + assert.True(t, requestBodyHasReset) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) } func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string { @@ -58,6 +88,11 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt } func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool { + // first we make an initial request to start the request counter + body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) + assert.Equal(t, "requests:1", body) + + // now we spam file updates and check if the request counter resets for i := 0; i < limit; i++ { updateTestFile("./testdata/files/test.txt", "updated", t) time.Sleep(pollingTime * time.Millisecond) From 96c2a1192862bdfba7a88633141c15505d577f07 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 18:19:52 +0200 Subject: [PATCH 052/155] Updates docs. --- docs/config.md | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/docs/config.md b/docs/config.md index f0c9a4d9f..7a34923a5 100644 --- a/docs/config.md +++ b/docs/config.md @@ -141,26 +141,59 @@ development environments. { frankenphp { worker /path/to/app/public/worker.php - watch /path/to/app/sourcefiles + watch /path/to/app } } ``` -The configuration above will watch the `/path/to/app/sourcefiles` directory recursively. -You can also add multiple `watch` directives +The configuration above will watch the `/path/to/app` directory recursively. + +#### Watcher Shortform + +You can also add multiple `watch` directives and use simple wildcard patterns, the following is valid: ```caddyfile { frankenphp { - watch /path/to/app/folder1 # watches all subdirectories - watch /path/to/app/folder2/*.php # watches only php files in the app directory - watch /path/to/app/folder3/**/*.php # watches only php files in the app directory and subdirectories + watch /path/to/folder1 # watches all subdirectories + watch /path/to/folder2/*.php # watches only php files in the app directory + watch /path/to/folder3/**/*.php # watches only php files in the app directory and subdirectories + watch /path/to/folder4 poll # watches all subdirectories with the 'poll' monitor type } } ``` -Be sure not to include files that are created at runtime (like logs) into you watcher, since they might cause unwanted -worker restarts. +#### Watcher Longform + +It's also possible to pass a more verbose config, that uses fswatch's native regular expressions: + +```caddyfile +{ + frankenphp { + watch { + path /path/to/folder1 + path /path/to/folder2 + recursive true # watch subdirectories + follow_symlinks false # weather to follow symlinks + exclude \.log$ # regex, exclude all files ending with .log + include \system.log$ # regex, specifically include all files ending with system.log + case_sensitive false # use case sensitive regex + extended_regex false # use extended regex + monitor_type default # allowed: "default", "fsevents", "kqueue", "inotify", "windows", "poll", "fen" + delay 150 # delay of triggering file change events in ms + } + } +} +``` + +#### Some notes + +- ``include`` will only apply to excluded files +- If ``include`` is defined, exclude will default to '\.', excluding all directories and files containing a dot +- ``exclude`` currently does not work properly on [some linux systems](https://github.com/emcrisostomo/fswatch/issues/247) + since it sometimes excludes the watched directory itself +- Be wary about watching files that are created at runtime (like logs), since they might cause unwanted worker restarts. + The file watcher is based on [fswatch](https://github.com/emcrisostomo/fswatch). ### Full Duplex (HTTP/1) From 3de70defea2750459d1ea0f965d90ac1d7587af0 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 18:20:10 +0200 Subject: [PATCH 053/155] Adds TODOS. --- watcher.go | 1 + worker.go | 1 + 2 files changed, 2 insertions(+) diff --git a/watcher.go b/watcher.go index 905f260ed..5403b1ff2 100644 --- a/watcher.go +++ b/watcher.go @@ -8,6 +8,7 @@ import ( ) var ( + // TODO: combine session and watchOpt into a struct watchSessions []*fswatch.Session // we block reloading until workers have stopped blockReloading atomic.Bool diff --git a/worker.go b/worker.go index 09669806c..3b9e9030a 100644 --- a/worker.go +++ b/worker.go @@ -100,6 +100,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { // TODO: make the max restart configurable if _, ok := workersRequestChans.Load(absFileName); ok { if fc.exitStatus == 0 { + // TODO: the watcher will still sometimes get stuck if errors are thrown in the worker file workersReadyWG.Add(1) if c := l.Check(zapcore.InfoLevel, "restarting"); c != nil { c.Write(zap.String("worker", absFileName)) From 45a73ae005ede8297f2f31531cf7a32aaaa710e5 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 18:21:23 +0200 Subject: [PATCH 054/155] go fmt. --- watcher.go | 8 +-- watcher_options.go | 110 ++++++++++++++++++++-------------------- watcher_options_test.go | 40 +++++++-------- watcher_test.go | 6 +-- 4 files changed, 82 insertions(+), 82 deletions(-) diff --git a/watcher.go b/watcher.go index 5403b1ff2..44d615b9c 100644 --- a/watcher.go +++ b/watcher.go @@ -9,13 +9,13 @@ import ( var ( // TODO: combine session and watchOpt into a struct - watchSessions []*fswatch.Session + watchSessions []*fswatch.Session // we block reloading until workers have stopped - blockReloading atomic.Bool + blockReloading atomic.Bool // when stopping the watcher we need to wait for reloading to finish - reloadWaitGroup sync.WaitGroup + reloadWaitGroup sync.WaitGroup // active watch options that need to be disabled on shutdown - activeWatchOpts []*watchOpt + activeWatchOpts []*watchOpt ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { diff --git a/watcher_options.go b/watcher_options.go index a7ab5a050..a5b17b0e7 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -14,21 +14,21 @@ type watchOpt struct { dirs []string isRecursive bool followSymlinks bool - isActive bool + isActive bool latency float64 wildCardPattern string - filters []fswatch.Filter - monitorType fswatch.MonitorType - eventTypes []fswatch.EventType + filters []fswatch.Filter + monitorType fswatch.MonitorType + eventTypes []fswatch.EventType } func getDefaultWatchOpt() watchOpt { return watchOpt{ - isActive: true, + isActive: true, isRecursive: true, - latency: 0.15, + latency: 0.15, monitorType: fswatch.SystemDefaultMonitor, - eventTypes: parseEventTypes(), + eventTypes: parseEventTypes(), } } @@ -89,53 +89,53 @@ func WithWatcherSymlinks(withSymlinks bool) WatchOption { func parseShortForm(watchOpt *watchOpt, fileName string) error { watchOpt.isRecursive = true dirName := fileName - splitDirName, baseName := filepath.Split(fileName) - if fileName != "." && fileName != ".." && strings.ContainsAny(baseName, "*.") { - dirName = splitDirName - watchOpt.wildCardPattern = baseName - watchOpt.isRecursive = false - } - - if strings.Contains(fileName, "/**/") { - dirName = strings.Split(fileName, "/**/")[0] - watchOpt.wildCardPattern = strings.Split(fileName, "/**/")[1] - watchOpt.isRecursive = true - } - - absDir, err := parseAbsPath(dirName) - if err != nil { - return err - } - watchOpt.dirs = []string{absDir} - return nil + splitDirName, baseName := filepath.Split(fileName) + if fileName != "." && fileName != ".." && strings.ContainsAny(baseName, "*.") { + dirName = splitDirName + watchOpt.wildCardPattern = baseName + watchOpt.isRecursive = false + } + + if strings.Contains(fileName, "/**/") { + dirName = strings.Split(fileName, "/**/")[0] + watchOpt.wildCardPattern = strings.Split(fileName, "/**/")[1] + watchOpt.isRecursive = true + } + + absDir, err := parseAbsPath(dirName) + if err != nil { + return err + } + watchOpt.dirs = []string{absDir} + return nil } func parseFilters(include string, exclude string, caseSensitive bool, extended bool) []fswatch.Filter { filters := []fswatch.Filter{} - if(include != "" && exclude == "") { + if include != "" && exclude == "" { exclude = "\\." } - if(include != "") { - includeFilter := fswatch.Filter { - Text: include, - FilterType: fswatch.FilterInclude, - CaseSensitive: caseSensitive, - Extended: extended, - } - filters = append(filters, includeFilter) - } - - if(exclude != "") { - excludeFilter := fswatch.Filter { - Text: exclude, - FilterType: fswatch.FilterExclude, - CaseSensitive: caseSensitive, - Extended: extended, - } - filters = append(filters, excludeFilter) - } + if include != "" { + includeFilter := fswatch.Filter{ + Text: include, + FilterType: fswatch.FilterInclude, + CaseSensitive: caseSensitive, + Extended: extended, + } + filters = append(filters, includeFilter) + } + + if exclude != "" { + excludeFilter := fswatch.Filter{ + Text: exclude, + FilterType: fswatch.FilterExclude, + CaseSensitive: caseSensitive, + Extended: extended, + } + filters = append(filters, excludeFilter) + } return filters } @@ -146,9 +146,9 @@ func parseMonitorType(monitorType string) fswatch.MonitorType { case "kqueue": return fswatch.KqueueMonitor case "inotify": - return fswatch.InotifyMonitor - case "windows": - return fswatch.WindowsMonitor + return fswatch.InotifyMonitor + case "windows": + return fswatch.WindowsMonitor case "poll": return fswatch.PollMonitor case "fen": @@ -159,7 +159,7 @@ func parseMonitorType(monitorType string) fswatch.MonitorType { } func parseEventTypes() []fswatch.EventType { - return []fswatch.EventType { + return []fswatch.EventType{ fswatch.Created, fswatch.Updated, fswatch.Renamed, @@ -169,15 +169,15 @@ func parseEventTypes() []fswatch.EventType { func parseAbsPath(path string) (string, error) { absDir, err := filepath.Abs(path) - if err != nil { - logger.Error("path could not be watched", zap.String("path", path), zap.Error(err)) - return "", err - } + if err != nil { + logger.Error("path could not be watched", zap.String("path", path), zap.Error(err)) + return "", err + } return absDir, nil } func (watchOpt *watchOpt) allowReload(fileName string) bool { - if(!watchOpt.isActive){ + if !watchOpt.isActive { return false } if watchOpt.wildCardPattern == "" { diff --git a/watcher_options_test.go b/watcher_options_test.go index 76e856a95..1ba5e04f5 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -1,10 +1,10 @@ package frankenphp import ( - "github.com/stretchr/testify/assert" - "testing" fswatch "github.com/dunglas/go-fswatch" + "github.com/stretchr/testify/assert" "path/filepath" + "testing" ) func TestSimpleRecursiveWatchOption(t *testing.T) { @@ -91,21 +91,21 @@ func TestAddingTwoFilePaths(t *testing.T) { assert.NoError(t, err1) assert.NoError(t, err2) - assert.Equal(t, []string{"/first/path","/second/path"}, watchOpt.dirs) + assert.Equal(t, []string{"/first/path", "/second/path"}, watchOpt.dirs) } func TestAddingAnInclusionFilterWithDefaultForExclusion(t *testing.T) { expectedInclusionFilter := fswatch.Filter{ - Text: "\\.php$", - FilterType: fswatch.FilterInclude, - CaseSensitive: true, - Extended: true, + Text: "\\.php$", + FilterType: fswatch.FilterInclude, + CaseSensitive: true, + Extended: true, } expectedExclusionFilter := fswatch.Filter{ - Text: "\\.", - FilterType: fswatch.FilterExclude, - CaseSensitive: true, - Extended: true, + Text: "\\.", + FilterType: fswatch.FilterExclude, + CaseSensitive: true, + Extended: true, } watchOpt := createWithOption(WithWatcherFilters("\\.php$", "", true, true), t) @@ -115,10 +115,10 @@ func TestAddingAnInclusionFilterWithDefaultForExclusion(t *testing.T) { func TestWithExclusionFilter(t *testing.T) { expectedExclusionFilter := fswatch.Filter{ - Text: "\\.php$", - FilterType: fswatch.FilterExclude, - CaseSensitive: false, - Extended: false, + Text: "\\.php$", + FilterType: fswatch.FilterExclude, + CaseSensitive: false, + Extended: false, } watchOpt := createWithOption(WithWatcherFilters("", "\\.php$", false, false), t) @@ -187,10 +187,10 @@ func TestAllowReloadIfOptionIsNotAWildcard(t *testing.T) { func createFromShortForm(shortForm string, t *testing.T) watchOpt { watchOpt := getDefaultWatchOpt() - applyOptions := WithWatcherShortForm(shortForm) - err := applyOptions(&watchOpt) - assert.NoError(t, err) - return watchOpt + applyOptions := WithWatcherShortForm(shortForm) + err := applyOptions(&watchOpt) + assert.NoError(t, err) + return watchOpt } func createWithOption(applyOptions WatchOption, t *testing.T) watchOpt { @@ -200,4 +200,4 @@ func createWithOption(applyOptions WatchOption, t *testing.T) watchOpt { assert.NoError(t, err) return watchOpt -} \ No newline at end of file +} diff --git a/watcher_test.go b/watcher_test.go index 486c8040b..cd82de9d4 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -1,6 +1,7 @@ package frankenphp_test import ( + "github.com/dunglas/frankenphp" "github.com/stretchr/testify/assert" "io" "net/http" @@ -9,7 +10,6 @@ import ( "path/filepath" "testing" "time" - "github.com/dunglas/frankenphp" ) // we have to wait a few milliseconds for the watcher debounce to take effect @@ -52,7 +52,7 @@ func TestWorkersReloadOnMatchingIncludedRegex(t *testing.T) { } func TestWorkersDoNotReloadOnExcludingRegex(t *testing.T) { - const exclude ="\\.txt$" + const exclude = "\\.txt$" watchOptions := []frankenphp.WatchOption{ frankenphp.WithWatcherDirs([]string{"./testdata"}), frankenphp.WithWatcherRecursion(true), @@ -90,7 +90,7 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool { // first we make an initial request to start the request counter body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) - assert.Equal(t, "requests:1", body) + assert.Equal(t, "requests:1", body) // now we spam file updates and check if the request counter resets for i := 0; i < limit; i++ { From bc73a16f758dc685462fcc19e16391c79c52bc2c Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 18:36:22 +0200 Subject: [PATCH 055/155] Fixes linting errors. --- dev-alpine.Dockerfile | 2 ++ dev.Dockerfile | 2 ++ docs/config.md | 6 +++--- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index 9ded73d5a..96de20157 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -15,6 +15,8 @@ ENV PHPIZE_DEPS="\ pkgconfig \ re2c" +SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + RUN apk add --no-cache \ $PHPIZE_DEPS \ argon2-dev \ diff --git a/dev.Dockerfile b/dev.Dockerfile index 5b608ece2..afb64ffae 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -15,6 +15,8 @@ ENV PHPIZE_DEPS="\ pkg-config \ re2c" +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + # hadolint ignore=DL3009 RUN apt-get update && \ apt-get -y --no-install-recommends install \ diff --git a/docs/config.md b/docs/config.md index 7a34923a5..2bb493ce7 100644 --- a/docs/config.md +++ b/docs/config.md @@ -226,9 +226,9 @@ You can find more information about this setting in the [Caddy documentation](ht The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it: -* `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate -* `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options) -* `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive +-`SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate +- `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options) +- `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive As for FPM and CLI SAPIs, environment variables are exposed by default in the `$_SERVER` superglobal. From 63975e79315da6800d14a615044024c6353efc2a Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 23:17:18 +0200 Subject: [PATCH 056/155] Also allows wildcards in the longform and adjusts docs. --- caddy/watch_config.go | 23 +++++++++++------------ docs/config.md | 21 ++++++++++++--------- watcher_options.go | 8 ++++++++ watcher_options_test.go | 13 +++++++++++++ 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/caddy/watch_config.go b/caddy/watch_config.go index ec4e2584e..3d2bb3b00 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -25,12 +25,14 @@ type watchConfig struct { ExcludeFiles string `json:"exclude,omitempty"` // Allowed: "default", "fsevents", "kqueue", "inotify", "windows", "poll", "fen" MonitorType string `json:"monitor_type,omitempty"` - // Determines weather to use the wildcard shortform - IsShortForm bool `json:"shortform,omitempty"` + // Use wildcard pattern instead of regex to match files + WildcardPattern string `json:"wildcard_pattern,omitempty"` + // Determines weather to use the one line short-form + isShortForm bool } func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { - if watchConfig.IsShortForm { + if watchConfig.isShortForm { return append(opts, frankenphp.WithFileWatcher( frankenphp.WithWatcherShortForm(watchConfig.Dirs[0]), frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), @@ -43,6 +45,7 @@ func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frank frankenphp.WithWatcherFilters(watchConfig.IncludeFiles, watchConfig.ExcludeFiles, watchConfig.CaseSensitive, watchConfig.ExtendedRegex), frankenphp.WithWatcherLatency(watchConfig.Latency), frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), + frankenphp.WithWildcardPattern(watchConfig.WildcardPattern), )) } @@ -53,7 +56,7 @@ func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error{ } if d.NextArg() { watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) - watchConfig.IsShortForm = true + watchConfig.isShortForm = true } if d.NextArg() { @@ -66,7 +69,7 @@ func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error{ for d.NextBlock(1) { v := d.Val() switch v { - case "directory", "dir": + case "dir", "directory", "path": if !d.NextArg() { return d.ArgErr() } @@ -120,16 +123,12 @@ func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error{ return err } watchConfig.CaseSensitive = v - case "shortform": + case "pattern", "wildcard": if !d.NextArg() { - watchConfig.IsShortForm = true + return d.ArgErr() continue } - v, err := strconv.ParseBool(d.Val()) - if err != nil { - return err - } - watchConfig.IsShortForm = v + watchConfig.WildcardPattern = d.Val() case "extended_regex": if !d.NextArg() { watchConfig.ExtendedRegex = true diff --git a/docs/config.md b/docs/config.md index 2bb493ce7..4d3d06bf4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -165,20 +165,22 @@ You can also add multiple `watch` directives and use simple wildcard patterns, t #### Watcher Longform -It's also possible to pass a more verbose config, that uses fswatch's native regular expressions: +It's also possible to pass a more verbose config, that uses fswatch's native regular expressions, which +allows for more fine-grained control over what files are watched: ```caddyfile { frankenphp { watch { - path /path/to/folder1 - path /path/to/folder2 - recursive true # watch subdirectories - follow_symlinks false # weather to follow symlinks - exclude \.log$ # regex, exclude all files ending with .log - include \system.log$ # regex, specifically include all files ending with system.log - case_sensitive false # use case sensitive regex - extended_regex false # use extended regex + dir /path/to/folder1 # required: directory to watch + dir /path/to/folder2 # multiple directories can be watched + recursive true # watch subdirectories (default: true) + follow_symlinks false # weather to follow symlinks (default: false) + exclude \.log$ # regex to exclude files (example: those ending in .log) + include \system.log$ # regex to include excluded files (example: those ending in system.log) + case_sensitive false # use case sensitive regex (default: false) + extended_regex false # use extended regex (default: false) + pattern *.php # only include files matching a wildcard pattern (example: those ending in .php) monitor_type default # allowed: "default", "fsevents", "kqueue", "inotify", "windows", "poll", "fen" delay 150 # delay of triggering file change events in ms } @@ -192,6 +194,7 @@ It's also possible to pass a more verbose config, that uses fswatch's native reg - If ``include`` is defined, exclude will default to '\.', excluding all directories and files containing a dot - ``exclude`` currently does not work properly on [some linux systems](https://github.com/emcrisostomo/fswatch/issues/247) since it sometimes excludes the watched directory itself +- directories can also be relative (to where the frankenphp process was started from) - Be wary about watching files that are created at runtime (like logs), since they might cause unwanted worker restarts. The file watcher is based on [fswatch](https://github.com/emcrisostomo/fswatch). diff --git a/watcher_options.go b/watcher_options.go index a5b17b0e7..434aefdbf 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -86,6 +86,14 @@ func WithWatcherSymlinks(withSymlinks bool) WatchOption { } } +func WithWildcardPattern(pattern string) WatchOption { + return func(o *watchOpt) error { + o.wildCardPattern = pattern + return nil + } +} + +// for the one line shortform in the caddy config, aka: 'watch /path/*pattern' func parseShortForm(watchOpt *watchOpt, fileName string) error { watchOpt.isRecursive = true dirName := fileName diff --git a/watcher_options_test.go b/watcher_options_test.go index 1ba5e04f5..0083cc6e8 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -150,6 +150,12 @@ func TestWithLatency(t *testing.T) { assert.Equal(t, 0.5, watchOpt.latency) } +func TestWithWildcardPattern(t *testing.T) { + watchOpt := createWithOption(WithWildcardPattern("*php"), t) + + assert.Equal(t, "*php", watchOpt.wildCardPattern) +} + func TestAllowReloadOnMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createFromShortForm("/some/path/**/*.php", t) @@ -185,6 +191,13 @@ func TestAllowReloadIfOptionIsNotAWildcard(t *testing.T) { assert.True(t, watchOpt.allowReload(fileName)) } +func TestDisallowExplicitlySetWildcardPattern(t *testing.T) { + const fileName = "/some/path/file.txt" + watchOpt := createWithOption(WithWildcardPattern("*php"), t) + + assert.False(t, watchOpt.allowReload(fileName)) +} + func createFromShortForm(shortForm string, t *testing.T) watchOpt { watchOpt := getDefaultWatchOpt() applyOptions := WithWatcherShortForm(shortForm) From 971e75a178a6c7402b2f93d2e7f06e155c586801 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 23:53:07 +0200 Subject: [PATCH 057/155] Adds debug log. --- watcher.go | 1 + 1 file changed, 1 insertion(+) diff --git a/watcher.go b/watcher.go index 44d615b9c..e3d9a0fec 100644 --- a/watcher.go +++ b/watcher.go @@ -51,6 +51,7 @@ func createSession(watchOpt *watchOpt, workerOpts []workerOpt) (*fswatch.Session fswatch.WithFilters(watchOpt.filters), } handleFileEvent := registerFileEvent(watchOpt, workerOpts) + logger.Debug("starting watcher session", zap.Strings("dirs", watchOpt.dirs)) return fswatch.NewSession(watchOpt.dirs, handleFileEvent, opts...) } From f2b7bda544137e03930080b1252ddb1620aea8df Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 6 Sep 2024 23:53:24 +0200 Subject: [PATCH 058/155] Fixes the watcher short form. --- caddy/watch_config.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 3d2bb3b00..5926a4a7c 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -28,11 +28,11 @@ type watchConfig struct { // Use wildcard pattern instead of regex to match files WildcardPattern string `json:"wildcard_pattern,omitempty"` // Determines weather to use the one line short-form - isShortForm bool + IsShortForm bool } func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { - if watchConfig.isShortForm { + if watchConfig.IsShortForm { return append(opts, frankenphp.WithFileWatcher( frankenphp.WithWatcherShortForm(watchConfig.Dirs[0]), frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), @@ -49,14 +49,14 @@ func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frank )) } -func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error{ +func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { watchConfig := watchConfig{ Recursive: true, Latency: 150, } if d.NextArg() { watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) - watchConfig.isShortForm = true + watchConfig.IsShortForm = true } if d.NextArg() { From 5de7a3dd0c5b402fcdd6122085b50f07797b3fdc Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 10:22:34 +0200 Subject: [PATCH 059/155] Refactors sessions and options into a struct. --- watcher.go | 109 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 48 deletions(-) diff --git a/watcher.go b/watcher.go index e3d9a0fec..11821e7c0 100644 --- a/watcher.go +++ b/watcher.go @@ -7,41 +7,76 @@ import ( "sync/atomic" ) +type watcher struct { + sessions []*fswatch.Session + watchOpts []*watchOpt + workerOpts []workerOpt +} + var ( - // TODO: combine session and watchOpt into a struct - watchSessions []*fswatch.Session - // we block reloading until workers have stopped + // the currently active file watcher + activeWatcher *watcher + // reloading is blocked if a reload is queued blockReloading atomic.Bool // when stopping the watcher we need to wait for reloading to finish reloadWaitGroup sync.WaitGroup - // active watch options that need to be disabled on shutdown - activeWatchOpts []*watchOpt ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { if len(watchOpts) == 0 || len(workerOpts) == 0 { return nil } - - watchSessions := make([]*fswatch.Session, len(watchOpts)) - activeWatchOpts = make([]*watchOpt, len(watchOpts)) - for i, watchOpt := range watchOpts { - session, err := createSession(&watchOpt, workerOpts) - if err != nil { - logger.Error("unable to start watcher", zap.Strings("dirs", watchOpt.dirs)) - return err - } - watchSessions[i] = session - activeWatchOpts[i] = &watchOpt - go session.Start() + activeWatcher = &watcher{workerOpts: workerOpts} + err := activeWatcher.startWatching(watchOpts) + if err != nil { + return err } - reloadWaitGroup = sync.WaitGroup{} blockReloading.Store(false) + return nil } -func createSession(watchOpt *watchOpt, workerOpts []workerOpt) (*fswatch.Session, error) { +func drainWatcher() { + if(activeWatcher == nil) { + return + } + logger.Info("stopping watcher...") + blockReloading.Store(true) + activeWatcher.stopWatching() + reloadWaitGroup.Wait() + activeWatcher = nil +} + +func (w *watcher) startWatching(watchOpts []watchOpt) error { + w.sessions = make([]*fswatch.Session, len(watchOpts)) + w.watchOpts = make([]*watchOpt, len(watchOpts)) + for i, watchOpt := range watchOpts { + session, err := createSession(&watchOpt) + if err != nil { + logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) + return err + } + w.watchOpts[i] = &watchOpt + w.sessions[i] = session + go session.Start() + } + return nil +} + +func (w *watcher) stopWatching() { + for i, session := range w.sessions { + w.watchOpts[i].isActive = false + if err := session.Stop(); err != nil { + logger.Error("failed to stop watcher", zap.Error(err)) + } + if err := session.Destroy(); err != nil { + logger.Error("failed to destroy watcher", zap.Error(err)) + } + } +} + +func createSession(watchOpt *watchOpt) (*fswatch.Session, error) { opts := []fswatch.Option{ fswatch.WithRecursive(watchOpt.isRecursive), fswatch.WithFollowSymlinks(watchOpt.followSymlinks), @@ -50,56 +85,34 @@ func createSession(watchOpt *watchOpt, workerOpts []workerOpt) (*fswatch.Session fswatch.WithMonitorType((fswatch.MonitorType)(watchOpt.monitorType)), fswatch.WithFilters(watchOpt.filters), } - handleFileEvent := registerFileEvent(watchOpt, workerOpts) + handleFileEvent := registerFileEvent(watchOpt) logger.Debug("starting watcher session", zap.Strings("dirs", watchOpt.dirs)) return fswatch.NewSession(watchOpt.dirs, handleFileEvent, opts...) } -func drainWatcher() { - stopWatcher() - reloadWaitGroup.Wait() -} - -func stopWatcher() { - logger.Info("stopping watcher...") - blockReloading.Store(true) - for _, session := range watchSessions { - if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher", zap.Error(err)) - } - if err := session.Destroy(); err != nil { - logger.Error("failed to destroy watcher", zap.Error(err)) - } - } - // we also need to deactivate the watchOpts to avoid a race condition in tests - for _, watchOpt := range activeWatchOpts { - watchOpt.isActive = false - } -} - -func registerFileEvent(watchOpt *watchOpt, workerOpts []workerOpt) func([]fswatch.Event) { +func registerFileEvent(watchOpt *watchOpt) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { - if handleFileEvent(event, watchOpt, workerOpts) { + if handleFileEvent(event, watchOpt) { break } } } } -func handleFileEvent(event fswatch.Event, watchOpt *watchOpt, workerOpts []workerOpt) bool { +func handleFileEvent(event fswatch.Event, watchOpt *watchOpt) bool { if !watchOpt.allowReload(event.Path) || !blockReloading.CompareAndSwap(false, true) { return false } logger.Info("filesystem change detected, restarting workers...", zap.String("path", event.Path)) - go triggerWorkerReload(workerOpts) + go triggerWorkerReload() return true } -func triggerWorkerReload(workerOpts []workerOpt) { +func triggerWorkerReload() { reloadWaitGroup.Add(1) - restartWorkers(workerOpts) + restartWorkers(activeWatcher.workerOpts) reloadWaitGroup.Done() blockReloading.Store(false) } From 816b15775ff16d5ca82d6260f0c761923740d5de Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 11:15:20 +0200 Subject: [PATCH 060/155] Fixes an overflow in the 'workersReadyWG' on unexpected terminations. --- worker.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/worker.go b/worker.go index 3b9e9030a..8e73ca113 100644 --- a/worker.go +++ b/worker.go @@ -11,6 +11,7 @@ import ( "runtime/cgo" "sync" "time" + "sync/atomic" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -20,6 +21,7 @@ var ( workersRequestChans sync.Map // map[fileName]chan *http.Request workersReadyWG sync.WaitGroup workerShutdownWG sync.WaitGroup + workersAreReady atomic.Bool workersDone chan interface{} ) @@ -48,6 +50,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { workersRequestChans.Store(absFileName, make(chan *http.Request)) shutdownWG.Add(nbWorkers) workerShutdownWG.Add(nbWorkers) + workersAreReady.Store(false) workersReadyWG.Add(nbWorkers) var ( @@ -100,8 +103,6 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { // TODO: make the max restart configurable if _, ok := workersRequestChans.Load(absFileName); ok { if fc.exitStatus == 0 { - // TODO: the watcher will still sometimes get stuck if errors are thrown in the worker file - workersReadyWG.Add(1) if c := l.Check(zapcore.InfoLevel, "restarting"); c != nil { c.Write(zap.String("worker", absFileName)) } @@ -125,6 +126,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { } workersReadyWG.Wait() + workersAreReady.Store(true) m.Lock() defer m.Unlock() @@ -147,9 +149,6 @@ func stopWorkers() { func drainWorkers() { stopWorkers() workerShutdownWG.Wait() - // Always reset the WaitGroup to ensure we're in a clean state - workersReadyWG = sync.WaitGroup{} - workerShutdownWG = sync.WaitGroup{} } func restartWorkers(workerOpts []workerOpt) { @@ -163,7 +162,9 @@ func restartWorkers(workerOpts []workerOpt) { //export go_frankenphp_worker_ready func go_frankenphp_worker_ready() { - workersReadyWG.Done() + if(!workersAreReady.Load()) { + workersReadyWG.Done() + } } //export go_frankenphp_worker_handle_request_start From c90eeb137cbd3f47029fa6b3f166cd25c75e9357 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 12:19:41 +0200 Subject: [PATCH 061/155] Properly logs errors coming from session.Start(). --- watcher.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/watcher.go b/watcher.go index 11821e7c0..159c67f63 100644 --- a/watcher.go +++ b/watcher.go @@ -59,7 +59,13 @@ func (w *watcher) startWatching(watchOpts []watchOpt) error { } w.watchOpts[i] = &watchOpt w.sessions[i] = session - go session.Start() + go func() { + err := session.Start() + if err != nil { + logger.Error("failed to start watcher", zap.Error(err)) + logger.Warn("make sure you are not reaching your system's max number of open files") + } + }() } return nil } From f4b7c5266d13d964d39b8fdfe07740a75e68d2ea Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 12:20:18 +0200 Subject: [PATCH 062/155] go fmt. --- watcher.go | 48 ++++++++++++++++++++++++------------------------ worker.go | 4 ++-- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/watcher.go b/watcher.go index 159c67f63..a0d2d33de 100644 --- a/watcher.go +++ b/watcher.go @@ -38,9 +38,9 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { } func drainWatcher() { - if(activeWatcher == nil) { - return - } + if activeWatcher == nil { + return + } logger.Info("stopping watcher...") blockReloading.Store(true) activeWatcher.stopWatching() @@ -52,34 +52,34 @@ func (w *watcher) startWatching(watchOpts []watchOpt) error { w.sessions = make([]*fswatch.Session, len(watchOpts)) w.watchOpts = make([]*watchOpt, len(watchOpts)) for i, watchOpt := range watchOpts { - session, err := createSession(&watchOpt) - if err != nil { - logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) - return err - } + session, err := createSession(&watchOpt) + if err != nil { + logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) + return err + } w.watchOpts[i] = &watchOpt - w.sessions[i] = session - go func() { - err := session.Start() - if err != nil { - logger.Error("failed to start watcher", zap.Error(err)) - logger.Warn("make sure you are not reaching your system's max number of open files") + w.sessions[i] = session + go func() { + err := session.Start() + if err != nil { + logger.Error("failed to start watcher", zap.Error(err)) + logger.Warn("make sure you are not reaching your system's max number of open files") } - }() - } + }() + } return nil } func (w *watcher) stopWatching() { for i, session := range w.sessions { - w.watchOpts[i].isActive = false - if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher", zap.Error(err)) - } - if err := session.Destroy(); err != nil { - logger.Error("failed to destroy watcher", zap.Error(err)) - } - } + w.watchOpts[i].isActive = false + if err := session.Stop(); err != nil { + logger.Error("failed to stop watcher", zap.Error(err)) + } + if err := session.Destroy(); err != nil { + logger.Error("failed to destroy watcher", zap.Error(err)) + } + } } func createSession(watchOpt *watchOpt) (*fswatch.Session, error) { diff --git a/worker.go b/worker.go index 8e73ca113..8389e373d 100644 --- a/worker.go +++ b/worker.go @@ -10,8 +10,8 @@ import ( "path/filepath" "runtime/cgo" "sync" - "time" "sync/atomic" + "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -162,7 +162,7 @@ func restartWorkers(workerOpts []workerOpt) { //export go_frankenphp_worker_ready func go_frankenphp_worker_ready() { - if(!workersAreReady.Load()) { + if !workersAreReady.Load() { workersReadyWG.Done() } } From 4a7322839d8fb6f1943c7311a5b2811d0009b81d Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 16:01:40 +0200 Subject: [PATCH 063/155] Adds --nocache. --- alpine.Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 87f77c9a0..b164ea2e5 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -134,8 +134,7 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 COPY --from=builder /usr/local/lib/libfswatch.so* /usr/local/lib/ -COPY --from=builder /usr/local/bin/fswatch /usr/local/bin/fswatch -RUN apk add libstdc++ #required for fswatch to work +RUN apk add --no-cache libstdc++ #required for fswatch to work COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \ From 043801d8bcdbc1c7da1546d149790c5fb203c5e3 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 16:02:33 +0200 Subject: [PATCH 064/155] Fixes lint issue. --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 4d3d06bf4..e8512ba6f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -229,7 +229,7 @@ You can find more information about this setting in the [Caddy documentation](ht The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it: --`SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate +- `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate - `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options) - `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive From 269a920686238376f393e3b7aba2c26fd5888b1b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 17:15:33 +0200 Subject: [PATCH 065/155] Refactors and resolves race condition on worker reload. --- watcher.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/watcher.go b/watcher.go index a0d2d33de..04e862e04 100644 --- a/watcher.go +++ b/watcher.go @@ -59,13 +59,7 @@ func (w *watcher) startWatching(watchOpts []watchOpt) error { } w.watchOpts[i] = &watchOpt w.sessions[i] = session - go func() { - err := session.Start() - if err != nil { - logger.Error("failed to start watcher", zap.Error(err)) - logger.Warn("make sure you are not reaching your system's max number of open files") - } - }() + go startSession(session) } return nil } @@ -91,12 +85,20 @@ func createSession(watchOpt *watchOpt) (*fswatch.Session, error) { fswatch.WithMonitorType((fswatch.MonitorType)(watchOpt.monitorType)), fswatch.WithFilters(watchOpt.filters), } - handleFileEvent := registerFileEvent(watchOpt) + handleFileEvent := registerEventHandler(watchOpt) logger.Debug("starting watcher session", zap.Strings("dirs", watchOpt.dirs)) return fswatch.NewSession(watchOpt.dirs, handleFileEvent, opts...) } -func registerFileEvent(watchOpt *watchOpt) func([]fswatch.Event) { +func startSession(session *fswatch.Session) { + err := session.Start() + if err != nil { + logger.Error("failed to start watcher", zap.Error(err)) + logger.Warn("make sure you are not reaching your system's max number of open files") + } +} + +func registerEventHandler(watchOpt *watchOpt) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { if handleFileEvent(event, watchOpt) { @@ -111,14 +113,15 @@ func handleFileEvent(event fswatch.Event, watchOpt *watchOpt) bool { return false } logger.Info("filesystem change detected, restarting workers...", zap.String("path", event.Path)) - go triggerWorkerReload() + go scheduleWorkerReload() return true } -func triggerWorkerReload() { +func scheduleWorkerReload() { + reloadWaitGroup.Wait() reloadWaitGroup.Add(1) + blockReloading.Store(false) restartWorkers(activeWatcher.workerOpts) reloadWaitGroup.Done() - blockReloading.Store(false) } From 8af18744a2137a95257c530660739e7d67e79f94 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 18:17:43 +0200 Subject: [PATCH 066/155] Implements debouncing with a timer as suggested by @withinboredom. --- watcher.go | 71 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/watcher.go b/watcher.go index 04e862e04..0dfb84eec 100644 --- a/watcher.go +++ b/watcher.go @@ -4,20 +4,23 @@ import ( fswatch "github.com/dunglas/go-fswatch" "go.uber.org/zap" "sync" - "sync/atomic" + "time" ) type watcher struct { sessions []*fswatch.Session watchOpts []*watchOpt workerOpts []workerOpt + trigger chan struct{} + stop chan struct{} } +// duration to wait before reloading workers after a file change +const debounceDuration = 100 * time.Millisecond + var ( // the currently active file watcher activeWatcher *watcher - // reloading is blocked if a reload is queued - blockReloading atomic.Bool // when stopping the watcher we need to wait for reloading to finish reloadWaitGroup sync.WaitGroup ) @@ -32,7 +35,6 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { return err } reloadWaitGroup = sync.WaitGroup{} - blockReloading.Store(false) return nil } @@ -42,7 +44,6 @@ func drainWatcher() { return } logger.Info("stopping watcher...") - blockReloading.Store(true) activeWatcher.stopWatching() reloadWaitGroup.Wait() activeWatcher = nil @@ -51,8 +52,10 @@ func drainWatcher() { func (w *watcher) startWatching(watchOpts []watchOpt) error { w.sessions = make([]*fswatch.Session, len(watchOpts)) w.watchOpts = make([]*watchOpt, len(watchOpts)) + w.trigger = make(chan struct{}) + w.stop = make(chan struct{}) for i, watchOpt := range watchOpts { - session, err := createSession(&watchOpt) + session, err := createSession(&watchOpt, w.trigger) if err != nil { logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) return err @@ -61,22 +64,19 @@ func (w *watcher) startWatching(watchOpts []watchOpt) error { w.sessions[i] = session go startSession(session) } + go listenForFileEvents(w.trigger, w.stop) return nil } func (w *watcher) stopWatching() { + close(w.stop) for i, session := range w.sessions { w.watchOpts[i].isActive = false - if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher", zap.Error(err)) - } - if err := session.Destroy(); err != nil { - logger.Error("failed to destroy watcher", zap.Error(err)) - } + stopSession(session) } } -func createSession(watchOpt *watchOpt) (*fswatch.Session, error) { +func createSession(watchOpt *watchOpt, triggerWatcher chan struct{}) (*fswatch.Session, error) { opts := []fswatch.Option{ fswatch.WithRecursive(watchOpt.isRecursive), fswatch.WithFollowSymlinks(watchOpt.followSymlinks), @@ -85,7 +85,7 @@ func createSession(watchOpt *watchOpt) (*fswatch.Session, error) { fswatch.WithMonitorType((fswatch.MonitorType)(watchOpt.monitorType)), fswatch.WithFilters(watchOpt.filters), } - handleFileEvent := registerEventHandler(watchOpt) + handleFileEvent := registerEventHandler(watchOpt, triggerWatcher) logger.Debug("starting watcher session", zap.Strings("dirs", watchOpt.dirs)) return fswatch.NewSession(watchOpt.dirs, handleFileEvent, opts...) } @@ -98,30 +98,47 @@ func startSession(session *fswatch.Session) { } } -func registerEventHandler(watchOpt *watchOpt) func([]fswatch.Event) { +func stopSession(session *fswatch.Session) { + if err := session.Stop(); err != nil { + logger.Error("failed to stop watcher", zap.Error(err)) + } + if err := session.Destroy(); err != nil { + logger.Error("failed to destroy watcher", zap.Error(err)) + } +} + +func registerEventHandler(watchOpt *watchOpt, triggerWatcher chan struct{}) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { - if handleFileEvent(event, watchOpt) { - break - } + if watchOpt.allowReload(event.Path){ + logger.Debug("filesystem change detected", zap.String("path", event.Path)) + triggerWatcher <- struct{}{} + break + } } } } -func handleFileEvent(event fswatch.Event, watchOpt *watchOpt) bool { - if !watchOpt.allowReload(event.Path) || !blockReloading.CompareAndSwap(false, true) { - return false +func listenForFileEvents(trigger chan struct{}, stop chan struct{}) { + timer := time.NewTimer(debounceDuration) + timer.Stop() + defer timer.Stop() + for { + select { + case <-stop: + break + case <-trigger: + timer.Reset(debounceDuration) + case <-timer.C: + timer.Stop() + scheduleWorkerReload() + } } - logger.Info("filesystem change detected, restarting workers...", zap.String("path", event.Path)) - go scheduleWorkerReload() - - return true } func scheduleWorkerReload() { - reloadWaitGroup.Wait() + logger.Info("filesystem change detected, restarting workers...") reloadWaitGroup.Add(1) - blockReloading.Store(false) restartWorkers(activeWatcher.workerOpts) reloadWaitGroup.Done() } From a16b59e28984e76b4f10c44a82fa303f74d770a5 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 18:30:26 +0200 Subject: [PATCH 067/155] Starts watcher even if no workers are defined. --- watcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher.go b/watcher.go index 0dfb84eec..fbe43d858 100644 --- a/watcher.go +++ b/watcher.go @@ -26,7 +26,7 @@ var ( ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { - if len(watchOpts) == 0 || len(workerOpts) == 0 { + if len(watchOpts) == 0 { return nil } activeWatcher = &watcher{workerOpts: workerOpts} From 2cc0d154fb95e5872e86de0f9a7d3a20da60dc2b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 18:33:01 +0200 Subject: [PATCH 068/155] Updates docs with file limit warning. --- docs/config.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/config.md b/docs/config.md index e8512ba6f..f8c62fea1 100644 --- a/docs/config.md +++ b/docs/config.md @@ -166,14 +166,14 @@ You can also add multiple `watch` directives and use simple wildcard patterns, t #### Watcher Longform It's also possible to pass a more verbose config, that uses fswatch's native regular expressions, which -allows for more fine-grained control over what files are watched: +allow more fine-grained control over what files are watched: ```caddyfile { frankenphp { watch { - dir /path/to/folder1 # required: directory to watch - dir /path/to/folder2 # multiple directories can be watched + path /path/to/folder1 # required: directory to watch + path /path/to/folder2 # multiple directories can be watched recursive true # watch subdirectories (default: true) follow_symlinks false # weather to follow symlinks (default: false) exclude \.log$ # regex to exclude files (example: those ending in .log) @@ -192,9 +192,11 @@ allows for more fine-grained control over what files are watched: - ``include`` will only apply to excluded files - If ``include`` is defined, exclude will default to '\.', excluding all directories and files containing a dot -- ``exclude`` currently does not work properly on [some linux systems](https://github.com/emcrisostomo/fswatch/issues/247) - since it sometimes excludes the watched directory itself -- directories can also be relative (to where the frankenphp process was started from) +- Excluding all files with ``exclude`` currently does not work properly on [some linux systems](https://github.com/emcrisostomo/fswatch/issues/247) +- When watching a lot of files (10.000+), you might need to increase the limit of open files allowed by your system. + The watcher will fail with an error message if the limit is reached. It's also possible to fall back to the + `poll` monitor type, which consumes more CPU but should work on any system. +- Directories can also be relative (to where the frankenphp process was started from) - Be wary about watching files that are created at runtime (like logs), since they might cause unwanted worker restarts. The file watcher is based on [fswatch](https://github.com/emcrisostomo/fswatch). From 94159d3f6018a4f99548ddae8337fc1ad75a0781 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 19:23:35 +0200 Subject: [PATCH 069/155] Adds watch config unit tests. --- caddy/go.mod | 3 + caddy/watch_config.go | 2 +- caddy/watch_config_test.go | 109 +++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 caddy/watch_config_test.go diff --git a/caddy/go.mod b/caddy/go.mod index 289e8cc7b..ddcd3c6c7 100644 --- a/caddy/go.mod +++ b/caddy/go.mod @@ -14,6 +14,7 @@ require ( github.com/dunglas/mercure/caddy v0.16.3 github.com/dunglas/vulcain/caddy v1.0.5 github.com/spf13/cobra v1.8.1 + github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 ) @@ -42,6 +43,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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.1.1 // indirect @@ -120,6 +122,7 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.20.2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.55.0 // indirect diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 5926a4a7c..e460ff229 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -84,7 +84,7 @@ func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { return err } watchConfig.Recursive = v - case "follow_symlinks": + case "follow_symlinks", "symlinks": if !d.NextArg() { watchConfig.FollowSymlinks = true continue diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go new file mode 100644 index 000000000..f9e74c4af --- /dev/null +++ b/caddy/watch_config_test.go @@ -0,0 +1,109 @@ +package caddy_test + +import ( + "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" + "testing" + "github.com/stretchr/testify/assert" + "github.com/dunglas/frankenphp/caddy" +) + +func TestParsingARecursiveShortForm(t *testing.T) { + app, err := parseTestConfig(` + frankenphp { + watch /path + } + `) + + assert.Nil(t, err) + assert.Equal(t, 1, len(app.Watch)) + assert.Equal(t, 1, len(app.Watch[0].Dirs)) + assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.True(t, app.Watch[0].Recursive) + assert.True(t, app.Watch[0].IsShortForm) +} + +func TestParseTwoShortForms(t *testing.T) { + app, err := parseTestConfig(` + frankenphp { + watch /path + watch /other/path poll + } + `) + + assert.Nil(t, err) + assert.Equal(t, 2, len(app.Watch)) + assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.Equal(t, "", app.Watch[0].MonitorType) + assert.Equal(t, "/other/path", app.Watch[1].Dirs[0]) + assert.Equal(t, "poll", app.Watch[1].MonitorType) +} + +func TestFailOnInvalidMonitorType(t *testing.T) { + _, err := parseTestConfig(` + frankenphp { + watch /path invalid_monitor + } + `) + + assert.Error(t, err) +} + +func TestFailOnMissingPathInShortForm(t *testing.T) { + _, err := parseTestConfig(` + frankenphp { + watch + } + `) + + assert.Error(t, err) +} + +func TestFailOnMissingPathInLongForm(t *testing.T) { + _, err := parseTestConfig(` + frankenphp { + watch { + monitor_type poll + } + } + `) + + assert.Error(t, err) +} + +func TestParseLongFormCorrectly(t *testing.T) { + app, err := parseTestConfig(` + frankenphp { + watch { + path /path + recursive false + symlinks true + case_sensitive true + extended_regex true + include *important.txt + exclude *.txt + pattern *important.txt + monitor_type poll + latency 100 + } + } + `) + + assert.Nil(t, err) + assert.Equal(t, 1, len(app.Watch)) + assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.False(t, app.Watch[0].Recursive) + assert.True(t, app.Watch[0].FollowSymlinks) + assert.True(t, app.Watch[0].CaseSensitive) + assert.True(t, app.Watch[0].ExtendedRegex) + assert.Equal(t, "*important.txt", app.Watch[0].IncludeFiles) + assert.Equal(t, "*.txt", app.Watch[0].ExcludeFiles) + assert.Equal(t, "*important.txt", app.Watch[0].WildcardPattern) + assert.Equal(t, "poll", app.Watch[0].MonitorType) + assert.Equal(t, 100, app.Watch[0].Latency) +} + +func parseTestConfig(config string) (*caddy.FrankenPHPApp, error) { + app := caddy.FrankenPHPApp{} + err := app.UnmarshalCaddyfile(caddyfile.NewTestDispenser(config)) + return &app, err +} \ No newline at end of file From eea350ba551e9520eb3b19d608957ec8f1226e2b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 19:24:17 +0200 Subject: [PATCH 070/155] Adjusts debounce timings. --- watcher.go | 2 +- watcher_test.go | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/watcher.go b/watcher.go index fbe43d858..bd7e7bc74 100644 --- a/watcher.go +++ b/watcher.go @@ -16,7 +16,7 @@ type watcher struct { } // duration to wait before reloading workers after a file change -const debounceDuration = 100 * time.Millisecond +const debounceDuration = 150 * time.Millisecond var ( // the currently active file watcher diff --git a/watcher_test.go b/watcher_test.go index cd82de9d4..d8669cfea 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -13,9 +13,11 @@ import ( ) // we have to wait a few milliseconds for the watcher debounce to take effect -const pollingTime = 150 -const minTimesToPollForChanges = 5 -const maxTimesToPollForChanges = 100 // we will poll a maximum of 100x150ms = 15s +const pollingTime = 250 +// in tests checking for no reload: we will poll 3x250ms = 0.75s +const minTimesToPollForChanges = 3 +// in tests checking for a reload: we will poll a maximum of 60x200ms = 12s +const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { const filePattern = "./testdata/**/*.txt" From 64d1fa059ff041ab04619bf9b8f735def388f4cd Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 19:26:29 +0200 Subject: [PATCH 071/155] go fmt. --- caddy/caddy.go | 2 +- caddy/watch_config.go | 122 ++++++++++++++++++------------------- caddy/watch_config_test.go | 6 +- watcher.go | 38 ++++++------ watcher_test.go | 2 + 5 files changed, 86 insertions(+), 84 deletions(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index 9e9489941..c1fae4d57 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -134,7 +134,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { f.NumThreads = v case "watch": - if err:= parseWatchDirective(f, d); err != nil { + if err := parseWatchDirective(f, d); err != nil { return err } case "worker": diff --git a/caddy/watch_config.go b/caddy/watch_config.go index e460ff229..ec92eb134 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -1,9 +1,9 @@ package caddy import ( - "strconv" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/dunglas/frankenphp" + "strconv" ) type watchConfig struct { @@ -12,7 +12,7 @@ type watchConfig struct { // Determines whether the watcher should be recursive. Recursive bool `json:"recursive,omitempty"` // Determines whether the watcher should follow symlinks. - FollowSymlinks bool `json:"follow_symlinks,omitempty"` + FollowSymlinks bool `json:"follow_symlinks,omitempty"` // Determines whether the regex should be case sensitive. CaseSensitive bool `json:"case_sensitive,omitempty"` // Determines whether the regex should be extended. @@ -34,25 +34,25 @@ type watchConfig struct { func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { if watchConfig.IsShortForm { return append(opts, frankenphp.WithFileWatcher( - frankenphp.WithWatcherShortForm(watchConfig.Dirs[0]), - frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), + frankenphp.WithWatcherShortForm(watchConfig.Dirs[0]), + frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), )) } return append(opts, frankenphp.WithFileWatcher( - frankenphp.WithWatcherDirs(watchConfig.Dirs), - frankenphp.WithWatcherRecursion(watchConfig.Recursive), - frankenphp.WithWatcherSymlinks(watchConfig.FollowSymlinks), - frankenphp.WithWatcherFilters(watchConfig.IncludeFiles, watchConfig.ExcludeFiles, watchConfig.CaseSensitive, watchConfig.ExtendedRegex), - frankenphp.WithWatcherLatency(watchConfig.Latency), - frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), - frankenphp.WithWildcardPattern(watchConfig.WildcardPattern), - )) + frankenphp.WithWatcherDirs(watchConfig.Dirs), + frankenphp.WithWatcherRecursion(watchConfig.Recursive), + frankenphp.WithWatcherSymlinks(watchConfig.FollowSymlinks), + frankenphp.WithWatcherFilters(watchConfig.IncludeFiles, watchConfig.ExcludeFiles, watchConfig.CaseSensitive, watchConfig.ExtendedRegex), + frankenphp.WithWatcherLatency(watchConfig.Latency), + frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), + frankenphp.WithWildcardPattern(watchConfig.WildcardPattern), + )) } func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { watchConfig := watchConfig{ Recursive: true, - Latency: 150, + Latency: 150, } if d.NextArg() { watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) @@ -60,49 +60,49 @@ func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { } if d.NextArg() { - if err := verifyMonitorType(d.Val(), d); err != nil { - return err - } - watchConfig.MonitorType = d.Val() - } + if err := verifyMonitorType(d.Val(), d); err != nil { + return err + } + watchConfig.MonitorType = d.Val() + } for d.NextBlock(1) { - v := d.Val() - switch v { + v := d.Val() + switch v { case "dir", "directory", "path": - if !d.NextArg() { - return d.ArgErr() - } - watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) - case "recursive": - if !d.NextArg() { - watchConfig.Recursive = true - continue - } - v, err := strconv.ParseBool(d.Val()) - if err != nil { - return err - } - watchConfig.Recursive = v - case "follow_symlinks", "symlinks": - if !d.NextArg() { - watchConfig.FollowSymlinks = true - continue - } - v, err := strconv.ParseBool(d.Val()) - if err != nil { - return err - } - watchConfig.FollowSymlinks = v - case "latency": - if !d.NextArg() { - return d.ArgErr() - } - v, err := strconv.Atoi(d.Val()) - if err != nil { - return err - } - watchConfig.Latency = v + if !d.NextArg() { + return d.ArgErr() + } + watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) + case "recursive": + if !d.NextArg() { + watchConfig.Recursive = true + continue + } + v, err := strconv.ParseBool(d.Val()) + if err != nil { + return err + } + watchConfig.Recursive = v + case "follow_symlinks", "symlinks": + if !d.NextArg() { + watchConfig.FollowSymlinks = true + continue + } + v, err := strconv.ParseBool(d.Val()) + if err != nil { + return err + } + watchConfig.FollowSymlinks = v + case "latency": + if !d.NextArg() { + return d.ArgErr() + } + v, err := strconv.Atoi(d.Val()) + if err != nil { + return err + } + watchConfig.Latency = v case "include", "include_files": if !d.NextArg() { return d.ArgErr() @@ -150,8 +150,8 @@ func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { default: return d.Errf("unknown watcher subdirective '%s'", v) } - } - if(len(watchConfig.Dirs) == 0) { + } + if len(watchConfig.Dirs) == 0 { return d.Err("The 'dir' argument must be specified for the watch directive") } f.Watch = append(f.Watch, watchConfig) @@ -160,9 +160,9 @@ func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { func verifyMonitorType(monitorType string, d *caddyfile.Dispenser) error { switch monitorType { - case "default", "system", "fsevents", "kqueue", "inotify", "windows", "poll", "fen": - return nil - default: - return d.Errf("unknown watcher monitor type '%s'", monitorType) - } -} \ No newline at end of file + case "default", "system", "fsevents", "kqueue", "inotify", "windows", "poll", "fen": + return nil + default: + return d.Errf("unknown watcher monitor type '%s'", monitorType) + } +} diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go index f9e74c4af..69d9050aa 100644 --- a/caddy/watch_config_test.go +++ b/caddy/watch_config_test.go @@ -2,9 +2,9 @@ package caddy_test import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "testing" - "github.com/stretchr/testify/assert" "github.com/dunglas/frankenphp/caddy" + "github.com/stretchr/testify/assert" + "testing" ) func TestParsingARecursiveShortForm(t *testing.T) { @@ -106,4 +106,4 @@ func parseTestConfig(config string) (*caddy.FrankenPHPApp, error) { app := caddy.FrankenPHPApp{} err := app.UnmarshalCaddyfile(caddyfile.NewTestDispenser(config)) return &app, err -} \ No newline at end of file +} diff --git a/watcher.go b/watcher.go index bd7e7bc74..d652bc9d6 100644 --- a/watcher.go +++ b/watcher.go @@ -11,7 +11,7 @@ type watcher struct { sessions []*fswatch.Session watchOpts []*watchOpt workerOpts []workerOpt - trigger chan struct{} + trigger chan struct{} stop chan struct{} } @@ -100,21 +100,21 @@ func startSession(session *fswatch.Session) { func stopSession(session *fswatch.Session) { if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher", zap.Error(err)) - } - if err := session.Destroy(); err != nil { - logger.Error("failed to destroy watcher", zap.Error(err)) - } + logger.Error("failed to stop watcher", zap.Error(err)) + } + if err := session.Destroy(); err != nil { + logger.Error("failed to destroy watcher", zap.Error(err)) + } } func registerEventHandler(watchOpt *watchOpt, triggerWatcher chan struct{}) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { - if watchOpt.allowReload(event.Path){ + if watchOpt.allowReload(event.Path) { logger.Debug("filesystem change detected", zap.String("path", event.Path)) - triggerWatcher <- struct{}{} - break - } + triggerWatcher <- struct{}{} + break + } } } } @@ -124,15 +124,15 @@ func listenForFileEvents(trigger chan struct{}, stop chan struct{}) { timer.Stop() defer timer.Stop() for { - select { - case <-stop: - break - case <-trigger: - timer.Reset(debounceDuration) - case <-timer.C: - timer.Stop() - scheduleWorkerReload() - } + select { + case <-stop: + break + case <-trigger: + timer.Reset(debounceDuration) + case <-timer.C: + timer.Stop() + scheduleWorkerReload() + } } } diff --git a/watcher_test.go b/watcher_test.go index d8669cfea..b1b65d5d1 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -14,8 +14,10 @@ import ( // we have to wait a few milliseconds for the watcher debounce to take effect const pollingTime = 250 + // in tests checking for no reload: we will poll 3x250ms = 0.75s const minTimesToPollForChanges = 3 + // in tests checking for a reload: we will poll a maximum of 60x200ms = 12s const maxTimesToPollForChanges = 60 From c60fbf64b887d70f1433f5fd6b78cd9a9921ceb3 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 19:31:40 +0200 Subject: [PATCH 072/155] Adds fswatch to static builder (test). --- static-builder.Dockerfile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/static-builder.Dockerfile b/static-builder.Dockerfile index 79c9b5e42..d413a0e53 100644 --- a/static-builder.Dockerfile +++ b/static-builder.Dockerfile @@ -73,6 +73,17 @@ RUN apk update; \ xz ; \ ln -sf /usr/bin/php83 /usr/bin/php +# install fswatch (necessary for file watching) +ARG FSWATCH_VERSION +WORKDIR /usr/local/src/fswatch +RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz +WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION +RUN ./configure && \ + make -j"$(nproc)" && \ + make install && \ + ldconfig /usr/local/lib && \ + fswatch --version + # FIXME: temporary workaround for https://github.com/golang/go/issues/68285 WORKDIR / RUN git clone https://go.googlesource.com/go goroot From 584a702237859fbd0cc2dd7a397d190705b383b3 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 21:37:17 +0200 Subject: [PATCH 073/155] Adds a short grace period between stopping and destroying the watcher sessions. --- docs/config.md | 2 +- watcher.go | 26 +++++++++++--------------- watcher_options.go | 5 ----- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/docs/config.md b/docs/config.md index f8c62fea1..0e01c7201 100644 --- a/docs/config.md +++ b/docs/config.md @@ -194,7 +194,7 @@ allow more fine-grained control over what files are watched: - If ``include`` is defined, exclude will default to '\.', excluding all directories and files containing a dot - Excluding all files with ``exclude`` currently does not work properly on [some linux systems](https://github.com/emcrisostomo/fswatch/issues/247) - When watching a lot of files (10.000+), you might need to increase the limit of open files allowed by your system. - The watcher will fail with an error message if the limit is reached. It's also possible to fall back to the + The watcher will fail with an error message if the limit is reached. It's also possible to fall back to the `poll` monitor type, which consumes more CPU but should work on any system. - Directories can also be relative (to where the frankenphp process was started from) - Be wary about watching files that are created at runtime (like logs), since they might cause unwanted worker restarts. diff --git a/watcher.go b/watcher.go index d652bc9d6..fa62b7e09 100644 --- a/watcher.go +++ b/watcher.go @@ -9,7 +9,6 @@ import ( type watcher struct { sessions []*fswatch.Session - watchOpts []*watchOpt workerOpts []workerOpt trigger chan struct{} stop chan struct{} @@ -51,7 +50,6 @@ func drainWatcher() { func (w *watcher) startWatching(watchOpts []watchOpt) error { w.sessions = make([]*fswatch.Session, len(watchOpts)) - w.watchOpts = make([]*watchOpt, len(watchOpts)) w.trigger = make(chan struct{}) w.stop = make(chan struct{}) for i, watchOpt := range watchOpts { @@ -60,7 +58,6 @@ func (w *watcher) startWatching(watchOpts []watchOpt) error { logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) return err } - w.watchOpts[i] = &watchOpt w.sessions[i] = session go startSession(session) } @@ -70,10 +67,18 @@ func (w *watcher) startWatching(watchOpts []watchOpt) error { func (w *watcher) stopWatching() { close(w.stop) - for i, session := range w.sessions { - w.watchOpts[i].isActive = false - stopSession(session) + for _, session := range w.sessions { + if err := session.Stop(); err != nil { + logger.Error("failed to stop watcher", zap.Error(err)) + } } + // mandatory grace period between stopping and destroying the watcher + time.Sleep(50 * time.Millisecond) + for _, session := range w.sessions { + if err := session.Destroy(); err != nil { + logger.Error("failed to stop watcher", zap.Error(err)) + } + } } func createSession(watchOpt *watchOpt, triggerWatcher chan struct{}) (*fswatch.Session, error) { @@ -98,15 +103,6 @@ func startSession(session *fswatch.Session) { } } -func stopSession(session *fswatch.Session) { - if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher", zap.Error(err)) - } - if err := session.Destroy(); err != nil { - logger.Error("failed to destroy watcher", zap.Error(err)) - } -} - func registerEventHandler(watchOpt *watchOpt, triggerWatcher chan struct{}) func([]fswatch.Event) { return func(events []fswatch.Event) { for _, event := range events { diff --git a/watcher_options.go b/watcher_options.go index 434aefdbf..56374db70 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -14,7 +14,6 @@ type watchOpt struct { dirs []string isRecursive bool followSymlinks bool - isActive bool latency float64 wildCardPattern string filters []fswatch.Filter @@ -24,7 +23,6 @@ type watchOpt struct { func getDefaultWatchOpt() watchOpt { return watchOpt{ - isActive: true, isRecursive: true, latency: 0.15, monitorType: fswatch.SystemDefaultMonitor, @@ -185,9 +183,6 @@ func parseAbsPath(path string) (string, error) { } func (watchOpt *watchOpt) allowReload(fileName string) bool { - if !watchOpt.isActive { - return false - } if watchOpt.wildCardPattern == "" { return true } From fe265428a14b264ec40d286f80b9a69191cb6a79 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 22:37:44 +0200 Subject: [PATCH 074/155] Adds caddy test. --- caddy/caddy_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index 076e41d0a..6bb2d7afa 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -271,3 +271,35 @@ func TestPHPServerDirectiveDisableFileServer(t *testing.T) { tester.AssertGetResponse("http://localhost:9080", http.StatusOK, "I am by birth a Genevese (i not set)") tester.AssertGetResponse("http://localhost:9080/hello.txt", http.StatusNotFound, "Not found") } + +func TestWorkerWithSleepingWatcher(t *testing.T) { + tester := caddytest.NewTester(t) + tester.InitServer(` + { + skip_install_trust + admin localhost:2999 + http_port 9080 + https_port 9443 + + frankenphp { + worker ../testdata/worker-with-watcher.php 1 + watch { + path . + include \.txt$ + recursive + monitor default + latency 100 + } + } + } + + localhost:9080 { + root * ../testdata + rewrite * worker-with-watcher.php + php + } + `, "caddyfile") + + tester.AssertGetResponse("http://localhost:9080", http.StatusOK, "requests:1") + tester.AssertGetResponse("http://localhost:9080", http.StatusOK, "requests:2") +} From 686ec68aebc890f08bfe13c55b1a03dd203a1523 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 7 Sep 2024 22:39:47 +0200 Subject: [PATCH 075/155] Adjusts sleep time. --- watcher.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/watcher.go b/watcher.go index fa62b7e09..823999b90 100644 --- a/watcher.go +++ b/watcher.go @@ -73,7 +73,8 @@ func (w *watcher) stopWatching() { } } // mandatory grace period between stopping and destroying the watcher - time.Sleep(50 * time.Millisecond) + // TODO: what is a good value here? fswatch sleeps for 3s in tests... + time.Sleep(100 * time.Millisecond) for _, session := range w.sessions { if err := session.Destroy(); err != nil { logger.Error("failed to stop watcher", zap.Error(err)) From 5edc067e925b55acabb7bc3c5e4738216b14a275 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 10 Sep 2024 23:58:59 +0200 Subject: [PATCH 076/155] Swap to edant/watcher. --- caddy/watch_config.go | 175 ++++++++---------------------------------- docs/config.md | 46 ++++------- options.go | 2 +- watcher-c.h | 76 ++++++++++++++++++ watcher.c | 21 +++++ watcher.go | 113 +++++++++++++-------------- watcher.h | 6 ++ 7 files changed, 205 insertions(+), 234 deletions(-) create mode 100644 watcher-c.h create mode 100644 watcher.c create mode 100644 watcher.h diff --git a/caddy/watch_config.go b/caddy/watch_config.go index ec92eb134..2e2b19d04 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -3,166 +3,55 @@ package caddy import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/dunglas/frankenphp" - "strconv" + "strings" + "path/filepath" ) type watchConfig struct { // FileName sets the path to the worker script. Dirs []string `json:"dir,omitempty"` - // Determines whether the watcher should be recursive. - Recursive bool `json:"recursive,omitempty"` - // Determines whether the watcher should follow symlinks. - FollowSymlinks bool `json:"follow_symlinks,omitempty"` - // Determines whether the regex should be case sensitive. - CaseSensitive bool `json:"case_sensitive,omitempty"` - // Determines whether the regex should be extended. - ExtendedRegex bool `json:"extended_regex,omitempty"` - // Latency of the watcher in ms. - Latency int `json:"latency,omitempty"` - // Include only files matching this regex - IncludeFiles string `json:"include,omitempty"` - // Exclude files matching this regex (will exclude all if empty) - ExcludeFiles string `json:"exclude,omitempty"` - // Allowed: "default", "fsevents", "kqueue", "inotify", "windows", "poll", "fen" - MonitorType string `json:"monitor_type,omitempty"` - // Use wildcard pattern instead of regex to match files - WildcardPattern string `json:"wildcard_pattern,omitempty"` - // Determines weather to use the one line short-form - IsShortForm bool + // Whether to watch the directory recursively + IsRecursive bool `json:"recursive,omitempty"` + // The shell filename pattern to match against + Pattern string `json:"pattern,omitempty"` } func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { - if watchConfig.IsShortForm { - return append(opts, frankenphp.WithFileWatcher( - frankenphp.WithWatcherShortForm(watchConfig.Dirs[0]), - frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), - )) - } return append(opts, frankenphp.WithFileWatcher( frankenphp.WithWatcherDirs(watchConfig.Dirs), - frankenphp.WithWatcherRecursion(watchConfig.Recursive), - frankenphp.WithWatcherSymlinks(watchConfig.FollowSymlinks), - frankenphp.WithWatcherFilters(watchConfig.IncludeFiles, watchConfig.ExcludeFiles, watchConfig.CaseSensitive, watchConfig.ExtendedRegex), - frankenphp.WithWatcherLatency(watchConfig.Latency), - frankenphp.WithWatcherMonitorType(watchConfig.MonitorType), - frankenphp.WithWildcardPattern(watchConfig.WildcardPattern), + frankenphp.WithWatcherRecursion(watchConfig.IsRecursive), + frankenphp.WithWatcherPattern(watchConfig.Pattern), )) } func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { - watchConfig := watchConfig{ - Recursive: true, - Latency: 150, - } - if d.NextArg() { - watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) - watchConfig.IsShortForm = true - } + if !d.NextArg() { + return d.Err("The 'watch' directive must be followed by a path") + } + f.Watch = append(f.Watch, parseFullPattern(d.Val())) - if d.NextArg() { - if err := verifyMonitorType(d.Val(), d); err != nil { - return err - } - watchConfig.MonitorType = d.Val() - } - - for d.NextBlock(1) { - v := d.Val() - switch v { - case "dir", "directory", "path": - if !d.NextArg() { - return d.ArgErr() - } - watchConfig.Dirs = append(watchConfig.Dirs, d.Val()) - case "recursive": - if !d.NextArg() { - watchConfig.Recursive = true - continue - } - v, err := strconv.ParseBool(d.Val()) - if err != nil { - return err - } - watchConfig.Recursive = v - case "follow_symlinks", "symlinks": - if !d.NextArg() { - watchConfig.FollowSymlinks = true - continue - } - v, err := strconv.ParseBool(d.Val()) - if err != nil { - return err - } - watchConfig.FollowSymlinks = v - case "latency": - if !d.NextArg() { - return d.ArgErr() - } - v, err := strconv.Atoi(d.Val()) - if err != nil { - return err - } - watchConfig.Latency = v - case "include", "include_files": - if !d.NextArg() { - return d.ArgErr() - } - watchConfig.IncludeFiles = d.Val() - case "exclude", "exclude_files": - if !d.NextArg() { - return d.ArgErr() - } - watchConfig.ExcludeFiles = d.Val() - case "case_sensitive": - if !d.NextArg() { - watchConfig.CaseSensitive = true - continue - } - v, err := strconv.ParseBool(d.Val()) - if err != nil { - return err - } - watchConfig.CaseSensitive = v - case "pattern", "wildcard": - if !d.NextArg() { - return d.ArgErr() - continue - } - watchConfig.WildcardPattern = d.Val() - case "extended_regex": - if !d.NextArg() { - watchConfig.ExtendedRegex = true - continue - } - v, err := strconv.ParseBool(d.Val()) - if err != nil { - return err - } - watchConfig.ExtendedRegex = v - case "monitor_type", "monitor": - if !d.NextArg() { - return d.ArgErr() - } - if err := verifyMonitorType(d.Val(), d); err != nil { - return err - } - watchConfig.MonitorType = d.Val() - default: - return d.Errf("unknown watcher subdirective '%s'", v) - } - } - if len(watchConfig.Dirs) == 0 { - return d.Err("The 'dir' argument must be specified for the watch directive") - } - f.Watch = append(f.Watch, watchConfig) return nil } -func verifyMonitorType(monitorType string, d *caddyfile.Dispenser) error { - switch monitorType { - case "default", "system", "fsevents", "kqueue", "inotify", "windows", "poll", "fen": - return nil - default: - return d.Errf("unknown watcher monitor type '%s'", monitorType) +// TODO: better path validation? +// for the one line short-form in the caddy config, aka: 'watch /path/*pattern' +func parseFullPattern(filePattern string) watchConfig { + watchConfig := watchConfig{IsRecursive: true} + dirName := filePattern + splitDirName, baseName := filepath.Split(filePattern) + if filePattern != "." && filePattern != ".." && strings.ContainsAny(baseName, "*.[?\\") { + dirName = splitDirName + watchConfig.Pattern = baseName + watchConfig.IsRecursive = false + } + + if strings.Contains(filePattern, "/**/") { + dirName = strings.Split(filePattern, "/**/")[0] + watchConfig.Pattern = strings.Split(filePattern, "/**/")[1] + watchConfig.IsRecursive = true } + watchConfig.Dirs = []string{dirName} + + return watchConfig } + diff --git a/docs/config.md b/docs/config.md index 0e01c7201..f7efe7758 100644 --- a/docs/config.md +++ b/docs/config.md @@ -146,9 +146,8 @@ development environments. } ``` -The configuration above will watch the `/path/to/app` directory recursively. - -#### Watcher Shortform +The configuration above will watch the `/path/to/app` directory recursively. +If any file changes, the worker will be restarted. You can also add multiple `watch` directives and use simple wildcard patterns, the following is valid: @@ -156,50 +155,37 @@ You can also add multiple `watch` directives and use simple wildcard patterns, t { frankenphp { watch /path/to/folder1 # watches all subdirectories - watch /path/to/folder2/*.php # watches only php files in the app directory - watch /path/to/folder3/**/*.php # watches only php files in the app directory and subdirectories - watch /path/to/folder4 poll # watches all subdirectories with the 'poll' monitor type + watch /path/to/folder2/*.php # watches files ending in .php in the /path/to/folder2 directory + watch /path/to/folder3/**/*.php # watches files ending in .php in the /path/to/folder3 directory and subdirectories } } ``` -#### Watcher Longform - -It's also possible to pass a more verbose config, that uses fswatch's native regular expressions, which -allow more fine-grained control over what files are watched: +Multiple directories can also be watched in one block: ```caddyfile { frankenphp { watch { - path /path/to/folder1 # required: directory to watch - path /path/to/folder2 # multiple directories can be watched - recursive true # watch subdirectories (default: true) - follow_symlinks false # weather to follow symlinks (default: false) - exclude \.log$ # regex to exclude files (example: those ending in .log) - include \system.log$ # regex to include excluded files (example: those ending in system.log) - case_sensitive false # use case sensitive regex (default: false) - extended_regex false # use extended regex (default: false) - pattern *.php # only include files matching a wildcard pattern (example: those ending in .php) - monitor_type default # allowed: "default", "fsevents", "kqueue", "inotify", "windows", "poll", "fen" - delay 150 # delay of triggering file change events in ms + dir /path/to/folder1 + dir /path/to/folder2 + dir /path/to/folder3 + recursive true + pattern *.php } } } ``` #### Some notes - -- ``include`` will only apply to excluded files -- If ``include`` is defined, exclude will default to '\.', excluding all directories and files containing a dot -- Excluding all files with ``exclude`` currently does not work properly on [some linux systems](https://github.com/emcrisostomo/fswatch/issues/247) -- When watching a lot of files (10.000+), you might need to increase the limit of open files allowed by your system. - The watcher will fail with an error message if the limit is reached. It's also possible to fall back to the - `poll` monitor type, which consumes more CPU but should work on any system. - Directories can also be relative (to where the frankenphp process was started from) -- Be wary about watching files that are created at runtime (like logs), since they might cause unwanted worker restarts. +- The `/**/` pattern signifies recursive watching +- If the last part of the pattern contains the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the + shell [filename pattern](https://pkg.go.dev/path/filepath#Match) +- The watcher will ignore symlinks +- Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts. -The file watcher is based on [fswatch](https://github.com/emcrisostomo/fswatch). +The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher). ### Full Duplex (HTTP/1) diff --git a/options.go b/options.go index 6bbd94d8d..20cb39b8e 100644 --- a/options.go +++ b/options.go @@ -44,7 +44,7 @@ func WithWorkers(fileName string, num int, env map[string]string) Option { // WithFileWatcher configures filesystem watchers. func WithFileWatcher(wo ...WatchOption) Option { return func(o *opt) error { - watchOpt := getDefaultWatchOpt() + watchOpt := watchOpt{} for _, option := range wo { if err := option(&watchOpt); err != nil { return err diff --git a/watcher-c.h b/watcher-c.h new file mode 100644 index 000000000..245266ddf --- /dev/null +++ b/watcher-c.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Represents "what happened" to a path. */ +static const int8_t WTR_WATCHER_EFFECT_RENAME = 0; +static const int8_t WTR_WATCHER_EFFECT_MODIFY = 1; +static const int8_t WTR_WATCHER_EFFECT_CREATE = 2; +static const int8_t WTR_WATCHER_EFFECT_DESTROY = 3; +static const int8_t WTR_WATCHER_EFFECT_OWNER = 4; +static const int8_t WTR_WATCHER_EFFECT_OTHER = 5; + +/* Represents "what kind" of path it is. */ +static const int8_t WTR_WATCHER_PATH_DIR = 0; +static const int8_t WTR_WATCHER_PATH_FILE = 1; +static const int8_t WTR_WATCHER_PATH_HARD_LINK = 2; +static const int8_t WTR_WATCHER_PATH_SYM_LINK = 3; +static const int8_t WTR_WATCHER_PATH_WATCHER = 4; +static const int8_t WTR_WATCHER_PATH_OTHER = 5; + +/* The `event` object is used to carry information about + filesystem events to the user through the (user-supplied) + callback given to `watch`. + The `event` object will contain the: + - `path_name`: The path to the event. + - `path_type`: One of: + - dir + - file + - hard_link + - sym_link + - watcher + - other + - `effect_type`: One of: + - rename + - modify + - create + - destroy + - owner + - other + - `effect_time`: + The time of the event in nanoseconds since epoch. +*/ +struct wtr_watcher_event { + int64_t effect_time; + char const* path_name; + char const* associated_path_name; + int8_t effect_type; + int8_t path_type; +}; + +/* Ensure the user's callback can receive + events and will return nothing. */ +typedef void (* wtr_watcher_callback)(struct wtr_watcher_event event, void* context); + +void* wtr_watcher_open(char const* const path, wtr_watcher_callback callback, void* context); + +bool wtr_watcher_close(void* watcher); + +/* The user, or the language we're working with, + might not prefer a callback-style API. + We provide a pipe-based API for these cases. + Instead of forwarding events to a callback, + we write json-serialized events to a pipe. */ +void* wtr_watcher_open_pipe(char const* const path, int* read_fd, int* write_fd); + +bool wtr_watcher_close_pipe(void* watcher, int read_fd, int write_fd); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/watcher.c b/watcher.c new file mode 100644 index 000000000..862639e1c --- /dev/null +++ b/watcher.c @@ -0,0 +1,21 @@ +#include "_cgo_export.h" +#include "watcher-c.h" + +void process_event(struct wtr_watcher_event event, void* data) { + go_handle_event((char*)event.path_name, event.effect_type, event.path_type, (uintptr_t)data); +} + +void* start_new_watcher(char const* const path, uintptr_t data) { + void* watcher = wtr_watcher_open(path, process_event, (void *)data); + if (!watcher) { + return NULL; + } + return watcher; +} + +int stop_watcher(void* watcher) { + if (!wtr_watcher_close(watcher)) { + return 1; + } + return 0; +} \ No newline at end of file diff --git a/watcher.go b/watcher.go index 823999b90..dbef8e6b7 100644 --- a/watcher.go +++ b/watcher.go @@ -1,17 +1,25 @@ package frankenphp +// #cgo LDFLAGS: -lwatcher-c-0.11.0 +// #cgo CFLAGS: -Wall -Werror +// #include +// #include +// #include "watcher.h" +import "C" import ( - fswatch "github.com/dunglas/go-fswatch" + "go.uber.org/zap" "sync" "time" + "runtime/cgo" + "unsafe" + "errors" ) type watcher struct { - sessions []*fswatch.Session + sessions []unsafe.Pointer workerOpts []workerOpt - trigger chan struct{} - stop chan struct{} + watchOpts []watchOpt } // duration to wait before reloading workers after a file change @@ -22,6 +30,8 @@ var ( activeWatcher *watcher // when stopping the watcher we need to wait for reloading to finish reloadWaitGroup sync.WaitGroup + triggerWatcher chan struct{} + stopWatcher chan struct{} ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { @@ -48,83 +58,66 @@ func drainWatcher() { activeWatcher = nil } +func startWatchOpt(watchOpt *watchOpt) (unsafe.Pointer, error) { + handle := cgo.NewHandle(watchOpt) + cPathTranslated := (*C.char)(C.CString(watchOpt.dirs[0])) + watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) + if(watchSession == C.NULL){ + logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) + return nil, errors.New("couldn't start watching") + } + return watchSession, nil +} + +func stopWatchSession(session unsafe.Pointer){ + success := C.stop_watcher(session) + if(success == 0){ + logger.Error("couldn't stop watching") + } +} + +//export go_handle_event +func go_handle_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) { + watchOpt := cgo.Handle(handle).Value().(*watchOpt) + if watchOpt.allowReload(C.GoString(path), int(eventType), int(pathType)) { + logger.Debug("valid file change detected", zap.String("path", C.GoString(path))) + triggerWatcher <- struct{}{} + } +} + func (w *watcher) startWatching(watchOpts []watchOpt) error { - w.sessions = make([]*fswatch.Session, len(watchOpts)) - w.trigger = make(chan struct{}) - w.stop = make(chan struct{}) - for i, watchOpt := range watchOpts { - session, err := createSession(&watchOpt, w.trigger) + w.sessions = make([]unsafe.Pointer, len(watchOpts)) + w.watchOpts = watchOpts + triggerWatcher = make(chan struct{}) + stopWatcher = make(chan struct{}) + for i, watchOpt := range w.watchOpts { + session, err := startWatchOpt(&watchOpt) if err != nil { logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) return err } w.sessions[i] = session - go startSession(session) } - go listenForFileEvents(w.trigger, w.stop) + go listenForFileEvents() return nil } func (w *watcher) stopWatching() { - close(w.stop) + close(stopWatcher) for _, session := range w.sessions { - if err := session.Stop(); err != nil { - logger.Error("failed to stop watcher", zap.Error(err)) - } - } - // mandatory grace period between stopping and destroying the watcher - // TODO: what is a good value here? fswatch sleeps for 3s in tests... - time.Sleep(100 * time.Millisecond) - for _, session := range w.sessions { - if err := session.Destroy(); err != nil { - logger.Error("failed to stop watcher", zap.Error(err)) - } - } -} - -func createSession(watchOpt *watchOpt, triggerWatcher chan struct{}) (*fswatch.Session, error) { - opts := []fswatch.Option{ - fswatch.WithRecursive(watchOpt.isRecursive), - fswatch.WithFollowSymlinks(watchOpt.followSymlinks), - fswatch.WithEventTypeFilters(watchOpt.eventTypes), - fswatch.WithLatency(watchOpt.latency), - fswatch.WithMonitorType((fswatch.MonitorType)(watchOpt.monitorType)), - fswatch.WithFilters(watchOpt.filters), - } - handleFileEvent := registerEventHandler(watchOpt, triggerWatcher) - logger.Debug("starting watcher session", zap.Strings("dirs", watchOpt.dirs)) - return fswatch.NewSession(watchOpt.dirs, handleFileEvent, opts...) -} - -func startSession(session *fswatch.Session) { - err := session.Start() - if err != nil { - logger.Error("failed to start watcher", zap.Error(err)) - logger.Warn("make sure you are not reaching your system's max number of open files") - } -} - -func registerEventHandler(watchOpt *watchOpt, triggerWatcher chan struct{}) func([]fswatch.Event) { - return func(events []fswatch.Event) { - for _, event := range events { - if watchOpt.allowReload(event.Path) { - logger.Debug("filesystem change detected", zap.String("path", event.Path)) - triggerWatcher <- struct{}{} - break - } - } + stopWatchSession(session) } } -func listenForFileEvents(trigger chan struct{}, stop chan struct{}) { +func listenForFileEvents() { timer := time.NewTimer(debounceDuration) timer.Stop() defer timer.Stop() for { select { - case <-stop: + case <-stopWatcher: break - case <-trigger: + case <-triggerWatcher: timer.Reset(debounceDuration) case <-timer.C: timer.Stop() diff --git a/watcher.h b/watcher.h new file mode 100644 index 000000000..eeed4229a --- /dev/null +++ b/watcher.h @@ -0,0 +1,6 @@ +#include +#include + +void* start_new_watcher(char const* const path, uintptr_t data); + +int stop_watcher(void* watcher); \ No newline at end of file From 1edd44194e95a53b5171c55d2272c5cfb336ea1f Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 11 Sep 2024 10:49:05 +0200 Subject: [PATCH 077/155] Fixes watch options and tests. --- caddy/caddy_test.go | 8 +- caddy/go.mod | 1 - caddy/go.sum | 2 - caddy/watch_config.go | 1 + caddy/watch_config_test.go | 82 +++++------- go.mod | 1 - go.sum | 2 - watcher.go | 65 +++++----- watcher_options.go | 175 ++++++-------------------- watcher_options_test.go | 248 ++++++++++++++----------------------- watcher_test.go | 46 +------ 11 files changed, 194 insertions(+), 437 deletions(-) diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index 6bb2d7afa..5007a08a8 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -283,13 +283,7 @@ func TestWorkerWithSleepingWatcher(t *testing.T) { frankenphp { worker ../testdata/worker-with-watcher.php 1 - watch { - path . - include \.txt$ - recursive - monitor default - latency 100 - } + watch ./**/*.php } } diff --git a/caddy/go.mod b/caddy/go.mod index ddcd3c6c7..aa9ea7e29 100644 --- a/caddy/go.mod +++ b/caddy/go.mod @@ -50,7 +50,6 @@ require ( github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dolthub/maphash v0.1.0 // indirect - github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 // indirect github.com/dunglas/httpsfv v1.0.2 // indirect github.com/dunglas/mercure v0.16.3 // indirect github.com/dunglas/vulcain v1.0.5 // indirect diff --git a/caddy/go.sum b/caddy/go.sum index c3c820600..5a1b21201 100644 --- a/caddy/go.sum +++ b/caddy/go.sum @@ -141,8 +141,6 @@ github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/dunglas/caddy-cbrotli v1.0.0 h1:+WNqXBkWyMcIpXB2rVZ3nwcElUbuAzf0kPxNXU4D+u0= github.com/dunglas/caddy-cbrotli v1.0.0/go.mod h1:KZsUu3fnQBgO0o3YDoQuO3Z61dFgUncr1F8rg8acwQw= -github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 h1:F5jAHx1qL6uxK0NCyBZ5N3Agrf+HZITjtWW1Jwrdz84= -github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15/go.mod h1:7Yj67KBnUukcR0gbP9HPCmyUKGEQ2mzDI3rs2fLZl0s= github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0= github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= github.com/dunglas/mercure v0.16.3 h1:zDEBFpvV61SlJnJYhFM87GKB4c2F4zdOKfs/xnrw/7Y= diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 2e2b19d04..545c2be49 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -50,6 +50,7 @@ func parseFullPattern(filePattern string) watchConfig { watchConfig.Pattern = strings.Split(filePattern, "/**/")[1] watchConfig.IsRecursive = true } + dirName = strings.TrimRight(dirName, "/") watchConfig.Dirs = []string{dirName} return watchConfig diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go index 69d9050aa..6757ca93b 100644 --- a/caddy/watch_config_test.go +++ b/caddy/watch_config_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -func TestParsingARecursiveShortForm(t *testing.T) { +func TestParsingARecursiveDirectory(t *testing.T) { app, err := parseTestConfig(` frankenphp { watch /path @@ -18,90 +18,64 @@ func TestParsingARecursiveShortForm(t *testing.T) { assert.Equal(t, 1, len(app.Watch)) assert.Equal(t, 1, len(app.Watch[0].Dirs)) assert.Equal(t, "/path", app.Watch[0].Dirs[0]) - assert.True(t, app.Watch[0].Recursive) - assert.True(t, app.Watch[0].IsShortForm) + assert.True(t, app.Watch[0].IsRecursive) + assert.Equal(t, "", app.Watch[0].Pattern) } -func TestParseTwoShortForms(t *testing.T) { +func TestParsingARecursiveDirectoryWithPattern(t *testing.T) { app, err := parseTestConfig(` frankenphp { - watch /path - watch /other/path poll + watch /path/**/*.php } `) assert.Nil(t, err) - assert.Equal(t, 2, len(app.Watch)) + assert.Equal(t, 1, len(app.Watch)) assert.Equal(t, "/path", app.Watch[0].Dirs[0]) - assert.Equal(t, "", app.Watch[0].MonitorType) - assert.Equal(t, "/other/path", app.Watch[1].Dirs[0]) - assert.Equal(t, "poll", app.Watch[1].MonitorType) + assert.True(t, app.Watch[0].IsRecursive) + assert.Equal(t, "*.php", app.Watch[0].Pattern) } -func TestFailOnInvalidMonitorType(t *testing.T) { - _, err := parseTestConfig(` +func TestParsingNonRecursiveDirectoryWithPattern(t *testing.T) { + app, err := parseTestConfig(` frankenphp { - watch /path invalid_monitor + watch /path/*.php } `) - assert.Error(t, err) + assert.Nil(t, err) + assert.Equal(t, 1, len(app.Watch)) + assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.False(t, app.Watch[0].IsRecursive) + assert.Equal(t, "*.php", app.Watch[0].Pattern) } -func TestFailOnMissingPathInShortForm(t *testing.T) { - _, err := parseTestConfig(` +func TestParseTwoShortForms(t *testing.T) { + app, err := parseTestConfig(` frankenphp { - watch + watch /path + watch /other/path/*.php } `) - assert.Error(t, err) + assert.Nil(t, err) + assert.Equal(t, 2, len(app.Watch)) + assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.Equal(t, "", app.Watch[0].Pattern) + assert.Equal(t, "/other/path", app.Watch[1].Dirs[0]) + assert.Equal(t, "*.php", app.Watch[1].Pattern) } -func TestFailOnMissingPathInLongForm(t *testing.T) { +func TestFailOnMissingPath(t *testing.T) { _, err := parseTestConfig(` frankenphp { - watch { - monitor_type poll - } + watch } `) assert.Error(t, err) } -func TestParseLongFormCorrectly(t *testing.T) { - app, err := parseTestConfig(` - frankenphp { - watch { - path /path - recursive false - symlinks true - case_sensitive true - extended_regex true - include *important.txt - exclude *.txt - pattern *important.txt - monitor_type poll - latency 100 - } - } - `) - - assert.Nil(t, err) - assert.Equal(t, 1, len(app.Watch)) - assert.Equal(t, "/path", app.Watch[0].Dirs[0]) - assert.False(t, app.Watch[0].Recursive) - assert.True(t, app.Watch[0].FollowSymlinks) - assert.True(t, app.Watch[0].CaseSensitive) - assert.True(t, app.Watch[0].ExtendedRegex) - assert.Equal(t, "*important.txt", app.Watch[0].IncludeFiles) - assert.Equal(t, "*.txt", app.Watch[0].ExcludeFiles) - assert.Equal(t, "*important.txt", app.Watch[0].WildcardPattern) - assert.Equal(t, "poll", app.Watch[0].MonitorType) - assert.Equal(t, 100, app.Watch[0].Latency) -} - func parseTestConfig(config string) (*caddy.FrankenPHPApp, error) { app := caddy.FrankenPHPApp{} err := app.UnmarshalCaddyfile(caddyfile.NewTestDispenser(config)) diff --git a/go.mod b/go.mod index d17b6154a..c82ae18fb 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ toolchain go1.22.6 retract v1.0.0-rc.1 // Human error require ( - github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 github.com/maypok86/otter v1.2.2 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 diff --git a/go.sum b/go.sum index 74b1a598f..4d010a6f2 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ 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/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= -github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15 h1:F5jAHx1qL6uxK0NCyBZ5N3Agrf+HZITjtWW1Jwrdz84= -github.com/dunglas/go-fswatch v0.0.0-20240820155803-888450fb7f15/go.mod h1:7Yj67KBnUukcR0gbP9HPCmyUKGEQ2mzDI3rs2fLZl0s= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= diff --git a/watcher.go b/watcher.go index dbef8e6b7..73a1c60ab 100644 --- a/watcher.go +++ b/watcher.go @@ -20,6 +20,8 @@ type watcher struct { sessions []unsafe.Pointer workerOpts []workerOpt watchOpts []watchOpt + trigger chan struct{} + stop chan struct{} } // duration to wait before reloading workers after a file change @@ -28,10 +30,8 @@ const debounceDuration = 150 * time.Millisecond var ( // the currently active file watcher activeWatcher *watcher - // when stopping the watcher we need to wait for reloading to finish + // after stopping the watcher we will wait for eventual reloads to finish reloadWaitGroup sync.WaitGroup - triggerWatcher chan struct{} - stopWatcher chan struct{} ) func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { @@ -58,7 +58,32 @@ func drainWatcher() { activeWatcher = nil } -func startWatchOpt(watchOpt *watchOpt) (unsafe.Pointer, error) { +func (w *watcher) startWatching(watchOpts []watchOpt) error { + w.trigger = make(chan struct{}) + w.stop = make(chan struct{}) + w.sessions = make([]unsafe.Pointer, len(watchOpts)) + w.watchOpts = watchOpts + for i, watchOpt := range w.watchOpts { + watchOpt.trigger = w.trigger + session, err := startSession(&watchOpt) + if err != nil { + logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) + return err + } + w.sessions[i] = session + } + go listenForFileEvents(w.trigger, w.stop) + return nil +} + +func (w *watcher) stopWatching() { + close(w.stop) + for _, session := range w.sessions { + stopSession(session) + } +} + +func startSession(watchOpt *watchOpt) (unsafe.Pointer, error) { handle := cgo.NewHandle(watchOpt) cPathTranslated := (*C.char)(C.CString(watchOpt.dirs[0])) watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) @@ -69,9 +94,9 @@ func startWatchOpt(watchOpt *watchOpt) (unsafe.Pointer, error) { return watchSession, nil } -func stopWatchSession(session unsafe.Pointer){ +func stopSession(session unsafe.Pointer){ success := C.stop_watcher(session) - if(success == 0){ + if(success == 1){ logger.Error("couldn't stop watching") } } @@ -81,35 +106,11 @@ func go_handle_event(path *C.char, eventType C.int, pathType C.int, handle C.uin watchOpt := cgo.Handle(handle).Value().(*watchOpt) if watchOpt.allowReload(C.GoString(path), int(eventType), int(pathType)) { logger.Debug("valid file change detected", zap.String("path", C.GoString(path))) - triggerWatcher <- struct{}{} - } -} - -func (w *watcher) startWatching(watchOpts []watchOpt) error { - w.sessions = make([]unsafe.Pointer, len(watchOpts)) - w.watchOpts = watchOpts - triggerWatcher = make(chan struct{}) - stopWatcher = make(chan struct{}) - for i, watchOpt := range w.watchOpts { - session, err := startWatchOpt(&watchOpt) - if err != nil { - logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) - return err - } - w.sessions[i] = session - } - go listenForFileEvents() - return nil -} - -func (w *watcher) stopWatching() { - close(stopWatcher) - for _, session := range w.sessions { - stopWatchSession(session) + watchOpt.trigger <- struct{}{} } } -func listenForFileEvents() { +func listenForFileEvents(triggerWatcher chan struct{}, stopWatcher chan struct{}) { timer := time.NewTimer(debounceDuration) timer.Stop() defer timer.Stop() diff --git a/watcher_options.go b/watcher_options.go index 56374db70..a284248f2 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -1,11 +1,8 @@ package frankenphp import ( - fswatch "github.com/dunglas/go-fswatch" "go.uber.org/zap" "path/filepath" - "strings" - "time" ) type WatchOption func(o *watchOpt) error @@ -13,27 +10,8 @@ type WatchOption func(o *watchOpt) error type watchOpt struct { dirs []string isRecursive bool - followSymlinks bool - latency float64 - wildCardPattern string - filters []fswatch.Filter - monitorType fswatch.MonitorType - eventTypes []fswatch.EventType -} - -func getDefaultWatchOpt() watchOpt { - return watchOpt{ - isRecursive: true, - latency: 0.15, - monitorType: fswatch.SystemDefaultMonitor, - eventTypes: parseEventTypes(), - } -} - -func WithWatcherShortForm(fileName string) WatchOption { - return func(o *watchOpt) error { - return parseShortForm(o, fileName) - } + pattern string + trigger chan struct{} } func WithWatcherDirs(dirs []string) WatchOption { @@ -49,27 +27,6 @@ func WithWatcherDirs(dirs []string) WatchOption { } } -func WithWatcherFilters(includeFiles string, excludeFiles string, caseSensitive bool, extendedRegex bool) WatchOption { - return func(o *watchOpt) error { - o.filters = parseFilters(includeFiles, excludeFiles, caseSensitive, extendedRegex) - return nil - } -} - -func WithWatcherLatency(latency int) WatchOption { - return func(o *watchOpt) error { - o.latency = (float64)(latency) * time.Millisecond.Seconds() - return nil - } -} - -func WithWatcherMonitorType(monitorType string) WatchOption { - return func(o *watchOpt) error { - o.monitorType = parseMonitorType(monitorType) - return nil - } -} - func WithWatcherRecursion(withRecursion bool) WatchOption { return func(o *watchOpt) error { o.isRecursive = withRecursion @@ -77,102 +34,13 @@ func WithWatcherRecursion(withRecursion bool) WatchOption { } } -func WithWatcherSymlinks(withSymlinks bool) WatchOption { +func WithWatcherPattern(pattern string) WatchOption { return func(o *watchOpt) error { - o.followSymlinks = withSymlinks + o.pattern = pattern return nil } } -func WithWildcardPattern(pattern string) WatchOption { - return func(o *watchOpt) error { - o.wildCardPattern = pattern - return nil - } -} - -// for the one line shortform in the caddy config, aka: 'watch /path/*pattern' -func parseShortForm(watchOpt *watchOpt, fileName string) error { - watchOpt.isRecursive = true - dirName := fileName - splitDirName, baseName := filepath.Split(fileName) - if fileName != "." && fileName != ".." && strings.ContainsAny(baseName, "*.") { - dirName = splitDirName - watchOpt.wildCardPattern = baseName - watchOpt.isRecursive = false - } - - if strings.Contains(fileName, "/**/") { - dirName = strings.Split(fileName, "/**/")[0] - watchOpt.wildCardPattern = strings.Split(fileName, "/**/")[1] - watchOpt.isRecursive = true - } - - absDir, err := parseAbsPath(dirName) - if err != nil { - return err - } - watchOpt.dirs = []string{absDir} - return nil -} - -func parseFilters(include string, exclude string, caseSensitive bool, extended bool) []fswatch.Filter { - filters := []fswatch.Filter{} - - if include != "" && exclude == "" { - exclude = "\\." - } - - if include != "" { - includeFilter := fswatch.Filter{ - Text: include, - FilterType: fswatch.FilterInclude, - CaseSensitive: caseSensitive, - Extended: extended, - } - filters = append(filters, includeFilter) - } - - if exclude != "" { - excludeFilter := fswatch.Filter{ - Text: exclude, - FilterType: fswatch.FilterExclude, - CaseSensitive: caseSensitive, - Extended: extended, - } - filters = append(filters, excludeFilter) - } - return filters -} - -func parseMonitorType(monitorType string) fswatch.MonitorType { - switch monitorType { - case "fsevents": - return fswatch.FseventsMonitor - case "kqueue": - return fswatch.KqueueMonitor - case "inotify": - return fswatch.InotifyMonitor - case "windows": - return fswatch.WindowsMonitor - case "poll": - return fswatch.PollMonitor - case "fen": - return fswatch.FenMonitor - default: - return fswatch.SystemDefaultMonitor - } -} - -func parseEventTypes() []fswatch.EventType { - return []fswatch.EventType{ - fswatch.Created, - fswatch.Updated, - fswatch.Renamed, - fswatch.Removed, - } -} - func parseAbsPath(path string) (string, error) { absDir, err := filepath.Abs(path) if err != nil { @@ -182,15 +50,42 @@ func parseAbsPath(path string) (string, error) { return absDir, nil } -func (watchOpt *watchOpt) allowReload(fileName string) bool { - if watchOpt.wildCardPattern == "" { +func (watchOpt *watchOpt) allowReload(fileName string, eventType int, pathType int) bool { + if(!isValidEventType(eventType) || !isValidPathType(pathType)) { + return false + } + if watchOpt.pattern == "" { return true } baseName := filepath.Base(fileName) - patternMatches, err := filepath.Match(watchOpt.wildCardPattern, baseName) + patternMatches, err := filepath.Match(watchOpt.pattern, baseName) if err != nil { logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) return false } - return patternMatches + if(watchOpt.isRecursive){ + return patternMatches + } + fileNameDir := filepath.Dir(fileName) + for _, dir := range watchOpt.dirs { + if dir == fileNameDir { + return patternMatches + } + } + return false +} + +// 0:rename,1:modify,2:create,3:destroy,4:owner,5:other, +func isValidEventType(eventType int) bool { + return eventType <= 3 +} + +// 0:dir,1:file,2:hard_link,3:sym_link,4:watcher,5:other, +func isValidPathType(eventType int) bool { + return eventType <= 2 +} + + +func isValidPath(fileName string) bool { + return fileName != "" } diff --git a/watcher_options_test.go b/watcher_options_test.go index 0083cc6e8..ccac90766 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -1,216 +1,148 @@ package frankenphp import ( - fswatch "github.com/dunglas/go-fswatch" "github.com/stretchr/testify/assert" "path/filepath" "testing" ) -func TestSimpleRecursiveWatchOption(t *testing.T) { - const shortForm = "/some/path" +func TestWithRecursion(t *testing.T) { + watchOpt := createWithOptions(t, WithWatcherRecursion(true)) - watchOpt := createFromShortForm(shortForm, t) - - assert.Empty(t, watchOpt.wildCardPattern) - assert.Equal(t, "/some/path", watchOpt.dirs[0]) assert.True(t, watchOpt.isRecursive) } -func TestSingleFileWatchOption(t *testing.T) { - const shortForm = "/some/path/watch-me.json" - - watchOpt := createFromShortForm(shortForm, t) - - assert.Equal(t, "watch-me.json", watchOpt.wildCardPattern) - assert.Equal(t, "/some/path", watchOpt.dirs[0]) - assert.False(t, watchOpt.isRecursive) -} - -func TestNonRecursivePatternWatchOption(t *testing.T) { - const shortForm = "/some/path/*.json" - - watchOpt := createFromShortForm(shortForm, t) - - assert.Equal(t, "*.json", watchOpt.wildCardPattern) - assert.Equal(t, "/some/path", watchOpt.dirs[0]) - assert.False(t, watchOpt.isRecursive) -} - -func TestRecursivePatternWatchOption(t *testing.T) { - const shortForm = "/some/path/**/*.json" +func TestWithWatcherPattern(t *testing.T) { + watchOpt := createWithOptions(t, WithWatcherPattern("*php")) - watchOpt := createFromShortForm(shortForm, t) - - assert.Equal(t, "*.json", watchOpt.wildCardPattern) - assert.Equal(t, "/some/path", watchOpt.dirs[0]) - assert.True(t, watchOpt.isRecursive) + assert.Equal(t, "*php", watchOpt.pattern) } -func TestRelativePathname(t *testing.T) { - const shortForm = "../testdata/**/*.txt" - absPath, err := filepath.Abs("../testdata") +func TestWithWatcherDir(t *testing.T) { + watchOpt := createWithOptions(t, WithWatcherDirs([]string{"/path/to/app"})) - watchOpt := createFromShortForm(shortForm, t) - - assert.NoError(t, err) - assert.Equal(t, "*.txt", watchOpt.wildCardPattern) - assert.Equal(t, absPath, watchOpt.dirs[0]) - assert.True(t, watchOpt.isRecursive) + assert.Equal(t, "/path/to/app", watchOpt.dirs[0]) } -func TestCurrentRelativePath(t *testing.T) { - const shortForm = "." - absPath, err := filepath.Abs(shortForm) +func TestWithRelativeWatcherDir(t *testing.T) { + absoluteDir, err := filepath.Abs(".") - watchOpt := createFromShortForm(shortForm, t) + watchOpt := createWithOptions(t, WithWatcherDirs([]string{"."})) assert.NoError(t, err) - assert.Equal(t, "", watchOpt.wildCardPattern) - assert.Equal(t, absPath, watchOpt.dirs[0]) - assert.True(t, watchOpt.isRecursive) -} - -func TestMatchPatternWithoutExtension(t *testing.T) { - const shortForm = "/some/path/**/fileName" - - watchOpt := createFromShortForm(shortForm, t) - - assert.Equal(t, "fileName", watchOpt.wildCardPattern) - assert.Equal(t, "/some/path", watchOpt.dirs[0]) - assert.True(t, watchOpt.isRecursive) -} - -func TestAddingTwoFilePaths(t *testing.T) { - watchOpt := getDefaultWatchOpt() - applyFirstPath := WithWatcherDirs([]string{"/first/path"}) - applySecondPath := WithWatcherDirs([]string{"/second/path"}) - - err1 := applyFirstPath(&watchOpt) - err2 := applySecondPath(&watchOpt) - - assert.NoError(t, err1) - assert.NoError(t, err2) - assert.Equal(t, []string{"/first/path", "/second/path"}, watchOpt.dirs) -} - -func TestAddingAnInclusionFilterWithDefaultForExclusion(t *testing.T) { - expectedInclusionFilter := fswatch.Filter{ - Text: "\\.php$", - FilterType: fswatch.FilterInclude, - CaseSensitive: true, - Extended: true, - } - expectedExclusionFilter := fswatch.Filter{ - Text: "\\.", - FilterType: fswatch.FilterExclude, - CaseSensitive: true, - Extended: true, - } - - watchOpt := createWithOption(WithWatcherFilters("\\.php$", "", true, true), t) - - assert.Equal(t, []fswatch.Filter{expectedInclusionFilter, expectedExclusionFilter}, watchOpt.filters) -} - -func TestWithExclusionFilter(t *testing.T) { - expectedExclusionFilter := fswatch.Filter{ - Text: "\\.php$", - FilterType: fswatch.FilterExclude, - CaseSensitive: false, - Extended: false, - } - - watchOpt := createWithOption(WithWatcherFilters("", "\\.php$", false, false), t) - - assert.Equal(t, []fswatch.Filter{expectedExclusionFilter}, watchOpt.filters) + assert.Equal(t, absoluteDir, watchOpt.dirs[0]) } -func TestWithPollMonitor(t *testing.T) { - watchOpt := createWithOption(WithWatcherMonitorType("poll"), t) - - assert.Equal(t, (int)(fswatch.PollMonitor), (int)(watchOpt.monitorType)) -} - -func TestWithSymlinks(t *testing.T) { - watchOpt := createWithOption(WithWatcherSymlinks(true), t) - - assert.True(t, watchOpt.followSymlinks) -} - -func TestWithoutRecursion(t *testing.T) { - watchOpt := createWithOption(WithWatcherRecursion(false), t) +func TestAllowReloadOnMatchingPattern(t *testing.T) { + const fileName = "/some/path/watch-me.php" + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("*.php"), + ) - assert.False(t, watchOpt.isRecursive) + assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) } -func TestWithLatency(t *testing.T) { - watchOpt := createWithOption(WithWatcherLatency(500), t) +func TestAllowReloadOnExactMatch(t *testing.T) { + const fileName = "/some/path/watch-me.php" + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("watch-me.php"), + ) - assert.Equal(t, 0.5, watchOpt.latency) + assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) } -func TestWithWildcardPattern(t *testing.T) { - watchOpt := createWithOption(WithWildcardPattern("*php"), t) +func TestDisallowOnDifferentFilename(t *testing.T) { + const fileName = "/some/path/watch-me.php" + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("dont-watch.php"), + ) - assert.Equal(t, "*php", watchOpt.wildCardPattern) + assert.False(t, watchOpt.allowReload(fileName, 0 , 0)) } -func TestAllowReloadOnMatch(t *testing.T) { +func TestAllowReloadOnRecursiveDirectory(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createFromShortForm("/some/path/**/*.php", t) + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(true), + WithWatcherPattern("*.php"), + ) - assert.True(t, watchOpt.allowReload(fileName)) + assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) } -func TestAllowReloadOnExactMatch(t *testing.T) { +func TestAllowReloadWithRecursionAndNoPattern(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createFromShortForm("/some/path/watch-me.php", t) + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(true), + ) - assert.True(t, watchOpt.allowReload(fileName)) + assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) } -func TestDisallowReload(t *testing.T) { +func TestDisallowOnDifferentPatterns(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createFromShortForm("/some/path/dont-watch.php", t) + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(true), + WithWatcherPattern(".txt"), + ) - assert.False(t, watchOpt.allowReload(fileName)) + assert.False(t, watchOpt.allowReload(fileName, 0 , 0)) } -func TestAllowReloadOnRecursiveDirectory(t *testing.T) { +func TestDisallowOnMissingRecursion(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createFromShortForm("/some", t) + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(false), + WithWatcherPattern(".php"), + ) - assert.True(t, watchOpt.allowReload(fileName)) + assert.False(t, watchOpt.allowReload(fileName, 0 , 0)) } -func TestAllowReloadIfOptionIsNotAWildcard(t *testing.T) { +func TestDisallowOnEventTypeBiggerThan3(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := getDefaultWatchOpt() + const eventType = 4 + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("watch-me.php"), + ) - assert.True(t, watchOpt.allowReload(fileName)) + assert.False(t, watchOpt.allowReload(fileName, eventType , 0)) } -func TestDisallowExplicitlySetWildcardPattern(t *testing.T) { - const fileName = "/some/path/file.txt" - watchOpt := createWithOption(WithWildcardPattern("*php"), t) - - assert.False(t, watchOpt.allowReload(fileName)) -} +func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { + const fileName = "/some/path/watch-me.php" + const pathType = 3 + watchOpt := createWithOptions( + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("watch-me.php"), + ) -func createFromShortForm(shortForm string, t *testing.T) watchOpt { - watchOpt := getDefaultWatchOpt() - applyOptions := WithWatcherShortForm(shortForm) - err := applyOptions(&watchOpt) - assert.NoError(t, err) - return watchOpt + assert.False(t, watchOpt.allowReload(fileName, 0 , pathType)) } -func createWithOption(applyOptions WatchOption, t *testing.T) watchOpt { - watchOpt := getDefaultWatchOpt() +func createWithOptions(t *testing.T, applyOptions ...WatchOption) watchOpt { + watchOpt :=watchOpt{} - err := applyOptions(&watchOpt) - - assert.NoError(t, err) + for _, applyOption := range applyOptions { + err := applyOption(&watchOpt) + assert.NoError(t, err) + } return watchOpt } diff --git a/watcher_test.go b/watcher_test.go index b1b65d5d1..2e0f31561 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -22,31 +22,10 @@ const minTimesToPollForChanges = 3 const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.txt" - watchOptions := []frankenphp.WatchOption{frankenphp.WithWatcherShortForm(filePattern)} - - runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { - requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) - assert.True(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) -} - -func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { - const filePattern = "./testdata/**/*.php" - watchOptions := []frankenphp.WatchOption{frankenphp.WithWatcherShortForm(filePattern)} - - runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { - requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) - assert.False(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) -} - -func TestWorkersReloadOnMatchingIncludedRegex(t *testing.T) { - const include = "\\.txt$" watchOptions := []frankenphp.WatchOption{ frankenphp.WithWatcherDirs([]string{"./testdata"}), + frankenphp.WithWatcherPattern("*.txt"), frankenphp.WithWatcherRecursion(true), - frankenphp.WithWatcherFilters(include, "", true, false), } runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { @@ -55,13 +34,12 @@ func TestWorkersReloadOnMatchingIncludedRegex(t *testing.T) { }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) } -func TestWorkersDoNotReloadOnExcludingRegex(t *testing.T) { - const exclude = "\\.txt$" +func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { watchOptions := []frankenphp.WatchOption{ - frankenphp.WithWatcherDirs([]string{"./testdata"}), - frankenphp.WithWatcherRecursion(true), - frankenphp.WithWatcherFilters("", exclude, false, false), - } + frankenphp.WithWatcherDirs([]string{"./testdata"}), + frankenphp.WithWatcherPattern("*.php"), + frankenphp.WithWatcherRecursion(true), + } runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) @@ -69,18 +47,6 @@ func TestWorkersDoNotReloadOnExcludingRegex(t *testing.T) { }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) } -func TestWorkerShouldReloadUsingPolling(t *testing.T) { - watchOptions := []frankenphp.WatchOption{ - frankenphp.WithWatcherDirs([]string{"./testdata/files"}), - frankenphp.WithWatcherMonitorType("poll"), - } - - runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { - requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) - assert.True(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) -} - func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string { req := httptest.NewRequest(method, url, nil) w := httptest.NewRecorder() From ba26532bc1cf434a3e159b066ed5cc8ef047a49c Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 11 Sep 2024 10:50:20 +0200 Subject: [PATCH 078/155] go fmt. --- caddy/watch_config.go | 7 ++-- watcher.go | 27 ++++++------- watcher_options.go | 23 +++++------ watcher_options_test.go | 90 ++++++++++++++++++++--------------------- watcher_test.go | 8 ++-- 5 files changed, 76 insertions(+), 79 deletions(-) diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 545c2be49..2867d942b 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -3,8 +3,8 @@ package caddy import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/dunglas/frankenphp" - "strings" "path/filepath" + "strings" ) type watchConfig struct { @@ -26,8 +26,8 @@ func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frank func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { if !d.NextArg() { - return d.Err("The 'watch' directive must be followed by a path") - } + return d.Err("The 'watch' directive must be followed by a path") + } f.Watch = append(f.Watch, parseFullPattern(d.Val())) return nil @@ -55,4 +55,3 @@ func parseFullPattern(filePattern string) watchConfig { return watchConfig } - diff --git a/watcher.go b/watcher.go index 73a1c60ab..142643ecf 100644 --- a/watcher.go +++ b/watcher.go @@ -7,21 +7,20 @@ package frankenphp // #include "watcher.h" import "C" import ( - + "errors" "go.uber.org/zap" + "runtime/cgo" "sync" "time" - "runtime/cgo" "unsafe" - "errors" ) type watcher struct { sessions []unsafe.Pointer workerOpts []workerOpt - watchOpts []watchOpt - trigger chan struct{} - stop chan struct{} + watchOpts []watchOpt + trigger chan struct{} + stop chan struct{} } // duration to wait before reloading workers after a file change @@ -86,17 +85,17 @@ func (w *watcher) stopWatching() { func startSession(watchOpt *watchOpt) (unsafe.Pointer, error) { handle := cgo.NewHandle(watchOpt) cPathTranslated := (*C.char)(C.CString(watchOpt.dirs[0])) - watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) - if(watchSession == C.NULL){ - logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) - return nil, errors.New("couldn't start watching") - } + watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) + if watchSession == C.NULL { + logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) + return nil, errors.New("couldn't start watching") + } return watchSession, nil } -func stopSession(session unsafe.Pointer){ +func stopSession(session unsafe.Pointer) { success := C.stop_watcher(session) - if(success == 1){ + if success == 1 { logger.Error("couldn't stop watching") } } @@ -105,7 +104,7 @@ func stopSession(session unsafe.Pointer){ func go_handle_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) { watchOpt := cgo.Handle(handle).Value().(*watchOpt) if watchOpt.allowReload(C.GoString(path), int(eventType), int(pathType)) { - logger.Debug("valid file change detected", zap.String("path", C.GoString(path))) + logger.Debug("valid file change detected", zap.String("path", C.GoString(path))) watchOpt.trigger <- struct{}{} } } diff --git a/watcher_options.go b/watcher_options.go index a284248f2..1aa0a902e 100644 --- a/watcher_options.go +++ b/watcher_options.go @@ -8,10 +8,10 @@ import ( type WatchOption func(o *watchOpt) error type watchOpt struct { - dirs []string - isRecursive bool - pattern string - trigger chan struct{} + dirs []string + isRecursive bool + pattern string + trigger chan struct{} } func WithWatcherDirs(dirs []string) WatchOption { @@ -51,7 +51,7 @@ func parseAbsPath(path string) (string, error) { } func (watchOpt *watchOpt) allowReload(fileName string, eventType int, pathType int) bool { - if(!isValidEventType(eventType) || !isValidPathType(pathType)) { + if !isValidEventType(eventType) || !isValidPathType(pathType) { return false } if watchOpt.pattern == "" { @@ -63,15 +63,15 @@ func (watchOpt *watchOpt) allowReload(fileName string, eventType int, pathType i logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) return false } - if(watchOpt.isRecursive){ + if watchOpt.isRecursive { return patternMatches } fileNameDir := filepath.Dir(fileName) - for _, dir := range watchOpt.dirs { - if dir == fileNameDir { - return patternMatches - } - } + for _, dir := range watchOpt.dirs { + if dir == fileNameDir { + return patternMatches + } + } return false } @@ -85,7 +85,6 @@ func isValidPathType(eventType int) bool { return eventType <= 2 } - func isValidPath(fileName string) bool { return fileName != "" } diff --git a/watcher_options_test.go b/watcher_options_test.go index ccac90766..ac5c7c784 100644 --- a/watcher_options_test.go +++ b/watcher_options_test.go @@ -41,104 +41,104 @@ func TestAllowReloadOnMatchingPattern(t *testing.T) { WithWatcherPattern("*.php"), ) - assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) + assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func TestAllowReloadOnExactMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some/path"}), - WithWatcherPattern("watch-me.php"), - ) + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("watch-me.php"), + ) - assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) + assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func TestDisallowOnDifferentFilename(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some/path"}), - WithWatcherPattern("dont-watch.php"), - ) + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("dont-watch.php"), + ) - assert.False(t, watchOpt.allowReload(fileName, 0 , 0)) + assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } func TestAllowReloadOnRecursiveDirectory(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some"}), - WithWatcherRecursion(true), - WithWatcherPattern("*.php"), - ) + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(true), + WithWatcherPattern("*.php"), + ) - assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) + assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func TestAllowReloadWithRecursionAndNoPattern(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some"}), - WithWatcherRecursion(true), - ) + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(true), + ) - assert.True(t, watchOpt.allowReload(fileName, 0 , 0)) + assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func TestDisallowOnDifferentPatterns(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some"}), - WithWatcherRecursion(true), - WithWatcherPattern(".txt"), - ) + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(true), + WithWatcherPattern(".txt"), + ) - assert.False(t, watchOpt.allowReload(fileName, 0 , 0)) + assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } func TestDisallowOnMissingRecursion(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some"}), - WithWatcherRecursion(false), - WithWatcherPattern(".php"), - ) + t, + WithWatcherDirs([]string{"/some"}), + WithWatcherRecursion(false), + WithWatcherPattern(".php"), + ) - assert.False(t, watchOpt.allowReload(fileName, 0 , 0)) + assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } func TestDisallowOnEventTypeBiggerThan3(t *testing.T) { const fileName = "/some/path/watch-me.php" const eventType = 4 watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some/path"}), - WithWatcherPattern("watch-me.php"), - ) + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("watch-me.php"), + ) - assert.False(t, watchOpt.allowReload(fileName, eventType , 0)) + assert.False(t, watchOpt.allowReload(fileName, eventType, 0)) } func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { const fileName = "/some/path/watch-me.php" const pathType = 3 watchOpt := createWithOptions( - t, - WithWatcherDirs([]string{"/some/path"}), - WithWatcherPattern("watch-me.php"), - ) + t, + WithWatcherDirs([]string{"/some/path"}), + WithWatcherPattern("watch-me.php"), + ) - assert.False(t, watchOpt.allowReload(fileName, 0 , pathType)) + assert.False(t, watchOpt.allowReload(fileName, 0, pathType)) } func createWithOptions(t *testing.T, applyOptions ...WatchOption) watchOpt { - watchOpt :=watchOpt{} + watchOpt := watchOpt{} for _, applyOption := range applyOptions { err := applyOption(&watchOpt) diff --git a/watcher_test.go b/watcher_test.go index 2e0f31561..fed895169 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -36,10 +36,10 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { watchOptions := []frankenphp.WatchOption{ - frankenphp.WithWatcherDirs([]string{"./testdata"}), - frankenphp.WithWatcherPattern("*.php"), - frankenphp.WithWatcherRecursion(true), - } + frankenphp.WithWatcherDirs([]string{"./testdata"}), + frankenphp.WithWatcherPattern("*.php"), + frankenphp.WithWatcherRecursion(true), + } runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) From e4377e9f4b84e26d2e44d75cf55b5add632d8bd6 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 11 Sep 2024 11:15:30 +0200 Subject: [PATCH 079/155] Adds TODO. --- worker.go | 1 + 1 file changed, 1 insertion(+) diff --git a/worker.go b/worker.go index 8389e373d..45ca0683b 100644 --- a/worker.go +++ b/worker.go @@ -158,6 +158,7 @@ func restartWorkers(workerOpts []workerOpt) { panic(err) } logger.Info("workers restarted successfully") + //TODO: Clear op_cache here at some point } //export go_frankenphp_worker_ready From 08b8f42802355d0f555af971002e42aeb3ac5794 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 11 Sep 2024 11:18:52 +0200 Subject: [PATCH 080/155] Installs edant/watcher in the bookworm image. --- Dockerfile | 23 +++++++++++------------ dev.Dockerfile | 19 +++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 020f153a2..7d321ad70 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,6 +68,8 @@ RUN apt-get update && \ libssl-dev \ libxml2-dev \ zlib1g-dev \ + git \ + meson \ && \ apt-get clean @@ -86,17 +88,14 @@ COPY --link caddy caddy COPY --link internal internal COPY --link testdata testdata -# install fswatch (necessary for file watching) -ARG FSWATCH_VERSION -# in the future, we may replace this custom compilation by the installation of https://packages.debian.org/bookworm/fswatch, which provides the library and headers, but the version currently shipped by Debian is too old -WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz -WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && \ - make -j"$(nproc)" && \ - make install && \ - ldconfig && \ - fswatch --version +# install edant/watcher (necessary for file watching) +WORKDIR /usr/local/src/watcher +RUN git clone --branch=next https://github.com/e-dant/watcher . +WORKDIR /usr/local/src/watcher/watcher-c +RUN meson build .. && \ + meson compile -C build && \ + cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ + ldconfig # See https://github.com/docker-library/php/blob/master/8.3/bookworm/zts/Dockerfile#L57-L59 for PHP values ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" @@ -116,7 +115,7 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 -COPY --from=builder /usr/local/lib/libfswatch.so* /usr/local/lib/ +COPY --from=builder /usr/local/lib/libwatcher-c* /usr/local/lib/ RUN ldconfig COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp diff --git a/dev.Dockerfile b/dev.Dockerfile index afb64ffae..e82e319e9 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -41,6 +41,7 @@ RUN apt-get update && \ valgrind \ neovim \ zsh \ + meson \ libtool-bin && \ echo 'set auto-load safe-path /' > /root/.gdbinit && \ echo '* soft core unlimited' >> /etc/security/limits.conf \ @@ -65,16 +66,14 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ echo "opcache.enable=1" >> /usr/local/lib/php.ini && \ php --version -# install fswatch (necessary for file watching) -ARG FSWATCH_VERSION='1.17.1' -WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz -WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && \ - make -j"$(nproc)" && \ - make install && \ - ldconfig && \ - fswatch --version +# install edant/watcher (necessary for file watching) +WORKDIR /usr/local/src/watcher +RUN git clone --branch=next https://github.com/e-dant/watcher . +WORKDIR /usr/local/src/watcher/watcher-c +RUN meson build .. && \ + meson compile -C build && \ + cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ + ldconfig WORKDIR /go/src/app COPY . . From c775523fa910c40c95a085f01428271bf5ae250b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 11 Sep 2024 11:39:43 +0200 Subject: [PATCH 081/155] Fixes linting. --- docs/config.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index f7efe7758..5ddbdba11 100644 --- a/docs/config.md +++ b/docs/config.md @@ -146,7 +146,7 @@ development environments. } ``` -The configuration above will watch the `/path/to/app` directory recursively. +The configuration above will watch the `/path/to/app` directory recursively. If any file changes, the worker will be restarted. You can also add multiple `watch` directives and use simple wildcard patterns, the following is valid: @@ -178,6 +178,7 @@ Multiple directories can also be watched in one block: ``` #### Some notes + - Directories can also be relative (to where the frankenphp process was started from) - The `/**/` pattern signifies recursive watching - If the last part of the pattern contains the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the From c2b7d500e0c67d015b392fdadb0e755aec38e38d Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 12 Sep 2024 10:26:37 +0200 Subject: [PATCH 082/155] Refactors the watcher into its own module. --- caddy/watch_config.go | 7 +-- caddy/watch_config_test.go | 1 + frankenphp.go | 9 +++- frankenphp_test.go | 3 +- options.go | 7 +-- watcher-c.h => watcher/watcher-c.h | 0 watcher.c => watcher/watcher.c | 0 watcher.go => watcher/watcher.go | 43 +++++++++++-------- watcher.h => watcher/watcher.h | 0 .../watcher_options.go | 14 +++--- .../watcher_options_test.go | 6 +-- watcher_test.go | 18 ++++---- 12 files changed, 61 insertions(+), 47 deletions(-) rename watcher-c.h => watcher/watcher-c.h (100%) rename watcher.c => watcher/watcher.c (100%) rename watcher.go => watcher/watcher.go (72%) rename watcher.h => watcher/watcher.h (100%) rename watcher_options.go => watcher/watcher_options.go (87%) rename watcher_options_test.go => watcher/watcher_options_test.go (98%) diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 2867d942b..77718cee6 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -3,6 +3,7 @@ package caddy import ( "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/dunglas/frankenphp" + "github.com/dunglas/frankenphp/watcher" "path/filepath" "strings" ) @@ -18,9 +19,9 @@ type watchConfig struct { func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { return append(opts, frankenphp.WithFileWatcher( - frankenphp.WithWatcherDirs(watchConfig.Dirs), - frankenphp.WithWatcherRecursion(watchConfig.IsRecursive), - frankenphp.WithWatcherPattern(watchConfig.Pattern), + watcher.WithWatcherDirs(watchConfig.Dirs), + watcher.WithWatcherRecursion(watchConfig.IsRecursive), + watcher.WithWatcherPattern(watchConfig.Pattern), )) } diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go index 6757ca93b..560949317 100644 --- a/caddy/watch_config_test.go +++ b/caddy/watch_config_test.go @@ -81,3 +81,4 @@ func parseTestConfig(config string) (*caddy.FrankenPHPApp, error) { err := app.UnmarshalCaddyfile(caddyfile.NewTestDispenser(config)) return &app, err } + diff --git a/frankenphp.go b/frankenphp.go index 5260da583..68915816f 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -41,6 +41,7 @@ import ( "sync" "unsafe" + "github.com/dunglas/frankenphp/watcher" "github.com/maypok86/otter" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -320,7 +321,11 @@ func Init(options ...Option) error { return err } - if err := initWatcher(opt.watch, opt.workers); err != nil { + restartWorkers := func() { + restartWorkers(opt.workers) + } + + if err := watcher.InitWatcher(opt.watch, restartWorkers, getLogger()); err != nil { return err } @@ -338,7 +343,7 @@ func Init(options ...Option) error { // Shutdown stops the workers and the PHP runtime. func Shutdown() { - drainWatcher() + watcher.DrainWatcher() drainWorkers() drainThreads() requestChan = nil diff --git a/frankenphp_test.go b/frankenphp_test.go index 9d1aad339..8856c318a 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -25,6 +25,7 @@ import ( "testing" "github.com/dunglas/frankenphp" + "github.com/dunglas/frankenphp/watcher" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -35,7 +36,7 @@ import ( type testOptions struct { workerScript string - watchOptions []frankenphp.WatchOption + watchOptions []watcher.WatchOption nbWorkers int env map[string]string nbParrallelRequests int diff --git a/options.go b/options.go index 20cb39b8e..76f60eb9e 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,7 @@ package frankenphp import ( + "github.com/dunglas/frankenphp/watcher" "go.uber.org/zap" ) @@ -13,7 +14,7 @@ type Option func(h *opt) error type opt struct { numThreads int workers []workerOpt - watch []watchOpt + watch []watcher.WatchOpt logger *zap.Logger } @@ -42,9 +43,9 @@ func WithWorkers(fileName string, num int, env map[string]string) Option { } // WithFileWatcher configures filesystem watchers. -func WithFileWatcher(wo ...WatchOption) Option { +func WithFileWatcher(wo ...watcher.WatchOption) Option { return func(o *opt) error { - watchOpt := watchOpt{} + watchOpt := watcher.WatchOpt{} for _, option := range wo { if err := option(&watchOpt); err != nil { return err diff --git a/watcher-c.h b/watcher/watcher-c.h similarity index 100% rename from watcher-c.h rename to watcher/watcher-c.h diff --git a/watcher.c b/watcher/watcher.c similarity index 100% rename from watcher.c rename to watcher/watcher.c diff --git a/watcher.go b/watcher/watcher.go similarity index 72% rename from watcher.go rename to watcher/watcher.go index 142643ecf..e537a8fef 100644 --- a/watcher.go +++ b/watcher/watcher.go @@ -1,4 +1,4 @@ -package frankenphp +package watcher // #cgo LDFLAGS: -lwatcher-c-0.11.0 // #cgo CFLAGS: -Wall -Werror @@ -17,8 +17,8 @@ import ( type watcher struct { sessions []unsafe.Pointer - workerOpts []workerOpt - watchOpts []watchOpt + callback func() + watchOpts []WatchOpt trigger chan struct{} stop chan struct{} } @@ -31,13 +31,14 @@ var ( activeWatcher *watcher // after stopping the watcher we will wait for eventual reloads to finish reloadWaitGroup sync.WaitGroup + logger *zap.Logger ) -func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { +func InitWatcher(watchOpts []WatchOpt, callback func(), logger *zap.Logger) error { if len(watchOpts) == 0 { return nil } - activeWatcher = &watcher{workerOpts: workerOpts} + activeWatcher = &watcher{callback: callback} err := activeWatcher.startWatching(watchOpts) if err != nil { return err @@ -47,17 +48,19 @@ func initWatcher(watchOpts []watchOpt, workerOpts []workerOpt) error { return nil } -func drainWatcher() { +func DrainWatcher() { if activeWatcher == nil { return } - logger.Info("stopping watcher...") + if logger != nil { + logger.Info("stopping watcher...") + } activeWatcher.stopWatching() reloadWaitGroup.Wait() activeWatcher = nil } -func (w *watcher) startWatching(watchOpts []watchOpt) error { +func (w *watcher) startWatching(watchOpts []WatchOpt) error { w.trigger = make(chan struct{}) w.stop = make(chan struct{}) w.sessions = make([]unsafe.Pointer, len(watchOpts)) @@ -66,7 +69,6 @@ func (w *watcher) startWatching(watchOpts []watchOpt) error { watchOpt.trigger = w.trigger session, err := startSession(&watchOpt) if err != nil { - logger.Error("unable to watch dirs", zap.Strings("dirs", watchOpt.dirs)) return err } w.sessions[i] = session @@ -82,29 +84,30 @@ func (w *watcher) stopWatching() { } } -func startSession(watchOpt *watchOpt) (unsafe.Pointer, error) { +func startSession(watchOpt *WatchOpt) (unsafe.Pointer, error) { handle := cgo.NewHandle(watchOpt) cPathTranslated := (*C.char)(C.CString(watchOpt.dirs[0])) watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) - if watchSession == C.NULL { - logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) - return nil, errors.New("couldn't start watching") + if watchSession != C.NULL { + return watchSession, nil } - return watchSession, nil + if logger != nil { + logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) + } + return nil, errors.New("couldn't start watching") } func stopSession(session unsafe.Pointer) { success := C.stop_watcher(session) - if success == 1 { + if success == 1 && logger != nil { logger.Error("couldn't stop watching") } } //export go_handle_event func go_handle_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) { - watchOpt := cgo.Handle(handle).Value().(*watchOpt) + watchOpt := cgo.Handle(handle).Value().(*WatchOpt) if watchOpt.allowReload(C.GoString(path), int(eventType), int(pathType)) { - logger.Debug("valid file change detected", zap.String("path", C.GoString(path))) watchOpt.trigger <- struct{}{} } } @@ -127,8 +130,10 @@ func listenForFileEvents(triggerWatcher chan struct{}, stopWatcher chan struct{} } func scheduleWorkerReload() { - logger.Info("filesystem change detected, restarting workers...") + if logger != nil { + logger.Info("filesystem change detected, restarting workers...") + } reloadWaitGroup.Add(1) - restartWorkers(activeWatcher.workerOpts) + activeWatcher.callback() reloadWaitGroup.Done() } diff --git a/watcher.h b/watcher/watcher.h similarity index 100% rename from watcher.h rename to watcher/watcher.h diff --git a/watcher_options.go b/watcher/watcher_options.go similarity index 87% rename from watcher_options.go rename to watcher/watcher_options.go index 1aa0a902e..8763e3251 100644 --- a/watcher_options.go +++ b/watcher/watcher_options.go @@ -1,13 +1,13 @@ -package frankenphp +package watcher import ( "go.uber.org/zap" "path/filepath" ) -type WatchOption func(o *watchOpt) error +type WatchOption func(o *WatchOpt) error -type watchOpt struct { +type WatchOpt struct { dirs []string isRecursive bool pattern string @@ -15,7 +15,7 @@ type watchOpt struct { } func WithWatcherDirs(dirs []string) WatchOption { - return func(o *watchOpt) error { + return func(o *WatchOpt) error { for _, dir := range dirs { absDir, err := parseAbsPath(dir) if err != nil { @@ -28,14 +28,14 @@ func WithWatcherDirs(dirs []string) WatchOption { } func WithWatcherRecursion(withRecursion bool) WatchOption { - return func(o *watchOpt) error { + return func(o *WatchOpt) error { o.isRecursive = withRecursion return nil } } func WithWatcherPattern(pattern string) WatchOption { - return func(o *watchOpt) error { + return func(o *WatchOpt) error { o.pattern = pattern return nil } @@ -50,7 +50,7 @@ func parseAbsPath(path string) (string, error) { return absDir, nil } -func (watchOpt *watchOpt) allowReload(fileName string, eventType int, pathType int) bool { +func (watchOpt *WatchOpt) allowReload(fileName string, eventType int, pathType int) bool { if !isValidEventType(eventType) || !isValidPathType(pathType) { return false } diff --git a/watcher_options_test.go b/watcher/watcher_options_test.go similarity index 98% rename from watcher_options_test.go rename to watcher/watcher_options_test.go index ac5c7c784..ce61e6948 100644 --- a/watcher_options_test.go +++ b/watcher/watcher_options_test.go @@ -1,4 +1,4 @@ -package frankenphp +package watcher import ( "github.com/stretchr/testify/assert" @@ -137,8 +137,8 @@ func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { assert.False(t, watchOpt.allowReload(fileName, 0, pathType)) } -func createWithOptions(t *testing.T, applyOptions ...WatchOption) watchOpt { - watchOpt := watchOpt{} +func createWithOptions(t *testing.T, applyOptions ...WatchOption) WatchOpt { + watchOpt := WatchOpt{} for _, applyOption := range applyOptions { err := applyOption(&watchOpt) diff --git a/watcher_test.go b/watcher_test.go index fed895169..f1e173186 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -1,7 +1,7 @@ package frankenphp_test import ( - "github.com/dunglas/frankenphp" + "github.com/dunglas/frankenphp/watcher" "github.com/stretchr/testify/assert" "io" "net/http" @@ -22,10 +22,10 @@ const minTimesToPollForChanges = 3 const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { - watchOptions := []frankenphp.WatchOption{ - frankenphp.WithWatcherDirs([]string{"./testdata"}), - frankenphp.WithWatcherPattern("*.txt"), - frankenphp.WithWatcherRecursion(true), + watchOptions := []watcher.WatchOption{ + watcher.WithWatcherDirs([]string{"./testdata"}), + watcher.WithWatcherPattern("*.txt"), + watcher.WithWatcherRecursion(true), } runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { @@ -35,10 +35,10 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { - watchOptions := []frankenphp.WatchOption{ - frankenphp.WithWatcherDirs([]string{"./testdata"}), - frankenphp.WithWatcherPattern("*.php"), - frankenphp.WithWatcherRecursion(true), + watchOptions := []watcher.WatchOption{ + watcher.WithWatcherDirs([]string{"./testdata"}), + watcher.WithWatcherPattern("*.php"), + watcher.WithWatcherRecursion(true), } runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { From 66c2e9afb89c0ec9b6788359fbef3bacea06f4b2 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 12 Sep 2024 10:27:24 +0200 Subject: [PATCH 083/155] Adjusts naming. --- watcher_test.go => frankenphp_with_watcher_test.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename watcher_test.go => frankenphp_with_watcher_test.go (100%) diff --git a/watcher_test.go b/frankenphp_with_watcher_test.go similarity index 100% rename from watcher_test.go rename to frankenphp_with_watcher_test.go From b2949546515790ca2d33cb63e2b8bb92112f0a92 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 13 Sep 2024 11:33:52 +0200 Subject: [PATCH 084/155] ADocker image adjustments and refactoring. --- Dockerfile | 2 ++ alpine.Dockerfile | 26 ++++++++++++++------------ caddy/watch_config_test.go | 1 - dev-alpine.Dockerfile | 22 ++++++++++++---------- dev.Dockerfile | 4 ++++ frankenphp_test.go | 2 +- frankenphp_with_watcher_test.go | 4 ++-- options.go | 2 +- static-builder.Dockerfile | 19 +++++++++---------- watcher/watcher.go | 18 +++++++++--------- watcher/watcher_options.go | 8 ++++---- watcher/watcher_options_test.go | 2 +- 12 files changed, 59 insertions(+), 51 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7d321ad70..666466fd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -69,6 +69,7 @@ RUN apt-get update && \ libxml2-dev \ zlib1g-dev \ git \ + # needed for the file watcher meson \ && \ apt-get clean @@ -87,6 +88,7 @@ COPY --link *.* ./ COPY --link caddy caddy COPY --link internal internal COPY --link testdata testdata +COPY --link watcher watcher # install edant/watcher (necessary for file watching) WORKDIR /usr/local/src/watcher diff --git a/alpine.Dockerfile b/alpine.Dockerfile index b164ea2e5..fabbd5ce0 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -70,6 +70,9 @@ RUN apk add --no-cache --virtual .build-deps \ readline-dev \ sqlite-dev \ upx \ + # Needed for the file watcher + meson \ + libstdc++ \ # Needed for the custom Go build git \ bash @@ -103,17 +106,15 @@ COPY --link *.* ./ COPY --link caddy caddy COPY --link internal internal COPY --link testdata testdata +COPY --link watcher watcher -# install fswatch (necessary for file watching) -ARG FSWATCH_VERSION -WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz -WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && \ - make -j"$(nproc)" && \ - make install && \ - ldconfig /usr/local/lib && \ - fswatch --version +# install edant/watcher (necessary for file watching) +WORKDIR /usr/local/src/watcher +RUN git clone --branch=next https://github.com/e-dant/watcher . +WORKDIR /usr/local/src/watcher/watcher-c +RUN meson build .. && \ + meson compile -C build && \ + cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ # See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55 ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" @@ -133,8 +134,9 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 -COPY --from=builder /usr/local/lib/libfswatch.so* /usr/local/lib/ -RUN apk add --no-cache libstdc++ #required for fswatch to work +# required for watcher to work +COPY --from=builder /usr/local/lib/libwatcher-c* /usr/local/lib/ +COPY --from=builder /usr/lib/libstdc++* /usr/local/lib/ COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \ diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go index 560949317..6757ca93b 100644 --- a/caddy/watch_config_test.go +++ b/caddy/watch_config_test.go @@ -81,4 +81,3 @@ func parseTestConfig(config string) (*caddy.FrankenPHPApp, error) { err := app.UnmarshalCaddyfile(caddyfile.NewTestDispenser(config)) return &app, err } - diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index 96de20157..eafb5b9da 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -31,6 +31,10 @@ RUN apk add --no-cache \ zlib-dev \ bison \ nss-tools \ + # file watcher + libstdc++ \ + meson \ + linux-headers \ # Dev tools \ git \ clang \ @@ -60,16 +64,14 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ echo "opcache.enable=1" >> /usr/local/lib/php.ini && \ php --version -# install fswatch (necessary for file watching) -ARG FSWATCH_VERSION='1.17.1' -WORKDIR /usr/local/src/fswatch -RUN wget -o https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz -WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && \ - make -j"$(nproc)" && \ - make install && \ - ldconfig /usr/local/lib && \ - fswatch --version +# install edant/watcher (necessary for file watching) +WORKDIR /usr/local/src/watcher +RUN git clone --branch=next https://github.com/e-dant/watcher . +WORKDIR /usr/local/src/watcher/watcher-c +RUN meson build .. && \ + meson compile -C build && \ + cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ + ldconfig /urs/local/lib WORKDIR /go/src/app COPY . . diff --git a/dev.Dockerfile b/dev.Dockerfile index e82e319e9..29c57ed05 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -74,6 +74,10 @@ RUN meson build .. && \ meson compile -C build && \ cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ ldconfig +# TODO: alternatively edant/watcher install with clang++ or cmake? (will create a libwatcher.so): +# RUN clang++ -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ +# cp libwatcher.so /usr/local/lib/libwatcher.so && \ +# ldconfig WORKDIR /go/src/app COPY . . diff --git a/frankenphp_test.go b/frankenphp_test.go index 8856c318a..96b56ab7a 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -36,7 +36,7 @@ import ( type testOptions struct { workerScript string - watchOptions []watcher.WatchOption + watchOptions []watcher.WithWatchOption nbWorkers int env map[string]string nbParrallelRequests int diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index f1e173186..47303af52 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -22,7 +22,7 @@ const minTimesToPollForChanges = 3 const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { - watchOptions := []watcher.WatchOption{ + watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDirs([]string{"./testdata"}), watcher.WithWatcherPattern("*.txt"), watcher.WithWatcherRecursion(true), @@ -35,7 +35,7 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { - watchOptions := []watcher.WatchOption{ + watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDirs([]string{"./testdata"}), watcher.WithWatcherPattern("*.php"), watcher.WithWatcherRecursion(true), diff --git a/options.go b/options.go index 76f60eb9e..45d1d375c 100644 --- a/options.go +++ b/options.go @@ -43,7 +43,7 @@ func WithWorkers(fileName string, num int, env map[string]string) Option { } // WithFileWatcher configures filesystem watchers. -func WithFileWatcher(wo ...watcher.WatchOption) Option { +func WithFileWatcher(wo ...watcher.WithWatchOption) Option { return func(o *opt) error { watchOpt := watcher.WatchOpt{} for _, option := range wo { diff --git a/static-builder.Dockerfile b/static-builder.Dockerfile index d413a0e53..3da901cd6 100644 --- a/static-builder.Dockerfile +++ b/static-builder.Dockerfile @@ -52,6 +52,7 @@ RUN apk update; \ linux-headers \ m4 \ make \ + meson \ pkgconfig \ php83 \ php83-common \ @@ -73,16 +74,14 @@ RUN apk update; \ xz ; \ ln -sf /usr/bin/php83 /usr/bin/php -# install fswatch (necessary for file watching) -ARG FSWATCH_VERSION -WORKDIR /usr/local/src/fswatch -RUN curl -L https://github.com/emcrisostomo/fswatch/releases/download/$FSWATCH_VERSION/fswatch-$FSWATCH_VERSION.tar.gz | tar xz -WORKDIR /usr/local/src/fswatch/fswatch-$FSWATCH_VERSION -RUN ./configure && \ - make -j"$(nproc)" && \ - make install && \ - ldconfig /usr/local/lib && \ - fswatch --version +# install edant/watcher (necessary for file watching) +WORKDIR /usr/local/src/watcher +RUN git clone --branch=next https://github.com/e-dant/watcher . +WORKDIR /usr/local/src/watcher/watcher-c +RUN meson build .. && \ + meson compile -C build && \ + cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ + ldconfig /usr/local/lib # FIXME: temporary workaround for https://github.com/golang/go/issues/68285 WORKDIR / diff --git a/watcher/watcher.go b/watcher/watcher.go index e537a8fef..adc922375 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -16,11 +16,11 @@ import ( ) type watcher struct { - sessions []unsafe.Pointer - callback func() - watchOpts []WatchOpt - trigger chan struct{} - stop chan struct{} + sessions []unsafe.Pointer + callback func() + watchOpts []WatchOpt + trigger chan struct{} + stop chan struct{} } // duration to wait before reloading workers after a file change @@ -31,7 +31,7 @@ var ( activeWatcher *watcher // after stopping the watcher we will wait for eventual reloads to finish reloadWaitGroup sync.WaitGroup - logger *zap.Logger + logger *zap.Logger ) func InitWatcher(watchOpts []WatchOpt, callback func(), logger *zap.Logger) error { @@ -92,9 +92,9 @@ func startSession(watchOpt *WatchOpt) (unsafe.Pointer, error) { return watchSession, nil } if logger != nil { - logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) - } - return nil, errors.New("couldn't start watching") + logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) + } + return nil, errors.New("couldn't start watching") } func stopSession(session unsafe.Pointer) { diff --git a/watcher/watcher_options.go b/watcher/watcher_options.go index 8763e3251..869308fff 100644 --- a/watcher/watcher_options.go +++ b/watcher/watcher_options.go @@ -5,7 +5,7 @@ import ( "path/filepath" ) -type WatchOption func(o *WatchOpt) error +type WithWatchOption func(o *WatchOpt) error type WatchOpt struct { dirs []string @@ -14,7 +14,7 @@ type WatchOpt struct { trigger chan struct{} } -func WithWatcherDirs(dirs []string) WatchOption { +func WithWatcherDirs(dirs []string) WithWatchOption { return func(o *WatchOpt) error { for _, dir := range dirs { absDir, err := parseAbsPath(dir) @@ -27,14 +27,14 @@ func WithWatcherDirs(dirs []string) WatchOption { } } -func WithWatcherRecursion(withRecursion bool) WatchOption { +func WithWatcherRecursion(withRecursion bool) WithWatchOption { return func(o *WatchOpt) error { o.isRecursive = withRecursion return nil } } -func WithWatcherPattern(pattern string) WatchOption { +func WithWatcherPattern(pattern string) WithWatchOption { return func(o *WatchOpt) error { o.pattern = pattern return nil diff --git a/watcher/watcher_options_test.go b/watcher/watcher_options_test.go index ce61e6948..d00b989b6 100644 --- a/watcher/watcher_options_test.go +++ b/watcher/watcher_options_test.go @@ -137,7 +137,7 @@ func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { assert.False(t, watchOpt.allowReload(fileName, 0, pathType)) } -func createWithOptions(t *testing.T, applyOptions ...WatchOption) WatchOpt { +func createWithOptions(t *testing.T, applyOptions ...WithWatchOption) WatchOpt { watchOpt := WatchOpt{} for _, applyOption := range applyOptions { From d72d189ad2a7c98de41debb652fbab41767a023b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 12:28:41 +0200 Subject: [PATCH 085/155] Testing installation methods. --- Dockerfile | 9 +++++++-- alpine.Dockerfile | 5 +++-- dev-alpine.Dockerfile | 3 ++- dev.Dockerfile | 14 +++++--------- docker-bake.hcl | 8 ++++---- static-builder.Dockerfile | 11 +---------- watcher/watcher.go | 2 +- 7 files changed, 23 insertions(+), 29 deletions(-) diff --git a/Dockerfile b/Dockerfile index 666466fd7..dac316acb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -91,8 +91,9 @@ COPY --link testdata testdata COPY --link watcher watcher # install edant/watcher (necessary for file watching) +ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher -RUN git clone --branch=next https://github.com/e-dant/watcher . +RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c RUN meson build .. && \ meson compile -C build && \ @@ -117,8 +118,12 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 +# copy watcher shared library COPY --from=builder /usr/local/lib/libwatcher-c* /usr/local/lib/ -RUN ldconfig +# fix for the file watcher on arm +RUN apt-get install -y gcc && \ + apt-get clean && \ + ldconfig COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \ diff --git a/alpine.Dockerfile b/alpine.Dockerfile index fabbd5ce0..6d4bb1fc0 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -109,8 +109,9 @@ COPY --link testdata testdata COPY --link watcher watcher # install edant/watcher (necessary for file watching) +ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher -RUN git clone --branch=next https://github.com/e-dant/watcher . +RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c RUN meson build .. && \ meson compile -C build && \ @@ -134,7 +135,7 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 -# required for watcher to work +# copy watcher shared library COPY --from=builder /usr/local/lib/libwatcher-c* /usr/local/lib/ COPY --from=builder /usr/lib/libstdc++* /usr/local/lib/ diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index eafb5b9da..615a43b57 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -65,8 +65,9 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ php --version # install edant/watcher (necessary for file watching) +ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher -RUN git clone --branch=next https://github.com/e-dant/watcher . +RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c RUN meson build .. && \ meson compile -C build && \ diff --git a/dev.Dockerfile b/dev.Dockerfile index 29c57ed05..8c7ca267e 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -67,17 +67,13 @@ RUN git clone --branch=PHP-8.3 https://github.com/php/php-src.git . && \ php --version # install edant/watcher (necessary for file watching) +ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher -RUN git clone --branch=next https://github.com/e-dant/watcher . +RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c -RUN meson build .. && \ - meson compile -C build && \ - cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ - ldconfig -# TODO: alternatively edant/watcher install with clang++ or cmake? (will create a libwatcher.so): -# RUN clang++ -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ -# cp libwatcher.so /usr/local/lib/libwatcher.so && \ -# ldconfig +RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ + cp libwatcher.so /usr/local/lib/libwatcher.so && \ + ldconfig WORKDIR /go/src/app COPY . . diff --git a/docker-bake.hcl b/docker-bake.hcl index 15bc175fa..9dcd70e7f 100644 --- a/docker-bake.hcl +++ b/docker-bake.hcl @@ -14,8 +14,8 @@ variable "GO_VERSION" { default = "1.22" } -variable FSWATCH_VERSION { - default = "1.17.1" +variable EDANT_WATCHER_VERSION { + default = "next" } variable "SHA" {} @@ -119,7 +119,7 @@ target "default" { } args = { FRANKENPHP_VERSION = VERSION - FSWATCH_VERSION = FSWATCH_VERSION + EDANT_WATCHER_VERSION = EDANT_WATCHER_VERSION } } @@ -145,7 +145,7 @@ target "static-builder" { } args = { FRANKENPHP_VERSION = VERSION - FSWATCH_VERSION = FSWATCH_VERSION + EDANT_WATCHER_VERSION = EDANT_WATCHER_VERSION } secret = ["id=github-token,env=GITHUB_TOKEN"] } diff --git a/static-builder.Dockerfile b/static-builder.Dockerfile index 3da901cd6..84ca5c3a6 100644 --- a/static-builder.Dockerfile +++ b/static-builder.Dockerfile @@ -52,7 +52,6 @@ RUN apk update; \ linux-headers \ m4 \ make \ - meson \ pkgconfig \ php83 \ php83-common \ @@ -74,15 +73,6 @@ RUN apk update; \ xz ; \ ln -sf /usr/bin/php83 /usr/bin/php -# install edant/watcher (necessary for file watching) -WORKDIR /usr/local/src/watcher -RUN git clone --branch=next https://github.com/e-dant/watcher . -WORKDIR /usr/local/src/watcher/watcher-c -RUN meson build .. && \ - meson compile -C build && \ - cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ - ldconfig /usr/local/lib - # FIXME: temporary workaround for https://github.com/golang/go/issues/68285 WORKDIR / RUN git clone https://go.googlesource.com/go goroot @@ -113,6 +103,7 @@ RUN go mod graph | awk '{if ($1 !~ "@") print $2}' | xargs go get WORKDIR /go/src/app COPY *.* ./ COPY caddy caddy +COPY watcher watcher RUN --mount=type=secret,id=github-token GITHUB_TOKEN=$(cat /run/secrets/github-token) ./build-static.sh && \ rm -Rf dist/static-php-cli/source/* diff --git a/watcher/watcher.go b/watcher/watcher.go index adc922375..e5d2398b3 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -1,6 +1,6 @@ package watcher -// #cgo LDFLAGS: -lwatcher-c-0.11.0 +// #cgo LDFLAGS: -lwatcher-c // #cgo CFLAGS: -Wall -Werror // #include // #include From 8f3ee2b13cd338819736a4e64e497f67a47748a9 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 14:53:22 +0200 Subject: [PATCH 086/155] Installs via gcc instead. --- Dockerfile | 13 +++++-------- alpine.Dockerfile | 13 ++++++------- build-static.sh | 14 +++++++++----- dev-alpine.Dockerfile | 14 ++++++-------- dev.Dockerfile | 5 ++--- watcher/watcher.go | 2 +- 6 files changed, 29 insertions(+), 32 deletions(-) diff --git a/Dockerfile b/Dockerfile index dac316acb..a3ef10d53 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,9 +68,7 @@ RUN apt-get update && \ libssl-dev \ libxml2-dev \ zlib1g-dev \ - git \ - # needed for the file watcher - meson \ + git \ && \ apt-get clean @@ -95,10 +93,9 @@ ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c -RUN meson build .. && \ - meson compile -C build && \ - cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ - ldconfig +RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ + cp libwatcher.so /usr/local/lib/libwatcher.so && \ + ldconfig /usr/local/lib # See https://github.com/docker-library/php/blob/master/8.3/bookworm/zts/Dockerfile#L57-L59 for PHP values ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" @@ -119,7 +116,7 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 # copy watcher shared library -COPY --from=builder /usr/local/lib/libwatcher-c* /usr/local/lib/ +COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/ # fix for the file watcher on arm RUN apt-get install -y gcc && \ apt-get clean && \ diff --git a/alpine.Dockerfile b/alpine.Dockerfile index 6d4bb1fc0..fce39d886 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -70,9 +70,8 @@ RUN apk add --no-cache --virtual .build-deps \ readline-dev \ sqlite-dev \ upx \ - # Needed for the file watcher - meson \ - libstdc++ \ + # Needed for the file watcher + libstdc++ \ # Needed for the custom Go build git \ bash @@ -113,9 +112,9 @@ ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c -RUN meson build .. && \ - meson compile -C build && \ - cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ +RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ + cp libwatcher.so /usr/local/lib/libwatcher.so && \ + ldconfig /usr/local/lib # See https://github.com/docker-library/php/blob/master/8.3/alpine3.20/zts/Dockerfile#L53-L55 ENV CGO_CFLAGS="-DFRANKENPHP_VERSION=$FRANKENPHP_VERSION $PHP_CFLAGS" @@ -136,7 +135,7 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 # copy watcher shared library -COPY --from=builder /usr/local/lib/libwatcher-c* /usr/local/lib/ +COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/ COPY --from=builder /usr/lib/libstdc++* /usr/local/lib/ COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp diff --git a/build-static.sh b/build-static.sh index 5c172dc1c..611a9642e 100755 --- a/build-static.sh +++ b/build-static.sh @@ -137,11 +137,15 @@ elif [ "${os}" = "linux" ] && [ -z "${DEBUG_SYMBOLS}" ]; then fi CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags || true) $(./buildroot/bin/php-config --libs || true)" -if [ "${os}" = "linux" ]; then - if echo "${PHP_EXTENSIONS}" | grep -qE "\b(intl|imagick|grpc|v8js|protobuf|mongodb|tbb)\b"; then - CGO_LDFLAGS="${CGO_LDFLAGS} -lstdc++" - fi -fi + +# install edant/watcher for file watching +git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher watcher +cd watcher/watcher-c +gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ +cp libwatcher.so ${PWD}/buildroot/lib/libwatcher.so +CGO_LDFLAGS="${CGO_LDFLAGS} -lstdc++ -lwatcher" +cd ../../ + export CGO_LDFLAGS LIBPHP_VERSION="$(./buildroot/bin/php-config --version)" diff --git a/dev-alpine.Dockerfile b/dev-alpine.Dockerfile index 615a43b57..7cb8e02de 100644 --- a/dev-alpine.Dockerfile +++ b/dev-alpine.Dockerfile @@ -31,10 +31,9 @@ RUN apk add --no-cache \ zlib-dev \ bison \ nss-tools \ - # file watcher - libstdc++ \ - meson \ - linux-headers \ + # file watcher + libstdc++ \ + linux-headers \ # Dev tools \ git \ clang \ @@ -69,10 +68,9 @@ ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c -RUN meson build .. && \ - meson compile -C build && \ - cp -r build/watcher-c/libwatcher-c* /usr/local/lib/ && \ - ldconfig /urs/local/lib +RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ + cp libwatcher.so /usr/local/lib/libwatcher.so && \ + ldconfig /usr/local/lib WORKDIR /go/src/app COPY . . diff --git a/dev.Dockerfile b/dev.Dockerfile index 8c7ca267e..26254a60d 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -41,7 +41,6 @@ RUN apt-get update && \ valgrind \ neovim \ zsh \ - meson \ libtool-bin && \ echo 'set auto-load safe-path /' > /root/.gdbinit && \ echo '* soft core unlimited' >> /etc/security/limits.conf \ @@ -72,8 +71,8 @@ WORKDIR /usr/local/src/watcher RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . WORKDIR /usr/local/src/watcher/watcher-c RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ - cp libwatcher.so /usr/local/lib/libwatcher.so && \ - ldconfig + cp libwatcher.so /usr/local/lib/libwatcher.so && \ + ldconfig /usr/local/lib WORKDIR /go/src/app COPY . . diff --git a/watcher/watcher.go b/watcher/watcher.go index e5d2398b3..70dbb4d2d 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -1,6 +1,6 @@ package watcher -// #cgo LDFLAGS: -lwatcher-c +// #cgo LDFLAGS: -lwatcher -lstdc++ // #cgo CFLAGS: -Wall -Werror // #include // #include From 1acecf3261c433360998f684db8c7f55c98e108d Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 14:54:00 +0200 Subject: [PATCH 087/155] Fixes pointer formats. --- watcher/watcher-c.h | 14 +++++++------- watcher/watcher.c | 10 +++++----- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/watcher/watcher-c.h b/watcher/watcher-c.h index 245266ddf..bb5b877f3 100644 --- a/watcher/watcher-c.h +++ b/watcher/watcher-c.h @@ -48,28 +48,28 @@ static const int8_t WTR_WATCHER_PATH_OTHER = 5; */ struct wtr_watcher_event { int64_t effect_time; - char const* path_name; - char const* associated_path_name; + char const *path_name; + char const *associated_path_name; int8_t effect_type; int8_t path_type; }; /* Ensure the user's callback can receive events and will return nothing. */ -typedef void (* wtr_watcher_callback)(struct wtr_watcher_event event, void* context); +typedef void (*wtr_watcher_callback)(struct wtr_watcher_event event, void *context); -void* wtr_watcher_open(char const* const path, wtr_watcher_callback callback, void* context); +void *wtr_watcher_open(char const *const path, wtr_watcher_callback callback, void *context); -bool wtr_watcher_close(void* watcher); +bool wtr_watcher_close(void *watcher); /* The user, or the language we're working with, might not prefer a callback-style API. We provide a pipe-based API for these cases. Instead of forwarding events to a callback, we write json-serialized events to a pipe. */ -void* wtr_watcher_open_pipe(char const* const path, int* read_fd, int* write_fd); +void *wtr_watcher_open_pipe(char const *const path, int *read_fd, int *write_fd); -bool wtr_watcher_close_pipe(void* watcher, int read_fd, int write_fd); +bool wtr_watcher_close_pipe(void *watcher, int read_fd, int write_fd); #ifdef __cplusplus } diff --git a/watcher/watcher.c b/watcher/watcher.c index 862639e1c..38f93e4a2 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -1,19 +1,19 @@ #include "_cgo_export.h" #include "watcher-c.h" -void process_event(struct wtr_watcher_event event, void* data) { - go_handle_event((char*)event.path_name, event.effect_type, event.path_type, (uintptr_t)data); +void process_event(struct wtr_watcher_event event, void *data) { + go_handle_event((char *)event.path_name, event.effect_type, event.path_type, (uintptr_t)data); } -void* start_new_watcher(char const* const path, uintptr_t data) { - void* watcher = wtr_watcher_open(path, process_event, (void *)data); +void *start_new_watcher(char const *const path, uintptr_t data) { + void *watcher = wtr_watcher_open(path, process_event, (void *)data); if (!watcher) { return NULL; } return watcher; } -int stop_watcher(void* watcher) { +int stop_watcher(void *watcher) { if (!wtr_watcher_close(watcher)) { return 1; } From 2f28c198d482c905c48a5ad3f5540921654453fb Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 15:12:34 +0200 Subject: [PATCH 088/155] Fixes lint issues. --- Dockerfile | 2 +- build-static.sh | 6 +++--- watcher/watcher-c.h | 9 ++++++--- watcher/watcher.c | 3 ++- watcher/watcher.h | 4 ++-- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index a3ef10d53..7a6897600 100644 --- a/Dockerfile +++ b/Dockerfile @@ -118,7 +118,7 @@ ENV GODEBUG=cgocheck=0 # copy watcher shared library COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/ # fix for the file watcher on arm -RUN apt-get install -y gcc && \ +RUN apt-get install -y --no-install-recommends gcc && \ apt-get clean && \ ldconfig diff --git a/build-static.sh b/build-static.sh index 611a9642e..923a2142f 100755 --- a/build-static.sh +++ b/build-static.sh @@ -139,10 +139,10 @@ fi CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags || true) $(./buildroot/bin/php-config --libs || true)" # install edant/watcher for file watching -git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher watcher +git clone --branch="${EDANT_WATCHER_VERSION}" https://github.com/e-dant/watcher watcher cd watcher/watcher-c -gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ -cp libwatcher.so ${PWD}/buildroot/lib/libwatcher.so +gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared +cp libwatcher.so "${PWD}/buildroot/lib/libwatcher.so" CGO_LDFLAGS="${CGO_LDFLAGS} -lstdc++ -lwatcher" cd ../../ diff --git a/watcher/watcher-c.h b/watcher/watcher-c.h index bb5b877f3..2abb8a82d 100644 --- a/watcher/watcher-c.h +++ b/watcher/watcher-c.h @@ -56,9 +56,11 @@ struct wtr_watcher_event { /* Ensure the user's callback can receive events and will return nothing. */ -typedef void (*wtr_watcher_callback)(struct wtr_watcher_event event, void *context); +typedef void (*wtr_watcher_callback)(struct wtr_watcher_event event, + void *context); -void *wtr_watcher_open(char const *const path, wtr_watcher_callback callback, void *context); +void *wtr_watcher_open(char const *const path, wtr_watcher_callback callback, + void *context); bool wtr_watcher_close(void *watcher); @@ -67,7 +69,8 @@ bool wtr_watcher_close(void *watcher); We provide a pipe-based API for these cases. Instead of forwarding events to a callback, we write json-serialized events to a pipe. */ -void *wtr_watcher_open_pipe(char const *const path, int *read_fd, int *write_fd); +void *wtr_watcher_open_pipe(char const *const path, int *read_fd, + int *write_fd); bool wtr_watcher_close_pipe(void *watcher, int read_fd, int write_fd); diff --git a/watcher/watcher.c b/watcher/watcher.c index 38f93e4a2..51b936e2d 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -2,7 +2,8 @@ #include "watcher-c.h" void process_event(struct wtr_watcher_event event, void *data) { - go_handle_event((char *)event.path_name, event.effect_type, event.path_type, (uintptr_t)data); + go_handle_event((char *)event.path_name, event.effect_type, event.path_type, + (uintptr_t)data); } void *start_new_watcher(char const *const path, uintptr_t data) { diff --git a/watcher/watcher.h b/watcher/watcher.h index eeed4229a..4f153a304 100644 --- a/watcher/watcher.h +++ b/watcher/watcher.h @@ -1,6 +1,6 @@ #include #include -void* start_new_watcher(char const* const path, uintptr_t data); +void *start_new_watcher(char const *const path, uintptr_t data); -int stop_watcher(void* watcher); \ No newline at end of file +int stop_watcher(void *watcher); \ No newline at end of file From 3f16a0fd8197d94b4c8cf0f038dbaf67454a0ef2 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 16:27:40 +0200 Subject: [PATCH 089/155] Fixes arm alpine and updates docs. --- alpine.Dockerfile | 5 +++-- build-static.sh | 2 +- docs/config.md | 30 +++++++----------------------- watcher/watcher_options.go | 1 + 4 files changed, 12 insertions(+), 26 deletions(-) diff --git a/alpine.Dockerfile b/alpine.Dockerfile index fce39d886..c0e2ff284 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -134,9 +134,10 @@ FROM common AS runner ENV GODEBUG=cgocheck=0 -# copy watcher shared library +# copy watcher shared library (libgcc and libstdc++ are needed for the watcher) COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/ -COPY --from=builder /usr/lib/libstdc++* /usr/local/lib/ +RUN apk add --no-cache libgcc libstdc++ && \ + ldconfig /usr/local/lib COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp RUN setcap cap_net_bind_service=+ep /usr/local/bin/frankenphp && \ diff --git a/build-static.sh b/build-static.sh index 923a2142f..9902be606 100755 --- a/build-static.sh +++ b/build-static.sh @@ -139,7 +139,7 @@ fi CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildroot/lib/libbrotlienc.a ${PWD}/buildroot/lib/libbrotlidec.a $(./buildroot/bin/php-config --ldflags || true) $(./buildroot/bin/php-config --libs || true)" # install edant/watcher for file watching -git clone --branch="${EDANT_WATCHER_VERSION}" https://github.com/e-dant/watcher watcher +git clone --branch="${EDANT_WATCHER_VERSION:-next}" https://github.com/e-dant/watcher watcher cd watcher/watcher-c gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared cp libwatcher.so "${PWD}/buildroot/lib/libwatcher.so" diff --git a/docs/config.md b/docs/config.md index 5ddbdba11..c6941b786 100644 --- a/docs/config.md +++ b/docs/config.md @@ -133,9 +133,9 @@ php_server [] { ### Watching for File Changes -Since workers won't restart automatically on file changes you can also -define a number of directories that should be watched. This is useful for -development environments. +Since workers won't restart automatically when updating your PHP files, you can also +define a number of directories that should be watched for file changes. +This is useful for development environments. ```caddyfile { @@ -147,40 +147,24 @@ development environments. ``` The configuration above will watch the `/path/to/app` directory recursively. -If any file changes, the worker will be restarted. +If any file changes, all workers will be restarted. -You can also add multiple `watch` directives and use simple wildcard patterns, the following is valid: +You can also add multiple `watch` directives and use simple pattern matching for files, the following is valid: ```caddyfile { frankenphp { - watch /path/to/folder1 # watches all subdirectories + watch /path/to/folder1 # watches all files in all subdirectories of /path/to/folder1 watch /path/to/folder2/*.php # watches files ending in .php in the /path/to/folder2 directory watch /path/to/folder3/**/*.php # watches files ending in .php in the /path/to/folder3 directory and subdirectories } } ``` -Multiple directories can also be watched in one block: - -```caddyfile -{ - frankenphp { - watch { - dir /path/to/folder1 - dir /path/to/folder2 - dir /path/to/folder3 - recursive true - pattern *.php - } - } -} -``` - #### Some notes - Directories can also be relative (to where the frankenphp process was started from) -- The `/**/` pattern signifies recursive watching +- The `/**/` pattern signifies recursive watching and may followed by a filename pattern - If the last part of the pattern contains the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the shell [filename pattern](https://pkg.go.dev/path/filepath#Match) - The watcher will ignore symlinks diff --git a/watcher/watcher_options.go b/watcher/watcher_options.go index 869308fff..7ef4b9fd8 100644 --- a/watcher/watcher_options.go +++ b/watcher/watcher_options.go @@ -50,6 +50,7 @@ func parseAbsPath(path string) (string, error) { return absDir, nil } +// TODO: support directory patterns func (watchOpt *WatchOpt) allowReload(fileName string, eventType int, pathType int) bool { if !isValidEventType(eventType) || !isValidPathType(pathType) { return false From a7670dfe7e4c0cdefd4c1b07d0ce6851104797d1 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 16:34:05 +0200 Subject: [PATCH 090/155] Clang format. --- watcher/watcher.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher/watcher.h b/watcher/watcher.h index 4f153a304..25a6e75c0 100644 --- a/watcher/watcher.h +++ b/watcher/watcher.h @@ -1,5 +1,5 @@ -#include #include +#include void *start_new_watcher(char const *const path, uintptr_t data); From 438e4e2f948a8abf62c96fe91744e479b763537b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 18:23:51 +0200 Subject: [PATCH 091/155] Fixes dirs. --- build-static.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-static.sh b/build-static.sh index 9902be606..d86a15a3e 100755 --- a/build-static.sh +++ b/build-static.sh @@ -142,7 +142,7 @@ CGO_LDFLAGS="${CGO_LDFLAGS} ${PWD}/buildroot/lib/libbrotlicommon.a ${PWD}/buildr git clone --branch="${EDANT_WATCHER_VERSION:-next}" https://github.com/e-dant/watcher watcher cd watcher/watcher-c gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared -cp libwatcher.so "${PWD}/buildroot/lib/libwatcher.so" +cp libwatcher.so "../../buildroot/lib/libwatcher.so" CGO_LDFLAGS="${CGO_LDFLAGS} -lstdc++ -lwatcher" cd ../../ From 66b9c3052894c146e91bc318a7494b1fe708a2cb Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sat, 14 Sep 2024 18:25:48 +0200 Subject: [PATCH 092/155] Adds watcher version arg. --- static-builder.Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/static-builder.Dockerfile b/static-builder.Dockerfile index 84ca5c3a6..42120ff09 100644 --- a/static-builder.Dockerfile +++ b/static-builder.Dockerfile @@ -12,6 +12,9 @@ ENV FRANKENPHP_VERSION=${FRANKENPHP_VERSION} ARG PHP_VERSION='' ENV PHP_VERSION=${PHP_VERSION} +ARG EDANT_WATCHER_VERSION='' +ENV EDANT_WATCHER_VERSION=${EDANT_WATCHER_VERSION} + ARG PHP_EXTENSIONS='' ARG PHP_EXTENSION_LIBS='' ARG CLEAN='' From cd342c3d8338885f3567cf5eaaa1c6b58ee0f095 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 16 Sep 2024 10:23:41 +0200 Subject: [PATCH 093/155] Uses static lib version. --- build-static.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/build-static.sh b/build-static.sh index eada891d6..5b4ec1d38 100755 --- a/build-static.sh +++ b/build-static.sh @@ -143,13 +143,14 @@ if [ "${os}" = "linux" ]; then fi fi -# install edant/watcher for file watching +# install edant/watcher for file watching (static version) git clone --branch="${EDANT_WATCHER_VERSION:-next}" https://github.com/e-dant/watcher watcher cd watcher/watcher-c -gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared -cp libwatcher.so "../../buildroot/lib/libwatcher.so" -CGO_LDFLAGS="${CGO_LDFLAGS} -lstdc++ -lwatcher" +gcc -c -o libwatcher.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra -fPIC +ar rcs libwatcher.a libwatcher.o +cp libwatcher.a "../../buildroot/lib/libwatcher.a" cd ../../ +CGO_LDFLAGS="${CGO_LDFLAGS} -lstdc++ ${PWD}/buildroot/lib/libwatcher.a" export CGO_LDFLAGS From db62988dbe478c78e3911c4e4d2ac37ffd9cce97 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 16 Sep 2024 11:30:49 +0200 Subject: [PATCH 094/155] Adds watcher to tests and sanitizers. --- .github/workflows/sanitizers.yaml | 13 +++++++++++++ .github/workflows/tests.yaml | 13 +++++++++++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index c40776045..516e9972c 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -30,6 +30,7 @@ jobs: USE_ZEND_ALLOC: 0 LIBRARY_PATH: ${{ github.workspace }}/php/target/lib LD_LIBRARY_PATH: ${{ github.workspace }}/php/target/lib + EDANT_WATCHER_VERSION: next steps: - name: Remove local PHP @@ -95,6 +96,18 @@ jobs: - name: Add PHP to the PATH run: echo "$(pwd)/php/target/bin" >> "$GITHUB_PATH" + - uses: actions/checkout@v4 + name: Checkout watcher + with: + repository: e-dant/watcher + ref: ${{ env.EDANT_WATCHER_VERSION }} + path: 'edant/watcher' + - name: Compile edant/watcher + run: | + cd edant/watcher/watcher-c/ + gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared + cp libwatcher.so /usr/local/lib/libwatcher.so + ldconfig - name: Set Set CGO flags run: | diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 364f1f5f1..84fbe56e7 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -23,6 +23,7 @@ jobs: env: GOEXPERIMENT: cgocheck2 GOMAXPROCS: 10 + EDANT_WATCHER_VERSION: next steps: - uses: actions/checkout@v4 @@ -43,6 +44,18 @@ jobs: env: phpts: ts debug: true + - uses: actions/checkout@v4 + name: Checkout watcher + with: + repository: e-dant/watcher + ref: ${{ env.EDANT_WATCHER_VERSION }} + path: 'edant/watcher' + - name: Compile edant/watcher + run: | + cd edant/watcher/watcher-c/ + gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared + cp libwatcher.so /usr/local/lib/libwatcher.so + ldconfig - name: Set CGO flags run: | From 771c695266220c6b9096f8a4a1d7f60a664dd638 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 16 Sep 2024 11:37:48 +0200 Subject: [PATCH 095/155] Uses sudo for copying the shared lib. --- .github/workflows/sanitizers.yaml | 4 ++-- .github/workflows/tests.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index 516e9972c..ec5879b1f 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -106,8 +106,8 @@ jobs: run: | cd edant/watcher/watcher-c/ gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared - cp libwatcher.so /usr/local/lib/libwatcher.so - ldconfig + sudo cp libwatcher.so /usr/local/lib/libwatcher.so + sudo ldconfig - name: Set Set CGO flags run: | diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 84fbe56e7..d323e3872 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -54,8 +54,8 @@ jobs: run: | cd edant/watcher/watcher-c/ gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared - cp libwatcher.so /usr/local/lib/libwatcher.so - ldconfig + sudo cp libwatcher.so /usr/local/lib/libwatcher.so + sudo ldconfig - name: Set CGO flags run: | From 8bf02ca7563cda890bd4a62c1283c789d7eb4e9d Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 16 Sep 2024 11:43:47 +0200 Subject: [PATCH 096/155] Removes unnused func. --- watcher/watcher_options.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/watcher/watcher_options.go b/watcher/watcher_options.go index 7ef4b9fd8..f1a1b411a 100644 --- a/watcher/watcher_options.go +++ b/watcher/watcher_options.go @@ -86,6 +86,3 @@ func isValidPathType(eventType int) bool { return eventType <= 2 } -func isValidPath(fileName string) bool { - return fileName != "" -} From 08939237eda9e00845af6c3db58d3179176e112d Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 16 Sep 2024 15:23:16 +0200 Subject: [PATCH 097/155] Refactoring. --- caddy/watch_config.go | 7 ++-- caddy/watch_config_test.go | 56 ++++++++++++++-------------- frankenphp_with_watcher_test.go | 4 +- watcher/watcher.c | 2 +- watcher/watcher.go | 33 ++++++++--------- watcher/watcher_options.go | 65 ++++++++++++++++----------------- watcher/watcher_options_test.go | 30 +++++++-------- 7 files changed, 96 insertions(+), 101 deletions(-) diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 77718cee6..4be8e8368 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -10,7 +10,7 @@ import ( type watchConfig struct { // FileName sets the path to the worker script. - Dirs []string `json:"dir,omitempty"` + Dir string `json:"dir,omitempty"` // Whether to watch the directory recursively IsRecursive bool `json:"recursive,omitempty"` // The shell filename pattern to match against @@ -19,7 +19,7 @@ type watchConfig struct { func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { return append(opts, frankenphp.WithFileWatcher( - watcher.WithWatcherDirs(watchConfig.Dirs), + watcher.WithWatcherDir(watchConfig.Dir), watcher.WithWatcherRecursion(watchConfig.IsRecursive), watcher.WithWatcherPattern(watchConfig.Pattern), )) @@ -51,8 +51,7 @@ func parseFullPattern(filePattern string) watchConfig { watchConfig.Pattern = strings.Split(filePattern, "/**/")[1] watchConfig.IsRecursive = true } - dirName = strings.TrimRight(dirName, "/") - watchConfig.Dirs = []string{dirName} + watchConfig.Dir = strings.TrimRight(dirName, "/") return watchConfig } diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go index 6757ca93b..8b88c06bc 100644 --- a/caddy/watch_config_test.go +++ b/caddy/watch_config_test.go @@ -7,63 +7,63 @@ import ( "testing" ) -func TestParsingARecursiveDirectory(t *testing.T) { +func TestParseRecursiveDirectoryWithoutPattern(t *testing.T) { app, err := parseTestConfig(` frankenphp { - watch /path + watch /path1 + watch /path2/ + watch /path3/**/ } `) assert.Nil(t, err) - assert.Equal(t, 1, len(app.Watch)) - assert.Equal(t, 1, len(app.Watch[0].Dirs)) - assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.Equal(t, 3, len(app.Watch)) + + assert.Equal(t, "/path1", app.Watch[0].Dir) + assert.Equal(t, "/path2", app.Watch[1].Dir) + assert.Equal(t, "/path3", app.Watch[2].Dir) assert.True(t, app.Watch[0].IsRecursive) + assert.True(t, app.Watch[1].IsRecursive) + assert.True(t, app.Watch[2].IsRecursive) assert.Equal(t, "", app.Watch[0].Pattern) + assert.Equal(t, "", app.Watch[1].Pattern) + assert.Equal(t, "", app.Watch[2].Pattern) } -func TestParsingARecursiveDirectoryWithPattern(t *testing.T) { +func TestParseRecursiveDirectoryWithPattern(t *testing.T) { app, err := parseTestConfig(` frankenphp { watch /path/**/*.php + watch /path/**/filename } `) assert.Nil(t, err) - assert.Equal(t, 1, len(app.Watch)) - assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.Equal(t, 2, len(app.Watch)) + assert.Equal(t, "/path", app.Watch[0].Dir) + assert.Equal(t, "/path", app.Watch[1].Dir) assert.True(t, app.Watch[0].IsRecursive) + assert.True(t, app.Watch[1].IsRecursive) assert.Equal(t, "*.php", app.Watch[0].Pattern) + assert.Equal(t, "filename", app.Watch[1].Pattern) } -func TestParsingNonRecursiveDirectoryWithPattern(t *testing.T) { +func TestParseNonRecursiveDirectoryWithPattern(t *testing.T) { app, err := parseTestConfig(` frankenphp { - watch /path/*.php + watch /path1/*.php + watch /path2/watch-me.php } `) assert.Nil(t, err) - assert.Equal(t, 1, len(app.Watch)) - assert.Equal(t, "/path", app.Watch[0].Dirs[0]) + assert.Equal(t, 2, len(app.Watch)) + assert.Equal(t, "/path1", app.Watch[0].Dir) + assert.Equal(t, "/path2", app.Watch[1].Dir) assert.False(t, app.Watch[0].IsRecursive) + assert.False(t, app.Watch[1].IsRecursive) assert.Equal(t, "*.php", app.Watch[0].Pattern) -} - -func TestParseTwoShortForms(t *testing.T) { - app, err := parseTestConfig(` - frankenphp { - watch /path - watch /other/path/*.php - } - `) - - assert.Nil(t, err) - assert.Equal(t, 2, len(app.Watch)) - assert.Equal(t, "/path", app.Watch[0].Dirs[0]) - assert.Equal(t, "", app.Watch[0].Pattern) - assert.Equal(t, "/other/path", app.Watch[1].Dirs[0]) - assert.Equal(t, "*.php", app.Watch[1].Pattern) + assert.Equal(t, "watch-me.php", app.Watch[1].Pattern) } func TestFailOnMissingPath(t *testing.T) { diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index 47303af52..b9ef56cbe 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -23,7 +23,7 @@ const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { watchOptions := []watcher.WithWatchOption{ - watcher.WithWatcherDirs([]string{"./testdata"}), + watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.txt"), watcher.WithWatcherRecursion(true), } @@ -36,7 +36,7 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { watchOptions := []watcher.WithWatchOption{ - watcher.WithWatcherDirs([]string{"./testdata"}), + watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.php"), watcher.WithWatcherRecursion(true), } diff --git a/watcher/watcher.c b/watcher/watcher.c index 51b936e2d..ff0a6bc41 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -2,7 +2,7 @@ #include "watcher-c.h" void process_event(struct wtr_watcher_event event, void *data) { - go_handle_event((char *)event.path_name, event.effect_type, event.path_type, + go_handle_file_watcher_event((char *)event.path_name, event.effect_type, event.path_type, (uintptr_t)data); } diff --git a/watcher/watcher.go b/watcher/watcher.go index 70dbb4d2d..6e6b721e1 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -23,7 +23,7 @@ type watcher struct { stop chan struct{} } -// duration to wait before reloading workers after a file change +// duration to wait before triggering a reload after a file change const debounceDuration = 150 * time.Millisecond var ( @@ -34,10 +34,14 @@ var ( logger *zap.Logger ) -func InitWatcher(watchOpts []WatchOpt, callback func(), logger *zap.Logger) error { +func InitWatcher(watchOpts []WatchOpt, callback func(), zapLogger *zap.Logger) error { if len(watchOpts) == 0 { return nil } + if activeWatcher != nil { + return errors.New("watcher is already running") + } + logger = zapLogger activeWatcher = &watcher{callback: callback} err := activeWatcher.startWatching(watchOpts) if err != nil { @@ -52,9 +56,7 @@ func DrainWatcher() { if activeWatcher == nil { return } - if logger != nil { - logger.Info("stopping watcher...") - } + logger.Info("stopping watcher...") activeWatcher.stopWatching() reloadWaitGroup.Wait() activeWatcher = nil @@ -86,26 +88,25 @@ func (w *watcher) stopWatching() { func startSession(watchOpt *WatchOpt) (unsafe.Pointer, error) { handle := cgo.NewHandle(watchOpt) - cPathTranslated := (*C.char)(C.CString(watchOpt.dirs[0])) + cPathTranslated := (*C.char)(C.CString(watchOpt.dir)) watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) if watchSession != C.NULL { return watchSession, nil } - if logger != nil { - logger.Error("couldn't start watching", zap.Strings("dirs", watchOpt.dirs)) - } + logger.Error("couldn't start watching", zap.String("dir", watchOpt.dir)) + return nil, errors.New("couldn't start watching") } func stopSession(session unsafe.Pointer) { success := C.stop_watcher(session) - if success == 1 && logger != nil { + if success == 1 { logger.Error("couldn't stop watching") } } -//export go_handle_event -func go_handle_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) { +//export go_handle_file_watcher_event +func go_handle_file_watcher_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) { watchOpt := cgo.Handle(handle).Value().(*WatchOpt) if watchOpt.allowReload(C.GoString(path), int(eventType), int(pathType)) { watchOpt.trigger <- struct{}{} @@ -124,15 +125,13 @@ func listenForFileEvents(triggerWatcher chan struct{}, stopWatcher chan struct{} timer.Reset(debounceDuration) case <-timer.C: timer.Stop() - scheduleWorkerReload() + scheduleReload() } } } -func scheduleWorkerReload() { - if logger != nil { - logger.Info("filesystem change detected, restarting workers...") - } +func scheduleReload() { + logger.Info("filesystem change detected") reloadWaitGroup.Add(1) activeWatcher.callback() reloadWaitGroup.Done() diff --git a/watcher/watcher_options.go b/watcher/watcher_options.go index f1a1b411a..ece7ac535 100644 --- a/watcher/watcher_options.go +++ b/watcher/watcher_options.go @@ -8,21 +8,20 @@ import ( type WithWatchOption func(o *WatchOpt) error type WatchOpt struct { - dirs []string + dir string isRecursive bool pattern string trigger chan struct{} } -func WithWatcherDirs(dirs []string) WithWatchOption { +func WithWatcherDir(dir string) WithWatchOption { return func(o *WatchOpt) error { - for _, dir := range dirs { - absDir, err := parseAbsPath(dir) - if err != nil { - return err - } - o.dirs = append(o.dirs, absDir) + absDir, err := filepath.Abs(dir) + if err != nil { + logger.Error("dir for watching is invalid", zap.String("dir", dir)) + return err } + o.dir = absDir return nil } } @@ -41,39 +40,15 @@ func WithWatcherPattern(pattern string) WithWatchOption { } } -func parseAbsPath(path string) (string, error) { - absDir, err := filepath.Abs(path) - if err != nil { - logger.Error("path could not be watched", zap.String("path", path), zap.Error(err)) - return "", err - } - return absDir, nil -} - // TODO: support directory patterns func (watchOpt *WatchOpt) allowReload(fileName string, eventType int, pathType int) bool { if !isValidEventType(eventType) || !isValidPathType(pathType) { return false } - if watchOpt.pattern == "" { - return true - } - baseName := filepath.Base(fileName) - patternMatches, err := filepath.Match(watchOpt.pattern, baseName) - if err != nil { - logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) - return false - } if watchOpt.isRecursive { - return patternMatches + return isValidRecursivePattern(fileName, watchOpt.pattern) } - fileNameDir := filepath.Dir(fileName) - for _, dir := range watchOpt.dirs { - if dir == fileNameDir { - return patternMatches - } - } - return false + return isValidNonRecursivePattern(fileName, watchOpt.pattern, watchOpt.dir) } // 0:rename,1:modify,2:create,3:destroy,4:owner,5:other, @@ -86,3 +61,25 @@ func isValidPathType(eventType int) bool { return eventType <= 2 } +func isValidRecursivePattern(fileName string, pattern string) bool { + if pattern == "" { + return true + } + baseName := filepath.Base(fileName) + patternMatches, err := filepath.Match(pattern, baseName) + if err != nil { + logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) + return false + } + + return patternMatches +} + +func isValidNonRecursivePattern(fileName string, pattern string, dir string) bool { + fileNameDir := filepath.Dir(fileName) + if dir == fileNameDir { + return isValidRecursivePattern(fileName, pattern) + } + + return false +} diff --git a/watcher/watcher_options_test.go b/watcher/watcher_options_test.go index d00b989b6..716170950 100644 --- a/watcher/watcher_options_test.go +++ b/watcher/watcher_options_test.go @@ -19,25 +19,25 @@ func TestWithWatcherPattern(t *testing.T) { } func TestWithWatcherDir(t *testing.T) { - watchOpt := createWithOptions(t, WithWatcherDirs([]string{"/path/to/app"})) + watchOpt := createWithOptions(t, WithWatcherDir("/path/to/app")) - assert.Equal(t, "/path/to/app", watchOpt.dirs[0]) + assert.Equal(t, "/path/to/app", watchOpt.dir) } func TestWithRelativeWatcherDir(t *testing.T) { absoluteDir, err := filepath.Abs(".") - watchOpt := createWithOptions(t, WithWatcherDirs([]string{"."})) + watchOpt := createWithOptions(t, WithWatcherDir(".")) assert.NoError(t, err) - assert.Equal(t, absoluteDir, watchOpt.dirs[0]) + assert.Equal(t, absoluteDir, watchOpt.dir) } func TestAllowReloadOnMatchingPattern(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some/path"}), + WithWatcherDir("/some/path"), WithWatcherPattern("*.php"), ) @@ -48,7 +48,7 @@ func TestAllowReloadOnExactMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some/path"}), + WithWatcherDir("/some/path"), WithWatcherPattern("watch-me.php"), ) @@ -59,7 +59,7 @@ func TestDisallowOnDifferentFilename(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some/path"}), + WithWatcherDir("/some/path"), WithWatcherPattern("dont-watch.php"), ) @@ -70,7 +70,7 @@ func TestAllowReloadOnRecursiveDirectory(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some"}), + WithWatcherDir("/some"), WithWatcherRecursion(true), WithWatcherPattern("*.php"), ) @@ -82,7 +82,7 @@ func TestAllowReloadWithRecursionAndNoPattern(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some"}), + WithWatcherDir("/some"), WithWatcherRecursion(true), ) @@ -93,9 +93,9 @@ func TestDisallowOnDifferentPatterns(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some"}), + WithWatcherDir("/some"), WithWatcherRecursion(true), - WithWatcherPattern(".txt"), + WithWatcherPattern("*.txt"), ) assert.False(t, watchOpt.allowReload(fileName, 0, 0)) @@ -105,9 +105,9 @@ func TestDisallowOnMissingRecursion(t *testing.T) { const fileName = "/some/path/watch-me.php" watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some"}), + WithWatcherDir("/some"), WithWatcherRecursion(false), - WithWatcherPattern(".php"), + WithWatcherPattern("*.php"), ) assert.False(t, watchOpt.allowReload(fileName, 0, 0)) @@ -118,7 +118,7 @@ func TestDisallowOnEventTypeBiggerThan3(t *testing.T) { const eventType = 4 watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some/path"}), + WithWatcherDir("/some/path"), WithWatcherPattern("watch-me.php"), ) @@ -130,7 +130,7 @@ func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { const pathType = 3 watchOpt := createWithOptions( t, - WithWatcherDirs([]string{"/some/path"}), + WithWatcherDir("/some/path"), WithWatcherPattern("watch-me.php"), ) From e855f84a8596ed5f9e82af27ab85c81d0a83518d Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Mon, 16 Sep 2024 17:11:26 +0200 Subject: [PATCH 098/155] Update .github/workflows/sanitizers.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- .github/workflows/sanitizers.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index ec5879b1f..0f493b2b5 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -105,7 +105,7 @@ jobs: - name: Compile edant/watcher run: | cd edant/watcher/watcher-c/ - gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared + clang -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra -shared ${{ matrix.sanitizer == 'msan' && '-fsanitize=memory -fno-omit-frame-pointer -fno-optimize-sibling-calls' || '' }} sudo cp libwatcher.so /usr/local/lib/libwatcher.so sudo ldconfig - From 5713209f56f4cf509bd5ae90f901fc2d2f1c7527 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 16 Sep 2024 17:13:59 +0200 Subject: [PATCH 099/155] Adds fpic. --- .github/workflows/sanitizers.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index 0f493b2b5..84fe6d458 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -105,7 +105,7 @@ jobs: - name: Compile edant/watcher run: | cd edant/watcher/watcher-c/ - clang -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra -shared ${{ matrix.sanitizer == 'msan' && '-fsanitize=memory -fno-omit-frame-pointer -fno-optimize-sibling-calls' || '' }} + clang -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra -fPIC -shared ${{ matrix.sanitizer == 'msan' && '-fsanitize=memory -fno-omit-frame-pointer -fno-optimize-sibling-calls' || '' }} sudo cp libwatcher.so /usr/local/lib/libwatcher.so sudo ldconfig - From fe978c0c88e8e70983e7243483cdff08f337c1a4 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 16 Sep 2024 17:18:03 +0200 Subject: [PATCH 100/155] Fixes linting. --- watcher/watcher.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/watcher/watcher.c b/watcher/watcher.c index ff0a6bc41..3d9d27fce 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -2,8 +2,8 @@ #include "watcher-c.h" void process_event(struct wtr_watcher_event event, void *data) { - go_handle_file_watcher_event((char *)event.path_name, event.effect_type, event.path_type, - (uintptr_t)data); + go_handle_file_watcher_event((char *)event.path_name, event.effect_type, + event.path_type, (uintptr_t)data); } void *start_new_watcher(char const *const path, uintptr_t data) { From a5fb0d272961ba2e3bc210c92c17e508779a706f Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 17 Sep 2024 13:16:09 +0200 Subject: [PATCH 101/155] Skips tests in msan. --- frankenphp_with_watcher_test.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index b9ef56cbe..1330efd5f 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" "time" ) @@ -22,6 +23,10 @@ const minTimesToPollForChanges = 3 const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { + if isRunningInMsanMode() { + t.Skip("Skipping watcher tests in memory sanitizer mode") + return + } watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.txt"), @@ -35,6 +40,10 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { + if isRunningInMsanMode() { + t.Skip("Skipping watcher tests in memory sanitizer mode") + return + } watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.php"), @@ -57,6 +66,11 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt return string(body) } +func isRunningInMsanMode() bool { + cflags := os.Getenv("CFLAGS") + return strings.Contains(cflags, "-fsanitize=memory") +} + func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool { // first we make an initial request to start the request counter body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) From 41796bd54c125aaa31030dba8fa5e24df67c5894 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 17 Sep 2024 17:33:41 +0200 Subject: [PATCH 102/155] Resets op_cache in every worker thread after termination --- docs/config.md | 10 +++++----- frankenphp.c | 11 +++++++++++ frankenphp.h | 2 ++ frankenphp_with_watcher_test.go | 2 +- worker.go | 12 +++++++++++- 5 files changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/config.md b/docs/config.md index c6941b786..f3301cf66 100644 --- a/docs/config.md +++ b/docs/config.md @@ -141,13 +141,13 @@ This is useful for development environments. { frankenphp { worker /path/to/app/public/worker.php - watch /path/to/app + watch /path/to/app/**/*.php } } ``` The configuration above will watch the `/path/to/app` directory recursively. -If any file changes, all workers will be restarted. +If any .php file changes, all workers will be restarted. You can also add multiple `watch` directives and use simple pattern matching for files, the following is valid: @@ -163,9 +163,9 @@ You can also add multiple `watch` directives and use simple pattern matching for #### Some notes -- Directories can also be relative (to where the frankenphp process was started from) -- The `/**/` pattern signifies recursive watching and may followed by a filename pattern -- If the last part of the pattern contains the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the +- Directories can also be relative (to where the frankenphp process is started from) +- The `/**/` pattern signifies recursive watching and may be followed by a filename pattern +- If the last part of the `watch` directive contains any of the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the shell [filename pattern](https://pkg.go.dev/path/filepath#Match) - The watcher will ignore symlinks - Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts. diff --git a/frankenphp.c b/frankenphp.c index d09c1a8e5..b0815d49d 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include #include @@ -1055,3 +1056,13 @@ int frankenphp_execute_script_cli(char *script, int argc, char **argv) { return (intptr_t)exit_status; } + +int frankenphp_execute_php_code(const char *php_code) { + int ret = 0; + + zend_try { ret = zend_eval_string(php_code, NULL, (char *)""); } + zend_catch {} + zend_end_try(); + + return ret == FAILURE; +} \ No newline at end of file diff --git a/frankenphp.h b/frankenphp.h index 8cb3761b0..32a8dea18 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -56,4 +56,6 @@ void frankenphp_register_bulk_variables(go_string known_variables[27], int frankenphp_execute_script_cli(char *script, int argc, char **argv); +int frankenphp_execute_php_code(const char *php_code); + #endif diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index 1330efd5f..bfd63a4b0 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -19,7 +19,7 @@ const pollingTime = 250 // in tests checking for no reload: we will poll 3x250ms = 0.75s const minTimesToPollForChanges = 3 -// in tests checking for a reload: we will poll a maximum of 60x200ms = 12s +// in tests checking for a reload: we will poll a maximum of 60x250ms = 15s const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { diff --git a/worker.go b/worker.go index 45ca0683b..8dbc0e29d 100644 --- a/worker.go +++ b/worker.go @@ -158,7 +158,6 @@ func restartWorkers(workerOpts []workerOpt) { panic(err) } logger.Info("workers restarted successfully") - //TODO: Clear op_cache here at some point } //export go_frankenphp_worker_ready @@ -192,6 +191,8 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t { if c := l.Check(zapcore.DebugLevel, "shutting down"); c != nil { c.Write(zap.String("worker", fc.scriptFilename)) } + // TODO: should opcache_reset be conditional? + resetOpCache() return 0 case r = <-rc: @@ -240,3 +241,12 @@ func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) { c.Write(fields...) } } + +func resetOpCache() { + failure := C.frankenphp_execute_php_code(C.CString("function_exists('opcache_reset') && opcache_reset();")) + if failure == 1 { + logger.Error("failed to reset opcache") + } else { + logger.Debug("opcache reset") + } +} From 5071e7e087f7013d265a18e2051ef51c37a68e3a Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 12:29:20 +0200 Subject: [PATCH 103/155] Review fixes part 1. --- caddy/watch_config.go | 2 +- caddy/watch_config_test.go | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 4be8e8368..12709ed8f 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -9,7 +9,7 @@ import ( ) type watchConfig struct { - // FileName sets the path to the worker script. + // Directory that should be watched for changes Dir string `json:"dir,omitempty"` // Whether to watch the directory recursively IsRecursive bool `json:"recursive,omitempty"` diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go index 8b88c06bc..a7d5c6c4f 100644 --- a/caddy/watch_config_test.go +++ b/caddy/watch_config_test.go @@ -16,9 +16,8 @@ func TestParseRecursiveDirectoryWithoutPattern(t *testing.T) { } `) - assert.Nil(t, err) - assert.Equal(t, 3, len(app.Watch)) - + assert.NoError(t, err) + assert.Len(t, app.Watch, 3) assert.Equal(t, "/path1", app.Watch[0].Dir) assert.Equal(t, "/path2", app.Watch[1].Dir) assert.Equal(t, "/path3", app.Watch[2].Dir) @@ -33,15 +32,15 @@ func TestParseRecursiveDirectoryWithoutPattern(t *testing.T) { func TestParseRecursiveDirectoryWithPattern(t *testing.T) { app, err := parseTestConfig(` frankenphp { - watch /path/**/*.php - watch /path/**/filename + watch /path1/**/*.php + watch /path2/**/filename } `) - assert.Nil(t, err) - assert.Equal(t, 2, len(app.Watch)) - assert.Equal(t, "/path", app.Watch[0].Dir) - assert.Equal(t, "/path", app.Watch[1].Dir) + assert.NoError(t, err) + assert.Len(t, app.Watch, 2) + assert.Equal(t, "/path1", app.Watch[0].Dir) + assert.Equal(t, "/path2", app.Watch[1].Dir) assert.True(t, app.Watch[0].IsRecursive) assert.True(t, app.Watch[1].IsRecursive) assert.Equal(t, "*.php", app.Watch[0].Pattern) @@ -56,8 +55,8 @@ func TestParseNonRecursiveDirectoryWithPattern(t *testing.T) { } `) - assert.Nil(t, err) - assert.Equal(t, 2, len(app.Watch)) + assert.NoError(t, err) + assert.Len(t, app.Watch, 2) assert.Equal(t, "/path1", app.Watch[0].Dir) assert.Equal(t, "/path2", app.Watch[1].Dir) assert.False(t, app.Watch[0].IsRecursive) From f199b1142fe64ce35b92a131bba3ded716f0d90f Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 12:29:45 +0200 Subject: [PATCH 104/155] Test: installing libstc++ instead of gcc. --- Dockerfile | 2 +- alpine.Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7a6897600..c7fafb564 100644 --- a/Dockerfile +++ b/Dockerfile @@ -118,7 +118,7 @@ ENV GODEBUG=cgocheck=0 # copy watcher shared library COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/ # fix for the file watcher on arm -RUN apt-get install -y --no-install-recommends gcc && \ +RUN apt-get install -y --no-install-recommends libstdc++6 && \ apt-get clean && \ ldconfig diff --git a/alpine.Dockerfile b/alpine.Dockerfile index c0e2ff284..d2fd74ad2 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -136,7 +136,7 @@ ENV GODEBUG=cgocheck=0 # copy watcher shared library (libgcc and libstdc++ are needed for the watcher) COPY --from=builder /usr/local/lib/libwatcher* /usr/local/lib/ -RUN apk add --no-cache libgcc libstdc++ && \ +RUN apk add --no-cache libstdc++ && \ ldconfig /usr/local/lib COPY --from=builder /usr/local/bin/frankenphp /usr/local/bin/frankenphp From 76d4ba059bab492065a9e65a90e5d835cd4e2356 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 12:30:34 +0200 Subject: [PATCH 105/155] Test: using msan ignorelist. --- .github/workflows/sanitizers.yaml | 2 +- frankenphp_with_watcher_test.go | 13 ------------- watcher/msan-ignorelist.txt | 1 + 3 files changed, 2 insertions(+), 14 deletions(-) create mode 100644 watcher/msan-ignorelist.txt diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index 84fe6d458..8b5d0bd14 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -23,7 +23,7 @@ jobs: matrix: sanitizer: ['asan', 'msan'] env: - CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -DZEND_TRACK_ARENA_ALLOC + CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -fsanitize-ignorelist=./watcher/msan-ignorelist.txt -DZEND_TRACK_ARENA_ALLOC LDFLAGS: -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} CC: clang CXX: clang++ diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index bfd63a4b0..4ac9a965c 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -23,10 +23,6 @@ const minTimesToPollForChanges = 3 const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { - if isRunningInMsanMode() { - t.Skip("Skipping watcher tests in memory sanitizer mode") - return - } watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.txt"), @@ -40,10 +36,6 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { - if isRunningInMsanMode() { - t.Skip("Skipping watcher tests in memory sanitizer mode") - return - } watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.php"), @@ -66,11 +58,6 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt return string(body) } -func isRunningInMsanMode() bool { - cflags := os.Getenv("CFLAGS") - return strings.Contains(cflags, "-fsanitize=memory") -} - func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool { // first we make an initial request to start the request counter body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) diff --git a/watcher/msan-ignorelist.txt b/watcher/msan-ignorelist.txt new file mode 100644 index 000000000..df7ba59ba --- /dev/null +++ b/watcher/msan-ignorelist.txt @@ -0,0 +1 @@ +src:/usr/local/lib/libwatcher* \ No newline at end of file From 55b358997a11d9b48373415bf29fea806ed394cf Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 12:35:25 +0200 Subject: [PATCH 106/155] Test: using msan ignorelist. --- frankenphp_with_watcher_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index 4ac9a965c..af95bbbbb 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -8,7 +8,6 @@ import ( "net/http/httptest" "os" "path/filepath" - "strings" "testing" "time" ) From 0bf555b2aab9816f995b927c3fd9d09152cad74e Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 12:40:02 +0200 Subject: [PATCH 107/155] Test: using msan ignorelist. --- .github/workflows/sanitizers.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index 8b5d0bd14..d8a564195 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -23,7 +23,7 @@ jobs: matrix: sanitizer: ['asan', 'msan'] env: - CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -fsanitize-ignorelist=./watcher/msan-ignorelist.txt -DZEND_TRACK_ARENA_ALLOC + CFLAGS: -g -O0 -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} -DZEND_TRACK_ARENA_ALLOC LDFLAGS: -fsanitize=${{ matrix.sanitizer == 'asan' && 'address' || 'memory' }} CC: clang CXX: clang++ @@ -117,7 +117,7 @@ jobs: } >> "$GITHUB_ENV" - name: Compile tests - run: go test -${{ matrix.sanitizer }} -v -x -c + run: go test -${{ matrix.sanitizer }} -fsanitize-ignorelist=./watcher/msan-ignorelist.txt -v -x -c - name: Run tests run: ./frankenphp.test -test.v From 266829bc6ad99742c1fa4b22f549a686dd13688d Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 17:08:14 +0200 Subject: [PATCH 108/155] Allows '/**/' for global recursion and '**/' for relative recursion. --- caddy/watch_config.go | 12 +++++++----- caddy/watch_config_test.go | 16 +++++++++------- docs/config.md | 4 ++-- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/caddy/watch_config.go b/caddy/watch_config.go index 12709ed8f..fd286716c 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -45,13 +45,15 @@ func parseFullPattern(filePattern string) watchConfig { watchConfig.Pattern = baseName watchConfig.IsRecursive = false } - - if strings.Contains(filePattern, "/**/") { - dirName = strings.Split(filePattern, "/**/")[0] - watchConfig.Pattern = strings.Split(filePattern, "/**/")[1] + if strings.Contains(filePattern, "**/") { + dirName = strings.Split(filePattern, "**/")[0] + watchConfig.Pattern = strings.Split(filePattern, "**/")[1] watchConfig.IsRecursive = true } - watchConfig.Dir = strings.TrimRight(dirName, "/") + watchConfig.Dir = dirName + if dirName != "/" { + watchConfig.Dir = strings.TrimRight(dirName, "/") + } return watchConfig } diff --git a/caddy/watch_config_test.go b/caddy/watch_config_test.go index a7d5c6c4f..2d9d34ee8 100644 --- a/caddy/watch_config_test.go +++ b/caddy/watch_config_test.go @@ -13,20 +13,22 @@ func TestParseRecursiveDirectoryWithoutPattern(t *testing.T) { watch /path1 watch /path2/ watch /path3/**/ + watch /**/ + watch **/ } `) assert.NoError(t, err) - assert.Len(t, app.Watch, 3) + assert.Len(t, app.Watch, 5) assert.Equal(t, "/path1", app.Watch[0].Dir) assert.Equal(t, "/path2", app.Watch[1].Dir) assert.Equal(t, "/path3", app.Watch[2].Dir) - assert.True(t, app.Watch[0].IsRecursive) - assert.True(t, app.Watch[1].IsRecursive) - assert.True(t, app.Watch[2].IsRecursive) - assert.Equal(t, "", app.Watch[0].Pattern) - assert.Equal(t, "", app.Watch[1].Pattern) - assert.Equal(t, "", app.Watch[2].Pattern) + assert.Equal(t, "/", app.Watch[3].Dir) + assert.Equal(t, "", app.Watch[4].Dir) + for _, w := range app.Watch { + assert.True(t, w.IsRecursive) + assert.Equal(t, "", w.Pattern) + } } func TestParseRecursiveDirectoryWithPattern(t *testing.T) { diff --git a/docs/config.md b/docs/config.md index f3301cf66..341ef5356 100644 --- a/docs/config.md +++ b/docs/config.md @@ -164,10 +164,10 @@ You can also add multiple `watch` directives and use simple pattern matching for #### Some notes - Directories can also be relative (to where the frankenphp process is started from) -- The `/**/` pattern signifies recursive watching and may be followed by a filename pattern +- The `**/` pattern signifies recursive watching and may be followed by a filename pattern - If the last part of the `watch` directive contains any of the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the shell [filename pattern](https://pkg.go.dev/path/filepath#Match) -- The watcher will ignore symlinks +- The watcher will not follow symlinks - Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts. The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher). From 0d0fa51edb7d6edd2cdd2c611580f15f4b8c3f0c Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 17:10:24 +0200 Subject: [PATCH 109/155] Reverts using the ignorelist. --- .github/workflows/sanitizers.yaml | 2 +- frankenphp_with_watcher_test.go | 13 +++++++++++++ watcher/msan-ignorelist.txt | 1 - 3 files changed, 14 insertions(+), 2 deletions(-) delete mode 100644 watcher/msan-ignorelist.txt diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index d8a564195..2d1972f17 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -117,7 +117,7 @@ jobs: } >> "$GITHUB_ENV" - name: Compile tests - run: go test -${{ matrix.sanitizer }} -fsanitize-ignorelist=./watcher/msan-ignorelist.txt -v -x -c + run: go test -${{ matrix.sanitizer }} -v -x -c - name: Run tests run: ./frankenphp.test -test.v diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index af95bbbbb..e2b4398fc 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -22,6 +22,10 @@ const minTimesToPollForChanges = 3 const maxTimesToPollForChanges = 60 func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { + if isRunningInMsanMode() { + t.Skip("Skipping watcher tests in memory sanitizer mode") + return + } watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.txt"), @@ -35,6 +39,10 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { + if isRunningInMsanMode() { + t.Skip("Skipping watcher tests in memory sanitizer mode") + return + } watchOptions := []watcher.WithWatchOption{ watcher.WithWatcherDir("./testdata"), watcher.WithWatcherPattern("*.php"), @@ -57,6 +65,11 @@ func fetchBody(method string, url string, handler func(http.ResponseWriter, *htt return string(body) } +func isRunningInMsanMode() bool { + cflags := os.Getenv("CFLAGS") + return strings.Contains(cflags, "-fsanitize=memory") +} + func pollForWorkerReset(t *testing.T, handler func(http.ResponseWriter, *http.Request), limit int) bool { // first we make an initial request to start the request counter body := fetchBody("GET", "http://example.com/worker-with-watcher.php", handler) diff --git a/watcher/msan-ignorelist.txt b/watcher/msan-ignorelist.txt deleted file mode 100644 index df7ba59ba..000000000 --- a/watcher/msan-ignorelist.txt +++ /dev/null @@ -1 +0,0 @@ -src:/usr/local/lib/libwatcher* \ No newline at end of file From f025e62c43e6cd435b56f67fd79727eb4fea84b2 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 19 Sep 2024 17:50:47 +0200 Subject: [PATCH 110/155] Calls opcache directly. --- frankenphp.c | 20 ++++++++++++++------ frankenphp.h | 2 +- frankenphp_with_watcher_test.go | 1 + worker.go | 8 ++++---- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/frankenphp.c b/frankenphp.c index b0815d49d..8960dda6f 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1,7 +1,6 @@ #include #include #include -#include #include #include #include @@ -1057,12 +1056,21 @@ int frankenphp_execute_script_cli(char *script, int argc, char **argv) { return (intptr_t)exit_status; } -int frankenphp_execute_php_code(const char *php_code) { - int ret = 0; +int frankenphp_execute_php_function(const char *php_function) { + zval retval = {0}; + zend_fcall_info fci = {0}; + zend_fcall_info_cache fci_cache = {0}; + zend_string *func_name = + zend_string_init(php_function, strlen(php_function), 0); + ZVAL_STR(&fci.function_name, func_name); + fci.size = sizeof fci; + fci.retval = &retval; + int success = 0; - zend_try { ret = zend_eval_string(php_code, NULL, (char *)""); } - zend_catch {} + zend_try { success = zend_call_function(&fci, &fci_cache) == SUCCESS; } zend_end_try(); - return ret == FAILURE; + zend_string_release(func_name); + + return success; } \ No newline at end of file diff --git a/frankenphp.h b/frankenphp.h index 32a8dea18..5feb71bba 100644 --- a/frankenphp.h +++ b/frankenphp.h @@ -56,6 +56,6 @@ void frankenphp_register_bulk_variables(go_string known_variables[27], int frankenphp_execute_script_cli(char *script, int argc, char **argv); -int frankenphp_execute_php_code(const char *php_code); +int frankenphp_execute_php_function(const char *php_function); #endif diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index e2b4398fc..6638a61a7 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "testing" "time" + "strings" ) // we have to wait a few milliseconds for the watcher debounce to take effect diff --git a/worker.go b/worker.go index 8dbc0e29d..8ca8cb178 100644 --- a/worker.go +++ b/worker.go @@ -243,10 +243,10 @@ func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) { } func resetOpCache() { - failure := C.frankenphp_execute_php_code(C.CString("function_exists('opcache_reset') && opcache_reset();")) - if failure == 1 { - logger.Error("failed to reset opcache") + success := C.frankenphp_execute_php_function(C.CString("opcache_reset")) + if success == 1 { + logger.Debug("opcache_reset successful") } else { - logger.Debug("opcache reset") + logger.Error("opcache_reset failed") } } From 03123c49173839287f53c155a88747121d884b49 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 20 Sep 2024 00:10:52 +0200 Subject: [PATCH 111/155] Adds --watch to php-server command --- caddy/caddy.go | 5 +++-- caddy/php-server.go | 9 +++++++-- caddy/watch_config.go | 14 ++++++-------- docs/worker.md | 9 ++++++++- watcher/watcher.go | 1 + 5 files changed, 25 insertions(+), 13 deletions(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index 75b4a8ae8..67fd5932d 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -134,9 +134,10 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { f.NumThreads = v case "watch": - if err := parseWatchDirective(f, d); err != nil { - return err + if !d.NextArg() { + return d.Err("The 'watch' directive must be followed by a path") } + f.Watch = append(f.Watch, parseWatchConfig(d.Val())) case "worker": wc := workerConfig{} if d.NextArg() { diff --git a/caddy/php-server.go b/caddy/php-server.go index 1f2ff41c3..f7f8bbfbd 100644 --- a/caddy/php-server.go +++ b/caddy/php-server.go @@ -29,7 +29,7 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "php-server", - Usage: "[--domain ] [--root ] [--listen ] [--worker /path/to/worker.php<,nb-workers>] [--access-log] [--debug] [--no-compress] [--mercure]", + Usage: "[--domain ] [--root ] [--listen ] [--worker /path/to/worker.php<,nb-workers>] [--watch path/to/watch] [--access-log] [--debug] [--no-compress] [--mercure]", Short: "Spins up a production-ready PHP server", Long: ` A simple but production-ready PHP server. Useful for quick deployments, @@ -48,6 +48,7 @@ For more advanced use cases, see https://github.com/dunglas/frankenphp/blob/main cmd.Flags().StringP("root", "r", "", "The path to the root of the site") cmd.Flags().StringP("listen", "l", "", "The address to which to bind the listener") cmd.Flags().StringArrayP("worker", "w", []string{}, "Worker script") + cmd.Flags().StringArrayP("watch", "", []string{}, "Directory to watch for file changes") cmd.Flags().BoolP("access-log", "a", false, "Enable the access log") cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs") cmd.Flags().BoolP("mercure", "m", false, "Enable the built-in Mercure.rocks hub") @@ -73,6 +74,10 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) { if err != nil { panic(err) } + watch, err := fs.GetStringArray("watch") + if err != nil { + panic(err) + } var workersOption []workerConfig if len(workers) != 0 { @@ -305,7 +310,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) { }, AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), - "frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption}, nil), + "frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption, Watch: parseWatchConfigs(watch)}, nil), }, } diff --git a/caddy/watch_config.go b/caddy/watch_config.go index fd286716c..3678ff60d 100644 --- a/caddy/watch_config.go +++ b/caddy/watch_config.go @@ -1,7 +1,6 @@ package caddy import ( - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/dunglas/frankenphp" "github.com/dunglas/frankenphp/watcher" "path/filepath" @@ -25,18 +24,17 @@ func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frank )) } -func parseWatchDirective(f *FrankenPHPApp, d *caddyfile.Dispenser) error { - if !d.NextArg() { - return d.Err("The 'watch' directive must be followed by a path") +func parseWatchConfigs(filePatterns []string) []watchConfig { + watchConfigs := []watchConfig{} + for _, filePattern := range filePatterns { + watchConfigs = append(watchConfigs, parseWatchConfig(filePattern)) } - f.Watch = append(f.Watch, parseFullPattern(d.Val())) - - return nil + return watchConfigs } // TODO: better path validation? // for the one line short-form in the caddy config, aka: 'watch /path/*pattern' -func parseFullPattern(filePattern string) watchConfig { +func parseWatchConfig(filePattern string) watchConfig { watchConfig := watchConfig{IsRecursive: true} dirName := filePattern splitDirName, baseName := filepath.Split(filePattern) diff --git a/docs/worker.md b/docs/worker.md index c2f4c0e19..efd03d67d 100644 --- a/docs/worker.md +++ b/docs/worker.md @@ -25,9 +25,16 @@ Use the `--worker` option of the `php-server` command to serve the content of th ./frankenphp php-server --worker /path/to/your/worker/script.php ``` -If your PHP app is [embeded in the binary](embed.md), you can add a custom `Caddyfile` in the root directory of the app. +If your PHP app is [embedded in the binary](embed.md), you can add a custom `Caddyfile` in the root directory of the app. It will be used automatically. +It's also possible to [restart the worker on file changes](config.md#watching-for-file-changes) with the `--watch` option. +The following command will trigger a restart if any file ending in `.php` in the `/path/to/your/app/` directory or subdirectories is modified: + +```console +./frankenphp php-server --worker /path/to/your/worker/script.php --watch "/path/to/your/app/**/*.php" +``` + ## Symfony Runtime The worker mode of FrankenPHP is supported by the [Symfony Runtime Component](https://symfony.com/doc/current/components/runtime.html). diff --git a/watcher/watcher.go b/watcher/watcher.go index 6e6b721e1..a74211313 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -91,6 +91,7 @@ func startSession(watchOpt *WatchOpt) (unsafe.Pointer, error) { cPathTranslated := (*C.char)(C.CString(watchOpt.dir)) watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) if watchSession != C.NULL { + logger.Debug("watching", zap.String("dir", watchOpt.dir), zap.String("pattern", watchOpt.pattern)) return watchSession, nil } logger.Error("couldn't start watching", zap.String("dir", watchOpt.dir)) From 4c8a9c165ebf9d373011477036094e349b72461e Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 24 Sep 2024 23:10:50 +0200 Subject: [PATCH 112/155] Properly free CStrings. --- frankenphp.go | 12 ++++++++++++ watcher/watcher.go | 5 +++-- worker.go | 11 +---------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 68915816f..8bfb3fd96 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -829,3 +829,15 @@ func freeArgs(argv []*C.char) { C.free(unsafe.Pointer(arg)) } } + +func executePhpFunction(functionName string) { + cFunctionName := C.CString(functionName) + defer C.free(unsafe.Pointer(cFunctionName)) + + success := C.frankenphp_execute_php_function(C.CString(functionName)) + if success == 1 { + logger.Debug("php function call successful", zap.String("function", functionName)) + } else { + logger.Error("php function call failed", zap.String("function", functionName)) + } +} diff --git a/watcher/watcher.go b/watcher/watcher.go index a74211313..c27d46451 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -88,8 +88,9 @@ func (w *watcher) stopWatching() { func startSession(watchOpt *WatchOpt) (unsafe.Pointer, error) { handle := cgo.NewHandle(watchOpt) - cPathTranslated := (*C.char)(C.CString(watchOpt.dir)) - watchSession := C.start_new_watcher(cPathTranslated, C.uintptr_t(handle)) + cDir := C.CString(watchOpt.dir) + defer C.free(unsafe.Pointer(cDir)) + watchSession := C.start_new_watcher(cDir, C.uintptr_t(handle)) if watchSession != C.NULL { logger.Debug("watching", zap.String("dir", watchOpt.dir), zap.String("pattern", watchOpt.pattern)) return watchSession, nil diff --git a/worker.go b/worker.go index 8ca8cb178..b3402b2c8 100644 --- a/worker.go +++ b/worker.go @@ -192,7 +192,7 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t { c.Write(zap.String("worker", fc.scriptFilename)) } // TODO: should opcache_reset be conditional? - resetOpCache() + executePhpFunction("opcache_reset") return 0 case r = <-rc: @@ -241,12 +241,3 @@ func go_frankenphp_finish_request(mrh, rh C.uintptr_t, deleteHandle bool) { c.Write(fields...) } } - -func resetOpCache() { - success := C.frankenphp_execute_php_function(C.CString("opcache_reset")) - if success == 1 { - logger.Debug("opcache_reset successful") - } else { - logger.Error("opcache_reset failed") - } -} From 5f37d25fcb0d511ded1af3b0931c9597edf8a05c Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 24 Sep 2024 23:49:57 +0200 Subject: [PATCH 113/155] Sorts alphabetically and uses curl instead of git. --- Dockerfile | 5 ++--- alpine.Dockerfile | 15 ++++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index c7fafb564..661afc77a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -68,7 +68,6 @@ RUN apt-get update && \ libssl-dev \ libxml2-dev \ zlib1g-dev \ - git \ && \ apt-get clean @@ -91,8 +90,8 @@ COPY --link watcher watcher # install edant/watcher (necessary for file watching) ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher -RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . -WORKDIR /usr/local/src/watcher/watcher-c +RUN curl -L https://github.com/e-dant/watcher/archive/refs/heads/$EDANT_WATCHER_VERSION.tar.gz | tar xz +WORKDIR /usr/local/src/watcher/watcher-c/watcher-$EDANT_WATCHER_VERSION RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ cp libwatcher.so /usr/local/lib/libwatcher.so && \ ldconfig /usr/local/lib diff --git a/alpine.Dockerfile b/alpine.Dockerfile index d2fd74ad2..f79d01ea2 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -58,11 +58,17 @@ ENV PATH=/usr/local/go/bin:$PATH RUN apk add --no-cache --virtual .build-deps \ $PHPIZE_DEPS \ argon2-dev \ + # Needed for the custom Go build + bash \ brotli-dev \ coreutils \ curl-dev \ + # Needed for the custom Go build + git \ gnu-libiconv-dev \ libsodium-dev \ + # Needed for the file watcher + libstdc++ \ libxml2-dev \ linux-headers \ oniguruma-dev \ @@ -70,11 +76,6 @@ RUN apk add --no-cache --virtual .build-deps \ readline-dev \ sqlite-dev \ upx \ - # Needed for the file watcher - libstdc++ \ - # Needed for the custom Go build - git \ - bash # FIXME: temporary workaround for https://github.com/golang/go/issues/68285 WORKDIR / @@ -110,8 +111,8 @@ COPY --link watcher watcher # install edant/watcher (necessary for file watching) ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher -RUN git clone --branch=$EDANT_WATCHER_VERSION https://github.com/e-dant/watcher . -WORKDIR /usr/local/src/watcher/watcher-c +RUN curl -L https://github.com/e-dant/watcher/archive/refs/heads/$EDANT_WATCHER_VERSION.tar.gz | tar xz +WORKDIR /usr/local/src/watcher/watcher-c/watcher-$EDANT_WATCHER_VERSION RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ cp libwatcher.so /usr/local/lib/libwatcher.so && \ ldconfig /usr/local/lib From f925f545f9c92a246a118b4affa118dbc68943c7 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Tue, 24 Sep 2024 23:51:18 +0200 Subject: [PATCH 114/155] Labeling and formatting. --- caddy/php-server.go | 2 +- caddy/{watch_config.go => watch.go} | 0 caddy/{watch_config_test.go => watch_test.go} | 0 docs/config.md | 16 ++++++++-------- worker.go | 1 - 5 files changed, 9 insertions(+), 10 deletions(-) rename caddy/{watch_config.go => watch.go} (100%) rename caddy/{watch_config_test.go => watch_test.go} (100%) diff --git a/caddy/php-server.go b/caddy/php-server.go index f7f8bbfbd..f31887e1d 100644 --- a/caddy/php-server.go +++ b/caddy/php-server.go @@ -29,7 +29,7 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "php-server", - Usage: "[--domain ] [--root ] [--listen ] [--worker /path/to/worker.php<,nb-workers>] [--watch path/to/watch] [--access-log] [--debug] [--no-compress] [--mercure]", + Usage: "[--domain ] [--root ] [--listen ] [--worker /path/to/worker.php<,nb-workers>] [--watch /path/to/watch] [--access-log] [--debug] [--no-compress] [--mercure]", Short: "Spins up a production-ready PHP server", Long: ` A simple but production-ready PHP server. Useful for quick deployments, diff --git a/caddy/watch_config.go b/caddy/watch.go similarity index 100% rename from caddy/watch_config.go rename to caddy/watch.go diff --git a/caddy/watch_config_test.go b/caddy/watch_test.go similarity index 100% rename from caddy/watch_config_test.go rename to caddy/watch_test.go diff --git a/docs/config.md b/docs/config.md index be1ada28a..a335d5a7a 100644 --- a/docs/config.md +++ b/docs/config.md @@ -163,12 +163,12 @@ You can also add multiple `watch` directives and use simple pattern matching for #### Some notes -- Directories can also be relative (to where the frankenphp process is started from) -- The `**/` pattern signifies recursive watching and may be followed by a filename pattern -- If the last part of the `watch` directive contains any of the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the +* Directories can also be relative (to where the frankenphp process is started from) +* The `**/` pattern signifies recursive watching and may be followed by a filename pattern +* If the last part of the `watch` directive contains any of the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the shell [filename pattern](https://pkg.go.dev/path/filepath#Match) -- The watcher will not follow symlinks -- Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts. +* The watcher will not follow symlinks +* Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts. The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher). @@ -202,9 +202,9 @@ You can find more information about this setting in the [Caddy documentation](ht The following environment variables can be used to inject Caddy directives in the `Caddyfile` without modifying it: -- `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate -- `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options) -- `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive +* `SERVER_NAME`: change [the addresses on which to listen](https://caddyserver.com/docs/caddyfile/concepts#addresses), the provided hostnames will also be used for the generated TLS certificate +* `CADDY_GLOBAL_OPTIONS`: inject [global options](https://caddyserver.com/docs/caddyfile/options) +* `FRANKENPHP_CONFIG`: inject config under the `frankenphp` directive As for FPM and CLI SAPIs, environment variables are exposed by default in the `$_SERVER` superglobal. diff --git a/worker.go b/worker.go index b3402b2c8..f9f81541b 100644 --- a/worker.go +++ b/worker.go @@ -191,7 +191,6 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t { if c := l.Check(zapcore.DebugLevel, "shutting down"); c != nil { c.Write(zap.String("worker", fc.scriptFilename)) } - // TODO: should opcache_reset be conditional? executePhpFunction("opcache_reset") return 0 From 1193de8cc4fb6ef3838af84f6f35d73852ceb021 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:53:34 +0200 Subject: [PATCH 115/155] Update .github/workflows/sanitizers.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- .github/workflows/sanitizers.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index 2d1972f17..081f32e49 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -96,7 +96,8 @@ jobs: - name: Add PHP to the PATH run: echo "$(pwd)/php/target/bin" >> "$GITHUB_PATH" - - uses: actions/checkout@v4 + - + uses: actions/checkout@v4 name: Checkout watcher with: repository: e-dant/watcher From ef21c7790bb27d452028ccbcdbf26dcfad76a732 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:53:44 +0200 Subject: [PATCH 116/155] Update .github/workflows/sanitizers.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- .github/workflows/sanitizers.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sanitizers.yaml b/.github/workflows/sanitizers.yaml index 081f32e49..39902ae9d 100644 --- a/.github/workflows/sanitizers.yaml +++ b/.github/workflows/sanitizers.yaml @@ -103,7 +103,8 @@ jobs: repository: e-dant/watcher ref: ${{ env.EDANT_WATCHER_VERSION }} path: 'edant/watcher' - - name: Compile edant/watcher + - + name: Compile edant/watcher run: | cd edant/watcher/watcher-c/ clang -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra -fPIC -shared ${{ matrix.sanitizer == 'msan' && '-fsanitize=memory -fno-omit-frame-pointer -fno-optimize-sibling-calls' || '' }} From 009c0b4ce336fc3d40f15d7d20f9ad944600f424 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:53:51 +0200 Subject: [PATCH 117/155] Update .github/workflows/tests.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- .github/workflows/tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d323e3872..b20dd3492 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -44,7 +44,8 @@ jobs: env: phpts: ts debug: true - - uses: actions/checkout@v4 + - + uses: actions/checkout@v4 name: Checkout watcher with: repository: e-dant/watcher From 6be5f99113937dd7dd8dcea87eafc2f8a4ad52eb Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:53:57 +0200 Subject: [PATCH 118/155] Update .github/workflows/tests.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- .github/workflows/tests.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index b20dd3492..6f2db6e84 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -51,7 +51,8 @@ jobs: repository: e-dant/watcher ref: ${{ env.EDANT_WATCHER_VERSION }} path: 'edant/watcher' - - name: Compile edant/watcher + - + name: Compile edant/watcher run: | cd edant/watcher/watcher-c/ gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared From be2c344c37dc3ccee31bf53299e25f13a8ed8e36 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:54:14 +0200 Subject: [PATCH 119/155] Update caddy/caddy.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- caddy/caddy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index 67fd5932d..e0b921b87 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -135,7 +135,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { f.NumThreads = v case "watch": if !d.NextArg() { - return d.Err("The 'watch' directive must be followed by a path") + return d.Err(`The "watch" directive must be followed by a path`) } f.Watch = append(f.Watch, parseWatchConfig(d.Val())) case "worker": From 4e22dcda400ad38bf6935ac984e064da4b1bc147 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:54:38 +0200 Subject: [PATCH 120/155] Update docs/config.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index a335d5a7a..e26fac188 100644 --- a/docs/config.md +++ b/docs/config.md @@ -147,7 +147,7 @@ This is useful for development environments. ``` The configuration above will watch the `/path/to/app` directory recursively. -If any .php file changes, all workers will be restarted. +If any `.php` file changes, all workers will be restarted. You can also add multiple `watch` directives and use simple pattern matching for files, the following is valid: From 6bad089b4027e2ce75e0cdd23fc0e9c8ba7eda24 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:54:53 +0200 Subject: [PATCH 121/155] Update frankenphp_with_watcher_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- frankenphp_with_watcher_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index 6638a61a7..fa6b4a765 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -42,6 +42,7 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { if isRunningInMsanMode() { t.Skip("Skipping watcher tests in memory sanitizer mode") + return } watchOptions := []watcher.WithWatchOption{ From 01e320ee7a7ad603dc316a928f7e97415939b4e2 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:55:20 +0200 Subject: [PATCH 122/155] Update watcher/watcher.h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- watcher/watcher.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher/watcher.h b/watcher/watcher.h index 25a6e75c0..20fb5e0d8 100644 --- a/watcher/watcher.h +++ b/watcher/watcher.h @@ -3,4 +3,4 @@ void *start_new_watcher(char const *const path, uintptr_t data); -int stop_watcher(void *watcher); \ No newline at end of file +int stop_watcher(void *watcher); From e40d455a7c8c171220a4e0ea4e40c0d99a6d2d9e Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:55:32 +0200 Subject: [PATCH 123/155] Update frankenphp.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- frankenphp.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frankenphp.c b/frankenphp.c index 8960dda6f..5308ea039 100644 --- a/frankenphp.c +++ b/frankenphp.c @@ -1073,4 +1073,4 @@ int frankenphp_execute_php_function(const char *php_function) { zend_string_release(func_name); return success; -} \ No newline at end of file +} From 26f02bbad36df82ad11d21deec6d1595dfee9610 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:55:47 +0200 Subject: [PATCH 124/155] Update watcher/watcher.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- watcher/watcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher/watcher.go b/watcher/watcher.go index c27d46451..dbc85d2de 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -56,7 +56,7 @@ func DrainWatcher() { if activeWatcher == nil { return } - logger.Info("stopping watcher...") + logger.Debug("stopping watcher") activeWatcher.stopWatching() reloadWaitGroup.Wait() activeWatcher = nil From dbbdf5e71e0e58a74e84a83016aa5d1282022d8a Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:57:00 +0200 Subject: [PATCH 125/155] Update docs/config.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index e26fac188..ab6aef5e6 100644 --- a/docs/config.md +++ b/docs/config.md @@ -161,7 +161,7 @@ You can also add multiple `watch` directives and use simple pattern matching for } ``` -#### Some notes +#### Advanced Watchers Configuration * Directories can also be relative (to where the frankenphp process is started from) * The `**/` pattern signifies recursive watching and may be followed by a filename pattern From 393be86b0ecf464059fc441153145242dc9a4dd6 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:57:27 +0200 Subject: [PATCH 126/155] Update frankenphp_with_watcher_test.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- frankenphp_with_watcher_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index fa6b4a765..a1baaf3ae 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -36,7 +36,7 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) assert.True(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) + }, &testOptions{workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { From 9df7894d742c62d9a677a18b07a61909ee69e75b Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:58:01 +0200 Subject: [PATCH 127/155] Update testdata/files/.gitignore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- testdata/files/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/files/.gitignore b/testdata/files/.gitignore index 314f02b1b..2211df63d 100644 --- a/testdata/files/.gitignore +++ b/testdata/files/.gitignore @@ -1 +1 @@ -*.txt \ No newline at end of file +*.txt From b2837dffc82200caeecf0ddf047dbc29f1a63ac9 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:58:26 +0200 Subject: [PATCH 128/155] Update watcher/watcher-c.h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- watcher/watcher-c.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher/watcher-c.h b/watcher/watcher-c.h index 2abb8a82d..5d6163c4f 100644 --- a/watcher/watcher-c.h +++ b/watcher/watcher-c.h @@ -76,4 +76,4 @@ bool wtr_watcher_close_pipe(void *watcher, int read_fd, int write_fd); #ifdef __cplusplus } -#endif \ No newline at end of file +#endif From 9a1ac7ce79e48974f7adbcb378fd24a34e1a1fa0 Mon Sep 17 00:00:00 2001 From: Alexander Stecher <45872305+AlliBalliBaba@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:58:50 +0200 Subject: [PATCH 129/155] Update watcher/watcher.c MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Kévin Dunglas --- watcher/watcher.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher/watcher.c b/watcher/watcher.c index 3d9d27fce..ab8b6d4ea 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -19,4 +19,4 @@ int stop_watcher(void *watcher) { return 1; } return 0; -} \ No newline at end of file +} From 4ce9d37a585e67df73b2c3596f3316e80c32ea50 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 25 Sep 2024 00:03:14 +0200 Subject: [PATCH 130/155] Fixes test and Dockerfile. --- alpine.Dockerfile | 2 +- frankenphp_with_watcher_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alpine.Dockerfile b/alpine.Dockerfile index f79d01ea2..cd3e69e1f 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -75,7 +75,7 @@ RUN apk add --no-cache --virtual .build-deps \ openssl-dev \ readline-dev \ sqlite-dev \ - upx \ + upx # FIXME: temporary workaround for https://github.com/golang/go/issues/68285 WORKDIR / diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index a1baaf3ae..fa6b4a765 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -36,7 +36,7 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) assert.True(t, requestBodyHasReset) - }, &testOptions{workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { From 01cc6eb11fbf25b30cb2bb4f4cda6b2c480cb426 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 25 Sep 2024 00:06:29 +0200 Subject: [PATCH 131/155] Fixes Dockerfiles. --- Dockerfile | 2 +- alpine.Dockerfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 661afc77a..a3ff89b71 100644 --- a/Dockerfile +++ b/Dockerfile @@ -91,7 +91,7 @@ COPY --link watcher watcher ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher RUN curl -L https://github.com/e-dant/watcher/archive/refs/heads/$EDANT_WATCHER_VERSION.tar.gz | tar xz -WORKDIR /usr/local/src/watcher/watcher-c/watcher-$EDANT_WATCHER_VERSION +WORKDIR /usr/local/src/watcher/watcher-$EDANT_WATCHER_VERSION/watcher-c RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ cp libwatcher.so /usr/local/lib/libwatcher.so && \ ldconfig /usr/local/lib diff --git a/alpine.Dockerfile b/alpine.Dockerfile index cd3e69e1f..2422eb6ac 100644 --- a/alpine.Dockerfile +++ b/alpine.Dockerfile @@ -112,7 +112,7 @@ COPY --link watcher watcher ARG EDANT_WATCHER_VERSION=next WORKDIR /usr/local/src/watcher RUN curl -L https://github.com/e-dant/watcher/archive/refs/heads/$EDANT_WATCHER_VERSION.tar.gz | tar xz -WORKDIR /usr/local/src/watcher/watcher-c/watcher-$EDANT_WATCHER_VERSION +WORKDIR /usr/local/src/watcher/watcher-$EDANT_WATCHER_VERSION/watcher-c RUN gcc -o libwatcher.so ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -O3 -Wall -Wextra -fPIC -shared && \ cp libwatcher.so /usr/local/lib/libwatcher.so && \ ldconfig /usr/local/lib From 672f7ee1bf4573936a3f22b660244f7e5564e340 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 26 Sep 2024 11:24:01 +0200 Subject: [PATCH 132/155] Resets go versions. --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c82ae18fb..1c9a54b62 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/dunglas/frankenphp -go 1.22.0 +go 1.21 -toolchain go1.22.6 +toolchain go1.22.0 retract v1.0.0-rc.1 // Human error From 5c3e0b9e0ccfb6ad8ad51f706a1a90a6d2af90d8 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 26 Sep 2024 11:25:09 +0200 Subject: [PATCH 133/155] Replaces unsafe.pointer with uintptr_t --- watcher/watcher.c | 18 +++++++----------- watcher/watcher.go | 19 +++++++++++-------- watcher/watcher.h | 4 ++-- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/watcher/watcher.c b/watcher/watcher.c index ab8b6d4ea..f53de6718 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -1,22 +1,18 @@ #include "_cgo_export.h" #include "watcher-c.h" -void process_event(struct wtr_watcher_event event, void *data) { +void handle_event(struct wtr_watcher_event event, void *data) { go_handle_file_watcher_event((char *)event.path_name, event.effect_type, event.path_type, (uintptr_t)data); } -void *start_new_watcher(char const *const path, uintptr_t data) { - void *watcher = wtr_watcher_open(path, process_event, (void *)data); - if (!watcher) { - return NULL; - } - return watcher; +uintptr_t start_new_watcher(char const *const path, uintptr_t data) { + return (uintptr_t)wtr_watcher_open(path, handle_event, (void *)data); } -int stop_watcher(void *watcher) { - if (!wtr_watcher_close(watcher)) { - return 1; +int stop_watcher(uintptr_t watcher) { + if (!wtr_watcher_close((void *)watcher)) { + return 0; } - return 0; + return 1; } diff --git a/watcher/watcher.go b/watcher/watcher.go index dbc85d2de..895c6ff3f 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -16,7 +16,7 @@ import ( ) type watcher struct { - sessions []unsafe.Pointer + sessions []C.uintptr_t callback func() watchOpts []WatchOpt trigger chan struct{} @@ -31,7 +31,10 @@ var ( activeWatcher *watcher // after stopping the watcher we will wait for eventual reloads to finish reloadWaitGroup sync.WaitGroup + // we are passing the logger from the main package to the watcher logger *zap.Logger + AlreadyStartedError = errors.New("The watcher is already running") + UnableToStartWatching = errors.New("Unable to start the watcher") ) func InitWatcher(watchOpts []WatchOpt, callback func(), zapLogger *zap.Logger) error { @@ -39,7 +42,7 @@ func InitWatcher(watchOpts []WatchOpt, callback func(), zapLogger *zap.Logger) e return nil } if activeWatcher != nil { - return errors.New("watcher is already running") + return AlreadyStartedError } logger = zapLogger activeWatcher = &watcher{callback: callback} @@ -65,7 +68,7 @@ func DrainWatcher() { func (w *watcher) startWatching(watchOpts []WatchOpt) error { w.trigger = make(chan struct{}) w.stop = make(chan struct{}) - w.sessions = make([]unsafe.Pointer, len(watchOpts)) + w.sessions = make([]C.uintptr_t, len(watchOpts)) w.watchOpts = watchOpts for i, watchOpt := range w.watchOpts { watchOpt.trigger = w.trigger @@ -86,23 +89,23 @@ func (w *watcher) stopWatching() { } } -func startSession(watchOpt *WatchOpt) (unsafe.Pointer, error) { +func startSession(watchOpt *WatchOpt) (C.uintptr_t, error) { handle := cgo.NewHandle(watchOpt) cDir := C.CString(watchOpt.dir) defer C.free(unsafe.Pointer(cDir)) watchSession := C.start_new_watcher(cDir, C.uintptr_t(handle)) - if watchSession != C.NULL { + if watchSession != 0 { logger.Debug("watching", zap.String("dir", watchOpt.dir), zap.String("pattern", watchOpt.pattern)) return watchSession, nil } logger.Error("couldn't start watching", zap.String("dir", watchOpt.dir)) - return nil, errors.New("couldn't start watching") + return watchSession, UnableToStartWatching } -func stopSession(session unsafe.Pointer) { +func stopSession(session C.uintptr_t) { success := C.stop_watcher(session) - if success == 1 { + if success == 0 { logger.Error("couldn't stop watching") } } diff --git a/watcher/watcher.h b/watcher/watcher.h index 20fb5e0d8..d492cb812 100644 --- a/watcher/watcher.h +++ b/watcher/watcher.h @@ -1,6 +1,6 @@ #include #include -void *start_new_watcher(char const *const path, uintptr_t data); +uintptr_t start_new_watcher(char const *const path, uintptr_t data); -int stop_watcher(void *watcher); +int stop_watcher(uintptr_t watcher); From 08eaf89b781baa838d1167a304fd2d8599f5c6eb Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Thu, 26 Sep 2024 21:10:27 +0200 Subject: [PATCH 134/155] Prevents worker channels from being destroyed on reload. --- worker.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/worker.go b/worker.go index f9f81541b..56a0428e8 100644 --- a/worker.go +++ b/worker.go @@ -22,12 +22,16 @@ var ( workersReadyWG sync.WaitGroup workerShutdownWG sync.WaitGroup workersAreReady atomic.Bool + workersAreDone atomic.Bool workersDone chan interface{} ) // TODO: start all the worker in parallell to reduce the boot time func initWorkers(opt []workerOpt) error { workersDone = make(chan interface{}) + workersAreReady.Store(false) + workersAreDone.Store(false) + for _, w := range opt { if err := startWorkers(w.fileName, w.num, w.env); err != nil { return err @@ -43,14 +47,12 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { return fmt.Errorf("workers %q: %w", fileName, err) } - if _, ok := workersRequestChans.Load(absFileName); ok { - return fmt.Errorf("workers %q: already started", absFileName) - } + if _, ok := workersRequestChans.Load(absFileName); !ok { + workersRequestChans.Store(absFileName, make(chan *http.Request)) + } - workersRequestChans.Store(absFileName, make(chan *http.Request)) shutdownWG.Add(nbWorkers) workerShutdownWG.Add(nbWorkers) - workersAreReady.Store(false) workersReadyWG.Add(nbWorkers) var ( @@ -101,7 +103,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { } // TODO: make the max restart configurable - if _, ok := workersRequestChans.Load(absFileName); ok { + if !workersAreDone.Load() { if fc.exitStatus == 0 { if c := l.Check(zapcore.InfoLevel, "restarting"); c != nil { c.Write(zap.String("worker", absFileName)) @@ -138,21 +140,19 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { } func stopWorkers() { - workersRequestChans.Range(func(k, v any) bool { - workersRequestChans.Delete(k) - - return true - }) + workersAreDone.Store(true) close(workersDone) } func drainWorkers() { stopWorkers() workerShutdownWG.Wait() + workersRequestChans = sync.Map{} } func restartWorkers(workerOpts []workerOpt) { - drainWorkers() + stopWorkers() + workerShutdownWG.Wait() if err := initWorkers(workerOpts); err != nil { logger.Error("failed to restart workers when watching files") panic(err) From df4e1441c8caa01ddf1b6597d4f0f87c61bf088a Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 00:09:52 +0200 Subject: [PATCH 135/155] Minimizes the public api by only passing a []string. --- caddy/caddy.go | 9 +- caddy/php-server.go | 2 +- caddy/watch.go | 57 ----------- caddy/watch_test.go | 84 ---------------- frankenphp_test.go | 7 +- frankenphp_with_watcher_test.go | 17 +--- options.go | 15 +-- watcher/watcher.c | 6 +- watcher/watcher.go | 24 +++-- watcher/watcher_options.go | 56 +++++++---- watcher/watcher_options_test.go | 171 ++++++++++++++++++-------------- 11 files changed, 167 insertions(+), 281 deletions(-) delete mode 100644 caddy/watch.go delete mode 100644 caddy/watch_test.go diff --git a/caddy/caddy.go b/caddy/caddy.go index e0b921b87..ec70e0e96 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -67,7 +67,7 @@ type FrankenPHPApp struct { // Workers configures the worker scripts to start. Workers []workerConfig `json:"workers,omitempty"` // Directories to watch for changes - Watch []watchConfig `json:"watch,omitempty"` + Watch []string `json:"watch,omitempty"` } // CaddyModule returns the Caddy module information. @@ -86,9 +86,8 @@ func (f *FrankenPHPApp) Start() error { for _, w := range f.Workers { opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env)) } - for _, watchConfig := range f.Watch { - opts = applyWatchConfig(opts, watchConfig) - } + + opts = append(opts, frankenphp.WithFileWatcher(f.Watch)) _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) { if err := frankenphp.Init(opts...); err != nil { @@ -137,7 +136,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { if !d.NextArg() { return d.Err(`The "watch" directive must be followed by a path`) } - f.Watch = append(f.Watch, parseWatchConfig(d.Val())) + f.Watch = append(f.Watch, d.Val()) case "worker": wc := workerConfig{} if d.NextArg() { diff --git a/caddy/php-server.go b/caddy/php-server.go index f31887e1d..0dbe9ec3c 100644 --- a/caddy/php-server.go +++ b/caddy/php-server.go @@ -310,7 +310,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) { }, AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), - "frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption, Watch: parseWatchConfigs(watch)}, nil), + "frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption, Watch: watch}, nil), }, } diff --git a/caddy/watch.go b/caddy/watch.go deleted file mode 100644 index 3678ff60d..000000000 --- a/caddy/watch.go +++ /dev/null @@ -1,57 +0,0 @@ -package caddy - -import ( - "github.com/dunglas/frankenphp" - "github.com/dunglas/frankenphp/watcher" - "path/filepath" - "strings" -) - -type watchConfig struct { - // Directory that should be watched for changes - Dir string `json:"dir,omitempty"` - // Whether to watch the directory recursively - IsRecursive bool `json:"recursive,omitempty"` - // The shell filename pattern to match against - Pattern string `json:"pattern,omitempty"` -} - -func applyWatchConfig(opts []frankenphp.Option, watchConfig watchConfig) []frankenphp.Option { - return append(opts, frankenphp.WithFileWatcher( - watcher.WithWatcherDir(watchConfig.Dir), - watcher.WithWatcherRecursion(watchConfig.IsRecursive), - watcher.WithWatcherPattern(watchConfig.Pattern), - )) -} - -func parseWatchConfigs(filePatterns []string) []watchConfig { - watchConfigs := []watchConfig{} - for _, filePattern := range filePatterns { - watchConfigs = append(watchConfigs, parseWatchConfig(filePattern)) - } - return watchConfigs -} - -// TODO: better path validation? -// for the one line short-form in the caddy config, aka: 'watch /path/*pattern' -func parseWatchConfig(filePattern string) watchConfig { - watchConfig := watchConfig{IsRecursive: true} - dirName := filePattern - splitDirName, baseName := filepath.Split(filePattern) - if filePattern != "." && filePattern != ".." && strings.ContainsAny(baseName, "*.[?\\") { - dirName = splitDirName - watchConfig.Pattern = baseName - watchConfig.IsRecursive = false - } - if strings.Contains(filePattern, "**/") { - dirName = strings.Split(filePattern, "**/")[0] - watchConfig.Pattern = strings.Split(filePattern, "**/")[1] - watchConfig.IsRecursive = true - } - watchConfig.Dir = dirName - if dirName != "/" { - watchConfig.Dir = strings.TrimRight(dirName, "/") - } - - return watchConfig -} diff --git a/caddy/watch_test.go b/caddy/watch_test.go deleted file mode 100644 index 2d9d34ee8..000000000 --- a/caddy/watch_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package caddy_test - -import ( - "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" - "github.com/dunglas/frankenphp/caddy" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestParseRecursiveDirectoryWithoutPattern(t *testing.T) { - app, err := parseTestConfig(` - frankenphp { - watch /path1 - watch /path2/ - watch /path3/**/ - watch /**/ - watch **/ - } - `) - - assert.NoError(t, err) - assert.Len(t, app.Watch, 5) - assert.Equal(t, "/path1", app.Watch[0].Dir) - assert.Equal(t, "/path2", app.Watch[1].Dir) - assert.Equal(t, "/path3", app.Watch[2].Dir) - assert.Equal(t, "/", app.Watch[3].Dir) - assert.Equal(t, "", app.Watch[4].Dir) - for _, w := range app.Watch { - assert.True(t, w.IsRecursive) - assert.Equal(t, "", w.Pattern) - } -} - -func TestParseRecursiveDirectoryWithPattern(t *testing.T) { - app, err := parseTestConfig(` - frankenphp { - watch /path1/**/*.php - watch /path2/**/filename - } - `) - - assert.NoError(t, err) - assert.Len(t, app.Watch, 2) - assert.Equal(t, "/path1", app.Watch[0].Dir) - assert.Equal(t, "/path2", app.Watch[1].Dir) - assert.True(t, app.Watch[0].IsRecursive) - assert.True(t, app.Watch[1].IsRecursive) - assert.Equal(t, "*.php", app.Watch[0].Pattern) - assert.Equal(t, "filename", app.Watch[1].Pattern) -} - -func TestParseNonRecursiveDirectoryWithPattern(t *testing.T) { - app, err := parseTestConfig(` - frankenphp { - watch /path1/*.php - watch /path2/watch-me.php - } - `) - - assert.NoError(t, err) - assert.Len(t, app.Watch, 2) - assert.Equal(t, "/path1", app.Watch[0].Dir) - assert.Equal(t, "/path2", app.Watch[1].Dir) - assert.False(t, app.Watch[0].IsRecursive) - assert.False(t, app.Watch[1].IsRecursive) - assert.Equal(t, "*.php", app.Watch[0].Pattern) - assert.Equal(t, "watch-me.php", app.Watch[1].Pattern) -} - -func TestFailOnMissingPath(t *testing.T) { - _, err := parseTestConfig(` - frankenphp { - watch - } - `) - - assert.Error(t, err) -} - -func parseTestConfig(config string) (*caddy.FrankenPHPApp, error) { - app := caddy.FrankenPHPApp{} - err := app.UnmarshalCaddyfile(caddyfile.NewTestDispenser(config)) - return &app, err -} diff --git a/frankenphp_test.go b/frankenphp_test.go index 96b56ab7a..36e4f38f1 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -25,7 +25,6 @@ import ( "testing" "github.com/dunglas/frankenphp" - "github.com/dunglas/frankenphp/watcher" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -36,7 +35,7 @@ import ( type testOptions struct { workerScript string - watchOptions []watcher.WithWatchOption + watch []string nbWorkers int env map[string]string nbParrallelRequests int @@ -64,9 +63,7 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), * if opts.workerScript != "" { initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env)) } - if len(opts.watchOptions) != 0 { - initOpts = append(initOpts, frankenphp.WithFileWatcher(opts.watchOptions...)) - } + initOpts = append(initOpts, frankenphp.WithFileWatcher(opts.watch)) initOpts = append(initOpts, opts.initOpts...) err := frankenphp.Init(initOpts...) diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index fa6b4a765..12cdcf3a7 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -1,7 +1,6 @@ package frankenphp_test import ( - "github.com/dunglas/frankenphp/watcher" "github.com/stretchr/testify/assert" "io" "net/http" @@ -27,16 +26,12 @@ func TestWorkersShouldReloadOnMatchingPattern(t *testing.T) { t.Skip("Skipping watcher tests in memory sanitizer mode") return } - watchOptions := []watcher.WithWatchOption{ - watcher.WithWatcherDir("./testdata"), - watcher.WithWatcherPattern("*.txt"), - watcher.WithWatcherRecursion(true), - } + watch := []string{"./testdata/**/*.txt"} runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { requestBodyHasReset := pollForWorkerReset(t, handler, maxTimesToPollForChanges) assert.True(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch: watch}) } func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { @@ -45,16 +40,12 @@ func TestWorkersShouldNotReloadOnExcludingPattern(t *testing.T) { return } - watchOptions := []watcher.WithWatchOption{ - watcher.WithWatcherDir("./testdata"), - watcher.WithWatcherPattern("*.php"), - watcher.WithWatcherRecursion(true), - } + watch := []string{"./testdata/**/*.php"} runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, i int) { requestBodyHasReset := pollForWorkerReset(t, handler, minTimesToPollForChanges) assert.False(t, requestBodyHasReset) - }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watchOptions: watchOptions}) + }, &testOptions{nbParrallelRequests: 1, nbWorkers: 1, workerScript: "worker-with-watcher.php", watch: watch}) } func fetchBody(method string, url string, handler func(http.ResponseWriter, *http.Request)) string { diff --git a/options.go b/options.go index 45d1d375c..652b500f7 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,6 @@ package frankenphp import ( - "github.com/dunglas/frankenphp/watcher" "go.uber.org/zap" ) @@ -14,7 +13,7 @@ type Option func(h *opt) error type opt struct { numThreads int workers []workerOpt - watch []watcher.WatchOpt + watch []string logger *zap.Logger } @@ -42,16 +41,10 @@ func WithWorkers(fileName string, num int, env map[string]string) Option { } } -// WithFileWatcher configures filesystem watchers. -func WithFileWatcher(wo ...watcher.WithWatchOption) Option { +// WithFileWatcher adds directories to be watched (shell file pattern). +func WithFileWatcher(patterns []string) Option { return func(o *opt) error { - watchOpt := watcher.WatchOpt{} - for _, option := range wo { - if err := option(&watchOpt); err != nil { - return err - } - } - o.watch = append(o.watch, watchOpt) + o.watch = patterns return nil } diff --git a/watcher/watcher.c b/watcher/watcher.c index f53de6718..e5f3e0b22 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -7,7 +7,11 @@ void handle_event(struct wtr_watcher_event event, void *data) { } uintptr_t start_new_watcher(char const *const path, uintptr_t data) { - return (uintptr_t)wtr_watcher_open(path, handle_event, (void *)data); + void * watcher = wtr_watcher_open(path, handle_event, (void *)data); + if (watcher == NULL) { + return 0; + } + return (uintptr_t)watcher; } int stop_watcher(uintptr_t watcher) { diff --git a/watcher/watcher.go b/watcher/watcher.go index 895c6ff3f..2908613bb 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -18,7 +18,6 @@ import ( type watcher struct { sessions []C.uintptr_t callback func() - watchOpts []WatchOpt trigger chan struct{} stop chan struct{} } @@ -37,8 +36,8 @@ var ( UnableToStartWatching = errors.New("Unable to start the watcher") ) -func InitWatcher(watchOpts []WatchOpt, callback func(), zapLogger *zap.Logger) error { - if len(watchOpts) == 0 { +func InitWatcher(filePatterns []string, callback func(), zapLogger *zap.Logger) error { + if len(filePatterns) == 0 { return nil } if activeWatcher != nil { @@ -46,7 +45,7 @@ func InitWatcher(watchOpts []WatchOpt, callback func(), zapLogger *zap.Logger) e } logger = zapLogger activeWatcher = &watcher{callback: callback} - err := activeWatcher.startWatching(watchOpts) + err := activeWatcher.startWatching(filePatterns) if err != nil { return err } @@ -65,14 +64,17 @@ func DrainWatcher() { activeWatcher = nil } -func (w *watcher) startWatching(watchOpts []WatchOpt) error { +func (w *watcher) startWatching(filePatterns []string) error { w.trigger = make(chan struct{}) w.stop = make(chan struct{}) - w.sessions = make([]C.uintptr_t, len(watchOpts)) - w.watchOpts = watchOpts - for i, watchOpt := range w.watchOpts { + w.sessions = make([]C.uintptr_t, len(filePatterns)) + watchOpts, err := parseFilePatterns(filePatterns) + if err != nil { + return err + } + for i, watchOpt := range watchOpts { watchOpt.trigger = w.trigger - session, err := startSession(&watchOpt) + session, err := startSession(watchOpt) if err != nil { return err } @@ -89,7 +91,7 @@ func (w *watcher) stopWatching() { } } -func startSession(watchOpt *WatchOpt) (C.uintptr_t, error) { +func startSession(watchOpt *watchOpt) (C.uintptr_t, error) { handle := cgo.NewHandle(watchOpt) cDir := C.CString(watchOpt.dir) defer C.free(unsafe.Pointer(cDir)) @@ -112,7 +114,7 @@ func stopSession(session C.uintptr_t) { //export go_handle_file_watcher_event func go_handle_file_watcher_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) { - watchOpt := cgo.Handle(handle).Value().(*WatchOpt) + watchOpt := cgo.Handle(handle).Value().(*watchOpt) if watchOpt.allowReload(C.GoString(path), int(eventType), int(pathType)) { watchOpt.trigger <- struct{}{} } diff --git a/watcher/watcher_options.go b/watcher/watcher_options.go index ece7ac535..efbcb70a8 100644 --- a/watcher/watcher_options.go +++ b/watcher/watcher_options.go @@ -3,45 +3,61 @@ package watcher import ( "go.uber.org/zap" "path/filepath" + "strings" ) -type WithWatchOption func(o *WatchOpt) error - -type WatchOpt struct { +type watchOpt struct { dir string isRecursive bool pattern string trigger chan struct{} } -func WithWatcherDir(dir string) WithWatchOption { - return func(o *WatchOpt) error { - absDir, err := filepath.Abs(dir) +func parseFilePatterns(filePatterns []string) ([]*watchOpt, error) { + watchOpts := make([]*watchOpt, 0, len(filePatterns)) + for _, filePattern := range filePatterns { + watchOpt, err := parseFilePattern(filePattern) if err != nil { - logger.Error("dir for watching is invalid", zap.String("dir", dir)) - return err + return nil, err } - o.dir = absDir - return nil + watchOpts = append(watchOpts, watchOpt) } + return watchOpts, nil } -func WithWatcherRecursion(withRecursion bool) WithWatchOption { - return func(o *WatchOpt) error { - o.isRecursive = withRecursion - return nil +// TODO: better path validation? +// for the one line short-form in the caddy config, aka: 'watch /path/*pattern' +func parseFilePattern(filePattern string) (*watchOpt, error) { + absPattern, err := filepath.Abs(filePattern) + if err != nil { + return nil, err } -} -func WithWatcherPattern(pattern string) WithWatchOption { - return func(o *WatchOpt) error { - o.pattern = pattern - return nil + var w watchOpt + w.isRecursive = true + dirName := absPattern + splitDirName, baseName := filepath.Split(absPattern) + if strings.Contains(absPattern, "**") { + split := strings.Split(absPattern, "**") + dirName = split[0] + w.pattern = strings.TrimLeft(split[1], "/") + w.isRecursive = true + } else if strings.ContainsAny(baseName, "*.[?\\") { + dirName = splitDirName + w.pattern = baseName + w.isRecursive = false } + + w.dir = dirName + if dirName != "/" { + w.dir = strings.TrimRight(dirName, "/") + } + + return &w, nil } // TODO: support directory patterns -func (watchOpt *WatchOpt) allowReload(fileName string, eventType int, pathType int) bool { +func (watchOpt *watchOpt) allowReload(fileName string, eventType int, pathType int) bool { if !isValidEventType(eventType) || !isValidPathType(pathType) { return false } diff --git a/watcher/watcher_options_test.go b/watcher/watcher_options_test.go index 716170950..fc6685d0e 100644 --- a/watcher/watcher_options_test.go +++ b/watcher/watcher_options_test.go @@ -6,143 +6,168 @@ import ( "testing" ) -func TestWithRecursion(t *testing.T) { - watchOpt := createWithOptions(t, WithWatcherRecursion(true)) +func TestRecursiveDirectoryWithoutPattern(t *testing.T) { + watchOpts, err := parseFilePatterns([]string { + "/path/to/folder1", + "/path/to/folder2/**/", + "/path/to/folder3/**", + "./", + ".", + "", + }) - assert.True(t, watchOpt.isRecursive) -} - -func TestWithWatcherPattern(t *testing.T) { - watchOpt := createWithOptions(t, WithWatcherPattern("*php")) - - assert.Equal(t, "*php", watchOpt.pattern) + assert.NoError(t, err) + assert.Len(t, watchOpts, 6) + assert.Equal(t, "/path/to/folder1", watchOpts[0].dir) + assert.Equal(t, "/path/to/folder2", watchOpts[1].dir) + assert.Equal(t, "/path/to/folder3", watchOpts[2].dir) + assert.Equal(t, currentDir(t), watchOpts[3].dir) + assert.Equal(t, currentDir(t), watchOpts[4].dir) + assert.Equal(t, currentDir(t), watchOpts[5].dir) + assertAllRecursive(t, watchOpts, true) + assertAllPattern(t, watchOpts, "") } -func TestWithWatcherDir(t *testing.T) { - watchOpt := createWithOptions(t, WithWatcherDir("/path/to/app")) +func TestRecursiveDirectoryWithPattern(t *testing.T) { + watchOpts, err := parseFilePatterns([]string { + "/path/to/folder1/**/*.php", + "/path/to/folder2/**/.env", + "/path/to/folder3/**/filename", + "**/?.php", + }) - assert.Equal(t, "/path/to/app", watchOpt.dir) + assert.NoError(t, err) + assert.Len(t, watchOpts, 4) + assert.Equal(t, "/path/to/folder1", watchOpts[0].dir) + assert.Equal(t, "/path/to/folder2", watchOpts[1].dir) + assert.Equal(t, "/path/to/folder3", watchOpts[2].dir) + assert.Equal(t, currentDir(t), watchOpts[3].dir) + assert.Equal(t, "*.php", watchOpts[0].pattern) + assert.Equal(t, ".env", watchOpts[1].pattern) + assert.Equal(t, "filename", watchOpts[2].pattern) + assert.Equal(t, "?.php", watchOpts[3].pattern) + assertAllRecursive(t, watchOpts, true) } -func TestWithRelativeWatcherDir(t *testing.T) { - absoluteDir, err := filepath.Abs(".") - - watchOpt := createWithOptions(t, WithWatcherDir(".")) +func TestNonRecursiveDirectoryWithPattern(t *testing.T) { + watchOpts, err := parseFilePatterns([]string { + "/path/to/folder1/*", + "/path/to/folder2/*.php", + "./*.php", + "*.php", + }) assert.NoError(t, err) - assert.Equal(t, absoluteDir, watchOpt.dir) + assert.Len(t, watchOpts, 4) + assert.Equal(t, "/path/to/folder1", watchOpts[0].dir) + assert.Equal(t, "/path/to/folder2", watchOpts[1].dir) + assert.Equal(t, currentDir(t), watchOpts[2].dir) + assert.Equal(t, currentDir(t), watchOpts[3].dir) + assert.Equal(t, "*", watchOpts[0].pattern) + assert.Equal(t, "*.php", watchOpts[1].pattern) + assert.Equal(t, "*.php", watchOpts[2].pattern) + assert.Equal(t, "*.php", watchOpts[2].pattern) + assertAllRecursive(t, watchOpts, false) } func TestAllowReloadOnMatchingPattern(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createWithOptions( - t, - WithWatcherDir("/some/path"), - WithWatcherPattern("*.php"), - ) + watchOpt, err := parseFilePattern("/some/path/*.php") + + assert.NoError(t, err) assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func TestAllowReloadOnExactMatch(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createWithOptions( - t, - WithWatcherDir("/some/path"), - WithWatcherPattern("watch-me.php"), - ) + watchOpt, err := parseFilePattern("/some/path/watch-me.php") + + assert.NoError(t, err) assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func TestDisallowOnDifferentFilename(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createWithOptions( - t, - WithWatcherDir("/some/path"), - WithWatcherPattern("dont-watch.php"), - ) + watchOpt, err := parseFilePattern("/some/path/dont-watch.php") + + assert.NoError(t, err) assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } -func TestAllowReloadOnRecursiveDirectory(t *testing.T) { +func TestAllowReloadOnRecursivePattern(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createWithOptions( - t, - WithWatcherDir("/some"), - WithWatcherRecursion(true), - WithWatcherPattern("*.php"), - ) + watchOpt, err := parseFilePattern("/some/**/*.php") + + assert.NoError(t, err) assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func TestAllowReloadWithRecursionAndNoPattern(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createWithOptions( - t, - WithWatcherDir("/some"), - WithWatcherRecursion(true), - ) + watchOpt, err := parseFilePattern("/some/") + + assert.NoError(t, err) assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } -func TestDisallowOnDifferentPatterns(t *testing.T) { +func TestDisallowOnDifferentRecursivePattern(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createWithOptions( - t, - WithWatcherDir("/some"), - WithWatcherRecursion(true), - WithWatcherPattern("*.txt"), - ) + watchOpt, err := parseFilePattern("/some/**/*.html") + + assert.NoError(t, err) assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } func TestDisallowOnMissingRecursion(t *testing.T) { const fileName = "/some/path/watch-me.php" - watchOpt := createWithOptions( - t, - WithWatcherDir("/some"), - WithWatcherRecursion(false), - WithWatcherPattern("*.php"), - ) + watchOpt, err := parseFilePattern("/some/*.php") + + assert.NoError(t, err) assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } func TestDisallowOnEventTypeBiggerThan3(t *testing.T) { const fileName = "/some/path/watch-me.php" const eventType = 4 - watchOpt := createWithOptions( - t, - WithWatcherDir("/some/path"), - WithWatcherPattern("watch-me.php"), - ) + watchOpt, err := parseFilePattern("/some/path") + + assert.NoError(t, err) assert.False(t, watchOpt.allowReload(fileName, eventType, 0)) } func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { const fileName = "/some/path/watch-me.php" const pathType = 3 - watchOpt := createWithOptions( - t, - WithWatcherDir("/some/path"), - WithWatcherPattern("watch-me.php"), - ) + watchOpt, err := parseFilePattern("/some/path") + + assert.NoError(t, err) assert.False(t, watchOpt.allowReload(fileName, 0, pathType)) } -func createWithOptions(t *testing.T, applyOptions ...WithWatchOption) WatchOpt { - watchOpt := WatchOpt{} +func currentDir(t *testing.T) string { + dir, err := filepath.Abs(".") + assert.NoError(t, err) + return dir +} - for _, applyOption := range applyOptions { - err := applyOption(&watchOpt) - assert.NoError(t, err) +func assertAllRecursive(t *testing.T, watchOpts []*watchOpt, isRecursive bool) { + for _, w := range watchOpts { + assert.Equal(t, isRecursive, w.isRecursive) } - return watchOpt } + +func assertAllPattern(t *testing.T, watchOpts []*watchOpt, pattern string) { + for _, w := range watchOpts { + assert.Equal(t, pattern, w.pattern) + } +} + From 3c7c7ccc2b6c3e27ebd29ae0bc3eb2ba5aed615e Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 12:37:49 +0200 Subject: [PATCH 136/155] Adds support for directory patterns and multiple '**' globs. --- watcher/watcher.go | 2 +- watcher/watcher_options.go | 121 ++++++++++++------ watcher/watcher_options_test.go | 217 +++++++++++++------------------- 3 files changed, 172 insertions(+), 168 deletions(-) diff --git a/watcher/watcher.go b/watcher/watcher.go index 2908613bb..638b61e54 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -97,7 +97,7 @@ func startSession(watchOpt *watchOpt) (C.uintptr_t, error) { defer C.free(unsafe.Pointer(cDir)) watchSession := C.start_new_watcher(cDir, C.uintptr_t(handle)) if watchSession != 0 { - logger.Debug("watching", zap.String("dir", watchOpt.dir), zap.String("pattern", watchOpt.pattern)) + logger.Debug("watching", zap.String("dir", watchOpt.dir), zap.Strings("patterns", watchOpt.patterns)) return watchSession, nil } logger.Error("couldn't start watching", zap.String("dir", watchOpt.dir)) diff --git a/watcher/watcher_options.go b/watcher/watcher_options.go index efbcb70a8..e84a660df 100644 --- a/watcher/watcher_options.go +++ b/watcher/watcher_options.go @@ -9,7 +9,7 @@ import ( type watchOpt struct { dir string isRecursive bool - pattern string + patterns []string trigger chan struct{} } @@ -25,46 +25,50 @@ func parseFilePatterns(filePatterns []string) ([]*watchOpt, error) { return watchOpts, nil } -// TODO: better path validation? -// for the one line short-form in the caddy config, aka: 'watch /path/*pattern' +// this method prepares the watchOpt struct for a single file pattern (aka /path/*pattern) +// TODO: using '/' is more efficient than filepath functions, but does not work on windows +// if windows is ever supported this needs to be adjusted func parseFilePattern(filePattern string) (*watchOpt, error) { absPattern, err := filepath.Abs(filePattern) if err != nil { return nil, err } - var w watchOpt - w.isRecursive = true - dirName := absPattern - splitDirName, baseName := filepath.Split(absPattern) - if strings.Contains(absPattern, "**") { - split := strings.Split(absPattern, "**") - dirName = split[0] - w.pattern = strings.TrimLeft(split[1], "/") - w.isRecursive = true - } else if strings.ContainsAny(baseName, "*.[?\\") { - dirName = splitDirName - w.pattern = baseName - w.isRecursive = false + w := &watchOpt{isRecursive:true, dir:absPattern} + + // first we try to split the pattern to determine + // where the directory ends and the pattern starts + splitPattern := strings.Split(absPattern, "/") + patternWithoutDir := "" + for i, part := range splitPattern { + // we found a pattern if it contains a glob character [*? + // if it contains a '.' it is also likely a filename + // TODO: directories with a '.' in the name are currently not allowed + if strings.ContainsAny(part, "[*?.") { + patternWithoutDir = filepath.Join(splitPattern[i:]...) + w.dir = filepath.Join(splitPattern[:i]...) + break + } } - w.dir = dirName - if dirName != "/" { - w.dir = strings.TrimRight(dirName, "/") + // now we split the pattern into multiple patterns according to the supported '**' syntax + w.patterns = strings.Split(patternWithoutDir, "**") + for i, pattern := range w.patterns { + w.patterns[i] = strings.Trim(pattern, "/") } - return &w, nil + // remove trailing slash and add leading slash + w.dir = "/" + strings.Trim(w.dir, "/") + + return w, nil } -// TODO: support directory patterns func (watchOpt *watchOpt) allowReload(fileName string, eventType int, pathType int) bool { if !isValidEventType(eventType) || !isValidPathType(pathType) { return false } - if watchOpt.isRecursive { - return isValidRecursivePattern(fileName, watchOpt.pattern) - } - return isValidNonRecursivePattern(fileName, watchOpt.pattern, watchOpt.dir) + + return isValidPattern(fileName, watchOpt.dir, watchOpt.patterns) } // 0:rename,1:modify,2:create,3:destroy,4:owner,5:other, @@ -77,25 +81,64 @@ func isValidPathType(eventType int) bool { return eventType <= 2 } -func isValidRecursivePattern(fileName string, pattern string) bool { - if pattern == "" { - return true - } - baseName := filepath.Base(fileName) - patternMatches, err := filepath.Match(pattern, baseName) - if err != nil { - logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) +func isValidPattern(fileName string, dir string, patterns []string) bool { + + // first we remove the dir from the pattern + if !strings.HasPrefix(fileName, dir) { return false } + fileNameWithoutDir := strings.TrimLeft(fileName, dir + "/") + + // if the pattern has size 1 we can match it directly against the filename + if len(patterns) == 1 { + return matchPattern(patterns[0], fileNameWithoutDir) + } - return patternMatches + return matchPatterns(patterns, fileNameWithoutDir) } -func isValidNonRecursivePattern(fileName string, pattern string, dir string) bool { - fileNameDir := filepath.Dir(fileName) - if dir == fileNameDir { - return isValidRecursivePattern(fileName, pattern) +// TODO: does this need performance optimization? +func matchPatterns(patterns []string, fileName string) bool { + partsToMatch := strings.Split(fileName, "/") + cursor := 0 + + // if there are multiple patterns due to '**' we need to match them individually + for i, pattern := range patterns { + patternSize := strings.Count(pattern, "/") + 1 + + // if we are at the last pattern we will start matching from the end of the filename + if(i == len(patterns) - 1) { + cursor = len(partsToMatch) - patternSize + } + + // the cursor will move through the fileName until the pattern matches + for j := cursor; j < len(partsToMatch); j++ { + cursor = j + subPattern := strings.Join(partsToMatch[j:j + patternSize], "/") + if matchPattern(pattern, subPattern) { + cursor = j + patternSize - 1 + break + } + if cursor > len(partsToMatch) - patternSize - 1 { + + return false + } + } + } + + return true +} + +func matchPattern(pattern string, fileName string) bool { + if pattern == "" { + return true } + patternMatches, err := filepath.Match(pattern, fileName) + if err != nil { + logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) + return false + } - return false + return patternMatches } + diff --git a/watcher/watcher_options_test.go b/watcher/watcher_options_test.go index fc6685d0e..313749b73 100644 --- a/watcher/watcher_options_test.go +++ b/watcher/watcher_options_test.go @@ -6,168 +6,129 @@ import ( "testing" ) -func TestRecursiveDirectoryWithoutPattern(t *testing.T) { - watchOpts, err := parseFilePatterns([]string { - "/path/to/folder1", - "/path/to/folder2/**/", - "/path/to/folder3/**", - "./", - ".", - "", - }) - - assert.NoError(t, err) - assert.Len(t, watchOpts, 6) - assert.Equal(t, "/path/to/folder1", watchOpts[0].dir) - assert.Equal(t, "/path/to/folder2", watchOpts[1].dir) - assert.Equal(t, "/path/to/folder3", watchOpts[2].dir) - assert.Equal(t, currentDir(t), watchOpts[3].dir) - assert.Equal(t, currentDir(t), watchOpts[4].dir) - assert.Equal(t, currentDir(t), watchOpts[5].dir) - assertAllRecursive(t, watchOpts, true) - assertAllPattern(t, watchOpts, "") -} - -func TestRecursiveDirectoryWithPattern(t *testing.T) { - watchOpts, err := parseFilePatterns([]string { - "/path/to/folder1/**/*.php", - "/path/to/folder2/**/.env", - "/path/to/folder3/**/filename", - "**/?.php", - }) - - assert.NoError(t, err) - assert.Len(t, watchOpts, 4) - assert.Equal(t, "/path/to/folder1", watchOpts[0].dir) - assert.Equal(t, "/path/to/folder2", watchOpts[1].dir) - assert.Equal(t, "/path/to/folder3", watchOpts[2].dir) - assert.Equal(t, currentDir(t), watchOpts[3].dir) - assert.Equal(t, "*.php", watchOpts[0].pattern) - assert.Equal(t, ".env", watchOpts[1].pattern) - assert.Equal(t, "filename", watchOpts[2].pattern) - assert.Equal(t, "?.php", watchOpts[3].pattern) - assertAllRecursive(t, watchOpts, true) -} - -func TestNonRecursiveDirectoryWithPattern(t *testing.T) { - watchOpts, err := parseFilePatterns([]string { - "/path/to/folder1/*", - "/path/to/folder2/*.php", - "./*.php", - "*.php", - }) - - assert.NoError(t, err) - assert.Len(t, watchOpts, 4) - assert.Equal(t, "/path/to/folder1", watchOpts[0].dir) - assert.Equal(t, "/path/to/folder2", watchOpts[1].dir) - assert.Equal(t, currentDir(t), watchOpts[2].dir) - assert.Equal(t, currentDir(t), watchOpts[3].dir) - assert.Equal(t, "*", watchOpts[0].pattern) - assert.Equal(t, "*.php", watchOpts[1].pattern) - assert.Equal(t, "*.php", watchOpts[2].pattern) - assert.Equal(t, "*.php", watchOpts[2].pattern) - assertAllRecursive(t, watchOpts, false) -} - -func TestAllowReloadOnMatchingPattern(t *testing.T) { +func TestDisallowOnEventTypeBiggerThan3(t *testing.T) { const fileName = "/some/path/watch-me.php" + const eventType = 4 - watchOpt, err := parseFilePattern("/some/path/*.php") + watchOpt, err := parseFilePattern("/some/path") assert.NoError(t, err) - assert.True(t, watchOpt.allowReload(fileName, 0, 0)) + assert.False(t, watchOpt.allowReload(fileName, eventType, 0)) } -func TestAllowReloadOnExactMatch(t *testing.T) { +func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { const fileName = "/some/path/watch-me.php" + const pathType = 3 - watchOpt, err := parseFilePattern("/some/path/watch-me.php") + watchOpt, err := parseFilePattern("/some/path") assert.NoError(t, err) - assert.True(t, watchOpt.allowReload(fileName, 0, 0)) + assert.False(t, watchOpt.allowReload(fileName, 0, pathType)) } -func TestDisallowOnDifferentFilename(t *testing.T) { - const fileName = "/some/path/watch-me.php" - - watchOpt, err := parseFilePattern("/some/path/dont-watch.php") - - assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, 0, 0)) +func TestValidRecursiveDirectories(t *testing.T) { + shouldMatch(t, "/path", "/path/file.php") + shouldMatch(t, "/path", "/path/subpath/file.php") + shouldMatch(t, "/path/", "/path/subpath/file.php") + shouldMatch(t, "/path**", "/path/subpath/file.php") + shouldMatch(t, "/path/**", "/path/subpath/file.php") + shouldMatch(t, "/path/**/", "/path/subpath/file.php") + shouldMatch(t, ".", relativeDir(t, "/file.php")) + shouldMatch(t, ".", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "./**", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "..", relativeDir(t, "/subpath/file.php")) } -func TestAllowReloadOnRecursivePattern(t *testing.T) { - const fileName = "/some/path/watch-me.php" - - watchOpt, err := parseFilePattern("/some/**/*.php") - - assert.NoError(t, err) - assert.True(t, watchOpt.allowReload(fileName, 0, 0)) +func TestInvalidRecursiveDirectories(t *testing.T) { + shouldNotMatch(t, "/path", "/other/file.php") + shouldNotMatch(t, "/path/**", "/other/file.php") + shouldNotMatch(t, ".", relativeDir(t, "/../other/file.php")) } -func TestAllowReloadWithRecursionAndNoPattern(t *testing.T) { - const fileName = "/some/path/watch-me.php" - - watchOpt, err := parseFilePattern("/some/") - - assert.NoError(t, err) - assert.True(t, watchOpt.allowReload(fileName, 0, 0)) +func TestValidNonRecursiveFilePatterns(t *testing.T) { + shouldMatch(t, "/*.php", "/file.php") + shouldMatch(t, "/path/*.php", "/path/file.php") + shouldMatch(t, "/path/?ile.php", "/path/file.php") + shouldMatch(t, "/path/file.php", "/path/file.php") + shouldMatch(t, "*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "./*.php", relativeDir(t, "/file.php")) } -func TestDisallowOnDifferentRecursivePattern(t *testing.T) { - const fileName = "/some/path/watch-me.php" - - watchOpt, err := parseFilePattern("/some/**/*.html") - - assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, 0, 0)) +func TestInValidNonRecursiveFilePatterns(t *testing.T) { + shouldNotMatch(t, "/path/*.txt", "/path/file.php") + shouldNotMatch(t, "/path/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/*.php", "/path/file.php") + shouldNotMatch(t, "*.txt", relativeDir(t, "/file.php")) + shouldNotMatch(t, "*.php", relativeDir(t, "/subpath/file.php")) } -func TestDisallowOnMissingRecursion(t *testing.T) { - const fileName = "/some/path/watch-me.php" - - watchOpt, err := parseFilePattern("/some/*.php") - - assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, 0, 0)) +func TestValidRecursiveFilePatterns(t *testing.T) { + shouldMatch(t, "/path/**/*.php", "/path/file.php") + shouldMatch(t, "/path/**/*.php", "/path/subpath/file.php") + shouldMatch(t, "/path/**/?ile.php", "/path/subpath/file.php") + shouldMatch(t, "/path/**/file.php", "/path/subpath/file.php") + shouldMatch(t, "**/*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "**/*.php", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "./**/*.php", relativeDir(t, "/subpath/file.php")) } -func TestDisallowOnEventTypeBiggerThan3(t *testing.T) { - const fileName = "/some/path/watch-me.php" - const eventType = 4 - - watchOpt, err := parseFilePattern("/some/path") - - assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, eventType, 0)) +func TestInvalidRecursiveFilePatterns(t *testing.T) { + shouldNotMatch(t, "/path/**/*.txt", "/path/file.php") + shouldNotMatch(t, "/other/**/*.txt", "/path/file.php") + shouldNotMatch(t, "/path/**/*.txt", "/path/subpath/file.php") + shouldNotMatch(t, "/path/**/?ilm.php", "/path/subpath/file.php") + shouldNotMatch(t, "**/*.php", "/other/file.php") + shouldNotMatch(t, "**/*.php", "/other/file.php") + shouldNotMatch(t, ".**/*.php", "/other/file.php") + shouldNotMatch(t, "./**/*.php", "/other/file.php") } -func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { - const fileName = "/some/path/watch-me.php" - const pathType = 3 - - watchOpt, err := parseFilePattern("/some/path") +func TestValidDirectoryPatterns(t *testing.T) { + shouldMatch(t, "/path/*/*.php", "/path/subpath/file.php") + shouldMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/file.php") + shouldMatch(t, "/path/?/*.php", "/path/1/file.php") + shouldMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/file.php") + shouldMatch(t, "/path/**/vendor/**/*.php", "/path/vendor/file.php") + shouldMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php") + shouldMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/subpath/vendor/subpath/file.php") + shouldMatch(t, "/path*/path*/*", "/path1/path2/file.php") +} - assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, 0, pathType)) +func TestInvalidDirectoryPatterns(t *testing.T) { + shouldNotMatch(t, "/path/subpath/*.php", "/path/other/file.php") + shouldNotMatch(t, "/path/*/*.php", "/path/subpath/subpath/file.php") + shouldNotMatch(t, "/path/?/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/vendor/subpath/subpath/file.php") + shouldNotMatch(t, "/path*/path*", "/path1/path1/file.php") } -func currentDir(t *testing.T) string { - dir, err := filepath.Abs(".") +func relativeDir(t *testing.T, relativePath string) string { + dir, err := filepath.Abs("." + relativePath) assert.NoError(t, err) return dir } -func assertAllRecursive(t *testing.T, watchOpts []*watchOpt, isRecursive bool) { +func assertAllPattern(t *testing.T, watchOpts []*watchOpt, pattern string) { for _, w := range watchOpts { - assert.Equal(t, isRecursive, w.isRecursive) + assert.Equal(t, pattern, w.patterns[0]) } } -func assertAllPattern(t *testing.T, watchOpts []*watchOpt, pattern string) { - for _, w := range watchOpts { - assert.Equal(t, pattern, w.pattern) - } +func shouldMatch(t *testing.T, pattern string, fileName string) { + watchOpt, err := parseFilePattern(pattern) + assert.NoError(t, err) + assert.True(t, watchOpt.allowReload(fileName, 0, 0)) +} + +func shouldNotMatch(t *testing.T, pattern string, fileName string) { + watchOpt, err := parseFilePattern(pattern) + assert.NoError(t, err) + assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } From 5af705f581ff3479d0bc5c005b71634d45014b0a Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 12:38:01 +0200 Subject: [PATCH 137/155] Adjusts label. --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index ab6aef5e6..79b5b00de 100644 --- a/docs/config.md +++ b/docs/config.md @@ -163,7 +163,7 @@ You can also add multiple `watch` directives and use simple pattern matching for #### Advanced Watchers Configuration -* Directories can also be relative (to where the frankenphp process is started from) +* Directories can also be relative (to where the FrankenPHP process is started from) * The `**/` pattern signifies recursive watching and may be followed by a filename pattern * If the last part of the `watch` directive contains any of the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the shell [filename pattern](https://pkg.go.dev/path/filepath#Match) From 9fbeeaffe1c3bd48fcad213d33fa5a21b82dd310 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 12:39:34 +0200 Subject: [PATCH 138/155] go fmt. --- frankenphp_with_watcher_test.go | 2 +- watcher/watcher.go | 16 ++++++------ watcher/watcher_options.go | 43 ++++++++++++++++----------------- watcher/watcher_options_test.go | 5 ++-- worker.go | 10 ++++---- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/frankenphp_with_watcher_test.go b/frankenphp_with_watcher_test.go index 12cdcf3a7..0829e22e3 100644 --- a/frankenphp_with_watcher_test.go +++ b/frankenphp_with_watcher_test.go @@ -7,9 +7,9 @@ import ( "net/http/httptest" "os" "path/filepath" + "strings" "testing" "time" - "strings" ) // we have to wait a few milliseconds for the watcher debounce to take effect diff --git a/watcher/watcher.go b/watcher/watcher.go index 638b61e54..33366f938 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -16,10 +16,10 @@ import ( ) type watcher struct { - sessions []C.uintptr_t - callback func() - trigger chan struct{} - stop chan struct{} + sessions []C.uintptr_t + callback func() + trigger chan struct{} + stop chan struct{} } // duration to wait before triggering a reload after a file change @@ -31,9 +31,9 @@ var ( // after stopping the watcher we will wait for eventual reloads to finish reloadWaitGroup sync.WaitGroup // we are passing the logger from the main package to the watcher - logger *zap.Logger - AlreadyStartedError = errors.New("The watcher is already running") - UnableToStartWatching = errors.New("Unable to start the watcher") + logger *zap.Logger + AlreadyStartedError = errors.New("The watcher is already running") + UnableToStartWatching = errors.New("Unable to start the watcher") ) func InitWatcher(filePatterns []string, callback func(), zapLogger *zap.Logger) error { @@ -68,7 +68,7 @@ func (w *watcher) startWatching(filePatterns []string) error { w.trigger = make(chan struct{}) w.stop = make(chan struct{}) w.sessions = make([]C.uintptr_t, len(filePatterns)) - watchOpts, err := parseFilePatterns(filePatterns) + watchOpts, err := parseFilePatterns(filePatterns) if err != nil { return err } diff --git a/watcher/watcher_options.go b/watcher/watcher_options.go index e84a660df..09cb401e6 100644 --- a/watcher/watcher_options.go +++ b/watcher/watcher_options.go @@ -34,7 +34,7 @@ func parseFilePattern(filePattern string) (*watchOpt, error) { return nil, err } - w := &watchOpt{isRecursive:true, dir:absPattern} + w := &watchOpt{isRecursive: true, dir: absPattern} // first we try to split the pattern to determine // where the directory ends and the pattern starts @@ -87,7 +87,7 @@ func isValidPattern(fileName string, dir string, patterns []string) bool { if !strings.HasPrefix(fileName, dir) { return false } - fileNameWithoutDir := strings.TrimLeft(fileName, dir + "/") + fileNameWithoutDir := strings.TrimLeft(fileName, dir+"/") // if the pattern has size 1 we can match it directly against the filename if len(patterns) == 1 { @@ -107,24 +107,24 @@ func matchPatterns(patterns []string, fileName string) bool { patternSize := strings.Count(pattern, "/") + 1 // if we are at the last pattern we will start matching from the end of the filename - if(i == len(patterns) - 1) { + if i == len(patterns)-1 { cursor = len(partsToMatch) - patternSize } // the cursor will move through the fileName until the pattern matches - for j := cursor; j < len(partsToMatch); j++ { - cursor = j - subPattern := strings.Join(partsToMatch[j:j + patternSize], "/") - if matchPattern(pattern, subPattern) { - cursor = j + patternSize - 1 - break - } - if cursor > len(partsToMatch) - patternSize - 1 { - - return false - } - } - } + for j := cursor; j < len(partsToMatch); j++ { + cursor = j + subPattern := strings.Join(partsToMatch[j:j+patternSize], "/") + if matchPattern(pattern, subPattern) { + cursor = j + patternSize - 1 + break + } + if cursor > len(partsToMatch)-patternSize-1 { + + return false + } + } + } return true } @@ -134,11 +134,10 @@ func matchPattern(pattern string, fileName string) bool { return true } patternMatches, err := filepath.Match(pattern, fileName) - if err != nil { - logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) - return false - } + if err != nil { + logger.Error("failed to match filename", zap.String("file", fileName), zap.Error(err)) + return false + } - return patternMatches + return patternMatches } - diff --git a/watcher/watcher_options_test.go b/watcher/watcher_options_test.go index 313749b73..8c6bb55fb 100644 --- a/watcher/watcher_options_test.go +++ b/watcher/watcher_options_test.go @@ -122,13 +122,12 @@ func assertAllPattern(t *testing.T, watchOpts []*watchOpt, pattern string) { func shouldMatch(t *testing.T, pattern string, fileName string) { watchOpt, err := parseFilePattern(pattern) - assert.NoError(t, err) + assert.NoError(t, err) assert.True(t, watchOpt.allowReload(fileName, 0, 0)) } func shouldNotMatch(t *testing.T, pattern string, fileName string) { watchOpt, err := parseFilePattern(pattern) - assert.NoError(t, err) + assert.NoError(t, err) assert.False(t, watchOpt.allowReload(fileName, 0, 0)) } - diff --git a/worker.go b/worker.go index 56a0428e8..96524e549 100644 --- a/worker.go +++ b/worker.go @@ -22,7 +22,7 @@ var ( workersReadyWG sync.WaitGroup workerShutdownWG sync.WaitGroup workersAreReady atomic.Bool - workersAreDone atomic.Bool + workersAreDone atomic.Bool workersDone chan interface{} ) @@ -30,7 +30,7 @@ var ( func initWorkers(opt []workerOpt) error { workersDone = make(chan interface{}) workersAreReady.Store(false) - workersAreDone.Store(false) + workersAreDone.Store(false) for _, w := range opt { if err := startWorkers(w.fileName, w.num, w.env); err != nil { @@ -48,8 +48,8 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { } if _, ok := workersRequestChans.Load(absFileName); !ok { - workersRequestChans.Store(absFileName, make(chan *http.Request)) - } + workersRequestChans.Store(absFileName, make(chan *http.Request)) + } shutdownWG.Add(nbWorkers) workerShutdownWG.Add(nbWorkers) @@ -152,7 +152,7 @@ func drainWorkers() { func restartWorkers(workerOpts []workerOpt) { stopWorkers() - workerShutdownWG.Wait() + workerShutdownWG.Wait() if err := initWorkers(workerOpts); err != nil { logger.Error("failed to restart workers when watching files") panic(err) From bfc07f22ae0760454386382c80dd8b75150b829b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 12:49:05 +0200 Subject: [PATCH 139/155] go mod tidy. --- go.sum | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/go.sum b/go.sum index 938803948..c15d88ef2 100644 --- a/go.sum +++ b/go.sum @@ -2,23 +2,20 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ= github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4= github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0= github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/maypok86/otter v1.2.2 h1:jJi0y8ruR/ZcKmJ4FbQj3QQTqKwV+LNrSOo2S1zbF5M= github.com/maypok86/otter v1.2.2/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= @@ -29,7 +26,6 @@ github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSz github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= From b399a3fb5826ee5c10b3d2a67eb50098afee2e78 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 12:51:33 +0200 Subject: [PATCH 140/155] Fixes merge conflict. --- worker.go | 1 + 1 file changed, 1 insertion(+) diff --git a/worker.go b/worker.go index a2f2198a9..d8de51687 100644 --- a/worker.go +++ b/worker.go @@ -132,6 +132,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { } workersReadyWG.Wait() + workersAreReady.Store(true) m.Lock() defer m.Unlock() From e1545317df97ee5350762a7fd120ba09072581b7 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 13:18:12 +0200 Subject: [PATCH 141/155] Refactoring and formatting. --- .../{watcher_options.go => watch_patterns.go} | 22 ++++++++--------- ...options_test.go => watch_patterns_test.go} | 20 ++++++++-------- watcher/watcher.c | 4 ++-- watcher/watcher.go | 24 +++++++++---------- 4 files changed, 35 insertions(+), 35 deletions(-) rename watcher/{watcher_options.go => watch_patterns.go} (83%) rename watcher/{watcher_options_test.go => watch_patterns_test.go} (89%) diff --git a/watcher/watcher_options.go b/watcher/watch_patterns.go similarity index 83% rename from watcher/watcher_options.go rename to watcher/watch_patterns.go index 09cb401e6..1c0547350 100644 --- a/watcher/watcher_options.go +++ b/watcher/watch_patterns.go @@ -6,35 +6,35 @@ import ( "strings" ) -type watchOpt struct { +type watchPattern struct { dir string isRecursive bool patterns []string trigger chan struct{} } -func parseFilePatterns(filePatterns []string) ([]*watchOpt, error) { - watchOpts := make([]*watchOpt, 0, len(filePatterns)) +func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) { + watchPatterns := make([]*watchPattern, 0, len(filePatterns)) for _, filePattern := range filePatterns { - watchOpt, err := parseFilePattern(filePattern) + watchPattern, err := parseFilePattern(filePattern) if err != nil { return nil, err } - watchOpts = append(watchOpts, watchOpt) + watchPatterns = append(watchPatterns, watchPattern) } - return watchOpts, nil + return watchPatterns, nil } -// this method prepares the watchOpt struct for a single file pattern (aka /path/*pattern) +// this method prepares the watchPattern struct for a single file pattern (aka /path/*pattern) // TODO: using '/' is more efficient than filepath functions, but does not work on windows // if windows is ever supported this needs to be adjusted -func parseFilePattern(filePattern string) (*watchOpt, error) { +func parseFilePattern(filePattern string) (*watchPattern, error) { absPattern, err := filepath.Abs(filePattern) if err != nil { return nil, err } - w := &watchOpt{isRecursive: true, dir: absPattern} + w := &watchPattern{isRecursive: true, dir: absPattern} // first we try to split the pattern to determine // where the directory ends and the pattern starts @@ -63,12 +63,12 @@ func parseFilePattern(filePattern string) (*watchOpt, error) { return w, nil } -func (watchOpt *watchOpt) allowReload(fileName string, eventType int, pathType int) bool { +func (watchPattern *watchPattern) allowReload(fileName string, eventType int, pathType int) bool { if !isValidEventType(eventType) || !isValidPathType(pathType) { return false } - return isValidPattern(fileName, watchOpt.dir, watchOpt.patterns) + return isValidPattern(fileName, watchPattern.dir, watchPattern.patterns) } // 0:rename,1:modify,2:create,3:destroy,4:owner,5:other, diff --git a/watcher/watcher_options_test.go b/watcher/watch_patterns_test.go similarity index 89% rename from watcher/watcher_options_test.go rename to watcher/watch_patterns_test.go index 8c6bb55fb..e6b2d2583 100644 --- a/watcher/watcher_options_test.go +++ b/watcher/watch_patterns_test.go @@ -10,20 +10,20 @@ func TestDisallowOnEventTypeBiggerThan3(t *testing.T) { const fileName = "/some/path/watch-me.php" const eventType = 4 - watchOpt, err := parseFilePattern("/some/path") + watchPattern, err := parseFilePattern("/some/path") assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, eventType, 0)) + assert.False(t, watchPattern.allowReload(fileName, eventType, 0)) } func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { const fileName = "/some/path/watch-me.php" const pathType = 3 - watchOpt, err := parseFilePattern("/some/path") + watchPattern, err := parseFilePattern("/some/path") assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, 0, pathType)) + assert.False(t, watchPattern.allowReload(fileName, 0, pathType)) } func TestValidRecursiveDirectories(t *testing.T) { @@ -114,20 +114,20 @@ func relativeDir(t *testing.T, relativePath string) string { return dir } -func assertAllPattern(t *testing.T, watchOpts []*watchOpt, pattern string) { - for _, w := range watchOpts { +func assertAllPattern(t *testing.T, watchPatterns []*watchPattern, pattern string) { + for _, w := range watchPatterns { assert.Equal(t, pattern, w.patterns[0]) } } func shouldMatch(t *testing.T, pattern string, fileName string) { - watchOpt, err := parseFilePattern(pattern) + watchPattern, err := parseFilePattern(pattern) assert.NoError(t, err) - assert.True(t, watchOpt.allowReload(fileName, 0, 0)) + assert.True(t, watchPattern.allowReload(fileName, 0, 0)) } func shouldNotMatch(t *testing.T, pattern string, fileName string) { - watchOpt, err := parseFilePattern(pattern) + watchPattern, err := parseFilePattern(pattern) assert.NoError(t, err) - assert.False(t, watchOpt.allowReload(fileName, 0, 0)) + assert.False(t, watchPattern.allowReload(fileName, 0, 0)) } diff --git a/watcher/watcher.c b/watcher/watcher.c index e5f3e0b22..3d2156121 100644 --- a/watcher/watcher.c +++ b/watcher/watcher.c @@ -7,9 +7,9 @@ void handle_event(struct wtr_watcher_event event, void *data) { } uintptr_t start_new_watcher(char const *const path, uintptr_t data) { - void * watcher = wtr_watcher_open(path, handle_event, (void *)data); + void *watcher = wtr_watcher_open(path, handle_event, (void *)data); if (watcher == NULL) { - return 0; + return 0; } return (uintptr_t)watcher; } diff --git a/watcher/watcher.go b/watcher/watcher.go index 33366f938..4a6e1d5ec 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -68,13 +68,13 @@ func (w *watcher) startWatching(filePatterns []string) error { w.trigger = make(chan struct{}) w.stop = make(chan struct{}) w.sessions = make([]C.uintptr_t, len(filePatterns)) - watchOpts, err := parseFilePatterns(filePatterns) + watchPatterns, err := parseFilePatterns(filePatterns) if err != nil { return err } - for i, watchOpt := range watchOpts { - watchOpt.trigger = w.trigger - session, err := startSession(watchOpt) + for i, watchPattern := range watchPatterns { + watchPattern.trigger = w.trigger + session, err := startSession(watchPattern) if err != nil { return err } @@ -91,16 +91,16 @@ func (w *watcher) stopWatching() { } } -func startSession(watchOpt *watchOpt) (C.uintptr_t, error) { - handle := cgo.NewHandle(watchOpt) - cDir := C.CString(watchOpt.dir) +func startSession(w *watchPattern) (C.uintptr_t, error) { + handle := cgo.NewHandle(w) + cDir := C.CString(w.dir) defer C.free(unsafe.Pointer(cDir)) watchSession := C.start_new_watcher(cDir, C.uintptr_t(handle)) if watchSession != 0 { - logger.Debug("watching", zap.String("dir", watchOpt.dir), zap.Strings("patterns", watchOpt.patterns)) + logger.Debug("watching", zap.String("dir", w.dir), zap.Strings("patterns", w.patterns)) return watchSession, nil } - logger.Error("couldn't start watching", zap.String("dir", watchOpt.dir)) + logger.Error("couldn't start watching", zap.String("dir", w.dir)) return watchSession, UnableToStartWatching } @@ -114,9 +114,9 @@ func stopSession(session C.uintptr_t) { //export go_handle_file_watcher_event func go_handle_file_watcher_event(path *C.char, eventType C.int, pathType C.int, handle C.uintptr_t) { - watchOpt := cgo.Handle(handle).Value().(*watchOpt) - if watchOpt.allowReload(C.GoString(path), int(eventType), int(pathType)) { - watchOpt.trigger <- struct{}{} + watchPattern := cgo.Handle(handle).Value().(*watchPattern) + if watchPattern.allowReload(C.GoString(path), int(eventType), int(pathType)) { + watchPattern.trigger <- struct{}{} } } From ea15b7eaa1e359a39b06abbed1c397b0e0daf1cc Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 14:01:18 +0200 Subject: [PATCH 142/155] Cleans up unused vars and functions. --- watcher/watch_patterns.go | 3 +-- watcher/watch_patterns_test.go | 6 ------ 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/watcher/watch_patterns.go b/watcher/watch_patterns.go index 1c0547350..6caa1cd2e 100644 --- a/watcher/watch_patterns.go +++ b/watcher/watch_patterns.go @@ -8,7 +8,6 @@ import ( type watchPattern struct { dir string - isRecursive bool patterns []string trigger chan struct{} } @@ -34,7 +33,7 @@ func parseFilePattern(filePattern string) (*watchPattern, error) { return nil, err } - w := &watchPattern{isRecursive: true, dir: absPattern} + w := &watchPattern{dir: absPattern} // first we try to split the pattern to determine // where the directory ends and the pattern starts diff --git a/watcher/watch_patterns_test.go b/watcher/watch_patterns_test.go index e6b2d2583..e1480577e 100644 --- a/watcher/watch_patterns_test.go +++ b/watcher/watch_patterns_test.go @@ -114,12 +114,6 @@ func relativeDir(t *testing.T, relativePath string) string { return dir } -func assertAllPattern(t *testing.T, watchPatterns []*watchPattern, pattern string) { - for _, w := range watchPatterns { - assert.Equal(t, pattern, w.patterns[0]) - } -} - func shouldMatch(t *testing.T, pattern string, fileName string) { watchPattern, err := parseFilePattern(pattern) assert.NoError(t, err) From 3feba9c11e0e3b31c6c276490b2ffbb195cf66f3 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 14:12:33 +0200 Subject: [PATCH 143/155] Allows dirs with a dot. --- watcher/watch_patterns.go | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/watcher/watch_patterns.go b/watcher/watch_patterns.go index 6caa1cd2e..0588f6dca 100644 --- a/watcher/watch_patterns.go +++ b/watcher/watch_patterns.go @@ -7,9 +7,9 @@ import ( ) type watchPattern struct { - dir string - patterns []string - trigger chan struct{} + dir string + patterns []string + trigger chan struct{} } func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) { @@ -26,7 +26,6 @@ func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) { // this method prepares the watchPattern struct for a single file pattern (aka /path/*pattern) // TODO: using '/' is more efficient than filepath functions, but does not work on windows -// if windows is ever supported this needs to be adjusted func parseFilePattern(filePattern string) (*watchPattern, error) { absPattern, err := filepath.Abs(filePattern) if err != nil { @@ -35,22 +34,20 @@ func parseFilePattern(filePattern string) (*watchPattern, error) { w := &watchPattern{dir: absPattern} - // first we try to split the pattern to determine - // where the directory ends and the pattern starts + // first we split the pattern to determine where the directory ends and the pattern starts splitPattern := strings.Split(absPattern, "/") patternWithoutDir := "" for i, part := range splitPattern { - // we found a pattern if it contains a glob character [*? - // if it contains a '.' it is also likely a filename - // TODO: directories with a '.' in the name are currently not allowed - if strings.ContainsAny(part, "[*?.") { + // we split the pattern on a glob character [*? or if it is a filename + isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".") + if isFilename || strings.ContainsAny(part, "[*?") { patternWithoutDir = filepath.Join(splitPattern[i:]...) w.dir = filepath.Join(splitPattern[:i]...) break } } - // now we split the pattern into multiple patterns according to the supported '**' syntax + // now we split the pattern according to the glob '**' syntax w.patterns = strings.Split(patternWithoutDir, "**") for i, pattern := range w.patterns { w.patterns[i] = strings.Trim(pattern, "/") @@ -81,7 +78,6 @@ func isValidPathType(eventType int) bool { } func isValidPattern(fileName string, dir string, patterns []string) bool { - // first we remove the dir from the pattern if !strings.HasPrefix(fileName, dir) { return false @@ -96,7 +92,6 @@ func isValidPattern(fileName string, dir string, patterns []string) bool { return matchPatterns(patterns, fileNameWithoutDir) } -// TODO: does this need performance optimization? func matchPatterns(patterns []string, fileName string) bool { partsToMatch := strings.Split(fileName, "/") cursor := 0 From 26672c31856e5e3c106989794ef1b293baa65e32 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 15:24:41 +0200 Subject: [PATCH 144/155] Makes test nicer. --- watcher/watch_patterns_test.go | 135 +++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 58 deletions(-) diff --git a/watcher/watch_patterns_test.go b/watcher/watch_patterns_test.go index e1480577e..4e5ef3a67 100644 --- a/watcher/watch_patterns_test.go +++ b/watcher/watch_patterns_test.go @@ -26,86 +26,99 @@ func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { assert.False(t, watchPattern.allowReload(fileName, 0, pathType)) } +func TestWatchesCorrectDir(t *testing.T) { + hasDir(t, "/path", "/path") + hasDir(t, "/path/", "/path") + hasDir(t, "/path/**/*.php", "/path") + hasDir(t, "/path/*.php", "/path") + hasDir(t, "/path/*/*.php", "/path") + hasDir(t, "/path/?dir/*.php", "/path") + hasDir(t, ".", relativeDir(t, "")) + hasDir(t, "./", relativeDir(t, "")) + hasDir(t, "./**", relativeDir(t, "")) + hasDir(t, "..", relativeDir(t, "/..")) +} + func TestValidRecursiveDirectories(t *testing.T) { - shouldMatch(t, "/path", "/path/file.php") - shouldMatch(t, "/path", "/path/subpath/file.php") - shouldMatch(t, "/path/", "/path/subpath/file.php") - shouldMatch(t, "/path**", "/path/subpath/file.php") - shouldMatch(t, "/path/**", "/path/subpath/file.php") - shouldMatch(t, "/path/**/", "/path/subpath/file.php") - shouldMatch(t, ".", relativeDir(t, "/file.php")) - shouldMatch(t, ".", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "./**", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "..", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "/path", "/path/file.php") + shouldMatch(t, "/path", "/path/subpath/file.php") + shouldMatch(t, "/path/", "/path/subpath/file.php") + shouldMatch(t, "/path**", "/path/subpath/file.php") + shouldMatch(t, "/path/**", "/path/subpath/file.php") + shouldMatch(t, "/path/**/", "/path/subpath/file.php") + shouldMatch(t, ".", relativeDir(t, "/file.php")) + shouldMatch(t, ".", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "./**", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "..", relativeDir(t, "/subpath/file.php")) } func TestInvalidRecursiveDirectories(t *testing.T) { - shouldNotMatch(t, "/path", "/other/file.php") - shouldNotMatch(t, "/path/**", "/other/file.php") - shouldNotMatch(t, ".", relativeDir(t, "/../other/file.php")) + shouldNotMatch(t, "/path", "/other/file.php") + shouldNotMatch(t, "/path/**", "/other/file.php") + shouldNotMatch(t, ".", "/other/file.php") } func TestValidNonRecursiveFilePatterns(t *testing.T) { - shouldMatch(t, "/*.php", "/file.php") - shouldMatch(t, "/path/*.php", "/path/file.php") - shouldMatch(t, "/path/?ile.php", "/path/file.php") - shouldMatch(t, "/path/file.php", "/path/file.php") - shouldMatch(t, "*.php", relativeDir(t, "/file.php")) - shouldMatch(t, "./*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "/*.php", "/file.php") + shouldMatch(t, "/path/*.php", "/path/file.php") + shouldMatch(t, "/path/?ile.php", "/path/file.php") + shouldMatch(t, "/path/file.php", "/path/file.php") + shouldMatch(t, "*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "./*.php", relativeDir(t, "/file.php")) } func TestInValidNonRecursiveFilePatterns(t *testing.T) { - shouldNotMatch(t, "/path/*.txt", "/path/file.php") - shouldNotMatch(t, "/path/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/*.php", "/path/file.php") - shouldNotMatch(t, "*.txt", relativeDir(t, "/file.php")) - shouldNotMatch(t, "*.php", relativeDir(t, "/subpath/file.php")) + shouldNotMatch(t, "/path/*.txt", "/path/file.php") + shouldNotMatch(t, "/path/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/*.php", "/path/file.php") + shouldNotMatch(t, "*.txt", relativeDir(t, "/file.php")) + shouldNotMatch(t, "*.php", relativeDir(t, "/subpath/file.php")) } func TestValidRecursiveFilePatterns(t *testing.T) { - shouldMatch(t, "/path/**/*.php", "/path/file.php") - shouldMatch(t, "/path/**/*.php", "/path/subpath/file.php") - shouldMatch(t, "/path/**/?ile.php", "/path/subpath/file.php") - shouldMatch(t, "/path/**/file.php", "/path/subpath/file.php") - shouldMatch(t, "**/*.php", relativeDir(t, "/file.php")) - shouldMatch(t, "**/*.php", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "./**/*.php", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "/path/**/*.php", "/path/file.php") + shouldMatch(t, "/path/**/*.php", "/path/subpath/file.php") + shouldMatch(t, "/path/**/?ile.php", "/path/subpath/file.php") + shouldMatch(t, "/path/**/file.php", "/path/subpath/file.php") + shouldMatch(t, "**/*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "**/*.php", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "./**/*.php", relativeDir(t, "/subpath/file.php")) } func TestInvalidRecursiveFilePatterns(t *testing.T) { - shouldNotMatch(t, "/path/**/*.txt", "/path/file.php") - shouldNotMatch(t, "/other/**/*.txt", "/path/file.php") - shouldNotMatch(t, "/path/**/*.txt", "/path/subpath/file.php") - shouldNotMatch(t, "/path/**/?ilm.php", "/path/subpath/file.php") - shouldNotMatch(t, "**/*.php", "/other/file.php") - shouldNotMatch(t, "**/*.php", "/other/file.php") - shouldNotMatch(t, ".**/*.php", "/other/file.php") - shouldNotMatch(t, "./**/*.php", "/other/file.php") + shouldNotMatch(t, "/path/**/*.txt", "/path/file.php") + shouldNotMatch(t, "/other/**/*.txt", "/path/file.php") + shouldNotMatch(t, "/path/**/*.txt", "/path/subpath/file.php") + shouldNotMatch(t, "/path/**/?ilm.php", "/path/subpath/file.php") + shouldNotMatch(t, "**/*.php", "/other/file.php") + shouldNotMatch(t, "**/*.php", "/other/file.php") + shouldNotMatch(t, ".**/*.php", "/other/file.php") + shouldNotMatch(t, "./**/*.php", "/other/file.php") } func TestValidDirectoryPatterns(t *testing.T) { - shouldMatch(t, "/path/*/*.php", "/path/subpath/file.php") - shouldMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/file.php") - shouldMatch(t, "/path/?/*.php", "/path/1/file.php") - shouldMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/file.php") - shouldMatch(t, "/path/**/vendor/**/*.php", "/path/vendor/file.php") - shouldMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php") - shouldMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/subpath/vendor/subpath/file.php") - shouldMatch(t, "/path*/path*/*", "/path1/path2/file.php") + shouldMatch(t, "/path/*/*.php", "/path/subpath/file.php") + shouldMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/file.php") + shouldMatch(t, "/path/?/*.php", "/path/1/file.php") + shouldMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/file.php") + shouldMatch(t, "/path/**/vendor/**/*.php", "/path/vendor/file.php") + shouldMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php") + shouldMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/subpath/vendor/subpath/file.php") + shouldMatch(t, "/path*/path*/*", "/path1/path2/file.php") } func TestInvalidDirectoryPatterns(t *testing.T) { - shouldNotMatch(t, "/path/subpath/*.php", "/path/other/file.php") - shouldNotMatch(t, "/path/*/*.php", "/path/subpath/subpath/file.php") - shouldNotMatch(t, "/path/?/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/vendor/subpath/subpath/file.php") - shouldNotMatch(t, "/path*/path*", "/path1/path1/file.php") + shouldNotMatch(t, "/path/subpath/*.php", "/path/other/file.php") + shouldNotMatch(t, "/path/*/*.php", "/path/subpath/subpath/file.php") + shouldNotMatch(t, "/path/?/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/vendor/subpath/subpath/file.php") + shouldNotMatch(t, "/path*/path*", "/path1/path1/file.php") } func relativeDir(t *testing.T, relativePath string) string { @@ -114,6 +127,12 @@ func relativeDir(t *testing.T, relativePath string) string { return dir } +func hasDir(t *testing.T, pattern string, dir string) { + watchPattern, err := parseFilePattern(pattern) + assert.NoError(t, err) + assert.Equal(t, dir, watchPattern.dir) +} + func shouldMatch(t *testing.T, pattern string, fileName string) { watchPattern, err := parseFilePattern(pattern) assert.NoError(t, err) From 67fcd2853a96f6a45df43a0b02fa1970960a2cec Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Fri, 27 Sep 2024 15:29:03 +0200 Subject: [PATCH 145/155] Add dir tests. --- watcher/watch_patterns_test.go | 136 ++++++++++++++++----------------- 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/watcher/watch_patterns_test.go b/watcher/watch_patterns_test.go index 4e5ef3a67..be04d5796 100644 --- a/watcher/watch_patterns_test.go +++ b/watcher/watch_patterns_test.go @@ -27,98 +27,98 @@ func TestDisallowOnPathTypeBiggerThan2(t *testing.T) { } func TestWatchesCorrectDir(t *testing.T) { - hasDir(t, "/path", "/path") - hasDir(t, "/path/", "/path") - hasDir(t, "/path/**/*.php", "/path") - hasDir(t, "/path/*.php", "/path") - hasDir(t, "/path/*/*.php", "/path") - hasDir(t, "/path/?dir/*.php", "/path") - hasDir(t, ".", relativeDir(t, "")) - hasDir(t, "./", relativeDir(t, "")) - hasDir(t, "./**", relativeDir(t, "")) - hasDir(t, "..", relativeDir(t, "/..")) + hasDir(t, "/path", "/path") + hasDir(t, "/path/", "/path") + hasDir(t, "/path/**/*.php", "/path") + hasDir(t, "/path/*.php", "/path") + hasDir(t, "/path/*/*.php", "/path") + hasDir(t, "/path/?dir/*.php", "/path") + hasDir(t, ".", relativeDir(t, "")) + hasDir(t, "./", relativeDir(t, "")) + hasDir(t, "./**", relativeDir(t, "")) + hasDir(t, "..", relativeDir(t, "/..")) } func TestValidRecursiveDirectories(t *testing.T) { - shouldMatch(t, "/path", "/path/file.php") - shouldMatch(t, "/path", "/path/subpath/file.php") - shouldMatch(t, "/path/", "/path/subpath/file.php") - shouldMatch(t, "/path**", "/path/subpath/file.php") - shouldMatch(t, "/path/**", "/path/subpath/file.php") - shouldMatch(t, "/path/**/", "/path/subpath/file.php") - shouldMatch(t, ".", relativeDir(t, "/file.php")) - shouldMatch(t, ".", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "./**", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "..", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "/path", "/path/file.php") + shouldMatch(t, "/path", "/path/subpath/file.php") + shouldMatch(t, "/path/", "/path/subpath/file.php") + shouldMatch(t, "/path**", "/path/subpath/file.php") + shouldMatch(t, "/path/**", "/path/subpath/file.php") + shouldMatch(t, "/path/**/", "/path/subpath/file.php") + shouldMatch(t, ".", relativeDir(t, "/file.php")) + shouldMatch(t, ".", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "./**", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "..", relativeDir(t, "/subpath/file.php")) } func TestInvalidRecursiveDirectories(t *testing.T) { - shouldNotMatch(t, "/path", "/other/file.php") - shouldNotMatch(t, "/path/**", "/other/file.php") - shouldNotMatch(t, ".", "/other/file.php") + shouldNotMatch(t, "/path", "/other/file.php") + shouldNotMatch(t, "/path/**", "/other/file.php") + shouldNotMatch(t, ".", "/other/file.php") } func TestValidNonRecursiveFilePatterns(t *testing.T) { - shouldMatch(t, "/*.php", "/file.php") - shouldMatch(t, "/path/*.php", "/path/file.php") - shouldMatch(t, "/path/?ile.php", "/path/file.php") - shouldMatch(t, "/path/file.php", "/path/file.php") - shouldMatch(t, "*.php", relativeDir(t, "/file.php")) - shouldMatch(t, "./*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "/*.php", "/file.php") + shouldMatch(t, "/path/*.php", "/path/file.php") + shouldMatch(t, "/path/?ile.php", "/path/file.php") + shouldMatch(t, "/path/file.php", "/path/file.php") + shouldMatch(t, "*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "./*.php", relativeDir(t, "/file.php")) } func TestInValidNonRecursiveFilePatterns(t *testing.T) { - shouldNotMatch(t, "/path/*.txt", "/path/file.php") - shouldNotMatch(t, "/path/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/*.php", "/path/file.php") - shouldNotMatch(t, "*.txt", relativeDir(t, "/file.php")) - shouldNotMatch(t, "*.php", relativeDir(t, "/subpath/file.php")) + shouldNotMatch(t, "/path/*.txt", "/path/file.php") + shouldNotMatch(t, "/path/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/*.php", "/path/file.php") + shouldNotMatch(t, "*.txt", relativeDir(t, "/file.php")) + shouldNotMatch(t, "*.php", relativeDir(t, "/subpath/file.php")) } func TestValidRecursiveFilePatterns(t *testing.T) { - shouldMatch(t, "/path/**/*.php", "/path/file.php") - shouldMatch(t, "/path/**/*.php", "/path/subpath/file.php") - shouldMatch(t, "/path/**/?ile.php", "/path/subpath/file.php") - shouldMatch(t, "/path/**/file.php", "/path/subpath/file.php") - shouldMatch(t, "**/*.php", relativeDir(t, "/file.php")) - shouldMatch(t, "**/*.php", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "./**/*.php", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "/path/**/*.php", "/path/file.php") + shouldMatch(t, "/path/**/*.php", "/path/subpath/file.php") + shouldMatch(t, "/path/**/?ile.php", "/path/subpath/file.php") + shouldMatch(t, "/path/**/file.php", "/path/subpath/file.php") + shouldMatch(t, "**/*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "**/*.php", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "./**/*.php", relativeDir(t, "/subpath/file.php")) } func TestInvalidRecursiveFilePatterns(t *testing.T) { - shouldNotMatch(t, "/path/**/*.txt", "/path/file.php") - shouldNotMatch(t, "/other/**/*.txt", "/path/file.php") - shouldNotMatch(t, "/path/**/*.txt", "/path/subpath/file.php") - shouldNotMatch(t, "/path/**/?ilm.php", "/path/subpath/file.php") - shouldNotMatch(t, "**/*.php", "/other/file.php") - shouldNotMatch(t, "**/*.php", "/other/file.php") - shouldNotMatch(t, ".**/*.php", "/other/file.php") - shouldNotMatch(t, "./**/*.php", "/other/file.php") + shouldNotMatch(t, "/path/**/*.txt", "/path/file.php") + shouldNotMatch(t, "/other/**/*.txt", "/path/file.php") + shouldNotMatch(t, "/path/**/*.txt", "/path/subpath/file.php") + shouldNotMatch(t, "/path/**/?ilm.php", "/path/subpath/file.php") + shouldNotMatch(t, "**/*.php", "/other/file.php") + shouldNotMatch(t, "**/*.php", "/other/file.php") + shouldNotMatch(t, ".**/*.php", "/other/file.php") + shouldNotMatch(t, "./**/*.php", "/other/file.php") } func TestValidDirectoryPatterns(t *testing.T) { - shouldMatch(t, "/path/*/*.php", "/path/subpath/file.php") - shouldMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/file.php") - shouldMatch(t, "/path/?/*.php", "/path/1/file.php") - shouldMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/file.php") - shouldMatch(t, "/path/**/vendor/**/*.php", "/path/vendor/file.php") - shouldMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php") - shouldMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/subpath/vendor/subpath/file.php") - shouldMatch(t, "/path*/path*/*", "/path1/path2/file.php") + shouldMatch(t, "/path/*/*.php", "/path/subpath/file.php") + shouldMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/file.php") + shouldMatch(t, "/path/?/*.php", "/path/1/file.php") + shouldMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/file.php") + shouldMatch(t, "/path/**/vendor/**/*.php", "/path/vendor/file.php") + shouldMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php") + shouldMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/subpath/vendor/subpath/file.php") + shouldMatch(t, "/path*/path*/*", "/path1/path2/file.php") } func TestInvalidDirectoryPatterns(t *testing.T) { - shouldNotMatch(t, "/path/subpath/*.php", "/path/other/file.php") - shouldNotMatch(t, "/path/*/*.php", "/path/subpath/subpath/file.php") - shouldNotMatch(t, "/path/?/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php") - shouldNotMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/vendor/subpath/subpath/file.php") - shouldNotMatch(t, "/path*/path*", "/path1/path1/file.php") + shouldNotMatch(t, "/path/subpath/*.php", "/path/other/file.php") + shouldNotMatch(t, "/path/*/*.php", "/path/subpath/subpath/file.php") + shouldNotMatch(t, "/path/?/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/*/*.php", "/path/subpath/vendor/subpath/subpath/file.php") + shouldNotMatch(t, "/path*/path*", "/path1/path1/file.php") } func relativeDir(t *testing.T, relativePath string) string { From bdd603deb857dc7d9805b0bc8e081fbe5effca3c Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sun, 29 Sep 2024 13:07:54 +0200 Subject: [PATCH 146/155] Moves the watch directive inside the worker directive. --- caddy/caddy.go | 18 ++++---- caddy/caddy_test.go | 14 +++--- caddy/php-server.go | 3 +- docs/config.md | 44 ++++++++----------- frankenphp.go | 8 +--- frankenphp_test.go | 3 +- options.go | 15 ++----- .../{watch_patterns.go => watch_pattern.go} | 17 +++---- ...patterns_test.go => watch_pattern_test.go} | 0 worker.go | 18 ++++++++ worker_test.go | 4 +- 11 files changed, 70 insertions(+), 74 deletions(-) rename watcher/{watch_patterns.go => watch_pattern.go} (89%) rename watcher/{watch_patterns_test.go => watch_pattern_test.go} (100%) diff --git a/caddy/caddy.go b/caddy/caddy.go index 1c64c867e..6e38b208b 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -62,6 +62,8 @@ type workerConfig struct { 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"` + // Directories to watch for file changes + Watch []string `json:"watch,omitempty"` } type FrankenPHPApp struct { @@ -69,8 +71,6 @@ type FrankenPHPApp struct { NumThreads int `json:"num_threads,omitempty"` // Workers configures the worker scripts to start. Workers []workerConfig `json:"workers,omitempty"` - // Directories to watch for changes - Watch []string `json:"watch,omitempty"` } // CaddyModule returns the Caddy module information. @@ -87,11 +87,9 @@ func (f *FrankenPHPApp) Start() error { opts := []frankenphp.Option{frankenphp.WithNumThreads(f.NumThreads), frankenphp.WithLogger(logger), frankenphp.WithMetrics(metrics)} for _, w := range f.Workers { - opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env)) + opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch)) } - opts = append(opts, frankenphp.WithFileWatcher(f.Watch)) - _, loaded, err := phpInterpreter.LoadOrNew(mainPHPInterpreterKey, func() (caddy.Destructor, error) { if err := frankenphp.Init(opts...); err != nil { return nil, err @@ -138,11 +136,6 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { } f.NumThreads = v - case "watch": - if !d.NextArg() { - return d.Err(`The "watch" directive must be followed by a path`) - } - f.Watch = append(f.Watch, d.Val()) case "worker": wc := workerConfig{} if d.NextArg() { @@ -186,6 +179,11 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { wc.Env = make(map[string]string) } wc.Env[args[0]] = args[1] + case "watch": + if !d.NextArg() { + return d.Err(`The "watch" directive must be followed by a path`) + } + wc.Watch = append(wc.Watch, d.Val()) } if wc.FileName == "" { diff --git a/caddy/caddy_test.go b/caddy/caddy_test.go index 37f6f2b65..d16615323 100644 --- a/caddy/caddy_test.go +++ b/caddy/caddy_test.go @@ -545,24 +545,26 @@ func TestAutoWorkerConfig(t *testing.T) { )) } -func TestWorkerWithSleepingWatcher(t *testing.T) { +func TestWorkerWithInactiveWatcher(t *testing.T) { tester := caddytest.NewTester(t) tester.InitServer(` { skip_install_trust admin localhost:2999 http_port 9080 - https_port 9443 frankenphp { - worker ../testdata/worker-with-watcher.php 1 - watch ./**/*.php + worker { + file ../testdata/worker-with-watcher.php + num 1 + watch ./**/*.php + } } } localhost:9080 { - root * ../testdata - rewrite * worker-with-watcher.php + root ../testdata + rewrite worker-with-watcher.php php } `, "caddyfile") diff --git a/caddy/php-server.go b/caddy/php-server.go index 0dbe9ec3c..7f650644c 100644 --- a/caddy/php-server.go +++ b/caddy/php-server.go @@ -95,6 +95,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) { workersOption = append(workersOption, workerConfig{FileName: parts[0], Num: num}) } + workersOption[0].Watch = watch } if frankenphp.EmbeddedAppPath != "" { @@ -310,7 +311,7 @@ func cmdPHPServer(fs caddycmd.Flags) (int, error) { }, AppsRaw: caddy.ModuleMap{ "http": caddyconfig.JSON(httpApp, nil), - "frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption, Watch: watch}, nil), + "frankenphp": caddyconfig.JSON(FrankenPHPApp{Workers: workersOption}, nil), }, } diff --git a/docs/config.md b/docs/config.md index 79b5b00de..732724fef 100644 --- a/docs/config.md +++ b/docs/config.md @@ -51,6 +51,7 @@ Optionally, the number of threads to create and [worker scripts](worker.md) to s 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. + watch # Sets the path to watch for file changes. Can be specified more than once for multiple paths. } } } @@ -133,41 +134,32 @@ php_server [] { ### Watching for File Changes -Since workers won't restart automatically when updating your PHP files, you can also -define a number of directories that should be watched for file changes. -This is useful for development environments. - -```caddyfile -{ - frankenphp { - worker /path/to/app/public/worker.php - watch /path/to/app/**/*.php - } -} -``` +Since workers only boot your application once and keep it in memory, any changes +to your PHP files will not be reflected immediately. -The configuration above will watch the `/path/to/app` directory recursively. -If any `.php` file changes, all workers will be restarted. +Workers can instead be restarted on file changes via the `watch` directive. +This is useful for development environments. -You can also add multiple `watch` directives and use simple pattern matching for files, the following is valid: +One or multiple `watch` directives followed by a +[shell filename pattern](https://pkg.go.dev/path/filepath#Match) can be defined like so: ```caddyfile { - frankenphp { - watch /path/to/folder1 # watches all files in all subdirectories of /path/to/folder1 - watch /path/to/folder2/*.php # watches files ending in .php in the /path/to/folder2 directory - watch /path/to/folder3/**/*.php # watches files ending in .php in the /path/to/folder3 directory and subdirectories - } + frankenphp { + worker { + file /path/to/app/public/worker.php + watch /path/to/app # watches all files in all subdirectories of /path/to/app + watch /path/to/app/*.php # watches files ending in .php in the /path/to/app directory + watch /path/to/app/**/*.php # watches files ending in .php in the /path/to/app directory and subdirectories + } + } } ``` -#### Advanced Watchers Configuration - +* The `**` pattern signifies recursive watching * Directories can also be relative (to where the FrankenPHP process is started from) -* The `**/` pattern signifies recursive watching and may be followed by a filename pattern -* If the last part of the `watch` directive contains any of the characters `*`, `?`, `[`, `\` or `.`, it will be matched against the - shell [filename pattern](https://pkg.go.dev/path/filepath#Match) -* The watcher will not follow symlinks +* If you have multiple workers defined, all of them will be restarted when a file changes +* The watcher will currently not follow symlinks * Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts. The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher). diff --git a/frankenphp.go b/frankenphp.go index 0eec9c745..0f103b9ef 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -42,7 +42,6 @@ import ( "time" "unsafe" - "github.com/dunglas/frankenphp/watcher" "github.com/maypok86/otter" "go.uber.org/zap" "go.uber.org/zap/zapcore" @@ -345,11 +344,7 @@ func Init(options ...Option) error { return err } - restartWorkers := func() { - restartWorkers(opt.workers) - } - - if err := watcher.InitWatcher(opt.watch, restartWorkers, getLogger()); err != nil { + if err := restartWorkersOnFileChanges(opt.workers); err != nil { return err } @@ -367,7 +362,6 @@ func Init(options ...Option) error { // Shutdown stops the workers and the PHP runtime. func Shutdown() { - watcher.DrainWatcher() drainWorkers() drainThreads() metrics.Shutdown() diff --git a/frankenphp_test.go b/frankenphp_test.go index 36e4f38f1..9628fe667 100644 --- a/frankenphp_test.go +++ b/frankenphp_test.go @@ -61,9 +61,8 @@ 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, opts.env)) + initOpts = append(initOpts, frankenphp.WithWorkers(testDataDir+opts.workerScript, opts.nbWorkers, opts.env, opts.watch)) } - initOpts = append(initOpts, frankenphp.WithFileWatcher(opts.watch)) initOpts = append(initOpts, opts.initOpts...) err := frankenphp.Init(initOpts...) diff --git a/options.go b/options.go index 0e1a3e30b..724e75c8b 100644 --- a/options.go +++ b/options.go @@ -13,7 +13,6 @@ type Option func(h *opt) error type opt struct { numThreads int workers []workerOpt - watch []string logger *zap.Logger metrics Metrics } @@ -22,6 +21,7 @@ type workerOpt struct { fileName string num int env PreparedEnv + watch []string } // WithNumThreads configures the number of PHP threads to start. @@ -42,18 +42,9 @@ func WithMetrics(m Metrics) Option { } // WithWorkers configures the PHP workers to start. -func WithWorkers(fileName string, num int, env map[string]string) Option { +func WithWorkers(fileName string, num int, env map[string]string, watch []string) Option { return func(o *opt) error { - o.workers = append(o.workers, workerOpt{fileName, num, PrepareEnv(env)}) - - return nil - } -} - -// WithFileWatcher adds directories to be watched (shell file pattern). -func WithFileWatcher(patterns []string) Option { - return func(o *opt) error { - o.watch = patterns + o.workers = append(o.workers, workerOpt{fileName, num, PrepareEnv(env), watch}) return nil } diff --git a/watcher/watch_patterns.go b/watcher/watch_pattern.go similarity index 89% rename from watcher/watch_patterns.go rename to watcher/watch_pattern.go index 0588f6dca..da3ea4bf1 100644 --- a/watcher/watch_patterns.go +++ b/watcher/watch_pattern.go @@ -27,33 +27,35 @@ func parseFilePatterns(filePatterns []string) ([]*watchPattern, error) { // this method prepares the watchPattern struct for a single file pattern (aka /path/*pattern) // TODO: using '/' is more efficient than filepath functions, but does not work on windows func parseFilePattern(filePattern string) (*watchPattern, error) { + w := &watchPattern{} + + // first we clean the pattern absPattern, err := filepath.Abs(filePattern) if err != nil { return nil, err } + w.dir = absPattern - w := &watchPattern{dir: absPattern} - - // first we split the pattern to determine where the directory ends and the pattern starts + // then we split the pattern to determine where the directory ends and the pattern starts splitPattern := strings.Split(absPattern, "/") patternWithoutDir := "" for i, part := range splitPattern { - // we split the pattern on a glob character [*? or if it is a filename isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".") - if isFilename || strings.ContainsAny(part, "[*?") { + isGlobCharacter := strings.ContainsAny(part, "[*?") + if isFilename || isGlobCharacter { patternWithoutDir = filepath.Join(splitPattern[i:]...) w.dir = filepath.Join(splitPattern[:i]...) break } } - // now we split the pattern according to the glob '**' syntax + // now we split the pattern according to the recursive '**' syntax w.patterns = strings.Split(patternWithoutDir, "**") for i, pattern := range w.patterns { w.patterns[i] = strings.Trim(pattern, "/") } - // remove trailing slash and add leading slash + // finally, we remove the trailing slash and add leading slash w.dir = "/" + strings.Trim(w.dir, "/") return w, nil @@ -114,7 +116,6 @@ func matchPatterns(patterns []string, fileName string) bool { break } if cursor > len(partsToMatch)-patternSize-1 { - return false } } diff --git a/watcher/watch_patterns_test.go b/watcher/watch_pattern_test.go similarity index 100% rename from watcher/watch_patterns_test.go rename to watcher/watch_pattern_test.go diff --git a/worker.go b/worker.go index d8de51687..616f96857 100644 --- a/worker.go +++ b/worker.go @@ -13,6 +13,7 @@ import ( "sync/atomic" "time" + "github.com/dunglas/frankenphp/watcher" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) @@ -149,11 +150,28 @@ func stopWorkers() { } func drainWorkers() { + watcher.DrainWatcher() stopWorkers() workerShutdownWG.Wait() workersRequestChans = sync.Map{} } +func restartWorkersOnFileChanges(workerOpts []workerOpt) error { + directoriesToWatch := []string{} + for _, w := range workerOpts { + directoriesToWatch = append(directoriesToWatch, w.watch...) + } + restartWorkers := func() { + restartWorkers(workerOpts) + } + if err := watcher.InitWatcher(directoriesToWatch, restartWorkers, getLogger()); err != nil { + + return err + } + + return nil +} + func restartWorkers(workerOpts []workerOpt) { stopWorkers() workerShutdownWG.Wait() diff --git a/worker_test.go b/worker_test.go index 27f9bbca4..25d25f993 100644 --- a/worker_test.go +++ b/worker_test.go @@ -117,8 +117,8 @@ func TestWorkerGetOpt(t *testing.T) { func ExampleServeHTTP_workers() { if err := frankenphp.Init( - frankenphp.WithWorkers("worker1.php", 4, map[string]string{"ENV1": "foo"}), - frankenphp.WithWorkers("worker2.php", 2, map[string]string{"ENV2": "bar"}), + frankenphp.WithWorkers("worker1.php", 4, map[string]string{"ENV1": "foo"}, []string{}), + frankenphp.WithWorkers("worker2.php", 2, map[string]string{"ENV2": "bar"}, []string{}), ); err != nil { panic(err) } From f2122fb70e2f2bf3dd8ab50dd8dc8b147312d47b Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sun, 29 Sep 2024 20:10:53 +0200 Subject: [PATCH 147/155] Adds debug log on special events. --- watcher/watch_pattern.go | 9 ++++++--- watcher/watch_pattern_test.go | 29 +++++++++++++++-------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/watcher/watch_pattern.go b/watcher/watch_pattern.go index da3ea4bf1..1d40c95d5 100644 --- a/watcher/watch_pattern.go +++ b/watcher/watch_pattern.go @@ -62,7 +62,7 @@ func parseFilePattern(filePattern string) (*watchPattern, error) { } func (watchPattern *watchPattern) allowReload(fileName string, eventType int, pathType int) bool { - if !isValidEventType(eventType) || !isValidPathType(pathType) { + if !isValidEventType(eventType) || !isValidPathType(pathType, fileName) { return false } @@ -75,8 +75,11 @@ func isValidEventType(eventType int) bool { } // 0:dir,1:file,2:hard_link,3:sym_link,4:watcher,5:other, -func isValidPathType(eventType int) bool { - return eventType <= 2 +func isValidPathType(pathType int, fileName string) bool { + if pathType == 4 { + logger.Debug("special edant/watcher event", zap.String("fileName", fileName)) + } + return pathType <= 2 } func isValidPattern(fileName string, dir string, patterns []string) bool { diff --git a/watcher/watch_pattern_test.go b/watcher/watch_pattern_test.go index be04d5796..3656ddc3a 100644 --- a/watcher/watch_pattern_test.go +++ b/watcher/watch_pattern_test.go @@ -46,10 +46,10 @@ func TestValidRecursiveDirectories(t *testing.T) { shouldMatch(t, "/path**", "/path/subpath/file.php") shouldMatch(t, "/path/**", "/path/subpath/file.php") shouldMatch(t, "/path/**/", "/path/subpath/file.php") - shouldMatch(t, ".", relativeDir(t, "/file.php")) - shouldMatch(t, ".", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "./**", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "..", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, ".", relativeDir(t, "file.php")) + shouldMatch(t, ".", relativeDir(t, "subpath/file.php")) + shouldMatch(t, "./**", relativeDir(t, "subpath/file.php")) + shouldMatch(t, "..", relativeDir(t, "subpath/file.php")) } func TestInvalidRecursiveDirectories(t *testing.T) { @@ -63,16 +63,16 @@ func TestValidNonRecursiveFilePatterns(t *testing.T) { shouldMatch(t, "/path/*.php", "/path/file.php") shouldMatch(t, "/path/?ile.php", "/path/file.php") shouldMatch(t, "/path/file.php", "/path/file.php") - shouldMatch(t, "*.php", relativeDir(t, "/file.php")) - shouldMatch(t, "./*.php", relativeDir(t, "/file.php")) + shouldMatch(t, "*.php", relativeDir(t, "file.php")) + shouldMatch(t, "./*.php", relativeDir(t, "file.php")) } func TestInValidNonRecursiveFilePatterns(t *testing.T) { shouldNotMatch(t, "/path/*.txt", "/path/file.php") shouldNotMatch(t, "/path/*.php", "/path/subpath/file.php") shouldNotMatch(t, "/*.php", "/path/file.php") - shouldNotMatch(t, "*.txt", relativeDir(t, "/file.php")) - shouldNotMatch(t, "*.php", relativeDir(t, "/subpath/file.php")) + shouldNotMatch(t, "*.txt", relativeDir(t, "file.php")) + shouldNotMatch(t, "*.php", relativeDir(t, "subpath/file.php")) } func TestValidRecursiveFilePatterns(t *testing.T) { @@ -80,18 +80,17 @@ func TestValidRecursiveFilePatterns(t *testing.T) { shouldMatch(t, "/path/**/*.php", "/path/subpath/file.php") shouldMatch(t, "/path/**/?ile.php", "/path/subpath/file.php") shouldMatch(t, "/path/**/file.php", "/path/subpath/file.php") - shouldMatch(t, "**/*.php", relativeDir(t, "/file.php")) - shouldMatch(t, "**/*.php", relativeDir(t, "/subpath/file.php")) - shouldMatch(t, "./**/*.php", relativeDir(t, "/subpath/file.php")) + shouldMatch(t, "**/*.php", relativeDir(t, "file.php")) + shouldMatch(t, "**/*.php", relativeDir(t, "subpath/file.php")) + shouldMatch(t, "./**/*.php", relativeDir(t, "subpath/file.php")) } func TestInvalidRecursiveFilePatterns(t *testing.T) { shouldNotMatch(t, "/path/**/*.txt", "/path/file.php") - shouldNotMatch(t, "/other/**/*.txt", "/path/file.php") + shouldNotMatch(t, "/path/**/*.txt", "/other/file.php") shouldNotMatch(t, "/path/**/*.txt", "/path/subpath/file.php") shouldNotMatch(t, "/path/**/?ilm.php", "/path/subpath/file.php") shouldNotMatch(t, "**/*.php", "/other/file.php") - shouldNotMatch(t, "**/*.php", "/other/file.php") shouldNotMatch(t, ".**/*.php", "/other/file.php") shouldNotMatch(t, "./**/*.php", "/other/file.php") } @@ -100,6 +99,7 @@ func TestValidDirectoryPatterns(t *testing.T) { shouldMatch(t, "/path/*/*.php", "/path/subpath/file.php") shouldMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/file.php") shouldMatch(t, "/path/?/*.php", "/path/1/file.php") + shouldMatch(t, "/path/**/vendor/*.php", "/path/vendor/file.php") shouldMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/file.php") shouldMatch(t, "/path/**/vendor/**/*.php", "/path/vendor/file.php") shouldMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/vendor/subpath/subpath/file.php") @@ -114,6 +114,7 @@ func TestInvalidDirectoryPatterns(t *testing.T) { shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/file.php") shouldNotMatch(t, "/path/*/*/*.php", "/path/subpath/subpath/subpath/file.php") shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/vendor/subpath/file.php") + shouldNotMatch(t, "/path/**/vendor/*.php", "/path/subpath/file.php") shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/file.php") shouldNotMatch(t, "/path/**/vendor/**/*.txt", "/path/subpath/vendor/subpath/file.php") shouldNotMatch(t, "/path/**/vendor/**/*.php", "/path/subpath/subpath/subpath/file.php") @@ -122,7 +123,7 @@ func TestInvalidDirectoryPatterns(t *testing.T) { } func relativeDir(t *testing.T, relativePath string) string { - dir, err := filepath.Abs("." + relativePath) + dir, err := filepath.Abs("./" + relativePath) assert.NoError(t, err) return dir } From 8670acbf740ddd48e89f5b1fefbbe2033dd2e787 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 30 Sep 2024 19:59:29 +0200 Subject: [PATCH 148/155] Removes line about symlinks. --- docs/config.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 732724fef..3d01e0b9f 100644 --- a/docs/config.md +++ b/docs/config.md @@ -159,7 +159,6 @@ One or multiple `watch` directives followed by a * The `**` pattern signifies recursive watching * Directories can also be relative (to where the FrankenPHP process is started from) * If you have multiple workers defined, all of them will be restarted when a file changes -* The watcher will currently not follow symlinks * Be wary about watching files that are created at runtime (like logs) since they might cause unwanted worker restarts. The file watcher is based on [e-dant/watcher](https://github.com/e-dant/watcher). From 27b43142f6b7753abd0f2e6eb59d14c179035ec4 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Mon, 30 Sep 2024 21:04:54 +0200 Subject: [PATCH 149/155] Hints at multiple possible --watch flags. --- caddy/php-server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/caddy/php-server.go b/caddy/php-server.go index 7f650644c..bc9b3c1fa 100644 --- a/caddy/php-server.go +++ b/caddy/php-server.go @@ -29,7 +29,7 @@ import ( func init() { caddycmd.RegisterCommand(caddycmd.Command{ Name: "php-server", - Usage: "[--domain ] [--root ] [--listen ] [--worker /path/to/worker.php<,nb-workers>] [--watch /path/to/watch] [--access-log] [--debug] [--no-compress] [--mercure]", + Usage: "[--domain ] [--root ] [--listen ] [--worker /path/to/worker.php<,nb-workers>] [--watch ] [--access-log] [--debug] [--no-compress] [--mercure]", Short: "Spins up a production-ready PHP server", Long: ` A simple but production-ready PHP server. Useful for quick deployments, From bc746e14c2f21fa1092bdc846ff09432113c19ed Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 2 Oct 2024 16:52:10 +0200 Subject: [PATCH 150/155] Adds ./**/*.php as default watch configuration. --- caddy/caddy.go | 6 ++++-- docs/config.md | 15 +++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index 6e38b208b..7114648fb 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -181,9 +181,11 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { wc.Env[args[0]] = args[1] case "watch": if !d.NextArg() { - return d.Err(`The "watch" directive must be followed by a path`) + // defaults to watching all PHP files in the current directory + wc.Watch = append(wc.Watch, "./**/*.php") + } else { + wc.Watch = append(wc.Watch, d.Val()) } - wc.Watch = append(wc.Watch, d.Val()) } if wc.FileName == "" { diff --git a/docs/config.md b/docs/config.md index 3d01e0b9f..2e53a5e2d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -140,8 +140,19 @@ to your PHP files will not be reflected immediately. Workers can instead be restarted on file changes via the `watch` directive. This is useful for development environments. -One or multiple `watch` directives followed by a -[shell filename pattern](https://pkg.go.dev/path/filepath#Match) can be defined like so: +```caddyfile +{ + frankenphp { + worker { + file /path/to/app/public/worker.php + watch + } + } +} +``` +If the watch directory is not specified, it will fall back to `./**/*.php`, which watches all PHP files in the current +directory and subdirectories. You can instead specify one or more directories via a +[shell filename pattern](https://pkg.go.dev/path/filepath#Match): ```caddyfile { From 6e685a00f48b788e126537d4eda05da491bbc47e Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 2 Oct 2024 16:53:45 +0200 Subject: [PATCH 151/155] Changes error to a warning. --- watcher/watcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watcher/watcher.go b/watcher/watcher.go index 4a6e1d5ec..937370361 100644 --- a/watcher/watcher.go +++ b/watcher/watcher.go @@ -108,7 +108,7 @@ func startSession(w *watchPattern) (C.uintptr_t, error) { func stopSession(session C.uintptr_t) { success := C.stop_watcher(session) if success == 0 { - logger.Error("couldn't stop watching") + logger.Warn("couldn't close the watcher") } } From ef7ab94237c5bdfd3bb6ed194515e8ee6e30f1e2 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 2 Oct 2024 22:35:35 +0200 Subject: [PATCH 152/155] Changes the default to './**/*.{php,yaml,yml,twig,env}' and supports the {bracket} pattern. --- caddy/caddy.go | 4 ++-- docs/config.md | 13 ++++++++----- frankenphp.go | 13 +++++++++---- watcher/watch_pattern.go | 31 ++++++++++++++++++++++++++++--- watcher/watch_pattern_test.go | 15 +++++++++++++++ worker.go | 2 +- 6 files changed, 63 insertions(+), 15 deletions(-) diff --git a/caddy/caddy.go b/caddy/caddy.go index 7114648fb..13ada1746 100644 --- a/caddy/caddy.go +++ b/caddy/caddy.go @@ -181,8 +181,8 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { wc.Env[args[0]] = args[1] case "watch": if !d.NextArg() { - // defaults to watching all PHP files in the current directory - wc.Watch = append(wc.Watch, "./**/*.php") + // the default if the watch directory is left empty: + wc.Watch = append(wc.Watch, "./**/*.{php,yaml,yml,twig,env}") } else { wc.Watch = append(wc.Watch, d.Val()) } diff --git a/docs/config.md b/docs/config.md index 2e53a5e2d..fd717941e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -150,8 +150,10 @@ This is useful for development environments. } } ``` -If the watch directory is not specified, it will fall back to `./**/*.php`, which watches all PHP files in the current -directory and subdirectories. You can instead specify one or more directories via a + +If the `watch` directory is not specified, it will fall back to `./**/*.{php,yaml,yml,twig,env}`, +which watches all `.php`, `.yaml`, `.yml`, `.twig` and `.env` files in the directory and subdirectories +where the FrankenPHP process was started. You can instead also specify one or more directories via a [shell filename pattern](https://pkg.go.dev/path/filepath#Match): ```caddyfile @@ -160,8 +162,9 @@ directory and subdirectories. You can instead specify one or more directories vi worker { file /path/to/app/public/worker.php watch /path/to/app # watches all files in all subdirectories of /path/to/app - watch /path/to/app/*.php # watches files ending in .php in the /path/to/app directory - watch /path/to/app/**/*.php # watches files ending in .php in the /path/to/app directory and subdirectories + watch /path/to/app/*.php # watches files ending in .php in /path/to/app + watch /path/to/app/**/*.php # watches PHP files in /path/to/app and subdirectories + watch /path/to/app/**/*.{php,twig} # watches PHP and Twig files in /path/to/app and subdirectories } } } @@ -192,7 +195,7 @@ This is an opt-in configuration that needs to be added to the global options in > [!CAUTION] > > Enabling this option may cause old HTTP/1.x clients that don't support full-duplex to deadlock. -This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config: +> This can also be configured using the `CADDY_GLOBAL_OPTIONS` environment config: ```sh CADDY_GLOBAL_OPTIONS="servers { enable_full_duplex }" diff --git a/frankenphp.go b/frankenphp.go index 0f103b9ef..defbcf4a8 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -865,14 +865,19 @@ func freeArgs(argv []*C.char) { } } -func executePhpFunction(functionName string) { +func executePHPFunction(functionName string) { cFunctionName := C.CString(functionName) defer C.free(unsafe.Pointer(cFunctionName)) - success := C.frankenphp_execute_php_function(C.CString(functionName)) + success := C.frankenphp_execute_php_function(cFunctionName) + if success == 1 { - logger.Debug("php function call successful", zap.String("function", functionName)) + if c := logger.Check(zapcore.DebugLevel, "php function call successful"); c != nil { + c.Write(zap.String("function", functionName)) + } } else { - logger.Error("php function call failed", zap.String("function", functionName)) + if c := logger.Check(zapcore.ErrorLevel, "php function call failed"); c != nil { + c.Write(zap.String("function", functionName)) + } } } diff --git a/watcher/watch_pattern.go b/watcher/watch_pattern.go index 1d40c95d5..0da197bcf 100644 --- a/watcher/watch_pattern.go +++ b/watcher/watch_pattern.go @@ -41,7 +41,7 @@ func parseFilePattern(filePattern string) (*watchPattern, error) { patternWithoutDir := "" for i, part := range splitPattern { isFilename := i == len(splitPattern)-1 && strings.Contains(part, ".") - isGlobCharacter := strings.ContainsAny(part, "[*?") + isGlobCharacter := strings.ContainsAny(part, "[*?{") if isFilename || isGlobCharacter { patternWithoutDir = filepath.Join(splitPattern[i:]...) w.dir = filepath.Join(splitPattern[:i]...) @@ -91,7 +91,7 @@ func isValidPattern(fileName string, dir string, patterns []string) bool { // if the pattern has size 1 we can match it directly against the filename if len(patterns) == 1 { - return matchPattern(patterns[0], fileNameWithoutDir) + return matchBracketPattern(patterns[0], fileNameWithoutDir) } return matchPatterns(patterns, fileNameWithoutDir) @@ -114,7 +114,7 @@ func matchPatterns(patterns []string, fileName string) bool { for j := cursor; j < len(partsToMatch); j++ { cursor = j subPattern := strings.Join(partsToMatch[j:j+patternSize], "/") - if matchPattern(pattern, subPattern) { + if matchBracketPattern(pattern, subPattern) { cursor = j + patternSize - 1 break } @@ -127,6 +127,31 @@ func matchPatterns(patterns []string, fileName string) bool { return true } +// we also check for the following bracket syntax: /path/*.{php,twig,yaml} +func matchBracketPattern(pattern string, fileName string) bool { + openingBracket := strings.Index(pattern, "{") + closingBracket := strings.Index(pattern, "}") + + // if there are no brackets we can match regularly + if openingBracket == -1 || closingBracket == -1 { + return matchPattern(pattern, fileName) + } + + beforeTheBrackets := pattern[:openingBracket] + betweenTheBrackets := pattern[openingBracket+1 : closingBracket] + afterTheBrackets := pattern[closingBracket+1:] + + // all bracket entries are checked individually, only one needs to match + // *.{php,twig,yaml} -> *.php, *.twig, *.yaml + for _, pattern := range strings.Split(betweenTheBrackets, ",") { + if matchPattern(beforeTheBrackets+pattern+afterTheBrackets, fileName) { + return true + } + } + + return false +} + func matchPattern(pattern string, fileName string) bool { if pattern == "" { return true diff --git a/watcher/watch_pattern_test.go b/watcher/watch_pattern_test.go index 3656ddc3a..8b15afea4 100644 --- a/watcher/watch_pattern_test.go +++ b/watcher/watch_pattern_test.go @@ -122,6 +122,21 @@ func TestInvalidDirectoryPatterns(t *testing.T) { shouldNotMatch(t, "/path*/path*", "/path1/path1/file.php") } +func TestValidExtendedPatterns(t *testing.T) { + shouldMatch(t, "/path/*.{php}", "/path/file.php") + shouldMatch(t, "/path/*.{php,twig}", "/path/file.php") + shouldMatch(t, "/path/*.{php,twig}", "/path/file.twig") + shouldMatch(t, "/path/**/{file.php,file.twig}", "/path/subpath/file.twig") + shouldMatch(t, "/path/{folder1,folder2}/file.php", "/path/folder1/file.php") +} + +func TestInValidExtendedPatterns(t *testing.T) { + shouldNotMatch(t, "/path/*.{php}", "/path/file.txt") + shouldNotMatch(t, "/path/*.{php,twig}", "/path/file.txt") + shouldNotMatch(t, "/path/{file.php,file.twig}", "/path/file.txt") + shouldNotMatch(t, "/path/{folder1,folder2}/file.php", "/path/folder3/file.php") +} + func relativeDir(t *testing.T, relativePath string) string { dir, err := filepath.Abs("./" + relativePath) assert.NoError(t, err) diff --git a/worker.go b/worker.go index 616f96857..bafc1deb4 100644 --- a/worker.go +++ b/worker.go @@ -213,7 +213,7 @@ func go_frankenphp_worker_handle_request_start(mrh C.uintptr_t) C.uintptr_t { if c := l.Check(zapcore.DebugLevel, "shutting down"); c != nil { c.Write(zap.String("worker", fc.scriptFilename)) } - executePhpFunction("opcache_reset") + executePHPFunction("opcache_reset") return 0 case r = <-rc: From b8d915c188afb0ea230ccb79f644705c817f3b02 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Wed, 2 Oct 2024 22:38:46 +0200 Subject: [PATCH 153/155] Fixes linting. --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index fd717941e..380dc62fd 100644 --- a/docs/config.md +++ b/docs/config.md @@ -151,7 +151,7 @@ This is useful for development environments. } ``` -If the `watch` directory is not specified, it will fall back to `./**/*.{php,yaml,yml,twig,env}`, +If the `watch` directory is not specified, it will fall back to `./**/*.{php,yaml,yml,twig,env}`, which watches all `.php`, `.yaml`, `.yml`, `.twig` and `.env` files in the directory and subdirectories where the FrankenPHP process was started. You can instead also specify one or more directories via a [shell filename pattern](https://pkg.go.dev/path/filepath#Match): From 043bb86e1092c7bf55cecbcec35cb6ba29d19a5d Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sun, 6 Oct 2024 01:21:18 +0200 Subject: [PATCH 154/155] Fixes merge conflict and adjust values. --- worker.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/worker.go b/worker.go index d8c346ac9..560dd3c6d 100644 --- a/worker.go +++ b/worker.go @@ -69,9 +69,9 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { l := getLogger() - const maxBackoff = 16 * time.Second - const minBackoff = 100 * time.Millisecond - const maxConsecutiveFailures = 3 + const maxBackoff = 10 * time.Millisecond + const minBackoff = 1 * time.Second + const maxConsecutiveFailures = 60 for i := 0; i < nbWorkers; i++ { go func() { @@ -197,6 +197,7 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { } workersReadyWG.Wait() + workersAreReady.Store(true) m.Lock() defer m.Unlock() @@ -252,8 +253,8 @@ func go_frankenphp_worker_ready(mrh C.uintptr_t) { fc.ready = true metrics.ReadyWorker(fc.scriptFilename) if !workersAreReady.Load() { - workersReadyWG.Done() - } + workersReadyWG.Done() + } } //export go_frankenphp_worker_handle_request_start From 8a2fb4559fbf9007067d7047da37b50a26f6fd97 Mon Sep 17 00:00:00 2001 From: "a.stecher" Date: Sun, 6 Oct 2024 11:48:21 +0200 Subject: [PATCH 155/155] Adjusts values. --- worker.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worker.go b/worker.go index 560dd3c6d..bce902d0c 100644 --- a/worker.go +++ b/worker.go @@ -69,8 +69,8 @@ func startWorkers(fileName string, nbWorkers int, env PreparedEnv) error { l := getLogger() - const maxBackoff = 10 * time.Millisecond - const minBackoff = 1 * time.Second + const maxBackoff = 1 * time.Second + const minBackoff = 10 * time.Millisecond const maxConsecutiveFailures = 60 for i := 0; i < nbWorkers; i++ {