mirror of https://github.com/jetkvm/kvm.git
Compare commits
6 Commits
35d8c3d9d1
...
87adfee033
| Author | SHA1 | Date |
|---|---|---|
|
|
87adfee033 | |
|
|
b49d67c87d | |
|
|
621be3c1d9 | |
|
|
36f06a064a | |
|
|
5f15d8b2f6 | |
|
|
28919bf37c |
|
|
@ -30,13 +30,15 @@ type LLDP struct {
|
||||||
advertiseOptions *AdvertiseOptions
|
advertiseOptions *AdvertiseOptions
|
||||||
onChange func(neighbors []Neighbor)
|
onChange func(neighbors []Neighbor)
|
||||||
|
|
||||||
neighbors *ttlcache.Cache[string, Neighbor]
|
neighbors *ttlcache.Cache[neighborCacheKey, Neighbor]
|
||||||
|
|
||||||
// State tracking
|
// State tracking
|
||||||
rxRunning bool
|
|
||||||
txRunning bool
|
txRunning bool
|
||||||
txCtx context.Context
|
txCtx context.Context
|
||||||
txCancel context.CancelFunc
|
txCancel context.CancelFunc
|
||||||
|
|
||||||
|
rxRunning bool
|
||||||
|
rxWaitGroup *sync.WaitGroup
|
||||||
rxCtx context.Context
|
rxCtx context.Context
|
||||||
rxCancel context.CancelFunc
|
rxCancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
@ -72,8 +74,9 @@ func NewLLDP(opts *Options) *LLDP {
|
||||||
advertiseOptions: opts.AdvertiseOptions,
|
advertiseOptions: opts.AdvertiseOptions,
|
||||||
enableRx: opts.EnableRx,
|
enableRx: opts.EnableRx,
|
||||||
enableTx: opts.EnableTx,
|
enableTx: opts.EnableTx,
|
||||||
|
rxWaitGroup: &sync.WaitGroup{},
|
||||||
l: opts.Logger,
|
l: opts.Logger,
|
||||||
neighbors: ttlcache.New(ttlcache.WithTTL[string, Neighbor](1 * time.Hour)),
|
neighbors: ttlcache.New(ttlcache.WithTTL[neighborCacheKey, Neighbor](1 * time.Hour)),
|
||||||
onChange: opts.OnChange,
|
onChange: opts.OnChange,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package lldp
|
package lldp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -27,22 +26,28 @@ type Neighbor struct {
|
||||||
Values map[string]string `json:"values"`
|
Values map[string]string `json:"values"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Neighbor) cacheKey() string {
|
type neighborCacheKey struct {
|
||||||
return fmt.Sprintf("%s-%s", n.Mac, n.Source)
|
mac string
|
||||||
|
source string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Neighbor) cacheKey() neighborCacheKey {
|
||||||
|
return neighborCacheKey{mac: n.Mac, source: n.Source}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LLDP) addNeighbor(neighbor *Neighbor, ttl time.Duration) {
|
func (l *LLDP) addNeighbor(neighbor *Neighbor, ttl time.Duration) {
|
||||||
logger := l.l.With().
|
logger := l.l.With().
|
||||||
|
Str("source", neighbor.Source).
|
||||||
Str("mac", neighbor.Mac).
|
Str("mac", neighbor.Mac).
|
||||||
Interface("neighbor", neighbor).
|
Interface("neighbor", neighbor).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
key := neighbor.cacheKey()
|
key := neighbor.cacheKey()
|
||||||
|
|
||||||
current_neigh := l.neighbors.Get(key)
|
currentNeighbor := l.neighbors.Get(key)
|
||||||
if current_neigh != nil {
|
if currentNeighbor != nil {
|
||||||
current_source := current_neigh.Value().Source
|
currentSource := currentNeighbor.Value().Source
|
||||||
if current_source == "lldp" && neighbor.Source != "lldp" {
|
if currentSource == "lldp" && neighbor.Source != "lldp" {
|
||||||
logger.Info().Msg("skip updating neighbor, as LLDP has higher priority")
|
logger.Info().Msg("skip updating neighbor, as LLDP has higher priority")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -56,6 +61,7 @@ func (l *LLDP) addNeighbor(neighbor *Neighbor, ttl time.Duration) {
|
||||||
|
|
||||||
func (l *LLDP) deleteNeighbor(neighbor *Neighbor) {
|
func (l *LLDP) deleteNeighbor(neighbor *Neighbor) {
|
||||||
logger := l.l.With().
|
logger := l.l.With().
|
||||||
|
Str("source", neighbor.Source).
|
||||||
Str("mac", neighbor.Mac).
|
Str("mac", neighbor.Mac).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -87,51 +87,47 @@ func (l *LLDP) setUpCapture() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *LLDP) doCapture(logger *zerolog.Logger, rxCtx context.Context) {
|
func (l *LLDP) doCapture(logger *zerolog.Logger) {
|
||||||
defer func() {
|
l.rxWaitGroup.Add(1)
|
||||||
l.mu.Lock()
|
defer l.rxWaitGroup.Done()
|
||||||
l.rxRunning = false
|
|
||||||
l.mu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
// TODO: use a channel to handle the packets
|
// TODO: use a channel to handle the packets
|
||||||
// PacketSource.Packets() is not reliable and can cause panics and the upstream hasn't fixed it yet
|
// PacketSource.Packets() is not reliable and can cause panics and the upstream hasn't fixed it yet
|
||||||
for rxCtx.Err() == nil {
|
for {
|
||||||
if l.pktSourceRx == nil || l.tPacketRx == nil {
|
// check if the context is done before blocking call
|
||||||
logger.Error().Msg("packet source or TPacketRx not initialized")
|
select {
|
||||||
break
|
case <-l.rxCtx.Done():
|
||||||
|
logger.Info().Msg("RX context cancelled")
|
||||||
|
return
|
||||||
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Trace().Msg("waiting for next packet")
|
||||||
packet, err := l.pktSourceRx.NextPacket()
|
packet, err := l.pktSourceRx.NextPacket()
|
||||||
if err == nil {
|
logger.Trace().Interface("packet", packet).Err(err).Msg("got next packet")
|
||||||
if handleErr := l.handlePacket(packet, logger); handleErr != nil {
|
|
||||||
logger.Error().
|
|
||||||
Err(handleErr).
|
|
||||||
Msg("error handling packet")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Immediately retry for temporary network errors and EAGAIN
|
if err != nil {
|
||||||
// temporary has been deprecated and most cases are timeouts
|
logger.Error().
|
||||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
Err(err).
|
||||||
continue
|
Msg("error getting next packet")
|
||||||
}
|
|
||||||
if err == syscall.EAGAIN {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Immediately break for known unrecoverable errors
|
// Immediately break for known unrecoverable errors
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF ||
|
if err == io.EOF || err == io.ErrUnexpectedEOF ||
|
||||||
err == io.ErrNoProgress || err == io.ErrClosedPipe || err == io.ErrShortBuffer ||
|
err == io.ErrNoProgress || err == io.ErrClosedPipe || err == io.ErrShortBuffer ||
|
||||||
err == syscall.EBADF ||
|
err == syscall.EBADF ||
|
||||||
strings.Contains(err.Error(), "use of closed file") {
|
strings.Contains(err.Error(), "use of closed file") {
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.handlePacket(packet, logger); err != nil {
|
||||||
logger.Error().
|
logger.Error().
|
||||||
Err(err).
|
Err(err).
|
||||||
Msg("error receiving LLDP packet")
|
Msg("error handling packet")
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -158,9 +154,7 @@ func (l *LLDP) startCapture() error {
|
||||||
l.rxCtx, l.rxCancel = context.WithCancel(context.Background())
|
l.rxCtx, l.rxCancel = context.WithCancel(context.Background())
|
||||||
l.rxRunning = true
|
l.rxRunning = true
|
||||||
|
|
||||||
// Capture context in closure
|
go l.doCapture(&logger)
|
||||||
rxCtx := l.rxCtx
|
|
||||||
go l.doCapture(&logger, rxCtx)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -369,8 +363,24 @@ func (l *LLDP) stopCapture() error {
|
||||||
logger.Info().Msg("cancelled RX context, waiting for goroutine to finish")
|
logger.Info().Msg("cancelled RX context, waiting for goroutine to finish")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a bit for goroutine to finish
|
// stop the TPacketRx
|
||||||
time.Sleep(500 * time.Millisecond)
|
go func() {
|
||||||
|
if l.tPacketRx == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// write an empty packet to the TPacketRx to interrupt the blocking read
|
||||||
|
// it's a shitty workaround until https://github.com/google/gopacket/pull/777 is merged,
|
||||||
|
// or we have a better solution, see https://github.com/google/gopacket/issues/1064
|
||||||
|
l.tPacketRx.WritePacketData([]byte{})
|
||||||
|
}()
|
||||||
|
|
||||||
|
// wait for the goroutine to finish
|
||||||
|
start := time.Now()
|
||||||
|
l.rxWaitGroup.Wait()
|
||||||
|
logger.Info().Dur("duration", time.Since(start)).Msg("RX goroutine finished")
|
||||||
|
|
||||||
|
l.rxRunning = false
|
||||||
|
|
||||||
if l.tPacketRx != nil {
|
if l.tPacketRx != nil {
|
||||||
logger.Info().Msg("closing TPacketRx")
|
logger.Info().Msg("closing TPacketRx")
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,10 @@ var (
|
||||||
func toLLDPCapabilitiesBytes(capabilities []string) uint16 {
|
func toLLDPCapabilitiesBytes(capabilities []string) uint16 {
|
||||||
r := uint16(0)
|
r := uint16(0)
|
||||||
for _, capability := range capabilities {
|
for _, capability := range capabilities {
|
||||||
if _, ok := capabilityMap[capability]; !ok {
|
mask, ok := capabilityMap[capability]
|
||||||
continue
|
if ok {
|
||||||
|
r |= mask
|
||||||
}
|
}
|
||||||
r |= capabilityMap[capability]
|
|
||||||
}
|
}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export interface RTCState {
|
||||||
peerConnection: RTCPeerConnection | null;
|
peerConnection: RTCPeerConnection | null;
|
||||||
setPeerConnection: (pc: RTCState["peerConnection"]) => void;
|
setPeerConnection: (pc: RTCState["peerConnection"]) => void;
|
||||||
|
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => void;
|
setRpcDataChannel: (channel: RTCDataChannel | null) => void;
|
||||||
rpcDataChannel: RTCDataChannel | null;
|
rpcDataChannel: RTCDataChannel | null;
|
||||||
|
|
||||||
hidRpcDisabled: boolean;
|
hidRpcDisabled: boolean;
|
||||||
|
|
@ -178,41 +178,42 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }),
|
setPeerConnection: (pc: RTCState["peerConnection"]) => set({ peerConnection: pc }),
|
||||||
|
|
||||||
rpcDataChannel: null,
|
rpcDataChannel: null,
|
||||||
setRpcDataChannel: (channel: RTCDataChannel) => set({ rpcDataChannel: channel }),
|
setRpcDataChannel: channel => set({ rpcDataChannel: channel }),
|
||||||
|
|
||||||
hidRpcDisabled: false,
|
hidRpcDisabled: false,
|
||||||
setHidRpcDisabled: (disabled: boolean) => set({ hidRpcDisabled: disabled }),
|
setHidRpcDisabled: disabled => set({ hidRpcDisabled: disabled }),
|
||||||
|
|
||||||
rpcHidProtocolVersion: null,
|
rpcHidProtocolVersion: null,
|
||||||
setRpcHidProtocolVersion: (version: number | null) => set({ rpcHidProtocolVersion: version }),
|
setRpcHidProtocolVersion: version => set({ rpcHidProtocolVersion: version }),
|
||||||
|
|
||||||
rpcHidChannel: null,
|
rpcHidChannel: null,
|
||||||
setRpcHidChannel: (channel: RTCDataChannel) => set({ rpcHidChannel: channel }),
|
setRpcHidChannel: channel => set({ rpcHidChannel: channel }),
|
||||||
|
|
||||||
rpcHidUnreliableChannel: null,
|
rpcHidUnreliableChannel: null,
|
||||||
setRpcHidUnreliableChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableChannel: channel }),
|
setRpcHidUnreliableChannel: channel => set({ rpcHidUnreliableChannel: channel }),
|
||||||
|
|
||||||
rpcHidUnreliableNonOrderedChannel: null,
|
rpcHidUnreliableNonOrderedChannel: null,
|
||||||
setRpcHidUnreliableNonOrderedChannel: (channel: RTCDataChannel) => set({ rpcHidUnreliableNonOrderedChannel: channel }),
|
setRpcHidUnreliableNonOrderedChannel: channel =>
|
||||||
|
set({ rpcHidUnreliableNonOrderedChannel: channel }),
|
||||||
|
|
||||||
transceiver: null,
|
transceiver: null,
|
||||||
setTransceiver: (transceiver: RTCRtpTransceiver) => set({ transceiver }),
|
setTransceiver: transceiver => set({ transceiver }),
|
||||||
|
|
||||||
peerConnectionState: null,
|
peerConnectionState: null,
|
||||||
setPeerConnectionState: (state: RTCPeerConnectionState) => set({ peerConnectionState: state }),
|
setPeerConnectionState: state => set({ peerConnectionState: state }),
|
||||||
|
|
||||||
mediaStream: null,
|
mediaStream: null,
|
||||||
setMediaStream: (stream: MediaStream) => set({ mediaStream: stream }),
|
setMediaStream: stream => set({ mediaStream: stream }),
|
||||||
|
|
||||||
videoStreamStats: null,
|
videoStreamStats: null,
|
||||||
appendVideoStreamStats: (stats: RTCInboundRtpStreamStats) => set({ videoStreamStats: stats }),
|
appendVideoStreamStats: stats => set({ videoStreamStats: stats }),
|
||||||
videoStreamStatsHistory: new Map(),
|
videoStreamStatsHistory: new Map(),
|
||||||
|
|
||||||
isTurnServerInUse: false,
|
isTurnServerInUse: false,
|
||||||
setTurnServerInUse: (inUse: boolean) => set({ isTurnServerInUse: inUse }),
|
setTurnServerInUse: inUse => set({ isTurnServerInUse: inUse }),
|
||||||
|
|
||||||
inboundRtpStats: new Map(),
|
inboundRtpStats: new Map(),
|
||||||
appendInboundRtpStats: (stats: RTCInboundRtpStreamStats) => {
|
appendInboundRtpStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats),
|
inboundRtpStats: appendStatToMap(stats, prevState.inboundRtpStats),
|
||||||
}));
|
}));
|
||||||
|
|
@ -220,7 +221,7 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
clearInboundRtpStats: () => set({ inboundRtpStats: new Map() }),
|
||||||
|
|
||||||
candidatePairStats: new Map(),
|
candidatePairStats: new Map(),
|
||||||
appendCandidatePairStats: (stats: RTCIceCandidatePairStats) => {
|
appendCandidatePairStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats),
|
candidatePairStats: appendStatToMap(stats, prevState.candidatePairStats),
|
||||||
}));
|
}));
|
||||||
|
|
@ -228,21 +229,21 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
clearCandidatePairStats: () => set({ candidatePairStats: new Map() }),
|
clearCandidatePairStats: () => set({ candidatePairStats: new Map() }),
|
||||||
|
|
||||||
localCandidateStats: new Map(),
|
localCandidateStats: new Map(),
|
||||||
appendLocalCandidateStats: (stats: RTCIceCandidateStats) => {
|
appendLocalCandidateStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats),
|
localCandidateStats: appendStatToMap(stats, prevState.localCandidateStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
remoteCandidateStats: new Map(),
|
remoteCandidateStats: new Map(),
|
||||||
appendRemoteCandidateStats: (stats: RTCIceCandidateStats) => {
|
appendRemoteCandidateStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats),
|
remoteCandidateStats: appendStatToMap(stats, prevState.remoteCandidateStats),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
diskDataChannelStats: new Map(),
|
diskDataChannelStats: new Map(),
|
||||||
appendDiskDataChannelStats: (stats: RTCDataChannelStats) => {
|
appendDiskDataChannelStats: stats => {
|
||||||
set(prevState => ({
|
set(prevState => ({
|
||||||
diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats),
|
diskDataChannelStats: appendStatToMap(stats, prevState.diskDataChannelStats),
|
||||||
}));
|
}));
|
||||||
|
|
@ -250,7 +251,7 @@ export const useRTCStore = create<RTCState>(set => ({
|
||||||
|
|
||||||
// Add these new properties to the store implementation
|
// Add these new properties to the store implementation
|
||||||
terminalChannel: null,
|
terminalChannel: null,
|
||||||
setTerminalChannel: (channel: RTCDataChannel) => set({ terminalChannel: channel }),
|
setTerminalChannel: channel => set({ terminalChannel: channel }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export interface MouseMove {
|
export interface MouseMove {
|
||||||
|
|
@ -270,12 +271,20 @@ export interface MouseState {
|
||||||
export const useMouseStore = create<MouseState>(set => ({
|
export const useMouseStore = create<MouseState>(set => ({
|
||||||
mouseX: 0,
|
mouseX: 0,
|
||||||
mouseY: 0,
|
mouseY: 0,
|
||||||
setMouseMove: (move?: MouseMove) => set({ mouseMove: move }),
|
setMouseMove: move => set({ mouseMove: move }),
|
||||||
setMousePosition: (x: number, y: number) => set({ mouseX: x, mouseY: y }),
|
setMousePosition: (x, y) => set({ mouseX: x, mouseY: y }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type HdmiStates = "ready" | "no_signal" | "no_lock" | "out_of_range" | "connecting";
|
export type HdmiStates =
|
||||||
export type HdmiErrorStates = Extract<VideoState["hdmiState"], "no_signal" | "no_lock" | "out_of_range">
|
| "ready"
|
||||||
|
| "no_signal"
|
||||||
|
| "no_lock"
|
||||||
|
| "out_of_range"
|
||||||
|
| "connecting";
|
||||||
|
export type HdmiErrorStates = Extract<
|
||||||
|
VideoState["hdmiState"],
|
||||||
|
"no_signal" | "no_lock" | "out_of_range"
|
||||||
|
>;
|
||||||
|
|
||||||
export interface HdmiState {
|
export interface HdmiState {
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
|
|
@ -290,10 +299,7 @@ export interface VideoState {
|
||||||
setClientSize: (width: number, height: number) => void;
|
setClientSize: (width: number, height: number) => void;
|
||||||
setSize: (width: number, height: number) => void;
|
setSize: (width: number, height: number) => void;
|
||||||
hdmiState: HdmiStates;
|
hdmiState: HdmiStates;
|
||||||
setHdmiState: (state: {
|
setHdmiState: (state: { ready: boolean; error?: HdmiErrorStates }) => void;
|
||||||
ready: boolean;
|
|
||||||
error?: HdmiErrorStates;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useVideoStore = create<VideoState>(set => ({
|
export const useVideoStore = create<VideoState>(set => ({
|
||||||
|
|
@ -304,7 +310,8 @@ export const useVideoStore = create<VideoState>(set => ({
|
||||||
clientHeight: 0,
|
clientHeight: 0,
|
||||||
|
|
||||||
// The video element's client size
|
// The video element's client size
|
||||||
setClientSize: (clientWidth: number, clientHeight: number) => set({ clientWidth, clientHeight }),
|
setClientSize: (clientWidth: number, clientHeight: number) =>
|
||||||
|
set({ clientWidth, clientHeight }),
|
||||||
|
|
||||||
// Resolution
|
// Resolution
|
||||||
setSize: (width: number, height: number) => set({ width, height }),
|
setSize: (width: number, height: number) => set({ width, height }),
|
||||||
|
|
@ -451,13 +458,15 @@ export interface MountMediaState {
|
||||||
|
|
||||||
export const useMountMediaStore = create<MountMediaState>(set => ({
|
export const useMountMediaStore = create<MountMediaState>(set => ({
|
||||||
remoteVirtualMediaState: null,
|
remoteVirtualMediaState: null,
|
||||||
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) => set({ remoteVirtualMediaState: state }),
|
setRemoteVirtualMediaState: (state: MountMediaState["remoteVirtualMediaState"]) =>
|
||||||
|
set({ remoteVirtualMediaState: state }),
|
||||||
|
|
||||||
modalView: "mode",
|
modalView: "mode",
|
||||||
setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }),
|
setModalView: (view: MountMediaState["modalView"]) => set({ modalView: view }),
|
||||||
|
|
||||||
isMountMediaDialogOpen: false,
|
isMountMediaDialogOpen: false,
|
||||||
setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) => set({ isMountMediaDialogOpen: isOpen }),
|
setIsMountMediaDialogOpen: (isOpen: MountMediaState["isMountMediaDialogOpen"]) =>
|
||||||
|
set({ isMountMediaDialogOpen: isOpen }),
|
||||||
|
|
||||||
uploadedFiles: [],
|
uploadedFiles: [],
|
||||||
addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) =>
|
addUploadedFile: (file: { name: string; size: string; uploadedAt: string }) =>
|
||||||
|
|
@ -474,7 +483,7 @@ export interface KeyboardLedState {
|
||||||
compose: boolean;
|
compose: boolean;
|
||||||
kana: boolean;
|
kana: boolean;
|
||||||
shift: boolean; // Optional, as not all keyboards have a shift LED
|
shift: boolean; // Optional, as not all keyboards have a shift LED
|
||||||
};
|
}
|
||||||
|
|
||||||
export const hidKeyBufferSize = 6;
|
export const hidKeyBufferSize = 6;
|
||||||
export const hidErrorRollOver = 0x01;
|
export const hidErrorRollOver = 0x01;
|
||||||
|
|
@ -509,14 +518,23 @@ export interface HidState {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useHidStore = create<HidState>(set => ({
|
export const useHidStore = create<HidState>(set => ({
|
||||||
keyboardLedState: { num_lock: false, caps_lock: false, scroll_lock: false, compose: false, kana: false, shift: false } as KeyboardLedState,
|
keyboardLedState: {
|
||||||
setKeyboardLedState: (ledState: KeyboardLedState): void => set({ keyboardLedState: ledState }),
|
num_lock: false,
|
||||||
|
caps_lock: false,
|
||||||
|
scroll_lock: false,
|
||||||
|
compose: false,
|
||||||
|
kana: false,
|
||||||
|
shift: false,
|
||||||
|
} as KeyboardLedState,
|
||||||
|
setKeyboardLedState: (ledState: KeyboardLedState): void =>
|
||||||
|
set({ keyboardLedState: ledState }),
|
||||||
|
|
||||||
keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
|
keysDownState: { modifier: 0, keys: [0, 0, 0, 0, 0, 0] } as KeysDownState,
|
||||||
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
setKeysDownState: (state: KeysDownState): void => set({ keysDownState: state }),
|
||||||
|
|
||||||
isVirtualKeyboardEnabled: false,
|
isVirtualKeyboardEnabled: false,
|
||||||
setVirtualKeyboardEnabled: (enabled: boolean): void => set({ isVirtualKeyboardEnabled: enabled }),
|
setVirtualKeyboardEnabled: (enabled: boolean): void =>
|
||||||
|
set({ isVirtualKeyboardEnabled: enabled }),
|
||||||
|
|
||||||
isPasteInProgress: false,
|
isPasteInProgress: false,
|
||||||
setPasteModeEnabled: (enabled: boolean): void => set({ isPasteInProgress: enabled }),
|
setPasteModeEnabled: (enabled: boolean): void => set({ isPasteInProgress: enabled }),
|
||||||
|
|
@ -568,7 +586,7 @@ export interface OtaState {
|
||||||
|
|
||||||
systemUpdateProgress: number;
|
systemUpdateProgress: number;
|
||||||
systemUpdatedAt: string | null;
|
systemUpdatedAt: string | null;
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface UpdateState {
|
export interface UpdateState {
|
||||||
isUpdatePending: boolean;
|
isUpdatePending: boolean;
|
||||||
|
|
@ -580,7 +598,7 @@ export interface UpdateState {
|
||||||
otaState: OtaState;
|
otaState: OtaState;
|
||||||
setOtaState: (state: OtaState) => void;
|
setOtaState: (state: OtaState) => void;
|
||||||
|
|
||||||
modalView: UpdateModalViews
|
modalView: UpdateModalViews;
|
||||||
setModalView: (view: UpdateModalViews) => void;
|
setModalView: (view: UpdateModalViews) => void;
|
||||||
|
|
||||||
updateErrorMessage: string | null;
|
updateErrorMessage: string | null;
|
||||||
|
|
@ -620,12 +638,11 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||||
|
|
||||||
updateErrorMessage: null,
|
updateErrorMessage: null,
|
||||||
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
setUpdateErrorMessage: (errorMessage: string) =>
|
||||||
|
set({ updateErrorMessage: errorMessage }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export type UsbConfigModalViews =
|
export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess";
|
||||||
| "updateUsbConfig"
|
|
||||||
| "updateUsbConfigSuccess";
|
|
||||||
|
|
||||||
export interface UsbConfigModalState {
|
export interface UsbConfigModalState {
|
||||||
modalView: UsbConfigModalViews;
|
modalView: UsbConfigModalViews;
|
||||||
|
|
@ -1012,5 +1029,5 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
} finally {
|
} finally {
|
||||||
set({ loading: false });
|
set({ loading: false });
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -559,8 +559,9 @@ export default function KvmIdRoute() {
|
||||||
clearCandidatePairStats();
|
clearCandidatePairStats();
|
||||||
setSidebarView(null);
|
setSidebarView(null);
|
||||||
setPeerConnection(null);
|
setPeerConnection(null);
|
||||||
|
setRpcDataChannel(null);
|
||||||
};
|
};
|
||||||
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
|
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView, setRpcDataChannel]);
|
||||||
|
|
||||||
// TURN server usage detection
|
// TURN server usage detection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,47 @@ export interface JsonRpcCallResponse<T = unknown> {
|
||||||
let rpcCallCounter = 0;
|
let rpcCallCounter = 0;
|
||||||
|
|
||||||
// Helper: wait for RTC data channel to be ready
|
// Helper: wait for RTC data channel to be ready
|
||||||
|
// This waits indefinitely for the channel to be ready, only aborting via the signal
|
||||||
|
// Throws if the channel instance changed while waiting (stale connection detected)
|
||||||
async function waitForRtcReady(signal: AbortSignal): Promise<RTCDataChannel> {
|
async function waitForRtcReady(signal: AbortSignal): Promise<RTCDataChannel> {
|
||||||
const pollInterval = 100;
|
const pollInterval = 100;
|
||||||
|
let lastSeenChannel: RTCDataChannel | null = null;
|
||||||
|
|
||||||
while (!signal.aborted) {
|
while (!signal.aborted) {
|
||||||
const state = useRTCStore.getState();
|
const state = useRTCStore.getState();
|
||||||
if (state.rpcDataChannel?.readyState === "open") {
|
const currentChannel = state.rpcDataChannel;
|
||||||
return state.rpcDataChannel;
|
|
||||||
|
// Channel instance changed (new connection replaced old one)
|
||||||
|
if (lastSeenChannel && currentChannel && lastSeenChannel !== currentChannel) {
|
||||||
|
console.debug("[waitForRtcReady] Channel instance changed, aborting wait");
|
||||||
|
throw new Error("RTC connection changed while waiting for readiness");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Channel was removed from store (connection closed)
|
||||||
|
if (lastSeenChannel && !currentChannel) {
|
||||||
|
console.debug("[waitForRtcReady] Channel was removed from store, aborting wait");
|
||||||
|
throw new Error("RTC connection was closed while waiting for readiness");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No channel yet, keep waiting
|
||||||
|
if (!currentChannel) {
|
||||||
|
await sleep(pollInterval);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track this channel instance
|
||||||
|
lastSeenChannel = currentChannel;
|
||||||
|
|
||||||
|
// Channel is ready!
|
||||||
|
if (currentChannel.readyState === "open") {
|
||||||
|
return currentChannel;
|
||||||
|
}
|
||||||
|
|
||||||
await sleep(pollInterval);
|
await sleep(pollInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Signal was aborted for some reason
|
||||||
|
console.debug("[waitForRtcReady] Aborted via signal");
|
||||||
throw new Error("RTC readiness check aborted");
|
throw new Error("RTC readiness check aborted");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,25 +127,26 @@ export async function callJsonRpc<T = unknown>(
|
||||||
const timeout = options.attemptTimeoutMs || 5000;
|
const timeout = options.attemptTimeoutMs || 5000;
|
||||||
|
|
||||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
const abortController = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => abortController.abort(), timeout);
|
|
||||||
|
|
||||||
// Exponential backoff for retries that starts at 500ms up to a maximum of 10 seconds
|
// Exponential backoff for retries that starts at 500ms up to a maximum of 10 seconds
|
||||||
const backoffMs = Math.min(500 * Math.pow(2, attempt), 10000);
|
const backoffMs = Math.min(500 * Math.pow(2, attempt), 10000);
|
||||||
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Wait for RTC readiness
|
// Wait for RTC readiness without timeout - this allows time for WebRTC to connect
|
||||||
const rpcDataChannel = await waitForRtcReady(abortController.signal);
|
const readyAbortController = new AbortController();
|
||||||
|
const rpcDataChannel = await waitForRtcReady(readyAbortController.signal);
|
||||||
|
|
||||||
|
// Now apply timeout only to the actual RPC request/response
|
||||||
|
const rpcAbortController = new AbortController();
|
||||||
|
timeoutId = setTimeout(() => rpcAbortController.abort(), timeout);
|
||||||
|
|
||||||
// Send RPC request and wait for response
|
// Send RPC request and wait for response
|
||||||
const response = await sendRpcRequest<T>(
|
const response = await sendRpcRequest<T>(
|
||||||
rpcDataChannel,
|
rpcDataChannel,
|
||||||
options,
|
options,
|
||||||
abortController.signal,
|
rpcAbortController.signal,
|
||||||
);
|
);
|
||||||
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
// Retry on error if attempts remain
|
// Retry on error if attempts remain
|
||||||
if (response.error && attempt < maxAttempts - 1) {
|
if (response.error && attempt < maxAttempts - 1) {
|
||||||
await sleep(backoffMs);
|
await sleep(backoffMs);
|
||||||
|
|
@ -124,8 +155,6 @@ export async function callJsonRpc<T = unknown>(
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
clearTimeout(timeoutId);
|
|
||||||
|
|
||||||
// Retry on timeout/error if attempts remain
|
// Retry on timeout/error if attempts remain
|
||||||
if (attempt < maxAttempts - 1) {
|
if (attempt < maxAttempts - 1) {
|
||||||
await sleep(backoffMs);
|
await sleep(backoffMs);
|
||||||
|
|
@ -135,6 +164,10 @@ export async function callJsonRpc<T = unknown>(
|
||||||
throw error instanceof Error
|
throw error instanceof Error
|
||||||
? error
|
? error
|
||||||
: new Error(`JSON-RPC call failed after ${timeout}ms`);
|
: new Error(`JSON-RPC call failed after ${timeout}ms`);
|
||||||
|
} finally {
|
||||||
|
if (timeoutId !== null) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
24
web.go
24
web.go
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -184,6 +185,8 @@ func setupRouter() *gin.Engine {
|
||||||
protected.PUT("/auth/password-local", handleUpdatePassword)
|
protected.PUT("/auth/password-local", handleUpdatePassword)
|
||||||
protected.DELETE("/auth/local-password", handleDeletePassword)
|
protected.DELETE("/auth/local-password", handleDeletePassword)
|
||||||
protected.POST("/storage/upload", handleUploadHttp)
|
protected.POST("/storage/upload", handleUploadHttp)
|
||||||
|
|
||||||
|
protected.POST("/device/send-wol/:mac-addr", handleSendWOLMagicPacket)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catch-all route for SPA
|
// Catch-all route for SPA
|
||||||
|
|
@ -341,7 +344,6 @@ func handleWebRTCSignalWsMessages(
|
||||||
|
|
||||||
l.Trace().Msg("sending ping frame")
|
l.Trace().Msg("sending ping frame")
|
||||||
err := wsCon.Ping(runCtx)
|
err := wsCon.Ping(runCtx)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warn().Str("error", err.Error()).Msg("websocket ping error")
|
l.Warn().Str("error", err.Error()).Msg("websocket ping error")
|
||||||
cancelRun()
|
cancelRun()
|
||||||
|
|
@ -807,3 +809,23 @@ func handleSetup(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"})
|
c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSendWOLMagicPacket(c *gin.Context) {
|
||||||
|
inputMacAddr := c.Param("mac-addr")
|
||||||
|
macAddr, err := net.ParseMAC(inputMacAddr)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Str("sendWol", inputMacAddr).Msg("Invalid mac address provided")
|
||||||
|
c.String(http.StatusBadRequest, "Invalid mac address provided")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
macAddrString := macAddr.String()
|
||||||
|
err = rpcSendWOLMagicPacket(macAddrString)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn().Err(err).Str("sendWOL", macAddrString).Msg("Failed to send WOL magic packet")
|
||||||
|
c.String(http.StatusInternalServerError, "Failed to send WOL to %s: %v", macAddrString, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.String(http.StatusOK, "WOL sent to %s ", macAddr)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue