161 lines
4.1 KiB
Go
161 lines
4.1 KiB
Go
package feichibridge
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type VehicleState struct {
|
|
LastRealtimeTime time.Time `json:"last_realtime_time,omitempty"`
|
|
LastRealtimeHash string `json:"last_realtime_hash,omitempty"`
|
|
LastRealtimeACKAt time.Time `json:"last_realtime_ack_at,omitempty"`
|
|
LastSnapshotReissueTime time.Time `json:"last_snapshot_reissue_time,omitempty"`
|
|
LastSnapshotReissueHash string `json:"last_snapshot_reissue_hash,omitempty"`
|
|
LastSnapshotReissueACKAt time.Time `json:"last_snapshot_reissue_ack_at,omitempty"`
|
|
BackfillCursor time.Time `json:"backfill_cursor,omitempty"`
|
|
LastBackfillACKAt time.Time `json:"last_backfill_ack_at,omitempty"`
|
|
}
|
|
|
|
func (s *StateStore) CommitSnapshotReissue(vin string, at time.Time, hash string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
current := s.state.Vehicles[vin]
|
|
current.LastSnapshotReissueTime = at
|
|
current.LastSnapshotReissueHash = hash
|
|
current.LastSnapshotReissueACKAt = time.Now()
|
|
s.state.Vehicles[vin] = current
|
|
return s.saveLocked()
|
|
}
|
|
|
|
type persistentState struct {
|
|
Version int `json:"version"`
|
|
PlatformSerial uint16 `json:"platform_serial"`
|
|
Vehicles map[string]VehicleState `json:"vehicles"`
|
|
}
|
|
|
|
type StateStore struct {
|
|
mu sync.Mutex
|
|
path string
|
|
state persistentState
|
|
}
|
|
|
|
func OpenStateStore(path string) (*StateStore, error) {
|
|
store := &StateStore{
|
|
path: path,
|
|
state: persistentState{
|
|
Version: 1,
|
|
Vehicles: map[string]VehicleState{},
|
|
},
|
|
}
|
|
encoded, err := os.ReadFile(path)
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return store, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read bridge state: %w", err)
|
|
}
|
|
if err := json.Unmarshal(encoded, &store.state); err != nil {
|
|
return nil, fmt.Errorf("decode bridge state: %w", err)
|
|
}
|
|
if store.state.Vehicles == nil {
|
|
store.state.Vehicles = map[string]VehicleState{}
|
|
}
|
|
return store, nil
|
|
}
|
|
|
|
func (s *StateStore) Vehicle(vin string) VehicleState {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.state.Vehicles[vin]
|
|
}
|
|
|
|
func (s *StateStore) CommitRealtime(vin string, at time.Time, hash string) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
current := s.state.Vehicles[vin]
|
|
current.LastRealtimeTime = at
|
|
current.LastRealtimeHash = hash
|
|
current.LastRealtimeACKAt = time.Now()
|
|
s.state.Vehicles[vin] = current
|
|
return s.saveLocked()
|
|
}
|
|
|
|
func (s *StateStore) CommitBackfill(vin string, at time.Time) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
current := s.state.Vehicles[vin]
|
|
current.BackfillCursor = at
|
|
current.LastBackfillACKAt = time.Now()
|
|
s.state.Vehicles[vin] = current
|
|
return s.saveLocked()
|
|
}
|
|
|
|
func (s *StateStore) AdvanceBackfillCursor(vin string, at time.Time) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
current := s.state.Vehicles[vin]
|
|
current.BackfillCursor = at
|
|
s.state.Vehicles[vin] = current
|
|
return s.saveLocked()
|
|
}
|
|
|
|
func (s *StateStore) NextPlatformSerial() (uint16, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.state.PlatformSerial++
|
|
if s.state.PlatformSerial == 0 {
|
|
s.state.PlatformSerial = 1
|
|
}
|
|
if err := s.saveLocked(); err != nil {
|
|
return 0, err
|
|
}
|
|
return s.state.PlatformSerial, nil
|
|
}
|
|
|
|
func (s *StateStore) saveLocked() error {
|
|
if err := os.MkdirAll(filepath.Dir(s.path), 0o750); err != nil {
|
|
return fmt.Errorf("create bridge state directory: %w", err)
|
|
}
|
|
encoded, err := json.MarshalIndent(s.state, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
temp, err := os.CreateTemp(filepath.Dir(s.path), ".state-*.json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tempName := temp.Name()
|
|
defer os.Remove(tempName)
|
|
if err := temp.Chmod(0o600); err != nil {
|
|
temp.Close()
|
|
return err
|
|
}
|
|
if _, err := temp.Write(encoded); err != nil {
|
|
temp.Close()
|
|
return err
|
|
}
|
|
if err := temp.Sync(); err != nil {
|
|
temp.Close()
|
|
return err
|
|
}
|
|
if err := temp.Close(); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(tempName, s.path); err != nil {
|
|
return fmt.Errorf("replace bridge state: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hashBytes(value []byte) string {
|
|
sum := sha256.Sum256(value)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|