Compare commits

...

7 Commits

Author SHA1 Message Date
Siyuan Miao fe127ed41c chore: bump version to 0.4.6 2025-06-25 13:28:09 +02:00
Aveline 3e7d8fb0f5
feat(usbgadget): suppress duplicate error logs (#630). 2025-06-20 18:52:37 +02:00
Marc Brooks 0d7f47c109
fix(ui) firefox permissions error handling (#631) 2025-06-20 14:24:54 +02:00
iain MacDonnell 254c001572
fix: keyboard_layout default config (en-US/en_US) (#633) 2025-06-20 14:13:36 +02:00
Aveline 6f037a832d
feat(native): restart jetkvm_native automatically (#629) 2025-06-20 14:08:19 +02:00
Marc Brooks ccba27cedd
chore(mDNS): ensure the mDNS mode is set every time network state changes (#624)
Eliminates (mostly) duplicate code
2025-06-19 09:29:21 +02:00
ronskvm cf9c6e5cc8
chore(hid): change absolute mouse usb interface descriptor's subclass field to zero
Changed absolute mouse usb interface descriptor's subclass field to zero.
2025-06-19 09:11:21 +02:00
12 changed files with 170 additions and 42 deletions

View File

@ -2,8 +2,8 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD)
BUILDDATE ?= $(shell date -u +%FT%T%z)
BUILDTS ?= $(shell date -u +%s)
REVISION ?= $(shell git rev-parse HEAD)
VERSION_DEV ?= 0.4.5-dev$(shell date +%Y%m%d%H%M)
VERSION ?= 0.4.4
VERSION_DEV ?= 0.4.6-dev$(shell date +%Y%m%d%H%M)
VERSION ?= 0.4.5
PROMETHEUS_TAG := github.com/prometheus/common/version
KVM_PKG_NAME := github.com/jetkvm/kvm

View File

@ -111,7 +111,7 @@ var defaultConfig = &Config{
ActiveExtension: "",
KeyboardMacros: []KeyboardMacro{},
DisplayRotation: "270",
KeyboardLayout: "en-US",
KeyboardLayout: "en_US",
DisplayMaxBrightness: 64,
DisplayDimAfterSec: 120, // 2 minutes
DisplayOffAfterSec: 1800, // 30 minutes

View File

@ -143,15 +143,21 @@ func (u *UsbGadget) listenKeyboardEvents() {
default:
l.Trace().Msg("reading from keyboard")
if u.keyboardHidFile == nil {
l.Error().Msg("keyboardHidFile is nil")
u.logWithSupression("keyboardHidFileNil", 100, &l, nil, "keyboardHidFile is nil")
// show the error every 100 times to avoid spamming the logs
time.Sleep(time.Second)
continue
}
// reset the counter
u.resetLogSuppressionCounter("keyboardHidFileNil")
n, err := u.keyboardHidFile.Read(buf)
if err != nil {
l.Error().Err(err).Msg("failed to read")
u.logWithSupression("keyboardHidFileRead", 100, &l, err, "failed to read")
continue
}
u.resetLogSuppressionCounter("keyboardHidFileRead")
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
if n != 1 {
l.Trace().Int("n", n).Msg("expected 1 byte, got")
@ -195,12 +201,12 @@ func (u *UsbGadget) keyboardWriteHidFile(data []byte) error {
_, err := u.keyboardHidFile.Write(data)
if err != nil {
u.log.Error().Err(err).Msg("failed to write to hidg0")
u.logWithSupression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
u.keyboardHidFile.Close()
u.keyboardHidFile = nil
return err
}
u.resetLogSuppressionCounter("keyboardWriteHidFile")
return nil
}

View File

@ -12,7 +12,7 @@ var absoluteMouseConfig = gadgetConfigItem{
configPath: []string{"hid.usb1"},
attrs: gadgetAttributes{
"protocol": "2",
"subclass": "1",
"subclass": "0",
"report_length": "6",
},
reportDesc: absoluteMouseCombinedReportDesc,
@ -75,11 +75,12 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
_, err := u.absMouseHidFile.Write(data)
if err != nil {
u.log.Error().Err(err).Msg("failed to write to hidg1")
u.logWithSupression("absMouseWriteHidFile", 100, u.log, err, "failed to write to hidg1")
u.absMouseHidFile.Close()
u.absMouseHidFile = nil
return err
}
u.resetLogSuppressionCounter("absMouseWriteHidFile")
return nil
}

View File

@ -65,11 +65,12 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
_, err := u.relMouseHidFile.Write(data)
if err != nil {
u.log.Error().Err(err).Msg("failed to write to hidg2")
u.logWithSupression("relMouseWriteHidFile", 100, u.log, err, "failed to write to hidg2")
u.relMouseHidFile.Close()
u.relMouseHidFile = nil
return err
}
u.resetLogSuppressionCounter("relMouseWriteHidFile")
return nil
}

View File

@ -79,6 +79,8 @@ type UsbGadget struct {
onKeyboardStateChange *func(state KeyboardState)
log *zerolog.Logger
logSuppressionCounter map[string]int
}
const configFSPath = "/sys/kernel/config"
@ -126,6 +128,8 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
strictMode: config.strictMode,
logSuppressionCounter: make(map[string]int),
absMouseAccumulatedWheelY: 0,
}
if err := g.Init(); err != nil {

View File

@ -6,6 +6,8 @@ import (
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog"
)
func joinPath(basePath string, paths []string) string {
@ -78,3 +80,27 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
return false
}
func (u *UsbGadget) logWithSupression(counterName string, every int, logger *zerolog.Logger, err error, msg string, args ...interface{}) {
if _, ok := u.logSuppressionCounter[counterName]; !ok {
u.logSuppressionCounter[counterName] = 0
} else {
u.logSuppressionCounter[counterName]++
}
l := logger.With().Int("counter", u.logSuppressionCounter[counterName]).Logger()
if u.logSuppressionCounter[counterName]%every == 0 {
if err != nil {
l.Error().Err(err).Msgf(msg, args...)
} else {
l.Error().Msgf(msg, args...)
}
}
}
func (u *UsbGadget) resetLogSuppressionCounter(counterName string) {
if _, ok := u.logSuppressionCounter[counterName]; !ok {
u.logSuppressionCounter[counterName] = 0
}
}

View File

@ -8,6 +8,7 @@ import (
"io"
"net"
"os"
"os/exec"
"sync"
"time"
@ -41,6 +42,11 @@ var ongoingRequests = make(map[int32]chan *CtrlResponse)
var lock = &sync.Mutex{}
var (
nativeCmd *exec.Cmd
nativeCmdLock = &sync.Mutex{}
)
func CallCtrlAction(action string, params map[string]interface{}) (*CtrlResponse, error) {
lock.Lock()
defer lock.Unlock()
@ -129,16 +135,26 @@ func StartNativeSocketServer(socketPath string, handleClient func(net.Conn), isC
scopedLogger.Info().Msg("server listening")
go func() {
conn, err := listener.Accept()
listener.Close()
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to accept socket")
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)
}
if isCtrl {
close(ctrlClientConnected)
scopedLogger.Debug().Msg("first native ctrl socket client connected")
}
handleClient(conn)
}()
return listener
@ -235,6 +251,51 @@ func handleVideoClient(conn net.Conn) {
}
}
func startNativeBinaryWithLock(binaryPath string) (*exec.Cmd, error) {
nativeCmdLock.Lock()
defer nativeCmdLock.Unlock()
cmd, err := startNativeBinary(binaryPath)
if err != nil {
return nil, err
}
nativeCmd = cmd
return cmd, nil
}
func restartNativeBinary(binaryPath string) error {
time.Sleep(10 * time.Second)
// restart the binary
nativeLogger.Info().Msg("restarting jetkvm_native binary")
cmd, err := startNativeBinary(binaryPath)
if err != nil {
nativeLogger.Warn().Err(err).Msg("failed to restart binary")
}
nativeCmd = cmd
return err
}
func superviseNativeBinary(binaryPath string) error {
nativeCmdLock.Lock()
defer nativeCmdLock.Unlock()
if nativeCmd == nil || nativeCmd.Process == nil {
return restartNativeBinary(binaryPath)
}
err := nativeCmd.Wait()
if err == nil {
nativeLogger.Info().Err(err).Msg("jetkvm_native binary exited with no error")
} else if exiterr, ok := err.(*exec.ExitError); ok {
nativeLogger.Warn().Int("exit_code", exiterr.ExitCode()).Msg("jetkvm_native binary exited with error")
} else {
nativeLogger.Warn().Err(err).Msg("jetkvm_native binary exited with unknown error")
}
return restartNativeBinary(binaryPath)
}
func ExtractAndRunNativeBin() error {
binaryPath := "/userdata/jetkvm/bin/jetkvm_native"
if err := ensureBinaryUpdated(binaryPath); err != nil {
@ -246,12 +307,28 @@ func ExtractAndRunNativeBin() error {
return fmt.Errorf("failed to make binary executable: %w", err)
}
// Run the binary in the background
cmd, err := startNativeBinary(binaryPath)
cmd, err := startNativeBinaryWithLock(binaryPath)
if err != nil {
return fmt.Errorf("failed to start binary: %w", err)
}
//TODO: add auto restart
// check if the binary is still running every 10 seconds
go func() {
for {
select {
case <-appCtx.Done():
nativeLogger.Info().Msg("stopping native binary supervisor")
return
default:
err := superviseNativeBinary(binaryPath)
if err != nil {
nativeLogger.Warn().Err(err).Msg("failed to supervise native binary")
time.Sleep(1 * time.Second) // Add a short delay to prevent rapid successive calls
}
}
}
}()
go func() {
<-appCtx.Done()
nativeLogger.Info().Int("pid", cmd.Process.Pid).Msg("killing process")

View File

@ -21,6 +21,7 @@ func networkStateChanged() {
// always restart mDNS when the network state changes
if mDNS != nil {
_ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
_ = mDNS.SetLocalNames([]string{
networkState.GetHostname(),
networkState.GetFQDN(),
@ -54,14 +55,6 @@ func initNetwork() error {
OnConfigChange: func(networkConfig *network.NetworkConfig) {
config.NetworkConfig = networkConfig
networkStateChanged()
if mDNS != nil {
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
_ = mDNS.SetLocalNames([]string{
networkState.GetHostname(),
networkState.GetFQDN(),
}, true)
}
},
})

View File

@ -115,9 +115,18 @@ export default function WebRTCVideo() {
const isFullscreenEnabled = document.fullscreenEnabled;
const checkNavigatorPermissions = useCallback(async (permissionName: string) => {
const name = permissionName as PermissionName;
const { state } = await navigator.permissions.query({ name });
return state === "granted";
if (!navigator.permissions || !navigator.permissions.query) {
return false; // if can't query permissions, assume NOT granted
}
try {
const name = permissionName as PermissionName;
const { state } = await navigator.permissions.query({ name });
return state === "granted";
} catch {
// ignore errors
}
return false; // if query fails, assume NOT granted
}, []);
const requestPointerLock = useCallback(async () => {
@ -128,7 +137,11 @@ export default function WebRTCVideo() {
const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock");
if (isPointerLockGranted && settings.mouseMode === "relative") {
await videoElm.current.requestPointerLock();
try {
await videoElm.current.requestPointerLock();
} catch {
// ignore errors
}
}
}, [checkNavigatorPermissions, isPointerLockPossible, settings.mouseMode]);
@ -136,10 +149,13 @@ export default function WebRTCVideo() {
if (videoElm.current === null) return;
const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock");
if (isKeyboardLockGranted) {
if ("keyboard" in navigator) {
if (isKeyboardLockGranted && "keyboard" in navigator) {
try {
// @ts-expect-error - keyboard lock is not supported in all browsers
await navigator.keyboard.lock();
await navigator.keyboard.lock();
} catch {
// ignore errors
}
}
}, [checkNavigatorPermissions]);
@ -148,8 +164,12 @@ export default function WebRTCVideo() {
if (videoElm.current === null || document.fullscreenElement !== videoElm.current) return;
if ("keyboard" in navigator) {
// @ts-expect-error - keyboard unlock is not supported in all browsers
await navigator.keyboard.unlock();
try {
// @ts-expect-error - keyboard unlock is not supported in all browsers
await navigator.keyboard.unlock();
} catch {
// ignore errors
}
}
}, []);

View File

@ -39,11 +39,11 @@ export default function PasteModal() {
state => state.setKeyboardLayout,
);
// this ensures we always get the original en-US if it hasn't been set yet
// this ensures we always get the original en_US if it hasn't been set yet
const safeKeyboardLayout = useMemo(() => {
if (keyboardLayout && keyboardLayout.length > 0)
return keyboardLayout;
return "en-US";
return "en_US";
}, [keyboardLayout]);
useEffect(() => {

View File

@ -25,11 +25,11 @@ export default function SettingsKeyboardRoute() {
state => state.setShowPressedKeys,
);
// this ensures we always get the original en-US if it hasn't been set yet
// this ensures we always get the original en_US if it hasn't been set yet
const safeKeyboardLayout = useMemo(() => {
if (keyboardLayout && keyboardLayout.length > 0)
return keyboardLayout;
return "en-US";
return "en_US";
}, [keyboardLayout]);
const layoutOptions = Object.entries(layouts).map(([code, language]) => { return { value: code, label: language } })