diff --git a/README.md b/README.md index 1767078..14a53c9 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Zooid supports a few environment variables, which configure shared resources lik ## Configuration -Configuration files are written using [toml](https://toml.io). The name of the configuration file should be the hostname the relay serves, for example `relay.example.com`. Config files contain the following sections: +Configuration files are written using [toml](https://toml.io) files placed in the `./config` directory. The name of the configuration file should be the hostname the relay serves, for example `relay.example.com`. Config files contain the following sections: ### `[self]` @@ -108,6 +108,5 @@ See `justfile` for defined commands. ## TODO -- [ ] Watch configuration files and hot reload - [ ] Free up resources after instance inactivity - [ ] Admin/member lists diff --git a/cmd/relay/main.go b/cmd/relay/main.go index 992433d..b35fed3 100644 --- a/cmd/relay/main.go +++ b/cmd/relay/main.go @@ -31,6 +31,8 @@ func main() { } }() + go zooid.MonitorInstances() + <-shutdown fmt.Println("\nShutting down gracefully...") diff --git a/go.mod b/go.mod index 0fb9910..9cbd61b 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( fiatjaf.com/nostr v0.0.0-20250924142401-59bd3c29fffd github.com/BurntSushi/toml v1.5.0 github.com/Masterminds/squirrel v1.5.4 + github.com/fsnotify/fsnotify v1.9.0 github.com/gosimple/slug v1.15.0 github.com/mattn/go-sqlite3 v1.14.32 github.com/spf13/afero v1.15.0 - github.com/stretchr/testify v1.10.0 ) require ( @@ -19,7 +19,6 @@ require ( github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/coder/websocket v1.8.13 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/fasthttp/websocket v1.5.12 // indirect @@ -33,7 +32,6 @@ require ( github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect github.com/rs/cors v1.11.1 // indirect github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 // indirect @@ -44,6 +42,6 @@ require ( github.com/valyala/fasthttp v1.59.0 // indirect golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect golang.org/x/net v0.41.0 // indirect + golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 95faea0..0e2e3d9 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjY github.com/dvyukov/go-fuzz v0.0.0-20200318091601-be3528f3a813/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -91,9 +93,10 @@ golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/zooid/config.go b/zooid/config.go index ef71d69..b30362b 100644 --- a/zooid/config.go +++ b/zooid/config.go @@ -49,7 +49,7 @@ type Config struct { } func LoadConfig(hostname string) (*Config, error) { - path := filepath.Join("configs", hostname) + path := filepath.Join("config", hostname) var config Config if _, err := toml.DecodeFile(path, &config); err != nil { diff --git a/zooid/instance.go b/zooid/instance.go index 951122f..70f2557 100644 --- a/zooid/instance.go +++ b/zooid/instance.go @@ -5,6 +5,7 @@ import ( "iter" "log" "net/http" + "path/filepath" "slices" "strings" "sync" @@ -13,9 +14,86 @@ import ( "fiatjaf.com/nostr/eventstore" "fiatjaf.com/nostr/khatru" "fiatjaf.com/nostr/nip29" + "github.com/fsnotify/fsnotify" "github.com/gosimple/slug" ) +// Top level instance creation/destruction + +var ( + instances map[string]*Instance + instancesOnce sync.Once + instancesMux sync.RWMutex +) + +func GetInstance(hostname string) (*Instance, error) { + instancesMux.RLock() + defer instancesMux.RUnlock() + + instancesOnce.Do(func() { + instances = make(map[string]*Instance) + }) + + instance, exists := instances[hostname] + + if !exists { + newInstance, err := MakeInstance(hostname) + if err != nil { + return nil, err + } + + instances[hostname] = newInstance + instance = newInstance + } + + return instance, nil +} + +func MonitorInstances() { + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Printf("Failed to create file watcher: %v", err) + return + } + defer watcher.Close() + + if err := watcher.Add("./config"); err != nil { + log.Printf("Failed to watch config directory: %v", err) + return + } + + log.Printf("Watching config directory for changes") + + for { + select { + case event, ok := <-watcher.Events: + if !ok { + return + } + + hostname := filepath.Base(event.Name) + + if event.Has(fsnotify.Write) || event.Has(fsnotify.Create) || event.Has(fsnotify.Remove) { + log.Printf("Config file changed/deleted: %s", event.Name) + + instancesMux.Lock() + if instance, exists := instances[hostname]; exists { + instance.Cleanup() + } + instancesMux.Unlock() + } + + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Printf("File watcher error: %v", err) + } + } +} + +// Instance struct + type Instance struct { Config *Config Events eventstore.Store @@ -60,6 +138,7 @@ func MakeInstance(hostname string) (*Instance, error) { Relay: khatru.NewRelay(), } + instance.Relay.Negentropy = true instance.Relay.Info.Name = config.Self.Name instance.Relay.Info.Icon = config.Self.Icon instance.Relay.Info.PubKey = &pubkey @@ -103,28 +182,14 @@ func MakeInstance(hostname string) (*Instance, error) { return instance, nil } -var ( - instances map[string]*Instance - instanceOnce sync.Once -) +func (instance *Instance) Cleanup() bool { + // Close the event store + instance.Events.Close() -func GetInstance(hostname string) (*Instance, error) { - instanceOnce.Do(func() { - instances = make(map[string]*Instance) - }) + // Remove from instances map + delete(instances, instance.Config.Host) - instance, exists := instances[hostname] - if !exists { - newInstance, err := MakeInstance(hostname) - if err != nil { - return nil, err - } - - instances[hostname] = newInstance - instance = newInstance - } - - return instance, nil + return true } // Utility methods