Compare commits

..

No commits in common. "4f02f0e5bf25c88d08f6eadb37b4a859625fa34a" and "c70840271e387a773abd6f8787a6b7155421a962" have entirely different histories.

6 changed files with 32 additions and 450 deletions

View File

@ -21,7 +21,7 @@ func LogRequest(log *logger.Logger, statusCode int, r *http.Request, duration ti
statusColor := getStatusColor(statusCode)
// Use the logger's raw message writer to bypass the standard format
log.LogRaw("%s [ %s%d%s] %s %s (%v)",
log.LogRaw("%s [%s%d%s] %s %s (%v)",
time.Now().Format(log.TimeFormat()),
statusColor, statusCode, colorReset,
r.Method, r.URL.Path, duration)

View File

@ -36,7 +36,7 @@ func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, pool
// ListenAndServe starts the server on the given address
func (s *Server) ListenAndServe(addr string) error {
s.httpServer.Addr = addr
s.logger.Info("Server listening at http://localhost%s", addr)
s.logger.Info("Server starting on %s", addr)
return s.httpServer.ListenAndServe()
}

View File

@ -34,11 +34,11 @@ var levelProps = map[int]struct {
tag string
color string
}{
LevelDebug: {" DBG", colorCyan},
LevelInfo: {"INFO", colorBlue},
LevelWarning: {"WARN", colorYellow},
LevelError: {" ERR", colorRed},
LevelFatal: {"FATL", colorPurple},
LevelDebug: {"DBG", colorCyan},
LevelInfo: {"INF", colorBlue},
LevelWarning: {"WRN", colorYellow},
LevelError: {"ERR", colorRed},
LevelFatal: {"FTL", colorPurple},
}
// Time format for log messages

View File

@ -1,44 +0,0 @@
package watchers
import (
"git.sharkk.net/Sky/Moonshark/core/logger"
"git.sharkk.net/Sky/Moonshark/core/routers"
)
// WatchLuaRouter sets up a watcher for a LuaRouter's routes directory
func WatchLuaRouter(router *routers.LuaRouter, routesDir string, log *logger.Logger) (*Watcher, error) {
config := WatcherConfig{
Dir: routesDir,
Callback: router.Refresh,
Log: log,
Recursive: true,
Adaptive: true,
}
watcher, err := WatchDirectory(config)
if err != nil {
return nil, err
}
log.Info("Started watching Lua routes directory with adaptive polling: %s", routesDir)
return watcher, nil
}
// WatchStaticRouter sets up a watcher for a StaticRouter's root directory
func WatchStaticRouter(router *routers.StaticRouter, staticDir string, log *logger.Logger) (*Watcher, error) {
config := WatcherConfig{
Dir: staticDir,
Callback: router.Refresh,
Log: log,
Recursive: true,
Adaptive: true,
}
watcher, err := WatchDirectory(config)
if err != nil {
return nil, err
}
log.Info("Started watching static files directory with adaptive polling: %s", staticDir)
return watcher, nil
}

View File

@ -1,347 +0,0 @@
package watchers
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// Default polling intervals
const (
defaultPollInterval = 1 * time.Second // Initial polling interval
extendedPollInterval = 5 * time.Second // Extended polling interval after inactivity
maxPollInterval = 10 * time.Second // Maximum polling interval after long inactivity
inactivityThreshold = 30 * time.Second // Time before extending polling interval
secondExtendThreshold = 2 * time.Minute // Time before second extension
)
// Default debounce time between detected change and callback
const defaultDebounceTime = 300 * time.Millisecond
// FileInfo stores metadata about a file for change detection
type FileInfo struct {
ModTime time.Time
Size int64
IsDir bool
}
// Watcher implements a simple polling-based file watcher
type Watcher struct {
// Directory to watch
dir string
// Map of file paths to their metadata
files map[string]FileInfo
filesMu sync.RWMutex
// Configuration
callback func() error
log *logger.Logger
pollInterval time.Duration // Current polling interval
basePollInterval time.Duration // Base (starting) polling interval
debounceTime time.Duration
recursive bool
adaptive bool // Whether to use adaptive polling intervals
// Adaptive polling
lastChangeTime time.Time // When we last detected a change
// Control channels
done chan struct{}
debounceCh chan struct{}
// Wait group for shutdown coordination
wg sync.WaitGroup
}
// WatcherConfig contains configuration for the file watcher
type WatcherConfig struct {
// Directory to watch
Dir string
// Callback function to call when changes are detected
Callback func() error
// Logger instance
Log *logger.Logger
// Poll interval (0 means use default)
PollInterval time.Duration
// Debounce time (0 means use default)
DebounceTime time.Duration
// Recursive watching (watch subdirectories)
Recursive bool
// Use adaptive polling intervals
Adaptive bool
}
// WatchDirectory sets up filesystem monitoring on a directory
// Returns the watcher for later cleanup and any setup error
func WatchDirectory(config WatcherConfig) (*Watcher, error) {
pollInterval := config.PollInterval
if pollInterval == 0 {
pollInterval = defaultPollInterval
}
debounceTime := config.DebounceTime
if debounceTime == 0 {
debounceTime = defaultDebounceTime
}
w := &Watcher{
dir: config.Dir,
files: make(map[string]FileInfo),
callback: config.Callback,
log: config.Log,
pollInterval: pollInterval,
basePollInterval: pollInterval,
debounceTime: debounceTime,
recursive: config.Recursive,
adaptive: config.Adaptive,
lastChangeTime: time.Now(),
done: make(chan struct{}),
debounceCh: make(chan struct{}, 1),
}
// Perform initial scan
if err := w.scanDirectory(); err != nil {
return nil, err
}
// Start the watcher routines
w.wg.Add(2)
go w.watchLoop()
go w.debounceLoop()
if config.Adaptive {
w.logDebug("Started watching with adaptive polling")
} else {
w.logDebug("Started watching with fixed polling interval: %v", pollInterval)
}
return w, nil
}
// Close stops the watcher
func (w *Watcher) Close() error {
close(w.done)
w.wg.Wait()
return nil
}
// watchLoop periodically scans the directory for changes
func (w *Watcher) watchLoop() {
defer w.wg.Done()
ticker := time.NewTicker(w.pollInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
changed, err := w.checkForChanges()
if err != nil {
w.logError("Error checking for changes: %v", err)
continue
}
if changed {
// Update last change time
w.lastChangeTime = time.Now()
if w.adaptive && w.pollInterval > w.basePollInterval {
w.pollInterval = w.basePollInterval
ticker.Reset(w.pollInterval)
w.logDebug("Reset to base polling interval: %v", w.pollInterval)
}
// Try to send a change notification, non-blocking
select {
case w.debounceCh <- struct{}{}:
default:
// Channel already has a pending notification
}
} else if w.adaptive {
// Consider extending polling interval if enough time has passed since last change
inactiveDuration := time.Since(w.lastChangeTime)
if w.pollInterval == w.basePollInterval && inactiveDuration > inactivityThreshold {
// First extension
w.pollInterval = extendedPollInterval
ticker.Reset(w.pollInterval)
w.logDebug("Extended polling interval to: %v after %v of inactivity",
w.pollInterval, inactiveDuration.Round(time.Second))
} else if w.pollInterval == extendedPollInterval && inactiveDuration > secondExtendThreshold {
// Second extension
w.pollInterval = maxPollInterval
ticker.Reset(w.pollInterval)
w.logDebug("Extended polling interval to: %v after %v of inactivity",
w.pollInterval, inactiveDuration.Round(time.Second))
}
}
case <-w.done:
return
}
}
}
// debounceLoop handles debouncing change notifications
func (w *Watcher) debounceLoop() {
defer w.wg.Done()
var timer *time.Timer
for {
select {
case <-w.debounceCh:
// Cancel existing timer if there is one
if timer != nil {
timer.Stop()
}
// Start a new timer
timer = time.AfterFunc(w.debounceTime, func() {
if err := w.callback(); err != nil {
w.logError("Refresh callback error: %v", err)
}
})
case <-w.done:
if timer != nil {
timer.Stop()
}
return
}
}
}
// scanDirectory builds the initial file list
func (w *Watcher) scanDirectory() error {
w.filesMu.Lock()
defer w.filesMu.Unlock()
return filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
w.logWarning("Error accessing path %s: %v", path, err)
return nil // Continue with other files
}
// Skip if not recursive and this is a subdirectory
if !w.recursive && info.IsDir() && path != w.dir {
return filepath.SkipDir
}
w.files[path] = FileInfo{
ModTime: info.ModTime(),
Size: info.Size(),
IsDir: info.IsDir(),
}
return nil
})
}
// logDebug logs a debug message with the watcher's directory prefix
func (w *Watcher) logDebug(format string, args ...any) {
w.log.Debug("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...))
}
// logInfo logs an info message with the watcher's directory prefix
func (w *Watcher) logInfo(format string, args ...any) {
w.log.Info("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...))
}
// logWarning logs a warning message with the watcher's directory prefix
func (w *Watcher) logWarning(format string, args ...any) {
w.log.Warning("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...))
}
// logError logs an error message with the watcher's directory prefix
func (w *Watcher) logError(format string, args ...any) {
w.log.Error("[Watcher] [%s] %s", w.dir, fmt.Sprintf(format, args...))
}
func (w *Watcher) checkForChanges() (bool, error) {
// Get current state
currentFiles := make(map[string]FileInfo)
err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
// File might have been deleted between directory read and stat
return nil
}
// Skip if not recursive and this is a subdirectory
if !w.recursive && info.IsDir() && path != w.dir {
return filepath.SkipDir
}
currentFiles[path] = FileInfo{
ModTime: info.ModTime(),
Size: info.Size(),
IsDir: info.IsDir(),
}
return nil
})
if err != nil {
return false, err
}
// Compare with previous state
w.filesMu.RLock()
previousFiles := w.files
w.filesMu.RUnlock()
// Check for different file count (quick check)
if len(currentFiles) != len(previousFiles) {
w.logDebug("File count changed: %d -> %d", len(previousFiles), len(currentFiles))
w.updateFiles(currentFiles)
return true, nil
}
// Check for modified, added, or removed files
for path, currentInfo := range currentFiles {
prevInfo, exists := previousFiles[path]
if !exists {
// New file
w.logDebug("New file detected: %s", path)
w.updateFiles(currentFiles)
return true, nil
}
if currentInfo.ModTime != prevInfo.ModTime || currentInfo.Size != prevInfo.Size {
// File modified
w.logDebug("File modified: %s", path)
w.updateFiles(currentFiles)
return true, nil
}
}
// Check for deleted files
for path := range previousFiles {
if _, exists := currentFiles[path]; !exists {
// File deleted
w.logDebug("File deleted: %s", path)
w.updateFiles(currentFiles)
return true, nil
}
}
// No changes detected
return false, nil
}
// updateFiles updates the internal file list
func (w *Watcher) updateFiles(newFiles map[string]FileInfo) {
w.filesMu.Lock()
w.files = newFiles
w.filesMu.Unlock()
}

