mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
1b6705a0ed
...
cd85dcd0db
Author | SHA1 | Date |
---|---|---|
|
cd85dcd0db | |
|
6484990fb9 | |
|
0a4a1af80e | |
|
fc3dbcd820 |
|
@ -69,6 +69,15 @@ jobs:
|
||||||
CI_USER: ${{ vars.JETKVM_CI_USER }}
|
CI_USER: ${{ vars.JETKVM_CI_USER }}
|
||||||
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
CI_HOST: ${{ vars.JETKVM_CI_HOST }}
|
||||||
CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
|
CI_SSH_PRIVATE: ${{ secrets.JETKVM_CI_SSH_PRIVATE }}
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
make build_dev_test
|
||||||
|
|
||||||
|
echo "+ Copying device-tests.tar.gz to remote host"
|
||||||
|
ssh jkci "cat > /userdata/jetkvm/device-tests.tar.gz" < device-tests.tar.gz
|
||||||
|
echo "+ Running go tests"
|
||||||
|
ssh jkci "cd /userdata/jetkvm && tar zxvf device-tests.tar.gz && ./run_all_tests -json"
|
||||||
- name: Deploy application
|
- name: Deploy application
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
[](https://twitter.com/jetkvm)
|
[](https://twitter.com/jetkvm)
|
||||||
|
|
||||||
|
[](https://goreportcard.com/report/github.com/jetkvm/kvm)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
JetKVM is a high-performance, open-source KVM over IP (Keyboard, Video, Mouse) solution designed for efficient remote management of computers, servers, and workstations. Whether you're dealing with boot failures, installing a new operating system, adjusting BIOS settings, or simply taking control of a machine from afar, JetKVM provides the tools to get it done effectively.
|
||||||
|
|
|
@ -26,6 +26,8 @@ show_help() {
|
||||||
echo "Optional:"
|
echo "Optional:"
|
||||||
echo " -u, --user <remote_user> Remote username (default: root)"
|
echo " -u, --user <remote_user> Remote username (default: root)"
|
||||||
echo " --run-go-tests Run go tests"
|
echo " --run-go-tests Run go tests"
|
||||||
|
echo " --run-go-tests-only Run go tests and exit"
|
||||||
|
echo " --run-go-tests-json Run go tests and output JSON"
|
||||||
echo " --skip-ui-build Skip frontend/UI build"
|
echo " --skip-ui-build Skip frontend/UI build"
|
||||||
echo " --help Display this help message"
|
echo " --help Display this help message"
|
||||||
echo
|
echo
|
||||||
|
@ -42,6 +44,8 @@ RESET_USB_HID_DEVICE=false
|
||||||
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
|
||||||
RUN_GO_TESTS=false
|
RUN_GO_TESTS=false
|
||||||
RUN_GO_TESTS_JSON=false
|
RUN_GO_TESTS_JSON=false
|
||||||
|
RUN_GO_TESTS_ONLY=false
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case $1 in
|
case $1 in
|
||||||
|
@ -67,6 +71,12 @@ while [[ $# -gt 0 ]]; do
|
||||||
;;
|
;;
|
||||||
--run-go-tests-json)
|
--run-go-tests-json)
|
||||||
RUN_GO_TESTS_JSON=true
|
RUN_GO_TESTS_JSON=true
|
||||||
|
RUN_GO_TESTS=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--run-go-tests-only)
|
||||||
|
RUN_GO_TESTS_ONLY=true
|
||||||
|
RUN_GO_TESTS=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--help)
|
--help)
|
||||||
|
@ -81,10 +91,6 @@ while [[ $# -gt 0 ]]; do
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ "$RUN_GO_TESTS_JSON" = true ]; then
|
|
||||||
RUN_GO_TESTS=true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify required parameters
|
# Verify required parameters
|
||||||
if [ -z "$REMOTE_HOST" ]; then
|
if [ -z "$REMOTE_HOST" ]; then
|
||||||
msg_err "Error: Remote IP is a required parameter"
|
msg_err "Error: Remote IP is a required parameter"
|
||||||
|
@ -114,8 +120,13 @@ if [ "$RUN_GO_TESTS" = true ]; then
|
||||||
set -e
|
set -e
|
||||||
cd ${REMOTE_PATH}
|
cd ${REMOTE_PATH}
|
||||||
tar zxvf device-tests.tar.gz
|
tar zxvf device-tests.tar.gz
|
||||||
./run_all_tests $TEST_ARGS
|
PION_LOG_TRACE=all ./run_all_tests $TEST_ARGS
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
if [ "$RUN_GO_TESTS_ONLY" = true ]; then
|
||||||
|
msg_info "▶ Go tests completed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg_info "▶ Building go binary"
|
msg_info "▶ Building go binary"
|
||||||
|
|
|
@ -35,20 +35,23 @@ const (
|
||||||
FileStateMounted
|
FileStateMounted
|
||||||
FileStateMountedConfigFS
|
FileStateMountedConfigFS
|
||||||
FileStateSymlink
|
FileStateSymlink
|
||||||
|
FileStateSymlinkInOrderConfigFS // configfs is a shithole, so we need to check if the symlinks are created in the correct order
|
||||||
|
FileStateSymlinkNotInOrderConfigFS
|
||||||
FileStateTouch
|
FileStateTouch
|
||||||
)
|
)
|
||||||
|
|
||||||
var FileStateString = map[FileState]string{
|
var FileStateString = map[FileState]string{
|
||||||
FileStateUnknown: "UNKNOWN",
|
FileStateUnknown: "UNKNOWN",
|
||||||
FileStateAbsent: "ABSENT",
|
FileStateAbsent: "ABSENT",
|
||||||
FileStateDirectory: "DIRECTORY",
|
FileStateDirectory: "DIRECTORY",
|
||||||
FileStateFile: "FILE",
|
FileStateFile: "FILE",
|
||||||
FileStateFileContentMatch: "FILE_CONTENT_MATCH",
|
FileStateFileContentMatch: "FILE_CONTENT_MATCH",
|
||||||
FileStateFileWrite: "FILE_WRITE",
|
FileStateFileWrite: "FILE_WRITE",
|
||||||
FileStateMounted: "MOUNTED",
|
FileStateMounted: "MOUNTED",
|
||||||
FileStateMountedConfigFS: "CONFIGFS_MOUNT",
|
FileStateMountedConfigFS: "CONFIGFS_MOUNT",
|
||||||
FileStateSymlink: "SYMLINK",
|
FileStateSymlink: "SYMLINK",
|
||||||
FileStateTouch: "TOUCH",
|
FileStateSymlinkInOrderConfigFS: "SYMLINK_IN_ORDER_CONFIGFS",
|
||||||
|
FileStateTouch: "TOUCH",
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -69,6 +72,8 @@ const (
|
||||||
FileChangeResolvedActionAppendFile
|
FileChangeResolvedActionAppendFile
|
||||||
FileChangeResolvedActionCreateSymlink
|
FileChangeResolvedActionCreateSymlink
|
||||||
FileChangeResolvedActionRecreateSymlink
|
FileChangeResolvedActionRecreateSymlink
|
||||||
|
FileChangeResolvedActionCreateDirectoryAndSymlinks
|
||||||
|
FileChangeResolvedActionReorderSymlinks
|
||||||
FileChangeResolvedActionCreateDirectory
|
FileChangeResolvedActionCreateDirectory
|
||||||
FileChangeResolvedActionRemoveDirectory
|
FileChangeResolvedActionRemoveDirectory
|
||||||
FileChangeResolvedActionTouch
|
FileChangeResolvedActionTouch
|
||||||
|
@ -76,19 +81,21 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{
|
var FileChangeResolvedActionString = map[FileChangeResolvedAction]string{
|
||||||
FileChangeResolvedActionUnknown: "UNKNOWN",
|
FileChangeResolvedActionUnknown: "UNKNOWN",
|
||||||
FileChangeResolvedActionDoNothing: "DO_NOTHING",
|
FileChangeResolvedActionDoNothing: "DO_NOTHING",
|
||||||
FileChangeResolvedActionRemove: "REMOVE",
|
FileChangeResolvedActionRemove: "REMOVE",
|
||||||
FileChangeResolvedActionCreateFile: "FILE_CREATE",
|
FileChangeResolvedActionCreateFile: "FILE_CREATE",
|
||||||
FileChangeResolvedActionWriteFile: "FILE_WRITE",
|
FileChangeResolvedActionWriteFile: "FILE_WRITE",
|
||||||
FileChangeResolvedActionUpdateFile: "FILE_UPDATE",
|
FileChangeResolvedActionUpdateFile: "FILE_UPDATE",
|
||||||
FileChangeResolvedActionAppendFile: "FILE_APPEND",
|
FileChangeResolvedActionAppendFile: "FILE_APPEND",
|
||||||
FileChangeResolvedActionCreateSymlink: "SYMLINK_CREATE",
|
FileChangeResolvedActionCreateSymlink: "SYMLINK_CREATE",
|
||||||
FileChangeResolvedActionRecreateSymlink: "SYMLINK_RECREATE",
|
FileChangeResolvedActionRecreateSymlink: "SYMLINK_RECREATE",
|
||||||
FileChangeResolvedActionCreateDirectory: "DIR_CREATE",
|
FileChangeResolvedActionCreateDirectoryAndSymlinks: "DIR_CREATE_AND_SYMLINKS",
|
||||||
FileChangeResolvedActionRemoveDirectory: "DIR_REMOVE",
|
FileChangeResolvedActionReorderSymlinks: "SYMLINK_REORDER",
|
||||||
FileChangeResolvedActionTouch: "TOUCH",
|
FileChangeResolvedActionCreateDirectory: "DIR_CREATE",
|
||||||
FileChangeResolvedActionMountConfigFS: "CONFIGFS_MOUNT",
|
FileChangeResolvedActionRemoveDirectory: "DIR_REMOVE",
|
||||||
|
FileChangeResolvedActionTouch: "TOUCH",
|
||||||
|
FileChangeResolvedActionMountConfigFS: "CONFIGFS_MOUNT",
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChangeSet struct {
|
type ChangeSet struct {
|
||||||
|
@ -99,6 +106,7 @@ type RequestedFileChange struct {
|
||||||
Component string
|
Component string
|
||||||
Key string
|
Key string
|
||||||
Path string // will be used as Key if Key is empty
|
Path string // will be used as Key if Key is empty
|
||||||
|
ParamSymlinks []symlink
|
||||||
ExpectedState FileState
|
ExpectedState FileState
|
||||||
ExpectedContent []byte
|
ExpectedContent []byte
|
||||||
DependsOn []string
|
DependsOn []string
|
||||||
|
@ -127,6 +135,10 @@ func (f *RequestedFileChange) String() string {
|
||||||
s = fmt.Sprintf("file: %s", f.Path)
|
s = fmt.Sprintf("file: %s", f.Path)
|
||||||
case FileStateSymlink:
|
case FileStateSymlink:
|
||||||
s = fmt.Sprintf("symlink: %s -> %s", f.Path, f.ExpectedContent)
|
s = fmt.Sprintf("symlink: %s -> %s", f.Path, f.ExpectedContent)
|
||||||
|
case FileStateSymlinkInOrderConfigFS:
|
||||||
|
s = fmt.Sprintf("symlink_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
|
||||||
|
case FileStateSymlinkNotInOrderConfigFS:
|
||||||
|
s = fmt.Sprintf("symlink_not_in_order_configfs: %s -> %s", f.Path, f.ExpectedContent)
|
||||||
case FileStateAbsent:
|
case FileStateAbsent:
|
||||||
s = fmt.Sprintf("absent: %s", f.Path)
|
s = fmt.Sprintf("absent: %s", f.Path)
|
||||||
case FileStateFileContentMatch:
|
case FileStateFileContentMatch:
|
||||||
|
@ -217,12 +229,20 @@ func (fc *FileChange) getActualState() error {
|
||||||
if fi.IsDir() {
|
if fi.IsDir() {
|
||||||
fc.ActualState = FileStateDirectory
|
fc.ActualState = FileStateDirectory
|
||||||
|
|
||||||
if fc.ExpectedState == FileStateMountedConfigFS {
|
switch fc.ExpectedState {
|
||||||
|
case FileStateMountedConfigFS:
|
||||||
err := fc.checkIfDirIsMountPoint()
|
err := fc.checkIfDirIsMountPoint()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warn().Err(err).Msg("failed to check if dir is mount point")
|
l.Warn().Err(err).Msg("failed to check if dir is mount point")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
case FileStateSymlinkInOrderConfigFS:
|
||||||
|
state, err := checkIfSymlinksInOrder(fc, &l)
|
||||||
|
if err != nil {
|
||||||
|
l.Warn().Err(err).Msg("failed to check if symlinks are in order")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fc.ActualState = state
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -323,6 +343,12 @@ func (fc *FileChange) getFileChangeResolvedAction() FileChangeResolvedAction {
|
||||||
return FileChangeResolvedActionRecreateSymlink
|
return FileChangeResolvedActionRecreateSymlink
|
||||||
}
|
}
|
||||||
return FileChangeResolvedActionCreateSymlink
|
return FileChangeResolvedActionCreateSymlink
|
||||||
|
case FileStateSymlinkInOrderConfigFS:
|
||||||
|
// if the file is already a symlink, check if the target is the same
|
||||||
|
if fc.ActualState == FileStateSymlinkInOrderConfigFS {
|
||||||
|
return FileChangeResolvedActionDoNothing
|
||||||
|
}
|
||||||
|
return FileChangeResolvedActionReorderSymlinks
|
||||||
case FileStateAbsent:
|
case FileStateAbsent:
|
||||||
if fc.ActualState == FileStateAbsent {
|
if fc.ActualState == FileStateAbsent {
|
||||||
return FileChangeResolvedActionDoNothing
|
return FileChangeResolvedActionDoNothing
|
||||||
|
@ -361,6 +387,7 @@ func (c *ChangeSet) ApplyChanges() error {
|
||||||
r := ChangeSetResolver{
|
r := ChangeSetResolver{
|
||||||
changeset: c,
|
changeset: c,
|
||||||
g: &dag.AcyclicGraph{},
|
g: &dag.AcyclicGraph{},
|
||||||
|
l: defaultLogger,
|
||||||
}
|
}
|
||||||
|
|
||||||
return r.Apply()
|
return r.Apply()
|
||||||
|
@ -381,6 +408,8 @@ func (c *ChangeSet) applyChange(change *FileChange) error {
|
||||||
return fmt.Errorf("failed to remove symlink: %w", err)
|
return fmt.Errorf("failed to remove symlink: %w", err)
|
||||||
}
|
}
|
||||||
return os.Symlink(string(change.ExpectedContent), change.Path)
|
return os.Symlink(string(change.ExpectedContent), change.Path)
|
||||||
|
case FileChangeResolvedActionReorderSymlinks:
|
||||||
|
return recreateSymlinks(change, nil)
|
||||||
case FileChangeResolvedActionCreateDirectory:
|
case FileChangeResolvedActionCreateDirectory:
|
||||||
return os.MkdirAll(change.Path, 0755)
|
return os.MkdirAll(change.Path, 0755)
|
||||||
case FileChangeResolvedActionRemove:
|
case FileChangeResolvedActionRemove:
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
package usbgadget
|
package usbgadget
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -27,6 +29,50 @@ var (
|
||||||
usbGadget *UsbGadget
|
usbGadget *UsbGadget
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var oldAbsoluteMouseCombinedReportDesc = []byte{
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
||||||
|
0x09, 0x02, // Usage (Mouse)
|
||||||
|
0xA1, 0x01, // Collection (Application)
|
||||||
|
|
||||||
|
// Report ID 1: Absolute Mouse Movement
|
||||||
|
0x85, 0x01, // Report ID (1)
|
||||||
|
0x09, 0x01, // Usage (Pointer)
|
||||||
|
0xA1, 0x00, // Collection (Physical)
|
||||||
|
0x05, 0x09, // Usage Page (Button)
|
||||||
|
0x19, 0x01, // Usage Minimum (0x01)
|
||||||
|
0x29, 0x03, // Usage Maximum (0x03)
|
||||||
|
0x15, 0x00, // Logical Minimum (0)
|
||||||
|
0x25, 0x01, // Logical Maximum (1)
|
||||||
|
0x75, 0x01, // Report Size (1)
|
||||||
|
0x95, 0x03, // Report Count (3)
|
||||||
|
0x81, 0x02, // Input (Data, Var, Abs)
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x75, 0x05, // Report Size (5)
|
||||||
|
0x81, 0x03, // Input (Cnst, Var, Abs)
|
||||||
|
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
|
||||||
|
0x09, 0x30, // Usage (X)
|
||||||
|
0x09, 0x31, // Usage (Y)
|
||||||
|
0x16, 0x00, 0x00, // Logical Minimum (0)
|
||||||
|
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
|
||||||
|
0x36, 0x00, 0x00, // Physical Minimum (0)
|
||||||
|
0x46, 0xFF, 0x7F, // Physical Maximum (32767)
|
||||||
|
0x75, 0x10, // Report Size (16)
|
||||||
|
0x95, 0x02, // Report Count (2)
|
||||||
|
0x81, 0x02, // Input (Data, Var, Abs)
|
||||||
|
0xC0, // End Collection
|
||||||
|
|
||||||
|
// Report ID 2: Relative Wheel Movement
|
||||||
|
0x85, 0x02, // Report ID (2)
|
||||||
|
0x09, 0x38, // Usage (Wheel)
|
||||||
|
0x15, 0x81, // Logical Minimum (-127)
|
||||||
|
0x25, 0x7F, // Logical Maximum (127)
|
||||||
|
0x75, 0x08, // Report Size (8)
|
||||||
|
0x95, 0x01, // Report Count (1)
|
||||||
|
0x81, 0x06, // Input (Data, Var, Rel)
|
||||||
|
|
||||||
|
0xC0, // End Collection
|
||||||
|
}
|
||||||
|
|
||||||
func TestUsbGadgetInit(t *testing.T) {
|
func TestUsbGadgetInit(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
|
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
|
||||||
|
@ -39,3 +85,31 @@ func TestUsbGadgetStrictModeInitFail(t *testing.T) {
|
||||||
u := NewUsbGadget("test", usbDevices, usbConfig, nil)
|
u := NewUsbGadget("test", usbDevices, usbConfig, nil)
|
||||||
assert.Nil(t, u, "should be nil")
|
assert.Nil(t, u, "should be nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUsbGadgetUDCNotBoundAfterReportDescrChanged(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
usbGadget = NewUsbGadget(usbGadgetName, usbDevices, usbConfig, nil)
|
||||||
|
assert.NotNil(usbGadget)
|
||||||
|
|
||||||
|
// release the usb gadget and create a new one
|
||||||
|
usbGadget = nil
|
||||||
|
|
||||||
|
altGadgetConfig := defaultGadgetConfig
|
||||||
|
|
||||||
|
oldAbsoluteMouseConfig := altGadgetConfig["absolute_mouse"]
|
||||||
|
oldAbsoluteMouseConfig.reportDesc = oldAbsoluteMouseCombinedReportDesc
|
||||||
|
altGadgetConfig["absolute_mouse"] = oldAbsoluteMouseConfig
|
||||||
|
|
||||||
|
usbGadget = newUsbGadget(usbGadgetName, altGadgetConfig, usbDevices, usbConfig, nil)
|
||||||
|
assert.NotNil(usbGadget)
|
||||||
|
|
||||||
|
udcs := getUdcs()
|
||||||
|
assert.Equal(1, len(udcs), "should be only one UDC")
|
||||||
|
// check if the UDC is bound
|
||||||
|
udc := udcs[0]
|
||||||
|
assert.NotNil(udc, "UDC should exist")
|
||||||
|
|
||||||
|
udcStr, err := os.ReadFile("/sys/kernel/config/usb_gadget/jetkvm/UDC")
|
||||||
|
assert.Nil(err, "usb_gadget/UDC should exist")
|
||||||
|
assert.Equal(strings.TrimSpace(udc), strings.TrimSpace(string(udcStr)), "UDC should be the same")
|
||||||
|
}
|
||||||
|
|
|
@ -3,12 +3,14 @@ package usbgadget
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
"github.com/sourcegraph/tf-dag/dag"
|
"github.com/sourcegraph/tf-dag/dag"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChangeSetResolver struct {
|
type ChangeSetResolver struct {
|
||||||
changeset *ChangeSet
|
changeset *ChangeSet
|
||||||
|
|
||||||
|
l *zerolog.Logger
|
||||||
g *dag.AcyclicGraph
|
g *dag.AcyclicGraph
|
||||||
|
|
||||||
changesMap map[string]*FileChange
|
changesMap map[string]*FileChange
|
||||||
|
@ -95,6 +97,10 @@ func (c *ChangeSetResolver) resolveChanges(initial bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, change := range c.resolvedChanges {
|
||||||
|
c.l.Trace().Str("change", change.String()).Msg("resolved change")
|
||||||
|
}
|
||||||
|
|
||||||
if !c.additionalResolveRequired || !initial {
|
if !c.additionalResolveRequired || !initial {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -108,9 +114,9 @@ func (c *ChangeSetResolver) applyChanges() error {
|
||||||
action := change.Action()
|
action := change.Action()
|
||||||
actionStr := FileChangeResolvedActionString[action]
|
actionStr := FileChangeResolvedActionString[action]
|
||||||
|
|
||||||
l := defaultLogger.Info()
|
l := c.l.Info()
|
||||||
if action == FileChangeResolvedActionDoNothing {
|
if action == FileChangeResolvedActionDoNothing {
|
||||||
l = defaultLogger.Trace()
|
l = c.l.Trace()
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Str("action", actionStr).Str("change", change.String()).Msg("applying change")
|
l.Str("action", actionStr).Str("change", change.String()).Msg("applying change")
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
package usbgadget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type symlink struct {
|
||||||
|
Path string
|
||||||
|
Target string
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareSymlinks(expected []symlink, actual []symlink) bool {
|
||||||
|
if len(expected) != len(actual) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return reflect.DeepEqual(expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIfSymlinksInOrder(fc *FileChange, logger *zerolog.Logger) (FileState, error) {
|
||||||
|
if logger == nil {
|
||||||
|
logger = defaultLogger
|
||||||
|
}
|
||||||
|
l := logger.With().Str("path", fc.Path).Logger()
|
||||||
|
|
||||||
|
if fc.ParamSymlinks == nil || len(fc.ParamSymlinks) == 0 {
|
||||||
|
return FileStateUnknown, fmt.Errorf("no symlinks to check")
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := os.Lstat(fc.Path)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return FileStateAbsent, nil
|
||||||
|
} else {
|
||||||
|
l.Warn().Err(err).Msg("failed to stat file")
|
||||||
|
return FileStateUnknown, fmt.Errorf("failed to stat file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fi.IsDir() {
|
||||||
|
return FileStateUnknown, fmt.Errorf("file is not a directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
files, err := os.ReadDir(fc.Path)
|
||||||
|
symlinks := make([]symlink, 0)
|
||||||
|
if err != nil {
|
||||||
|
return FileStateUnknown, fmt.Errorf("failed to read directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Type()&os.ModeSymlink != os.ModeSymlink {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(fc.Path, file.Name())
|
||||||
|
target, err := os.Readlink(path)
|
||||||
|
if err != nil {
|
||||||
|
return FileStateUnknown, fmt.Errorf("failed to read symlink")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !filepath.IsAbs(target) {
|
||||||
|
target = filepath.Join(fc.Path, target)
|
||||||
|
newTarget, err := filepath.Abs(target)
|
||||||
|
if err != nil {
|
||||||
|
return FileStateUnknown, fmt.Errorf("failed to get absolute path")
|
||||||
|
}
|
||||||
|
target = newTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
symlinks = append(symlinks, symlink{
|
||||||
|
Path: path,
|
||||||
|
Target: target,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the symlinks with the expected symlinks
|
||||||
|
if compareSymlinks(fc.ParamSymlinks, symlinks) {
|
||||||
|
return FileStateSymlinkInOrderConfigFS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Trace().Interface("expected", fc.ParamSymlinks).Interface("actual", symlinks).Msg("symlinks are not in order")
|
||||||
|
|
||||||
|
return FileStateSymlinkNotInOrderConfigFS, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func recreateSymlinks(fc *FileChange, logger *zerolog.Logger) error {
|
||||||
|
if logger == nil {
|
||||||
|
logger = defaultLogger
|
||||||
|
}
|
||||||
|
// remove all symlinks
|
||||||
|
files, err := os.ReadDir(fc.Path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logger.With().Str("path", fc.Path).Logger()
|
||||||
|
l.Info().Msg("recreate symlinks")
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if file.Type()&os.ModeSymlink != os.ModeSymlink {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
l.Info().Str("name", file.Name()).Msg("remove symlink")
|
||||||
|
err := os.Remove(path.Join(fc.Path, file.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to remove symlink")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Info().Interface("param-symlinks", fc.ParamSymlinks).Msg("create symlinks")
|
||||||
|
|
||||||
|
// create the symlinks
|
||||||
|
for _, symlink := range fc.ParamSymlinks {
|
||||||
|
l.Info().Str("name", symlink.Path).Str("target", symlink.Target).Msg("create symlink")
|
||||||
|
|
||||||
|
path := symlink.Path
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
path = filepath.Join(fc.Path, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := os.Symlink(symlink.Target, path)
|
||||||
|
if err != nil {
|
||||||
|
l.Warn().Err(err).Msg("failed to create symlink")
|
||||||
|
return fmt.Errorf("failed to create symlink")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -22,6 +22,8 @@ type UsbGadgetTransaction struct {
|
||||||
configC1Path string
|
configC1Path string
|
||||||
orderedConfigItems orderedGadgetConfigItems
|
orderedConfigItems orderedGadgetConfigItems
|
||||||
isGadgetConfigItemEnabled func(key string) bool
|
isGadgetConfigItemEnabled func(key string) bool
|
||||||
|
|
||||||
|
reorderSymlinkChanges *RequestedFileChange
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UsbGadget) newUsbGadgetTransaction(lock bool) error {
|
func (u *UsbGadget) newUsbGadgetTransaction(lock bool) error {
|
||||||
|
@ -96,6 +98,8 @@ func (tx *UsbGadgetTransaction) removeFile(component string, path string, descri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) Commit() error {
|
func (tx *UsbGadgetTransaction) Commit() error {
|
||||||
|
tx.addFileChange("gadget-finalize", *tx.reorderSymlinkChanges)
|
||||||
|
|
||||||
err := tx.c.Apply()
|
err := tx.c.Apply()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.log.Error().Err(err).Msg("failed to update usbgadget configuration")
|
tx.log.Error().Err(err).Msg("failed to update usbgadget configuration")
|
||||||
|
@ -151,6 +155,21 @@ func (tx *UsbGadgetTransaction) WriteGadgetConfig() {
|
||||||
tx.WriteUDC()
|
tx.WriteUDC()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tx *UsbGadgetTransaction) getDisableKeys() []string {
|
||||||
|
disableKeys := make([]string, 0)
|
||||||
|
for _, item := range tx.orderedConfigItems {
|
||||||
|
if !tx.isGadgetConfigItemEnabled(item.key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.item.configPath == nil || item.item.configAttrs != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
disableKeys = append(disableKeys, fmt.Sprintf("disable-%s", item.item.device))
|
||||||
|
}
|
||||||
|
return disableKeys
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) DisableGadgetItemConfig(item gadgetConfigItem) {
|
func (tx *UsbGadgetTransaction) DisableGadgetItemConfig(item gadgetConfigItem) {
|
||||||
// remove symlink if exists
|
// remove symlink if exists
|
||||||
if item.configPath == nil {
|
if item.configPath == nil {
|
||||||
|
@ -174,7 +193,7 @@ func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, dep
|
||||||
beforeChange := make([]string, 0)
|
beforeChange := make([]string, 0)
|
||||||
disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device)
|
disableGadgetItemKey := fmt.Sprintf("disable-%s", item.device)
|
||||||
if item.configPath != nil && item.configAttrs == nil {
|
if item.configPath != nil && item.configAttrs == nil {
|
||||||
beforeChange = append(beforeChange, disableGadgetItemKey)
|
beforeChange = append(beforeChange, tx.getDisableKeys()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(item.attrs) > 0 {
|
if len(item.attrs) > 0 {
|
||||||
|
@ -234,13 +253,7 @@ func (tx *UsbGadgetTransaction) writeGadgetItemConfig(item gadgetConfigItem, dep
|
||||||
Description: "remove symlink",
|
Description: "remove symlink",
|
||||||
})
|
})
|
||||||
|
|
||||||
tx.addFileChange(component, RequestedFileChange{
|
tx.addReorderSymlinkChange(configPath, gadgetItemPath, files)
|
||||||
Path: configPath,
|
|
||||||
ExpectedState: FileStateSymlink,
|
|
||||||
ExpectedContent: []byte(gadgetItemPath),
|
|
||||||
Description: "create symlink",
|
|
||||||
DependsOn: files,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
@ -263,6 +276,27 @@ func (tx *UsbGadgetTransaction) writeGadgetAttrs(basePath string, attrs gadgetAt
|
||||||
return files
|
return files
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tx *UsbGadgetTransaction) addReorderSymlinkChange(path string, target string, deps []string) {
|
||||||
|
tx.log.Trace().Str("path", path).Str("target", target).Msg("add reorder symlink change")
|
||||||
|
|
||||||
|
if tx.reorderSymlinkChanges == nil {
|
||||||
|
tx.reorderSymlinkChanges = &RequestedFileChange{
|
||||||
|
Component: "gadget-finalize",
|
||||||
|
Key: "reorder-symlinks",
|
||||||
|
Path: tx.configC1Path,
|
||||||
|
ExpectedState: FileStateSymlinkInOrderConfigFS,
|
||||||
|
Description: "order symlinks",
|
||||||
|
ParamSymlinks: []symlink{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.reorderSymlinkChanges.DependsOn = append(tx.reorderSymlinkChanges.DependsOn, deps...)
|
||||||
|
tx.reorderSymlinkChanges.ParamSymlinks = append(tx.reorderSymlinkChanges.ParamSymlinks, symlink{
|
||||||
|
Path: path,
|
||||||
|
Target: target,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (tx *UsbGadgetTransaction) WriteUDC() {
|
func (tx *UsbGadgetTransaction) WriteUDC() {
|
||||||
// bound the gadget to a UDC (USB Device Controller)
|
// bound the gadget to a UDC (USB Device Controller)
|
||||||
path := path.Join(tx.kvmGadgetPath, "UDC")
|
path := path.Join(tx.kvmGadgetPath, "UDC")
|
||||||
|
@ -270,6 +304,7 @@ func (tx *UsbGadgetTransaction) WriteUDC() {
|
||||||
Path: path,
|
Path: path,
|
||||||
ExpectedState: FileStateFileContentMatch,
|
ExpectedState: FileStateFileContentMatch,
|
||||||
ExpectedContent: []byte(tx.udc),
|
ExpectedContent: []byte(tx.udc),
|
||||||
|
DependsOn: []string{"reorder-symlinks"},
|
||||||
Description: "write UDC",
|
Description: "write UDC",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,24 +107,16 @@ func (u *UsbGadget) AbsMouseWheelReport(wheelY int8) error {
|
||||||
u.absMouseLock.Lock()
|
u.absMouseLock.Lock()
|
||||||
defer u.absMouseLock.Unlock()
|
defer u.absMouseLock.Unlock()
|
||||||
|
|
||||||
// Accumulate the wheelY value
|
// Only send a report if the value is non-zero
|
||||||
u.absMouseAccumulatedWheelY += float64(wheelY) / 8.0
|
if wheelY == 0 {
|
||||||
|
|
||||||
// Only send a report if the accumulated value is significant
|
|
||||||
if abs(u.absMouseAccumulatedWheelY) < 1.0 {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
scaledWheelY := int8(u.absMouseAccumulatedWheelY)
|
|
||||||
|
|
||||||
err := u.absMouseWriteHidFile([]byte{
|
err := u.absMouseWriteHidFile([]byte{
|
||||||
2, // Report ID 2
|
2, // Report ID 2
|
||||||
byte(scaledWheelY), // Scaled Wheel Y (signed)
|
byte(wheelY), // Wheel Y (signed)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reset the accumulator, keeping any remainder
|
|
||||||
u.absMouseAccumulatedWheelY -= float64(scaledWheelY)
|
|
||||||
|
|
||||||
u.resetUserInputTime()
|
u.resetUserInputTime()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,10 @@ var defaultLogger = logging.GetSubsystemLogger("usbgadget")
|
||||||
|
|
||||||
// NewUsbGadget creates a new UsbGadget.
|
// NewUsbGadget creates a new UsbGadget.
|
||||||
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
||||||
|
return newUsbGadget(name, defaultGadgetConfig, enabledDevices, config, logger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUsbGadget(name string, configMap map[string]gadgetConfigItem, enabledDevices *Devices, config *Config, logger *zerolog.Logger) *UsbGadget {
|
||||||
if logger == nil {
|
if logger == nil {
|
||||||
logger = defaultLogger
|
logger = defaultLogger
|
||||||
}
|
}
|
||||||
|
@ -96,7 +100,7 @@ func NewUsbGadget(name string, enabledDevices *Devices, config *Config, logger *
|
||||||
name: name,
|
name: name,
|
||||||
kvmGadgetPath: path.Join(gadgetPath, name),
|
kvmGadgetPath: path.Join(gadgetPath, name),
|
||||||
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
configC1Path: path.Join(gadgetPath, name, "configs/c.1"),
|
||||||
configMap: defaultGadgetConfig,
|
configMap: configMap,
|
||||||
customConfig: *config,
|
customConfig: *config,
|
||||||
configLock: sync.Mutex{},
|
configLock: sync.Mutex{},
|
||||||
keyboardLock: sync.Mutex{},
|
keyboardLock: sync.Mutex{},
|
||||||
|
|
|
@ -8,14 +8,6 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Helper function to get absolute value of float64
|
|
||||||
func abs(x float64) float64 {
|
|
||||||
if x < 0 {
|
|
||||||
return -x
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinPath(basePath string, paths []string) string {
|
func joinPath(basePath string, paths []string) string {
|
||||||
pathArr := append([]string{basePath}, paths...)
|
pathArr := append([]string{basePath}, paths...)
|
||||||
return filepath.Join(pathArr...)
|
return filepath.Join(pathArr...)
|
||||||
|
|
|
@ -259,25 +259,25 @@ export default function WebRTCVideo() {
|
||||||
(e: WheelEvent) => {
|
(e: WheelEvent) => {
|
||||||
if (blockWheelEvent) return;
|
if (blockWheelEvent) return;
|
||||||
|
|
||||||
// Determine if the wheel event is from a trackpad or a mouse wheel
|
// Determine if the wheel event is an accel scroll value
|
||||||
const isTrackpad = Math.abs(e.deltaY) < trackpadThreshold;
|
const isAccel = Math.abs(e.deltaY) >= 100;
|
||||||
|
|
||||||
// Apply appropriate sensitivity based on input device
|
// Calculate the accel scroll value
|
||||||
const scrollSensitivity = isTrackpad ? trackpadSensitivity : mouseSensitivity;
|
const accelScrollValue = e.deltaY / 100;
|
||||||
|
|
||||||
// Calculate the scroll value
|
// Calculate the no accel scroll value
|
||||||
const scroll = e.deltaY * scrollSensitivity;
|
const noAccelScrollValue = e.deltaY > 0 ? 1 : (e.deltaY < 0 ? -1 : 0);
|
||||||
|
|
||||||
// Apply clamping
|
// Get scroll value
|
||||||
const clampedScroll = Math.max(clampMin, Math.min(clampMax, scroll));
|
const scrollValue = isAccel ? accelScrollValue : noAccelScrollValue;
|
||||||
|
|
||||||
// Round to the nearest integer
|
// Apply clamping (i.e. min and max mouse wheel hardware value)
|
||||||
const roundedScroll = Math.round(clampedScroll);
|
const clampedScrollValue = Math.max(-127, Math.min(127, scrollValue));
|
||||||
|
|
||||||
// Invert the scroll value to match expected behavior
|
// Invert the clamped scroll value to match expected behavior
|
||||||
const invertedScroll = -roundedScroll;
|
const invertedScrollValue = -clampedScrollValue;
|
||||||
|
|
||||||
send("wheelReport", { wheelY: invertedScroll });
|
send("wheelReport", { wheelY : invertedScrollValue });
|
||||||
|
|
||||||
// Apply blocking delay
|
// Apply blocking delay
|
||||||
setBlockWheelEvent(true);
|
setBlockWheelEvent(true);
|
||||||
|
|
Loading…
Reference in New Issue