initial: Syntrex extraction from sentinel-community (615 files)

This commit is contained in:
DmitrL-dev 2026-03-11 15:12:02 +10:00
commit 2c50c993b1
175 changed files with 32396 additions and 0 deletions

View file

@ -0,0 +1,94 @@
// Package lifecycle manages graceful shutdown with auto-save of session state,
// cache flush, and database closure.
package lifecycle
import (
"context"
"io"
"log"
"sync"
"time"
)
// ShutdownFunc is a function called during graceful shutdown.
// Name is used for logging. The function receives a context with a deadline.
type ShutdownFunc struct {
Name string
Fn func(ctx context.Context) error
}
// Manager orchestrates graceful shutdown of all resources.
type Manager struct {
mu sync.Mutex
hooks []ShutdownFunc
timeout time.Duration
done bool
}
// NewManager creates a new lifecycle Manager.
// Timeout is the maximum time allowed for all shutdown hooks to complete.
func NewManager(timeout time.Duration) *Manager {
if timeout <= 0 {
timeout = 10 * time.Second
}
return &Manager{
timeout: timeout,
}
}
// OnShutdown registers a shutdown hook. Hooks are called in LIFO order
// (last registered = first called), matching defer semantics.
func (m *Manager) OnShutdown(name string, fn func(ctx context.Context) error) {
m.mu.Lock()
defer m.mu.Unlock()
m.hooks = append(m.hooks, ShutdownFunc{Name: name, Fn: fn})
}
// OnClose registers an io.Closer as a shutdown hook.
func (m *Manager) OnClose(name string, c io.Closer) {
m.OnShutdown(name, func(_ context.Context) error {
return c.Close()
})
}
// Shutdown executes all registered hooks in reverse order (LIFO).
// It logs each step and any errors. Returns the first error encountered.
func (m *Manager) Shutdown() error {
m.mu.Lock()
if m.done {
m.mu.Unlock()
return nil
}
m.done = true
hooks := make([]ShutdownFunc, len(m.hooks))
copy(hooks, m.hooks)
m.mu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
defer cancel()
log.Printf("Graceful shutdown started (%d hooks, timeout %s)", len(hooks), m.timeout)
var firstErr error
// Execute in reverse order (LIFO).
for i := len(hooks) - 1; i >= 0; i-- {
h := hooks[i]
log.Printf(" shutdown: %s", h.Name)
if err := h.Fn(ctx); err != nil {
log.Printf(" shutdown %s: ERROR: %v", h.Name, err)
if firstErr == nil {
firstErr = err
}
}
}
log.Printf("Graceful shutdown complete")
return firstErr
}
// Done returns true if Shutdown has already been called.
func (m *Manager) Done() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.done
}

View file

@ -0,0 +1,125 @@
package lifecycle
import (
"context"
"errors"
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewManager_Defaults(t *testing.T) {
m := NewManager(0)
require.NotNil(t, m)
assert.Equal(t, 10*time.Second, m.timeout)
assert.False(t, m.Done())
}
func TestNewManager_CustomTimeout(t *testing.T) {
m := NewManager(5 * time.Second)
assert.Equal(t, 5*time.Second, m.timeout)
}
func TestManager_Shutdown_LIFO(t *testing.T) {
m := NewManager(5 * time.Second)
order := []string{}
m.OnShutdown("first", func(_ context.Context) error {
order = append(order, "first")
return nil
})
m.OnShutdown("second", func(_ context.Context) error {
order = append(order, "second")
return nil
})
m.OnShutdown("third", func(_ context.Context) error {
order = append(order, "third")
return nil
})
err := m.Shutdown()
require.NoError(t, err)
assert.Equal(t, []string{"third", "second", "first"}, order)
assert.True(t, m.Done())
}
func TestManager_Shutdown_Idempotent(t *testing.T) {
m := NewManager(5 * time.Second)
count := 0
m.OnShutdown("counter", func(_ context.Context) error {
count++
return nil
})
_ = m.Shutdown()
_ = m.Shutdown()
_ = m.Shutdown()
assert.Equal(t, 1, count)
}
func TestManager_Shutdown_ReturnsFirstError(t *testing.T) {
m := NewManager(5 * time.Second)
errFirst := errors.New("first error")
errSecond := errors.New("second error")
m.OnShutdown("ok", func(_ context.Context) error { return nil })
m.OnShutdown("fail1", func(_ context.Context) error { return errFirst })
m.OnShutdown("fail2", func(_ context.Context) error { return errSecond })
// LIFO: fail2 runs first, then fail1, then ok.
err := m.Shutdown()
assert.Equal(t, errSecond, err)
}
func TestManager_Shutdown_ContinuesOnError(t *testing.T) {
m := NewManager(5 * time.Second)
reached := false
m.OnShutdown("will-run", func(_ context.Context) error {
reached = true
return nil
})
m.OnShutdown("will-fail", func(_ context.Context) error {
return errors.New("fail")
})
_ = m.Shutdown()
assert.True(t, reached, "hook after error should still run")
}
type mockCloser struct {
closed bool
}
func (m *mockCloser) Close() error {
m.closed = true
return nil
}
func TestManager_OnClose(t *testing.T) {
m := NewManager(5 * time.Second)
mc := &mockCloser{}
m.OnClose("mock-closer", mc)
_ = m.Shutdown()
assert.True(t, mc.closed)
}
func TestManager_OnClose_Interface(t *testing.T) {
m := NewManager(5 * time.Second)
// Verify OnClose accepts io.Closer interface.
var c io.Closer = &mockCloser{}
m.OnClose("io-closer", c)
err := m.Shutdown()
require.NoError(t, err)
}
func TestManager_EmptyShutdown(t *testing.T) {
m := NewManager(5 * time.Second)
err := m.Shutdown()
require.NoError(t, err)
assert.True(t, m.Done())
}

View file

@ -0,0 +1,75 @@
package lifecycle
import (
"crypto/rand"
"fmt"
"log"
"os"
)
// ShredDatabase irreversibly destroys a database file by overwriting
// its header with random bytes, making it unreadable without backup.
//
// For SQLite: overwrites first 100 bytes (header with magic bytes "SQLite format 3\000").
// For BoltDB: overwrites first 4096 bytes (two 4KB meta pages).
//
// WARNING: This operation is IRREVERSIBLE. Data is only recoverable from peer backup.
func ShredDatabase(dbPath string, headerSize int) error {
f, err := os.OpenFile(dbPath, os.O_WRONLY, 0)
if err != nil {
return fmt.Errorf("shred: open %s: %w", dbPath, err)
}
defer f.Close()
// Overwrite header with random bytes.
noise := make([]byte, headerSize)
if _, err := rand.Read(noise); err != nil {
return fmt.Errorf("shred: random: %w", err)
}
if _, err := f.WriteAt(noise, 0); err != nil {
return fmt.Errorf("shred: write %s: %w", dbPath, err)
}
// Force flush to disk.
if err := f.Sync(); err != nil {
return fmt.Errorf("shred: sync %s: %w", dbPath, err)
}
log.Printf("SHRED: %s header (%d bytes) destroyed", dbPath, headerSize)
return nil
}
// ShredSQLite shreds a SQLite database (100-byte header).
func ShredSQLite(dbPath string) error {
return ShredDatabase(dbPath, 100)
}
// ShredBoltDB shreds a BoltDB database (4096-byte meta pages).
func ShredBoltDB(dbPath string) error {
return ShredDatabase(dbPath, 4096)
}
// ShredAll shreds all known database files in the .rlm directory.
func ShredAll(rlmDir string) []error {
var errs []error
sqlitePath := rlmDir + "/memory/memory_bridge_v2.db"
if _, err := os.Stat(sqlitePath); err == nil {
if err := ShredSQLite(sqlitePath); err != nil {
errs = append(errs, err)
}
}
boltPath := rlmDir + "/cache.db"
if _, err := os.Stat(boltPath); err == nil {
if err := ShredBoltDB(boltPath); err != nil {
errs = append(errs, err)
}
}
if len(errs) == 0 {
log.Printf("SHRED: All databases destroyed in %s", rlmDir)
}
return errs
}

View file

@ -0,0 +1,75 @@
package lifecycle
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestShredSQLite(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "test.db")
// Create fake SQLite file with magic header.
header := []byte("SQLite format 3\x00")
data := make([]byte, 4096)
copy(data, header)
require.NoError(t, os.WriteFile(dbPath, data, 0644))
// Verify magic exists.
content, _ := os.ReadFile(dbPath)
assert.Equal(t, "SQLite format 3", string(content[:15]))
// Shred.
err := ShredSQLite(dbPath)
assert.NoError(t, err)
// Verify magic is destroyed.
content, _ = os.ReadFile(dbPath)
assert.NotEqual(t, "SQLite format 3", string(content[:15]),
"SQLite header should be shredded")
assert.Len(t, content, 4096, "file size should not change")
}
func TestShredBoltDB(t *testing.T) {
dir := t.TempDir()
dbPath := filepath.Join(dir, "cache.db")
// Create fake BoltDB file.
data := make([]byte, 8192) // 2 pages
copy(data, []byte("BOLT\x00\x00"))
require.NoError(t, os.WriteFile(dbPath, data, 0644))
err := ShredBoltDB(dbPath)
assert.NoError(t, err)
content, _ := os.ReadFile(dbPath)
assert.NotEqual(t, "BOLT", string(content[:4]),
"BoltDB header should be shredded")
}
func TestShredAll(t *testing.T) {
dir := t.TempDir()
// Create directory structure.
memDir := filepath.Join(dir, "memory")
os.MkdirAll(memDir, 0755)
// Create fake databases.
os.WriteFile(filepath.Join(memDir, "memory_bridge_v2.db"),
make([]byte, 4096), 0644)
os.WriteFile(filepath.Join(dir, "cache.db"),
make([]byte, 8192), 0644)
errs := ShredAll(dir)
assert.Empty(t, errs, "should shred without errors")
}
func TestShred_NonexistentFile(t *testing.T) {
err := ShredSQLite("/nonexistent/path/db.sqlite")
assert.Error(t, err, "should error on nonexistent file")
}