Compare commits

...

4 Commits

Author SHA1 Message Date
adammkelly 940b2739a0
Merge a0609a5f86 into 3e7d8fb0f5 2025-06-20 14:19:18 -05:00
Aveline 3e7d8fb0f5
feat(usbgadget): suppress duplicate error logs (#630). 2025-06-20 18:52:37 +02:00
adammkelly a0609a5f86
Merge branch 'jetkvm:dev' into dev 2025-05-31 21:34:08 +02:00
Adam Kelly 3276427e9c feat(ui): Enable reboot of device (#421) 2025-05-22 22:14:00 +01:00
8 changed files with 130 additions and 6 deletions

View File

@ -143,15 +143,21 @@ func (u *UsbGadget) listenKeyboardEvents() {
default: default:
l.Trace().Msg("reading from keyboard") l.Trace().Msg("reading from keyboard")
if u.keyboardHidFile == nil { 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) time.Sleep(time.Second)
continue continue
} }
// reset the counter
u.resetLogSuppressionCounter("keyboardHidFileNil")
n, err := u.keyboardHidFile.Read(buf) n, err := u.keyboardHidFile.Read(buf)
if err != nil { if err != nil {
l.Error().Err(err).Msg("failed to read") u.logWithSupression("keyboardHidFileRead", 100, &l, err, "failed to read")
continue continue
} }
u.resetLogSuppressionCounter("keyboardHidFileRead")
l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard") l.Trace().Int("n", n).Bytes("buf", buf).Msg("got data from keyboard")
if n != 1 { if n != 1 {
l.Trace().Int("n", n).Msg("expected 1 byte, got") 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) _, err := u.keyboardHidFile.Write(data)
if err != nil { 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.Close()
u.keyboardHidFile = nil u.keyboardHidFile = nil
return err return err
} }
u.resetLogSuppressionCounter("keyboardWriteHidFile")
return nil return nil
} }

View File

@ -75,11 +75,12 @@ func (u *UsbGadget) absMouseWriteHidFile(data []byte) error {
_, err := u.absMouseHidFile.Write(data) _, err := u.absMouseHidFile.Write(data)
if err != nil { 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.Close()
u.absMouseHidFile = nil u.absMouseHidFile = nil
return err return err
} }
u.resetLogSuppressionCounter("absMouseWriteHidFile")
return nil return nil
} }

View File

@ -65,11 +65,12 @@ func (u *UsbGadget) relMouseWriteHidFile(data []byte) error {
_, err := u.relMouseHidFile.Write(data) _, err := u.relMouseHidFile.Write(data)
if err != nil { 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.Close()
u.relMouseHidFile = nil u.relMouseHidFile = nil
return err return err
} }
u.resetLogSuppressionCounter("relMouseWriteHidFile")
return nil return nil
} }

View File

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

View File

@ -6,6 +6,8 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"github.com/rs/zerolog"
) )
func joinPath(basePath string, paths []string) string { func joinPath(basePath string, paths []string) string {
@ -78,3 +80,27 @@ func compareFileContent(oldContent []byte, newContent []byte, looserMatch bool)
return false 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

@ -42,6 +42,7 @@ import SettingsHardwareRoute from "./routes/devices.$id.settings.hardware";
import SettingsVideoRoute from "./routes/devices.$id.settings.video"; import SettingsVideoRoute from "./routes/devices.$id.settings.video";
import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance"; import SettingsAppearanceRoute from "./routes/devices.$id.settings.appearance";
import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index"; import * as SettingsGeneralIndexRoute from "./routes/devices.$id.settings.general._index";
import SettingsGeneralRebootRoute from "./routes/devices.$id.settings.general.reboot";
import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update"; import SettingsGeneralUpdateRoute from "./routes/devices.$id.settings.general.update";
import SettingsNetworkRoute from "./routes/devices.$id.settings.network"; import SettingsNetworkRoute from "./routes/devices.$id.settings.network";
import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth"; import SecurityAccessLocalAuthRoute from "./routes/devices.$id.settings.access.local-auth";
@ -140,6 +141,10 @@ if (isOnDevice) {
index: true, index: true,
element: <SettingsGeneralIndexRoute.default />, element: <SettingsGeneralIndexRoute.default />,
}, },
{
path: "reboot",
element: <SettingsGeneralRebootRoute />,
},
{ {
path: "update", path: "update",
element: <SettingsGeneralUpdateRoute />, element: <SettingsGeneralUpdateRoute />,

View File

@ -92,6 +92,21 @@ export default function SettingsGeneralRoute() {
/> />
</SettingsItem> </SettingsItem>
</div> </div>
<div className="mt-2 flex items-center justify-between gap-x-2">
<SettingsItem
title="Reboot Device"
description="Power cycle the JetKVM"
/>
<div>
<Button
size="SM"
theme="light"
text="Reboot Device"
onClick={() => navigateTo("./reboot")}
/>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,66 @@
import { useNavigate } from "react-router-dom";
import { useCallback } from "react";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { Button } from "@components/Button";
export default function SettingsGeneralRebootRoute() {
const navigate = useNavigate();
const [send] = useJsonRpc();
const onConfirmUpdate = useCallback(() => {
// This is where we send the RPC to the golang binary
send("reboot", {force: true});
}, [send]);
{
/* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */
}
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
}
export function Dialog({
onClose,
onConfirmUpdate,
}: {
onClose: () => void;
onConfirmUpdate: () => void;
}) {
return (
<div className="pointer-events-auto relative mx-auto text-left">
<div>
<ConfirmationBox
onYes={onConfirmUpdate}
onNo={onClose}
/>
</div>
</div>
);
}
function ConfirmationBox({
onYes,
onNo,
}: {
onYes: () => void;
onNo: () => void;
}) {
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left">
<p className="text-base font-semibold text-black dark:text-white">
Reboot JetKVM
</p>
<p className="text-sm text-slate-600 dark:text-slate-300">
Do you want to proceed with rebooting the system?
</p>
<div className="mt-4 flex gap-x-2">
<Button size="SM" theme="light" text="Yes" onClick={onYes} />
<Button size="SM" theme="blank" text="No" onClick={onNo} />
</div>
</div>
</div>
);
}