Files
kvm/config.go
luckfox-eng29 5e17c52afc Update App version to 0.0.4
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
2025-12-23 11:17:28 +08:00

437 lines
12 KiB
Go

package kvm
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"kvm/internal/logging"
"kvm/internal/network"
"kvm/internal/usbgadget"
)
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 {
STUN string `json:"stun"`
JigglerEnabled bool `json:"jiggler_enabled"`
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"`
ForceHpd bool `json:"force_hpd"` // 强制输出EDID
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 *network.NetworkConfig `json:"network_config"`
AppliedNetworkConfig *network.NetworkConfig `json:"applied_network_config,omitempty"`
DefaultLogLevel string `json:"default_log_level"`
TailScaleAutoStart bool `json:"tailscale_autostart"`
TailScaleXEdge bool `json:"tailscale_xedge"`
ZeroTierNetworkID string `json:"zerotier_network_id"`
ZeroTierAutoStart bool `json:"zerotier_autostart"`
FrpcAutoStart bool `json:"frpc_autostart"`
FrpcToml string `json:"frpc_toml"`
CloudflaredAutoStart bool `json:"cloudflared_autostart"`
CloudflaredToken string `json:"cloudflared_token"`
IO0Status bool `json:"io0_status"`
IO1Status bool `json:"io1_status"`
AudioMode string `json:"audio_mode"`
TimeZone string `json:"time_zone"`
LEDGreenMode string `json:"led_green_mode"`
LEDYellowMode string `json:"led_yellow_mode"`
AutoMountSystemInfo bool `json:"auto_mount_system_info_img"`
EasytierAutoStart bool `json:"easytier_autostart"`
EasytierConfig EasytierConfig `json:"easytier_config"`
VntAutoStart bool `json:"vnt_autostart"`
VntConfig VntConfig `json:"vnt_config"`
}
type VntConfig struct {
Token string `json:"token"`
DeviceId string `json:"device_id"`
Name string `json:"name"`
ServerAddr string `json:"server_addr"`
ConfigMode string `json:"config_mode"` // "params" or "file"
ConfigFile string `json:"config_file"`
Model string `json:"model"`
Password string `json:"password"`
}
const configPath = "/userdata/kvm_config.json"
const sdConfigPath = "/mnt/sdcard/kvm_config.json"
var defaultConfig = &Config{
STUN: "stun:stun.l.google.com:19302",
AutoUpdateEnabled: false, // Set a default value
ActiveExtension: "",
KeyboardMacros: []KeyboardMacro{},
DisplayRotation: "180",
TimeZone: "UTC-8",
KeyboardLayout: "en_US",
DisplayMaxBrightness: 64,
DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes
TLSMode: "",
ForceHpd: false, // 默认不强制输出EDID
UsbConfig: &usbgadget.Config{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "KVM",
Product: "USB Emulation Device",
},
UsbDevices: &usbgadget.Devices{
AbsoluteMouse: true,
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
Audio: false, //At any given time, only one of Audio and Mtp can be set to true
Mtp: false,
},
NetworkConfig: &network.NetworkConfig{},
AppliedNetworkConfig: nil,
DefaultLogLevel: "INFO",
ZeroTierAutoStart: false,
TailScaleAutoStart: false,
TailScaleXEdge: false,
FrpcAutoStart: false,
CloudflaredAutoStart: false,
IO0Status: true,
IO1Status: true,
AudioMode: "disabled",
LEDGreenMode: "network-rx",
LEDYellowMode: "kernel-activity",
AutoMountSystemInfo: true,
}
var (
config *Config
configLock = &sync.Mutex{}
)
func LoadConfig() {
configLock.Lock()
defer configLock.Unlock()
if config != nil {
logger.Debug().Msg("config already loaded, skipping")
return
}
// load the default config
if defaultConfig.UsbConfig.SerialNumber == "" {
serialNumber, err := extractSerialNumber()
if err != nil {
logger.Warn().Err(err).Msg("failed to extract serial number")
} else {
defaultConfig.UsbConfig.SerialNumber = serialNumber
}
}
loadedConfig := *defaultConfig
config = &loadedConfig
file, err := os.Open(configPath)
if err != nil {
logger.Debug().Msg("default config file doesn't exist, using default")
return
}
defer file.Close()
// load and merge the default config with the user config
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
logger.Warn().Err(err).Msg("config file JSON parsing failed")
os.Remove(configPath)
if _, err := os.Stat(sdConfigPath); err == nil {
os.Remove(sdConfigPath)
}
return
}
// merge the user config with the default config
if loadedConfig.UsbConfig == nil {
loadedConfig.UsbConfig = defaultConfig.UsbConfig
}
if loadedConfig.UsbDevices == nil {
loadedConfig.UsbDevices = defaultConfig.UsbDevices
}
if loadedConfig.NetworkConfig == nil {
loadedConfig.NetworkConfig = defaultConfig.NetworkConfig
}
config = &loadedConfig
logging.GetRootLogger().UpdateLogLevel(config.DefaultLogLevel)
logger.Info().Str("path", configPath).Msg("config loaded")
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
if cerr := out.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if _, err := io.Copy(out, in); err != nil {
return err
}
if err := out.Sync(); err != nil {
return err
}
return nil
}
func SyncConfigSD(isUpdate bool) {
resp, err := rpcGetSDMountStatus()
if err != nil {
logger.Error().Err(err).Msg("failed to get sd mount status")
return
}
if resp.Status == SDMountOK {
if _, err := os.Stat(configPath); err != nil {
if err := SaveConfig(); err != nil {
logger.Error().Err(err).Msg("failed to create kvm_config.json")
return
}
}
if isUpdate {
if _, err := os.Stat(sdConfigPath); err == nil {
if err := copyFile(sdConfigPath, configPath); err != nil {
logger.Error().Err(err).Msg("failed to copy kvm_config.json from sdcard to userdata")
return
}
} else {
if err := copyFile(configPath, sdConfigPath); err != nil {
logger.Error().Err(err).Msg("failed to copy kvm_config.json from userdata to sdcard")
return
}
}
} else {
if err := copyFile(configPath, sdConfigPath); err != nil {
logger.Error().Err(err).Msg("failed to copy kvm_config.json from userdata to sdcard")
return
}
}
}
}
func SaveConfig() error {
configLock.Lock()
defer configLock.Unlock()
logger.Trace().Str("path", configPath).Msg("Saving config")
file, err := os.Create(configPath)
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)
}
SyncConfigSD(false)
return nil
}
func ensureConfigLoaded() {
if config == nil {
LoadConfig()
}
}
var systemInfoWriteLock sync.Mutex
func writeSystemInfoImg() error {
systemInfoWriteLock.Lock()
defer systemInfoWriteLock.Unlock()
imgPath := filepath.Join(imagesFolder, "system_info.img")
unverifiedimgPath := filepath.Join(imagesFolder, "system_info.img") + ".unverified"
mountPoint := "/mnt/system_info"
run := func(cmd string, args ...string) error {
c := exec.Command(cmd, args...)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c.Run()
}
if _, err := os.Stat(unverifiedimgPath); err == nil {
err := os.Rename(unverifiedimgPath, imgPath)
if err != nil {
return fmt.Errorf("failed to rename %s to %s: %v", unverifiedimgPath, imgPath, err)
}
return nil
}
isMounted := false
if f, err := os.Open("/proc/mounts"); err == nil {
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) >= 2 && fields[1] == mountPoint {
isMounted = true
break
}
}
}
if isMounted {
logger.Info().Msgf("%s is mounted, umounting...\n", mountPoint)
_ = run("umount", mountPoint)
}
if _, err := os.Stat(mountPoint); err == nil {
if err := os.Remove(mountPoint); err != nil {
return fmt.Errorf("failed to remove %s: %v", mountPoint, err)
}
}
if _, err := os.Stat(imgPath); err == nil {
if err := copyFile(imgPath, unverifiedimgPath); err != nil {
logger.Error().Err(err).Msg("failed to copy system_info.img")
return err
}
} else {
if err := run("dd", "if=/dev/zero", "of="+unverifiedimgPath, "bs=1M", "count=4"); err != nil {
return fmt.Errorf("dd failed: %v", err)
}
if err := run("mkfs.vfat", unverifiedimgPath); err != nil {
return fmt.Errorf("mkfs.vfat failed: %v", err)
}
}
if err := os.MkdirAll(mountPoint, 0755); err != nil {
return fmt.Errorf("mkdir failed: %v", err)
}
if err := run("mount", "-o", "loop", unverifiedimgPath, mountPoint); err != nil {
return fmt.Errorf("mount failed: %v", err)
}
if err := run("cp", "/etc/hostname", mountPoint+"/hostname.txt"); err != nil {
return fmt.Errorf("copy hostname failed: %v", err)
}
if err := run("sh", "-c", "ip addr show > "+mountPoint+"/network_info.txt"); err != nil {
return fmt.Errorf("write network info failed: %v", err)
}
_ = run("umount", mountPoint)
if err := os.RemoveAll(mountPoint); err != nil {
return fmt.Errorf("failed to remove %s: %v", mountPoint, err)
}
if err := os.Rename(unverifiedimgPath, imgPath); err != nil {
return fmt.Errorf("failed to rename %s to %s: %v", unverifiedimgPath, imgPath, err)
}
logger.Info().Msg("system_info.img update successfully")
return nil
}