mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-20 15:31:24 +02:00
restructured the project to comply community guideline, for others check release note
This commit is contained in:
21
internal/watcher/config_file_watcher.go
Normal file
21
internal/watcher/config_file_watcher.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/yusing/go-proxy/internal/common"
|
||||
)
|
||||
|
||||
var configDirWatcher *dirWatcher
|
||||
var configDirWatcherMu sync.Mutex
|
||||
|
||||
// create a new file watcher for file under ConfigBasePath
|
||||
func NewConfigFileWatcher(filename string) Watcher {
|
||||
configDirWatcherMu.Lock()
|
||||
defer configDirWatcherMu.Unlock()
|
||||
if configDirWatcher == nil {
|
||||
configDirWatcher = NewDirectoryWatcher(context.Background(), common.ConfigBasePath)
|
||||
}
|
||||
return configDirWatcher.Add(filename)
|
||||
}
|
||||
146
internal/watcher/directory_watcher.go
Normal file
146
internal/watcher/directory_watcher.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/sirupsen/logrus"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
F "github.com/yusing/go-proxy/internal/utils/functional"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type dirWatcher struct {
|
||||
dir string
|
||||
w *fsnotify.Watcher
|
||||
|
||||
fwMap F.Map[string, *fileWatcher]
|
||||
mu sync.Mutex
|
||||
|
||||
eventCh chan Event
|
||||
errCh chan E.NestedError
|
||||
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewDirectoryWatcher(ctx context.Context, dirPath string) *dirWatcher {
|
||||
//! subdirectories are not watched
|
||||
w, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
logrus.Panicf("unable to create fs watcher: %s", err)
|
||||
}
|
||||
if err = w.Add(dirPath); err != nil {
|
||||
logrus.Panicf("unable to create fs watcher: %s", err)
|
||||
}
|
||||
helper := &dirWatcher{
|
||||
dir: dirPath,
|
||||
w: w,
|
||||
fwMap: F.NewMapOf[string, *fileWatcher](),
|
||||
eventCh: make(chan Event),
|
||||
errCh: make(chan E.NestedError),
|
||||
ctx: ctx,
|
||||
}
|
||||
go helper.start()
|
||||
return helper
|
||||
}
|
||||
|
||||
func (h *dirWatcher) Events(_ context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
return h.eventCh, h.errCh
|
||||
}
|
||||
|
||||
func (h *dirWatcher) Add(relPath string) *fileWatcher {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
// check if the watcher already exists
|
||||
s, ok := h.fwMap.Load(relPath)
|
||||
if ok {
|
||||
return s
|
||||
}
|
||||
s = &fileWatcher{
|
||||
relPath: relPath,
|
||||
eventCh: make(chan Event),
|
||||
errCh: make(chan E.NestedError),
|
||||
}
|
||||
go func() {
|
||||
defer func() {
|
||||
close(s.eventCh)
|
||||
close(s.errCh)
|
||||
}()
|
||||
for {
|
||||
select {
|
||||
case <-h.ctx.Done():
|
||||
return
|
||||
case _, ok := <-h.eventCh:
|
||||
if !ok { // directory watcher closed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
h.fwMap.Store(relPath, s)
|
||||
return s
|
||||
}
|
||||
|
||||
func (h *dirWatcher) start() {
|
||||
defer close(h.eventCh)
|
||||
defer h.w.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-h.ctx.Done():
|
||||
return
|
||||
case fsEvent, ok := <-h.w.Events:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// retrieve the watcher
|
||||
relPath := strings.TrimPrefix(fsEvent.Name, h.dir)
|
||||
relPath = strings.TrimPrefix(relPath, "/")
|
||||
|
||||
msg := Event{
|
||||
Type: events.EventTypeFile,
|
||||
ActorName: relPath,
|
||||
}
|
||||
switch {
|
||||
case fsEvent.Has(fsnotify.Write):
|
||||
msg.Action = events.ActionFileWritten
|
||||
case fsEvent.Has(fsnotify.Create):
|
||||
msg.Action = events.ActionFileCreated
|
||||
case fsEvent.Has(fsnotify.Remove):
|
||||
msg.Action = events.ActionFileDeleted
|
||||
case fsEvent.Has(fsnotify.Rename):
|
||||
msg.Action = events.ActionFileRenamed
|
||||
default: // ignore other events
|
||||
continue
|
||||
}
|
||||
|
||||
// send event to directory watcher
|
||||
select {
|
||||
case h.eventCh <- msg:
|
||||
default:
|
||||
}
|
||||
|
||||
// send event to file watcher too
|
||||
w, ok := h.fwMap.Load(relPath)
|
||||
if ok {
|
||||
select {
|
||||
case w.eventCh <- msg:
|
||||
default:
|
||||
}
|
||||
}
|
||||
case err := <-h.w.Errors:
|
||||
if errors.Is(err, fsnotify.ErrClosed) {
|
||||
// closed manually?
|
||||
return
|
||||
}
|
||||
select {
|
||||
case h.errCh <- E.From(err):
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
internal/watcher/docker_watcher.go
Normal file
152
internal/watcher/docker_watcher.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
docker_events "github.com/docker/docker/api/types/events"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/sirupsen/logrus"
|
||||
D "github.com/yusing/go-proxy/internal/docker"
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type (
|
||||
DockerWatcher struct {
|
||||
host string
|
||||
client D.Client
|
||||
logrus.FieldLogger
|
||||
}
|
||||
DockerListOptions = docker_events.ListOptions
|
||||
)
|
||||
|
||||
// https://docs.docker.com/reference/api/engine/version/v1.47/#tag/System/operation/SystemPingHead
|
||||
var (
|
||||
DockerFilterContainer = filters.Arg("type", string(docker_events.ContainerEventType))
|
||||
DockerFilterStart = filters.Arg("event", string(docker_events.ActionStart))
|
||||
DockerFilterStop = filters.Arg("event", string(docker_events.ActionStop))
|
||||
DockerFilterDie = filters.Arg("event", string(docker_events.ActionDie))
|
||||
DockerFilterKill = filters.Arg("event", string(docker_events.ActionKill))
|
||||
DockerFilterPause = filters.Arg("event", string(docker_events.ActionPause))
|
||||
DockerFilterUnpause = filters.Arg("event", string(docker_events.ActionUnPause))
|
||||
|
||||
NewDockerFilter = filters.NewArgs
|
||||
|
||||
dockerWatcherRetryInterval = 3 * time.Second
|
||||
)
|
||||
|
||||
func DockerrFilterContainerName(name string) filters.KeyValuePair {
|
||||
return filters.Arg("container", name)
|
||||
}
|
||||
|
||||
func NewDockerWatcher(host string) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
host: host,
|
||||
FieldLogger: (logrus.
|
||||
WithField("module", "docker_watcher").
|
||||
WithField("host", host))}
|
||||
}
|
||||
|
||||
func NewDockerWatcherWithClient(client D.Client) DockerWatcher {
|
||||
return DockerWatcher{
|
||||
client: client,
|
||||
FieldLogger: (logrus.
|
||||
WithField("module", "docker_watcher").
|
||||
WithField("host", client.DaemonHost()))}
|
||||
}
|
||||
|
||||
func (w DockerWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
return w.EventsWithOptions(ctx, optionsWatchAll)
|
||||
}
|
||||
|
||||
func (w DockerWatcher) EventsWithOptions(ctx context.Context, options DockerListOptions) (<-chan Event, <-chan E.NestedError) {
|
||||
eventCh := make(chan Event)
|
||||
errCh := make(chan E.NestedError)
|
||||
|
||||
eventsCtx, eventsCancel := context.WithCancel(ctx)
|
||||
|
||||
go func() {
|
||||
defer close(eventCh)
|
||||
defer close(errCh)
|
||||
|
||||
defer func() {
|
||||
if w.client.Connected() {
|
||||
w.client.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if !w.client.Connected() {
|
||||
var err E.NestedError
|
||||
attempts := 0
|
||||
for {
|
||||
w.client, err = D.ConnectClient(w.host)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
attempts++
|
||||
errCh <- E.FailWith(fmt.Sprintf("docker connection attempt #%d", attempts), err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
time.Sleep(dockerWatcherRetryInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Debugf("client connected")
|
||||
|
||||
cEventCh, cErrCh := w.client.Events(eventsCtx, options)
|
||||
|
||||
w.Debugf("watcher started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if err := E.From(ctx.Err()); err != nil && err.IsNot(context.Canceled) {
|
||||
errCh <- err
|
||||
}
|
||||
return
|
||||
case msg := <-cEventCh:
|
||||
action, ok := events.DockerEventMap[msg.Action]
|
||||
if !ok {
|
||||
w.Debugf("ignored unknown docker event: %s for container %s", msg.Action, msg.Actor.Attributes["name"])
|
||||
continue
|
||||
}
|
||||
event := Event{
|
||||
Type: events.EventTypeDocker,
|
||||
ActorID: msg.Actor.ID,
|
||||
ActorAttributes: msg.Actor.Attributes, // labels
|
||||
ActorName: msg.Actor.Attributes["name"],
|
||||
Action: action,
|
||||
}
|
||||
eventCh <- event
|
||||
case err := <-cErrCh:
|
||||
if err == nil {
|
||||
continue
|
||||
}
|
||||
errCh <- E.From(err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
eventsCancel()
|
||||
time.Sleep(dockerWatcherRetryInterval)
|
||||
eventsCtx, eventsCancel = context.WithCancel(ctx)
|
||||
cEventCh, cErrCh = w.client.Events(ctx, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return eventCh, errCh
|
||||
}
|
||||
|
||||
var optionsWatchAll = DockerListOptions{Filters: NewDockerFilter(
|
||||
DockerFilterContainer,
|
||||
DockerFilterStart,
|
||||
DockerFilterStop,
|
||||
DockerFilterDie,
|
||||
)}
|
||||
78
internal/watcher/events/events.go
Normal file
78
internal/watcher/events/events.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
dockerEvents "github.com/docker/docker/api/types/events"
|
||||
)
|
||||
|
||||
type (
|
||||
Event struct {
|
||||
Type EventType
|
||||
ActorName string // docker: container name, file: relative file path
|
||||
ActorID string // docker: container id, file: empty
|
||||
ActorAttributes map[string]string // docker: container labels, file: empty
|
||||
Action Action
|
||||
}
|
||||
Action uint16
|
||||
EventType string
|
||||
)
|
||||
|
||||
const (
|
||||
ActionFileWritten Action = (1 << iota)
|
||||
ActionFileCreated
|
||||
ActionFileDeleted
|
||||
ActionFileRenamed
|
||||
|
||||
ActionContainerCreate
|
||||
ActionContainerStart
|
||||
ActionContainerUnpause
|
||||
|
||||
ActionContainerKill
|
||||
ActionContainerStop
|
||||
ActionContainerPause
|
||||
ActionContainerDie
|
||||
|
||||
actionContainerWakeMask = ActionContainerCreate | ActionContainerStart | ActionContainerUnpause
|
||||
actionContainerSleepMask = ActionContainerKill | ActionContainerStop | ActionContainerPause | ActionContainerDie
|
||||
)
|
||||
|
||||
const (
|
||||
EventTypeDocker EventType = "docker"
|
||||
EventTypeFile EventType = "file"
|
||||
)
|
||||
|
||||
var DockerEventMap = map[dockerEvents.Action]Action{
|
||||
dockerEvents.ActionCreate: ActionContainerCreate,
|
||||
dockerEvents.ActionStart: ActionContainerStart,
|
||||
dockerEvents.ActionUnPause: ActionContainerUnpause,
|
||||
|
||||
dockerEvents.ActionKill: ActionContainerKill,
|
||||
dockerEvents.ActionStop: ActionContainerStop,
|
||||
dockerEvents.ActionPause: ActionContainerPause,
|
||||
dockerEvents.ActionDie: ActionContainerDie,
|
||||
}
|
||||
|
||||
var dockerActionNameMap = func() (m map[Action]string) {
|
||||
m = make(map[Action]string, len(DockerEventMap))
|
||||
for k, v := range DockerEventMap {
|
||||
m[v] = string(k)
|
||||
}
|
||||
return
|
||||
}()
|
||||
|
||||
func (e Event) String() string {
|
||||
return fmt.Sprintf("%s %s", e.ActorName, e.Action)
|
||||
}
|
||||
|
||||
func (a Action) String() string {
|
||||
return dockerActionNameMap[a]
|
||||
}
|
||||
|
||||
func (a Action) IsContainerWake() bool {
|
||||
return a&actionContainerWakeMask != 0
|
||||
}
|
||||
|
||||
func (a Action) IsContainerSleep() bool {
|
||||
return a&actionContainerSleepMask != 0
|
||||
}
|
||||
17
internal/watcher/file_watcher.go
Normal file
17
internal/watcher/file_watcher.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
)
|
||||
|
||||
type fileWatcher struct {
|
||||
relPath string
|
||||
eventCh chan Event
|
||||
errCh chan E.NestedError
|
||||
}
|
||||
|
||||
func (fw *fileWatcher) Events(ctx context.Context) (<-chan Event, <-chan E.NestedError) {
|
||||
return fw.eventCh, fw.errCh
|
||||
}
|
||||
14
internal/watcher/watcher.go
Normal file
14
internal/watcher/watcher.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package watcher
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/yusing/go-proxy/internal/error"
|
||||
"github.com/yusing/go-proxy/internal/watcher/events"
|
||||
)
|
||||
|
||||
type Event = events.Event
|
||||
|
||||
type Watcher interface {
|
||||
Events(ctx context.Context) (<-chan Event, <-chan E.NestedError)
|
||||
}
|
||||
Reference in New Issue
Block a user