Skip to content

Commit 2f1897f

Browse files
committed
use watcher-go
1 parent 3e3683f commit 2f1897f

19 files changed

Lines changed: 170 additions & 289 deletions

caddy/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/dunglas/frankenphp/caddy
22

3-
go 1.25.0
3+
go 1.25.4
44

55
replace github.com/dunglas/frankenphp => ../
66

@@ -63,6 +63,7 @@ require (
6363
github.com/dunglas/skipfilter v1.0.0 // indirect
6464
github.com/dunglas/vulcain v1.2.1 // indirect
6565
github.com/dustin/go-humanize v1.0.1 // indirect
66+
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 // indirect
6667
github.com/felixge/httpsnoop v1.0.4 // indirect
6768
github.com/fsnotify/fsnotify v1.9.0 // indirect
6869
github.com/fxamacker/cbor/v2 v2.9.0 // indirect

caddy/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ github.com/dunglas/vulcain/caddy v1.2.1/go.mod h1:8QrmLTfURmW2VgjTR6Gb9a53FrZjsp
159159
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
160160
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
161161
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
162+
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
163+
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
162164
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
163165
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
164166
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=

frankenphp.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -319,20 +319,15 @@ func Init(options ...Option) error {
319319
convertToRegularThread(getInactivePHPThread())
320320
}
321321

322-
watchPatterns, err := initWorkers(opt.workers)
323-
if err != nil {
322+
if err := initWorkers(opt.workers); err != nil {
324323
Shutdown()
325324

326325
return err
327326
}
328327

329-
watchPatterns = append(watchPatterns, opt.hotReload...)
330-
331-
if len(watchPatterns) > 0 {
332-
if err := watcher.InitWatcher(globalCtx, globalLogger, watchPatterns); err != nil {
333-
Shutdown()
334-
return err
335-
}
328+
if err := initWatchers(opt); err != nil {
329+
Shutdown()
330+
return err
336331
}
337332

338333
initAutoScaling(mainThread)

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/dunglas/frankenphp
22

3-
go 1.25.0
3+
go 1.25.4
44

55
retract v1.0.0-rc.1 // Human error
66

@@ -26,6 +26,7 @@ require (
2626
github.com/cespare/xxhash/v2 v2.3.0 // indirect
2727
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2828
github.com/dunglas/skipfilter v1.0.0 // indirect
29+
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 // indirect
2930
github.com/felixge/httpsnoop v1.0.4 // indirect
3031
github.com/fsnotify/fsnotify v1.9.0 // indirect
3132
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ github.com/dunglas/mercure v0.21.2 h1:qaLTScSwsCHDps++4AeLWrRp3BysdR5EoHBqu7JNha
2222
github.com/dunglas/mercure v0.21.2/go.mod h1:3ElA7VwRI8BHUIAVU8oGlvPaqGwsKU5zZVWFNSFg/+U=
2323
github.com/dunglas/skipfilter v1.0.0 h1:JG9SgGg4n6BlFwuTYzb9RIqjH7PfwszvWehanrYWPF4=
2424
github.com/dunglas/skipfilter v1.0.0/go.mod h1:ryhr8j7CAHSjzeN7wI6YEuwoArQ3OQmRqWWVCEAfb9w=
25+
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146 h1:h3vVM6X45PK0mAk8NqiYNQGXTyhvXy1HQ5GhuQN4eeA=
26+
github.com/e-dant/watcher/watcher-go v0.0.0-20251208164151-f88ec3b7e146/go.mod h1:sVUOkwtftoj71nnJRG2S0oWNfXFdKpz/M9vK0z06nmM=
2527
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
2628
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
2729
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=

hotreload.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//go:build !nomercure && !nowatcher
2+
3+
package frankenphp
4+
5+
import (
6+
"encoding/json"
7+
"log/slog"
8+
9+
"github.com/dunglas/frankenphp/internal/watcher"
10+
"github.com/dunglas/mercure"
11+
watcherGo "github.com/e-dant/watcher/watcher-go"
12+
)
13+
14+
// WithHotReload sets files to watch for file changes to trigger a hot reload update.
15+
func WithHotReload(name string, hub *mercure.Hub, patterns []string) Option {
16+
return func(o *opt) error {
17+
o.hotReload = append(o.hotReload, &watcher.PatternGroup{
18+
Patterns: patterns,
19+
Callback: func(events []*watcherGo.Event) {
20+
// Wait for workers to restart before sending the update
21+
go func() {
22+
data, err := json.Marshal(events)
23+
if err != nil {
24+
if globalLogger.Enabled(globalCtx, slog.LevelError) {
25+
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error marshaling watcher events", slog.Any("error", err))
26+
}
27+
28+
return
29+
}
30+
31+
if err := hub.Publish(globalCtx, &mercure.Update{
32+
Topics: []string{"https://frankenphp.dev/hot-reload/" + name},
33+
Event: mercure.Event{Data: string(data)},
34+
Debug: globalLogger.Enabled(globalCtx, slog.LevelDebug),
35+
}); err != nil && globalLogger.Enabled(globalCtx, slog.LevelError) {
36+
globalLogger.LogAttrs(globalCtx, slog.LevelError, "error publishing hot reloading Mercure update", slog.Any("error", err))
37+
}
38+
}()
39+
},
40+
})
41+
42+
return nil
43+
}
44+
}

internal/watcher/pattern.go

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,13 @@
22

33
package watcher
44

5-
// #cgo LDFLAGS: -lwatcher-c -lstdc++
6-
// #include <stdint.h>
7-
// #include <stdlib.h>
8-
// #include "watcher.h"
9-
import "C"
105
import (
116
"log/slog"
127
"path/filepath"
13-
"runtime/cgo"
148
"strings"
15-
"time"
16-
"unsafe"
179

1810
"github.com/dunglas/frankenphp/internal/fastabs"
11+
"github.com/e-dant/watcher/watcher-go"
1912
)
2013

2114
type pattern struct {
@@ -24,31 +17,16 @@ type pattern struct {
2417
parsedValues []string
2518
events chan eventHolder
2619
failureCount int
27-
watcher C.uintptr_t
28-
h cgo.Handle
29-
}
3020

31-
func (p *pattern) startSession() error {
32-
p.h = cgo.NewHandle(p)
33-
cDir := C.CString(p.value)
34-
defer C.free(unsafe.Pointer(cDir))
21+
watcher *watcher.Watcher
22+
}
3523

36-
p.watcher = C.start_new_watcher(cDir, C.uintptr_t(p.h))
37-
if p.watcher != 0 {
38-
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
39-
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "watching", slog.String("pattern", p.value))
40-
}
24+
func (p *pattern) startSession() {
25+
p.watcher = watcher.NewWatcher(p.value, p.handle)
4126

42-
return nil
27+
if globalLogger.Enabled(globalCtx, slog.LevelDebug) {
28+
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "watching", slog.String("pattern", p.value))
4329
}
44-
45-
if globalLogger.Enabled(globalCtx, slog.LevelError) {
46-
globalLogger.LogAttrs(globalCtx, slog.LevelError, "couldn't start watching", slog.String("pattern", p.value))
47-
}
48-
49-
p.h.Delete()
50-
51-
return ErrUnableToStartWatching
5230
}
5331

5432
// this method prepares the pattern struct (aka /path/*pattern)
@@ -94,7 +72,7 @@ func (p *pattern) parse() (err error) {
9472
return nil
9573
}
9674

97-
func (p *pattern) allowReload(event *Event) bool {
75+
func (p *pattern) allowReload(event *watcher.Event) bool {
9876
if !isValidEventType(event.EffectType) || !isValidPathType(event) {
9977
return false
10078
}
@@ -105,9 +83,9 @@ func (p *pattern) allowReload(event *Event) bool {
10583
return p.isValidPattern(event.PathName) || p.isValidPattern(event.AssociatedPathName)
10684
}
10785

108-
func (p *pattern) handle(event *Event) {
86+
func (p *pattern) handle(event *watcher.Event) {
10987
// If the watcher prematurely sends the die@ event, retry watching
110-
if event.PathType == PathTypeWatcher && strings.HasPrefix(event.PathName, "e/self/die@") && watcherIsActive.Load() {
88+
if event.PathType == watcher.PathTypeWatcher && strings.HasPrefix(event.PathName, "e/self/die@") && watcherIsActive.Load() {
11189
p.retryWatching()
11290

11391
return
@@ -119,23 +97,19 @@ func (p *pattern) handle(event *Event) {
11997
}
12098

12199
func (p *pattern) stop() {
122-
if C.stop_watcher(p.watcher) == 0 && globalLogger.Enabled(globalCtx, slog.LevelWarn) {
123-
globalLogger.LogAttrs(globalCtx, slog.LevelWarn, "couldn't close the watcher")
124-
}
125-
126-
p.h.Delete()
100+
p.watcher.Close()
127101
}
128102

129-
func isValidEventType(effectType EffectType) bool {
130-
return effectType <= EffectTypeDestroy
103+
func isValidEventType(effectType watcher.EffectType) bool {
104+
return effectType <= watcher.EffectTypeDestroy
131105
}
132106

133-
func isValidPathType(event *Event) bool {
134-
if event.PathType == PathTypeWatcher && globalLogger.Enabled(globalCtx, slog.LevelDebug) {
107+
func isValidPathType(event *watcher.Event) bool {
108+
if event.PathType == watcher.PathTypeWatcher && globalLogger.Enabled(globalCtx, slog.LevelDebug) {
135109
globalLogger.LogAttrs(globalCtx, slog.LevelDebug, "special e-dant/watcher event", slog.Any("event", event))
136110
}
137111

138-
return event.PathType <= PathTypeHardLink
112+
return event.PathType <= watcher.PathTypeHardLink
139113
}
140114

141115
func (p *pattern) isValidPattern(fileName string) bool {
@@ -234,18 +208,3 @@ func matchPattern(pattern string, fileName string) bool {
234208

235209
return patternMatches
236210
}
237-
238-
//export go_handle_file_watcher_event
239-
func go_handle_file_watcher_event(event C.struct_wtr_watcher_event, handle C.uintptr_t) {
240-
p := cgo.Handle(handle).Value().(*pattern)
241-
242-
e := &Event{
243-
EffectTime: time.Unix(int64(event.effect_time)/1000000000, int64(event.effect_time)%1000000000),
244-
PathName: C.GoString(event.path_name),
245-
AssociatedPathName: C.GoString(event.associated_path_name),
246-
EffectType: EffectType(event.effect_type),
247-
PathType: PathType(event.path_type),
248-
}
249-
250-
p.handle(e)
251-
}

internal/watcher/pattern_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"path/filepath"
77
"testing"
88

9+
"github.com/e-dant/watcher/watcher-go"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
1112
)
@@ -14,14 +15,14 @@ func TestDisallowOnEventTypeBiggerThan3(t *testing.T) {
1415
w := pattern{value: "/some/path"}
1516
require.NoError(t, w.parse())
1617

17-
assert.False(t, w.allowReload(&Event{PathName: "/some/path/watch-me.php", EffectType: EffectTypeOwner}))
18+
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", EffectType: watcher.EffectTypeOwner}))
1819
}
1920

2021
func TestDisallowOnPathTypeBiggerThan2(t *testing.T) {
2122
w := pattern{value: "/some/path"}
2223
require.NoError(t, w.parse())
2324

24-
assert.False(t, w.allowReload(&Event{PathName: "/some/path/watch-me.php", PathType: PathTypeSymLink}))
25+
assert.False(t, w.allowReload(&watcher.Event{PathName: "/some/path/watch-me.php", PathType: watcher.PathTypeSymLink}))
2526
}
2627

2728
func TestWatchesCorrectDir(t *testing.T) {
@@ -306,7 +307,7 @@ func TestAnAssociatedEventTriggersTheWatcher(t *testing.T) {
306307
require.NoError(t, w.parse())
307308
w.events = make(chan eventHolder)
308309

309-
e := &Event{PathName: "/path/temporary_file", AssociatedPathName: "/path/file.php"}
310+
e := &watcher.Event{PathName: "/path/temporary_file", AssociatedPathName: "/path/file.php"}
310311
go w.handle(e)
311312

312313
assert.Equal(t, e, (<-w.events).event)
@@ -333,7 +334,7 @@ func shouldMatch(t *testing.T, p string, fileName string) {
333334
w := pattern{value: p}
334335
require.NoError(t, w.parse())
335336

336-
assert.True(t, w.allowReload(&Event{PathName: fileName}))
337+
assert.True(t, w.allowReload(&watcher.Event{PathName: fileName}))
337338
}
338339

339340
func shouldNotMatch(t *testing.T, p string, fileName string) {
@@ -342,5 +343,5 @@ func shouldNotMatch(t *testing.T, p string, fileName string) {
342343
w := pattern{value: p}
343344
require.NoError(t, w.parse())
344345

345-
assert.False(t, w.allowReload(&Event{PathName: fileName}))
346+
assert.False(t, w.allowReload(&watcher.Event{PathName: fileName}))
346347
}

internal/watcher/types.go

Lines changed: 0 additions & 81 deletions
This file was deleted.

0 commit comments

Comments
 (0)