Files
kvm/config.go
Marc Brooks d1027206bc Enhance synctrace logging (#980)
* Enhance synctrace logging.

Switched the maps to be indexed by the .Pointer (not a string)
Grouped the lockCount, unlockCount ,and lastLock in an trackingEntry so we can detect unlocks of something that wasn't ever locked and excessive unlocks and also tracks the first time locked and the last unlock time.
Added LogDangledLocks for debugging use.
Added a panic handler to the Main so we can log out panics

* Switch to traceable sync for most everything

* More documentation

* Update internal/sync/log.go

* Update DEVELOPMENT.md

* Resolve merge issue.

* Applied review comments

* Restore --enable-sync-trace option.

* Use WithLevel so we can re-panic as desired
2026-01-28 09:19:56 +01:00

335 lines
9.4 KiB
Go

package kvm
import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/internal/sync"
"github.com/jetkvm/kvm/internal/usbgadget"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
const (
DefaultAPIURL = "https://api.jetkvm.com"
)
type WakeOnLanDevice struct {
Name string `json:"name"`
MacAddress string `json:"macAddress"`
}
// Constants for keyboard macro limits
const (
MaxMacrosPerDevice = 25
MaxStepsPerMacro = 10
MaxKeysPerStep = 10
MinStepDelay = 50
MaxStepDelay = 2000
)
type KeyboardMacroStep struct {
Keys []string `json:"keys"`
Modifiers []string `json:"modifiers"`
Delay int `json:"delay"`
}
func (s *KeyboardMacroStep) Validate() error {
if len(s.Keys) > MaxKeysPerStep {
return fmt.Errorf("too many keys in step (max %d)", MaxKeysPerStep)
}
if s.Delay < MinStepDelay {
s.Delay = MinStepDelay
} else if s.Delay > MaxStepDelay {
s.Delay = MaxStepDelay
}
return nil
}
type KeyboardMacro struct {
ID string `json:"id"`
Name string `json:"name"`
Steps []KeyboardMacroStep `json:"steps"`
SortOrder int `json:"sortOrder,omitempty"`
}
func (m *KeyboardMacro) Validate() error {
if m.Name == "" {
return fmt.Errorf("macro name cannot be empty")
}
if len(m.Steps) == 0 {
return fmt.Errorf("macro must have at least one step")
}
if len(m.Steps) > MaxStepsPerMacro {
return fmt.Errorf("too many steps in macro (max %d)", MaxStepsPerMacro)
}
for i := range m.Steps {
if err := m.Steps[i].Validate(); err != nil {
return fmt.Errorf("invalid step %d: %w", i+1, err)
}
}
return nil
}
type Config struct {
CloudURL string `json:"cloud_url"`
UpdateAPIURL string `json:"update_api_url"`
CloudAppURL string `json:"cloud_app_url"`
CloudToken string `json:"cloud_token"`
GoogleIdentity string `json:"google_identity"`
JigglerEnabled bool `json:"jiggler_enabled"`
JigglerConfig *JigglerConfig `json:"jiggler_config"`
AutoUpdateEnabled bool `json:"auto_update_enabled"`
IncludePreRelease bool `json:"include_pre_release"`
HashedPassword string `json:"hashed_password"`
LocalAuthToken string `json:"local_auth_token"`
LocalAuthMode string `json:"localAuthMode"` //TODO: fix it with migration
LocalLoopbackOnly bool `json:"local_loopback_only"`
WakeOnLanDevices []WakeOnLanDevice `json:"wake_on_lan_devices"`
KeyboardMacros []KeyboardMacro `json:"keyboard_macros"`
KeyboardLayout string `json:"keyboard_layout"`
EdidString string `json:"hdmi_edid_string"`
ActiveExtension string `json:"active_extension"`
DisplayRotation string `json:"display_rotation"`
DisplayMaxBrightness int `json:"display_max_brightness"`
DisplayDimAfterSec int `json:"display_dim_after_sec"`
DisplayOffAfterSec int `json:"display_off_after_sec"`
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
UsbConfig *usbgadget.Config `json:"usb_config"`
UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *types.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
VideoQualityFactor float64 `json:"video_quality_factor"`
NativeMaxRestart uint `json:"native_max_restart_attempts"`
}
// GetUpdateAPIURL returns the update API URL
func (c *Config) GetUpdateAPIURL() string {
if c.UpdateAPIURL == "" {
return DefaultAPIURL
}
return strings.TrimSuffix(c.UpdateAPIURL, "/") + "/releases"
}
// GetDisplayRotation returns the display rotation
func (c *Config) GetDisplayRotation() uint16 {
rotationInt, err := strconv.ParseUint(c.DisplayRotation, 10, 16)
if err != nil {
logger.Warn().Err(err).Msg("invalid display rotation, using default")
return 270
}
return uint16(rotationInt)
}
// SetDisplayRotation sets the display rotation
func (c *Config) SetDisplayRotation(rotation string) error {
_, err := strconv.ParseUint(rotation, 10, 16)
if err != nil {
logger.Warn().Err(err).Msg("invalid display rotation, using default")
return err
}
c.DisplayRotation = rotation
return nil
}
const configPath = "/userdata/kvm_config.json"
// it's a temporary solution to avoid sharing the same pointer
// we should migrate to a proper config solution in the future
var (
defaultJigglerConfig = JigglerConfig{
InactivityLimitSeconds: 60,
JitterPercentage: 25,
ScheduleCronTab: "0 * * * * *",
Timezone: "UTC",
}
defaultUsbConfig = usbgadget.Config{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
}
defaultUsbDevices = usbgadget.Devices{
AbsoluteMouse: true,
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
}
)
func getDefaultConfig() Config {
return Config{
CloudURL: DefaultAPIURL,
UpdateAPIURL: DefaultAPIURL,
CloudAppURL: "https://app.jetkvm.com",
AutoUpdateEnabled: true, // Set a default value
ActiveExtension: "",
KeyboardMacros: []KeyboardMacro{},
DisplayRotation: "270",
KeyboardLayout: "en-US",
DisplayMaxBrightness: 64,
DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes
JigglerEnabled: false,
// This is the "Standard" jiggler option in the UI
JigglerConfig: func() *JigglerConfig { c := defaultJigglerConfig; return &c }(),
TLSMode: "",
UsbConfig: func() *usbgadget.Config { c := defaultUsbConfig; return &c }(),
UsbDevices: func() *usbgadget.Devices { c := defaultUsbDevices; return &c }(),
NetworkConfig: func() *types.NetworkConfig {
c := &types.NetworkConfig{}
_ = confparser.SetDefaultsAndValidate(c)
return c
}(),
DefaultLogLevel: "WARN",
VideoQualityFactor: 1.0,
}
}
var (
config *Config
configLock = &sync.Mutex{}
)
var (
configSuccess = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_config_last_reload_successful",
Help: "The last configuration load succeeded",
},
)
configSuccessTime = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_config_last_reload_success_timestamp_seconds",
Help: "Timestamp of last successful config load",
},
)
)
func LoadConfig() {
configLock.Lock()
defer configLock.Unlock()
if config != nil {
logger.Debug().Msg("config already loaded, skipping")
return
}
// load the default config
defaultConfig := getDefaultConfig()
config = &defaultConfig
file, err := os.Open(configPath)
if err != nil {
logger.Debug().Msg("default config file doesn't exist, using default")
configSuccess.Set(1.0)
configSuccessTime.SetToCurrentTime()
return
}
defer file.Close()
// load and merge the default config with the user config
loadedConfig := defaultConfig
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
logger.Warn().Err(err).Msg("config file JSON parsing failed")
configSuccess.Set(0.0)
return
}
// merge the user config with the default config
if loadedConfig.UsbConfig == nil {
loadedConfig.UsbConfig = getDefaultConfig().UsbConfig
}
if loadedConfig.UsbDevices == nil {
loadedConfig.UsbDevices = getDefaultConfig().UsbDevices
}
if loadedConfig.NetworkConfig == nil {
loadedConfig.NetworkConfig = getDefaultConfig().NetworkConfig
}
if loadedConfig.JigglerConfig == nil {
loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig
}
// fixup old keyboard layout value
if loadedConfig.KeyboardLayout == "en_US" {
loadedConfig.KeyboardLayout = "en-US"
}
// Migrate old verbose log level to sensible default
if loadedConfig.DefaultLogLevel == "INFO" {
loadedConfig.DefaultLogLevel = "WARN"
}
config = &loadedConfig
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
configSuccess.Set(1.0)
configSuccessTime.SetToCurrentTime()
logger.Info().Str("path", configPath).Msg("config loaded")
}
func SaveConfig() error {
return saveConfig(configPath)
}
func SaveBackupConfig() error {
return saveConfig(configPath + ".bak")
}
func saveConfig(path string) error {
configLock.Lock()
defer configLock.Unlock()
logger.Trace().Str("path", path).Msg("Saving config")
// fixup old keyboard layout value
if config.KeyboardLayout == "en_US" {
config.KeyboardLayout = "en-US"
}
file, err := os.Create(path)
if err != nil {
return fmt.Errorf("failed to create config file: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
if err := encoder.Encode(config); err != nil {
return fmt.Errorf("failed to encode config: %w", err)
}
if err := file.Sync(); err != nil {
return fmt.Errorf("failed to wite config: %w", err)
}
logger.Info().Str("path", path).Msg("config saved")
return nil
}
func ensureConfigLoaded() {
if config == nil {
LoadConfig()
}
}