Merge PR #1512
This commit is contained in:
commit
08dfb9a4c8
2 changed files with 77 additions and 20 deletions
|
|
@ -2,6 +2,7 @@ package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
@ -522,9 +523,7 @@ func setupConfigWatcherPolling(configPath string, debug bool) (chan *config.Conf
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
// Get initial file info
|
lastFingerprint := getFileFingerprint(configPath)
|
||||||
lastModTime := getFileModTime(configPath)
|
|
||||||
lastSize := getFileSize(configPath)
|
|
||||||
|
|
||||||
ticker := time.NewTicker(2 * time.Second) // Check every 2 seconds
|
ticker := time.NewTicker(2 * time.Second) // Check every 2 seconds
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
@ -532,11 +531,12 @@ func setupConfigWatcherPolling(configPath string, debug bool) (chan *config.Conf
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
currentModTime := getFileModTime(configPath)
|
currentFingerprint := getFileFingerprint(configPath)
|
||||||
currentSize := getFileSize(configPath)
|
|
||||||
|
|
||||||
// Check if file changed (modification time or size changed)
|
// Detect changes by file fingerprint instead of mtime/size alone.
|
||||||
if currentModTime.After(lastModTime) || currentSize != lastSize {
|
// This avoids missing edits that land within the same filesystem
|
||||||
|
// timestamp granularity window and keep the same file size.
|
||||||
|
if currentFingerprint != lastFingerprint {
|
||||||
if debug {
|
if debug {
|
||||||
logger.Debugf("🔍 Config file change detected")
|
logger.Debugf("🔍 Config file change detected")
|
||||||
}
|
}
|
||||||
|
|
@ -562,8 +562,7 @@ func setupConfigWatcherPolling(configPath string, debug bool) (chan *config.Conf
|
||||||
logger.Info("✓ Config file validated and loaded")
|
logger.Info("✓ Config file validated and loaded")
|
||||||
|
|
||||||
// Update last known state
|
// Update last known state
|
||||||
lastModTime = currentModTime
|
lastFingerprint = currentFingerprint
|
||||||
lastSize = currentSize
|
|
||||||
|
|
||||||
// Send new config to main loop (non-blocking)
|
// Send new config to main loop (non-blocking)
|
||||||
select {
|
select {
|
||||||
|
|
@ -588,22 +587,34 @@ func setupConfigWatcherPolling(configPath string, debug bool) (chan *config.Conf
|
||||||
return configChan, stopFunc
|
return configChan, stopFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFileModTime returns the modification time of a file, or zero time if file doesn't exist
|
type fileFingerprint struct {
|
||||||
func getFileModTime(path string) time.Time {
|
ModTime time.Time
|
||||||
info, err := os.Stat(path)
|
Size int64
|
||||||
if err != nil {
|
Hash [32]byte
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
return info.ModTime()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFileSize returns the size of a file, or 0 if file doesn't exist
|
// getFileFingerprint returns a stable fingerprint for config change detection.
|
||||||
func getFileSize(path string) int64 {
|
// Hashing the content closes the gap where mtime granularity and unchanged size
|
||||||
|
// would otherwise hide rapid successive edits from the polling watcher.
|
||||||
|
func getFileFingerprint(path string) fileFingerprint {
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return fileFingerprint{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fileFingerprint{
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
Size: info.Size(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileFingerprint{
|
||||||
|
ModTime: info.ModTime(),
|
||||||
|
Size: info.Size(),
|
||||||
|
Hash: sha256.Sum256(data),
|
||||||
}
|
}
|
||||||
return info.Size()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupCronTool(
|
func setupCronTool(
|
||||||
|
|
|
||||||
46
cmd/picoclaw/internal/gateway/helpers_test.go
Normal file
46
cmd/picoclaw/internal/gateway/helpers_test.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package gateway
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetFileFingerprintDetectsSameSizeSameTimestampChanges(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
configPath := filepath.Join(tmpDir, "config.json")
|
||||||
|
|
||||||
|
initialContent := []byte(`{"model":"aaaa"}`)
|
||||||
|
require.NoError(t, os.WriteFile(configPath, initialContent, 0o600))
|
||||||
|
|
||||||
|
original := getFileFingerprint(configPath)
|
||||||
|
require.NotZero(t, original.ModTime)
|
||||||
|
require.NotZero(t, original.Size)
|
||||||
|
|
||||||
|
updatedContent := []byte(`{"model":"bbbb"}`)
|
||||||
|
require.Len(t, updatedContent, len(initialContent))
|
||||||
|
require.NoError(t, os.WriteFile(configPath, updatedContent, 0o600))
|
||||||
|
|
||||||
|
// Simulate a coarse-grained filesystem timestamp where two writes inside the
|
||||||
|
// same second end up with the same reported modification time.
|
||||||
|
require.NoError(t, os.Chtimes(configPath, original.ModTime, original.ModTime))
|
||||||
|
|
||||||
|
updated := getFileFingerprint(configPath)
|
||||||
|
|
||||||
|
assert.Equal(t, original.ModTime, updated.ModTime)
|
||||||
|
assert.Equal(t, original.Size, updated.Size)
|
||||||
|
assert.NotEqual(t, original.Hash, updated.Hash)
|
||||||
|
assert.NotEqual(t, original, updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetFileFingerprintMissingFile(t *testing.T) {
|
||||||
|
fp := getFileFingerprint(filepath.Join(t.TempDir(), "missing.json"))
|
||||||
|
|
||||||
|
assert.Equal(t, fileFingerprint{}, fp)
|
||||||
|
assert.True(t, fp.ModTime.IsZero())
|
||||||
|
assert.Equal(t, int64(0), fp.Size)
|
||||||
|
assert.Equal(t, [32]byte{}, fp.Hash)
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue