Files
kvm/display.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

387 lines
11 KiB
Go

package kvm
import (
"context"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/jetkvm/kvm/internal/sync"
"github.com/prometheus/common/version"
)
var (
backlightState = 0 // 0 - NORMAL, 1 - DIMMED, 2 - OFF
)
var (
dimTicker *time.Ticker
offTicker *time.Ticker
)
const (
backlightControlClass string = "/sys/class/backlight/backlight/brightness"
)
func switchToMainScreen() {
if networkManager == nil {
nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
return
}
if networkManager.IsUp() {
nativeInstance.SwitchToScreenIfDifferent("home_screen")
} else {
nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
}
}
func updateDisplayUsbState() {
if usbState == "configured" {
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
_, _ = nativeInstance.UIObjAddState("usb_status_label", "LV_STATE_CHECKED")
} else {
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Disconnected")
_, _ = nativeInstance.UIObjClearState("usb_status_label", "LV_STATE_CHECKED")
}
}
func updateDisplay() {
if networkManager != nil {
nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkManager.IPv4String())
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkManager.IPv6String())
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
}
_, _ = nativeInstance.UIObjHide("menu_btn_network")
_, _ = nativeInstance.UIObjHide("menu_btn_access")
switch config.NetworkConfig.DHCPClient.String {
case "jetdhcpc":
nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to udhcpc")
case "udhcpc":
nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to JetKVM")
}
updateDisplayUsbState()
if lastVideoState.Ready {
nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Connected")
_, _ = nativeInstance.UIObjAddState("hdmi_status_label", "LV_STATE_CHECKED")
} else {
nativeInstance.UpdateLabelIfChanged("hdmi_status_label", "Disconnected")
_, _ = nativeInstance.UIObjClearState("hdmi_status_label", "LV_STATE_CHECKED")
}
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
if networkManager != nil && networkManager.IsUp() {
nativeInstance.UISetVar("main_screen", "home_screen")
nativeInstance.SwitchToScreenIf("home_screen", []string{"no_network_screen", "boot_screen"})
} else {
nativeInstance.UISetVar("main_screen", "no_network_screen")
nativeInstance.SwitchToScreenIf("no_network_screen", []string{"home_screen", "boot_screen"})
}
if cloudConnectionState == CloudConnectionStateNotConfigured {
_, _ = nativeInstance.UIObjHide("cloud_status_icon")
} else {
_, _ = nativeInstance.UIObjShow("cloud_status_icon")
}
switch cloudConnectionState {
case CloudConnectionStateDisconnected:
_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud_disconnected")
stopCloudBlink()
case CloudConnectionStateConnecting:
_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud")
restartCloudBlink()
case CloudConnectionStateConnected:
_, _ = nativeInstance.UIObjSetImageSrc("cloud_status_icon", "cloud")
stopCloudBlink()
}
}
const (
cloudBlinkInterval = 2 * time.Second
cloudBlinkDuration = 1 * time.Second
)
var (
cloudBlinkTicker *time.Ticker
cloudBlinkCancel context.CancelFunc
cloudBlinkLock = sync.Mutex{}
)
func doCloudBlink(ctx context.Context) {
for range cloudBlinkTicker.C {
if cloudConnectionState != CloudConnectionStateConnecting {
continue
}
_, _ = nativeInstance.UIObjFadeOut("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds()))
select {
case <-ctx.Done():
return
case <-time.After(cloudBlinkDuration):
}
_, _ = nativeInstance.UIObjFadeIn("ui_Home_Header_Cloud_Status_Icon", uint32(cloudBlinkDuration.Milliseconds()))
select {
case <-ctx.Done():
return
case <-time.After(cloudBlinkDuration):
}
}
}
func restartCloudBlink() {
stopCloudBlink()
startCloudBlink()
}
func startCloudBlink() {
cloudBlinkLock.Lock()
defer cloudBlinkLock.Unlock()
if cloudBlinkTicker == nil {
cloudBlinkTicker = time.NewTicker(cloudBlinkInterval)
} else {
cloudBlinkTicker.Reset(cloudBlinkInterval)
}
ctx, cancel := context.WithCancel(context.Background())
cloudBlinkCancel = cancel
go doCloudBlink(ctx)
}
func stopCloudBlink() {
cloudBlinkLock.Lock()
defer cloudBlinkLock.Unlock()
if cloudBlinkCancel != nil {
cloudBlinkCancel()
cloudBlinkCancel = nil
}
if cloudBlinkTicker != nil {
cloudBlinkTicker.Stop()
}
}
var (
displayInited = false
displayUpdateLock = sync.Mutex{}
waitDisplayUpdate = sync.Mutex{}
)
func requestDisplayUpdate(shouldWakeDisplay bool, reason string) {
displayUpdateLock.Lock()
defer displayUpdateLock.Unlock()
if !displayInited {
displayLogger.Info().Msg("display not inited, skipping updates")
return
}
go func() {
if shouldWakeDisplay {
wakeDisplay(false, reason)
}
displayLogger.Debug().Msg("display updating")
// TODO: only run once regardless how many pending updates
updateDisplay()
}()
}
func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
waitDisplayUpdate.Lock()
defer waitDisplayUpdate.Unlock()
requestDisplayUpdate(shouldWakeDisplay, reason)
}
func updateStaticContents() {
//contents that never change
if networkManager != nil {
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
}
// get cpu info
if cpuInfo, err := os.ReadFile("/proc/cpuinfo"); err == nil {
// get the line starting with "Serial"
for line := range strings.SplitSeq(string(cpuInfo), "\n") {
if strings.HasPrefix(line, "Serial") {
serial := strings.SplitN(line, ":", 2)[1]
nativeInstance.UpdateLabelAndChangeVisibility("cpu_serial", strings.TrimSpace(serial))
break
}
}
}
// get kernel version
if kernelVersion, err := os.ReadFile("/proc/version"); err == nil {
kernelVersion := strings.TrimPrefix(string(kernelVersion), "Linux version ")
kernelVersion = strings.SplitN(kernelVersion, " ", 2)[0]
nativeInstance.UpdateLabelAndChangeVisibility("kernel_version", kernelVersion)
}
nativeInstance.UpdateLabelAndChangeVisibility("build_branch", version.Branch)
nativeInstance.UpdateLabelAndChangeVisibility("build_date", version.BuildDate)
nativeInstance.UpdateLabelAndChangeVisibility("golang_version", version.GoVersion)
// nativeInstance.UpdateLabelAndChangeVisibility("boot_screen_device_id", GetDeviceID())
}
// configureDisplayOnNativeRestart is called when the native process restarts
// it ensures the display is configured correctly after the restart
func configureDisplayOnNativeRestart() {
displayLogger.Info().Msg("native restarted, configuring display")
updateStaticContents()
requestDisplayUpdate(true, "native_restart")
}
// setDisplayBrightness sets /sys/class/backlight/backlight/brightness to alter
// the backlight brightness of the JetKVM hardware's display.
func setDisplayBrightness(brightness int, reason string) error {
// NOTE: The actual maximum value for this is 255, but out-of-the-box, the value is set to 64.
// The maximum set here is set to 100 to reduce the risk of drawing too much power (and besides, 255 is very bright!).
if brightness > 100 || brightness < 0 {
return errors.New("brightness value out of bounds, must be between 0 and 100")
}
// Check the display backlight class is available
if _, err := os.Stat(backlightControlClass); errors.Is(err, os.ErrNotExist) {
return errors.New("brightness value cannot be set, possibly not running on JetKVM hardware")
}
// Set the value
bs := []byte(strconv.Itoa(brightness))
err := os.WriteFile(backlightControlClass, bs, 0644)
if err != nil {
return err
}
displayLogger.Info().Int("brightness", brightness).Str("reason", reason).Msg("set brightness")
return nil
}
// tick_displayDim() is called when when dim ticker expires, it simply reduces the brightness
// of the display by half of the max brightness.
func tick_displayDim() {
err := setDisplayBrightness(config.DisplayMaxBrightness/2, "tick_display_dim")
if err != nil {
displayLogger.Warn().Err(err).Msg("failed to dim display")
}
dimTicker.Stop()
backlightState = 1
}
// tick_displayOff() is called when the off ticker expires, it turns off the display
// by setting the brightness to zero.
func tick_displayOff() {
err := setDisplayBrightness(0, "tick_display_off")
if err != nil {
displayLogger.Warn().Err(err).Msg("failed to turn off display")
}
offTicker.Stop()
backlightState = 2
}
// wakeDisplay sets the display brightness back to config.DisplayMaxBrightness and stores the time the display
// last woke, ready for displayTimeoutTick to put the display back in the dim/off states.
// Set force to true to skip the backlight state check, this should be done if altering the tickers.
func wakeDisplay(force bool, reason string) {
if backlightState == 0 && !force {
return
}
// Don't try to wake up if the display is turned off.
if config.DisplayMaxBrightness == 0 {
return
}
if reason == "" {
reason = "wake_display"
}
err := setDisplayBrightness(config.DisplayMaxBrightness, reason)
if err != nil {
displayLogger.Warn().Err(err).Msg("failed to wake display")
}
if config.DisplayDimAfterSec != 0 && dimTicker != nil {
dimTicker.Reset(time.Duration(config.DisplayDimAfterSec) * time.Second)
}
if config.DisplayOffAfterSec != 0 && offTicker != nil {
offTicker.Reset(time.Duration(config.DisplayOffAfterSec) * time.Second)
}
backlightState = 0
}
// startBacklightTickers starts the two tickers for dimming and switching off the display
// if they're not already set. This is done separately to the init routine as the "never dim"
// option has the value set to zero, but time.NewTicker only accept positive values.
func startBacklightTickers() {
// Don't start the tickers if the display is switched off.
// Set the display to off if that's the case.
if config.DisplayMaxBrightness == 0 {
_ = setDisplayBrightness(0, "display_disabled")
return
}
// Stop existing tickers to prevent multiple active instances on repeated calls
if dimTicker != nil {
dimTicker.Stop()
}
if offTicker != nil {
offTicker.Stop()
}
if config.DisplayDimAfterSec != 0 {
displayLogger.Info().Msg("dim_ticker has started")
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
go func() {
for range dimTicker.C {
tick_displayDim()
}
}()
}
if config.DisplayOffAfterSec != 0 {
displayLogger.Info().Msg("off_ticker has started")
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
go func() {
for range offTicker.C {
tick_displayOff()
}
}()
}
}
func initDisplay() {
go func() {
displayLogger.Info().Msg("setting initial display contents")
time.Sleep(500 * time.Millisecond)
updateStaticContents()
updateDisplayUsbState()
displayInited = true
displayLogger.Info().Msg("display inited")
startBacklightTickers()
requestDisplayUpdate(true, "init_display")
}()
}