Files
kvm/main.go
T
Lian Duan eb56468d25 feat(ota): include device SKU in update requests (#1429)
* feat(ota): include device SKU in update requests

Read /etc/jetkvm-sku at startup and pass the value as a sku query
parameter when checking for OTA updates, enabling the cloud API to
serve variant-specific firmware for jetkvm-v2 (eMMC) and
jetkvm-v2-sdmmc hardware. Falls back to "jetkvm-v2" when the file
is absent for backwards compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ota): include SKU in auto-update loop

The background auto-update loop in main.go also constructs UpdateParams
and was missing the SKU field, so periodic updates would not transmit
the device SKU to the cloud API.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 23:36:29 +02:00

204 lines
4.6 KiB
Go

package kvm
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jetkvm/kvm/internal/ota"
"github.com/erikdubbelboer/gspt"
"github.com/gwatts/rootcerts"
"github.com/rs/zerolog"
)
var appCtx context.Context
var procPrefix string = "jetkvm: [app]"
func setProcTitle(status string) {
if status != "" {
status = " " + status
}
title := fmt.Sprintf("%s%s", procPrefix, status)
gspt.SetProcTitle(title)
}
func Main() {
setProcTitle("starting")
logger.Log().Msg("JetKVM Starting Up")
defer func() {
if r := recover(); r != nil {
logger.WithLevel(zerolog.PanicLevel).Interface("error", r).Msg("Received panic")
panic(r) // Re-panic to crash as usual
}
}()
checkFailsafeReason()
if failsafeModeActive {
procPrefix = "jetkvm: [app+failsafe]"
logger.Warn().Str("reason", failsafeModeReason).Msg("failsafe mode activated")
}
LoadConfig()
var cancel context.CancelFunc
appCtx, cancel = context.WithCancel(context.Background())
defer cancel()
systemVersionLocal, appVersionLocal, err := GetLocalVersion()
if err != nil {
logger.Warn().Err(err).Msg("failed to get local version")
}
logger.Info().
Interface("system_version", systemVersionLocal).
Interface("app_version", appVersionLocal).
Msg("starting JetKVM")
go runWatchdog()
// initialize usb gadget
setProcTitle("initUsbGadget")
initUsbGadget()
setProcTitle("initNative")
initNative(systemVersionLocal, appVersionLocal)
initDisplay()
http.DefaultClient.Timeout = 1 * time.Minute
err = rootcerts.UpdateDefaultTransport()
if err != nil {
logger.Warn().Err(err).Msg("failed to load Root CA certificates")
}
logger.Info().
Int("ca_certs_loaded", len(rootcerts.Certs())).
Msg("loaded Root CA certificates")
initOta()
http.DefaultClient.Timeout = 1 * time.Minute
// Initialize network
setProcTitle("initNetwork")
if err := initNetwork(); err != nil {
logger.Error().Err(err).Msg("failed to initialize network")
// TODO: reset config to default
os.Exit(1)
}
// Initialize time sync
setProcTitle("initTimeSync")
initTimeSync()
timeSync.Start()
// Initialize mDNS
setProcTitle("initMdns")
if err := initMdns(); err != nil {
logger.Error().Err(err).Msg("failed to initialize mDNS")
}
setProcTitle("initPrometheus")
initPrometheus()
if err := setInitialVirtualMediaState(); err != nil {
logger.Warn().Err(err).Msg("failed to set initial virtual media state")
}
if err := initImagesFolder(); err != nil {
logger.Warn().Err(err).Msg("failed to init images folder")
}
initJiggler()
// Initialize MQTT
initMQTT()
defer func() {
if mqttManager != nil {
mqttManager.Close()
}
}()
// start video sleep mode timer
startVideoSleepModeTicker()
go func() {
// wait for 15 minutes before starting auto-update checks
// this is to avoid interfering with initial setup processes
// and to ensure the system is stable before checking for updates
time.Sleep(15 * time.Minute)
for {
logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
if !config.AutoUpdateEnabled {
logger.Debug().Msg("auto-update disabled")
time.Sleep(5 * time.Minute) // we'll check if auto-updates are enabled in five minutes
continue
}
if currentSession != nil {
logger.Debug().Msg("skipping update since a session is active")
time.Sleep(1 * time.Minute)
continue
}
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
time.Sleep(30 * time.Second)
continue
}
includePreRelease := config.IncludePreRelease
err = otaState.TryUpdate(context.Background(), ota.UpdateParams{
DeviceID: GetDeviceID(),
SKU: GetDeviceSKU(),
IncludePreRelease: includePreRelease,
})
if err != nil {
logger.Warn().Err(err).Msg("failed to auto update")
}
time.Sleep(1 * time.Hour)
}
}()
//go RunFuseServer()
go RunWebServer()
go RunWebSecureServer()
// Web secure server is started only if TLS mode is enabled
if config.TLSMode != "" {
startWebSecureServer()
}
// As websocket client already checks if the cloud token is set, we can start it here.
go RunWebsocketClient()
initPublicIPState()
initSerialPort()
setProcTitle("ready")
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
logger.Log().Msg("JetKVM Shutting Down")
//if fuseServer != nil {
// err := setMassStorageImage(" ")
// if err != nil {
// logger.Infof("Failed to unmount mass storage image: %v", err)
// }
// err = fuseServer.Unmount()
// if err != nil {
// logger.Infof("Failed to unmount fuse: %v", err)
// }
// os.Exit(0)
}