View File

@ -13,37 +13,9 @@ import (
"git.sharkk.net/Sky/Moonshark/core/logger"
"git.sharkk.net/Sky/Moonshark/core/routers"
"git.sharkk.net/Sky/Moonshark/core/utils"
"git.sharkk.net/Sky/Moonshark/core/watchers"
"git.sharkk.net/Sky/Moonshark/core/workers"
)
// initRouters sets up the Lua and static routers
func initRouters(routesDir, staticDir string, log *logger.Logger) (*routers.LuaRouter, *routers.StaticRouter, error) {
// Ensure directories exist
if err := utils.EnsureDir(routesDir); err != nil {
return nil, nil, fmt.Errorf("routes directory doesn't exist, and could not create it: %v", err)
}
if err := utils.EnsureDir(staticDir); err != nil {
return nil, nil, fmt.Errorf("static directory doesn't exist, and could not create it: %v", err)
}
// Initialize Lua router for dynamic routes
luaRouter, err := routers.NewLuaRouter(routesDir)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize Lua router: %v", err)
}
log.Info("Lua router initialized with routes from %s", routesDir)
// Initialize static file router
staticRouter, err := routers.NewStaticRouter(staticDir)
if err != nil {
return nil, nil, fmt.Errorf("failed to initialize static router: %v", err)
}
log.Info("Static router initialized with files from %s", staticDir)
return luaRouter, staticRouter, nil
}
func main() {
// Initialize logger
log := logger.New(logger.LevelDebug, true)
@ -65,31 +37,18 @@ func main() {
// Initialize routers
routesDir := cfg.GetString("routes_dir", "./routes")
staticDir := cfg.GetString("static_dir", "./static")
luaRouter, staticRouter, err := initRouters(routesDir, staticDir, log)
if err != nil {
log.Fatal("Router initialization failed: %v", err)
}
// Set up file watchers for automatic reloading
luaWatcher, err := watchers.WatchLuaRouter(luaRouter, routesDir, log)
if err != nil {
log.Warning("Failed to watch routes directory: %v", err)
} else {
defer luaWatcher.Close()
log.Info("File watcher active for Lua routes")
}
staticWatcher, err := watchers.WatchStaticRouter(staticRouter, staticDir, log)
if err != nil {
log.Warning("Failed to watch static directory: %v", err)
} else {
defer staticWatcher.Close()
log.Info("File watcher active for static files")
}
// Get worker pool size from config or use default
workerPoolSize := cfg.GetInt("worker_pool_size", 4)
// Ensure directories exist
if err = utils.EnsureDir(routesDir); err != nil {
log.Fatal("Routes directory doesn't exist, and could not create it: %v", err)
}
if err = utils.EnsureDir(staticDir); err != nil {
log.Fatal("Static directory doesn't exist, and could not create it: %v", err)
}
// Initialize worker pool
pool, err := workers.NewPool(workerPoolSize)
if err != nil {
@ -98,6 +57,20 @@ func main() {
log.Info("Worker pool initialized with %d workers", workerPoolSize)
defer pool.Shutdown()
// Initialize Lua router for dynamic routes
luaRouter, err := routers.NewLuaRouter(routesDir)
if err != nil {
log.Fatal("Failed to initialize Lua router: %v", err)
}
log.Info("Lua router initialized with routes from %s", routesDir)
// Initialize static file router
staticRouter, err := routers.NewStaticRouter(staticDir)
if err != nil {
log.Fatal("Failed to initialize static router: %v", err)
}
log.Info("Static router initialized with files from %s", staticDir)
// Create HTTP server
server := http.New(luaRouter, staticRouter, pool, log)
@ -107,15 +80,15 @@ func main() {
// Start server in a goroutine
go func() {
if err := server.ListenAndServe(fmt.Sprintf(":%d", port)); err != nil {
addr := fmt.Sprintf(":%d", port)
log.Info("Server listening on http://localhost%s", addr)
if err := server.ListenAndServe(addr); err != nil {
if err.Error() != "http: Server closed" {
log.Error("Server error: %v", err)
}
}
}()
log.Info("Server started on port %d", port)
// Wait for interrupt signal
<-stop
log.Info("Shutdown signal received")