mirror of https://github.com/jetkvm/kvm.git
Updates: merge 'dev' intu 'feat/audio-support'
This commit is contained in:
parent
9cb976ab8d
commit
d9072673c0
|
@ -112,7 +112,8 @@ func handleKeyboardReportDirect(params map[string]interface{}) (interface{}, err
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, rpcKeyboardReport(modifier, keys)
|
_, err = rpcKeyboardReport(modifier, keys)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct handler for absolute mouse reports
|
// Direct handler for absolute mouse reports
|
||||||
|
|
|
@ -1,293 +0,0 @@
|
||||||
package usbgadget
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UsbGadgetInterface defines the interface for USB gadget operations
|
|
||||||
// This allows for mocking in tests and separating hardware operations from business logic
|
|
||||||
type UsbGadgetInterface interface {
|
|
||||||
// Configuration methods
|
|
||||||
Init() error
|
|
||||||
UpdateGadgetConfig() error
|
|
||||||
SetGadgetConfig(config *Config)
|
|
||||||
SetGadgetDevices(devices *Devices)
|
|
||||||
OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool)
|
|
||||||
|
|
||||||
// Hardware control methods
|
|
||||||
RebindUsb(ignoreUnbindError bool) error
|
|
||||||
IsUDCBound() (bool, error)
|
|
||||||
BindUDC() error
|
|
||||||
UnbindUDC() error
|
|
||||||
|
|
||||||
// HID file management
|
|
||||||
PreOpenHidFiles()
|
|
||||||
CloseHidFiles()
|
|
||||||
|
|
||||||
// Transaction methods
|
|
||||||
WithTransaction(fn func() error) error
|
|
||||||
WithTransactionTimeout(fn func() error, timeout time.Duration) error
|
|
||||||
|
|
||||||
// Path methods
|
|
||||||
GetConfigPath(itemKey string) (string, error)
|
|
||||||
GetPath(itemKey string) (string, error)
|
|
||||||
|
|
||||||
// Input methods (matching actual UsbGadget implementation)
|
|
||||||
KeyboardReport(modifier uint8, keys []uint8) error
|
|
||||||
AbsMouseReport(x, y int, buttons uint8) error
|
|
||||||
AbsMouseWheelReport(wheelY int8) error
|
|
||||||
RelMouseReport(mx, my int8, buttons uint8) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure UsbGadget implements the interface
|
|
||||||
var _ UsbGadgetInterface = (*UsbGadget)(nil)
|
|
||||||
|
|
||||||
// MockUsbGadget provides a mock implementation for testing
|
|
||||||
type MockUsbGadget struct {
|
|
||||||
name string
|
|
||||||
enabledDevices Devices
|
|
||||||
customConfig Config
|
|
||||||
log *zerolog.Logger
|
|
||||||
|
|
||||||
// Mock state
|
|
||||||
initCalled bool
|
|
||||||
updateConfigCalled bool
|
|
||||||
rebindCalled bool
|
|
||||||
udcBound bool
|
|
||||||
hidFilesOpen bool
|
|
||||||
transactionCount int
|
|
||||||
|
|
||||||
// Mock behavior controls
|
|
||||||
ShouldFailInit bool
|
|
||||||
ShouldFailUpdateConfig bool
|
|
||||||
ShouldFailRebind bool
|
|
||||||
ShouldFailUDCBind bool
|
|
||||||
InitDelay time.Duration
|
|
||||||
UpdateConfigDelay time.Duration
|
|
||||||
RebindDelay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockUsbGadget creates a new mock USB gadget for testing
|
|
||||||
func NewMockUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *MockUsbGadget {
|
|
||||||
if enabledDevices == nil {
|
|
||||||
enabledDevices = &defaultUsbGadgetDevices
|
|
||||||
}
|
|
||||||
if config == nil {
|
|
||||||
config = &Config{isEmpty: true}
|
|
||||||
}
|
|
||||||
if logger == nil {
|
|
||||||
logger = defaultLogger
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MockUsbGadget{
|
|
||||||
name: name,
|
|
||||||
enabledDevices: *enabledDevices,
|
|
||||||
customConfig: *config,
|
|
||||||
log: logger,
|
|
||||||
udcBound: false,
|
|
||||||
hidFilesOpen: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init mocks USB gadget initialization
|
|
||||||
func (m *MockUsbGadget) Init() error {
|
|
||||||
if m.InitDelay > 0 {
|
|
||||||
time.Sleep(m.InitDelay)
|
|
||||||
}
|
|
||||||
if m.ShouldFailInit {
|
|
||||||
return m.logError("mock init failure", nil)
|
|
||||||
}
|
|
||||||
m.initCalled = true
|
|
||||||
m.udcBound = true
|
|
||||||
m.log.Info().Msg("mock USB gadget initialized")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateGadgetConfig mocks gadget configuration update
|
|
||||||
func (m *MockUsbGadget) UpdateGadgetConfig() error {
|
|
||||||
if m.UpdateConfigDelay > 0 {
|
|
||||||
time.Sleep(m.UpdateConfigDelay)
|
|
||||||
}
|
|
||||||
if m.ShouldFailUpdateConfig {
|
|
||||||
return m.logError("mock update config failure", nil)
|
|
||||||
}
|
|
||||||
m.updateConfigCalled = true
|
|
||||||
m.log.Info().Msg("mock USB gadget config updated")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGadgetConfig mocks setting gadget configuration
|
|
||||||
func (m *MockUsbGadget) SetGadgetConfig(config *Config) {
|
|
||||||
if config != nil {
|
|
||||||
m.customConfig = *config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetGadgetDevices mocks setting enabled devices
|
|
||||||
func (m *MockUsbGadget) SetGadgetDevices(devices *Devices) {
|
|
||||||
if devices != nil {
|
|
||||||
m.enabledDevices = *devices
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// OverrideGadgetConfig mocks gadget config override
|
|
||||||
func (m *MockUsbGadget) OverrideGadgetConfig(itemKey string, itemAttr string, value string) (error, bool) {
|
|
||||||
m.log.Info().Str("itemKey", itemKey).Str("itemAttr", itemAttr).Str("value", value).Msg("mock override gadget config")
|
|
||||||
return nil, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// RebindUsb mocks USB rebinding
|
|
||||||
func (m *MockUsbGadget) RebindUsb(ignoreUnbindError bool) error {
|
|
||||||
if m.RebindDelay > 0 {
|
|
||||||
time.Sleep(m.RebindDelay)
|
|
||||||
}
|
|
||||||
if m.ShouldFailRebind {
|
|
||||||
return m.logError("mock rebind failure", nil)
|
|
||||||
}
|
|
||||||
m.rebindCalled = true
|
|
||||||
m.log.Info().Msg("mock USB gadget rebound")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUDCBound mocks UDC binding status check
|
|
||||||
func (m *MockUsbGadget) IsUDCBound() (bool, error) {
|
|
||||||
return m.udcBound, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BindUDC mocks UDC binding
|
|
||||||
func (m *MockUsbGadget) BindUDC() error {
|
|
||||||
if m.ShouldFailUDCBind {
|
|
||||||
return m.logError("mock UDC bind failure", nil)
|
|
||||||
}
|
|
||||||
m.udcBound = true
|
|
||||||
m.log.Info().Msg("mock UDC bound")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnbindUDC mocks UDC unbinding
|
|
||||||
func (m *MockUsbGadget) UnbindUDC() error {
|
|
||||||
m.udcBound = false
|
|
||||||
m.log.Info().Msg("mock UDC unbound")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreOpenHidFiles mocks HID file pre-opening
|
|
||||||
func (m *MockUsbGadget) PreOpenHidFiles() {
|
|
||||||
m.hidFilesOpen = true
|
|
||||||
m.log.Info().Msg("mock HID files pre-opened")
|
|
||||||
}
|
|
||||||
|
|
||||||
// CloseHidFiles mocks HID file closing
|
|
||||||
func (m *MockUsbGadget) CloseHidFiles() {
|
|
||||||
m.hidFilesOpen = false
|
|
||||||
m.log.Info().Msg("mock HID files closed")
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTransaction mocks transaction execution
|
|
||||||
func (m *MockUsbGadget) WithTransaction(fn func() error) error {
|
|
||||||
return m.WithTransactionTimeout(fn, 60*time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTransactionTimeout mocks transaction execution with timeout
|
|
||||||
func (m *MockUsbGadget) WithTransactionTimeout(fn func() error, timeout time.Duration) error {
|
|
||||||
m.transactionCount++
|
|
||||||
m.log.Info().Int("transactionCount", m.transactionCount).Msg("mock transaction started")
|
|
||||||
|
|
||||||
// Execute the function in a mock transaction context
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
done := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
done <- fn()
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-done:
|
|
||||||
if err != nil {
|
|
||||||
m.log.Error().Err(err).Msg("mock transaction failed")
|
|
||||||
} else {
|
|
||||||
m.log.Info().Msg("mock transaction completed")
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
case <-ctx.Done():
|
|
||||||
m.log.Error().Dur("timeout", timeout).Msg("mock transaction timed out")
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfigPath mocks getting configuration path
|
|
||||||
func (m *MockUsbGadget) GetConfigPath(itemKey string) (string, error) {
|
|
||||||
return "/mock/config/path/" + itemKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPath mocks getting path
|
|
||||||
func (m *MockUsbGadget) GetPath(itemKey string) (string, error) {
|
|
||||||
return "/mock/path/" + itemKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// KeyboardReport mocks keyboard input
|
|
||||||
func (m *MockUsbGadget) KeyboardReport(modifier uint8, keys []uint8) error {
|
|
||||||
m.log.Debug().Uint8("modifier", modifier).Int("keyCount", len(keys)).Msg("mock keyboard input sent")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbsMouseReport mocks absolute mouse input
|
|
||||||
func (m *MockUsbGadget) AbsMouseReport(x, y int, buttons uint8) error {
|
|
||||||
m.log.Debug().Int("x", x).Int("y", y).Uint8("buttons", buttons).Msg("mock absolute mouse input sent")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AbsMouseWheelReport mocks absolute mouse wheel input
|
|
||||||
func (m *MockUsbGadget) AbsMouseWheelReport(wheelY int8) error {
|
|
||||||
m.log.Debug().Int8("wheelY", wheelY).Msg("mock absolute mouse wheel input sent")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RelMouseReport mocks relative mouse input
|
|
||||||
func (m *MockUsbGadget) RelMouseReport(mx, my int8, buttons uint8) error {
|
|
||||||
m.log.Debug().Int8("mx", mx).Int8("my", my).Uint8("buttons", buttons).Msg("mock relative mouse input sent")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods for mock
|
|
||||||
func (m *MockUsbGadget) logError(msg string, err error) error {
|
|
||||||
if err == nil {
|
|
||||||
err = fmt.Errorf("%s", msg)
|
|
||||||
}
|
|
||||||
m.log.Error().Err(err).Msg(msg)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock state inspection methods for testing
|
|
||||||
func (m *MockUsbGadget) IsInitCalled() bool {
|
|
||||||
return m.initCalled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUsbGadget) IsUpdateConfigCalled() bool {
|
|
||||||
return m.updateConfigCalled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUsbGadget) IsRebindCalled() bool {
|
|
||||||
return m.rebindCalled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUsbGadget) IsHidFilesOpen() bool {
|
|
||||||
return m.hidFilesOpen
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUsbGadget) GetTransactionCount() int {
|
|
||||||
return m.transactionCount
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUsbGadget) GetEnabledDevices() Devices {
|
|
||||||
return m.enabledDevices
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *MockUsbGadget) GetCustomConfig() Config {
|
|
||||||
return m.customConfig
|
|
||||||
}
|
|
200
native.go
200
native.go
|
@ -1,10 +1,9 @@
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
package kvm
|
package kvm
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
@ -15,6 +14,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/resource"
|
"github.com/jetkvm/kvm/resource"
|
||||||
|
|
||||||
|
"github.com/pion/webrtc/v4/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ctrlSocketConn net.Conn
|
var ctrlSocketConn net.Conn
|
||||||
|
@ -55,15 +56,200 @@ func CallCtrlAction(action string, params map[string]any) (*CtrlResponse, error)
|
||||||
Seq: seq,
|
Seq: seq,
|
||||||
Params: params,
|
Params: params,
|
||||||
}
|
}
|
||||||
cmd.Stdout = &nativeOutput{logger: nativeLogger}
|
|
||||||
cmd.Stderr = &nativeOutput{logger: nativeLogger}
|
|
||||||
|
|
||||||
err := cmd.Start()
|
responseChan := make(chan *CtrlResponse)
|
||||||
|
ongoingRequests[seq] = responseChan
|
||||||
|
seq++
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(ctrlAction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
delete(ongoingRequests, ctrlAction.Seq)
|
||||||
|
return nil, fmt.Errorf("error marshaling ctrl action: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd, nil
|
scopedLogger := nativeLogger.With().
|
||||||
|
Str("action", ctrlAction.Action).
|
||||||
|
Interface("params", ctrlAction.Params).Logger()
|
||||||
|
|
||||||
|
scopedLogger.Debug().Msg("sending ctrl action")
|
||||||
|
|
||||||
|
err = WriteCtrlMessage(jsonData)
|
||||||
|
if err != nil {
|
||||||
|
delete(ongoingRequests, ctrlAction.Seq)
|
||||||
|
return nil, ErrorfL(&scopedLogger, "error writing ctrl message", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case response := <-responseChan:
|
||||||
|
delete(ongoingRequests, seq)
|
||||||
|
if response.Error != "" {
|
||||||
|
return nil, ErrorfL(
|
||||||
|
&scopedLogger,
|
||||||
|
"error native response: %s",
|
||||||
|
errors.New(response.Error),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
close(responseChan)
|
||||||
|
delete(ongoingRequests, seq)
|
||||||
|
return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteCtrlMessage(message []byte) error {
|
||||||
|
if ctrlSocketConn == nil {
|
||||||
|
return fmt.Errorf("ctrl socket not conn ected")
|
||||||
|
}
|
||||||
|
_, err := ctrlSocketConn.Write(message)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var nativeCtrlSocketListener net.Listener //nolint:unused
|
||||||
|
var nativeVideoSocketListener net.Listener //nolint:unused
|
||||||
|
|
||||||
|
var ctrlClientConnected = make(chan struct{})
|
||||||
|
|
||||||
|
func waitCtrlClientConnected() {
|
||||||
|
<-ctrlClientConnected
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener {
|
||||||
|
scopedLogger := nativeLogger.With().
|
||||||
|
Str("socket_path", socketPath).
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
// Remove the socket file if it already exists
|
||||||
|
if _, err := os.Stat(socketPath); err == nil {
|
||||||
|
if err := os.Remove(socketPath); err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listener, err := net.Listen("unixpacket", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("failed to start server")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedLogger.Info().Msg("server listening")
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isCtrl {
|
||||||
|
// check if the channel is closed
|
||||||
|
select {
|
||||||
|
case <-ctrlClientConnected:
|
||||||
|
scopedLogger.Debug().Msg("ctrl client reconnected")
|
||||||
|
default:
|
||||||
|
close(ctrlClientConnected)
|
||||||
|
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go handleClient(conn)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartNativeCtrlSocketServer() {
|
||||||
|
nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true)
|
||||||
|
nativeLogger.Debug().Msg("native app ctrl sock started")
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartNativeVideoSocketServer() {
|
||||||
|
nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false)
|
||||||
|
nativeLogger.Debug().Msg("native app video sock started")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCtrlClient(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
scopedLogger := nativeLogger.With().
|
||||||
|
Str("addr", conn.RemoteAddr().String()).
|
||||||
|
Str("type", "ctrl").
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
scopedLogger.Info().Msg("native ctrl socket client connected")
|
||||||
|
if ctrlSocketConn != nil {
|
||||||
|
scopedLogger.Debug().Msg("closing existing native socket connection")
|
||||||
|
ctrlSocketConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctrlSocketConn = conn
|
||||||
|
|
||||||
|
// Restore HDMI EDID if applicable
|
||||||
|
go restoreHdmiEdid()
|
||||||
|
|
||||||
|
readBuf := make([]byte, 4096)
|
||||||
|
for {
|
||||||
|
n, err := conn.Read(readBuf)
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("error reading from ctrl sock")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
readMsg := string(readBuf[:n])
|
||||||
|
|
||||||
|
ctrlResp := CtrlResponse{}
|
||||||
|
err = json.Unmarshal([]byte(readMsg), &ctrlResp)
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
scopedLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg")
|
||||||
|
|
||||||
|
if ctrlResp.Seq != 0 {
|
||||||
|
responseChan, ok := ongoingRequests[ctrlResp.Seq]
|
||||||
|
if ok {
|
||||||
|
responseChan <- &ctrlResp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch ctrlResp.Event {
|
||||||
|
case "video_input_state":
|
||||||
|
HandleVideoStateMessage(ctrlResp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scopedLogger.Debug().Msg("ctrl sock disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleVideoClient(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
scopedLogger := nativeLogger.With().
|
||||||
|
Str("addr", conn.RemoteAddr().String()).
|
||||||
|
Str("type", "video").
|
||||||
|
Logger()
|
||||||
|
|
||||||
|
scopedLogger.Info().Msg("native video socket client connected")
|
||||||
|
|
||||||
|
inboundPacket := make([]byte, maxFrameSize)
|
||||||
|
lastFrame := time.Now()
|
||||||
|
for {
|
||||||
|
n, err := conn.Read(inboundPacket)
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("error during read")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
sinceLastFrame := now.Sub(lastFrame)
|
||||||
|
lastFrame = now
|
||||||
|
if currentSession != nil {
|
||||||
|
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
|
||||||
|
if err != nil {
|
||||||
|
scopedLogger.Warn().Err(err).Msg("error writing sample")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
|
func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package kvm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type nativeOutput struct {
|
||||||
|
mu *sync.Mutex
|
||||||
|
logger *zerolog.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *nativeOutput) Write(p []byte) (n int, err error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
w.logger.Msg(string(p))
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func startNativeBinary(binaryPath string) (*exec.Cmd, error) {
|
||||||
|
// Run the binary in the background
|
||||||
|
cmd := exec.Command(binaryPath)
|
||||||
|
|
||||||
|
nativeOutputLock := sync.Mutex{}
|
||||||
|
nativeStdout := &nativeOutput{
|
||||||
|
mu: &nativeOutputLock,
|
||||||
|
logger: nativeLogger.Info().Str("pipe", "stdout"),
|
||||||
|
}
|
||||||
|
nativeStderr := &nativeOutput{
|
||||||
|
mu: &nativeOutputLock,
|
||||||
|
logger: nativeLogger.Info().Str("pipe", "stderr"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect stdout and stderr to the current process
|
||||||
|
cmd.Stdout = nativeStdout
|
||||||
|
cmd.Stderr = nativeStderr
|
||||||
|
|
||||||
|
// Set the process group ID so we can kill the process and its children when this process exits
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
Setpgid: true,
|
||||||
|
Pdeathsig: syscall.SIGKILL,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the command
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to start binary: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd, nil
|
||||||
|
}
|
|
@ -8,9 +8,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func startNativeBinary(binaryPath string) (*exec.Cmd, error) {
|
func startNativeBinary(binaryPath string) (*exec.Cmd, error) {
|
||||||
return nil, fmt.Errorf("startNativeBinary is only supported on Linux")
|
return nil, fmt.Errorf("not supported")
|
||||||
}
|
|
||||||
|
|
||||||
func ExtractAndRunNativeBin() error {
|
|
||||||
return fmt.Errorf("ExtractAndRunNativeBin is only supported on Linux")
|
|
||||||
}
|
}
|
||||||
|
|
343
native_shared.go
343
native_shared.go
|
@ -1,343 +0,0 @@
|
||||||
package kvm
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jetkvm/kvm/resource"
|
|
||||||
"github.com/pion/webrtc/v4/pkg/media"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CtrlAction struct {
|
|
||||||
Action string `json:"action"`
|
|
||||||
Seq int32 `json:"seq,omitempty"`
|
|
||||||
Params map[string]interface{} `json:"params,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CtrlResponse struct {
|
|
||||||
Seq int32 `json:"seq,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
Errno int32 `json:"errno,omitempty"`
|
|
||||||
Result map[string]interface{} `json:"result,omitempty"`
|
|
||||||
Event string `json:"event,omitempty"`
|
|
||||||
Data json.RawMessage `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EventHandler func(event CtrlResponse)
|
|
||||||
|
|
||||||
var seq int32 = 1
|
|
||||||
|
|
||||||
var ongoingRequests = make(map[int32]chan *CtrlResponse)
|
|
||||||
|
|
||||||
var lock = &sync.Mutex{}
|
|
||||||
|
|
||||||
var ctrlSocketConn net.Conn
|
|
||||||
|
|
||||||
var nativeCtrlSocketListener net.Listener //nolint:unused
|
|
||||||
var nativeVideoSocketListener net.Listener //nolint:unused
|
|
||||||
|
|
||||||
var ctrlClientConnected = make(chan struct{})
|
|
||||||
|
|
||||||
func waitCtrlClientConnected() {
|
|
||||||
<-ctrlClientConnected
|
|
||||||
}
|
|
||||||
|
|
||||||
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
|
|
||||||
lock.Lock()
|
|
||||||
defer lock.Unlock()
|
|
||||||
ctrlAction := CtrlAction{
|
|
||||||
Action: action,
|
|
||||||
Seq: seq,
|
|
||||||
Params: params,
|
|
||||||
}
|
|
||||||
|
|
||||||
responseChan := make(chan *CtrlResponse)
|
|
||||||
ongoingRequests[seq] = responseChan
|
|
||||||
seq++
|
|
||||||
|
|
||||||
jsonData, err := json.Marshal(ctrlAction)
|
|
||||||
if err != nil {
|
|
||||||
delete(ongoingRequests, ctrlAction.Seq)
|
|
||||||
return nil, fmt.Errorf("error marshaling ctrl action: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
scopedLogger := nativeLogger.With().
|
|
||||||
Str("action", ctrlAction.Action).
|
|
||||||
Interface("params", ctrlAction.Params).Logger()
|
|
||||||
|
|
||||||
scopedLogger.Debug().Msg("sending ctrl action")
|
|
||||||
|
|
||||||
err = WriteCtrlMessage(jsonData)
|
|
||||||
if err != nil {
|
|
||||||
delete(ongoingRequests, ctrlAction.Seq)
|
|
||||||
return nil, ErrorfL(&scopedLogger, "error writing ctrl message", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case response := <-responseChan:
|
|
||||||
delete(ongoingRequests, seq)
|
|
||||||
if response.Error != "" {
|
|
||||||
return nil, ErrorfL(
|
|
||||||
&scopedLogger,
|
|
||||||
"error native response: %s",
|
|
||||||
errors.New(response.Error),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
case <-time.After(5 * time.Second):
|
|
||||||
close(responseChan)
|
|
||||||
delete(ongoingRequests, seq)
|
|
||||||
return nil, ErrorfL(&scopedLogger, "timeout waiting for response", nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WriteCtrlMessage(message []byte) error {
|
|
||||||
if ctrlSocketConn == nil {
|
|
||||||
return fmt.Errorf("ctrl socket not connected")
|
|
||||||
}
|
|
||||||
_, err := ctrlSocketConn.Write(message)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isCtrl bool) net.Listener {
|
|
||||||
scopedLogger := nativeLogger.With().
|
|
||||||
Str("socket_path", socketPath).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
// Remove the socket file if it already exists
|
|
||||||
if _, err := os.Stat(socketPath); err == nil {
|
|
||||||
if err := os.Remove(socketPath); err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to remove existing socket file")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
listener, err := net.Listen("unixpacket", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to start server")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("server listening")
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isCtrl {
|
|
||||||
// check if the channel is closed
|
|
||||||
select {
|
|
||||||
case <-ctrlClientConnected:
|
|
||||||
scopedLogger.Debug().Msg("ctrl client reconnected")
|
|
||||||
default:
|
|
||||||
close(ctrlClientConnected)
|
|
||||||
scopedLogger.Debug().Msg("first native ctrl socket client connected")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
go handleClient(conn)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return listener
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartNativeCtrlSocketServer() {
|
|
||||||
nativeCtrlSocketListener = StartNativeSocketServer("/var/run/jetkvm_ctrl.sock", handleCtrlClient, true)
|
|
||||||
nativeLogger.Debug().Msg("native app ctrl sock started")
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartNativeVideoSocketServer() {
|
|
||||||
nativeVideoSocketListener = StartNativeSocketServer("/var/run/jetkvm_video.sock", handleVideoClient, false)
|
|
||||||
nativeLogger.Debug().Msg("native app video sock started")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleCtrlClient(conn net.Conn) {
|
|
||||||
// Lock to OS thread to isolate blocking socket I/O
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
scopedLogger := nativeLogger.With().
|
|
||||||
Str("addr", conn.RemoteAddr().String()).
|
|
||||||
Str("type", "ctrl").
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("native ctrl socket client connected (OS thread locked)")
|
|
||||||
if ctrlSocketConn != nil {
|
|
||||||
scopedLogger.Debug().Msg("closing existing native socket connection")
|
|
||||||
ctrlSocketConn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctrlSocketConn = conn
|
|
||||||
|
|
||||||
// Restore HDMI EDID if applicable
|
|
||||||
go restoreHdmiEdid()
|
|
||||||
|
|
||||||
readBuf := make([]byte, 4096)
|
|
||||||
for {
|
|
||||||
n, err := conn.Read(readBuf)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("error reading from ctrl sock")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
readMsg := string(readBuf[:n])
|
|
||||||
|
|
||||||
ctrlResp := CtrlResponse{}
|
|
||||||
err = json.Unmarshal([]byte(readMsg), &ctrlResp)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Str("data", readMsg).Msg("error parsing ctrl sock msg")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
scopedLogger.Trace().Interface("data", ctrlResp).Msg("ctrl sock msg")
|
|
||||||
|
|
||||||
if ctrlResp.Seq != 0 {
|
|
||||||
responseChan, ok := ongoingRequests[ctrlResp.Seq]
|
|
||||||
if ok {
|
|
||||||
responseChan <- &ctrlResp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch ctrlResp.Event {
|
|
||||||
case "video_input_state":
|
|
||||||
HandleVideoStateMessage(ctrlResp)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scopedLogger.Debug().Msg("ctrl sock disconnected")
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleVideoClient(conn net.Conn) {
|
|
||||||
// Lock to OS thread to isolate blocking video I/O
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
scopedLogger := nativeLogger.With().
|
|
||||||
Str("addr", conn.RemoteAddr().String()).
|
|
||||||
Str("type", "video").
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
scopedLogger.Info().Msg("native video socket client connected (OS thread locked)")
|
|
||||||
|
|
||||||
inboundPacket := make([]byte, maxVideoFrameSize)
|
|
||||||
lastFrame := time.Now()
|
|
||||||
for {
|
|
||||||
n, err := conn.Read(inboundPacket)
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("error during read")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
sinceLastFrame := now.Sub(lastFrame)
|
|
||||||
lastFrame = now
|
|
||||||
if currentSession != nil {
|
|
||||||
err := currentSession.VideoTrack.WriteSample(media.Sample{Data: inboundPacket[:n], Duration: sinceLastFrame})
|
|
||||||
if err != nil {
|
|
||||||
scopedLogger.Warn().Err(err).Msg("error writing sample")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldOverwrite(destPath string, srcHash []byte) bool {
|
|
||||||
if srcHash == nil {
|
|
||||||
nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, doing overwriting")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
dstHash, err := os.ReadFile(destPath + ".sha256")
|
|
||||||
if err != nil {
|
|
||||||
nativeLogger.Debug().Msg("error reading existing jetkvm_native.sha256, doing overwriting")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return !bytes.Equal(srcHash, dstHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNativeSha256() ([]byte, error) {
|
|
||||||
version, err := resource.ResourceFS.ReadFile("jetkvm_native.sha256")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return version, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetNativeVersion() (string, error) {
|
|
||||||
version, err := getNativeSha256()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(string(version)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureBinaryUpdated(destPath string) error {
|
|
||||||
// Lock to OS thread for file I/O operations
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
|
|
||||||
srcFile, err := resource.ResourceFS.Open("jetkvm_native")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer srcFile.Close()
|
|
||||||
|
|
||||||
srcHash, err := getNativeSha256()
|
|
||||||
if err != nil {
|
|
||||||
nativeLogger.Debug().Msg("error reading embedded jetkvm_native.sha256, proceeding with update")
|
|
||||||
srcHash = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = os.Stat(destPath)
|
|
||||||
if shouldOverwrite(destPath, srcHash) || err != nil {
|
|
||||||
nativeLogger.Info().
|
|
||||||
Interface("hash", srcHash).
|
|
||||||
Msg("writing jetkvm_native")
|
|
||||||
|
|
||||||
_ = os.Remove(destPath)
|
|
||||||
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_RDWR, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = io.Copy(destFile, srcFile)
|
|
||||||
destFile.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if srcHash != nil {
|
|
||||||
err = os.WriteFile(destPath+".sha256", srcHash, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nativeLogger.Info().Msg("jetkvm_native updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore the HDMI EDID value from the config.
|
|
||||||
// Called after successful connection to jetkvm_native.
|
|
||||||
func restoreHdmiEdid() {
|
|
||||||
if config.EdidString != "" {
|
|
||||||
nativeLogger.Info().Str("edid", config.EdidString).Msg("Restoring HDMI EDID")
|
|
||||||
_, err := CallCtrlAction("set_edid", map[string]interface{}{"edid": config.EdidString})
|
|
||||||
if err != nil {
|
|
||||||
nativeLogger.Warn().Err(err).Msg("Failed to restore HDMI EDID")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -341,7 +341,7 @@ export default function Actionbar({
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isAudioEnabledInUsb) {
|
if (isAudioEnabledInUsb) {
|
||||||
setDisableFocusTrap(true);
|
setDisableVideoFocusTrap(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,8 +2,6 @@ import { useCallback, useEffect } from "react";
|
||||||
|
|
||||||
import { useRTCStore } from "@/hooks/stores";
|
import { useRTCStore } from "@/hooks/stores";
|
||||||
|
|
||||||
import { devError } from '../utils/debug';
|
|
||||||
|
|
||||||
export interface JsonRpcRequest {
|
export interface JsonRpcRequest {
|
||||||
jsonrpc: string;
|
jsonrpc: string;
|
||||||
method: string;
|
method: string;
|
||||||
|
|
|
@ -654,6 +654,10 @@ export default function KvmIdRoute() {
|
||||||
|
|
||||||
const { send } = useJsonRpc(onJsonRpcRequest);
|
const { send } = useJsonRpc(onJsonRpcRequest);
|
||||||
|
|
||||||
|
// Initialize microphone hook
|
||||||
|
const microphoneHook = useMicrophone();
|
||||||
|
const { syncMicrophoneState } = microphoneHook;
|
||||||
|
|
||||||
// Handle audio device changes to sync microphone state
|
// Handle audio device changes to sync microphone state
|
||||||
const handleAudioDeviceChanged = useCallback((data: { enabled: boolean; reason: string }) => {
|
const handleAudioDeviceChanged = useCallback((data: { enabled: boolean; reason: string }) => {
|
||||||
console.log('[AudioDeviceChanged] Audio device changed:', data);
|
console.log('[AudioDeviceChanged] Audio device changed:', data);
|
||||||
|
|
2
video.go
2
video.go
|
@ -5,7 +5,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// max frame size for 1080p video, specified in mpp venc setting
|
// max frame size for 1080p video, specified in mpp venc setting
|
||||||
const maxVideoFrameSize = 1920 * 1080 / 2
|
const maxFrameSize = 1920 * 1080 / 2
|
||||||
|
|
||||||
func writeCtrlAction(action string) error {
|
func writeCtrlAction(action string) error {
|
||||||
actionMessage := map[string]string{
|
actionMessage := map[string]string{
|
||||||
|
|
Loading…
Reference in New Issue