restructured the project to comply community guideline, for others check release note

This commit is contained in:
yusing
2024-09-28 09:51:34 +08:00
parent 4120fd8d1c
commit 90487bfde6
134 changed files with 1110 additions and 625 deletions

View 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)
}

View 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:
}
}
}
}

View 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,
)}

View 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
}

View 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
}

View 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)
}