2024-06-22 09:41:49 +02:00
|
|
|
// Copyright 2024, Command Line Inc.
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2024-06-20 08:59:41 +02:00
|
|
|
package wconfig
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
|
|
"github.com/wavetermdev/thenextwave/pkg/eventbus"
|
|
|
|
"github.com/wavetermdev/thenextwave/pkg/wavebase"
|
|
|
|
)
|
|
|
|
|
|
|
|
const configDir = "config"
|
|
|
|
|
|
|
|
var configDirAbsPath = filepath.Join(wavebase.GetWaveHomeDir(), configDir)
|
|
|
|
|
|
|
|
var instance *Watcher
|
|
|
|
var once sync.Once
|
|
|
|
|
|
|
|
type Watcher struct {
|
|
|
|
initialized bool
|
|
|
|
watcher *fsnotify.Watcher
|
|
|
|
mutex sync.Mutex
|
|
|
|
settingsFile string
|
|
|
|
getSettingsDefaults func() SettingsConfigType
|
|
|
|
settingsData SettingsConfigType
|
|
|
|
}
|
|
|
|
|
|
|
|
type WatcherUpdate struct {
|
|
|
|
File string `json:"file"`
|
|
|
|
Update SettingsConfigType `json:"update"`
|
|
|
|
Error string `json:"error"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func readFileContents(filePath string, getDefaults func() SettingsConfigType) (*SettingsConfigType, error) {
|
|
|
|
if getDefaults == nil {
|
|
|
|
log.Printf("oopsie")
|
|
|
|
return nil, fmt.Errorf("watcher started without defaults")
|
|
|
|
}
|
|
|
|
content := getDefaults()
|
|
|
|
data, err := os.ReadFile(filePath)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("doopsie: %v", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := json.Unmarshal(data, &content); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &content, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetWatcher returns the singleton instance of the Watcher
|
|
|
|
func GetWatcher() *Watcher {
|
|
|
|
once.Do(func() {
|
|
|
|
watcher, err := fsnotify.NewWatcher()
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("failed to create file watcher: %v", err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
os.MkdirAll(configDirAbsPath, 0751)
|
|
|
|
instance = &Watcher{watcher: watcher}
|
|
|
|
log.Printf("started config watcher: %v\n", configDirAbsPath)
|
|
|
|
if err := instance.addSettingsFile(settingsAbsPath, getSettingsConfigDefaults); err != nil {
|
|
|
|
log.Printf("failed to add path %s to watcher: %v", configDirAbsPath, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return instance
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) addSettingsFile(filename string, getDefaults func() SettingsConfigType) error {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
w.getSettingsDefaults = getDefaults
|
|
|
|
filename = filepath.ToSlash(filename)
|
|
|
|
|
|
|
|
stat, err := os.Lstat(filename)
|
|
|
|
if err != nil {
|
|
|
|
fmt.Printf("warning: cannot stat file: %v", err)
|
|
|
|
} else {
|
|
|
|
if stat.IsDir() {
|
|
|
|
return fmt.Errorf("warning: can't watch directory instead of file: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
w.settingsFile = filename
|
|
|
|
w.watcher.Add(filepath.Dir(filename))
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) Start() {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
log.Printf("starting file watcher\n")
|
|
|
|
w.initialized = true
|
|
|
|
w.sendInitialValues()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case event, ok := <-w.watcher.Events:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
w.handleEvent(event)
|
|
|
|
case err, ok := <-w.watcher.Errors:
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
log.Println("watcher error:", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
// for initial values, exit on first error
|
|
|
|
func (w *Watcher) sendInitialValues() error {
|
|
|
|
filename := w.settingsFile
|
|
|
|
|
|
|
|
content, err := readFileContents(w.settingsFile, w.getSettingsDefaults)
|
|
|
|
if os.IsNotExist(err) || os.IsPermission(err) {
|
|
|
|
log.Printf("settings file cannot be read: using defaults")
|
|
|
|
defaults := w.getSettingsDefaults()
|
|
|
|
content = &defaults
|
|
|
|
} else if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
message := WatcherUpdate{
|
|
|
|
File: filename,
|
|
|
|
Update: *content,
|
|
|
|
}
|
|
|
|
|
|
|
|
w.settingsData = message.Update
|
|
|
|
|
|
|
|
log.Printf("watcher: initial values: %s -> %v", filename, content)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) Close() {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
if w.watcher != nil {
|
|
|
|
w.watcher.Close()
|
|
|
|
w.watcher = nil
|
|
|
|
log.Println("file watcher closed")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) broadcast(message WatcherUpdate) {
|
|
|
|
// send to frontend
|
|
|
|
eventbus.SendEvent(eventbus.WSEventType{
|
|
|
|
EventType: "config",
|
|
|
|
Data: message,
|
|
|
|
})
|
|
|
|
|
|
|
|
if message.File == w.settingsFile {
|
|
|
|
w.settingsData = message.Update
|
|
|
|
}
|
|
|
|
|
|
|
|
if message.Error != "" {
|
|
|
|
log.Printf("watcher: error processing %s. sending defaults: %v", message.File, message.Error)
|
|
|
|
} else {
|
|
|
|
log.Printf("watcher: update: %s -> %v", message.File, message.Update)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) GetSettingsConfig() SettingsConfigType {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
return w.settingsData
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) handleEvent(event fsnotify.Event) {
|
|
|
|
w.mutex.Lock()
|
|
|
|
defer w.mutex.Unlock()
|
|
|
|
|
|
|
|
fileName := filepath.ToSlash(event.Name)
|
|
|
|
|
|
|
|
// only consider events for the tracked files
|
|
|
|
if fileName != w.settingsFile {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
defaults := w.getSettingsDefaults()
|
|
|
|
|
|
|
|
if event.Op&fsnotify.Remove == fsnotify.Remove || event.Op&fsnotify.Rename == fsnotify.Rename {
|
|
|
|
message := WatcherUpdate{
|
|
|
|
File: fileName,
|
|
|
|
Update: defaults,
|
|
|
|
}
|
|
|
|
w.broadcast(message)
|
|
|
|
}
|
|
|
|
|
|
|
|
if event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create {
|
|
|
|
content, err := readFileContents(fileName, w.getSettingsDefaults)
|
|
|
|
if err != nil {
|
|
|
|
message := WatcherUpdate{
|
|
|
|
File: fileName,
|
|
|
|
Update: defaults,
|
|
|
|
Error: err.Error(),
|
|
|
|
}
|
|
|
|
w.broadcast(message)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
message := WatcherUpdate{
|
|
|
|
File: fileName,
|
|
|
|
Update: *content,
|
|
|
|
}
|
|
|
|
|
|
|
|
w.broadcast(message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) AddWidget(newWidget WidgetsConfigType) error {
|
|
|
|
current := w.GetSettingsConfig()
|
|
|
|
current.Widgets = append(current.Widgets, newWidget)
|
|
|
|
update, err := json.Marshal(current)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
|
|
|
|
return os.WriteFile(w.settingsFile, update, 0644)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Watcher) RmWidget(idx uint) error {
|
|
|
|
current := w.GetSettingsConfig().Widgets
|
|
|
|
truncated := append(current[:idx], current[idx+1:]...)
|
|
|
|
update, err := json.Marshal(truncated)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
os.MkdirAll(filepath.Dir(w.settingsFile), 0751)
|
|
|
|
return os.WriteFile(w.settingsFile, update, 0644)
|
|
|
|
}
|