/* $Id: UsbTest.cpp $ */ /** @file * UsbTest - User frontend for the Linux usbtest USB test and benchmarking module. * Integrates with our test framework for nice outputs. */ /* * Copyright (C) 2014-2023 Oracle and/or its affiliates. * * This file is part of VirtualBox base platform packages, as * available from https://www.virtualbox.org. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation, in version 3 of the * License. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . * * The contents of this file may alternatively be used under the terms * of the Common Development and Distribution License Version 1.0 * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included * in the VirtualBox distribution, in which case the provisions of the * CDDL are applicable instead of those of the GPL. * * You may elect to license modified versions of this file under the * terms and conditions of either the GPL or the CDDL or both. * * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 */ /********************************************************************************************************************************* * Header Files * *********************************************************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /********************************************************************************************************************************* * Defined Constants And Macros * *********************************************************************************************************************************/ /********************************************************************************************************************************* * Structures and Typedefs * *********************************************************************************************************************************/ /** * USB test request data. * There is no public header with this information so we define it ourself here. */ typedef struct USBTESTPARMS { /** Specifies the test to run. */ uint32_t idxTest; /** How many iterations the test should be executed. */ uint32_t cIterations; /** Size of the data packets. */ uint32_t cbData; /** Size of */ uint32_t cbVariation; /** Length of the S/G list for the test. */ uint32_t cSgLength; /** Returned time data after completing the test. */ struct timeval TimeTest; } USBTESTPARAMS; /** Pointer to a test parameter structure. */ typedef USBTESTPARAMS *PUSBTESTPARAMS; /** * USB device descriptor. Used to search for the test device based * on the vendor and product id. */ #pragma pack(1) typedef struct USBDEVDESC { uint8_t bLength; uint8_t bDescriptorType; uint16_t bcdUSB; uint8_t bDeviceClass; uint8_t bDeviceSubClass; uint8_t bDeviceProtocol; uint8_t bMaxPacketSize0; uint16_t idVendor; uint16_t idProduct; uint16_t bcdDevice; uint8_t iManufacturer; uint8_t iProduct; uint8_t iSerialNumber; uint8_t bNumConfigurations; } USBDEVDESC; #pragma pack() #define USBTEST_REQUEST _IOWR('U', 100, USBTESTPARMS) /** * Callback to set up the test parameters for a specific test. * * @returns IPRT status code. * @retval VINF_SUCCESS if setting the parameters up succeeded. Any other error code * otherwise indicating the kind of error. * @param idxTest The test index. * @param pszTest Test name. * @param pParams The USB test parameters to set up. */ typedef DECLCALLBACKTYPE(int, FNUSBTESTPARAMSSETUP,(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams)); /** Pointer to a USB test parameters setup callback. */ typedef FNUSBTESTPARAMSSETUP *PFNUSBTESTPARAMSSETUP; /** * USB test descriptor. */ typedef struct USBTESTDESC { /** (Sort of) Descriptive test name. */ const char *pszName; /** Flag whether the test is excluded. */ bool fExcluded; /** The parameter setup callback. */ PFNUSBTESTPARAMSSETUP pfnParamsSetup; } USBTESTDESC; /** Pointer a USB test descriptor. */ typedef USBTESTDESC *PUSBTESTDESC; /** * USB speed values. */ typedef enum USBTESTSPEED { USBTESTSPEED_ANY = 0, USBTESTSPEED_UNKNOWN, USBTESTSPEED_LOW, USBTESTSPEED_FULL, USBTESTSPEED_HIGH, USBTESTSPEED_SUPER } USBTESTSPEED; /********************************************************************************************************************************* * Global Variables * *********************************************************************************************************************************/ /** Some forward method declarations. */ static DECLCALLBACK(int) usbTestParamsSetupReadWrite(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams); static DECLCALLBACK(int) usbTestParamsSetupControlWrites(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams); /** Command line parameters */ static const RTGETOPTDEF g_aCmdOptions[] = { {"--device", 'd', RTGETOPT_REQ_STRING }, {"--help", 'h', RTGETOPT_REQ_NOTHING}, {"--exclude", 'e', RTGETOPT_REQ_UINT32}, {"--exclude-all", 'a', RTGETOPT_REQ_NOTHING}, {"--include", 'i', RTGETOPT_REQ_UINT32}, {"--expected-speed", 's', RTGETOPT_REQ_STRING } }; static USBTESTDESC g_aTests[] = { /* pszTest fExcluded pfnParamsSetup */ {"NOP", false, usbTestParamsSetupReadWrite}, {"Non-queued Bulk write", false, usbTestParamsSetupReadWrite}, {"Non-queued Bulk read", false, usbTestParamsSetupReadWrite}, {"Non-queued Bulk write variabe size", false, usbTestParamsSetupReadWrite}, {"Non-queued Bulk read variabe size", false, usbTestParamsSetupReadWrite}, {"Queued Bulk write", false, usbTestParamsSetupReadWrite}, {"Queued Bulk read", false, usbTestParamsSetupReadWrite}, {"Queued Bulk write variabe size", false, usbTestParamsSetupReadWrite}, {"Queued Bulk read variabe size", false, usbTestParamsSetupReadWrite}, {"Chapter 9 Control Test", false, usbTestParamsSetupReadWrite}, {"Queued control messaging", false, usbTestParamsSetupReadWrite}, {"Unlink reads", false, usbTestParamsSetupReadWrite}, {"Unlink writes", false, usbTestParamsSetupReadWrite}, {"Set/Clear halts", false, usbTestParamsSetupReadWrite}, {"Control writes", false, usbTestParamsSetupControlWrites}, {"Isochronous write", false, usbTestParamsSetupReadWrite}, {"Isochronous read", false, usbTestParamsSetupReadWrite}, {"Bulk write unaligned (DMA)", false, usbTestParamsSetupReadWrite}, {"Bulk read unaligned (DMA)", false, usbTestParamsSetupReadWrite}, {"Bulk write unaligned (no DMA)", false, usbTestParamsSetupReadWrite}, {"Bulk read unaligned (no DMA)", false, usbTestParamsSetupReadWrite}, {"Control writes unaligned", false, usbTestParamsSetupControlWrites}, {"Isochronous write unaligned", false, usbTestParamsSetupReadWrite}, {"Isochronous read unaligned", false, usbTestParamsSetupReadWrite}, {"Unlink queued Bulk", false, usbTestParamsSetupReadWrite} }; /** The test handle. */ static RTTEST g_hTest; /** The expected device speed. */ static USBTESTSPEED g_enmSpeed = USBTESTSPEED_ANY; /** * Setup callback for basic read/write (bulk, isochronous) tests. * * @copydoc FNUSBTESTPARAMSSETUP */ static DECLCALLBACK(int) usbTestParamsSetupReadWrite(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams) { NOREF(idxTest); NOREF(pszTest); pParams->cIterations = 1000; pParams->cbData = 512; pParams->cbVariation = 512; pParams->cSgLength = 32; return VINF_SUCCESS; } /** * Setup callback for the control writes test. * * @copydoc FNUSBTESTPARAMSSETUP */ static DECLCALLBACK(int) usbTestParamsSetupControlWrites(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams) { NOREF(idxTest); NOREF(pszTest); pParams->cIterations = 1000; pParams->cbData = 512; /* * Must be smaller than cbData or the parameter check in the usbtest module fails, * no idea yet why it must be this. */ pParams->cbVariation = 256; pParams->cSgLength = 32; return VINF_SUCCESS; } /** * Shows tool usage text. */ static void usbTestUsage(PRTSTREAM pStrm) { char szExec[RTPATH_MAX]; RTStrmPrintf(pStrm, "usage: %s [options]\n", RTPathFilename(RTProcGetExecutablePath(szExec, sizeof(szExec)))); RTStrmPrintf(pStrm, "\n"); RTStrmPrintf(pStrm, "options: \n"); for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) { const char *pszHelp; switch (g_aCmdOptions[i].iShort) { case 'h': pszHelp = "Displays this help and exit"; break; case 'd': pszHelp = "Use the specified test device"; break; case 'e': pszHelp = "Exclude the given test id from the list"; break; case 'a': pszHelp = "Exclude all tests from the list (useful to enable single tests later with --include)"; break; case 'i': pszHelp = "Include the given test id in the list"; break; case 's': pszHelp = "The device speed to expect"; break; default: pszHelp = "Option undocumented"; break; } char szOpt[256]; RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); RTStrmPrintf(pStrm, " %-30s%s\n", szOpt, pszHelp); } } /** * Searches for a USB test device and returns the bus and device ID and the device speed. */ static int usbTestDeviceQueryBusAndDevId(uint16_t *pu16BusId, uint16_t *pu16DevId, USBTESTSPEED *penmSpeed) { bool fFound = false; #define USBTEST_USB_DEV_SYSFS "/sys/bus/usb/devices/" RTDIR hDirUsb = NULL; int rc = RTDirOpen(&hDirUsb, USBTEST_USB_DEV_SYSFS); if (RT_SUCCESS(rc)) { do { RTDIRENTRY DirUsbBus; rc = RTDirRead(hDirUsb, &DirUsbBus, NULL); if ( RT_SUCCESS(rc) && RTStrNCmp(DirUsbBus.szName, "usb", 3) && RTLinuxSysFsExists(USBTEST_USB_DEV_SYSFS "%s/idVendor", DirUsbBus.szName)) { int64_t idVendor = 0; int64_t idProduct = 0; int64_t iBusId = 0; int64_t iDevId = 0; char aszSpeed[20]; rc = RTLinuxSysFsReadIntFile(16, &idVendor, USBTEST_USB_DEV_SYSFS "%s/idVendor", DirUsbBus.szName); if (RT_SUCCESS(rc)) rc = RTLinuxSysFsReadIntFile(16, &idProduct, USBTEST_USB_DEV_SYSFS "%s/idProduct", DirUsbBus.szName); if (RT_SUCCESS(rc)) rc = RTLinuxSysFsReadIntFile(16, &iBusId, USBTEST_USB_DEV_SYSFS "%s/busnum", DirUsbBus.szName); if (RT_SUCCESS(rc)) rc = RTLinuxSysFsReadIntFile(16, &iDevId, USBTEST_USB_DEV_SYSFS "%s/devnum", DirUsbBus.szName); if (RT_SUCCESS(rc)) rc = RTLinuxSysFsReadStrFile(&aszSpeed[0], sizeof(aszSpeed), NULL, USBTEST_USB_DEV_SYSFS "%s/speed", DirUsbBus.szName); if ( RT_SUCCESS(rc) && idVendor == 0x0525 && idProduct == 0xa4a0) { if (penmSpeed) { /* Parse the speed. */ if (!RTStrCmp(&aszSpeed[0], "1.5")) *penmSpeed = USBTESTSPEED_LOW; else if (!RTStrCmp(&aszSpeed[0], "12")) *penmSpeed = USBTESTSPEED_FULL; else if (!RTStrCmp(&aszSpeed[0], "480")) *penmSpeed = USBTESTSPEED_HIGH; else if ( !RTStrCmp(&aszSpeed[0], "5000") || !RTStrCmp(&aszSpeed[0], "10000")) *penmSpeed = USBTESTSPEED_SUPER; else *penmSpeed = USBTESTSPEED_UNKNOWN; } if (pu16BusId) *pu16BusId = (uint16_t)iBusId; if (pu16DevId) *pu16DevId = (uint16_t)iDevId; fFound = true; break; } } else if (rc != VERR_NO_MORE_FILES) rc = VINF_SUCCESS; } while ( RT_SUCCESS(rc) && !fFound); if (rc == VERR_NO_MORE_FILES) rc = VINF_SUCCESS; RTDirClose(hDirUsb); } if (RT_SUCCESS(rc) && !fFound) rc = VERR_NOT_FOUND; return rc; } /** * Search for a USB test device and return the device path. * * @returns Path to the USB test device or NULL if none was found. */ static char *usbTestFindDevice(void) { /* * Very crude and quick way to search for the correct test device. * Assumption is that the path looks like /dev/bus/usb/%3d/%3d. */ char *pszDevPath = NULL; RTDIR hDirUsb = NULL; int rc = RTDirOpen(&hDirUsb, "/dev/bus/usb"); if (RT_SUCCESS(rc)) { do { RTDIRENTRY DirUsbBus; rc = RTDirRead(hDirUsb, &DirUsbBus, NULL); if (RT_SUCCESS(rc)) { char aszPath[RTPATH_MAX + 1]; RTStrPrintf(&aszPath[0], RT_ELEMENTS(aszPath), "/dev/bus/usb/%s", DirUsbBus.szName); RTDIR hDirUsbBus = NULL; rc = RTDirOpen(&hDirUsbBus, &aszPath[0]); if (RT_SUCCESS(rc)) { do { RTDIRENTRY DirUsbDev; rc = RTDirRead(hDirUsbBus, &DirUsbDev, NULL); if (RT_SUCCESS(rc)) { char aszPathDev[RTPATH_MAX + 1]; RTStrPrintf(&aszPathDev[0], RT_ELEMENTS(aszPathDev), "/dev/bus/usb/%s/%s", DirUsbBus.szName, DirUsbDev.szName); RTFILE hFileDev; rc = RTFileOpen(&hFileDev, aszPathDev, RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_NONE); if (RT_SUCCESS(rc)) { USBDEVDESC DevDesc; rc = RTFileRead(hFileDev, &DevDesc, sizeof(DevDesc), NULL); RTFileClose(hFileDev); if ( RT_SUCCESS(rc) && DevDesc.idVendor == 0x0525 && DevDesc.idProduct == 0xa4a0) pszDevPath = RTStrDup(aszPathDev); } rc = VINF_SUCCESS; } else if (rc != VERR_NO_MORE_FILES) rc = VINF_SUCCESS; } while ( RT_SUCCESS(rc) && !pszDevPath); rc = VINF_SUCCESS; RTDirClose(hDirUsbBus); } } else if (rc != VERR_NO_MORE_FILES) rc = VINF_SUCCESS; } while ( RT_SUCCESS(rc) && !pszDevPath); RTDirClose(hDirUsb); } return pszDevPath; } static int usbTestIoctl(int iDevFd, int iInterface, PUSBTESTPARAMS pParams) { struct usbdevfs_ioctl IoCtlData; IoCtlData.ifno = iInterface; IoCtlData.ioctl_code = (int)USBTEST_REQUEST; IoCtlData.data = pParams; return ioctl(iDevFd, USBDEVFS_IOCTL, &IoCtlData); } /** * Test execution worker. * * @param pszDevice The device to use for testing. */ static void usbTestExec(const char *pszDevice) { int iDevFd; RTTestSub(g_hTest, "Opening device"); iDevFd = open(pszDevice, O_RDWR); if (iDevFd != -1) { USBTESTPARAMS Params; RTTestPassed(g_hTest, "Opening device successful\n"); for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) { RTTestSub(g_hTest, g_aTests[i].pszName); if (g_aTests[i].fExcluded) { RTTestSkipped(g_hTest, "Excluded from list"); continue; } int rc = g_aTests[i].pfnParamsSetup(i, g_aTests[i].pszName, &Params); if (RT_SUCCESS(rc)) { Params.idxTest = i; /* Assume the test interface has the number 0 for now. */ int rcPosix = usbTestIoctl(iDevFd, 0, &Params); if (rcPosix < 0 && errno == EOPNOTSUPP) { RTTestSkipped(g_hTest, "Not supported"); continue; } if (rcPosix < 0) { /* * The error status code of the unlink testcase is * offset by 2000 for the sync and 1000 for the sync code path * (see drivers/usb/misc/usbtest.c in the Linux kernel sources). * * Adjust to the actual status code so converting doesn't assert. */ int iTmpErrno = errno; if (iTmpErrno >= 2000) iTmpErrno -= 2000; else if (iTmpErrno >= 1000) iTmpErrno -= 1000; RTTestFailed(g_hTest, "Test failed with %Rrc\n", RTErrConvertFromErrno(iTmpErrno)); } else { uint64_t u64Ns = Params.TimeTest.tv_sec * RT_NS_1SEC + Params.TimeTest.tv_usec * RT_NS_1US; RTTestValue(g_hTest, "Runtime", u64Ns, RTTESTUNIT_NS); } } else RTTestFailed(g_hTest, "Setting up test parameters failed with %Rrc\n", rc); RTTestSubDone(g_hTest); } close(iDevFd); } else RTTestFailed(g_hTest, "Opening device failed with %Rrc\n", RTErrConvertFromErrno(errno)); } int main(int argc, char *argv[]) { /* * Init IPRT and globals. */ int rc = RTTestInitAndCreate("UsbTest", &g_hTest); if (rc) return rc; /* * Default values. */ const char *pszDevice = NULL; RTGETOPTUNION ValueUnion; RTGETOPTSTATE GetState; RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); while ((rc = RTGetOpt(&GetState, &ValueUnion))) { switch (rc) { case 'h': usbTestUsage(g_pStdOut); return RTEXITCODE_SUCCESS; case 'd': pszDevice = ValueUnion.psz; break; case 's': if (!RTStrICmp(ValueUnion.psz, "Low")) g_enmSpeed = USBTESTSPEED_LOW; else if (!RTStrICmp(ValueUnion.psz, "Full")) g_enmSpeed = USBTESTSPEED_FULL; else if (!RTStrICmp(ValueUnion.psz, "High")) g_enmSpeed = USBTESTSPEED_HIGH; else if (!RTStrICmp(ValueUnion.psz, "Super")) g_enmSpeed = USBTESTSPEED_SUPER; else { RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid speed passed to --expected-speed\n"); RTTestErrorInc(g_hTest); return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); } break; case 'e': if (ValueUnion.u32 < RT_ELEMENTS(g_aTests)) g_aTests[ValueUnion.u32].fExcluded = true; else { RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid test number passed to --exclude\n"); RTTestErrorInc(g_hTest); return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); } break; case 'a': for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) g_aTests[i].fExcluded = true; break; case 'i': if (ValueUnion.u32 < RT_ELEMENTS(g_aTests)) g_aTests[ValueUnion.u32].fExcluded = false; else { RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid test number passed to --include\n"); RTTestErrorInc(g_hTest); return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); } break; default: return RTGetOptPrintError(rc, &ValueUnion); } } /* * Start testing. */ RTTestBanner(g_hTest); /* Find the first test device if none was given. */ if (!pszDevice) { RTTestSub(g_hTest, "Detecting device"); pszDevice = usbTestFindDevice(); if (!pszDevice) RTTestFailed(g_hTest, "Failed to find suitable device\n"); RTTestSubDone(g_hTest); } if (pszDevice) { /* First check that the requested speed matches. */ if (g_enmSpeed != USBTESTSPEED_ANY) { RTTestSub(g_hTest, "Checking correct device speed"); USBTESTSPEED enmSpeed = USBTESTSPEED_UNKNOWN; rc = usbTestDeviceQueryBusAndDevId(NULL, NULL, &enmSpeed); if (RT_SUCCESS(rc)) { if (enmSpeed == g_enmSpeed) RTTestPassed(g_hTest, "Reported device speed matches requested speed\n"); else RTTestFailed(g_hTest, "Reported device speed doesn'match requested speed (%u vs %u)\n", enmSpeed, g_enmSpeed); } else RTTestFailed(g_hTest, "Failed to query device speed with rc=%Rrc\n", rc); RTTestSubDone(g_hTest); } usbTestExec(pszDevice); } RTEXITCODE rcExit = RTTestSummaryAndDestroy(g_hTest); return rcExit; }