Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions caddy/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,39 @@ func TestModuleWorkerWithCustomName(t *testing.T) {
require.Equal(t, "m#custom-worker-name", module.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
require.Equal(t, "m#custom-worker-name", app.Workers[0].Name, "Worker should have the custom name, prefixed with m#")
}

func TestWorkerWatchAllowsExcludePatterns(t *testing.T) {
// Create a test configuration with an exclude watch pattern
configWithExcludeWatch := `
{
php {
worker {
file ../testdata/worker-with-env.php
num 1
watch
watch !./vendor/**
watch ./src/**/*.php
}
}
}`

// Parse the configuration
d := caddyfile.NewTestDispenser(configWithExcludeWatch)
module := &FrankenPHPModule{}

// Unmarshal the configuration
err := module.UnmarshalCaddyfile(d)

// Verify that no error was returned
require.NoError(t, err, "Expected no error when configuring a worker with an exclude watch pattern")

// Verify that the worker was added to the module
require.Len(t, module.Workers, 1, "Expected one worker to be added to the module")
require.Equal(t, "../testdata/worker-with-env.php", module.Workers[0].FileName, "Worker should have the correct filename")

// Verify that the watch patterns were set correctly
require.Len(t, module.Workers[0].Watch, 3, "Expected three watch patterns")
require.Equal(t, defaultWatchPattern, module.Workers[0].Watch[0], "First watch pattern should be the default")
require.Equal(t, "!./vendor/**", module.Workers[0].Watch[1], "Second watch pattern should be the exclude pattern")
require.Equal(t, "./src/**/*.php", module.Workers[0].Watch[2], "Third watch pattern should match the configuration")
}
25 changes: 22 additions & 3 deletions internal/watcher/pattern.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type pattern struct {
parsedValues []string
events chan eventHolder
failureCount int
isExclude bool

watcher *watcher.Watcher
}
Expand All @@ -31,8 +32,17 @@ func (p *pattern) startSession() {

// this method prepares the pattern struct (aka /path/*pattern)
func (p *pattern) parse() (err error) {
// first we clean the value
absPattern, err := fastabs.FastAbs(p.value)
// detect exclusion before resolving to absolute
raw := strings.TrimSpace(p.value)
if strings.HasPrefix(raw, "!") {
p.isExclude = true
raw = strings.TrimSpace(strings.TrimPrefix(raw, "!"))
} else {
p.isExclude = false
}

// then we clean the value
absPattern, err := fastabs.FastAbs(raw)
if err != nil {
return err
}
Expand Down Expand Up @@ -72,7 +82,7 @@ func (p *pattern) parse() (err error) {
return nil
}

func (p *pattern) allowReload(event *watcher.Event) bool {
func (p *pattern) matchesEvent(event *watcher.Event) bool {
if !isValidEventType(event.EffectType) || !isValidPathType(event) {
return false
}
Expand All @@ -83,6 +93,15 @@ func (p *pattern) allowReload(event *watcher.Event) bool {
return p.isValidPattern(event.PathName) || p.isValidPattern(event.AssociatedPathName)
}

func (p *pattern) allowReload(event *watcher.Event) bool {
// Excludes never trigger reload by themselves, but they still match events for filtering.
if p.isExclude {
return false
}

return p.matchesEvent(event)
}

func (p *pattern) handle(event *watcher.Event) {
// If the watcher prematurely sends the die@ event, retry watching
if event.PathType == watcher.PathTypeWatcher && strings.HasPrefix(event.PathName, "e/self/die@") && watcherIsActive.Load() {
Expand Down
38 changes: 38 additions & 0 deletions internal/watcher/pattern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,44 @@ func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
assert.Equal(t, e, (<-w.events).event)
}

func TestGlobalWatcherIsExcludedEvent(t *testing.T) {
pg := &PatternGroup{Patterns: []string{"/app/**/*.php", "!/app/vendor/**"}}

include := &pattern{patternGroup: pg, value: "/app/**/*.php"}
exclude := &pattern{patternGroup: pg, value: "!/app/vendor/**"}

require.NoError(t, include.parse())
require.NoError(t, exclude.parse())

gw := &globalWatcher{
groups: []*PatternGroup{pg},
watchers: []*pattern{include, exclude},
excludes: map[*PatternGroup][]*pattern{pg: {exclude}},
}

inVendor := &watcher.Event{PathName: "/app/vendor/pkg/file.php"}
assert.True(t, include.matchesEvent(inVendor))
assert.True(t, gw.isExcludedEvent(pg, inVendor))

notInVendor := &watcher.Event{PathName: "/app/src/file.php"}
assert.True(t, include.matchesEvent(notInVendor))
assert.False(t, gw.isExcludedEvent(pg, notInVendor))
}

func TestExcludeMatchesAssociatedPath(t *testing.T) {
pg := &PatternGroup{Patterns: []string{"/app/**/*.php", "!/app/vendor/**"}}

exclude := &pattern{patternGroup: pg, value: "!/app/vendor/**"}
require.NoError(t, exclude.parse())

gw := &globalWatcher{
excludes: map[*PatternGroup][]*pattern{pg: {exclude}},
}

e := &watcher.Event{PathName: "/tmp/temporary", AssociatedPathName: "/app/vendor/x/file.php"}
assert.True(t, gw.isExcludedEvent(pg, e))
}

func relativeDir(t *testing.T, relativePath string) string {
dir, err := filepath.Abs("./" + relativePath)
assert.NoError(t, err)
Expand Down
24 changes: 23 additions & 1 deletion internal/watcher/watcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type eventHolder struct {
type globalWatcher struct {
groups []*PatternGroup
watchers []*pattern
excludes map[*PatternGroup][]*pattern
events chan eventHolder
stop chan struct{}
}
Expand Down Expand Up @@ -138,11 +139,18 @@ func (p *pattern) retryWatching() {
func (g *globalWatcher) startWatching() error {
g.events = make(chan eventHolder)
g.stop = make(chan struct{})
g.excludes = make(map[*PatternGroup][]*pattern)

if err := g.parseFilePatterns(); err != nil {
return err
}

for _, w := range g.watchers {
if w.isExclude {
g.excludes[w.patternGroup] = append(g.excludes[w.patternGroup], w)
}
}

for _, w := range g.watchers {
w.events = g.events
w.startSession()
Expand Down Expand Up @@ -170,6 +178,17 @@ func (g *globalWatcher) stopWatching() {
}
}

func (g *globalWatcher) isExcludedEvent(pg *PatternGroup, e *watcher.Event) bool {
excludes := g.excludes[pg]
for _, ex := range excludes {
if ex.matchesEvent(e) {
return true
}
}

return false
}

func (g *globalWatcher) listenForFileEvents() {
timer := time.NewTimer(debounceDuration)
timer.Stop()
Expand All @@ -182,8 +201,11 @@ func (g *globalWatcher) listenForFileEvents() {
case <-g.stop:
return
case eh := <-g.events:
timer.Reset(debounceDuration)
if g.isExcludedEvent(eh.patternGroup, eh.event) {
continue
}

timer.Reset(debounceDuration)
eventsPerGroup[eh.patternGroup] = append(eventsPerGroup[eh.patternGroup], eh.event)
case <-timer.C:
timer.Stop()
Expand Down