mirror of
https://github.com/jetkvm/kvm.git
synced 2026-05-21 05:20:35 +00:00
d1027206bc
* 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
335 lines
9.4 KiB
Go
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()
|
|
}
|
|
}
|