You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

152 lines
3.7 KiB

package systemtime
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
// Settings configures clock discipline without waiting for an external server
// or rewriting an already valid clock. Browser bootstrap remains independent.
type Settings struct {
Timezone string `json:"timezone"`
NTPEnabled bool `json:"ntp_enabled"`
Servers []string `json:"servers"`
}
type SettingsController interface {
Configure(context.Context, Settings) error
}
type NTPSettingsApplier interface {
ConfigureNTP(context.Context, bool, []string) error
}
func (c *TransactionalController) Configure(ctx context.Context, settings Settings) error {
zone, err := NormalizeTimezone(settings.Timezone)
if err != nil {
return err
}
if settings.NTPEnabled {
normalized, err := NormalizeNTPRequest(NTPRequest{Timezone: zone, Servers: settings.Servers})
if err != nil {
return err
}
settings.Servers = normalized.Servers
}
if c == nil || c.applier == nil {
return errors.New("time settings applier unavailable")
}
ntp, ok := c.ntpApplier.(NTPSettingsApplier)
if !ok {
return errors.New("NTP settings applier unavailable")
}
c.mu.Lock()
defer c.mu.Unlock()
rollback, err := c.applier.ApplyTimezone(ctx, zone)
if err != nil {
return err
}
if rollback == nil {
return errors.New("timezone rollback missing")
}
if err := ntp.ConfigureNTP(ctx, settings.NTPEnabled, settings.Servers); err != nil {
return errors.Join(err, rollback())
}
return nil
}
func (MockController) Configure(ctx context.Context, settings Settings) error {
_, err := NormalizeTimezone(settings.Timezone)
return err
}
func (a *OSNTPApplier) ConfigureNTP(ctx context.Context, enabled bool, servers []string) error {
var normalized NTPRequest
var err error
if enabled {
normalized, err = NormalizeNTPRequest(NTPRequest{Timezone: "UTC", Servers: servers})
if err != nil {
return err
}
}
previous, mode, existed, err := readExistingNTPConfig(a.configPath)
if err != nil {
return err
}
running, err := a.running()
if err != nil {
return err
}
if running {
if err := a.runner.Run(ctx, a.initPath, "stop"); err != nil {
return fmt.Errorf("stop NTP: %w", err)
}
if err := a.waitNTPState(ctx, false); err != nil {
return err
}
}
if !enabled {
return nil
}
err = writeAtomicFile(a.configPath, buildNTPConfig(normalized.Servers), 0o644)
if err == nil {
err = a.runner.Run(ctx, a.initPath, "start")
if err == nil {
err = a.waitNTPState(ctx, true)
}
}
if err == nil {
return nil
}
// Preserve the previous configuration if the daemon rejects the change.
if existed {
err = errors.Join(err, writeAtomicFile(a.configPath, previous, mode))
} else if removeErr := os.Remove(a.configPath); removeErr != nil && !os.IsNotExist(removeErr) {
err = errors.Join(err, removeErr)
}
return errors.Join(err, a.runRecovery(a.initPath, "start"))
}
func ntpDaemonRunning() (bool, error) {
entries, err := os.ReadDir("/proc")
if err != nil {
return false, err
}
for _, entry := range entries {
if _, err := strconv.Atoi(entry.Name()); err != nil {
continue
}
exe, err := os.Readlink(filepath.Join("/proc", entry.Name(), "exe"))
if err == nil && filepath.Base(strings.TrimSuffix(exe, " (deleted)")) == "ntpd" {
return true, nil
}
}
return false, nil
}
func (a *OSNTPApplier) waitNTPState(ctx context.Context, desired bool) error {
timeout := time.NewTimer(2 * time.Second)
defer timeout.Stop()
for {
running, err := a.running()
if err != nil {
return err
}
if running == desired {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timeout.C:
return errors.New("NTP daemon did not reach configured state")
case <-time.After(50 * time.Millisecond):
}
}
}