Compare commits

...

7 Commits

Author SHA1 Message Date
Siyuan a9cd36c5fb fix: update NetworkConfig type in config.go 2025-10-09 21:05:31 +00:00
Siyuan 52ddc9ebe5 fix: do not apply IPv6 DHCP lease if it's from udhcpc 2025-10-09 21:04:02 +00:00
Aveline b84aa3822d
Merge branch 'dev' into feat/nmrewrite 2025-10-09 22:50:50 +02:00
Aveline cc9ff74276
feat: add HDMI sleep mode (#881) 2025-10-09 14:52:51 +02:00
Marc Brooks b144d9926f
Remove the temporary directory after extracting buildkit (#874) 2025-10-07 11:57:26 +02:00
Aylen e755a6e1b1
Update openSUSE image reference to Leap 16.0 (#865) 2025-10-07 11:57:10 +02:00
Marc Brooks 99a8c2711c
Add podman support (#875)
Reimplement #141 since we've changed everything since
2025-10-07 11:43:25 +02:00
15 changed files with 359 additions and 147 deletions

View File

@ -1,5 +1,5 @@
{ {
"name": "JetKVM", "name": "JetKVM docker devcontainer",
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie", "image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {

View File

@ -32,4 +32,5 @@ wget https://github.com/jetkvm/rv1106-system/releases/download/${BUILDKIT_VERSIO
sudo mkdir -p /opt/jetkvm-native-buildkit && \ sudo mkdir -p /opt/jetkvm-native-buildkit && \
sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \ sudo tar --use-compress-program="unzstd --long=31" -xvf buildkit.tar.zst -C /opt/jetkvm-native-buildkit && \
rm buildkit.tar.zst rm buildkit.tar.zst
popd popd
rm -rf "${BUILDKIT_TMPDIR}"

View File

@ -0,0 +1,19 @@
{
"name": "JetKVM podman devcontainer",
"image": "mcr.microsoft.com/devcontainers/go:1.25-trixie",
"features": {
"ghcr.io/devcontainers/features/node:1": {
// Should match what is defined in ui/package.json
"version": "22.19.0"
}
},
"runArgs": [
"--userns=keep-id",
"--security-opt=label=disable",
"--security-opt=label=nested"
],
"containerUser": "vscode",
"containerEnv": {
"HOME": "/home/vscode"
}
}

View File

@ -104,6 +104,7 @@ type Config struct {
UsbDevices *usbgadget.Devices `json:"usb_devices"` UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *types.NetworkConfig `json:"network_config"` NetworkConfig *types.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"` DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
} }
func (c *Config) GetDisplayRotation() uint16 { func (c *Config) GetDisplayRotation() uint16 {

View File

@ -19,6 +19,7 @@ type Native struct {
onVideoFrameReceived func(frame []byte, duration time.Duration) onVideoFrameReceived func(frame []byte, duration time.Duration)
onIndevEvent func(event string) onIndevEvent func(event string)
onRpcEvent func(event string) onRpcEvent func(event string)
sleepModeSupported bool
videoLock sync.Mutex videoLock sync.Mutex
screenLock sync.Mutex screenLock sync.Mutex
} }
@ -62,6 +63,8 @@ func NewNative(opts NativeOptions) *Native {
} }
} }
sleepModeSupported := isSleepModeSupported()
return &Native{ return &Native{
ready: make(chan struct{}), ready: make(chan struct{}),
l: nativeLogger, l: nativeLogger,
@ -73,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
onVideoFrameReceived: onVideoFrameReceived, onVideoFrameReceived: onVideoFrameReceived,
onIndevEvent: onIndevEvent, onIndevEvent: onIndevEvent,
onRpcEvent: onRpcEvent, onRpcEvent: onRpcEvent,
sleepModeSupported: sleepModeSupported,
videoLock: sync.Mutex{}, videoLock: sync.Mutex{},
screenLock: sync.Mutex{}, screenLock: sync.Mutex{},
} }

View File

@ -1,5 +1,12 @@
package native package native
import (
"os"
)
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
// VideoState is the state of the video stream.
type VideoState struct { type VideoState struct {
Ready bool `json:"ready"` Ready bool `json:"ready"`
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
@ -8,6 +15,58 @@ type VideoState struct {
FramePerSecond float64 `json:"fps"` FramePerSecond float64 `json:"fps"`
} }
func isSleepModeSupported() bool {
_, err := os.Stat(sleepModeFile)
return err == nil
}
func (n *Native) setSleepMode(enabled bool) error {
if !n.sleepModeSupported {
return nil
}
bEnabled := "0"
if enabled {
bEnabled = "1"
}
return os.WriteFile(sleepModeFile, []byte(bEnabled), 0644)
}
func (n *Native) getSleepMode() (bool, error) {
if !n.sleepModeSupported {
return false, nil
}
data, err := os.ReadFile(sleepModeFile)
if err == nil {
return string(data) == "1", nil
}
return false, nil
}
// VideoSetSleepMode sets the sleep mode for the video stream.
func (n *Native) VideoSetSleepMode(enabled bool) error {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return n.setSleepMode(enabled)
}
// VideoGetSleepMode gets the sleep mode for the video stream.
func (n *Native) VideoGetSleepMode() (bool, error) {
n.videoLock.Lock()
defer n.videoLock.Unlock()
return n.getSleepMode()
}
// VideoSleepModeSupported checks if the sleep mode is supported.
func (n *Native) VideoSleepModeSupported() bool {
return n.sleepModeSupported
}
// VideoSetQualityFactor sets the quality factor for the video stream.
func (n *Native) VideoSetQualityFactor(factor float64) error { func (n *Native) VideoSetQualityFactor(factor float64) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -15,6 +74,7 @@ func (n *Native) VideoSetQualityFactor(factor float64) error {
return videoSetStreamQualityFactor(factor) return videoSetStreamQualityFactor(factor)
} }
// VideoGetQualityFactor gets the quality factor for the video stream.
func (n *Native) VideoGetQualityFactor() (float64, error) { func (n *Native) VideoGetQualityFactor() (float64, error) {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -22,6 +82,7 @@ func (n *Native) VideoGetQualityFactor() (float64, error) {
return videoGetStreamQualityFactor() return videoGetStreamQualityFactor()
} }
// VideoSetEDID sets the EDID for the video stream.
func (n *Native) VideoSetEDID(edid string) error { func (n *Native) VideoSetEDID(edid string) error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -29,6 +90,7 @@ func (n *Native) VideoSetEDID(edid string) error {
return videoSetEDID(edid) return videoSetEDID(edid)
} }
// VideoGetEDID gets the EDID for the video stream.
func (n *Native) VideoGetEDID() (string, error) { func (n *Native) VideoGetEDID() (string, error) {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -36,6 +98,7 @@ func (n *Native) VideoGetEDID() (string, error) {
return videoGetEDID() return videoGetEDID()
} }
// VideoLogStatus gets the log status for the video stream.
func (n *Native) VideoLogStatus() (string, error) { func (n *Native) VideoLogStatus() (string, error) {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -43,6 +106,7 @@ func (n *Native) VideoLogStatus() (string, error) {
return videoLogStatus(), nil return videoLogStatus(), nil
} }
// VideoStop stops the video stream.
func (n *Native) VideoStop() error { func (n *Native) VideoStop() error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
@ -51,10 +115,14 @@ func (n *Native) VideoStop() error {
return nil return nil
} }
// VideoStart starts the video stream.
func (n *Native) VideoStart() error { func (n *Native) VideoStart() error {
n.videoLock.Lock() n.videoLock.Lock()
defer n.videoLock.Unlock() defer n.videoLock.Unlock()
// disable sleep mode before starting video
_ = n.setSleepMode(false)
videoStart() videoStart()
return nil return nil
} }

View File

@ -1215,6 +1215,8 @@ var rpcHandlers = map[string]RPCHandler{
"getEDID": {Func: rpcGetEDID}, "getEDID": {Func: rpcGetEDID},
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}}, "setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
"getVideoLogStatus": {Func: rpcGetVideoLogStatus}, "getVideoLogStatus": {Func: rpcGetVideoLogStatus},
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
"getDevChannelState": {Func: rpcGetDevChannelState}, "getDevChannelState": {Func: rpcGetDevChannelState},
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}}, "setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getLocalVersion": {Func: rpcGetLocalVersion}, "getLocalVersion": {Func: rpcGetLocalVersion},

View File

@ -75,6 +75,9 @@ func Main() {
} }
initJiggler() initJiggler()
// start video sleep mode timer
startVideoSleepModeTicker()
go func() { go func() {
time.Sleep(15 * time.Minute) time.Sleep(15 * time.Minute)
for { for {

View File

@ -727,6 +727,20 @@ func (im *InterfaceManager) ReconcileLinkAddrs(addrs []types.IPAddress, family i
// applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs // applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs
func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error { func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error {
if lease == nil {
return fmt.Errorf("DHCP lease is nil")
}
if lease.DHCPClient != "jetdhcpc" {
im.logger.Warn().Str("dhcp_client", lease.DHCPClient).Msg("ignoring DHCP lease, not implemented yet")
return nil
}
if lease.IsIPv6() {
im.logger.Warn().Msg("ignoring IPv6 DHCP lease, not implemented yet")
return nil
}
// Convert DHCP lease to IPv4Config // Convert DHCP lease to IPv4Config
ipv4Config := im.convertDHCPLeaseToIPv4Config(lease) ipv4Config := im.convertDHCPLeaseToIPv4Config(lease)

View File

@ -3,19 +3,17 @@ package jetdhcpc
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net" "net"
"slices" "slices"
"time" "time"
"github.com/jetkvm/kvm/internal/sync" "github.com/jetkvm/kvm/internal/sync"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/go-co-op/gocron/v2"
"github.com/insomniacslk/dhcp/dhcpv4" "github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6" "github.com/insomniacslk/dhcp/dhcpv6"
"github.com/jetkvm/kvm/internal/network/types" "github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -83,8 +81,10 @@ type Config struct {
UpdateResolvConf func([]string) error UpdateResolvConf func([]string) error
} }
// Client is a DHCP client.
type Client struct { type Client struct {
types.DHCPClient types.DHCPClient
ifaces []string ifaces []string
cfg Config cfg Config
l *zerolog.Logger l *zerolog.Logger
@ -101,24 +101,28 @@ type Client struct {
lease4Mu sync.Mutex lease4Mu sync.Mutex
lease6Mu sync.Mutex lease6Mu sync.Mutex
scheduler gocron.Scheduler timer4 *time.Timer
stateDir string timer6 *time.Timer
stateDir string
} }
var (
defaultTimerDuration = 1 * time.Second
defaultLinkUpTimeout = 30 * time.Second
)
// NewClient creates a new DHCP client for the given interface. // NewClient creates a new DHCP client for the given interface.
func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logger) (*Client, error) { func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logger) (*Client, error) {
scheduler, err := gocron.NewScheduler() timer4 := time.NewTimer(defaultTimerDuration)
if err != nil { timer6 := time.NewTimer(defaultTimerDuration)
return nil, fmt.Errorf("failed to create scheduler: %w", err)
}
cfg := *c cfg := *c
if cfg.LinkUpTimeout == 0 { if cfg.LinkUpTimeout == 0 {
cfg.LinkUpTimeout = 30 * time.Second cfg.LinkUpTimeout = defaultLinkUpTimeout
} }
if cfg.Timeout == 0 { if cfg.Timeout == 0 {
cfg.Timeout = 30 * time.Second cfg.Timeout = defaultLinkUpTimeout
} }
if cfg.Retries == 0 { if cfg.Retries == 0 {
@ -126,12 +130,11 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
} }
return &Client{ return &Client{
ctx: ctx, ctx: ctx,
ifaces: ifaces, ifaces: ifaces,
cfg: cfg, cfg: cfg,
l: l, l: l,
scheduler: scheduler, stateDir: "/run/jetkvm-dhcp",
stateDir: "/run/jetkvm-dhcp",
currentLease4: nil, currentLease4: nil,
currentLease6: nil, currentLease6: nil,
@ -141,9 +144,45 @@ func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logge
mu: sync.Mutex{}, mu: sync.Mutex{},
cfgMu: sync.Mutex{}, cfgMu: sync.Mutex{},
timer4: timer4,
timer6: timer6,
}, nil }, nil
} }
func resetTimer(t *time.Timer, l *zerolog.Logger) {
l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later")
t.Reset(defaultTimerDuration)
}
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
for range t.C {
if _, err := c.ensureInterfaceUp(ifname); err != nil {
c.l.Error().Err(err).Msg("failed to ensure interface up")
resetTimer(t, c.l)
continue
}
var (
lease *Lease
err error
)
switch family {
case link.AfInet:
lease, err = c.requestLease4(ifname)
case link.AfInet6:
lease, err = c.requestLease6(ifname)
}
if err != nil {
c.l.Error().Err(err).Msg("failed to request lease")
resetTimer(t, c.l)
continue
}
c.handleLeaseChange(lease)
}
}
func (c *Client) ensureInterfaceUp(ifname string) (*link.Link, error) { func (c *Client) ensureInterfaceUp(ifname string) (*link.Link, error) {
nlm := link.GetNetlinkManager() nlm := link.GetNetlinkManager()
iface, err := nlm.GetLinkByName(ifname) iface, err := nlm.GetLinkByName(ifname)
@ -153,76 +192,7 @@ func (c *Client) ensureInterfaceUp(ifname string) (*link.Link, error) {
return nlm.EnsureInterfaceUpWithTimeout(c.ctx, iface, c.cfg.LinkUpTimeout) return nlm.EnsureInterfaceUpWithTimeout(c.ctx, iface, c.cfg.LinkUpTimeout)
} }
func (c *Client) sendInitialRequests() chan any { // Lease4 returns the current IPv4 lease
return c.sendRequests(c.cfg.IPv4, c.cfg.IPv6)
}
func (c *Client) sendRequestsFamily(
family int,
wg *sync.WaitGroup,
r *chan any,
l *zerolog.Logger,
iface *link.Link,
) {
wg.Add(1)
go func(iface *link.Link) {
defer wg.Done()
var (
lease *Lease
err error
)
switch family {
case link.AfInet:
lease, err = c.requestLease4(iface)
case link.AfInet6:
lease, err = c.requestLease6(iface)
}
if err != nil {
l.Error().Err(err).Msg("Could not get lease")
return
}
(*r) <- lease
}(iface)
}
func (c *Client) sendRequests(ipv4, ipv6 bool) chan any {
c.mu.Lock()
defer c.mu.Unlock()
// Yeah, this is a hack, until we can cancel all leases in progress.
r := make(chan any, 3*len(c.ifaces))
var wg sync.WaitGroup
for _, iface := range c.ifaces {
wg.Add(1)
go func(ifname string) {
defer wg.Done()
l := c.l.With().Str("interface", ifname).Logger()
iface, err := c.ensureInterfaceUp(ifname)
if err != nil {
l.Error().Err(err).Msg("Could not bring up interface")
return
}
if ipv4 {
c.sendRequestsFamily(link.AfInet, &wg, &r, &l, iface)
}
if ipv6 {
c.sendRequestsFamily(link.AfInet6, &wg, &r, &l, iface)
}
}(iface)
}
go func() {
wg.Wait()
close(r)
}()
return r
}
func (c *Client) Lease4() *types.DHCPLease { func (c *Client) Lease4() *types.DHCPLease {
c.lease4Mu.Lock() c.lease4Mu.Lock()
defer c.lease4Mu.Unlock() defer c.lease4Mu.Unlock()
@ -234,6 +204,7 @@ func (c *Client) Lease4() *types.DHCPLease {
return c.currentLease4.ToDHCPLease() return c.currentLease4.ToDHCPLease()
} }
// Lease6 returns the current IPv6 lease
func (c *Client) Lease6() *types.DHCPLease { func (c *Client) Lease6() *types.DHCPLease {
c.lease6Mu.Lock() c.lease6Mu.Lock()
defer c.lease6Mu.Unlock() defer c.lease6Mu.Unlock()
@ -245,6 +216,7 @@ func (c *Client) Lease6() *types.DHCPLease {
return c.currentLease6.ToDHCPLease() return c.currentLease6.ToDHCPLease()
} }
// Domain returns the current domain
func (c *Client) Domain() string { func (c *Client) Domain() string {
c.lease4Mu.Lock() c.lease4Mu.Lock()
defer c.lease4Mu.Unlock() defer c.lease4Mu.Unlock()
@ -263,50 +235,23 @@ func (c *Client) Domain() string {
return "" return ""
} }
// handleLeaseChange handles lease changes
func (c *Client) handleLeaseChange(lease *Lease) { func (c *Client) handleLeaseChange(lease *Lease) {
// do not use defer here, because we need to unlock the mutex before returning // do not use defer here, because we need to unlock the mutex before returning
ipv4 := lease.p4 != nil ipv4 := lease.p4 != nil
version := "ipv4"
if ipv4 { if ipv4 {
c.lease4Mu.Lock() c.lease4Mu.Lock()
c.currentLease4 = lease c.currentLease4 = lease
c.lease4Mu.Unlock()
} else { } else {
version = "ipv6"
c.lease6Mu.Lock() c.lease6Mu.Lock()
c.currentLease6 = lease c.currentLease6 = lease
} c.lease6Mu.Unlock()
// clear all current jobs with the same tags
// c.scheduler.RemoveByTags(version)
// add scheduler job to renew the lease
if lease.RenewalTime > 0 {
c.scheduler.NewJob(
gocron.DurationJob(time.Duration(lease.RenewalTime)*time.Second),
gocron.NewTask(func() {
c.l.Info().Msg("renewing lease")
for lease := range c.sendRequests(ipv4, !ipv4) {
if lease, ok := lease.(*Lease); ok {
c.handleLeaseChange(lease)
}
}
}),
gocron.WithName(fmt.Sprintf("renew-%s", version)),
gocron.WithSingletonMode(gocron.LimitModeWait),
gocron.WithTags(version),
)
} }
c.apply() c.apply()
if ipv4 {
c.lease4Mu.Unlock()
} else {
c.lease6Mu.Unlock()
}
// TODO: handle lease expiration // TODO: handle lease expiration
if c.cfg.OnLease4Change != nil && ipv4 { if c.cfg.OnLease4Change != nil && ipv4 {
c.cfg.OnLease4Change(lease.ToDHCPLease()) c.cfg.OnLease4Change(lease.ToDHCPLease())
@ -317,14 +262,23 @@ func (c *Client) handleLeaseChange(lease *Lease) {
} }
} }
func (c *Client) renew() { func (c *Client) doRenewLoop() {
for lease := range c.sendRequests(c.cfg.IPv4, c.cfg.IPv6) { timer := time.NewTimer(time.Duration(c.currentLease4.RenewalTime) * time.Second)
if lease, ok := lease.(*Lease); ok { defer timer.Stop()
c.handleLeaseChange(lease)
} for range timer.C {
c.renew()
} }
} }
func (c *Client) renew() {
// for lease := range c.sendRequests(c.cfg.IPv4, c.cfg.IPv6) {
// if lease, ok := lease.(*Lease); ok {
// c.handleLeaseChange(lease)
// }
// }
}
func (c *Client) Renew() error { func (c *Client) Renew() error {
go c.renew() go c.renew()
return nil return nil
@ -350,17 +304,29 @@ func (c *Client) SetIPv4(ipv4 bool) {
c.lease4Mu.Lock() c.lease4Mu.Lock()
c.currentLease4 = nil c.currentLease4 = nil
c.lease4Mu.Unlock() c.lease4Mu.Unlock()
c.scheduler.RemoveByTags("ipv4")
} }
c.sendRequests(ipv4, c.cfg.IPv6) c.timer4.Stop()
} }
func (c *Client) SetIPv6(ipv6 bool) { func (c *Client) SetIPv6(ipv6 bool) {
c.cfgMu.Lock() c.cfgMu.Lock()
defer c.cfgMu.Unlock() defer c.cfgMu.Unlock()
currentIPv6 := c.cfg.IPv6
c.cfg.IPv6 = ipv6 c.cfg.IPv6 = ipv6
if currentIPv6 == ipv6 {
return
}
if !ipv6 {
c.lease6Mu.Lock()
c.currentLease6 = nil
c.lease4Mu.Unlock()
}
c.timer6.Stop()
} }
func (c *Client) Start() error { func (c *Client) Start() error {
@ -368,15 +334,14 @@ func (c *Client) Start() error {
c.l.Warn().Err(err).Msg("failed to kill udhcpc processes, continuing anyway") c.l.Warn().Err(err).Msg("failed to kill udhcpc processes, continuing anyway")
} }
c.scheduler.Start() for _, iface := range c.ifaces {
if c.cfg.IPv4 {
go func() { go c.requestLoop(c.timer4, link.AfInet, iface)
for lease := range c.sendInitialRequests() {
if lease, ok := lease.(*Lease); ok {
c.handleLeaseChange(lease)
}
} }
}() if c.cfg.IPv6 {
go c.requestLoop(c.timer6, link.AfInet6, iface)
}
}
return nil return nil
} }

View File

@ -8,8 +8,12 @@ import (
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
) )
func (c *Client) requestLease4(iface netlink.Link) (*Lease, error) { func (c *Client) requestLease4(ifname string) (*Lease, error) {
ifname := iface.Attrs().Name iface, err := netlink.LinkByName(ifname)
if err != nil {
return nil, err
}
l := c.l.With().Str("interface", ifname).Logger() l := c.l.With().Str("interface", ifname).Logger()
mods := []nclient4.ClientOpt{ mods := []nclient4.ClientOpt{

View File

@ -55,10 +55,14 @@ func isIPv6RouteReady(serverAddr net.IP) waitForCondition {
} }
} }
func (c *Client) requestLease6(iface netlink.Link) (*Lease, error) { func (c *Client) requestLease6(ifname string) (*Lease, error) {
ifname := iface.Attrs().Name
l := c.l.With().Str("interface", ifname).Logger() l := c.l.With().Str("interface", ifname).Logger()
iface, err := netlink.LinkByName(ifname)
if err != nil {
return nil, err
}
clientPort := dhcpv6.DefaultClientPort clientPort := dhcpv6.DefaultClientPort
if c.cfg.V6ClientPort != nil { if c.cfg.V6ClientPort != nil {
clientPort = *c.cfg.V6ClientPort clientPort = *c.cfg.V6ClientPort

View File

@ -374,8 +374,8 @@ function UrlView({
icon: FedoraIcon, icon: FedoraIcon,
}, },
{ {
name: "openSUSE Leap 15.6", name: "openSUSE Leap 16.0",
url: "https://download.opensuse.org/distribution/leap/15.6/iso/openSUSE-Leap-15.6-NET-x86_64-Media.iso", url: "https://download.opensuse.org/distribution/leap/16.0/offline/Leap-16.0-online-installer-x86_64.install.iso",
icon: OpenSUSEIcon, icon: OpenSUSEIcon,
}, },
{ {

103
video.go
View File

@ -1,10 +1,22 @@
package kvm package kvm
import ( import (
"context"
"fmt"
"time"
"github.com/jetkvm/kvm/internal/native" "github.com/jetkvm/kvm/internal/native"
) )
var lastVideoState native.VideoState var (
lastVideoState native.VideoState
videoSleepModeCtx context.Context
videoSleepModeCancel context.CancelFunc
)
const (
defaultVideoSleepModeDuration = 1 * time.Minute
)
func triggerVideoStateUpdate() { func triggerVideoStateUpdate() {
go func() { go func() {
@ -17,3 +29,92 @@ func triggerVideoStateUpdate() {
func rpcGetVideoState() (native.VideoState, error) { func rpcGetVideoState() (native.VideoState, error) {
return lastVideoState, nil return lastVideoState, nil
} }
type rpcVideoSleepModeResponse struct {
Supported bool `json:"supported"`
Enabled bool `json:"enabled"`
Duration int `json:"duration"`
}
func rpcGetVideoSleepMode() rpcVideoSleepModeResponse {
sleepMode, _ := nativeInstance.VideoGetSleepMode()
return rpcVideoSleepModeResponse{
Supported: nativeInstance.VideoSleepModeSupported(),
Enabled: sleepMode,
Duration: config.VideoSleepAfterSec,
}
}
func rpcSetVideoSleepMode(duration int) error {
if duration < 0 {
duration = -1 // disable
}
config.VideoSleepAfterSec = duration
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
// we won't restart the ticker here,
// as the session can't be inactive when this function is called
return nil
}
func stopVideoSleepModeTicker() {
nativeLogger.Trace().Msg("stopping HDMI sleep mode ticker")
if videoSleepModeCancel != nil {
nativeLogger.Trace().Msg("canceling HDMI sleep mode ticker context")
videoSleepModeCancel()
videoSleepModeCancel = nil
videoSleepModeCtx = nil
}
}
func startVideoSleepModeTicker() {
if !nativeInstance.VideoSleepModeSupported() {
return
}
var duration time.Duration
if config.VideoSleepAfterSec == 0 {
duration = defaultVideoSleepModeDuration
} else if config.VideoSleepAfterSec > 0 {
duration = time.Duration(config.VideoSleepAfterSec) * time.Second
} else {
stopVideoSleepModeTicker()
return
}
// Stop any existing timer and goroutine
stopVideoSleepModeTicker()
// Create new context for this ticker
videoSleepModeCtx, videoSleepModeCancel = context.WithCancel(context.Background())
go doVideoSleepModeTicker(videoSleepModeCtx, duration)
}
func doVideoSleepModeTicker(ctx context.Context, duration time.Duration) {
timer := time.NewTimer(duration)
defer timer.Stop()
nativeLogger.Trace().Msg("HDMI sleep mode ticker started")
for {
select {
case <-timer.C:
if getActiveSessions() > 0 {
nativeLogger.Warn().Msg("not going to enter HDMI sleep mode because there are active sessions")
continue
}
nativeLogger.Trace().Msg("entering HDMI sleep mode")
_ = nativeInstance.VideoSetSleepMode(true)
case <-ctx.Done():
nativeLogger.Trace().Msg("HDMI sleep mode ticker stopped")
return
}
}
}

View File

@ -39,6 +39,34 @@ type Session struct {
keysDownStateQueue chan usbgadget.KeysDownState keysDownStateQueue chan usbgadget.KeysDownState
} }
var (
actionSessions int = 0
activeSessionsMutex = &sync.Mutex{}
)
func incrActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
actionSessions++
return actionSessions
}
func decrActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
actionSessions--
return actionSessions
}
func getActiveSessions() int {
activeSessionsMutex.Lock()
defer activeSessionsMutex.Unlock()
return actionSessions
}
func (s *Session) resetKeepAliveTime() { func (s *Session) resetKeepAliveTime() {
s.keepAliveJitterLock.Lock() s.keepAliveJitterLock.Lock()
defer s.keepAliveJitterLock.Unlock() defer s.keepAliveJitterLock.Unlock()
@ -312,9 +340,8 @@ func newSession(config SessionConfig) (*Session, error) {
if connectionState == webrtc.ICEConnectionStateConnected { if connectionState == webrtc.ICEConnectionStateConnected {
if !isConnected { if !isConnected {
isConnected = true isConnected = true
actionSessions++
onActiveSessionsChanged() onActiveSessionsChanged()
if actionSessions == 1 { if incrActiveSessions() == 1 {
onFirstSessionConnected() onFirstSessionConnected()
} }
} }
@ -353,9 +380,8 @@ func newSession(config SessionConfig) (*Session, error) {
} }
if isConnected { if isConnected {
isConnected = false isConnected = false
actionSessions--
onActiveSessionsChanged() onActiveSessionsChanged()
if actionSessions == 0 { if decrActiveSessions() == 0 {
onLastSessionDisconnected() onLastSessionDisconnected()
} }
} }
@ -364,16 +390,16 @@ func newSession(config SessionConfig) (*Session, error) {
return session, nil return session, nil
} }
var actionSessions = 0
func onActiveSessionsChanged() { func onActiveSessionsChanged() {
requestDisplayUpdate(true, "active_sessions_changed") requestDisplayUpdate(true, "active_sessions_changed")
} }
func onFirstSessionConnected() { func onFirstSessionConnected() {
_ = nativeInstance.VideoStart() _ = nativeInstance.VideoStart()
stopVideoSleepModeTicker()
} }
func onLastSessionDisconnected() { func onLastSessionDisconnected() {
_ = nativeInstance.VideoStop() _ = nativeInstance.VideoStop()
startVideoSleepModeTicker()
} }