Compare commits

...

7 Commits

Author SHA1 Message Date
Marc Brooks 8e00b3d581
Fix CoPilot complaints 2025-11-05 14:56:27 -06:00
Marc Brooks e12ddaff79
Use a single exported HidKeyBufferSize from hid_keyboard 2025-11-05 14:56:04 -06:00
Marc Brooks 8de61db3d8
Return a duration with the queue (not a bare int) 2025-11-05 14:54:40 -06:00
Marc Brooks da8c82da34
Remove unused translation for Shift. 2025-11-05 14:54:06 -06:00
Marc Brooks 49b9a35951
Be explicit about minimum and maximum delay
Use range correctly in the UI element and error messaging.
2025-11-05 14:09:10 -06:00
Marc Brooks 05057cb6fa
Better loop name
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-05 13:53:55 -06:00
Marc Brooks 57a7aa6a8b
Protect suspension mutex
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-05 13:53:23 -06:00
19 changed files with 39 additions and 41 deletions

View File

@ -42,16 +42,17 @@ const (
func GetQueueIndex(messageType MessageType) (int, time.Duration) {
switch messageType {
case TypeHandshake:
return HandshakeQueue, 1
return HandshakeQueue, 1 * time.Second
case TypeKeyboardReport, TypeKeypressReport, TypeKeyboardLedState, TypeKeydownState, TypeKeyboardMacroState:
return KeyboardQueue, 1
return KeyboardQueue, 1 * time.Second
case TypePointerReport, TypeMouseReport, TypeWheelReport:
return MouseQueue, 1
return MouseQueue, 1 * time.Second
// we don't want to block the queue for these messages
case TypeKeyboardMacroReport, TypeCancelKeyboardMacroReport, TypeKeyboardMacroTokenState:
return MacroQueue, 60 // 1 minute timeout
return MacroQueue, 60 * time.Second
default:
return OtherQueue, 5
return OtherQueue, 5 * time.Second
}
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"github.com/google/uuid"
"github.com/jetkvm/kvm/internal/usbgadget"
)
// Message ..
@ -135,18 +136,16 @@ func (m *Message) KeyboardReport() (KeyboardReport, error) {
// Macro ..
type KeyboardMacroStep struct {
Modifier byte // 1 byte
Keys []byte // 6 bytes: HidKeyBufferSize
Keys []byte // 6 bytes: usbgadget.HidKeyBufferSize
Delay uint16 // 2 bytes
}
type KeyboardMacroReport struct {
IsPaste bool
StepCount uint32
Steps []KeyboardMacroStep
}
// HidKeyBufferSize is the size of the keys buffer in the keyboard report.
const HidKeyBufferSize int = 6
// KeyboardMacroReport returns the keyboard macro report from the message.
func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
if m.t != TypeKeyboardMacroReport {
@ -171,7 +170,7 @@ func (m *Message) KeyboardMacroReport() (KeyboardMacroReport, error) {
Delay: binary.BigEndian.Uint16(m.d[offset+7 : offset+9]),
})
offset += 1 + HidKeyBufferSize + 2
offset += 1 + usbgadget.HidKeyBufferSize + 2
}
return KeyboardMacroReport{

View File

@ -76,7 +76,7 @@ var keyboardReportDesc = []byte{
const (
hidReadBufferSize = 8
hidKeyBufferSize = 6
HidKeyBufferSize = 6
hidErrorRollOver = 0x01
// https://www.usb.org/sites/default/files/documents/hid1_11.pdf
// https://www.usb.org/sites/default/files/hut1_2.pdf
@ -161,13 +161,18 @@ func (u *UsbGadget) SetOnKeysDownChange(f func(state KeysDownState)) {
}
var suspendedKeyDownMessages bool = false
var suspendedKeyDownMessagesLock sync.Mutex
func (u *UsbGadget) SuspendKeyDownMessages() {
suspendedKeyDownMessagesLock.Lock()
suspendedKeyDownMessages = true
suspendedKeyDownMessagesLock.Unlock()
}
func (u *UsbGadget) ResumeSuspendKeyDownMessages() {
suspendedKeyDownMessagesLock.Lock()
suspendedKeyDownMessages = false
suspendedKeyDownMessagesLock.Unlock()
}
func (u *UsbGadget) SetOnKeepAliveReset(f func()) {
@ -337,7 +342,7 @@ func (u *UsbGadget) keyboardWriteHidFile(modifier byte, keys []byte) error {
return err
}
_, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:hidKeyBufferSize]...))
_, err := u.writeWithTimeout(u.keyboardHidFile, append([]byte{modifier, 0x00}, keys[:HidKeyBufferSize]...))
if err != nil {
u.logWithSuppression("keyboardWriteHidFile", 100, u.log, err, "failed to write to hidg0")
u.keyboardHidFile.Close()
@ -381,11 +386,11 @@ func (u *UsbGadget) UpdateKeysDown(modifier byte, keys []byte) KeysDownState {
func (u *UsbGadget) KeyboardReport(modifier byte, keys []byte) error {
defer u.resetUserInputTime()
if len(keys) > hidKeyBufferSize {
keys = keys[:hidKeyBufferSize]
if len(keys) > HidKeyBufferSize {
keys = keys[:HidKeyBufferSize]
}
if len(keys) < hidKeyBufferSize {
keys = append(keys, make([]byte, hidKeyBufferSize-len(keys))...)
if len(keys) < HidKeyBufferSize {
keys = append(keys, make([]byte, HidKeyBufferSize-len(keys))...)
}
err := u.keyboardWriteHidFile(modifier, keys)
@ -468,7 +473,7 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
// handle other keys that are not modifier keys by placing or removing them
// from the key buffer since the buffer tracks currently pressed keys
overrun := true
for i := range hidKeyBufferSize {
for i := range HidKeyBufferSize {
// If we find the key in the buffer the buffer, we either remove it (if press is false)
// or do nothing (if down is true) because the buffer tracks currently pressed keys
// and if we find a zero byte, we can place the key there (if press is true)
@ -479,7 +484,7 @@ func (u *UsbGadget) keypressReport(key byte, press bool) (KeysDownState, error)
// we are releasing the key, remove it from the buffer
if keys[i] != 0 {
copy(keys[i:], keys[i+1:])
keys[hidKeyBufferSize-1] = 0 // Clear the last byte
keys[HidKeyBufferSize-1] = 0 // Clear the last byte
}
}
overrun = false // We found a slot for the key

View File

@ -135,7 +135,7 @@ func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDev
keyboardStateCtx: keyboardCtx,
keyboardStateCancel: keyboardCancel,
keyboardState: 0,
keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to hidKeyBufferSize (6) zero bytes
keysDownState: KeysDownState{Modifier: 0, Keys: []byte{0, 0, 0, 0, 0, 0}}, // must be initialized to usbgadget.HidKeyBufferSize (6) zero bytes
kbdAutoReleaseTimers: make(map[byte]*time.Timer),
enabledDevices: *enabledDevices,
lastUserInput: time.Now(),

View File

@ -1210,7 +1210,7 @@ func executeKeyboardMacro(ctx context.Context, isPaste bool, macro []hidrpc.Keyb
case <-ctx.Done():
// make sure keyboard state is reset and the client gets notified
gadget.ResumeSuspendKeyDownMessages()
err := rpcKeyboardReport(0, make([]byte, hidrpc.HidKeyBufferSize))
err := rpcKeyboardReport(0, make([]byte, usbgadget.HidKeyBufferSize))
if err != nil {
logger.Warn().Err(err).Msg("failed to reset keyboard state")
}

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Videresendt af Cloudflare",
"info_resolution": "Opløsning:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Flytte",
"info_usb_state": "USB-tilstand:",
"info_video_size": "Videostørrelse:",
"input_disabled": "Input deaktiveret",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Weitergeleitet von Cloudflare",
"info_resolution": "Auflösung:",
"info_scroll_lock": "Rollen-Taste",
"info_shift": "Schicht",
"info_usb_state": "USB-Status:",
"info_video_size": "Videogröße:",
"input_disabled": "Eingabe deaktiviert",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Relayed by Cloudflare",
"info_resolution": "Resolution:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Shift",
"info_usb_state": "USB State:",
"info_video_size": "Video Size:",
"input_disabled": "Input disabled",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Retransmitido por Cloudflare",
"info_resolution": "Resolución:",
"info_scroll_lock": "Bloq Despl",
"info_shift": "Cambio",
"info_usb_state": "Estado USB:",
"info_video_size": "Tamaño del vídeo:",
"input_disabled": "Entrada deshabilitada",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Relayé par Cloudflare",
"info_resolution": "Résolution :",
"info_scroll_lock": "Verrouillage du défilement",
"info_shift": "Maj",
"info_usb_state": "État USB :",
"info_video_size": "Taille de la vidéo :",
"input_disabled": "Entrée désactivée",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Rilasciato da Cloudflare",
"info_resolution": "Risoluzione:",
"info_scroll_lock": "Blocco scorrimento",
"info_shift": "Spostare",
"info_usb_state": "Stato USB:",
"info_video_size": "Dimensioni video:",
"input_disabled": "Input disabilitato",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Videresendt av Cloudflare",
"info_resolution": "Oppløsning:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Skifte",
"info_usb_state": "USB-tilstand:",
"info_video_size": "Videostørrelse:",
"input_disabled": "Inndata deaktivert",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "Vidarebefordras av Cloudflare",
"info_resolution": "Upplösning:",
"info_scroll_lock": "Scroll Lock",
"info_shift": "Flytta",
"info_usb_state": "USB-status:",
"info_video_size": "Videostorlek:",
"input_disabled": "Inmatning inaktiverad",

View File

@ -331,7 +331,6 @@
"info_relayed_by_cloudflare": "由 Cloudflare 转发",
"info_resolution": "分辨率:",
"info_scroll_lock": "滚动锁定",
"info_shift": "Shift",
"info_usb_state": "USB 状态:",
"info_video_size": "视频大小:",
"input_disabled": "输入禁用",

View File

@ -19,6 +19,8 @@ import { TextAreaWithLabel } from "@components/TextArea";
// uint32 max value / 4
const pasteMaxLength = 1073741824;
const defaultDelay = 20;
const minimumDelay = 10;
const maximumDelay = 65534;
export default function PasteModal() {
const TextAreaRef = useRef<HTMLTextAreaElement>(null);
@ -31,7 +33,7 @@ export default function PasteModal() {
const [invalidChars, setInvalidChars] = useState<string[]>([]);
const [delayValue, setDelayValue] = useState(defaultDelay);
const delay = useMemo(() => {
if (delayValue < 0 || delayValue > 65534) {
if (delayValue < minimumDelay || delayValue > maximumDelay) {
return defaultDelay;
}
return delayValue;
@ -189,18 +191,18 @@ export default function PasteModal() {
type="number"
label={m.paste_modal_delay_between_keys()}
placeholder={m.paste_modal_delay_between_keys()}
min={50}
max={65534}
min={minimumDelay}
max={maximumDelay}
value={delayValue}
onChange={e => {
setDelayValue(parseInt(e.target.value, 10));
}}
/>
{(delayValue < defaultDelay || delayValue > 65534) && (
{(delayValue < minimumDelay || delayValue > maximumDelay) && (
<div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400">
{m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
{m.paste_modal_delay_out_of_range({ min: minimumDelay, max: maximumDelay })}
</span>
</div>
)}

View File

@ -386,7 +386,7 @@ export class CancelKeyboardMacroReportMessage extends RpcMessage {
constructor(token: string) {
super(HID_RPC_MESSAGE_TYPES.CancelKeyboardMacroReport);
this.token = (token == null || token === undefined || token === "")
this.token = (token == null || token === "")
? "00000000-0000-0000-0000-000000000000"
: token;
}
@ -397,11 +397,11 @@ export class CancelKeyboardMacroReportMessage extends RpcMessage {
}
public static unmarshal(data: Uint8Array): CancelKeyboardMacroReportMessage | undefined {
if (data.length == 0) {
if (data.length === 0) {
return new CancelKeyboardMacroReportMessage("00000000-0000-0000-0000-000000000000");
}
if (data.length != 16) {
if (data.length !== 16) {
throw new Error(`Invalid cancel message length: ${data.length}`);
}

View File

@ -89,7 +89,7 @@ def main(argv):
)
report = {
"generated_at": datetime.utcnow().isoformat() + "Z",
"generated_at": datetime.now().isoformat(),
"en_json": str(en_path),
"total_string_keys": total_keys,
"duplicate_groups": sorted(

View File

@ -82,7 +82,7 @@ def main():
print(f"Generating report for {len(usages)} usages ...")
report = {
"generated_at": datetime.utcnow().isoformat() + "Z",
"generated_at": datetime.now().isoformat(),
"en_json": str(en_path),
"src_root": args.src,
"total_keys": len(keys),

View File

@ -261,8 +261,8 @@ func newSession(config SessionConfig) (*Session, error) {
}
}()
for queue := range session.hidQueues {
go session.handleQueue(session.hidQueues[queue])
for queueIndex := range session.hidQueues {
go session.handleQueue(session.hidQueues[queueIndex])
}
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {