diff --git a/Makefile b/Makefile index 591d64b4..fa56a237 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ DOCKER_RUN_BASE := docker run -e UID=$(UID) -e GID=$(GID) -v $(PWD):/home/build/ GO_BUILD_CMD := cd /home/build/NanoKVM/server && go mod tidy && CGO_ENABLED=1 GOOS=linux GOARCH=riscv64 CC=riscv64-unknown-linux-musl-gcc CGO_CFLAGS="-mcpu=c906fdv -march=rv64imafdcv0p7xthead -mcmodel=medany -mabi=lp64d" go build SUPPORT_BUILD_CMD := . ./home/build/MaixCDK/bin/activate && cd /home/build/NanoKVM/support/sg2002 && ./build kvm_system && ./build kvm_system add_to_kvmapp -.PHONY: help check-root builder-image rebuild-image check-image shell app support all clean +.PHONY: help check-root builder-image rebuild-image check-image shell app support all test-usb-scripts clean # Default target all: app support @@ -31,6 +31,7 @@ help: @echo " app - Build Go application server" @echo " support - Build hardware support libraries" @echo " all - Build both app and support (default)" + @echo " test-usb-scripts - Run USB init script regression tests" @echo " clean - Clean build artifacts" @echo "" @echo "Prerequisites:" @@ -83,6 +84,10 @@ support: check-root builder-image @echo "Building support..." @$(DOCKER_RUN_BASE) -it $(IMAGE_NAME) /bin/bash -c '$(SUPPORT_BUILD_CMD)' +test-usb-scripts: + @echo "Running USB init script tests..." + @sh tests/usb-init-scripts-test.sh + # Clean build artifacts clean: @echo "Cleaning build artifacts..." @@ -94,4 +99,4 @@ clean: rm -rf support/sg2002/build; \ echo "Removed support/sg2002/build"; \ fi - @echo "Clean completed." \ No newline at end of file + @echo "Clean completed." diff --git a/kvmapp/system/init.d/S03usb-common b/kvmapp/system/init.d/S03usb-common new file mode 100644 index 00000000..5783eaa1 --- /dev/null +++ b/kvmapp/system/init.d/S03usb-common @@ -0,0 +1,225 @@ +#!/bin/sh +# shellcheck disable=SC2034 + +USB_BOOT_DIR="${USB_BOOT_DIR:-/boot}" +USB_GADGET_ROOT="${USB_GADGET_ROOT:-/sys/kernel/config/usb_gadget}" +USB_GADGET_NAME="${USB_GADGET_NAME:-g0}" +USB_UDC_CLASS="${USB_UDC_CLASS:-/sys/class/udc}" +USB_OTG_ROLE="${USB_OTG_ROLE:-/proc/cviusb/otg_role}" +USB_PHY_DEVICE="${USB_PHY_DEVICE:-4340000.usb}" +USB_DWC2_BIND="${USB_DWC2_BIND:-/sys/bus/platform/drivers/dwc2/bind}" +USB_DWC2_UNBIND="${USB_DWC2_UNBIND:-/sys/bus/platform/drivers/dwc2/unbind}" +PATH="/sbin:/usr/sbin:/bin:/usr/bin${PATH:+:${PATH}}" +export PATH + +USB_DEFAULT_VENDOR_ID='0x3346' +USB_DEFAULT_PRODUCT_ID='0x1009' +USB_SERIAL_NUMBER='0123456789ABCDEF' +USB_MANUFACTURER='sipeed' +USB_PRODUCT='NanoKVM' +USB_CONFIGURATION='NanoKVM' + +USB_NORMAL_BCD_USB='0x0200' +USB_NORMAL_BCD_DEVICE='0x0510' +USB_NORMAL_CONFIG_ATTRIBUTES='0xE0' +USB_NORMAL_MAX_POWER='120' +USB_HID_ONLY_BCD_USB='0x0110' +USB_HID_ONLY_BCD_DEVICE='0x0623' +USB_HID_ONLY_CONFIG_ATTRIBUTES='0xA0' +USB_HID_ONLY_MAX_POWER='200' + +USB_HID_KEYBOARD_FUNC='hid.GS0' +USB_HID_RELATIVE_MOUSE_FUNC='hid.GS1' +USB_HID_ABSOLUTE_MOUSE_FUNC='hid.GS2' +USB_HID_BOOT_SUBCLASS='1' +USB_HID_NON_BOOT_SUBCLASS='0' +USB_HID_KEYBOARD_PROTOCOL='1' +USB_HID_MOUSE_PROTOCOL='2' +USB_HID_KEYBOARD_REPORT_LENGTH='8' +USB_HID_RELATIVE_MOUSE_REPORT_LENGTH='4' +USB_HID_ABSOLUTE_MOUSE_REPORT_LENGTH='6' + +USB_HID_REPORT_NORMAL_KEYBOARD='normal-keyboard' +USB_HID_REPORT_HID_ONLY_KEYBOARD='hid-only-keyboard' +USB_HID_REPORT_RELATIVE_MOUSE='relative-mouse' +USB_HID_REPORT_NORMAL_ABSOLUTE_MOUSE='normal-absolute-mouse' +USB_HID_REPORT_HID_ONLY_ABSOLUTE_MOUSE='hid-only-absolute-mouse' + +remove_existing_gadget(){ + gadget="${USB_GADGET_ROOT}/${USB_GADGET_NAME}" + [ -d "${gadget}" ] || return 0 + + echo '' > "${gadget}/UDC" 2>/dev/null || true + if [ -d "${gadget}/os_desc" ] + then + find "${gadget}/os_desc" -mindepth 1 -maxdepth 1 -type l -exec rm {} \; + fi + if [ -d "${gadget}/configs/c.1" ] + then + find "${gadget}/configs/c.1" -mindepth 1 -maxdepth 1 -type l -exec rm {} \; + rmdir "${gadget}/configs/c.1/strings/0x409" 2>/dev/null || true + rmdir "${gadget}/configs/c.1/strings" 2>/dev/null || true + rmdir "${gadget}/configs/c.1" 2>/dev/null || true + fi + + if [ -d "${gadget}/functions" ] + then + for func in "${gadget}/functions"/* + do + [ -d "${func}" ] || continue + rmdir "${func}/lun.0" 2>/dev/null || true + rmdir "${func}" 2>/dev/null || true + done + fi + + rmdir "${gadget}/strings/0x409" 2>/dev/null || true + rmdir "${gadget}/strings" 2>/dev/null || true + rmdir "${gadget}/configs" 2>/dev/null || true + rmdir "${gadget}/functions" 2>/dev/null || true + rmdir "${gadget}" 2>/dev/null || true +} + +write_usb_strings(){ + mkdir strings/0x409 + printf '%s' "${USB_SERIAL_NUMBER}" > strings/0x409/serialnumber + printf '%s' "${USB_MANUFACTURER}" > strings/0x409/manufacturer + printf '%s' "${USB_PRODUCT}" > strings/0x409/product +} + +write_usb_config_string(){ + mkdir -p configs/c.1/strings/0x409 + printf '%s' "${USB_CONFIGURATION}" > configs/c.1/strings/0x409/configuration +} + +write_boot_or_default(){ + if [ -e "${USB_BOOT_DIR}/$2" ] + then + cat "${USB_BOOT_DIR}/$2" > "$1" + else + echo "$3" > "$1" + fi +} + +prepare_usb_gadget(){ + echo "usb mode: device" + cd "${USB_GADGET_ROOT}" || exit 1 + remove_existing_gadget + mkdir "${USB_GADGET_NAME}" + cd "${USB_GADGET_NAME}" || exit 1 + + echo "$1" > bcdUSB + echo "$2" > bcdDevice + write_boot_or_default idVendor usb.vid "${USB_DEFAULT_VENDOR_ID}" + write_boot_or_default idProduct usb.pid "${USB_DEFAULT_PRODUCT_ID}" + write_usb_strings + + mkdir configs/c.1 + echo "$3" > configs/c.1/bmAttributes + echo "$4" > configs/c.1/MaxPower + write_usb_config_string +} + +set_hid_wakeup(){ + if [ ! -e "${USB_BOOT_DIR}/usb.notwakeup" ] && [ -e "functions/$1/wakeup_on_write" ] + then + echo 1 > "functions/$1/wakeup_on_write" + fi +} + +add_hid_function(){ + func="$1" + subclass="$2" + protocol="$3" + report_length="$4" + report_desc="$5" + + mkdir "functions/${func}" + echo "${subclass}" > "functions/${func}/subclass" + set_hid_wakeup "${func}" + echo "${protocol}" > "functions/${func}/protocol" + echo "${report_length}" > "functions/${func}/report_length" + write_hid_report_desc "${report_desc}" > "functions/${func}/report_desc" + ln -s "functions/${func}" configs/c.1 +} + +write_hid_report_desc(){ + case "$1" in + "${USB_HID_REPORT_NORMAL_KEYBOARD}") + printf '%b' '\005\001\011\006\241\001\005\007\031\340\051\347\025\000\045\001\165\001\225\010\201\002\225\001\165\010\201\003\225\005\165\001\005\010\031\001\051\005\221\002\225\001\165\003\221\003\225\006\165\010\025\000\045\347\005\007\031\000\051\347\201\000\300' + ;; + "${USB_HID_REPORT_HID_ONLY_KEYBOARD}") + printf '%b' '\005\001\011\006\241\001\005\007\031\340\051\347\025\000\045\001\165\001\225\010\201\002\225\001\165\010\201\003\225\005\165\001\005\010\031\001\051\005\221\002\225\001\165\003\221\003\225\006\165\010\025\000\045\145\005\007\031\000\051\145\201\000\300' + ;; + "${USB_HID_REPORT_RELATIVE_MOUSE}") + printf '%b' '\005\001\011\002\241\001\011\001\241\000\005\011\031\001\051\003\025\000\045\001\225\003\165\001\201\002\225\001\165\005\201\003\005\001\011\060\011\061\011\070\025\201\045\177\165\010\225\003\201\006\300\300' + ;; + "${USB_HID_REPORT_NORMAL_ABSOLUTE_MOUSE}") + printf '%b' '\005\001\011\002\241\001\011\001\241\000\005\011\031\001\051\005\025\000\045\001\225\005\165\001\201\002\225\001\165\003\201\001\005\001\011\060\011\061\025\000\046\377\177\065\000\106\377\177\165\020\225\002\201\002\005\001\011\070\025\201\045\177\065\000\105\000\165\010\225\001\201\006\300\300' + ;; + "${USB_HID_REPORT_HID_ONLY_ABSOLUTE_MOUSE}") + printf '%b' '\005\001\011\002\241\001\011\001\241\000\005\011\031\001\051\003\025\000\045\001\225\003\165\001\201\002\225\001\165\005\201\001\005\001\011\060\011\061\025\000\046\377\177\065\000\106\377\177\165\020\225\002\201\002\005\001\011\070\025\201\045\177\065\000\105\000\165\010\225\001\201\006\300\300' + ;; + *) echo "unknown HID report descriptor: $1" >&2; return 1 ;; + esac +} + +add_hid_functions(){ + keyboard_report="$1" + absolute_mouse_report="$2" + add_hid_function "${USB_HID_KEYBOARD_FUNC}" "${USB_HID_BOOT_SUBCLASS}" "${USB_HID_KEYBOARD_PROTOCOL}" "${USB_HID_KEYBOARD_REPORT_LENGTH}" "${keyboard_report}" + add_hid_function "${USB_HID_RELATIVE_MOUSE_FUNC}" "${USB_HID_BOOT_SUBCLASS}" "${USB_HID_MOUSE_PROTOCOL}" "${USB_HID_RELATIVE_MOUSE_REPORT_LENGTH}" "${USB_HID_REPORT_RELATIVE_MOUSE}" + add_hid_function "${USB_HID_ABSOLUTE_MOUSE_FUNC}" "${USB_HID_NON_BOOT_SUBCLASS}" "${USB_HID_MOUSE_PROTOCOL}" "${USB_HID_ABSOLUTE_MOUSE_REPORT_LENGTH}" "${absolute_mouse_report}" +} + +first_udc(){ find "${USB_UDC_CLASS}" -mindepth 1 -maxdepth 1 -exec basename {} \; | sort | sed -n '1p'; } + +bind_first_udc(){ + udc=$(first_udc) + [ -n "${udc}" ] || return 1 + printf '%s' "${udc}" > UDC + echo device > "${USB_OTG_ROLE}" +} + +unbind_udc(){ + echo '' > "${USB_GADGET_ROOT}/${USB_GADGET_NAME}/UDC" +} + +start_usb_host(){ + unbind_udc + echo host > "${USB_OTG_ROLE}" +} + +restart_udc(){ + echo '' > "${USB_GADGET_ROOT}/${USB_GADGET_NAME}/UDC" + sleep 1 + udc=$(first_udc) + [ -n "${udc}" ] || return 1 + printf '%s' "${udc}" > "${USB_GADGET_ROOT}/${USB_GADGET_NAME}/UDC" +} + +restart_usb_dev(){ + restart_udc + echo "USB Restart OK!" +} + +restart_usb_phy(){ + command -v start_usb_dev >/dev/null || { + echo "start_usb_dev is not defined" >&2 + return 1 + } + echo "${USB_PHY_DEVICE}" > "${USB_DWC2_UNBIND}" + sleep 1 + echo "${USB_PHY_DEVICE}" > "${USB_DWC2_BIND}" + start_usb_host + start_usb_dev +} + +run_usb_action(){ + case "$1" in + start) start_usb_dev ;; + restart) restart_usb_dev ;; + stop) start_usb_host ;; + stop_start) start_usb_host; start_usb_dev ;; + restart_phy) restart_usb_phy ;; + esac +} diff --git a/kvmapp/system/init.d/S03usbdev b/kvmapp/system/init.d/S03usbdev index e4a2c834..ca7c2dc7 100755 --- a/kvmapp/system/init.d/S03usbdev +++ b/kvmapp/system/init.d/S03usbdev @@ -1,179 +1,77 @@ #!/bin/sh # kvmhwd Rev2.2 -start_usb_dev(){ - . /etc/profile - echo "usb mode: device" - cd /sys/kernel/config/usb_gadget +# shellcheck source=/kvmapp/system/init.d/S03usb-common +. "${USB_COMMON:-/kvmapp/system/init.d/S03usb-common}" - mkdir g0 - cd g0 +USB_NCM_FUNC='ncm.usb0' +USB_RNDIS_FUNC='rndis.usb0' +USB_MASS_STORAGE_FUNC='mass_storage.disk0' +USB_MASS_STORAGE_INQUIRY='NanoKVM USB Mass Storage0520' +USB_LEGACY_EMPTY_DISK_BACKING='/dev/mmcblk0p3' - # echo 0x3346 > idVendor - # echo 0x1009 > idProduct - if [ -e /boot/usb.vid ] - then - cat /boot/usb.vid > idVendor - else - echo 0x3346 > idVendor - fi - if [ -e /boot/usb.pid ] - then - cat /boot/usb.pid > idProduct - else - echo 0x1009 > idProduct - fi +start_usb_dev(){ + prepare_usb_gadget "${USB_NORMAL_BCD_USB}" "${USB_NORMAL_BCD_DEVICE}" "${USB_NORMAL_CONFIG_ATTRIBUTES}" "${USB_NORMAL_MAX_POWER}" echo 0xEF > bDeviceClass echo 0x02 > bDeviceSubClass echo 0x01 > bDeviceProtocol - mkdir strings/0x409 - echo '0123456789ABCDEF' > strings/0x409/serialnumber - echo 'sipeed' > strings/0x409/manufacturer - echo 'NanoKVM' > strings/0x409/product - - mkdir configs/c.1 - echo 0xE0 > configs/c.1/bmAttributes - echo 120 > configs/c.1/MaxPower - mkdir configs/c.1/strings/0x409 - echo "NanoKVM" > configs/c.1/strings/0x409/configuration - - if [ -e /boot/usb.ncm ] + if [ -e "${USB_BOOT_DIR}/usb.ncm" ] then - mkdir functions/ncm.usb0 - echo WINNCM > functions/ncm.usb0/os_desc/interface.ncm/compatible_id - ln -s functions/ncm.usb0 configs/c.1/ - else - if [ -e /boot/usb.rndis0 ] - then - mkdir functions/rndis.usb0 - echo e0 > functions/rndis.usb0/class - echo 01 > functions/rndis.usb0/subclass - echo 03 > functions/rndis.usb0/protocol - echo RNDIS > functions/rndis.usb0/os_desc/interface.rndis/compatible_id - echo 5162001 > functions/rndis.usb0/os_desc/interface.rndis/sub_compatible_id - ln -s functions/rndis.usb0 configs/c.1/ - fi + mkdir "functions/${USB_NCM_FUNC}" + echo WINNCM > "functions/${USB_NCM_FUNC}/os_desc/interface.ncm/compatible_id" + ln -s "functions/${USB_NCM_FUNC}" configs/c.1/ + elif [ -e "${USB_BOOT_DIR}/usb.rndis0" ] + then + mkdir "functions/${USB_RNDIS_FUNC}" + echo e0 > "functions/${USB_RNDIS_FUNC}/class" + echo 01 > "functions/${USB_RNDIS_FUNC}/subclass" + echo 03 > "functions/${USB_RNDIS_FUNC}/protocol" + echo RNDIS > "functions/${USB_RNDIS_FUNC}/os_desc/interface.rndis/compatible_id" + echo 5162001 > "functions/${USB_RNDIS_FUNC}/os_desc/interface.rndis/sub_compatible_id" + ln -s "functions/${USB_RNDIS_FUNC}" configs/c.1/ fi - if [ -e /boot/usb.ncm ] || [ -e /boot/usb.rndis0 ] + if [ -e "${USB_BOOT_DIR}/usb.ncm" ] || [ -e "${USB_BOOT_DIR}/usb.rndis0" ] then - # Microsoft OS 2.0 Descriptor echo 1 > os_desc/use echo 0xCD > os_desc/b_vendor_code echo MSFT100 > os_desc/qw_sign ln -s configs/c.1 os_desc fi - if [ ! -e /boot/disable_hid ] + if [ ! -e "${USB_BOOT_DIR}/disable_hid" ] then - # keyboard - mkdir functions/hid.GS0 - if [ -e /boot/BIOS ] - then - echo 1 > functions/hid.GS0/subclass - fi - if [ ! -e /boot/usb.notwakeup ] - then - echo 1 > functions/hid.GS0/wakeup_on_write - fi - echo 1 > functions/hid.GS0/protocol - echo 8 > functions/hid.GS0/report_length - echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\xE7\\x05\\x07\\x19\\x00\\x29\\xE7\\x81\\x00\\xc0 > functions/hid.GS0/report_desc - ln -s functions/hid.GS0 configs/c.1 - - # mouse - mkdir functions/hid.GS1 - if [ -e /boot/BIOS ] - then - echo 1 > functions/hid.GS1/subclass - fi - if [ ! -e /boot/usb.notwakeup ] - then - echo 1 > functions/hid.GS1/wakeup_on_write - fi - echo 2 > functions/hid.GS1/protocol - echo 4 > functions/hid.GS1/report_length - echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc - ln -s functions/hid.GS1 configs/c.1 + add_hid_functions "${USB_HID_REPORT_NORMAL_KEYBOARD}" "${USB_HID_REPORT_NORMAL_ABSOLUTE_MOUSE}" + fi - # touchpad - mkdir functions/hid.GS2 - if [ -e /boot/BIOS ] + if [ -e "${USB_BOOT_DIR}/usb.disk0" ] + then + mkdir "functions/${USB_MASS_STORAGE_FUNC}" + ln -s "functions/${USB_MASS_STORAGE_FUNC}" configs/c.1/ + echo 1 > "functions/${USB_MASS_STORAGE_FUNC}/lun.0/removable" + if [ -e "${USB_BOOT_DIR}/usb.disk0.ro" ] then - echo 1 > functions/hid.GS2/subclass + echo 1 > "functions/${USB_MASS_STORAGE_FUNC}/lun.0/ro" + echo 0 > "functions/${USB_MASS_STORAGE_FUNC}/lun.0/cdrom" fi - if [ ! -e /boot/usb.notwakeup ] + echo "${USB_MASS_STORAGE_INQUIRY}" > "functions/${USB_MASS_STORAGE_FUNC}/lun.0/inquiry_string" + disk=$(cat "${USB_BOOT_DIR}/usb.disk0") + if [ -z "${disk}" ] then - echo 1 > functions/hid.GS2/wakeup_on_write + # if [ ! -e /mnt/usbdisk.img ] + # then + # fallocate -l 8G /mnt/usbdisk.img + # mkfs.vfat /mnt/usbdisk.img + # fi + echo "${USB_LEGACY_EMPTY_DISK_BACKING}" > "functions/${USB_MASS_STORAGE_FUNC}/lun.0/file" + else + printf '%s' "${disk}" > "functions/${USB_MASS_STORAGE_FUNC}/lun.0/file" fi - echo 2 > functions/hid.GS2/protocol - echo 6 > functions/hid.GS2/report_length - echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x05\\x15\\x00\\x25\\x01\\x95\\x05\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x03\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x15\\x00\\x26\\xff\\x7f\\x35\\x00\\x46\\xff\\x7f\\x75\\x10\\x95\\x02\\x81\\x02\\x05\\x01\\x09\\x38\\x15\\x81\\x25\\x7f\\x35\\x00\\x45\\x00\\x75\\x08\\x95\\x01\\x81\\x06\\xc0\\xc0 > functions/hid.GS2/report_desc - ln -s functions/hid.GS2 configs/c.1 fi - if [ -e /boot/usb.disk0 ] - then - mkdir functions/mass_storage.disk0 - ln -s functions/mass_storage.disk0 configs/c.1/ - echo 1 > functions/mass_storage.disk0/lun.0/removable - if [ -e /boot/usb.disk0.ro ] - then - echo 1 > functions/mass_storage.disk0/lun.0/ro - echo 0 > functions/mass_storage.disk0/lun.0/cdrom - fi - echo "NanoKVM USB Mass Storage0520" > functions/mass_storage.disk0/lun.0/inquiry_string - disk=$(cat /boot/usb.disk0) - if [ -z "${disk}" ] - then - # if [ ! -e /mnt/usbdisk.img ] - # then - # fallocate -l 8G /mnt/usbdisk.img - # mkfs.vfat /mnt/usbdisk.img - # fi - echo /dev/mmcblk0p3 > functions/mass_storage.disk0/lun.0/file - else - cat /boot/usb.disk0 > functions/mass_storage.disk0/lun.0/file - fi - fi - - ls /sys/class/udc/ | cat > UDC - echo device > /proc/cviusb/otg_role -} - -start_usb_host(){ - echo '' > /sys/kernel/config/usb_gadget/g0/UDC - echo host > /proc/cviusb/otg_role -} - -restart_usb_dev(){ - echo > /sys/kernel/config/usb_gadget/g0/UDC - sleep 1 - ls /sys/class/udc/ | cat > /sys/kernel/config/usb_gadget/g0/UDC - echo "USB Restart OK!" + bind_first_udc } -case "$1" in - start) - start_usb_dev - ;; - restart) - restart_usb_dev - ;; - stop) - start_usb_host - ;; - stop_start) - start_usb_host - start_usb_dev - ;; - restart_phy) - echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/unbind - sleep 1 - echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/bind - start_usb_host - start_usb_dev - ;; -esac +run_usb_action "$1" diff --git a/kvmapp/system/init.d/S03usbhid b/kvmapp/system/init.d/S03usbhid index b5f160e8..8f1179b1 100755 --- a/kvmapp/system/init.d/S03usbhid +++ b/kvmapp/system/init.d/S03usbhid @@ -1,112 +1,13 @@ #!/bin/sh # kvmhwd hid only -start_usb_dev(){ - . /etc/profile - echo "usb mode: device" - cd /sys/kernel/config/usb_gadget - - mkdir g0 - cd g0 - - echo 0x0101 > bcdUSB - echo 0x0623 > bcdDevice - # echo 0x3346 > idVendor - # echo 0x1009 > idProduct - if [ -e /boot/usb.vid ] - then - cat /boot/usb.vid > idVendor - else - echo 0x3346 > idVendor - fi - if [ -e /boot/usb.pid ] - then - cat /boot/usb.pid > idProduct - else - echo 0x1009 > idProduct - fi - mkdir strings/0x409 - # echo '0' > strings/0x409/serialnumber - echo 'sipeed' > strings/0x409/manufacturer - echo 'NanoKVM' > strings/0x409/product - - mkdir configs/c.1 - echo 0xA0 > configs/c.1/bmAttributes - echo 200 > configs/c.1/MaxPower - mkdir configs/c.1/strings/0x409 - echo "NanoKVM" > configs/c.1/strings/0x409/configuration - - # keyboard - mkdir functions/hid.GS0 - echo 1 > functions/hid.GS0/subclass - if [ ! -e /boot/usb.notwakeup ] - then - echo 1 > functions/hid.GS0/wakeup_on_write - fi - echo 1 > functions/hid.GS0/protocol - echo 8 > functions/hid.GS0/report_length - echo -ne \\x05\\x01\\x09\\x06\\xa1\\x01\\x05\\x07\\x19\\xe0\\x29\\xe7\\x15\\x00\\x25\\x01\\x75\\x01\\x95\\x08\\x81\\x02\\x95\\x01\\x75\\x08\\x81\\x03\\x95\\x05\\x75\\x01\\x05\\x08\\x19\\x01\\x29\\x05\\x91\\x02\\x95\\x01\\x75\\x03\\x91\\x03\\x95\\x06\\x75\\x08\\x15\\x00\\x25\\x65\\x05\\x07\\x19\\x00\\x29\\x65\\x81\\x00\\xc0 > functions/hid.GS0/report_desc - ln -s functions/hid.GS0 configs/c.1 - - # mouse - mkdir functions/hid.GS1 - echo 1 > functions/hid.GS1/subclass - if [ ! -e /boot/usb.notwakeup ] - then - echo 1 > functions/hid.GS1/wakeup_on_write - fi - echo 2 > functions/hid.GS1/protocol - echo 4 > functions/hid.GS1/report_length - echo -ne \\x5\\x1\\x9\\x2\\xa1\\x1\\x9\\x1\\xa1\\x0\\x5\\x9\\x19\\x1\\x29\\x3\\x15\\x0\\x25\\x1\\x95\\x3\\x75\\x1\\x81\\x2\\x95\\x1\\x75\\x5\\x81\\x3\\x5\\x1\\x9\\x30\\x9\\x31\\x9\\x38\\x15\\x81\\x25\\x7f\\x75\\x8\\x95\\x3\\x81\\x6\\xc0\\xc0 > functions/hid.GS1/report_desc - ln -s functions/hid.GS1 configs/c.1 +# shellcheck source=/kvmapp/system/init.d/S03usb-common +. "${USB_COMMON:-/kvmapp/system/init.d/S03usb-common}" - # touchpad - mkdir functions/hid.GS2 - echo 1 > functions/hid.GS2/subclass - if [ ! -e /boot/usb.notwakeup ] - then - echo 1 > functions/hid.GS2/wakeup_on_write - fi - echo 2 > functions/hid.GS2/protocol - echo 6 > functions/hid.GS2/report_length - echo -ne \\x05\\x01\\x09\\x02\\xa1\\x01\\x09\\x01\\xa1\\x00\\x05\\x09\\x19\\x01\\x29\\x03\\x15\\x00\\x25\\x01\\x95\\x03\\x75\\x01\\x81\\x02\\x95\\x01\\x75\\x05\\x81\\x01\\x05\\x01\\x09\\x30\\x09\\x31\\x15\\x00\\x26\\xff\\x7f\\x35\\x00\\x46\\xff\\x7f\\x75\\x10\\x95\\x02\\x81\\x02\\x05\\x01\\x09\\x38\\x15\\x81\\x25\\x7f\\x35\\x00\\x45\\x00\\x75\\x08\\x95\\x01\\x81\\x06\\xc0\\xc0 > functions/hid.GS2/report_desc - ln -s functions/hid.GS2 configs/c.1 - - ls /sys/class/udc/ | cat > UDC - echo device > /proc/cviusb/otg_role -} - -start_usb_host(){ - echo '' > /sys/kernel/config/usb_gadget/g0/UDC - echo host > /proc/cviusb/otg_role -} - -restart_usb_dev(){ - echo > /sys/kernel/config/usb_gadget/g0/UDC - sleep 1 - ls /sys/class/udc/ | cat > /sys/kernel/config/usb_gadget/g0/UDC - echo "USB Restart OK!" +start_usb_dev(){ + prepare_usb_gadget "${USB_HID_ONLY_BCD_USB}" "${USB_HID_ONLY_BCD_DEVICE}" "${USB_HID_ONLY_CONFIG_ATTRIBUTES}" "${USB_HID_ONLY_MAX_POWER}" + add_hid_functions "${USB_HID_REPORT_HID_ONLY_KEYBOARD}" "${USB_HID_REPORT_HID_ONLY_ABSOLUTE_MOUSE}" + bind_first_udc } -case "$1" in - start) - start_usb_dev - ;; - restart) - restart_usb_dev - ;; - stop) - start_usb_host - ;; - stop_start) - start_usb_host - start_usb_dev - ;; - restart_phy) - echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/unbind - sleep 1 - echo "4340000.usb" > /sys/bus/platform/drivers/dwc2/bind - start_usb_host - start_usb_dev - ;; -esac +run_usb_action "$1" diff --git a/server/service/hid/hid.go b/server/service/hid/hid.go index 60185ba8..6efd6ca6 100644 --- a/server/service/hid/hid.go +++ b/server/service/hid/hid.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "os" + "strings" "sync" "syscall" "time" @@ -193,6 +194,24 @@ func (h *Hid) closeDeviceNoLock(device hidDevice) { } } +func (h *Hid) closeDeletedDeviceNoLock(device hidDevice) { + file := device.get() + if file == nil || !hidFileWasDeleted(file) { + return + } + + log.Debugf("close %s because the cached HID handle was deleted", device.path) + h.closeDeviceNoLock(device) +} + +func hidFileWasDeleted(file *os.File) bool { + target, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", file.Fd())) + if err != nil { + return false + } + return strings.HasSuffix(target, " (deleted)") +} + // writeWithTimeout bounds how long callers hold HID locks when writing to a // nonblocking descriptor. EAGAIN means the host is not accepting HID reports // yet, so retry until the caller's deadline expires. @@ -230,6 +249,15 @@ func isRetryableWriteError(err error) bool { return errors.Is(err, syscall.EAGAIN) || errors.Is(err, syscall.EWOULDBLOCK) } +func isStaleHIDWriteError(err error) bool { + return errors.Is(err, os.ErrClosed) || + errors.Is(err, syscall.EIO) || + errors.Is(err, syscall.ENODEV) || + errors.Is(err, syscall.ENXIO) || + errors.Is(err, syscall.EPIPE) || + errors.Is(err, syscall.ESHUTDOWN) +} + func (h *Hid) Open() { h.kbMutex.Lock() defer h.kbMutex.Unlock() @@ -272,10 +300,31 @@ func (h *Hid) writeHID(device hidDevice, data []byte) error { device.mu.Lock() defer device.mu.Unlock() + h.closeDeletedDeviceNoLock(device) if err := h.openDeviceNoLock(device); err != nil { return err } + if err := h.writeHIDNoLock(device, data); err != nil { + h.closeDeviceNoLock(device) + if !isStaleHIDWriteError(err) { + return formatHIDWriteError(err) + } + + if reopenErr := h.openDeviceNoLock(device); reopenErr != nil { + return fmt.Errorf("reopen %s after stale HID write: %w", device.path, reopenErr) + } + if retryErr := h.writeHIDNoLock(device, data); retryErr != nil { + h.closeDeviceNoLock(device) + return formatHIDWriteError(retryErr) + } + } + + log.Debugf("write to %s: %v", device.path, data) + return nil +} + +func (h *Hid) writeHIDNoLock(device hidDevice, data []byte) error { file := device.get() if file == nil { return fmt.Errorf("%s: hid handle is nil", device.path) @@ -287,17 +336,19 @@ func (h *Hid) writeHID(device hidDevice, data []byte) error { } if err := writeWithTimeout(file, data, hidWriteTimeout); err != nil { - h.closeDeviceNoLock(device) - switch { - case errors.Is(err, os.ErrClosed): - return fmt.Errorf("hid already closed: %w", err) - case errors.Is(err, os.ErrDeadlineExceeded): - return fmt.Errorf("timeout after %s: %w", hidWriteTimeout, err) - default: - return err - } + return err } - log.Debugf("write to %s: %v", device.path, data) return nil } + +func formatHIDWriteError(err error) error { + switch { + case errors.Is(err, os.ErrClosed): + return fmt.Errorf("hid already closed: %w", err) + case errors.Is(err, os.ErrDeadlineExceeded): + return fmt.Errorf("timeout after %s: %w", hidWriteTimeout, err) + default: + return err + } +} diff --git a/server/service/hid/hid_test.go b/server/service/hid/hid_test.go new file mode 100644 index 00000000..064c95e2 --- /dev/null +++ b/server/service/hid/hid_test.go @@ -0,0 +1,147 @@ +package hid + +import ( + "errors" + "io" + "os" + "runtime" + "syscall" + "testing" + "time" +) + +type scriptedWriter struct { + writes []scriptedWrite +} + +type scriptedWrite struct { + n int + err error +} + +func (w *scriptedWriter) Write(_ []byte) (int, error) { + if len(w.writes) == 0 { + return 0, io.ErrUnexpectedEOF + } + write := w.writes[0] + w.writes = w.writes[1:] + return write.n, write.err +} + +func TestHidFileWasDeleted(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("deleted fd detection uses /proc/self/fd") + } + + file, err := os.CreateTemp(t.TempDir(), "hidg") + if err != nil { + t.Fatal(err) + } + if hidFileWasDeleted(file) { + t.Fatal("new file reported as deleted") + } + + if err := os.Remove(file.Name()); err != nil { + t.Fatal(err) + } + if !hidFileWasDeleted(file) { + t.Fatal("unlinked open file did not report as deleted") + } +} + +func TestIsStaleHIDWriteError(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + {name: "closed", err: os.ErrClosed, want: true}, + {name: "io", err: syscall.EIO, want: true}, + {name: "no device", err: syscall.ENODEV, want: true}, + {name: "no such device address", err: syscall.ENXIO, want: true}, + {name: "pipe", err: syscall.EPIPE, want: true}, + {name: "shutdown", err: syscall.ESHUTDOWN, want: true}, + {name: "wrapped", err: errors.Join(syscall.EIO), want: true}, + {name: "again", err: syscall.EAGAIN, want: false}, + {name: "deadline", err: os.ErrDeadlineExceeded, want: false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isStaleHIDWriteError(tt.err); got != tt.want { + t.Fatalf("isStaleHIDWriteError(%v) = %v, want %v", tt.err, got, tt.want) + } + }) + } +} + +func TestWriteWithTimeoutRetriesEAGAIN(t *testing.T) { + writer := &scriptedWriter{ + writes: []scriptedWrite{ + {err: syscall.EAGAIN}, + {n: 3}, + }, + } + + if err := writeWithTimeout(writer, []byte{1, 2, 3}, 20*time.Millisecond); err != nil { + t.Fatal(err) + } + if len(writer.writes) != 0 { + t.Fatalf("unused scripted writes: %d", len(writer.writes)) + } +} + +func TestWriteWithTimeoutRejectsShortWrite(t *testing.T) { + writer := &scriptedWriter{ + writes: []scriptedWrite{{n: 2}}, + } + + if !errors.Is(writeWithTimeout(writer, []byte{1, 2, 3}, 20*time.Millisecond), io.ErrShortWrite) { + t.Fatal("writeWithTimeout did not reject a short write") + } +} + +func TestWriteWithTimeoutExpiresAfterRetryableErrors(t *testing.T) { + writer := &scriptedWriter{ + writes: []scriptedWrite{ + {err: syscall.EAGAIN}, + {err: syscall.EAGAIN}, + {err: syscall.EAGAIN}, + }, + } + + if !errors.Is(writeWithTimeout(writer, []byte{1}, time.Millisecond), os.ErrDeadlineExceeded) { + t.Fatal("writeWithTimeout did not time out after retryable errors") + } +} + +func TestOpenNoLockWithRetryRetriesUntilSuccess(t *testing.T) { + attempts := 0 + err := openNoLockWithRetry(func() error { + attempts++ + if attempts < 3 { + return syscall.ENODEV + } + return nil + }, 100*time.Millisecond, time.Millisecond) + if err != nil { + t.Fatal(err) + } + if attempts != 3 { + t.Fatalf("attempts = %d, want 3", attempts) + } +} + +func TestOpenNoLockWithRetryReturnsLastError(t *testing.T) { + attempts := 0 + err := openNoLockWithRetry(func() error { + attempts++ + return syscall.ENODEV + }, time.Millisecond, time.Millisecond) + if !errors.Is(err, syscall.ENODEV) { + t.Fatalf("openNoLockWithRetry error = %v, want ENODEV", err) + } + if attempts == 0 { + t.Fatal("openNoLockWithRetry did not attempt to open") + } +} diff --git a/server/service/hid/status.go b/server/service/hid/status.go index 43cfce1b..da791923 100644 --- a/server/service/hid/status.go +++ b/server/service/hid/status.go @@ -1,23 +1,30 @@ package hid import ( - "NanoKVM-Server/proto" + "context" "errors" "fmt" "io" "os" "os/exec" + "path/filepath" "strings" "time" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" + + "NanoKVM-Server/proto" ) const ( - ModeNormal = "normal" - ModeHidOnly = "hid-only" - ModeFlag = "/sys/kernel/config/usb_gadget/g0/bcdDevice" + ModeNormal = "normal" + ModeHidOnly = "hid-only" + DefaultModeFlag = "/sys/kernel/config/usb_gadget/g0/bcdDevice" +) + +var ( + modeFlag = DefaultModeFlag ModeNormalScript = "/kvmapp/system/init.d/S03usbdev" ModeHidOnlyScript = "/kvmapp/system/init.d/S03usbhid" @@ -120,8 +127,10 @@ func ResetUSBPHY() error { h.CloseNoLock() defer h.Unlock() - command := fmt.Sprintf("%s restart_phy", USBDevScript) - if err := exec.Command("sh", "-c", command).Run(); err != nil { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := exec.CommandContext(ctx, "sh", USBDevScript, "restart_phy").Run(); err != nil { return fmt.Errorf("restart usb phy: %w", err) } @@ -150,7 +159,7 @@ func copyModeFile(srcScript string) error { } // create and copy to temporary file - tmpFile, err := os.CreateTemp("/etc/init.d/", ".S03usbdev-") + tmpFile, err := os.CreateTemp(filepath.Dir(USBDevScript), ".S03usbdev-") if err != nil { log.Errorf("failed to create temp %s: %s", USBDevScript, err) return err @@ -195,9 +204,9 @@ func copyModeFile(srcScript string) error { } func getHidMode() (string, error) { - data, err := os.ReadFile(ModeFlag) + data, err := os.ReadFile(modeFlag) if err != nil { - log.Errorf("failed to read %s: %s", ModeFlag, err) + log.Errorf("failed to read %s: %s", modeFlag, err) return "", err } diff --git a/server/service/hid/status_test.go b/server/service/hid/status_test.go new file mode 100644 index 00000000..9766d304 --- /dev/null +++ b/server/service/hid/status_test.go @@ -0,0 +1,146 @@ +package hid + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestGetHidMode(t *testing.T) { + tests := []struct { + name string + flag string + want string + }{ + {name: "normal", flag: "0x0510\n", want: ModeNormal}, + {name: "hid only", flag: "0x0623\n", want: ModeHidOnly}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + withModeFlag(t, tt.flag) + + got, err := getHidMode() + if err != nil { + t.Fatal(err) + } + if got != tt.want { + t.Fatalf("getHidMode() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestGetHidModeRejectsUnknownBcdDevice(t *testing.T) { + withModeFlag(t, "0x9999\n") + + if _, err := getHidMode(); err == nil { + t.Fatal("getHidMode succeeded for an unknown bcdDevice") + } +} + +func TestGetHidModeRequiresModeFlag(t *testing.T) { + oldModeFlag := modeFlag + t.Cleanup(func() { + modeFlag = oldModeFlag + }) + modeFlag = filepath.Join(t.TempDir(), "missing-bcdDevice") + + if _, err := getHidMode(); err == nil { + t.Fatal("getHidMode succeeded with a missing bcdDevice") + } +} + +func TestCopyModeFileCopiesScriptAtomically(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("script mode assertions are POSIX-specific") + } + + dir := t.TempDir() + src := filepath.Join(dir, "S03usbhid") + dst := filepath.Join(dir, "S03usbdev") + content := []byte("#!/bin/sh\nexit 0\n") + if err := os.WriteFile(src, content, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(dst, []byte("old"), 0o644); err != nil { + t.Fatal(err) + } + + oldUSBDevScript := USBDevScript + t.Cleanup(func() { + USBDevScript = oldUSBDevScript + }) + USBDevScript = dst + + if err := copyModeFile(src); err != nil { + t.Fatal(err) + } + + got, err := os.ReadFile(dst) + if err != nil { + t.Fatal(err) + } + if string(got) != string(content) { + t.Fatalf("target content = %q, want %q", got, content) + } + + info, err := os.Stat(dst) + if err != nil { + t.Fatal(err) + } + if info.Mode().Perm() != 0o755 { + t.Fatalf("target mode = %v, want 0755", info.Mode().Perm()) + } + + leftovers, err := filepath.Glob(filepath.Join(dir, ".S03usbdev-*")) + if err != nil { + t.Fatal(err) + } + if len(leftovers) != 0 { + t.Fatalf("temporary mode files left behind: %v", leftovers) + } +} + +func TestCopyModeFileFailureKeepsTarget(t *testing.T) { + dir := t.TempDir() + src := filepath.Join(dir, "missing") + dst := filepath.Join(dir, "S03usbdev") + oldContent := []byte("old script\n") + if err := os.WriteFile(dst, oldContent, 0o755); err != nil { + t.Fatal(err) + } + + oldUSBDevScript := USBDevScript + t.Cleanup(func() { + USBDevScript = oldUSBDevScript + }) + USBDevScript = dst + + if err := copyModeFile(src); err == nil { + t.Fatal("copyModeFile succeeded with a missing source") + } + + got, err := os.ReadFile(dst) + if err != nil { + t.Fatal(err) + } + if string(got) != string(oldContent) { + t.Fatalf("target changed after failed copy: got %q, want %q", got, oldContent) + } +} + +func withModeFlag(t *testing.T, content string) { + t.Helper() + + oldModeFlag := modeFlag + t.Cleanup(func() { + modeFlag = oldModeFlag + }) + + modeFlag = filepath.Join(t.TempDir(), "bcdDevice") + if err := os.WriteFile(modeFlag, []byte(content), 0o666); err != nil { + t.Fatal(err) + } +} diff --git a/support/sg2002/kvm_system/main/lib/system_state/system_state.cpp b/support/sg2002/kvm_system/main/lib/system_state/system_state.cpp index d98d5767..0d73dd95 100644 --- a/support/sg2002/kvm_system/main/lib/system_state/system_state.cpp +++ b/support/sg2002/kvm_system/main/lib/system_state/system_state.cpp @@ -10,6 +10,15 @@ using namespace maix::sys; extern kvm_sys_state_t kvm_sys_state; extern kvm_oled_state_t kvm_oled_state; +static const char *USB_HID0_LINK = "/sys/kernel/config/usb_gadget/g0/configs/c.1/hid.GS0"; +static const char *USB_HID1_LINK = "/sys/kernel/config/usb_gadget/g0/configs/c.1/hid.GS1"; +static const char *USB_HID2_LINK = "/sys/kernel/config/usb_gadget/g0/configs/c.1/hid.GS2"; + +static bool path_exists(const char *path) +{ + return access(path, F_OK) == 0; +} + int get_nic_state(const char* interface_name) { int sock; @@ -224,10 +233,12 @@ void kvm_update_usb_state() else kvm_sys_state.usb_state = -1; // hid_state & udisk_state (rndis_state单独处理) if(kvm_sys_state.usb_state == 1){ - if(access("/sys/kernel/config/usb_gadget/g0/configs/c.1/hid.GS*", F_OK) == 0) - kvm_sys_state.hid_state = 1; - if(access("/sys/kernel/config/usb_gadget/g0/configs/c.1/mass_storage.disk0", F_OK) == 0) - kvm_sys_state.udisk_state = 1; + kvm_sys_state.hid_state = + path_exists(USB_HID0_LINK) || + path_exists(USB_HID1_LINK) || + path_exists(USB_HID2_LINK); + kvm_sys_state.udisk_state = + path_exists("/sys/kernel/config/usb_gadget/g0/configs/c.1/mass_storage.disk0"); } else { kvm_sys_state.hid_state = 0; kvm_sys_state.udisk_state = 0; @@ -519,4 +530,4 @@ int check_watchdog() return -1; } } else return 0; -} \ No newline at end of file +} diff --git a/tests/usb-init-scripts-test.sh b/tests/usb-init-scripts-test.sh new file mode 100644 index 00000000..90fc953a --- /dev/null +++ b/tests/usb-init-scripts-test.sh @@ -0,0 +1,520 @@ +#!/bin/sh +set -eu + +ROOT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd) +COMMON_SCRIPT="${ROOT_DIR}/kvmapp/system/init.d/S03usb-common" +NORMAL_SCRIPT="${ROOT_DIR}/kvmapp/system/init.d/S03usbdev" +HID_ONLY_SCRIPT="${ROOT_DIR}/kvmapp/system/init.d/S03usbhid" +TMP_DIRS="" +TEST_COMMON_SCRIPT="" + +CONFIGFS_GADGET_ATTRS="UDC bcdUSB bcdDevice idVendor idProduct bDeviceClass bDeviceSubClass bDeviceProtocol" +CONFIGFS_STRING_ATTRS="serialnumber manufacturer product" +CONFIGFS_CONFIG_ATTRS="bmAttributes MaxPower" +CONFIGFS_CONFIG_STRING_ATTRS="configuration" +CONFIGFS_HID_ATTRS="subclass protocol report_length report_desc wakeup_on_write" +CONFIGFS_LUN_ATTRS="removable ro cdrom inquiry_string file" +CONFIGFS_OS_DESC_ATTRS="use b_vendor_code qw_sign" +CONFIGFS_RNDIS_ATTRS="class subclass protocol" +CONFIGFS_RNDIS_OS_DESC_ATTRS="compatible_id sub_compatible_id" +CONFIGFS_NCM_OS_DESC_ATTRS="compatible_id" + +cleanup(){ + for dir in ${TMP_DIRS} + do + rm -rf "${dir}" + done +} +trap cleanup EXIT + +fail(){ + echo "FAIL: $*" >&2 + exit 1 +} + +assert_eq(){ + [ "$1" = "$2" ] || fail "$3: got '$1', want '$2'" +} + +assert_no_file(){ + [ ! -e "$1" ] && [ ! -L "$1" ] || fail "unexpected file: $1" +} + +assert_link(){ + path="$1" + want="$2" + [ -e "${path}" ] || [ -L "${path}" ] || fail "missing file: ${path}" + got=$(readlink "${path}") + assert_eq "${got}" "${want}" "symlink ${path}" +} + +assert_hex(){ + assert_eq "$(od -An -tx1 -v "$1" | tr -d ' \n')" "$2" "hex $1" +} + +hex_of(){ + printf '%s' "$1" | od -An -tx1 -v | tr -d ' \n' +} + +assert_text_bytes(){ + assert_hex "$1" "$(hex_of "$2")" +} + +USB_DEFAULT_UDC="4340000.usb" +USB_PHY_DEVICE="4340000.usb" +USB_SECONDARY_UDC="4350000.usb" +USB_SERIAL_NUMBER="0123456789ABCDEF" +USB_MANUFACTURER="sipeed" +USB_PRODUCT="NanoKVM" +USB_CONFIGURATION="NanoKVM" +USB_NORMAL_BCD_USB="0x0200" +USB_NORMAL_BCD_DEVICE="0x0510" +USB_HID_ONLY_BCD_USB="0x0110" +USB_HID_ONLY_BCD_DEVICE="0x0623" +USB_HID_ONLY_CONFIG_ATTRIBUTES="0xA0" +USB_HID_KEYBOARD_FUNC="hid.GS0" +USB_HID_RELATIVE_MOUSE_FUNC="hid.GS1" +USB_HID_ABSOLUTE_MOUSE_FUNC="hid.GS2" +USB_MASS_STORAGE_FUNC="mass_storage.disk0" +USB_RNDIS_FUNC="rndis.usb0" +USB_NCM_FUNC="ncm.usb0" +USB_LEGACY_EMPTY_DISK_BACKING="/dev/mmcblk0p3" +USB_TEST_IMAGE="/data/install.iso" + +KEYBOARD_REPORT_DESC="05010906a101050719e029e71500250175019508810295017508810395057501050819012905910295017503910395067508150025e70507190029e78100c0" +HID_ONLY_KEYBOARD_REPORT_DESC="05010906a101050719e029e71500250175019508810295017508810395057501050819012905910295017503910395067508150025650507190029658100c0" +RELATIVE_MOUSE_REPORT_DESC="05010902a1010901a1000509190129031500250195037501810295017505810305010930093109381581257f750895038106c0c0" +ABSOLUTE_MOUSE_REPORT_DESC="05010902a1010901a10005091901290515002501950575018102950175038101050109300931150026ff7f350046ff7f751095028102050109381581257f35004500750895018106c0c0" +HID_ONLY_ABSOLUTE_MOUSE_REPORT_DESC="05010902a1010901a10005091901290315002501950375018102950175058101050109300931150026ff7f350046ff7f751095028102050109381581257f35004500750895018106c0c0" + +setup_fake_configfs_tools(){ + base=$(mktemp -d) + TMP_DIRS="${TMP_DIRS} ${base}" + fake_bin="${base}/bin" + mkdir -p "${fake_bin}" + + real_mkdir=$(command -v mkdir) + real_rmdir=$(command -v rmdir) + + cat > "${fake_bin}/mkdir" < "\${dir}/subclass" + touch_attrs "\${dir}" ${CONFIGFS_HID_ATTRS} + ;; + functions/rndis.*|*/functions/rndis.*) + "${real_mkdir}" -p "\${dir}/os_desc/interface.rndis" + touch_attrs "\${dir}" ${CONFIGFS_RNDIS_ATTRS} + touch_attrs "\${dir}/os_desc/interface.rndis" ${CONFIGFS_RNDIS_OS_DESC_ATTRS} + ;; + functions/ncm.*|*/functions/ncm.*) + "${real_mkdir}" -p "\${dir}/os_desc/interface.ncm" + touch_attrs "\${dir}/os_desc/interface.ncm" ${CONFIGFS_NCM_OS_DESC_ATTRS} + ;; + functions/mass_storage.*/lun.0|*/functions/mass_storage.*/lun.0) + touch_attrs "\${dir}" ${CONFIGFS_LUN_ATTRS} + ;; + functions/mass_storage.*|*/functions/mass_storage.*) + "${real_mkdir}" -p "\${dir}/lun.0" + prepare_fake_configfs_dir "\${dir}/lun.0" + ;; + esac +} +for arg in "\$@" +do + case "\${arg}" in + -*) ;; + *) [ -d "\${arg}" ] && prepare_fake_configfs_dir "\${arg}" ;; + esac +done +EOF + + cat > "${fake_bin}/rmdir" < "${TEST_COMMON_SCRIPT}" +} + +assert_hid_attr(){ + assert_eq "$(cat "$1/functions/$2/$3")" "$4" "$5" +} + +assert_hid_boot_interfaces(){ + g="$1" + label="$2" + assert_hid_attr "${g}" "${USB_HID_KEYBOARD_FUNC}" subclass 1 "${label} keyboard boot subclass" + assert_hid_attr "${g}" "${USB_HID_KEYBOARD_FUNC}" protocol 1 "${label} keyboard protocol" + assert_hid_attr "${g}" "${USB_HID_RELATIVE_MOUSE_FUNC}" subclass 1 "${label} mouse boot subclass" + assert_hid_attr "${g}" "${USB_HID_RELATIVE_MOUSE_FUNC}" protocol 2 "${label} mouse protocol" + assert_hid_attr "${g}" "${USB_HID_ABSOLUTE_MOUSE_FUNC}" subclass 0 "${label} touch non-boot subclass" + assert_hid_attr "${g}" "${USB_HID_ABSOLUTE_MOUSE_FUNC}" protocol 2 "${label} touch protocol" +} + +assert_hid_function(){ + assert_link "$1/configs/c.1/$2" "functions/$2" + assert_hid_attr "$1" "$2" subclass "$4" "$3 subclass" + assert_hid_attr "$1" "$2" protocol "$5" "$3 protocol" + assert_hid_attr "$1" "$2" report_length "$6" "$3 report length" + assert_hex "$1/functions/$2/report_desc" "$7" + assert_hid_attr "$1" "$2" wakeup_on_write "$8" "$3 wake" +} + +assert_hid_functions(){ + assert_hid_function "$1" "${USB_HID_KEYBOARD_FUNC}" "$2 keyboard" 1 1 8 "$3" "$5" + assert_hid_function "$1" "${USB_HID_RELATIVE_MOUSE_FUNC}" "$2 mouse" 1 2 4 "${RELATIVE_MOUSE_REPORT_DESC}" "$5" + assert_hid_function "$1" "${USB_HID_ABSOLUTE_MOUSE_FUNC}" "$2 touch" 0 2 6 "$4" "$5" +} + +assert_normal_composite_descriptors(){ + g="$1" + label="$2" + assert_eq "$(cat "${g}/bDeviceClass")" "0xEF" "${label} device class" + assert_eq "$(cat "${g}/bDeviceSubClass")" "0x02" "${label} device subclass" + assert_eq "$(cat "${g}/bDeviceProtocol")" "0x01" "${label} device protocol" +} + +assert_os_descriptors(){ + g="$1" + label="$2" + assert_link "${g}/os_desc/c.1" "configs/c.1" + assert_eq "$(cat "${g}/os_desc/use")" "1" "${label} OS descriptor enabled" + assert_eq "$(cat "${g}/os_desc/b_vendor_code")" "0xCD" "${label} vendor code" + assert_eq "$(cat "${g}/os_desc/qw_sign")" "MSFT100" "${label} OS descriptor signature" +} + +assert_rndis_function(){ + g="$1" + label="$2" + assert_link "${g}/configs/c.1/${USB_RNDIS_FUNC}" "functions/${USB_RNDIS_FUNC}" + assert_eq "$(cat "${g}/functions/${USB_RNDIS_FUNC}/class")" "e0" "${label} RNDIS class" + assert_eq "$(cat "${g}/functions/${USB_RNDIS_FUNC}/subclass")" "01" "${label} RNDIS subclass" + assert_eq "$(cat "${g}/functions/${USB_RNDIS_FUNC}/protocol")" "03" "${label} RNDIS protocol" + assert_eq "$(cat "${g}/functions/${USB_RNDIS_FUNC}/os_desc/interface.rndis/compatible_id")" "RNDIS" "${label} RNDIS compatible id" + assert_eq "$(cat "${g}/functions/${USB_RNDIS_FUNC}/os_desc/interface.rndis/sub_compatible_id")" "5162001" "${label} RNDIS sub-compatible id" +} + +assert_no_hid_functions(){ + g="$1" + for func in "${USB_HID_KEYBOARD_FUNC}" "${USB_HID_RELATIVE_MOUSE_FUNC}" "${USB_HID_ABSOLUTE_MOUSE_FUNC}" + do + assert_no_file "${g}/configs/c.1/${func}" + assert_no_file "${g}/functions/${func}" + done +} + +new_env(){ + base=$(mktemp -d) + TMP_DIRS="${TMP_DIRS} ${base}" + mkdir -p "${base}/boot" "${base}/dwc2" "${base}/gadget" "${base}/udc" "${base}/proc/cviusb" + touch "${base}/dwc2/bind" "${base}/dwc2/unbind" + touch "${base}/udc/${USB_DEFAULT_UDC}" + touch "${base}/proc/cviusb/otg_role" + echo "${base}" +} + +run_script_action(){ + script="$1" + action="$2" + base="$3" + USB_COMMON="${TEST_COMMON_SCRIPT}" \ + USB_BOOT_DIR="${base}/boot" \ + USB_GADGET_ROOT="${base}/gadget" \ + USB_UDC_CLASS="${base}/udc" \ + USB_OTG_ROLE="${base}/proc/cviusb/otg_role" \ + USB_PHY_DEVICE="${USB_PHY_DEVICE}" \ + USB_DWC2_BIND="${base}/dwc2/bind" \ + USB_DWC2_UNBIND="${base}/dwc2/unbind" \ + sh "${script}" "${action}" >/dev/null +} + +run_start(){ + script="$1" + base="$2" + run_script_action "${script}" start "${base}" +} + +test_normal_hid_descriptors(){ + base=$(new_env) + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_eq "$(cat "${g}/bcdUSB")" "${USB_NORMAL_BCD_USB}" "normal bcdUSB" + assert_eq "$(cat "${g}/bcdDevice")" "${USB_NORMAL_BCD_DEVICE}" "normal bcdDevice" + assert_normal_composite_descriptors "${g}" normal + assert_text_bytes "${g}/strings/0x409/manufacturer" "${USB_MANUFACTURER}" + assert_text_bytes "${g}/strings/0x409/product" "${USB_PRODUCT}" + assert_text_bytes "${g}/strings/0x409/serialnumber" "${USB_SERIAL_NUMBER}" + assert_text_bytes "${g}/configs/c.1/strings/0x409/configuration" "${USB_CONFIGURATION}" + + assert_hid_functions "${g}" normal "${KEYBOARD_REPORT_DESC}" "${ABSOLUTE_MOUSE_REPORT_DESC}" 1 + assert_eq "$(cat "${g}/UDC")" "${USB_DEFAULT_UDC}" "normal UDC" + assert_eq "$(cat "${base}/proc/cviusb/otg_role")" "device" "normal OTG role" +} + +test_normal_bios_flag_keeps_only_boot_hid_interfaces(){ + base=$(new_env) + touch "${base}/boot/BIOS" + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_hid_boot_interfaces "${g}" BIOS +} + +test_normal_disable_hid_removes_hid_functions(){ + base=$(new_env) + touch "${base}/boot/disable_hid" + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_no_hid_functions "${g}" +} + +test_normal_legacy_mass_storage(){ + base=$(new_env) + : > "${base}/boot/usb.disk0" + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_link "${g}/configs/c.1/${USB_MASS_STORAGE_FUNC}" "functions/${USB_MASS_STORAGE_FUNC}" + assert_eq "$(cat "${g}/functions/${USB_MASS_STORAGE_FUNC}/lun.0/removable")" "1" "mass storage removable" + assert_eq "$(cat "${g}/functions/${USB_MASS_STORAGE_FUNC}/lun.0/file")" "${USB_LEGACY_EMPTY_DISK_BACKING}" "legacy empty disk backing" +} + +test_normal_mounted_image_and_network(){ + base=$(new_env) + printf '%s' "${USB_TEST_IMAGE}" > "${base}/boot/usb.disk0" + touch "${base}/boot/usb.disk0.ro" + touch "${base}/boot/usb.rndis0" + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_normal_composite_descriptors "${g}" normal-network + assert_rndis_function "${g}" normal-network + assert_os_descriptors "${g}" normal-network + assert_eq "$(cat "${g}/functions/${USB_MASS_STORAGE_FUNC}/lun.0/file")" "${USB_TEST_IMAGE}" "mounted image" + assert_eq "$(cat "${g}/functions/${USB_MASS_STORAGE_FUNC}/lun.0/ro")" "1" "media ro flag" + assert_eq "$(cat "${g}/functions/${USB_MASS_STORAGE_FUNC}/lun.0/cdrom")" "0" "media cdrom flag" +} + +test_network_restart_removes_os_desc_link(){ + base=$(new_env) + touch "${base}/boot/usb.rndis0" + run_start "${NORMAL_SCRIPT}" "${base}" + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_os_descriptors "${g}" network-restart + assert_rndis_function "${g}" network-restart + assert_hid_functions "${g}" network-restart "${KEYBOARD_REPORT_DESC}" "${ABSOLUTE_MOUSE_REPORT_DESC}" 1 +} + +test_ncm_network_descriptors(){ + base=$(new_env) + touch "${base}/boot/usb.ncm" + touch "${base}/boot/usb.rndis0" + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_link "${g}/configs/c.1/${USB_NCM_FUNC}" "functions/${USB_NCM_FUNC}" + assert_no_file "${g}/configs/c.1/${USB_RNDIS_FUNC}" + assert_eq "$(cat "${g}/functions/${USB_NCM_FUNC}/os_desc/interface.ncm/compatible_id")" "WINNCM" "NCM compatible id" + assert_normal_composite_descriptors "${g}" ncm + assert_os_descriptors "${g}" ncm +} + +test_mode_switches_rebuild_gadget_contents(){ + base=$(new_env) + touch "${base}/boot/usb.disk0" + touch "${base}/boot/usb.rndis0" + + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + assert_eq "$(cat "${g}/bcdDevice")" "${USB_NORMAL_BCD_DEVICE}" "normal mode bcdDevice" + assert_link "${g}/configs/c.1/${USB_MASS_STORAGE_FUNC}" "functions/${USB_MASS_STORAGE_FUNC}" + assert_rndis_function "${g}" mode-switch-normal + assert_os_descriptors "${g}" mode-switch-normal + + run_start "${HID_ONLY_SCRIPT}" "${base}" + g="${base}/gadget/g0" + assert_eq "$(cat "${g}/bcdDevice")" "${USB_HID_ONLY_BCD_DEVICE}" "hid-only mode bcdDevice" + assert_eq "$(cat "${g}/bDeviceClass")" "" "hid-only device class" + assert_eq "$(cat "${g}/bDeviceSubClass")" "" "hid-only device subclass" + assert_eq "$(cat "${g}/bDeviceProtocol")" "" "hid-only device protocol" + assert_hid_functions "${g}" switched-hid-only "${HID_ONLY_KEYBOARD_REPORT_DESC}" "${HID_ONLY_ABSOLUTE_MOUSE_REPORT_DESC}" 1 + assert_no_file "${g}/configs/c.1/${USB_MASS_STORAGE_FUNC}" + assert_no_file "${g}/configs/c.1/${USB_RNDIS_FUNC}" + assert_no_file "${g}/os_desc/c.1" + + run_start "${NORMAL_SCRIPT}" "${base}" + g="${base}/gadget/g0" + assert_eq "$(cat "${g}/bcdDevice")" "${USB_NORMAL_BCD_DEVICE}" "switched-back normal bcdDevice" + assert_normal_composite_descriptors "${g}" switched-normal + assert_hid_functions "${g}" switched-normal "${KEYBOARD_REPORT_DESC}" "${ABSOLUTE_MOUSE_REPORT_DESC}" 1 + assert_link "${g}/configs/c.1/${USB_MASS_STORAGE_FUNC}" "functions/${USB_MASS_STORAGE_FUNC}" + assert_rndis_function "${g}" switched-normal + assert_os_descriptors "${g}" switched-normal +} + +test_hid_only_descriptors_and_no_wake(){ + base=$(new_env) + touch "${base}/boot/usb.notwakeup" + touch "${base}/boot/usb.disk0" + touch "${base}/boot/usb.rndis0" + run_start "${HID_ONLY_SCRIPT}" "${base}" + g="${base}/gadget/g0" + + assert_eq "$(cat "${g}/bcdUSB")" "${USB_HID_ONLY_BCD_USB}" "hid-only bcdUSB" + assert_eq "$(cat "${g}/bcdDevice")" "${USB_HID_ONLY_BCD_DEVICE}" "hid-only bcdDevice" + assert_eq "$(cat "${g}/configs/c.1/bmAttributes")" "${USB_HID_ONLY_CONFIG_ATTRIBUTES}" "hid-only bmAttributes" + assert_text_bytes "${g}/strings/0x409/serialnumber" "${USB_SERIAL_NUMBER}" + assert_text_bytes "${g}/strings/0x409/manufacturer" "${USB_MANUFACTURER}" + assert_text_bytes "${g}/strings/0x409/product" "${USB_PRODUCT}" + assert_text_bytes "${g}/configs/c.1/strings/0x409/configuration" "${USB_CONFIGURATION}" + assert_hid_functions "${g}" hid-only "${HID_ONLY_KEYBOARD_REPORT_DESC}" "${HID_ONLY_ABSOLUTE_MOUSE_REPORT_DESC}" "" + assert_no_file "${g}/configs/c.1/${USB_MASS_STORAGE_FUNC}" + assert_no_file "${g}/configs/c.1/${USB_RNDIS_FUNC}" +} + +test_uses_one_udc(){ + base=$(new_env) + touch "${base}/udc/${USB_SECONDARY_UDC}" + run_start "${NORMAL_SCRIPT}" "${base}" + + assert_eq "$(cat "${base}/gadget/g0/UDC")" "${USB_DEFAULT_UDC}" "first UDC selected" +} + +test_stop_unbinds_and_sets_host_role(){ + base=$(new_env) + run_start "${NORMAL_SCRIPT}" "${base}" + run_script_action "${NORMAL_SCRIPT}" stop "${base}" + + assert_eq "$(cat "${base}/gadget/g0/UDC")" "" "stopped UDC" + assert_eq "$(cat "${base}/proc/cviusb/otg_role")" "host" "stopped OTG role" +} + +test_stop_then_start_rebuilds_gadget(){ + base=$(new_env) + run_start "${NORMAL_SCRIPT}" "${base}" + run_script_action "${NORMAL_SCRIPT}" stop "${base}" + run_script_action "${NORMAL_SCRIPT}" start "${base}" + g="${base}/gadget/g0" + + assert_eq "$(cat "${g}/UDC")" "${USB_DEFAULT_UDC}" "stop-start UDC" + assert_eq "$(cat "${base}/proc/cviusb/otg_role")" "device" "stop-start OTG role" + assert_hid_functions "${g}" stop-start "${KEYBOARD_REPORT_DESC}" "${ABSOLUTE_MOUSE_REPORT_DESC}" 1 +} + +test_restart_phy_rebuilds_gadget(){ + base=$(new_env) + run_start "${NORMAL_SCRIPT}" "${base}" + run_script_action "${NORMAL_SCRIPT}" restart_phy "${base}" + g="${base}/gadget/g0" + + assert_eq "$(cat "${base}/dwc2/unbind")" "${USB_PHY_DEVICE}" "phy unbind" + assert_eq "$(cat "${base}/dwc2/bind")" "${USB_PHY_DEVICE}" "phy bind" + assert_eq "$(cat "${g}/UDC")" "${USB_DEFAULT_UDC}" "restart-phy UDC" + assert_eq "$(cat "${base}/proc/cviusb/otg_role")" "device" "restart-phy OTG role" + assert_hid_functions "${g}" restart-phy "${KEYBOARD_REPORT_DESC}" "${ABSOLUTE_MOUSE_REPORT_DESC}" 1 +} + +test_restart_rebinds_udc(){ + base=$(new_env) + run_start "${NORMAL_SCRIPT}" "${base}" + run_script_action "${NORMAL_SCRIPT}" restart "${base}" + + assert_eq "$(cat "${base}/gadget/g0/UDC")" "${USB_DEFAULT_UDC}" "restarted UDC" +} + +setup_fake_configfs_tools +test_normal_hid_descriptors +test_normal_bios_flag_keeps_only_boot_hid_interfaces +test_normal_disable_hid_removes_hid_functions +test_normal_legacy_mass_storage +test_normal_mounted_image_and_network +test_network_restart_removes_os_desc_link +test_ncm_network_descriptors +test_mode_switches_rebuild_gadget_contents +test_hid_only_descriptors_and_no_wake +test_uses_one_udc +test_stop_unbinds_and_sets_host_role +test_stop_then_start_rebuilds_gadget +test_restart_phy_rebuilds_gadget +test_restart_rebinds_udc + +echo "usb init script tests passed"