Compare commits

...

9 Commits

Author SHA1 Message Date
Aveline 99faae710f
Merge aee1a01cee into cc9ff74276 2025-10-10 23:08:53 +00:00
Siyuan aee1a01cee fix: symlink handling 2025-10-10 23:08:27 +00:00
Siyuan ae77887ab2 fix: symlink direction 2025-10-10 22:58:09 +00:00
Siyuan aa9d78998f fix: dhcpc button doesnt work 2025-10-10 22:56:27 +00:00
Siyuan a3f7b5e937 refactor: rename error dump file 2025-10-10 22:54:35 +00:00
Siyuan ff81768b88 fix: fix field reference in confparser 2025-10-10 22:47:14 +00:00
Siyuan d02ae062e4 fix: make timesync non-blocking 2025-10-10 22:44:57 +00:00
Siyuan 02382e4632 fix: mDNS options 2025-10-10 22:37:09 +00:00
Siyuan 775b0f1049 fix: reset config 2025-10-10 21:29:21 +00:00
15 changed files with 215 additions and 112 deletions

View File

@ -16,10 +16,10 @@ import (
)
const (
envChildID = "JETKVM_CHILD_ID"
errorDumpDir = "/userdata/jetkvm/"
errorDumpStateFile = ".has_error_dump"
errorDumpTemplate = "jetkvm-%s.log"
envChildID = "JETKVM_CHILD_ID"
errorDumpDir = "/userdata/jetkvm/"
errorDumpLastFile = "last-crash.log"
errorDumpTemplate = "jetkvm-%s.log"
)
func program() {
@ -117,53 +117,47 @@ func supervise() error {
return nil
}
func isSymlinkTo(dst, src string) bool {
file, err := os.Stat(dst)
func isSymlinkTo(oldName, newName string) bool {
file, err := os.Stat(newName)
if err != nil {
return false
}
if file.Mode()&os.ModeSymlink != os.ModeSymlink {
return false
}
target, err := os.Readlink(dst)
target, err := os.Readlink(newName)
if err != nil {
return false
}
return target == src
return target == oldName
}
func ensureSymlink(dst, src string) error {
if isSymlinkTo(dst, src) {
func ensureSymlink(oldName, newName string) error {
if isSymlinkTo(oldName, newName) {
return nil
}
_ = os.Remove(dst)
return os.Symlink(src, dst)
_ = os.Remove(newName)
return os.Symlink(oldName, newName)
}
func createErrorDump(logFile *os.File) {
logFile.Close()
func renameFile(f *os.File, newName string) error {
_ = f.Close()
// touch the error dump state file
if err := os.WriteFile(filepath.Join(errorDumpDir, errorDumpStateFile), []byte{}, 0644); err != nil {
return
// try to rename the file first
if err := os.Rename(f.Name(), newName); err == nil {
return nil
}
fileName := fmt.Sprintf(errorDumpTemplate, time.Now().Format("20060102150405"))
filePath := filepath.Join(errorDumpDir, fileName)
if err := os.Rename(logFile.Name(), filePath); err == nil {
fmt.Printf("error dump created: %s\n", filePath)
return
}
fnSrc, err := os.Open(logFile.Name())
// copy the log file to the error dump directory
fnSrc, err := os.Open(f.Name())
if err != nil {
return
return fmt.Errorf("failed to open file: %w", err)
}
defer fnSrc.Close()
fnDst, err := os.Create(filePath)
fnDst, err := os.Create(newName)
if err != nil {
return
return fmt.Errorf("failed to create file: %w", err)
}
defer fnDst.Close()
@ -171,20 +165,42 @@ func createErrorDump(logFile *os.File) {
for {
n, err := fnSrc.Read(buf)
if err != nil && err != io.EOF {
return
return fmt.Errorf("failed to read file: %w", err)
}
if n == 0 {
break
}
if _, err := fnDst.Write(buf[:n]); err != nil {
return
return fmt.Errorf("failed to write file: %w", err)
}
}
fmt.Printf("error dump created: %s\n", filePath)
return nil
}
_ = ensureSymlink(filePath, filepath.Join(errorDumpDir, "last-crash.log"))
func createErrorDump(logFile *os.File) {
fmt.Println()
fileName := fmt.Sprintf(
errorDumpTemplate,
time.Now().Format("20060102-150405"),
)
filePath := filepath.Join(errorDumpDir, fileName)
if err := renameFile(logFile, filePath); err != nil {
fmt.Printf("failed to rename file: %v\n", err)
return
}
fmt.Printf("error dump copied: %s\n", filePath)
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
if err := ensureSymlink(filePath, lastFilePath); err != nil {
fmt.Printf("failed to create symlink: %v\n", err)
return
}
}
func doSupervise() {

View File

@ -7,6 +7,7 @@ import (
"strconv"
"sync"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/internal/usbgadget"
@ -128,41 +129,55 @@ func (c *Config) SetDisplayRotation(rotation string) error {
const configPath = "/userdata/kvm_config.json"
var defaultConfig = &Config{
CloudURL: "https://api.jetkvm.com",
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: &JigglerConfig{
// 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",
},
TLSMode: "",
UsbConfig: &usbgadget.Config{
}
defaultUsbConfig = usbgadget.Config{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
},
UsbDevices: &usbgadget.Devices{
}
defaultUsbDevices = usbgadget.Devices{
AbsoluteMouse: true,
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
},
NetworkConfig: &types.NetworkConfig{},
DefaultLogLevel: "INFO",
}
)
func getDefaultConfig() Config {
return Config{
CloudURL: "https://api.jetkvm.com",
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: "INFO",
}
}
var (
@ -195,7 +210,8 @@ func LoadConfig() {
}
// load the default config
config = defaultConfig
defaultConfig := getDefaultConfig()
config = &defaultConfig
file, err := os.Open(configPath)
if err != nil {
@ -207,7 +223,7 @@ func LoadConfig() {
defer file.Close()
// load and merge the default config with the user config
loadedConfig := *defaultConfig
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)
@ -216,19 +232,19 @@ func LoadConfig() {
// merge the user config with the default config
if loadedConfig.UsbConfig == nil {
loadedConfig.UsbConfig = defaultConfig.UsbConfig
loadedConfig.UsbConfig = getDefaultConfig().UsbConfig
}
if loadedConfig.UsbDevices == nil {
loadedConfig.UsbDevices = defaultConfig.UsbDevices
loadedConfig.UsbDevices = getDefaultConfig().UsbDevices
}
if loadedConfig.NetworkConfig == nil {
loadedConfig.NetworkConfig = defaultConfig.NetworkConfig
loadedConfig.NetworkConfig = getDefaultConfig().NetworkConfig
}
if loadedConfig.JigglerConfig == nil {
loadedConfig.JigglerConfig = defaultConfig.JigglerConfig
loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig
}
// fixup old keyboard layout value

View File

@ -381,28 +381,28 @@ func (f *FieldConfig) validateSingleValue(val string, index int) error {
switch validateType {
case "int":
if _, err := strconv.Atoi(val); err != nil {
return fmt.Errorf("field `%s` is not a valid integer: %s", f.Name, val)
return fmt.Errorf("field `%s` is not a valid integer: %s", fieldRef, val)
}
case "ipv6_prefix_length":
valInt, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", f.Name, val)
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", fieldRef, val)
}
if valInt < 0 || valInt > 128 {
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", f.Name, val)
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", fieldRef, val)
}
case "ipv4":
if net.ParseIP(val).To4() == nil {
return fmt.Errorf("%s is not a valid IPv4 address: %s", fieldRef, val)
return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", fieldRef, val)
}
case "ipv6":
if net.ParseIP(val).To16() == nil {
return fmt.Errorf("%s is not a valid IPv6 address: %s", fieldRef, val)
return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", fieldRef, val)
}
case "ipv6_prefix":
if i, _, err := net.ParseCIDR(val); err != nil {
if i.To16() == nil {
return fmt.Errorf("%s is not a valid IPv6 prefix: %s", fieldRef, val)
return fmt.Errorf("field `%s` is not a valid IPv6 prefix: %s", fieldRef, val)
}
}
case "ipv4_or_ipv6":
@ -430,7 +430,7 @@ func (f *FieldConfig) validateSingleValue(val string, index int) error {
return fmt.Errorf("%s is not a valid CIDR notation: %s", fieldRef, val)
}
default:
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", fieldRef, validateType)
}
}

View File

@ -146,14 +146,17 @@ func (m *MDNS) start(allowRestart bool) error {
return nil
}
// Start starts the mDNS server
func (m *MDNS) Start() error {
return m.start(false)
}
// Restart restarts the mDNS server
func (m *MDNS) Restart() error {
return m.start(true)
}
// Stop stops the mDNS server
func (m *MDNS) Stop() error {
m.lock.Lock()
defer m.lock.Unlock()
@ -165,26 +168,46 @@ func (m *MDNS) Stop() error {
return m.conn.Close()
}
func (m *MDNS) SetLocalNames(localNames []string, always bool) error {
if reflect.DeepEqual(m.localNames, localNames) && !always {
return nil
func (m *MDNS) setLocalNames(localNames []string) {
m.lock.Lock()
defer m.lock.Unlock()
if reflect.DeepEqual(m.localNames, localNames) {
return
}
m.localNames = localNames
_ = m.Restart()
return nil
return
}
func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error {
func (m *MDNS) setListenOptions(listenOptions *MDNSListenOptions) {
m.lock.Lock()
defer m.lock.Unlock()
if m.listenOptions != nil &&
m.listenOptions.IPv4 == listenOptions.IPv4 &&
m.listenOptions.IPv6 == listenOptions.IPv6 {
return nil
return
}
m.listenOptions = listenOptions
_ = m.Restart()
return nil
}
// SetLocalNames sets the local names and restarts the mDNS server
func (m *MDNS) SetLocalNames(localNames []string) error {
m.setLocalNames(localNames)
return m.Restart()
}
// SetListenOptions sets the listen options and restarts the mDNS server
func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error {
m.setListenOptions(listenOptions)
return m.Restart()
}
// SetOptions sets the local names and listen options and restarts the mDNS server
func (m *MDNS) SetOptions(options *MDNSOptions) error {
m.setLocalNames(options.LocalNames)
m.setListenOptions(options.ListenOptions)
return m.Restart()
}

File diff suppressed because one or more lines are too long

View File

@ -48,6 +48,10 @@ void action_switch_to_reset_config(lv_event_t *e) {
loadScreen(SCREEN_ID_RESET_CONFIG_SCREEN);
}
void action_switch_to_dhcpc(lv_event_t *e) {
loadScreen(SCREEN_ID_SWITCH_DHCP_CLIENT_SCREEN);
}
void action_switch_to_reboot(lv_event_t *e) {
loadScreen(SCREEN_ID_REBOOT_SCREEN);
}

View File

@ -25,6 +25,7 @@ extern void action_reset_config(lv_event_t * e);
extern void action_reboot(lv_event_t * e);
extern void action_switch_to_reboot(lv_event_t * e);
extern void action_dhcpc(lv_event_t * e);
extern void action_switch_to_dhcpc(lv_event_t * e);
#ifdef __cplusplus

View File

@ -893,7 +893,7 @@ void create_screen_menu_advanced_screen() {
objects.menu_btn_dhcp_client = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), 50);
lv_obj_add_event_cb(obj, action_switch_to_reboot, LV_EVENT_PRESSED, (void *)0);
lv_obj_add_event_cb(obj, action_switch_to_dhcpc, LV_EVENT_PRESSED, (void *)0);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SNAPPABLE);
add_style_menu_button(obj);
{
@ -2278,7 +2278,7 @@ void create_screen_switch_dhcp_client_screen() {
lv_obj_set_pos(obj, LV_PCT(0), LV_PCT(0));
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
add_style_header_link(obj);
lv_label_set_text(obj, "Reset Config");
lv_label_set_text(obj, "DHCP Client");
}
}
}

View File

@ -720,7 +720,8 @@ func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error {
}
func rpcResetConfig() error {
config = defaultConfig
defaultConfig := getDefaultConfig()
config = &defaultConfig
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to reset config: %w", err)
}

20
mdns.go
View File

@ -1,23 +1,23 @@
package kvm
import (
"fmt"
"github.com/jetkvm/kvm/internal/mdns"
)
var mDNS *mdns.MDNS
func initMdns() error {
options := getMdnsOptions()
if options == nil {
return fmt.Errorf("failed to get mDNS options")
}
m, err := mdns.NewMDNS(&mdns.MDNSOptions{
Logger: logger,
LocalNames: []string{
"jetkvm", "jetkvm.local",
// networkManager.GetHostname(),
// networkManager.GetFQDN(),
},
ListenOptions: &mdns.MDNSListenOptions{
IPv4: config.NetworkConfig.MDNSMode.String != "disabled",
IPv6: config.NetworkConfig.MDNSMode.String != "disabled",
},
Logger: logger,
LocalNames: options.LocalNames,
ListenOptions: options.ListenOptions,
})
if err != nil {
return err

View File

@ -32,19 +32,47 @@ func toRpcNetworkSettings(config *types.NetworkConfig) *RpcNetworkSettings {
}
}
func getMdnsOptions() *mdns.MDNSOptions {
if networkManager == nil {
return nil
}
var ipv4, ipv6 bool
switch config.NetworkConfig.MDNSMode.String {
case "auto":
ipv4 = true
ipv6 = true
case "ipv4_only":
ipv4 = true
case "ipv6_only":
ipv6 = true
}
return &mdns.MDNSOptions{
LocalNames: []string{
networkManager.Hostname(),
networkManager.FQDN(),
},
ListenOptions: &mdns.MDNSListenOptions{
IPv4: ipv4,
IPv6: ipv6,
},
}
}
func restartMdns() {
if mDNS == nil {
return
}
_ = mDNS.SetListenOptions(&mdns.MDNSListenOptions{
IPv4: config.NetworkConfig.MDNSMode.String != "disabled",
IPv6: config.NetworkConfig.MDNSMode.String != "disabled",
})
_ = mDNS.SetLocalNames([]string{
networkManager.Hostname(),
networkManager.FQDN(),
}, true)
options := getMdnsOptions()
if options == nil {
return
}
if err := mDNS.SetOptions(options); err != nil {
networkLogger.Error().Err(err).Msg("failed to restart mDNS")
}
}
func triggerTimeSyncOnNetworkStateChange() {
@ -63,9 +91,11 @@ func triggerTimeSyncOnNetworkStateChange() {
}
// sync time
if err := timeSync.Sync(); err != nil {
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
}
go func() {
if err := timeSync.Sync(); err != nil {
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
}
}()
}
func networkStateChanged(_ string, state types.InterfaceState) {
@ -115,6 +145,7 @@ func initNetwork() error {
nc := config.NetworkConfig
nm := nmlite.NewNetworkManager(context.Background(), networkLogger)
networkLogger.Info().Interface("networkConfig", nc).Str("hostname", nc.Hostname.String).Str("domain", nc.Domain.String).Msg("initializing network manager")
_ = setHostname(nm, nc.Hostname.String, nc.Domain.String)
nm.SetOnInterfaceStateChange(networkStateChanged)
if err := nm.AddInterface(NetIfName, nc); err != nil {

View File

@ -83,7 +83,7 @@ func (hm *ResolvConfManager) getDomain() string {
}
}
return ""
return "local"
}
func (hm *ResolvConfManager) reconcileHostname() error {

View File

@ -120,12 +120,12 @@ func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool,
}
}
if !compareStringSlices(im.state.IPv4Addresses, ipv4Addresses) {
if !sortAndCompareStringSlices(im.state.IPv4Addresses, ipv4Addresses) {
im.state.IPv4Addresses = ipv4Addresses
stateChanged = true
}
if !compareIPv6AddressSlices(im.state.IPv6Addresses, ipv6Addresses) {
if !sortAndCompareIPv6AddressSlices(im.state.IPv6Addresses, ipv6Addresses) {
im.state.IPv6Addresses = ipv6Addresses
stateChanged = true
}

View File

@ -163,7 +163,10 @@ func (nm *NetworkManager) GetInterfaceState(iface string) (*types.InterfaceState
return nil, err
}
return im.GetState(), nil
state := im.GetState()
state.Hostname = nm.Hostname()
return state, nil
}
// GetInterfaceConfig returns the current configuration of a specific interface

View File

@ -25,7 +25,7 @@ func lifetimeToTime(lifetime int) *time.Time {
return &t
}
func compareStringSlices(a, b []string) bool {
func sortAndCompareStringSlices(a, b []string) bool {
if len(a) != len(b) {
return false
}
@ -42,7 +42,7 @@ func compareStringSlices(a, b []string) bool {
return true
}
func compareIPv6AddressSlices(a, b []types.IPv6Address) bool {
func sortAndCompareIPv6AddressSlices(a, b []types.IPv6Address) bool {
if len(a) != len(b) {
return false
}