From f215e02bf85f68d3a6106c2a1f4f7f063f819064 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:17:27 +0200 Subject: Adding upstream version 7.0.14-dfsg. Signed-off-by: Daniel Baumann --- .../Main/src-server/linux/HostDnsServiceLinux.cpp | 476 ++++++ .../Main/src-server/linux/HostHardwareLinux.cpp | 1331 +++++++++++++++ src/VBox/Main/src-server/linux/HostPowerLinux.cpp | 198 +++ src/VBox/Main/src-server/linux/Makefile.kup | 0 src/VBox/Main/src-server/linux/NetIf-linux.cpp | 329 ++++ .../Main/src-server/linux/PerformanceLinux.cpp | 609 +++++++ src/VBox/Main/src-server/linux/USBGetDevices.cpp | 1795 ++++++++++++++++++++ .../Main/src-server/linux/USBProxyBackendLinux.cpp | 399 +++++ src/VBox/Main/src-server/linux/vbox-libhal.cpp | 105 ++ 9 files changed, 5242 insertions(+) create mode 100644 src/VBox/Main/src-server/linux/HostDnsServiceLinux.cpp create mode 100644 src/VBox/Main/src-server/linux/HostHardwareLinux.cpp create mode 100644 src/VBox/Main/src-server/linux/HostPowerLinux.cpp create mode 100644 src/VBox/Main/src-server/linux/Makefile.kup create mode 100644 src/VBox/Main/src-server/linux/NetIf-linux.cpp create mode 100644 src/VBox/Main/src-server/linux/PerformanceLinux.cpp create mode 100644 src/VBox/Main/src-server/linux/USBGetDevices.cpp create mode 100644 src/VBox/Main/src-server/linux/USBProxyBackendLinux.cpp create mode 100644 src/VBox/Main/src-server/linux/vbox-libhal.cpp (limited to 'src/VBox/Main/src-server/linux') diff --git a/src/VBox/Main/src-server/linux/HostDnsServiceLinux.cpp b/src/VBox/Main/src-server/linux/HostDnsServiceLinux.cpp new file mode 100644 index 00000000..ed80412e --- /dev/null +++ b/src/VBox/Main/src-server/linux/HostDnsServiceLinux.cpp @@ -0,0 +1,476 @@ +/* $Id: HostDnsServiceLinux.cpp $ */ +/** @file + * Linux specific DNS information fetching. + */ + +/* + * Copyright (C) 2013-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_MAIN_HOST +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +/* Workaround for defining __flexarr to [] which beats us in + * struct inotify_event (char name __flexarr). */ +#include +#undef __flexarr +#define __flexarr [0] +#include +#include +#include +#include + +#include "../HostDnsService.h" + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +static const char g_szEtcFolder[] = "/etc"; +static const char g_szResolvConfPath[] = "/etc/resolv.conf"; +static const char g_szResolvConfFilename[] = "resolv.conf"; + + +HostDnsServiceLinux::~HostDnsServiceLinux() +{ + if (m_fdShutdown >= 0) + { + close(m_fdShutdown); + m_fdShutdown = -1; + } +} + +HRESULT HostDnsServiceLinux::init(HostDnsMonitorProxy *pProxy) +{ + return HostDnsServiceResolvConf::init(pProxy, "/etc/resolv.conf"); +} + +int HostDnsServiceLinux::monitorThreadShutdown(RTMSINTERVAL uTimeoutMs) +{ + RT_NOREF(uTimeoutMs); + + if (m_fdShutdown >= 0) + send(m_fdShutdown, "", 1, MSG_NOSIGNAL); + + return VINF_SUCCESS; +} + +/** + * Format the notifcation event mask into a buffer for logging purposes. + */ +static const char *InotifyMaskToStr(char *psz, size_t cb, uint32_t fMask) +{ + static struct { const char *pszName; uint32_t cchName, fFlag; } const s_aFlags[] = + { +# define ENTRY(fFlag) { #fFlag, sizeof(#fFlag) - 1, fFlag } + ENTRY(IN_ACCESS), + ENTRY(IN_MODIFY), + ENTRY(IN_ATTRIB), + ENTRY(IN_CLOSE_WRITE), + ENTRY(IN_CLOSE_NOWRITE), + ENTRY(IN_OPEN), + ENTRY(IN_MOVED_FROM), + ENTRY(IN_MOVED_TO), + ENTRY(IN_CREATE), + ENTRY(IN_DELETE), + ENTRY(IN_DELETE_SELF), + ENTRY(IN_MOVE_SELF), + ENTRY(IN_Q_OVERFLOW), + ENTRY(IN_IGNORED), + ENTRY(IN_UNMOUNT), + ENTRY(IN_ISDIR), + }; + size_t offDst = 0; + for (size_t i = 0; i < RT_ELEMENTS(s_aFlags); i++) + if (fMask & s_aFlags[i].fFlag) + { + if (offDst && offDst < cb) + psz[offDst++] = ' '; + if (offDst < cb) + { + size_t cbToCopy = RT_MIN(s_aFlags[i].cchName, cb - offDst); + memcpy(&psz[offDst], s_aFlags[i].pszName, cbToCopy); + offDst += cbToCopy; + } + + fMask &= ~s_aFlags[i].fFlag; + if (!fMask) + break; + } + if (fMask && offDst < cb) + RTStrPrintf(&psz[offDst], cb - offDst, offDst ? " %#x" : "%#x", fMask); + else + psz[RT_MIN(offDst, cb - 1)] = '\0'; + return psz; +} + +/** + * Helper for HostDnsServiceLinux::monitorThreadProc. + */ +static int monitorSymlinkedDir(int iInotifyFd, char szRealResolvConf[PATH_MAX], size_t *poffFilename) +{ + RT_BZERO(szRealResolvConf, PATH_MAX); + + /* Check that it's a symlink first. */ + struct stat st; + if ( lstat(g_szResolvConfPath, &st) >= 0 + && S_ISLNK(st.st_mode)) + { + /* If realpath fails, the file must've been deleted while we were busy: */ + if ( realpath(g_szResolvConfPath, szRealResolvConf) + && strchr(szRealResolvConf, '/')) + { + /* Cut of the filename part. We only need that for deletion checks and such. */ + size_t const offFilename = strrchr(szRealResolvConf, '/') - &szRealResolvConf[0]; + *poffFilename = offFilename + 1; + szRealResolvConf[offFilename] = '\0'; + + /* Try set up directory monitoring. (File monitoring is done via the symlink.) */ + return inotify_add_watch(iInotifyFd, szRealResolvConf, IN_MOVE | IN_CREATE | IN_DELETE); + } + } + + *poffFilename = 0; + szRealResolvConf[0] = '\0'; + return -1; +} + +/** @todo If this code is needed elsewhere, we should abstract it into an IPRT + * thingy that monitors a file (path) for changes. This code is a little + * bit too complex to be duplicated. */ +int HostDnsServiceLinux::monitorThreadProc(void) +{ + /* + * Create a socket pair for signalling shutdown (see monitorThreadShutdown). + * ASSUME Linux 2.6.27 or later and that we can use SOCK_CLOEXEC. + */ + int aiStopPair[2]; + int iRc = socketpair(AF_LOCAL, SOCK_DGRAM | SOCK_CLOEXEC, 0, aiStopPair); + int iErr = errno; + AssertLogRelMsgReturn(iRc == 0, ("socketpair: failed (%d: %s)\n", iErr, strerror(iErr)), RTErrConvertFromErrno(iErr)); + + m_fdShutdown = aiStopPair[0]; + + onMonitorThreadInitDone(); + + /* + * inotify initialization (using inotify_init1 w/ IN_CLOEXEC introduced + * in 2.6.27 shouldn't be a problem any more). + * + * Note! Ignoring failures here is safe, because poll will ignore entires + * with negative fd values. + */ + int const iNotifyFd = inotify_init1(IN_CLOEXEC); + if (iNotifyFd < 0) + LogRel(("HostDnsServiceLinux::monitorThreadProc: Warning! inotify_init failed (errno=%d)\n", errno)); + + /* Monitor the /etc directory so we can detect moves, creating and unlinking + involving /etc/resolv.conf: */ + int const iWdDir = inotify_add_watch(iNotifyFd, g_szEtcFolder, IN_MOVE | IN_CREATE | IN_DELETE); + + /* In case g_szResolvConfPath is a symbolic link, monitor the target directory + too for changes to what it links to (kept up to date via iWdDir). */ + char szRealResolvConf[PATH_MAX]; + size_t offRealResolvConfName = 0; + int iWdSymDir = ::monitorSymlinkedDir(iNotifyFd, szRealResolvConf, &offRealResolvConfName); + + /* Monitor the resolv.conf itself if it exists, following all symlinks. */ + int iWdFile = inotify_add_watch(iNotifyFd, g_szResolvConfPath, IN_CLOSE_WRITE | IN_DELETE_SELF); + + LogRel5(("HostDnsServiceLinux::monitorThreadProc: inotify: %d - iWdDir=%d iWdSymDir=%d iWdFile=%d\n", + iNotifyFd, iWdDir, iWdSymDir, iWdFile)); + + /* + * poll initialization: + */ + pollfd aFdPolls[2]; + RT_ZERO(aFdPolls); + + aFdPolls[0].fd = iNotifyFd; + aFdPolls[0].events = POLLIN; + + aFdPolls[1].fd = aiStopPair[1]; + aFdPolls[1].events = POLLIN; + + /* + * The monitoring loop. + */ + int vrcRet = VINF_SUCCESS; + for (;;) + { + /* + * Wait for something to happen. + */ + iRc = poll(aFdPolls, RT_ELEMENTS(aFdPolls), -1 /*infinite timeout*/); + if (iRc == -1) + { + if (errno != EINTR) + { + LogRelMax(32, ("HostDnsServiceLinux::monitorThreadProc: poll failed %d: errno=%d\n", iRc, errno)); + RTThreadSleep(1); + } + continue; + } + Log5Func(("poll returns %d: [0]=%#x [1]=%#x\n", iRc, aFdPolls[1].revents, aFdPolls[0].revents)); + + AssertMsgBreakStmt( (aFdPolls[0].revents & (POLLERR | POLLNVAL)) == 0 /* (ok for fd=-1 too, revents=0 then) */ + && (aFdPolls[1].revents & (POLLERR | POLLNVAL)) == 0, + ("Debug Me: [0]=%d,%#x [1]=%d, %#x\n", + aFdPolls[0].fd, aFdPolls[0].revents, aFdPolls[0].fd, aFdPolls[1].revents), + vrcRet = VERR_INTERNAL_ERROR); + + /* + * Check for shutdown first. + */ + if (aFdPolls[1].revents & POLLIN) + break; /** @todo should probably drain aiStopPair[1] here if we're really paranoid. + * we'll be closing our end of the socket/pipe, so any stuck write + * should return too (ECONNRESET, ENOTCONN or EPIPE). */ + + if (aFdPolls[0].revents & POLLIN) + { + /* + * Read the notification event. + */ +#define INOTIFY_EVENT_SIZE (RT_UOFFSETOF(struct inotify_event, name)) + union + { + uint8_t abBuf[(INOTIFY_EVENT_SIZE * 2 - 1 + NAME_MAX) / INOTIFY_EVENT_SIZE * INOTIFY_EVENT_SIZE * 4]; + uint64_t uAlignTrick[2]; + } uEvtBuf; + + ssize_t cbEvents = read(iNotifyFd, &uEvtBuf, sizeof(uEvtBuf)); + Log5Func(("read(inotify) -> %zd\n", cbEvents)); + if (cbEvents > 0) + Log5(("%.*Rhxd\n", cbEvents, &uEvtBuf)); + + /* + * Process the events. + * + * We'll keep the old watch descriptor number till after we're done + * parsing this block of events. Even so, the removal of watches + * isn't race free, as they'll get automatically removed when what + * is being watched is unliked. + */ + int iWdFileNew = iWdFile; + int iWdSymDirNew = iWdSymDir; + bool fTryReRead = false; + struct inotify_event const *pCurEvt = (struct inotify_event const *)&uEvtBuf; + while (cbEvents >= (ssize_t)INOTIFY_EVENT_SIZE) + { + char szTmp[64]; + if (pCurEvt->len == 0) + LogRel5(("HostDnsServiceLinux::monitorThreadProc: event: wd=%#x mask=%#x (%s) cookie=%#x\n", + pCurEvt->wd, pCurEvt->mask, InotifyMaskToStr(szTmp, sizeof(szTmp), pCurEvt->mask), pCurEvt->cookie)); + else + LogRel5(("HostDnsServiceLinux::monitorThreadProc: event: wd=%#x mask=%#x (%s) cookie=%#x len=%#x '%s'\n", + pCurEvt->wd, pCurEvt->mask, InotifyMaskToStr(szTmp, sizeof(szTmp), pCurEvt->mask), + pCurEvt->cookie, pCurEvt->len, pCurEvt->name)); + + /* + * The file itself (symlinks followed, remember): + */ + if (pCurEvt->wd == iWdFile) + { + if (pCurEvt->mask & IN_CLOSE_WRITE) + { + Log5Func(("file: close-after-write => trigger re-read\n")); + fTryReRead = true; + } + else if (pCurEvt->mask & IN_DELETE_SELF) + { + Log5Func(("file: deleted self\n")); + if (iWdFileNew != -1) + { + iRc = inotify_rm_watch(iNotifyFd, iWdFileNew); + AssertMsg(iRc >= 0, ("%d/%d\n", iRc, errno)); + iWdFileNew = -1; + } + } + else if (pCurEvt->mask & IN_IGNORED) + iWdFileNew = -1; /* file deleted */ + else + AssertMsgFailed(("file: mask=%#x\n", pCurEvt->mask)); + } + /* + * The /etc directory + * + * We only care about events relating to the creation, deletion and + * renaming of 'resolv.conf'. We'll restablish both the direct file + * watching and the watching of any symlinked directory on all of + * these events, although for the former we'll delay the re-starting + * of the watching till all events have been processed. + */ + else if (pCurEvt->wd == iWdDir) + { + if ( pCurEvt->len > 0 + && strcmp(g_szResolvConfFilename, pCurEvt->name) == 0) + { + if (pCurEvt->mask & (IN_MOVE | IN_CREATE | IN_DELETE)) + { + if (iWdFileNew >= 0) + { + iRc = inotify_rm_watch(iNotifyFd, iWdFileNew); + Log5Func(("dir: moved / created / deleted: dropped file watch (%d - iRc=%d/err=%d)\n", + iWdFileNew, iRc, errno)); + iWdFileNew = -1; + } + if (iWdSymDirNew >= 0) + { + iRc = inotify_rm_watch(iNotifyFd, iWdSymDirNew); + Log5Func(("dir: moved / created / deleted: dropped symlinked dir watch (%d - %s/%s - iRc=%d/err=%d)\n", + iWdSymDirNew, szRealResolvConf, &szRealResolvConf[offRealResolvConfName], iRc, errno)); + iWdSymDirNew = -1; + offRealResolvConfName = 0; + } + if (pCurEvt->mask & (IN_MOVED_TO | IN_CREATE)) + { + Log5Func(("dir: moved_to / created: trigger re-read\n")); + fTryReRead = true; + + iWdSymDirNew = ::monitorSymlinkedDir(iNotifyFd, szRealResolvConf, &offRealResolvConfName); + if (iWdSymDirNew < 0) + Log5Func(("dir: moved_to / created: re-stablished symlinked-directory monitoring: iWdSymDir=%d (%s/%s)\n", + iWdSymDirNew, szRealResolvConf, &szRealResolvConf[offRealResolvConfName])); + } + } + else + AssertMsgFailed(("dir: %#x\n", pCurEvt->mask)); + } + } + /* + * The directory of a symlinked resolv.conf. + * + * Where we only care when the symlink target is created, moved_to, + * deleted or moved_from - i.e. a minimal version of the /etc event + * processing above. + * + * Note! Since we re-statablish monitoring above, szRealResolvConf + * might not match the event we're processing. Fortunately, + * this shouldn't be important except for debug logging. + */ + else if (pCurEvt->wd == iWdSymDir) + { + if ( pCurEvt->len > 0 + && offRealResolvConfName > 0 + && strcmp(&szRealResolvConf[offRealResolvConfName], pCurEvt->name) == 0) + { + if (iWdFileNew >= 0) + { + iRc = inotify_rm_watch(iNotifyFd, iWdFileNew); + Log5Func(("symdir: moved / created / deleted: drop file watch (%d - iRc=%d/err=%d)\n", + iWdFileNew, iRc, errno)); + iWdFileNew = -1; + } + if (pCurEvt->mask & (IN_MOVED_TO | IN_CREATE)) + { + Log5Func(("symdir: moved_to / created: trigger re-read\n")); + fTryReRead = true; + } + } + } + /* We can get here it seems if our inotify_rm_watch calls above takes + place after new events relating to the two descriptors happens. */ + else + Log5Func(("Unknown (obsoleted) wd value: %d (mask=%#x cookie=%#x len=%#x)\n", + pCurEvt->wd, pCurEvt->mask, pCurEvt->cookie, pCurEvt->len)); + + /* advance to the next event */ + Assert(pCurEvt->len / INOTIFY_EVENT_SIZE * INOTIFY_EVENT_SIZE == pCurEvt->len); + size_t const cbCurEvt = INOTIFY_EVENT_SIZE + pCurEvt->len; + pCurEvt = (struct inotify_event const *)((uintptr_t)pCurEvt + cbCurEvt); + cbEvents -= cbCurEvt; + } + + /* + * Commit the new watch descriptor numbers now that we're + * done processing event using the old ones. + */ + iWdFile = iWdFileNew; + iWdSymDir = iWdSymDirNew; + + /* + * If the resolv.conf watch descriptor is -1, try restablish it here. + */ + if (iWdFile == -1) + { + iWdFile = inotify_add_watch(iNotifyFd, g_szResolvConfPath, IN_CLOSE_WRITE | IN_DELETE_SELF); + if (iWdFile >= 0) + { + Log5Func(("Re-established file watcher: iWdFile=%d\n", iWdFile)); + fTryReRead = true; + } + } + + /* + * If any of the events indicate that we should re-read the file, we + * do so now. Should reduce number of unnecessary re-reads. + */ + if (fTryReRead) + { + Log5Func(("Calling readResolvConf()...\n")); + try + { + readResolvConf(); + } + catch (...) + { + LogRel(("HostDnsServiceLinux::monitorThreadProc: readResolvConf threw exception!\n")); + } + } + } + } + + /* + * Close file descriptors. + */ + if (aiStopPair[0] == m_fdShutdown) /* paranoia */ + { + m_fdShutdown = -1; + close(aiStopPair[0]); + } + close(aiStopPair[1]); + close(iNotifyFd); + LogRel5(("HostDnsServiceLinux::monitorThreadProc: returns %Rrc\n", vrcRet)); + return vrcRet; +} + diff --git a/src/VBox/Main/src-server/linux/HostHardwareLinux.cpp b/src/VBox/Main/src-server/linux/HostHardwareLinux.cpp new file mode 100644 index 00000000..39893deb --- /dev/null +++ b/src/VBox/Main/src-server/linux/HostHardwareLinux.cpp @@ -0,0 +1,1331 @@ +/* $Id: HostHardwareLinux.cpp $ */ +/** @file + * VirtualBox Main - Code for handling hardware detection under Linux, VBoxSVC. + */ + +/* + * Copyright (C) 2008-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_MAIN +#include "HostHardwareLinux.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +#ifdef VBOX_USB_WITH_SYSFS +# ifdef VBOX_USB_WITH_INOTIFY +# include /* O_CLOEXEC */ +# include +# include +# include +# include +# endif +#endif + +//#include + +#include +#include +#include +#include +#include +#include +#include + +/* + * Define NVME constant here to allow building + * on several kernel versions even if the + * building host doesn't contain certain NVME + * includes + */ +#define NVME_IOCTL_ID _IO('N', 0x40) + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +#ifdef TESTCASE +static bool testing() { return true; } +static bool fNoProbe = false; +static bool noProbe() { return fNoProbe; } +static void setNoProbe(bool val) { fNoProbe = val; } +#else +static bool testing() { return false; } +static bool noProbe() { return false; } +static void setNoProbe(bool val) { (void)val; } +#endif + + +/********************************************************************************************************************************* +* Typedefs and Defines * +*********************************************************************************************************************************/ +typedef enum SysfsWantDevice_T +{ + DVD, + Floppy, + FixedDisk +} SysfsWantDevice_T; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static int getDriveInfoFromEnv(const char *pcszVar, DriveInfoList *pList, bool isDVD, bool *pfSuccess) RT_NOTHROW_PROTO; +static int getDriveInfoFromSysfs(DriveInfoList *pList, SysfsWantDevice_T wantDevice, bool *pfSuccess) RT_NOTHROW_PROTO; + + +/** + * Find the length of a string, ignoring trailing non-ascii or control + * characters + * + * @note Code duplicated in HostHardwareFreeBSD.cpp + */ +static size_t strLenStripped(const char *pcsz) RT_NOTHROW_DEF +{ + size_t cch = 0; + for (size_t i = 0; pcsz[i] != '\0'; ++i) + if (pcsz[i] > 32 /*space*/ && pcsz[i] < 127 /*delete*/) + cch = i; + return cch + 1; +} + + +/** + * Get the name of a floppy drive according to the Linux floppy driver. + * + * @returns true on success, false if the name was not available (i.e. the + * device was not readable, or the file name wasn't a PC floppy + * device) + * @param pcszNode the path to the device node for the device + * @param Number the Linux floppy driver number for the drive. Required. + * @param pszName where to store the name retrieved + */ +static bool floppyGetName(const char *pcszNode, unsigned Number, floppy_drive_name pszName) RT_NOTHROW_DEF +{ + AssertPtrReturn(pcszNode, false); + AssertPtrReturn(pszName, false); + AssertReturn(Number <= 7, false); + RTFILE File; + int vrc = RTFileOpen(&File, pcszNode, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_NON_BLOCK); + if (RT_SUCCESS(vrc)) + { + int iRcIoCtl; + vrc = RTFileIoCtl(File, FDGETDRVTYP, pszName, 0, &iRcIoCtl); + RTFileClose(File); + if (RT_SUCCESS(vrc) && iRcIoCtl >= 0) + return true; + } + return false; +} + + +/** + * Create a UDI and a description for a floppy drive based on a number and the + * driver's name for it. + * + * We deliberately return an ugly sequence of characters as the description + * rather than an English language string to avoid translation issues. + * + * @param pcszName the floppy driver name for the device (optional) + * @param Number the number of the floppy (0 to 3 on FDC 0, 4 to 7 on + * FDC 1) + * @param pszDesc where to store the device description (optional) + * @param cbDesc the size of the buffer in @a pszDesc + * @param pszUdi where to store the device UDI (optional) + * @param cbUdi the size of the buffer in @a pszUdi + */ +static void floppyCreateDeviceStrings(const floppy_drive_name pcszName, unsigned Number, + char *pszDesc, size_t cbDesc, char *pszUdi, size_t cbUdi) RT_NOTHROW_DEF +{ + AssertPtrNullReturnVoid(pcszName); + AssertPtrNullReturnVoid(pszDesc); + AssertReturnVoid(!pszDesc || cbDesc > 0); + AssertPtrNullReturnVoid(pszUdi); + AssertReturnVoid(!pszUdi || cbUdi > 0); + AssertReturnVoid(Number <= 7); + if (pcszName) + { + const char *pcszSize; + switch(pcszName[0]) + { + case 'd': case 'q': case 'h': + pcszSize = "5.25\""; + break; + case 'D': case 'H': case 'E': case 'u': + pcszSize = "3.5\""; + break; + default: + pcszSize = "(unknown)"; + } + if (pszDesc) + RTStrPrintf(pszDesc, cbDesc, "%s %s K%s", pcszSize, &pcszName[1], + Number > 3 ? ", FDC 2" : ""); + } + else + { + if (pszDesc) + RTStrPrintf(pszDesc, cbDesc, "FDD %d%s", (Number & 4) + 1, + Number > 3 ? ", FDC 2" : ""); + } + if (pszUdi) + RTStrPrintf(pszUdi, cbUdi, + "/org/freedesktop/Hal/devices/platform_floppy_%u_storage", + Number); +} + + +/** + * Check whether a device number might correspond to a CD-ROM device according + * to Documentation/devices.txt in the Linux kernel source. + * + * @returns true if it might, false otherwise + * @param Number the device number (major and minor combination) + */ +static bool isCdromDevNum(dev_t Number) RT_NOTHROW_DEF +{ + int major = major(Number); + int minor = minor(Number); + if (major == IDE0_MAJOR && !(minor & 0x3f)) + return true; + if (major == SCSI_CDROM_MAJOR) + return true; + if (major == CDU31A_CDROM_MAJOR) + return true; + if (major == GOLDSTAR_CDROM_MAJOR) + return true; + if (major == OPTICS_CDROM_MAJOR) + return true; + if (major == SANYO_CDROM_MAJOR) + return true; + if (major == MITSUMI_X_CDROM_MAJOR) + return true; + if (major == IDE1_MAJOR && !(minor & 0x3f)) + return true; + if (major == MITSUMI_CDROM_MAJOR) + return true; + if (major == CDU535_CDROM_MAJOR) + return true; + if (major == MATSUSHITA_CDROM_MAJOR) + return true; + if (major == MATSUSHITA_CDROM2_MAJOR) + return true; + if (major == MATSUSHITA_CDROM3_MAJOR) + return true; + if (major == MATSUSHITA_CDROM4_MAJOR) + return true; + if (major == AZTECH_CDROM_MAJOR) + return true; + if (major == 30 /* CM205_CDROM_MAJOR */) /* no #define for some reason */ + return true; + if (major == CM206_CDROM_MAJOR) + return true; + if (major == IDE3_MAJOR && !(minor & 0x3f)) + return true; + if (major == 46 /* Parallel port ATAPI CD-ROM */) /* no #define */ + return true; + if (major == IDE4_MAJOR && !(minor & 0x3f)) + return true; + if (major == IDE5_MAJOR && !(minor & 0x3f)) + return true; + if (major == IDE6_MAJOR && !(minor & 0x3f)) + return true; + if (major == IDE7_MAJOR && !(minor & 0x3f)) + return true; + if (major == IDE8_MAJOR && !(minor & 0x3f)) + return true; + if (major == IDE9_MAJOR && !(minor & 0x3f)) + return true; + if (major == 113 /* VIOCD_MAJOR */) + return true; + return false; +} + + +/** + * Send an SCSI INQUIRY command to a device and return selected information. + * + * @returns iprt status code + * @retval VERR_TRY_AGAIN if the query failed but might succeed next time + * @param pcszNode the full path to the device node + * @param pbType where to store the SCSI device type on success (optional) + * @param pszVendor where to store the vendor id string on success (optional) + * @param cbVendor the size of the @a pszVendor buffer + * @param pszModel where to store the product id string on success (optional) + * @param cbModel the size of the @a pszModel buffer + * @note check documentation on the SCSI INQUIRY command and the Linux kernel + * SCSI headers included above if you want to understand what is going + * on in this method. + */ +static int cdromDoInquiry(const char *pcszNode, uint8_t *pbType, char *pszVendor, size_t cbVendor, + char *pszModel, size_t cbModel) RT_NOTHROW_DEF +{ + LogRelFlowFunc(("pcszNode=%s, pbType=%p, pszVendor=%p, cbVendor=%zu, pszModel=%p, cbModel=%zu\n", + pcszNode, pbType, pszVendor, cbVendor, pszModel, cbModel)); + AssertPtrReturn(pcszNode, VERR_INVALID_POINTER); + AssertPtrNullReturn(pbType, VERR_INVALID_POINTER); + AssertPtrNullReturn(pszVendor, VERR_INVALID_POINTER); + AssertPtrNullReturn(pszModel, VERR_INVALID_POINTER); + + RTFILE hFile = NIL_RTFILE; + int vrc = RTFileOpen(&hFile, pcszNode, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_NON_BLOCK); + if (RT_SUCCESS(vrc)) + { + int iRcIoCtl = 0; + unsigned char auchResponse[96] = { 0 }; + struct cdrom_generic_command CdromCommandReq; + RT_ZERO(CdromCommandReq); + CdromCommandReq.cmd[0] = INQUIRY; + CdromCommandReq.cmd[4] = sizeof(auchResponse); + CdromCommandReq.buffer = auchResponse; + CdromCommandReq.buflen = sizeof(auchResponse); + CdromCommandReq.data_direction = CGC_DATA_READ; + CdromCommandReq.timeout = 5000; /* ms */ + vrc = RTFileIoCtl(hFile, CDROM_SEND_PACKET, &CdromCommandReq, 0, &iRcIoCtl); + if (RT_SUCCESS(vrc) && iRcIoCtl < 0) + vrc = RTErrConvertFromErrno(-CdromCommandReq.stat); + RTFileClose(hFile); + + if (RT_SUCCESS(vrc)) + { + if (pbType) + *pbType = auchResponse[0] & 0x1f; + if (pszVendor) + { + RTStrPrintf(pszVendor, cbVendor, "%.8s", &auchResponse[8] /* vendor id string */); + RTStrPurgeEncoding(pszVendor); + } + if (pszModel) + { + RTStrPrintf(pszModel, cbModel, "%.16s", &auchResponse[16] /* product id string */); + RTStrPurgeEncoding(pszModel); + } + LogRelFlowFunc(("returning success: type=%u, vendor=%.8s, product=%.16s\n", + auchResponse[0] & 0x1f, &auchResponse[8], &auchResponse[16])); + return VINF_SUCCESS; + } + } + LogRelFlowFunc(("returning %Rrc\n", vrc)); + return vrc; +} + + +/** + * Initialise the device strings (description and UDI) for a DVD drive based on + * vendor and model name strings. + * + * @param pcszVendor the vendor ID string + * @param pcszModel the product ID string + * @param pszDesc where to store the description string (optional) + * @param cbDesc the size of the buffer in @a pszDesc + * @param pszUdi where to store the UDI string (optional) + * @param cbUdi the size of the buffer in @a pszUdi + * + * @note Used for more than DVDs these days. + */ +static void dvdCreateDeviceStrings(const char *pcszVendor, const char *pcszModel, + char *pszDesc, size_t cbDesc, char *pszUdi, size_t cbUdi) RT_NOEXCEPT +{ + AssertPtrReturnVoid(pcszVendor); + AssertPtrReturnVoid(pcszModel); + AssertPtrNullReturnVoid(pszDesc); + AssertReturnVoid(!pszDesc || cbDesc > 0); + AssertPtrNullReturnVoid(pszUdi); + AssertReturnVoid(!pszUdi || cbUdi > 0); + + size_t cchModel = strLenStripped(pcszModel); + /* + * Vendor and Model strings can contain trailing spaces. + * Create trimmed copy of them because we should not modify + * original strings. + */ + char* pszStartTrimmed = RTStrStripL(pcszVendor); + char* pszVendor = RTStrDup(pszStartTrimmed); + RTStrStripR(pszVendor); + pszStartTrimmed = RTStrStripL(pcszModel); + char* pszModel = RTStrDup(pszStartTrimmed); + RTStrStripR(pszModel); + + size_t cbVendor = strlen(pszVendor); + + /* Create a cleaned version of the model string for the UDI string. */ + char szCleaned[128]; + for (unsigned i = 0; i < sizeof(szCleaned) && pcszModel[i] != '\0'; ++i) + if ( (pcszModel[i] >= '0' && pcszModel[i] <= '9') + || (pcszModel[i] >= 'A' && pcszModel[i] <= 'z')) + szCleaned[i] = pcszModel[i]; + else + szCleaned[i] = '_'; + szCleaned[RT_MIN(cchModel, sizeof(szCleaned) - 1)] = '\0'; + + /* Construct the description string as "Vendor Product" */ + if (pszDesc) + { + if (cbVendor > 0) + { + RTStrPrintf(pszDesc, cbDesc, "%.*s %s", cbVendor, pszVendor, strlen(pszModel) > 0 ? pszModel : "(unknown drive model)"); + RTStrPurgeEncoding(pszDesc); + } + else + RTStrCopy(pszDesc, cbDesc, pszModel); + } + /* Construct the UDI string */ + if (pszUdi) + { + if (cchModel > 0) + RTStrPrintf(pszUdi, cbUdi, "/org/freedesktop/Hal/devices/storage_model_%s", szCleaned); + else + pszUdi[0] = '\0'; + } +} + + +/** + * Check whether the device is the NVME device. + * @returns true on success, false if the name was not available (i.e. the + * device was not readable, or the file name wasn't a NVME device) + * @param pcszNode the path to the device node for the device + */ +static bool probeNVME(const char *pcszNode) RT_NOTHROW_DEF +{ + AssertPtrReturn(pcszNode, false); + RTFILE File; + int vrc = RTFileOpen(&File, pcszNode, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE | RTFILE_O_NON_BLOCK); + if (RT_SUCCESS(vrc)) + { + int iRcIoCtl; + vrc = RTFileIoCtl(File, NVME_IOCTL_ID, NULL, 0, &iRcIoCtl); + RTFileClose(File); + if (RT_SUCCESS(vrc) && iRcIoCtl >= 0) + return true; + } + return false; +} + +/** + * Check whether a device node points to a valid device and create a UDI and + * a description for it, and store the device number, if it does. + * + * @returns true if the device is valid, false otherwise + * @param pcszNode the path to the device node + * @param isDVD are we looking for a DVD device (or a floppy device)? + * @param pDevice where to store the device node (optional) + * @param pszDesc where to store the device description (optional) + * @param cbDesc the size of the buffer in @a pszDesc + * @param pszUdi where to store the device UDI (optional) + * @param cbUdi the size of the buffer in @a pszUdi + */ +static bool devValidateDevice(const char *pcszNode, bool isDVD, dev_t *pDevice, + char *pszDesc, size_t cbDesc, char *pszUdi, size_t cbUdi) RT_NOTHROW_DEF +{ + AssertPtrReturn(pcszNode, false); + AssertPtrNullReturn(pDevice, false); + AssertPtrNullReturn(pszDesc, false); + AssertReturn(!pszDesc || cbDesc > 0, false); + AssertPtrNullReturn(pszUdi, false); + AssertReturn(!pszUdi || cbUdi > 0, false); + + RTFSOBJINFO ObjInfo; + if (RT_FAILURE(RTPathQueryInfo(pcszNode, &ObjInfo, RTFSOBJATTRADD_UNIX))) + return false; + if (!RTFS_IS_DEV_BLOCK(ObjInfo.Attr.fMode)) + return false; + if (pDevice) + *pDevice = ObjInfo.Attr.u.Unix.Device; + + if (isDVD) + { + char szVendor[128], szModel[128]; + uint8_t u8Type; + if (!isCdromDevNum(ObjInfo.Attr.u.Unix.Device)) + return false; + if (RT_FAILURE(cdromDoInquiry(pcszNode, &u8Type, + szVendor, sizeof(szVendor), + szModel, sizeof(szModel)))) + return false; + if (u8Type != TYPE_ROM) + return false; + dvdCreateDeviceStrings(szVendor, szModel, pszDesc, cbDesc, pszUdi, cbUdi); + } + else + { + /* Floppies on Linux are legacy devices with hardcoded majors and minors */ + if (major(ObjInfo.Attr.u.Unix.Device) != FLOPPY_MAJOR) + return false; + + unsigned Number; + switch (minor(ObjInfo.Attr.u.Unix.Device)) + { + case 0: case 1: case 2: case 3: + Number = minor(ObjInfo.Attr.u.Unix.Device); + break; + case 128: case 129: case 130: case 131: + Number = minor(ObjInfo.Attr.u.Unix.Device) - 128 + 4; + break; + default: + return false; + } + + floppy_drive_name szName; + if (!floppyGetName(pcszNode, Number, szName)) + return false; + floppyCreateDeviceStrings(szName, Number, pszDesc, cbDesc, pszUdi, cbUdi); + } + return true; +} + + +int VBoxMainDriveInfo::updateDVDs() RT_NOEXCEPT +{ + LogFlowThisFunc(("entered\n")); + int vrc; + try + { + mDVDList.clear(); + /* Always allow the user to override our auto-detection using an + * environment variable. */ + bool fSuccess = false; /* Have we succeeded in finding anything yet? */ + vrc = getDriveInfoFromEnv("VBOX_CDROM", &mDVDList, true /* isDVD */, &fSuccess); + setNoProbe(false); + if (RT_SUCCESS(vrc) && (!fSuccess || testing())) + vrc = getDriveInfoFromSysfs(&mDVDList, DVD, &fSuccess); + if (RT_SUCCESS(vrc) && testing()) + { + setNoProbe(true); + vrc = getDriveInfoFromSysfs(&mDVDList, DVD, &fSuccess); + } + } + catch (std::bad_alloc &e) + { + vrc = VERR_NO_MEMORY; + } + LogFlowThisFunc(("vrc=%Rrc\n", vrc)); + return vrc; +} + +int VBoxMainDriveInfo::updateFloppies() RT_NOEXCEPT +{ + LogFlowThisFunc(("entered\n")); + int vrc; + try + { + mFloppyList.clear(); + bool fSuccess = false; /* Have we succeeded in finding anything yet? */ + vrc = getDriveInfoFromEnv("VBOX_FLOPPY", &mFloppyList, false /* isDVD */, &fSuccess); + setNoProbe(false); + if (RT_SUCCESS(vrc) && (!fSuccess || testing())) + vrc = getDriveInfoFromSysfs(&mFloppyList, Floppy, &fSuccess); + if (RT_SUCCESS(vrc) && testing()) + { + setNoProbe(true); + vrc = getDriveInfoFromSysfs(&mFloppyList, Floppy, &fSuccess); + } + } + catch (std::bad_alloc &) + { + vrc = VERR_NO_MEMORY; + } + LogFlowThisFunc(("vrc=%Rrc\n", vrc)); + return vrc; +} + +int VBoxMainDriveInfo::updateFixedDrives() RT_NOEXCEPT +{ + LogFlowThisFunc(("entered\n")); + int vrc; + try + { + mFixedDriveList.clear(); + setNoProbe(false); + bool fSuccess = false; /* Have we succeeded in finding anything yet? */ + vrc = getDriveInfoFromSysfs(&mFixedDriveList, FixedDisk, &fSuccess); + if (RT_SUCCESS(vrc) && testing()) + { + setNoProbe(true); + vrc = getDriveInfoFromSysfs(&mFixedDriveList, FixedDisk, &fSuccess); + } + } + catch (std::bad_alloc &) + { + vrc = VERR_NO_MEMORY; + } + LogFlowThisFunc(("vrc=%Rrc\n", vrc)); + return vrc; +} + + +/** + * Extract the names of drives from an environment variable and add them to a + * list if they are valid. + * + * @returns iprt status code + * @param pcszVar the name of the environment variable. The variable + * value should be a list of device node names, separated + * by ':' characters. + * @param pList the list to append the drives found to + * @param isDVD are we looking for DVD drives or for floppies? + * @param pfSuccess this will be set to true if we found at least one drive + * and to false otherwise. Optional. + * + * @note This is duplicated in HostHardwareFreeBSD.cpp. + */ +static int getDriveInfoFromEnv(const char *pcszVar, DriveInfoList *pList, bool isDVD, bool *pfSuccess) RT_NOTHROW_DEF +{ + AssertPtrReturn(pcszVar, VERR_INVALID_POINTER); + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrNullReturn(pfSuccess, VERR_INVALID_POINTER); + LogFlowFunc(("pcszVar=%s, pList=%p, isDVD=%d, pfSuccess=%p\n", pcszVar, pList, isDVD, pfSuccess)); + int vrc = VINF_SUCCESS; + bool success = false; + char *pszFreeMe = RTEnvDupEx(RTENV_DEFAULT, pcszVar); + + try + { + char *pszCurrent = pszFreeMe; + while (pszCurrent && *pszCurrent != '\0') + { + char *pszNext = strchr(pszCurrent, ':'); + if (pszNext) + *pszNext++ = '\0'; + + char szReal[RTPATH_MAX]; + char szDesc[256], szUdi[256]; + if ( RT_SUCCESS(RTPathReal(pszCurrent, szReal, sizeof(szReal))) + && devValidateDevice(szReal, isDVD, NULL, szDesc, sizeof(szDesc), szUdi, sizeof(szUdi))) + { + pList->push_back(DriveInfo(szReal, szUdi, szDesc)); + success = true; + } + pszCurrent = pszNext; + } + if (pfSuccess != NULL) + *pfSuccess = success; + } + catch (std::bad_alloc &) + { + vrc = VERR_NO_MEMORY; + } + RTStrFree(pszFreeMe); + LogFlowFunc(("vrc=%Rrc, success=%d\n", vrc, success)); + return vrc; +} + + +class SysfsBlockDev +{ +public: + SysfsBlockDev(const char *pcszName, SysfsWantDevice_T wantDevice) RT_NOEXCEPT + : mpcszName(pcszName), mWantDevice(wantDevice), misConsistent(true), misValid(false) + { + if (findDeviceNode()) + { + switch (mWantDevice) + { + case DVD: validateAndInitForDVD(); break; + case Floppy: validateAndInitForFloppy(); break; + default: validateAndInitForFixedDisk(); break; + } + } + } +private: + /** The name of the subdirectory of /sys/block for this device */ + const char *mpcszName; + /** Are we looking for a floppy, a DVD or a fixed disk device? */ + SysfsWantDevice_T mWantDevice; + /** The device node for the device */ + char mszNode[RTPATH_MAX]; + /** Does the sysfs entry look like we expect it too? This is a canary + * for future sysfs ABI changes. */ + bool misConsistent; + /** Is this entry a valid specimen of what we are looking for? */ + bool misValid; + /** Human readable drive description string */ + char mszDesc[256]; + /** Unique identifier for the drive. Should be identical to hal's UDI for + * the device. May not be unique for two identical drives. */ + char mszUdi[256]; +private: + /* Private methods */ + + /** + * Fill in the device node member based on the /sys/block subdirectory. + * @returns boolean success value + */ + bool findDeviceNode() RT_NOEXCEPT + { + dev_t dev = 0; + int vrc = RTLinuxSysFsReadDevNumFile(&dev, "block/%s/dev", mpcszName); + if (RT_FAILURE(vrc) || dev == 0) + { + misConsistent = false; + return false; + } + vrc = RTLinuxCheckDevicePath(dev, RTFS_TYPE_DEV_BLOCK, mszNode, sizeof(mszNode), "%s", mpcszName); + return RT_SUCCESS(vrc); + } + + /** Check whether the sysfs block entry is valid for a DVD device and + * initialise the string data members for the object. We try to get all + * the information we need from sysfs if possible, to avoid unnecessarily + * poking the device, and if that fails we fall back to an SCSI INQUIRY + * command. */ + void validateAndInitForDVD() RT_NOEXCEPT + { + int64_t type = 0; + int vrc = RTLinuxSysFsReadIntFile(10, &type, "block/%s/device/type", mpcszName); + if (RT_SUCCESS(vrc) && type != TYPE_ROM) + return; + if (type == TYPE_ROM) + { + char szVendor[128]; + vrc = RTLinuxSysFsReadStrFile(szVendor, sizeof(szVendor), NULL, "block/%s/device/vendor", mpcszName); + if (RT_SUCCESS(vrc)) + { + char szModel[128]; + vrc = RTLinuxSysFsReadStrFile(szModel, sizeof(szModel), NULL, "block/%s/device/model", mpcszName); + if (RT_SUCCESS(vrc)) + { + misValid = true; + dvdCreateDeviceStrings(szVendor, szModel, mszDesc, sizeof(mszDesc), mszUdi, sizeof(mszUdi)); + return; + } + } + } + if (!noProbe()) + probeAndInitForDVD(); + } + + /** Try to find out whether a device is a DVD drive by sending it an + * SCSI INQUIRY command. If it is, initialise the string and validity + * data members for the object based on the returned data. + */ + void probeAndInitForDVD() RT_NOEXCEPT + { + AssertReturnVoid(mszNode[0] != '\0'); + uint8_t bType = 0; + char szVendor[128] = ""; + char szModel[128] = ""; + int vrc = cdromDoInquiry(mszNode, &bType, szVendor, sizeof(szVendor), szModel, sizeof(szModel)); + if (RT_SUCCESS(vrc) && bType == TYPE_ROM) + { + misValid = true; + dvdCreateDeviceStrings(szVendor, szModel, mszDesc, sizeof(mszDesc), mszUdi, sizeof(mszUdi)); + } + } + + /** Check whether the sysfs block entry is valid for a floppy device and + * initialise the string data members for the object. Since we only + * support floppies using the basic "floppy" driver, we check the driver + * using the entry name and a driver-specific ioctl. */ + void validateAndInitForFloppy() RT_NOEXCEPT + { + floppy_drive_name szName; + char szDriver[8]; + if ( mpcszName[0] != 'f' + || mpcszName[1] != 'd' + || mpcszName[2] < '0' + || mpcszName[2] > '7' + || mpcszName[3] != '\0') + return; + bool fHaveName = false; + if (!noProbe()) + fHaveName = floppyGetName(mszNode, mpcszName[2] - '0', szName); + int vrc = RTLinuxSysFsGetLinkDest(szDriver, sizeof(szDriver), NULL, "block/%s/%s", mpcszName, "device/driver"); + if (RT_SUCCESS(vrc)) + { + if (RTStrCmp(szDriver, "floppy")) + return; + } + else if (!fHaveName) + return; + floppyCreateDeviceStrings(fHaveName ? szName : NULL, + mpcszName[2] - '0', mszDesc, + sizeof(mszDesc), mszUdi, sizeof(mszUdi)); + misValid = true; + } + + void validateAndInitForFixedDisk() RT_NOEXCEPT + { + /* + * For current task only device path is needed. Therefore, device probing + * is skipped and other fields are empty if there aren't files in the + * device entry. + */ + int64_t type = 0; + int vrc = RTLinuxSysFsReadIntFile(10, &type, "block/%s/device/type", mpcszName); + if (!RT_SUCCESS(vrc) || type != TYPE_DISK) + { + if (noProbe() || !probeNVME(mszNode)) + { + char szDriver[16]; + vrc = RTLinuxSysFsGetLinkDest(szDriver, sizeof(szDriver), NULL, "block/%s/%s", mpcszName, "device/device/driver"); + if (RT_FAILURE(vrc) || RTStrCmp(szDriver, "nvme")) + return; + } + } + char szVendor[128]; + char szModel[128]; + size_t cbRead = 0; + vrc = RTLinuxSysFsReadStrFile(szVendor, sizeof(szVendor), &cbRead, "block/%s/device/vendor", mpcszName); + szVendor[cbRead] = '\0'; + /* Assume the model is always present. Vendor is not present for NVME disks */ + cbRead = 0; + vrc = RTLinuxSysFsReadStrFile(szModel, sizeof(szModel), &cbRead, "block/%s/device/model", mpcszName); + szModel[cbRead] = '\0'; + if (RT_SUCCESS(vrc)) + { + misValid = true; + dvdCreateDeviceStrings(szVendor, szModel, mszDesc, sizeof(mszDesc), mszUdi, sizeof(mszUdi)); + } + } + +public: + bool isConsistent() const RT_NOEXCEPT + { + return misConsistent; + } + bool isValid() const RT_NOEXCEPT + { + return misValid; + } + const char *getDesc() const RT_NOEXCEPT + { + return mszDesc; + } + const char *getUdi() const RT_NOEXCEPT + { + return mszUdi; + } + const char *getNode() const RT_NOEXCEPT + { + return mszNode; + } +}; + + +/** + * Helper function to query the sysfs subsystem for information about DVD + * drives attached to the system. + * @returns iprt status code + * @param pList where to add information about the drives detected + * @param wantDevice The kind of devices we're looking for. + * @param pfSuccess Did we find anything? + * + * @returns IPRT status code + * @throws Nothing. + */ +static int getDriveInfoFromSysfs(DriveInfoList *pList, SysfsWantDevice_T wantDevice, bool *pfSuccess) RT_NOTHROW_DEF +{ + AssertPtrReturn(pList, VERR_INVALID_POINTER); + AssertPtrNullReturn(pfSuccess, VERR_INVALID_POINTER); /* Valid or Null */ + LogFlowFunc (("pList=%p, wantDevice=%u, pfSuccess=%p\n", + pList, (unsigned)wantDevice, pfSuccess)); + if (!RTPathExists("/sys")) + return VINF_SUCCESS; + + bool fSuccess = true; + unsigned cFound = 0; + RTDIR hDir = NIL_RTDIR; + int vrc = RTDirOpen(&hDir, "/sys/block"); + /* This might mean that sysfs semantics have changed */ + AssertReturn(vrc != VERR_FILE_NOT_FOUND, VINF_SUCCESS); + if (RT_SUCCESS(vrc)) + { + for (;;) + { + RTDIRENTRY entry; + vrc = RTDirRead(hDir, &entry, NULL); + Assert(vrc != VERR_BUFFER_OVERFLOW); /* Should never happen... */ + if (RT_FAILURE(vrc)) /* Including overflow and no more files */ + break; + if (entry.szName[0] == '.') + continue; + SysfsBlockDev dev(entry.szName, wantDevice); + /* This might mean that sysfs semantics have changed */ + AssertBreakStmt(dev.isConsistent(), fSuccess = false); + if (!dev.isValid()) + continue; + try + { + pList->push_back(DriveInfo(dev.getNode(), dev.getUdi(), dev.getDesc())); + } + catch (std::bad_alloc &e) + { + vrc = VERR_NO_MEMORY; + break; + } + ++cFound; + } + RTDirClose(hDir); + } + if (vrc == VERR_NO_MORE_FILES) + vrc = VINF_SUCCESS; + else if (RT_FAILURE(vrc)) + /* Clean up again */ + while (cFound-- > 0) + pList->pop_back(); + if (pfSuccess) + *pfSuccess = fSuccess; + LogFlow (("vrc=%Rrc, fSuccess=%u\n", vrc, (unsigned)fSuccess)); + return vrc; +} + + +/** Helper for readFilePathsFromDir(). Adds a path to the vector if it is not + * NULL and not a dotfile (".", "..", ".*"). */ +static int maybeAddPathToVector(const char *pcszPath, const char *pcszEntry, VECTOR_PTR(char *) *pvecpchDevs) RT_NOTHROW_DEF +{ + if (!pcszPath) + return 0; + if (pcszEntry[0] == '.') + return 0; + char *pszPath = RTStrDup(pcszPath); + if (pszPath) + { + int vrc = VEC_PUSH_BACK_PTR(pvecpchDevs, char *, pszPath); + if (RT_SUCCESS(vrc)) + return 0; + } + return ENOMEM; +} + +/** + * Helper for readFilePaths(). + * + * Adds the entries from the open directory @a pDir to the vector @a pvecpchDevs + * using either the full path or the realpath() and skipping hidden files + * and files on which realpath() fails. + */ +static int readFilePathsFromDir(const char *pcszPath, DIR *pDir, VECTOR_PTR(char *) *pvecpchDevs, int withRealPath) RT_NOTHROW_DEF +{ + struct dirent entry, *pResult; + int err; + +#if RT_GNUC_PREREQ(4, 6) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + for (err = readdir_r(pDir, &entry, &pResult); + pResult != NULL && err == 0; + err = readdir_r(pDir, &entry, &pResult)) +#if RT_GNUC_PREREQ(4, 6) +# pragma GCC diagnostic pop +#endif + { + /* We (implicitly) require that PATH_MAX be defined */ + char szPath[PATH_MAX + 1], szRealPath[PATH_MAX + 1], *pszPath; + if (snprintf(szPath, sizeof(szPath), "%s/%s", pcszPath, + entry.d_name) < 0) + return errno; + if (withRealPath) + pszPath = realpath(szPath, szRealPath); + else + pszPath = szPath; + if ((err = maybeAddPathToVector(pszPath, entry.d_name, pvecpchDevs))) + return err; + } + return err; +} + + +/** + * Helper for walkDirectory to dump the names of a directory's entries into a + * vector of char pointers. + * + * @returns zero on success or (positive) posix error value. + * @param pcszPath the path to dump. + * @param pvecpchDevs an empty vector of char pointers - must be cleaned up + * by the caller even on failure. + * @param withRealPath whether to canonicalise the filename with realpath + */ +static int readFilePaths(const char *pcszPath, VECTOR_PTR(char *) *pvecpchDevs, int withRealPath) RT_NOTHROW_DEF +{ + AssertPtrReturn(pvecpchDevs, EINVAL); + AssertReturn(VEC_SIZE_PTR(pvecpchDevs) == 0, EINVAL); + AssertPtrReturn(pcszPath, EINVAL); + + DIR *pDir = opendir(pcszPath); + if (!pDir) + return RTErrConvertFromErrno(errno); + int err = readFilePathsFromDir(pcszPath, pDir, pvecpchDevs, withRealPath); + if (closedir(pDir) < 0 && !err) + err = errno; + return RTErrConvertFromErrno(err); +} + + +class hotplugNullImpl : public VBoxMainHotplugWaiterImpl +{ +public: + hotplugNullImpl(const char *) {} + virtual ~hotplugNullImpl (void) {} + /** @copydoc VBoxMainHotplugWaiter::Wait */ + virtual int Wait (RTMSINTERVAL cMillies) + { + NOREF(cMillies); + return VERR_NOT_SUPPORTED; + } + /** @copydoc VBoxMainHotplugWaiter::Interrupt */ + virtual void Interrupt (void) {} + virtual int getStatus(void) + { + return VERR_NOT_SUPPORTED; + } + +}; + +#ifdef VBOX_USB_WITH_SYSFS +# ifdef VBOX_USB_WITH_INOTIFY +/** Class wrapper around an inotify watch (or a group of them to be precise). + */ +typedef struct inotifyWatch +{ + /** The native handle of the inotify fd. */ + int mhInotify; +} inotifyWatch; + +/** The flags we pass to inotify - modify, create, delete, change permissions + */ +#define MY_IN_FLAGS (IN_CREATE | IN_DELETE | IN_MODIFY | IN_ATTRIB) +AssertCompile(MY_IN_FLAGS == 0x306); + +static int iwAddWatch(inotifyWatch *pSelf, const char *pcszPath) +{ + errno = 0; + if ( inotify_add_watch(pSelf->mhInotify, pcszPath, MY_IN_FLAGS) >= 0 + || errno == EACCES) + return VINF_SUCCESS; + /* Other errors listed in the manpage can be treated as fatal */ + return RTErrConvertFromErrno(errno); +} + +/** Object initialisation */ +static int iwInit(inotifyWatch *pSelf) +{ + AssertPtr(pSelf); + pSelf->mhInotify = -1; + int fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + if (fd >= 0) + { + pSelf->mhInotify = fd; + return VINF_SUCCESS; + } + Assert(errno > 0); + return RTErrConvertFromErrno(errno); +} + +static void iwTerm(inotifyWatch *pSelf) +{ + AssertPtrReturnVoid(pSelf); + if (pSelf->mhInotify != -1) + { + close(pSelf->mhInotify); + pSelf->mhInotify = -1; + } +} + +static int iwGetFD(inotifyWatch *pSelf) +{ + AssertPtrReturn(pSelf, -1); + return pSelf->mhInotify; +} + +# define SYSFS_WAKEUP_STRING "Wake up!" + +class hotplugInotifyImpl : public VBoxMainHotplugWaiterImpl +{ + /** Pipe used to interrupt wait(), the read end. */ + int mhWakeupPipeR; + /** Pipe used to interrupt wait(), the write end. */ + int mhWakeupPipeW; + /** The inotify watch set */ + inotifyWatch mWatches; + /** Flag to mark that the Wait() method is currently being called, and to + * ensure that it isn't called multiple times in parallel. */ + volatile uint32_t mfWaiting; + /** The root of the USB devices tree. */ + const char *mpcszDevicesRoot; + /** iprt result code from object initialisation. Should be AssertReturn-ed + * on at the start of all methods. I went this way because I didn't want + * to deal with exceptions. */ + int mStatus; + /** ID values associates with the wakeup pipe and the FAM socket for polling + */ + enum + { + RPIPE_ID = 0, + INOTIFY_ID, + MAX_POLLID + }; + + /** Clean up any resources in use, gracefully skipping over any which have + * not yet been allocated or already cleaned up. Intended to be called + * from the destructor or after a failed initialisation. */ + void term(void); + + int drainInotify(); + + /** Read the wakeup string from the wakeup pipe */ + int drainWakeupPipe(void); +public: + hotplugInotifyImpl(const char *pcszDevicesRoot); + virtual ~hotplugInotifyImpl(void) + { + term(); +#ifdef DEBUG + /** The first call to term should mark all resources as freed, so this + * should be a semantic no-op. */ + term(); +#endif + } + /** Is inotify available and working on this system? If so we expect that + * this implementation will be usable. */ + static bool Available(void) + { + int const fd = inotify_init1(IN_CLOEXEC | IN_NONBLOCK); + if (fd >= 0) + close(fd); + return fd >= 0; + } + + virtual int getStatus(void) + { + return mStatus; + } + + /** @copydoc VBoxMainHotplugWaiter::Wait */ + virtual int Wait(RTMSINTERVAL); + /** @copydoc VBoxMainHotplugWaiter::Interrupt */ + virtual void Interrupt(void); +}; + +/** Simplified version of RTPipeCreate */ +static int pipeCreateSimple(int *phPipeRead, int *phPipeWrite) +{ + AssertPtrReturn(phPipeRead, VERR_INVALID_POINTER); + AssertPtrReturn(phPipeWrite, VERR_INVALID_POINTER); + + /* + * Create the pipe and set the close-on-exec flag. + * ASSUMES we're building and running on Linux 2.6.27 or later (pipe2). + */ + int aFds[2] = {-1, -1}; + if (pipe2(aFds, O_CLOEXEC)) + return RTErrConvertFromErrno(errno); + + *phPipeRead = aFds[0]; + *phPipeWrite = aFds[1]; + + /* + * Before we leave, make sure to shut up SIGPIPE. + */ + signal(SIGPIPE, SIG_IGN); + return VINF_SUCCESS; +} + +hotplugInotifyImpl::hotplugInotifyImpl(const char *pcszDevicesRoot) + : mhWakeupPipeR(-1), mhWakeupPipeW(-1), mfWaiting(0) + , mpcszDevicesRoot(pcszDevicesRoot), mStatus(VERR_WRONG_ORDER) +{ +# ifdef DEBUG + /* Excercise the code path (term() on a not-fully-initialised object) as + * well as we can. On an uninitialised object this method is a semantic + * no-op. */ + mWatches.mhInotify = -1; /* term will access this variable */ + term(); + /* For now this probing method should only be used if nothing else is + * available */ +# endif + + int vrc = iwInit(&mWatches); + if (RT_SUCCESS(vrc)) + { + vrc = iwAddWatch(&mWatches, mpcszDevicesRoot); + if (RT_SUCCESS(vrc)) + vrc = pipeCreateSimple(&mhWakeupPipeR, &mhWakeupPipeW); + } + mStatus = vrc; + if (RT_FAILURE(vrc)) + term(); +} + +void hotplugInotifyImpl::term(void) +{ + /** This would probably be a pending segfault, so die cleanly */ + AssertRelease(!mfWaiting); + if (mhWakeupPipeR != -1) + { + close(mhWakeupPipeR); + mhWakeupPipeR = -1; + } + if (mhWakeupPipeW != -1) + { + close(mhWakeupPipeW); + mhWakeupPipeW = -1; + } + iwTerm(&mWatches); +} + +int hotplugInotifyImpl::drainInotify() +{ + char chBuf[RTPATH_MAX + 256]; /* Should always be big enough */ + ssize_t cchRead; + + AssertRCReturn(mStatus, VERR_WRONG_ORDER); + errno = 0; + do + cchRead = read(iwGetFD(&mWatches), chBuf, sizeof(chBuf)); + while (cchRead > 0); + if (cchRead == 0) + return VINF_SUCCESS; + if ( cchRead < 0 + && ( errno == EAGAIN +#if EAGAIN != EWOULDBLOCK + || errno == EWOULDBLOCK +#endif + )) + return VINF_SUCCESS; + Assert(errno > 0); + return RTErrConvertFromErrno(errno); +} + +int hotplugInotifyImpl::drainWakeupPipe(void) +{ + char szBuf[sizeof(SYSFS_WAKEUP_STRING)]; + ssize_t cbRead; + + AssertRCReturn(mStatus, VERR_WRONG_ORDER); + cbRead = read(mhWakeupPipeR, szBuf, sizeof(szBuf)); + Assert(cbRead > 0); + NOREF(cbRead); + return VINF_SUCCESS; +} + +int hotplugInotifyImpl::Wait(RTMSINTERVAL aMillies) +{ + AssertRCReturn(mStatus, VERR_WRONG_ORDER); + bool fEntered = ASMAtomicCmpXchgU32(&mfWaiting, 1, 0); + AssertReturn(fEntered, VERR_WRONG_ORDER); + + VECTOR_PTR(char *) vecpchDevs; + VEC_INIT_PTR(&vecpchDevs, char *, RTStrFree); + int vrc = readFilePaths(mpcszDevicesRoot, &vecpchDevs, false); + if (RT_SUCCESS(vrc)) + { + char **ppszEntry; + VEC_FOR_EACH(&vecpchDevs, char *, ppszEntry) + if (RT_FAILURE(vrc = iwAddWatch(&mWatches, *ppszEntry))) + break; + + if (RT_SUCCESS(vrc)) + { + struct pollfd pollFD[MAX_POLLID]; + pollFD[RPIPE_ID].fd = mhWakeupPipeR; + pollFD[RPIPE_ID].events = POLLIN; + pollFD[INOTIFY_ID].fd = iwGetFD(&mWatches); + pollFD[INOTIFY_ID].events = POLLIN | POLLERR | POLLHUP; + errno = 0; + int cPolled = poll(pollFD, RT_ELEMENTS(pollFD), aMillies); + if (cPolled < 0) + { + Assert(errno > 0); + vrc = RTErrConvertFromErrno(errno); + } + else if (pollFD[RPIPE_ID].revents) + { + vrc = drainWakeupPipe(); + if (RT_SUCCESS(vrc)) + vrc = VERR_INTERRUPTED; + } + else if ((pollFD[INOTIFY_ID].revents)) + { + if (cPolled == 1) + vrc = drainInotify(); + else + AssertFailedStmt(vrc = VERR_INTERNAL_ERROR); + } + else + { + if (errno == 0 && cPolled == 0) + vrc = VERR_TIMEOUT; + else + AssertFailedStmt(vrc = VERR_INTERNAL_ERROR); + } + } + } + + mfWaiting = 0; + VEC_CLEANUP_PTR(&vecpchDevs); + return vrc; +} + +void hotplugInotifyImpl::Interrupt(void) +{ + AssertRCReturnVoid(mStatus); + ssize_t cbWritten = write(mhWakeupPipeW, SYSFS_WAKEUP_STRING, + sizeof(SYSFS_WAKEUP_STRING)); + if (cbWritten > 0) + fsync(mhWakeupPipeW); +} + +# endif /* VBOX_USB_WITH_INOTIFY */ +#endif /* VBOX_USB_WTH_SYSFS */ + +VBoxMainHotplugWaiter::VBoxMainHotplugWaiter(const char *pcszDevicesRoot) +{ + try + { +#ifdef VBOX_USB_WITH_SYSFS +# ifdef VBOX_USB_WITH_INOTIFY + if (hotplugInotifyImpl::Available()) + { + mImpl = new hotplugInotifyImpl(pcszDevicesRoot); + return; + } +# endif /* VBOX_USB_WITH_INOTIFY */ +#endif /* VBOX_USB_WITH_SYSFS */ + mImpl = new hotplugNullImpl(pcszDevicesRoot); + } + catch (std::bad_alloc &e) + { } +} diff --git a/src/VBox/Main/src-server/linux/HostPowerLinux.cpp b/src/VBox/Main/src-server/linux/HostPowerLinux.cpp new file mode 100644 index 00000000..c27b149a --- /dev/null +++ b/src/VBox/Main/src-server/linux/HostPowerLinux.cpp @@ -0,0 +1,198 @@ +/* $Id: HostPowerLinux.cpp $ */ +/** @file + * VirtualBox interface to host's power notification service + */ + +/* + * Copyright (C) 2015-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#define LOG_GROUP LOG_GROUP_MAIN_HOST +#include "HostPower.h" +#include "LoggingNew.h" + +#include +#include +#include + +static bool checkDBusError(DBusError *pError, DBusConnection **pConnection) +{ + if (dbus_error_is_set(pError)) + { + LogRel(("HostPowerServiceLinux: DBus connection Error (%s)\n", pError->message)); + dbus_error_free(pError); + if (*pConnection) + { + /* Close the socket or whatever underlying the connection. */ + dbus_connection_close(*pConnection); + /* Free in-process resources used for the now-closed connection. */ + dbus_connection_unref(*pConnection); + *pConnection = NULL; + } + return true; + } + return false; +} + +HostPowerServiceLinux::HostPowerServiceLinux(VirtualBox *aVirtualBox) + : HostPowerService(aVirtualBox) + , mThread(NIL_RTTHREAD) + , mpConnection(NULL) +{ + DBusError error; + + int vrc = RTDBusLoadLib(); + if (RT_FAILURE(vrc)) + { + LogRel(("HostPowerServiceLinux: DBus library not found. Service not available.\n")); + return; + } + dbus_error_init(&error); + /* Connect to the DBus. The connection will be not shared with any other + * in-process callers of dbus_bus_get(). This is considered wasteful (see + * API documentation) but simplifies our code, specifically shutting down. + * The session bus allows up to 100000 connections per user as it "is just + * running as the user anyway" (see session.conf.in in the DBus sources). */ + mpConnection = dbus_bus_get_private(DBUS_BUS_SYSTEM, &error); + if (checkDBusError(&error, &mpConnection)) + return; + /* We do not want to exit(1) if the connection is broken. */ + dbus_connection_set_exit_on_disconnect(mpConnection, FALSE); + /* Tell the bus to wait for the sleep signal(s). */ + /* The current systemd-logind interface. */ + dbus_bus_add_match(mpConnection, "type='signal',interface='org.freedesktop.login1.Manager'", &error); + /* The previous UPower interfaces (2010 - ca 2013). */ + dbus_bus_add_match(mpConnection, "type='signal',interface='org.freedesktop.UPower'", &error); + dbus_connection_flush(mpConnection); + if (checkDBusError(&error, &mpConnection)) + return; + + /* Grab another reference so that both the destruct and thread each has one: */ + DBusConnection *pForAssert = dbus_connection_ref(mpConnection); + Assert(pForAssert == mpConnection); RT_NOREF(pForAssert); + + /* Create the new worker thread. */ + vrc = RTThreadCreate(&mThread, HostPowerServiceLinux::powerChangeNotificationThread, this, 0 /* cbStack */, + RTTHREADTYPE_MSG_PUMP, RTTHREADFLAGS_WAITABLE, "MainPower"); + if (RT_FAILURE(vrc)) + { + LogRel(("HostPowerServiceLinux: RTThreadCreate failed with %Rrc\n", vrc)); + dbus_connection_unref(mpConnection); + } +} + + +HostPowerServiceLinux::~HostPowerServiceLinux() +{ + /* Closing the connection should cause the event loop to exit. */ + LogFunc((": Stopping thread\n")); + if (mpConnection) + { + dbus_connection_close(mpConnection); + dbus_connection_unref(mpConnection); + mpConnection = NULL; + } + + if (mThread != NIL_RTTHREAD) + { + /* HACK ALERT! This poke call should _not_ be necessary as dbus_connection_close() + should close the socket and force the poll/dbus_connection_read_write + call to return with POLLHUP/FALSE. It does so when stepping it in the + debugger, but not in real life (asan build; dbus-1.12.20-1.fc32; linux 5.8). + + Poking the thread is a crude crude way to wake it up from whatever + stuff it's actually blocked on and realize that the connection has + been dropped. */ + + uint64_t msElapsed = RTTimeMilliTS(); + int vrc = RTThreadWait(mThread, 10 /*ms*/, NULL); + if (RT_FAILURE(vrc)) + { + RTThreadPoke(mThread); + vrc = RTThreadWait(mThread, RT_MS_5SEC, NULL); + } + msElapsed = RTTimeMilliTS() - msElapsed; + if (vrc != VINF_SUCCESS) + LogRelThisFunc(("RTThreadWait() failed after %llu ms: %Rrc\n", msElapsed, vrc)); + mThread = NIL_RTTHREAD; + } +} + + +DECLCALLBACK(int) HostPowerServiceLinux::powerChangeNotificationThread(RTTHREAD hThreadSelf, void *pInstance) +{ + NOREF(hThreadSelf); + HostPowerServiceLinux *pPowerObj = static_cast(pInstance); + DBusConnection *pConnection = pPowerObj->mpConnection; + + Log(("HostPowerServiceLinux: Thread started\n")); + while (dbus_connection_read_write(pConnection, -1)) + { + DBusMessage *pMessage = NULL; + + for (;;) + { + pMessage = dbus_connection_pop_message(pConnection); + if (pMessage == NULL) + break; + + /* The systemd-logind interface notification. */ + DBusMessageIter args; + if ( dbus_message_is_signal(pMessage, "org.freedesktop.login1.Manager", "PrepareForSleep") + && dbus_message_iter_init(pMessage, &args) + && dbus_message_iter_get_arg_type(&args) == DBUS_TYPE_BOOLEAN) + { + dbus_bool_t fSuspend; + dbus_message_iter_get_basic(&args, &fSuspend); + + /* Trinary operator does not work here as Reason_... is an + * anonymous enum. */ + if (fSuspend) + pPowerObj->notify(Reason_HostSuspend); + else + pPowerObj->notify(Reason_HostResume); + } + + /* The UPowerd interface notifications. Sleeping is the older one, + * NotifySleep the newer. This gives us one second grace before the + * suspend triggers. */ + if ( dbus_message_is_signal(pMessage, "org.freedesktop.UPower", "Sleeping") + || dbus_message_is_signal(pMessage, "org.freedesktop.UPower", "NotifySleep")) + pPowerObj->notify(Reason_HostSuspend); + if ( dbus_message_is_signal(pMessage, "org.freedesktop.UPower", "Resuming") + || dbus_message_is_signal(pMessage, "org.freedesktop.UPower", "NotifyResume")) + pPowerObj->notify(Reason_HostResume); + + /* Free local resources held for the message. */ + dbus_message_unref(pMessage); + } + } + + /* Close the socket or whatever underlying the connection. */ + dbus_connection_close(pConnection); + + /* Free in-process resources used for the now-closed connection. */ + dbus_connection_unref(pConnection); + + Log(("HostPowerServiceLinux: Exiting thread\n")); + return VINF_SUCCESS; +} + diff --git a/src/VBox/Main/src-server/linux/Makefile.kup b/src/VBox/Main/src-server/linux/Makefile.kup new file mode 100644 index 00000000..e69de29b diff --git a/src/VBox/Main/src-server/linux/NetIf-linux.cpp b/src/VBox/Main/src-server/linux/NetIf-linux.cpp new file mode 100644 index 00000000..f4a99eeb --- /dev/null +++ b/src/VBox/Main/src-server/linux/NetIf-linux.cpp @@ -0,0 +1,329 @@ +/* $Id: NetIf-linux.cpp $ */ +/** @file + * Main - NetIfList, Linux implementation. + */ + +/* + * Copyright (C) 2008-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_MAIN_HOST + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "HostNetworkInterfaceImpl.h" +#include "netif.h" +#include "LoggingNew.h" + +/** + * Obtain the name of the interface used for default routing. + * + * NOTE: There is a copy in Devices/Network/testcase/tstIntNet-1.cpp. + * + * @returns VBox status code. + * + * @param pszName The buffer where to put the name. + * @param cbName Size of of the destination buffer. + */ +static int getDefaultIfaceName(char *pszName, size_t cbName) +{ + FILE *fp = fopen("/proc/net/route", "r"); + char szBuf[1024]; + char szIfName[17]; + uint32_t uAddr; + uint32_t uGateway; + uint32_t uMask; + int iTmp; + unsigned uFlags; + + if (fp) + { + while (fgets(szBuf, sizeof(szBuf)-1, fp)) + { + int n = sscanf(szBuf, "%16s %x %x %x %d %d %d %x %d %d %d\n", + szIfName, &uAddr, &uGateway, &uFlags, &iTmp, &iTmp, &iTmp, + &uMask, &iTmp, &iTmp, &iTmp); + if (n < 10 || !(uFlags & RTF_UP)) + continue; + + if (uAddr == 0 && uMask == 0) + { + fclose(fp); + szIfName[sizeof(szIfName) - 1] = '\0'; + return RTStrCopy(pszName, cbName, szIfName); + } + } + fclose(fp); + } + return VERR_INTERNAL_ERROR; +} + +static uint32_t getInterfaceSpeed(const char *pszName) +{ + /* + * I wish I could do simple ioctl here, but older kernels require root + * privileges for any ethtool commands. + */ + char szBuf[256]; + uint32_t uSpeed = 0; + /* First, we try to retrieve the speed via sysfs. */ + RTStrPrintf(szBuf, sizeof(szBuf), "/sys/class/net/%s/speed", pszName); + FILE *fp = fopen(szBuf, "r"); + if (fp) + { + if (fscanf(fp, "%u", &uSpeed) != 1) + uSpeed = 0; + fclose(fp); + } + if (uSpeed == 10) + { + /* Check the cable is plugged in at all */ + unsigned uCarrier = 0; + RTStrPrintf(szBuf, sizeof(szBuf), "/sys/class/net/%s/carrier", pszName); + fp = fopen(szBuf, "r"); + if (fp) + { + if (fscanf(fp, "%u", &uCarrier) != 1 || uCarrier == 0) + uSpeed = 0; + fclose(fp); + } + } + + if (uSpeed == 0) + { + /* Failed to get speed via sysfs, go to plan B. */ + int vrc = NetIfAdpCtlOut(pszName, "speed", szBuf, sizeof(szBuf)); + if (RT_SUCCESS(vrc)) + uSpeed = RTStrToUInt32(szBuf); + } + return uSpeed; +} + +static int getInterfaceInfo(int iSocket, const char *pszName, PNETIFINFO pInfo) +{ + // Zeroing out pInfo is a bad idea as it should contain both short and long names at + // this point. So make sure the structure is cleared by the caller if necessary! + // memset(pInfo, 0, sizeof(*pInfo)); + struct ifreq Req; + RT_ZERO(Req); + RTStrCopy(Req.ifr_name, sizeof(Req.ifr_name), pszName); + if (ioctl(iSocket, SIOCGIFHWADDR, &Req) >= 0) + { + switch (Req.ifr_hwaddr.sa_family) + { + case ARPHRD_ETHER: + pInfo->enmMediumType = NETIF_T_ETHERNET; + break; + default: + pInfo->enmMediumType = NETIF_T_UNKNOWN; + break; + } + /* Generate UUID from name and MAC address. */ + RTUUID uuid; + RTUuidClear(&uuid); + memcpy(&uuid, Req.ifr_name, RT_MIN(sizeof(Req.ifr_name), sizeof(uuid))); + uuid.Gen.u8ClockSeqHiAndReserved = (uint8_t)((uuid.Gen.u8ClockSeqHiAndReserved & 0x3f) | 0x80); + uuid.Gen.u16TimeHiAndVersion = (uint16_t)((uuid.Gen.u16TimeHiAndVersion & 0x0fff) | 0x4000); + memcpy(uuid.Gen.au8Node, &Req.ifr_hwaddr.sa_data, sizeof(uuid.Gen.au8Node)); + pInfo->Uuid = uuid; + + memcpy(&pInfo->MACAddress, Req.ifr_hwaddr.sa_data, sizeof(pInfo->MACAddress)); + + if (ioctl(iSocket, SIOCGIFADDR, &Req) >= 0) + memcpy(pInfo->IPAddress.au8, + &((struct sockaddr_in *)&Req.ifr_addr)->sin_addr.s_addr, + sizeof(pInfo->IPAddress.au8)); + + if (ioctl(iSocket, SIOCGIFNETMASK, &Req) >= 0) + memcpy(pInfo->IPNetMask.au8, + &((struct sockaddr_in *)&Req.ifr_addr)->sin_addr.s_addr, + sizeof(pInfo->IPNetMask.au8)); + + if (ioctl(iSocket, SIOCGIFFLAGS, &Req) >= 0) + pInfo->enmStatus = Req.ifr_flags & IFF_UP ? NETIF_S_UP : NETIF_S_DOWN; + + struct iwreq WRq; + RT_ZERO(WRq); + RTStrCopy(WRq.ifr_name, sizeof(WRq.ifr_name), pszName); + pInfo->fWireless = ioctl(iSocket, SIOCGIWNAME, &WRq) >= 0; + + FILE *fp = fopen("/proc/net/if_inet6", "r"); + if (fp) + { + RTNETADDRIPV6 IPv6Address; + unsigned uIndex, uLength, uScope, uTmp; + char szName[30]; + for (;;) + { + RT_ZERO(szName); + int n = fscanf(fp, + "%08x%08x%08x%08x" + " %02x %02x %02x %02x %20s\n", + &IPv6Address.au32[0], &IPv6Address.au32[1], + &IPv6Address.au32[2], &IPv6Address.au32[3], + &uIndex, &uLength, &uScope, &uTmp, szName); + if (n == EOF) + break; + if (n != 9 || uLength > 128) + { + Log(("getInterfaceInfo: Error while reading /proc/net/if_inet6, n=%d uLength=%u\n", + n, uLength)); + break; + } + if (!strcmp(Req.ifr_name, szName)) + { + pInfo->IPv6Address.au32[0] = htonl(IPv6Address.au32[0]); + pInfo->IPv6Address.au32[1] = htonl(IPv6Address.au32[1]); + pInfo->IPv6Address.au32[2] = htonl(IPv6Address.au32[2]); + pInfo->IPv6Address.au32[3] = htonl(IPv6Address.au32[3]); + RTNetPrefixToMaskIPv6(uLength, &pInfo->IPv6NetMask); + } + } + fclose(fp); + } + /* + * Don't even try to get speed for non-Ethernet interfaces, it only + * produces errors. + */ + if (pInfo->enmMediumType == NETIF_T_ETHERNET) + pInfo->uSpeedMbits = getInterfaceSpeed(pszName); + else + pInfo->uSpeedMbits = 0; + } + return VINF_SUCCESS; +} + +int NetIfList(std::list > &list) +{ + char szDefaultIface[256]; + int vrc = getDefaultIfaceName(szDefaultIface, sizeof(szDefaultIface)); + if (RT_FAILURE(vrc)) + { + Log(("NetIfList: Failed to find default interface.\n")); + szDefaultIface[0] = '\0'; + } + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock >= 0) + { + FILE *fp = fopen("/proc/net/dev", "r"); + if (fp) + { + char buf[256]; + while (fgets(buf, sizeof(buf), fp)) + { + char *pszEndOfName = strchr(buf, ':'); + if (!pszEndOfName) + continue; + *pszEndOfName = 0; + size_t iFirstNonWS = strspn(buf, " "); + char *pszName = buf + iFirstNonWS; + NETIFINFO Info; + RT_ZERO(Info); + vrc = getInterfaceInfo(sock, pszName, &Info); + if (RT_FAILURE(vrc)) + break; + if (Info.enmMediumType == NETIF_T_ETHERNET) + { + ComObjPtr IfObj; + IfObj.createObject(); + + HostNetworkInterfaceType_T enmType; + if (strncmp(pszName, RT_STR_TUPLE("vboxnet"))) + enmType = HostNetworkInterfaceType_Bridged; + else + enmType = HostNetworkInterfaceType_HostOnly; + + if (SUCCEEDED(IfObj->init(pszName, enmType, &Info))) + { + if (strcmp(pszName, szDefaultIface) == 0) + list.push_front(IfObj); + else + list.push_back(IfObj); + } + } + + } + fclose(fp); + } + close(sock); + } + else + vrc = VERR_INTERNAL_ERROR; + + return vrc; +} + +int NetIfGetConfigByName(PNETIFINFO pInfo) +{ + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) + return VERR_NOT_IMPLEMENTED; + int vrc = getInterfaceInfo(sock, pInfo->szShortName, pInfo); + close(sock); + return vrc; +} + +/** + * Retrieve the physical link speed in megabits per second. If the interface is + * not up or otherwise unavailable the zero speed is returned. + * + * @returns VBox status code. + * + * @param pcszIfName Interface name. + * @param puMbits Where to store the link speed. + */ +int NetIfGetLinkSpeed(const char *pcszIfName, uint32_t *puMbits) +{ + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) + return VERR_OUT_OF_RESOURCES; + struct ifreq Req; + RT_ZERO(Req); + RTStrCopy(Req.ifr_name, sizeof(Req.ifr_name), pcszIfName); + if (ioctl(sock, SIOCGIFHWADDR, &Req) >= 0) + { + if (ioctl(sock, SIOCGIFFLAGS, &Req) >= 0) + if (Req.ifr_flags & IFF_UP) + { + close(sock); + *puMbits = getInterfaceSpeed(pcszIfName); + return VINF_SUCCESS; + } + } + close(sock); + *puMbits = 0; + return VWRN_NOT_FOUND; +} diff --git a/src/VBox/Main/src-server/linux/PerformanceLinux.cpp b/src/VBox/Main/src-server/linux/PerformanceLinux.cpp new file mode 100644 index 00000000..0d6441cb --- /dev/null +++ b/src/VBox/Main/src-server/linux/PerformanceLinux.cpp @@ -0,0 +1,609 @@ +/* $Id: PerformanceLinux.cpp $ */ +/** @file + * VBox Linux-specific Performance Classes implementation. + */ + +/* + * Copyright (C) 2008-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#define LOG_GROUP LOG_GROUP_MAIN_PERFORMANCECOLLECTOR +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "LoggingNew.h" +#include "Performance.h" + +#define VBOXVOLINFO_NAME "VBoxVolInfo" + +namespace pm { + +class CollectorLinux : public CollectorHAL +{ +public: + CollectorLinux(); + virtual int preCollect(const CollectorHints& hints, uint64_t /* iTick */); + virtual int getHostMemoryUsage(ULONG *total, ULONG *used, ULONG *available); + virtual int getHostFilesystemUsage(const char *name, ULONG *total, ULONG *used, ULONG *available); + virtual int getHostDiskSize(const char *name, uint64_t *size); + virtual int getProcessMemoryUsage(RTPROCESS process, ULONG *used); + + virtual int getRawHostCpuLoad(uint64_t *user, uint64_t *kernel, uint64_t *idle); + virtual int getRawHostNetworkLoad(const char *name, uint64_t *rx, uint64_t *tx); + virtual int getRawHostDiskLoad(const char *name, uint64_t *disk_ms, uint64_t *total_ms); + virtual int getRawProcessCpuLoad(RTPROCESS process, uint64_t *user, uint64_t *kernel, uint64_t *total); + + virtual int getDiskListByFs(const char *name, DiskList& listUsage, DiskList& listLoad); +private: + virtual int _getRawHostCpuLoad(); + int getRawProcessStats(RTPROCESS process, uint64_t *cpuUser, uint64_t *cpuKernel, ULONG *memPagesUsed); + void getDiskName(char *pszDiskName, size_t cbDiskName, const char *pszDevName, bool fTrimDigits); + void addVolumeDependencies(const char *pcszVolume, DiskList& listDisks); + void addRaidDisks(const char *pcszDevice, DiskList& listDisks); + char *trimTrailingDigits(char *pszName); + char *trimNewline(char *pszName); + + struct VMProcessStats + { + uint64_t cpuUser; + uint64_t cpuKernel; + ULONG pagesUsed; + }; + + typedef std::map VMProcessMap; + + VMProcessMap mProcessStats; + uint64_t mUser, mKernel, mIdle; + uint64_t mSingleUser, mSingleKernel, mSingleIdle; + uint32_t mHZ; + ULONG mTotalRAM; +}; + +CollectorHAL *createHAL() +{ + return new CollectorLinux(); +} + +// Collector HAL for Linux + +CollectorLinux::CollectorLinux() +{ + long hz = sysconf(_SC_CLK_TCK); + if (hz == -1) + { + LogRel(("CollectorLinux failed to obtain HZ from kernel, assuming 100.\n")); + mHZ = 100; + } + else + mHZ = (uint32_t)hz; + LogFlowThisFunc(("mHZ=%u\n", mHZ)); + + uint64_t cb; + int vrc = RTSystemQueryTotalRam(&cb); + if (RT_FAILURE(vrc)) + mTotalRAM = 0; + else + mTotalRAM = (ULONG)(cb / 1024); +} + +int CollectorLinux::preCollect(const CollectorHints& hints, uint64_t /* iTick */) +{ + std::vector processes; + hints.getProcesses(processes); + + std::vector::iterator it; + for (it = processes.begin(); it != processes.end(); ++it) + { + VMProcessStats vmStats; + int vrc = getRawProcessStats(*it, &vmStats.cpuUser, &vmStats.cpuKernel, &vmStats.pagesUsed); + /* On failure, do NOT stop. Just skip the entry. Having the stats for + * one (probably broken) process frozen/zero is a minor issue compared + * to not updating many process stats and the host cpu stats. */ + if (RT_SUCCESS(vrc)) + mProcessStats[*it] = vmStats; + } + if (hints.isHostCpuLoadCollected() || !mProcessStats.empty()) + { + _getRawHostCpuLoad(); + } + return VINF_SUCCESS; +} + +int CollectorLinux::_getRawHostCpuLoad() +{ + int vrc = VINF_SUCCESS; + long long unsigned uUser, uNice, uKernel, uIdle, uIowait, uIrq, uSoftirq; + FILE *f = fopen("/proc/stat", "r"); + + if (f) + { + char szBuf[128]; + if (fgets(szBuf, sizeof(szBuf), f)) + { + if (sscanf(szBuf, "cpu %llu %llu %llu %llu %llu %llu %llu", + &uUser, &uNice, &uKernel, &uIdle, &uIowait, + &uIrq, &uSoftirq) == 7) + { + mUser = uUser + uNice; + mKernel = uKernel + uIrq + uSoftirq; + mIdle = uIdle + uIowait; + } + /* Try to get single CPU stats. */ + if (fgets(szBuf, sizeof(szBuf), f)) + { + if (sscanf(szBuf, "cpu0 %llu %llu %llu %llu %llu %llu %llu", + &uUser, &uNice, &uKernel, &uIdle, &uIowait, + &uIrq, &uSoftirq) == 7) + { + mSingleUser = uUser + uNice; + mSingleKernel = uKernel + uIrq + uSoftirq; + mSingleIdle = uIdle + uIowait; + } + else + { + /* Assume that this is not an SMP system. */ + Assert(RTMpGetCount() == 1); + mSingleUser = mUser; + mSingleKernel = mKernel; + mSingleIdle = mIdle; + } + } + else + vrc = VERR_FILE_IO_ERROR; + } + else + vrc = VERR_FILE_IO_ERROR; + fclose(f); + } + else + vrc = VERR_ACCESS_DENIED; + + return vrc; +} + +int CollectorLinux::getRawHostCpuLoad(uint64_t *user, uint64_t *kernel, uint64_t *idle) +{ + *user = mUser; + *kernel = mKernel; + *idle = mIdle; + return VINF_SUCCESS; +} + +int CollectorLinux::getRawProcessCpuLoad(RTPROCESS process, uint64_t *user, uint64_t *kernel, uint64_t *total) +{ + VMProcessMap::const_iterator it = mProcessStats.find(process); + + if (it == mProcessStats.end()) + { + Log (("No stats pre-collected for process %x\n", process)); + return VERR_INTERNAL_ERROR; + } + *user = it->second.cpuUser; + *kernel = it->second.cpuKernel; + *total = mUser + mKernel + mIdle; + return VINF_SUCCESS; +} + +int CollectorLinux::getHostMemoryUsage(ULONG *total, ULONG *used, ULONG *available) +{ + AssertReturn(mTotalRAM, VERR_INTERNAL_ERROR); + uint64_t cb; + int vrc = RTSystemQueryAvailableRam(&cb); + if (RT_SUCCESS(vrc)) + { + *total = mTotalRAM; + *available = (ULONG)(cb / 1024); + *used = *total - *available; + } + return vrc; +} + +int CollectorLinux::getHostFilesystemUsage(const char *path, ULONG *total, ULONG *used, ULONG *available) +{ + struct statvfs stats; + + if (statvfs(path, &stats) == -1) + { + LogRel(("Failed to collect %s filesystem usage: errno=%d.\n", path, errno)); + return VERR_ACCESS_DENIED; + } + uint64_t cbBlock = stats.f_frsize ? stats.f_frsize : stats.f_bsize; + *total = (ULONG)(cbBlock * stats.f_blocks / _1M); + *used = (ULONG)(cbBlock * (stats.f_blocks - stats.f_bfree) / _1M); + *available = (ULONG)(cbBlock * stats.f_bavail / _1M); + + return VINF_SUCCESS; +} + +int CollectorLinux::getHostDiskSize(const char *pszFile, uint64_t *size) +{ + char *pszPath = NULL; + + RTStrAPrintf(&pszPath, "/sys/block/%s/size", pszFile); + Assert(pszPath); + + int vrc = VINF_SUCCESS; + if (!RTLinuxSysFsExists(pszPath)) + vrc = VERR_FILE_NOT_FOUND; + else + { + int64_t cSize = 0; + vrc = RTLinuxSysFsReadIntFile(0, &cSize, pszPath); + if (RT_SUCCESS(vrc)) + *size = cSize * 512; + } + RTStrFree(pszPath); + return vrc; +} + +int CollectorLinux::getProcessMemoryUsage(RTPROCESS process, ULONG *used) +{ + VMProcessMap::const_iterator it = mProcessStats.find(process); + + if (it == mProcessStats.end()) + { + Log (("No stats pre-collected for process %x\n", process)); + return VERR_INTERNAL_ERROR; + } + *used = it->second.pagesUsed * (PAGE_SIZE / 1024); + return VINF_SUCCESS; +} + +int CollectorLinux::getRawProcessStats(RTPROCESS process, uint64_t *cpuUser, uint64_t *cpuKernel, ULONG *memPagesUsed) +{ + int vrc = VINF_SUCCESS; + char *pszName; + pid_t pid2; + char c; + int iTmp; + long long unsigned int u64Tmp; + unsigned uTmp; + unsigned long ulTmp; + signed long ilTmp; + ULONG u32user, u32kernel; + char buf[80]; /** @todo this should be tied to max allowed proc name. */ + + RTStrAPrintf(&pszName, "/proc/%d/stat", process); + FILE *f = fopen(pszName, "r"); + RTStrFree(pszName); + + if (f) + { + if (fscanf(f, "%d %79s %c %d %d %d %d %d %u %lu %lu %lu %lu %u %u " + "%ld %ld %ld %ld %ld %ld %llu %lu %u", + &pid2, buf, &c, &iTmp, &iTmp, &iTmp, &iTmp, &iTmp, &uTmp, + &ulTmp, &ulTmp, &ulTmp, &ulTmp, &u32user, &u32kernel, + &ilTmp, &ilTmp, &ilTmp, &ilTmp, &ilTmp, &ilTmp, &u64Tmp, + &ulTmp, memPagesUsed) == 24) + { + Assert((pid_t)process == pid2); + *cpuUser = u32user; + *cpuKernel = u32kernel; + } + else + vrc = VERR_FILE_IO_ERROR; + fclose(f); + } + else + vrc = VERR_ACCESS_DENIED; + + return vrc; +} + +int CollectorLinux::getRawHostNetworkLoad(const char *pszFile, uint64_t *rx, uint64_t *tx) +{ + char szIfName[/*IFNAMSIZ*/ 16 + 36]; + + RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/net/%s/statistics/rx_bytes", pszFile); + if (!RTLinuxSysFsExists(szIfName)) + return VERR_FILE_NOT_FOUND; + + int64_t cSize = 0; + int vrc = RTLinuxSysFsReadIntFile(0, &cSize, szIfName); + if (RT_FAILURE(vrc)) + return vrc; + + *rx = cSize; + + RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/net/%s/statistics/tx_bytes", pszFile); + if (!RTLinuxSysFsExists(szIfName)) + return VERR_FILE_NOT_FOUND; + + vrc = RTLinuxSysFsReadIntFile(0, &cSize, szIfName); + if (RT_FAILURE(vrc)) + return vrc; + + *tx = cSize; + return VINF_SUCCESS; +} + +int CollectorLinux::getRawHostDiskLoad(const char *name, uint64_t *disk_ms, uint64_t *total_ms) +{ +#if 0 + int vrc = VINF_SUCCESS; + char szIfName[/*IFNAMSIZ*/ 16 + 36]; + long long unsigned int u64Busy, tmp; + + RTStrPrintf(szIfName, sizeof(szIfName), "/sys/class/block/%s/stat", name); + FILE *f = fopen(szIfName, "r"); + if (f) + { + if (fscanf(f, "%llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu", + &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &u64Busy, &tmp) == 11) + { + *disk_ms = u64Busy; + *total_ms = (uint64_t)(mSingleUser + mSingleKernel + mSingleIdle) * 1000 / mHZ; + } + else + vrc = VERR_FILE_IO_ERROR; + fclose(f); + } + else + vrc = VERR_ACCESS_DENIED; +#else + int vrc = VERR_MISSING; + FILE *f = fopen("/proc/diskstats", "r"); + if (f) + { + char szBuf[128]; + while (fgets(szBuf, sizeof(szBuf), f)) + { + char *pszBufName = szBuf; + while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */ + while (RT_C_IS_DIGIT(*pszBufName)) ++pszBufName; /* Skip major */ + while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */ + while (RT_C_IS_DIGIT(*pszBufName)) ++pszBufName; /* Skip minor */ + while (*pszBufName == ' ') ++pszBufName; /* Skip spaces */ + + char *pszBufData = strchr(pszBufName, ' '); + if (!pszBufData) + { + LogRel(("CollectorLinux::getRawHostDiskLoad() failed to parse disk stats: %s\n", szBuf)); + continue; + } + *pszBufData++ = '\0'; + if (!strcmp(name, pszBufName)) + { + long long unsigned int u64Busy, tmp; + + if (sscanf(pszBufData, "%llu %llu %llu %llu %llu %llu %llu %llu %llu %llu %llu", + &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &u64Busy, &tmp) == 11) + { + *disk_ms = u64Busy; + *total_ms = (uint64_t)(mSingleUser + mSingleKernel + mSingleIdle) * 1000 / mHZ; + vrc = VINF_SUCCESS; + } + else + vrc = VERR_FILE_IO_ERROR; + break; + } + } + fclose(f); + } +#endif + + return vrc; +} + +char *CollectorLinux::trimNewline(char *pszName) +{ + size_t cbName = strlen(pszName); + if (cbName == 0) + return pszName; + + char *pszEnd = pszName + cbName - 1; + while (pszEnd > pszName && *pszEnd == '\n') + pszEnd--; + pszEnd[1] = '\0'; + + return pszName; +} + +char *CollectorLinux::trimTrailingDigits(char *pszName) +{ + size_t cbName = strlen(pszName); + if (cbName == 0) + return pszName; + + char *pszEnd = pszName + cbName - 1; + while (pszEnd > pszName && (RT_C_IS_DIGIT(*pszEnd) || *pszEnd == '\n')) + pszEnd--; + pszEnd[1] = '\0'; + + return pszName; +} + +/** + * Use the partition name to get the name of the disk. Any path component is stripped. + * if fTrimDigits is true, trailing digits are stripped as well, for example '/dev/sda5' + * is converted to 'sda'. + * + * @param pszDiskName Where to store the name of the disk. + * @param cbDiskName The size of the buffer pszDiskName points to. + * @param pszDevName The device name used to get the disk name. + * @param fTrimDigits Trim trailing digits (e.g. /dev/sda5) + */ +void CollectorLinux::getDiskName(char *pszDiskName, size_t cbDiskName, const char *pszDevName, bool fTrimDigits) +{ + unsigned cbName = 0; + size_t cbDevName = strlen(pszDevName); + const char *pszEnd = pszDevName + cbDevName - 1; + if (fTrimDigits) + while (pszEnd > pszDevName && RT_C_IS_DIGIT(*pszEnd)) + pszEnd--; + while (pszEnd > pszDevName && *pszEnd != '/') + { + cbName++; + pszEnd--; + } + RTStrCopy(pszDiskName, RT_MIN(cbName + 1, cbDiskName), pszEnd + 1); +} + +void CollectorLinux::addRaidDisks(const char *pcszDevice, DiskList& listDisks) +{ + FILE *f = fopen("/proc/mdstat", "r"); + if (f) + { + char szBuf[128]; + while (fgets(szBuf, sizeof(szBuf), f)) + { + char *pszBufName = szBuf; + + char *pszBufData = strchr(pszBufName, ' '); + if (!pszBufData) + { + LogRel(("CollectorLinux::addRaidDisks() failed to parse disk stats: %s\n", szBuf)); + continue; + } + *pszBufData++ = '\0'; + if (!strcmp(pcszDevice, pszBufName)) + { + while (*pszBufData == ':') ++pszBufData; /* Skip delimiter */ + while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */ + while (RT_C_IS_ALNUM(*pszBufData)) ++pszBufData; /* Skip status */ + while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */ + while (RT_C_IS_ALNUM(*pszBufData)) ++pszBufData; /* Skip type */ + + while (*pszBufData != '\0') + { + while (*pszBufData == ' ') ++pszBufData; /* Skip spaces */ + char *pszDisk = pszBufData; + while (RT_C_IS_ALPHA(*pszBufData)) + ++pszBufData; + if (*pszBufData) + { + *pszBufData++ = '\0'; + listDisks.push_back(RTCString(pszDisk)); + while (*pszBufData != '\0' && *pszBufData != ' ') + ++pszBufData; + } + else + listDisks.push_back(RTCString(pszDisk)); + } + break; + } + } + fclose(f); + } +} + +void CollectorLinux::addVolumeDependencies(const char *pcszVolume, DiskList& listDisks) +{ + char szVolInfo[RTPATH_MAX]; + int vrc = RTPathAppPrivateArch(szVolInfo, sizeof(szVolInfo) - sizeof("/" VBOXVOLINFO_NAME " ") - strlen(pcszVolume)); + if (RT_FAILURE(vrc)) + { + LogRel(("VolInfo: Failed to get program path, vrc=%Rrc\n", vrc)); + return; + } + strcat(szVolInfo, "/" VBOXVOLINFO_NAME " "); + strcat(szVolInfo, pcszVolume); + + FILE *fp = popen(szVolInfo, "r"); + if (fp) + { + char szBuf[128]; + + while (fgets(szBuf, sizeof(szBuf), fp)) + if (strncmp(szBuf, RT_STR_TUPLE("dm-"))) + listDisks.push_back(RTCString(trimTrailingDigits(szBuf))); + else + listDisks.push_back(RTCString(trimNewline(szBuf))); + + pclose(fp); + } + else + listDisks.push_back(RTCString(pcszVolume)); +} + +int CollectorLinux::getDiskListByFs(const char *pszPath, DiskList& listUsage, DiskList& listLoad) +{ + FILE *mtab = setmntent("/etc/mtab", "r"); + if (mtab) + { + struct mntent *mntent; + while ((mntent = getmntent(mtab))) + { + /* Skip rootfs entry, there must be another root mount. */ + if (strcmp(mntent->mnt_fsname, "rootfs") == 0) + continue; + if (strcmp(pszPath, mntent->mnt_dir) == 0) + { + char szDevName[128]; + char szFsName[1024]; + /* Try to resolve symbolic link if necessary. Yes, we access the file system here! */ + int vrc = RTPathReal(mntent->mnt_fsname, szFsName, sizeof(szFsName)); + if (RT_FAILURE(vrc)) + continue; /* something got wrong, just ignore this path */ + /* check against the actual mtab entry, NOT the real path as /dev/mapper/xyz is + * often a symlink to something else */ + if (!strncmp(mntent->mnt_fsname, RT_STR_TUPLE("/dev/mapper"))) + { + /* LVM */ + getDiskName(szDevName, sizeof(szDevName), mntent->mnt_fsname, false /*=fTrimDigits*/); + addVolumeDependencies(szDevName, listUsage); + listLoad = listUsage; + } + else if (!strncmp(szFsName, RT_STR_TUPLE("/dev/md"))) + { + /* Software RAID */ + getDiskName(szDevName, sizeof(szDevName), szFsName, false /*=fTrimDigits*/); + listUsage.push_back(RTCString(szDevName)); + addRaidDisks(szDevName, listLoad); + } + else + { + /* Plain disk partition. Trim the trailing digits to get the drive name */ + getDiskName(szDevName, sizeof(szDevName), szFsName, true /*=fTrimDigits*/); + listUsage.push_back(RTCString(szDevName)); + listLoad.push_back(RTCString(szDevName)); + } + if (listUsage.empty() || listLoad.empty()) + { + LogRel(("Failed to retrive disk info: getDiskName(%s) --> %s\n", + mntent->mnt_fsname, szDevName)); + } + break; + } + } + endmntent(mtab); + } + return VINF_SUCCESS; +} + +} + diff --git a/src/VBox/Main/src-server/linux/USBGetDevices.cpp b/src/VBox/Main/src-server/linux/USBGetDevices.cpp new file mode 100644 index 00000000..4e0c1bb7 --- /dev/null +++ b/src/VBox/Main/src-server/linux/USBGetDevices.cpp @@ -0,0 +1,1795 @@ +/* $Id: USBGetDevices.cpp $ */ +/** @file + * VirtualBox Linux host USB device enumeration. + */ + +/* + * Copyright (C) 2006-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define VBOX_USB_WITH_USBFS +#include "USBGetDevices.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "vector.h" + +#ifdef VBOX_WITH_LINUX_COMPILER_H +# include +#endif +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ +/** Structure describing a host USB device */ +typedef struct USBDeviceInfo +{ + /** The device node of the device. */ + char *mDevice; + /** The system identifier of the device. Specific to the probing + * method. */ + char *mSysfsPath; + /** List of interfaces as sysfs paths */ + VECTOR_PTR(char *) mvecpszInterfaces; +} USBDeviceInfo; + + +/** + * Does some extra checks to improve the detected device state. + * + * We cannot distinguish between USED_BY_HOST_CAPTURABLE and + * USED_BY_GUEST, HELD_BY_PROXY all that well and it shouldn't be + * necessary either. + * + * We will however, distinguish between the device we have permissions + * to open and those we don't. This is necessary for two reasons. + * + * Firstly, because it's futile to even attempt opening a device which we + * don't have access to, it only serves to confuse the user. (That said, + * it might also be a bit confusing for the user to see that a USB device + * is grayed out with no further explanation, and no way of generating an + * error hinting at why this is the case.) + * + * Secondly and more importantly, we're racing against udevd with respect + * to permissions and group settings on newly plugged devices. When we + * detect a new device that we cannot access we will poll on it for a few + * seconds to give udevd time to fix it. The polling is actually triggered + * in the 'new device' case in the compare loop. + * + * The USBDEVICESTATE_USED_BY_HOST state is only used for this no-access + * case, while USBDEVICESTATE_UNSUPPORTED is only used in the 'hub' case. + * When it's neither of these, we set USBDEVICESTATE_UNUSED or + * USBDEVICESTATE_USED_BY_HOST_CAPTURABLE depending on whether there is + * a driver associated with any of the interfaces. + * + * All except the access check and a special idVendor == 0 precaution + * is handled at parse time. + * + * @returns The adjusted state. + * @param pDevice The device. + */ +static USBDEVICESTATE usbDeterminState(PCUSBDEVICE pDevice) +{ + /* + * If it's already flagged as unsupported, there is nothing to do. + */ + USBDEVICESTATE enmState = pDevice->enmState; + if (enmState == USBDEVICESTATE_UNSUPPORTED) + return USBDEVICESTATE_UNSUPPORTED; + + /* + * Root hubs and similar doesn't have any vendor id, just + * refuse these device. + */ + if (!pDevice->idVendor) + return USBDEVICESTATE_UNSUPPORTED; + + /* + * Check if we've got access to the device, if we haven't flag + * it as used-by-host. + */ +#ifndef VBOX_USB_WITH_SYSFS + const char *pszAddress = pDevice->pszAddress; +#else + if (pDevice->pszAddress == NULL) + /* We can't do much with the device without an address. */ + return USBDEVICESTATE_UNSUPPORTED; + const char *pszAddress = strstr(pDevice->pszAddress, "//device:"); + pszAddress = pszAddress != NULL + ? pszAddress + sizeof("//device:") - 1 + : pDevice->pszAddress; +#endif + if ( access(pszAddress, R_OK | W_OK) != 0 + && errno == EACCES) + return USBDEVICESTATE_USED_BY_HOST; + +#ifdef VBOX_USB_WITH_SYSFS + /** + * @todo Check that any other essential fields are present and mark as + * invalid if not. Particularly to catch the case where the device was + * unplugged while we were reading in its properties. + */ +#endif + + return enmState; +} + + +/** + * Dumps a USBDEVICE structure to the log using LogLevel 3. + * @param pDev The structure to log. + * @todo This is really common code. + */ +static void usbLogDevice(PUSBDEVICE pDev) +{ + NOREF(pDev); + if (LogIs3Enabled()) + { + Log3(("USB device:\n")); + Log3(("Product: %s (%x)\n", pDev->pszProduct, pDev->idProduct)); + Log3(("Manufacturer: %s (Vendor ID %x)\n", pDev->pszManufacturer, pDev->idVendor)); + Log3(("Serial number: %s (%llx)\n", pDev->pszSerialNumber, pDev->u64SerialHash)); + Log3(("Device revision: %d\n", pDev->bcdDevice)); + Log3(("Device class: %x\n", pDev->bDeviceClass)); + Log3(("Device subclass: %x\n", pDev->bDeviceSubClass)); + Log3(("Device protocol: %x\n", pDev->bDeviceProtocol)); + Log3(("USB version number: %d\n", pDev->bcdUSB)); + Log3(("Device speed: %s\n", + pDev->enmSpeed == USBDEVICESPEED_UNKNOWN ? "unknown" + : pDev->enmSpeed == USBDEVICESPEED_LOW ? "1.5 MBit/s" + : pDev->enmSpeed == USBDEVICESPEED_FULL ? "12 MBit/s" + : pDev->enmSpeed == USBDEVICESPEED_HIGH ? "480 MBit/s" + : pDev->enmSpeed == USBDEVICESPEED_SUPER ? "5.0 GBit/s" + : pDev->enmSpeed == USBDEVICESPEED_VARIABLE ? "variable" + : "invalid")); + Log3(("Number of configurations: %d\n", pDev->bNumConfigurations)); + Log3(("Bus number: %d\n", pDev->bBus)); + Log3(("Port number: %d\n", pDev->bPort)); + Log3(("Device number: %d\n", pDev->bDevNum)); + Log3(("Device state: %s\n", + pDev->enmState == USBDEVICESTATE_UNSUPPORTED ? "unsupported" + : pDev->enmState == USBDEVICESTATE_USED_BY_HOST ? "in use by host" + : pDev->enmState == USBDEVICESTATE_USED_BY_HOST_CAPTURABLE ? "in use by host, possibly capturable" + : pDev->enmState == USBDEVICESTATE_UNUSED ? "not in use" + : pDev->enmState == USBDEVICESTATE_HELD_BY_PROXY ? "held by proxy" + : pDev->enmState == USBDEVICESTATE_USED_BY_GUEST ? "used by guest" + : "invalid")); + Log3(("OS device address: %s\n", pDev->pszAddress)); + } +} + + +#ifdef VBOX_USB_WITH_USBFS + +/** + * "reads" the number suffix. + * + * It's more like validating it and skipping the necessary number of chars. + */ +static int usbfsReadSkipSuffix(char **ppszNext) +{ + char *pszNext = *ppszNext; + if (!RT_C_IS_SPACE(*pszNext) && *pszNext) + { + /* skip unit */ + if (pszNext[0] == 'm' && pszNext[1] == 's') + pszNext += 2; + else if (pszNext[0] == 'm' && pszNext[1] == 'A') + pszNext += 2; + + /* skip parenthesis */ + if (*pszNext == '(') + { + pszNext = strchr(pszNext, ')'); + if (!pszNext++) + { + AssertMsgFailed(("*ppszNext=%s\n", *ppszNext)); + return VERR_PARSE_ERROR; + } + } + + /* blank or end of the line. */ + if (!RT_C_IS_SPACE(*pszNext) && *pszNext) + { + AssertMsgFailed(("pszNext=%s\n", pszNext)); + return VERR_PARSE_ERROR; + } + + /* it's ok. */ + *ppszNext = pszNext; + } + + return VINF_SUCCESS; +} + + +/** + * Reads a USB number returning the number and the position of the next character to parse. + */ +static int usbfsReadNum(const char *pszValue, unsigned uBase, uint32_t u32Mask, void *pvNum, char **ppszNext) +{ + /* + * Initialize return value to zero and strip leading spaces. + */ + switch (u32Mask) + { + case 0xff: *(uint8_t *)pvNum = 0; break; + case 0xffff: *(uint16_t *)pvNum = 0; break; + case 0xffffffff: *(uint32_t *)pvNum = 0; break; + } + pszValue = RTStrStripL(pszValue); + if (*pszValue) + { + /* + * Try convert the number. + */ + char *pszNext; + uint32_t u32 = 0; + RTStrToUInt32Ex(pszValue, &pszNext, uBase, &u32); + if (pszNext == pszValue) + { + AssertMsgFailed(("pszValue=%d\n", pszValue)); + return VERR_NO_DATA; + } + + /* + * Check the range. + */ + if (u32 & ~u32Mask) + { + AssertMsgFailed(("pszValue=%d u32=%#x lMask=%#x\n", pszValue, u32, u32Mask)); + return VERR_OUT_OF_RANGE; + } + + int vrc = usbfsReadSkipSuffix(&pszNext); + if (RT_FAILURE(vrc)) + return vrc; + + *ppszNext = pszNext; + + /* + * Set the value. + */ + switch (u32Mask) + { + case 0xff: *(uint8_t *)pvNum = (uint8_t)u32; break; + case 0xffff: *(uint16_t *)pvNum = (uint16_t)u32; break; + case 0xffffffff: *(uint32_t *)pvNum = (uint32_t)u32; break; + } + } + return VINF_SUCCESS; +} + + +static int usbfsRead8(const char *pszValue, unsigned uBase, uint8_t *pu8, char **ppszNext) +{ + return usbfsReadNum(pszValue, uBase, 0xff, pu8, ppszNext); +} + + +static int usbfsRead16(const char *pszValue, unsigned uBase, uint16_t *pu16, char **ppszNext) +{ + return usbfsReadNum(pszValue, uBase, 0xffff, pu16, ppszNext); +} + + +/** + * Reads a USB BCD number returning the number and the position of the next character to parse. + * The returned number contains the integer part in the high byte and the decimal part in the low byte. + */ +static int usbfsReadBCD(const char *pszValue, unsigned uBase, uint16_t *pu16, char **ppszNext) +{ + /* + * Initialize return value to zero and strip leading spaces. + */ + *pu16 = 0; + pszValue = RTStrStripL(pszValue); + if (*pszValue) + { + /* + * Try convert the number. + */ + /* integer part */ + char *pszNext; + uint32_t u32Int = 0; + RTStrToUInt32Ex(pszValue, &pszNext, uBase, &u32Int); + if (pszNext == pszValue) + { + AssertMsgFailed(("pszValue=%s\n", pszValue)); + return VERR_NO_DATA; + } + if (u32Int & ~0xff) + { + AssertMsgFailed(("pszValue=%s u32Int=%#x (int)\n", pszValue, u32Int)); + return VERR_OUT_OF_RANGE; + } + + /* skip dot and read decimal part */ + if (*pszNext != '.') + { + AssertMsgFailed(("pszValue=%s pszNext=%s (int)\n", pszValue, pszNext)); + return VERR_PARSE_ERROR; + } + char *pszValue2 = RTStrStripL(pszNext + 1); + uint32_t u32Dec = 0; + RTStrToUInt32Ex(pszValue2, &pszNext, uBase, &u32Dec); + if (pszNext == pszValue) + { + AssertMsgFailed(("pszValue=%s\n", pszValue)); + return VERR_NO_DATA; + } + if (u32Dec & ~0xff) + { + AssertMsgFailed(("pszValue=%s u32Dec=%#x\n", pszValue, u32Dec)); + return VERR_OUT_OF_RANGE; + } + + /* + * Validate and skip stuff following the number. + */ + int vrc = usbfsReadSkipSuffix(&pszNext); + if (RT_FAILURE(vrc)) + return vrc; + *ppszNext = pszNext; + + /* + * Set the value. + */ + *pu16 = (uint16_t)((u32Int << 8) | (uint16_t)u32Dec); + } + return VINF_SUCCESS; +} + + +/** + * Reads a string, i.e. allocates memory and copies it. + * + * We assume that a string is Utf8 and if that's not the case + * (pre-2.6.32-kernels used Latin-1, but so few devices return non-ASCII that + * this usually goes unnoticed) then we mercilessly force it to be so. + */ +static int usbfsReadStr(const char *pszValue, const char **ppsz) +{ + char *psz; + + if (*ppsz) + RTStrFree((char *)*ppsz); + psz = RTStrDup(pszValue); + if (psz) + { + USBLibPurgeEncoding(psz); + *ppsz = psz; + return VINF_SUCCESS; + } + return VERR_NO_MEMORY; +} + + +/** + * Skips the current property. + */ +static char *usbfsReadSkip(char *pszValue) +{ + char *psz = strchr(pszValue, '='); + if (psz) + psz = strchr(psz + 1, '='); + if (!psz) + return strchr(pszValue, '\0'); + while (psz > pszValue && !RT_C_IS_SPACE(psz[-1])) + psz--; + Assert(psz > pszValue); + return psz; +} + + +/** + * Determine the USB speed. + */ +static int usbfsReadSpeed(const char *pszValue, USBDEVICESPEED *pSpd, char **ppszNext) +{ + pszValue = RTStrStripL(pszValue); + /* verified with Linux 2.4.0 ... Linux 2.6.25 */ + if (!strncmp(pszValue, RT_STR_TUPLE("1.5"))) + *pSpd = USBDEVICESPEED_LOW; + else if (!strncmp(pszValue, RT_STR_TUPLE("12 "))) + *pSpd = USBDEVICESPEED_FULL; + else if (!strncmp(pszValue, RT_STR_TUPLE("480"))) + *pSpd = USBDEVICESPEED_HIGH; + else if (!strncmp(pszValue, RT_STR_TUPLE("5000"))) + *pSpd = USBDEVICESPEED_SUPER; + else + *pSpd = USBDEVICESPEED_UNKNOWN; + while (pszValue[0] != '\0' && !RT_C_IS_SPACE(pszValue[0])) + pszValue++; + *ppszNext = (char *)pszValue; + return VINF_SUCCESS; +} + + +/** + * Compare a prefix and returns pointer to the char following it if it matches. + */ +static char *usbfsPrefix(char *psz, const char *pszPref, size_t cchPref) +{ + if (strncmp(psz, pszPref, cchPref)) + return NULL; + return psz + cchPref; +} + + +/** Just a worker for USBProxyServiceLinux::getDevices that avoids some code duplication. */ +static int usbfsAddDeviceToChain(PUSBDEVICE pDev, PUSBDEVICE *ppFirst, PUSBDEVICE **pppNext, const char *pszUsbfsRoot, + bool fUnsupportedDevicesToo, int vrc) +{ + /* usbDeterminState requires the address. */ + PUSBDEVICE pDevNew = (PUSBDEVICE)RTMemDup(pDev, sizeof(*pDev)); + if (pDevNew) + { + RTStrAPrintf((char **)&pDevNew->pszAddress, "%s/%03d/%03d", pszUsbfsRoot, pDevNew->bBus, pDevNew->bDevNum); + if (pDevNew->pszAddress) + { + pDevNew->enmState = usbDeterminState(pDevNew); + if (pDevNew->enmState != USBDEVICESTATE_UNSUPPORTED || fUnsupportedDevicesToo) + { + if (*pppNext) + **pppNext = pDevNew; + else + *ppFirst = pDevNew; + *pppNext = &pDevNew->pNext; + } + else + deviceFree(pDevNew); + } + else + { + deviceFree(pDevNew); + vrc = VERR_NO_MEMORY; + } + } + else + { + vrc = VERR_NO_MEMORY; + deviceFreeMembers(pDev); + } + + return vrc; +} + + +static int usbfsOpenDevicesFile(const char *pszUsbfsRoot, FILE **ppFile) +{ + char *pszPath; + FILE *pFile; + RTStrAPrintf(&pszPath, "%s/devices", pszUsbfsRoot); + if (!pszPath) + return VERR_NO_MEMORY; + pFile = fopen(pszPath, "r"); + RTStrFree(pszPath); + if (!pFile) + return RTErrConvertFromErrno(errno); + *ppFile = pFile; + return VINF_SUCCESS; +} + + +/** + * USBProxyService::getDevices() implementation for usbfs. + * + * The @a fUnsupportedDevicesToo flag tells the function to return information + * about unsupported devices as well. This is used as a sanity test to check + * that a devices file is really what we expect. + */ +static PUSBDEVICE usbfsGetDevices(const char *pszUsbfsRoot, bool fUnsupportedDevicesToo) +{ + PUSBDEVICE pFirst = NULL; + FILE *pFile = NULL; + int vrc = usbfsOpenDevicesFile(pszUsbfsRoot, &pFile); + if (RT_SUCCESS(vrc)) + { + PUSBDEVICE *ppNext = NULL; + int cHits = 0; + char szLine[1024]; + USBDEVICE Dev; + RT_ZERO(Dev); + Dev.enmState = USBDEVICESTATE_UNUSED; + + /* Set close on exit and hope no one is racing us. */ + vrc = fcntl(fileno(pFile), F_SETFD, FD_CLOEXEC) >= 0 + ? VINF_SUCCESS + : RTErrConvertFromErrno(errno); + while ( RT_SUCCESS(vrc) + && fgets(szLine, sizeof(szLine), pFile)) + { + char *psz; + char *pszValue; + + /* validate and remove the trailing newline. */ + psz = strchr(szLine, '\0'); + if (psz[-1] != '\n' && !feof(pFile)) + { + AssertMsgFailed(("Line too long. (cch=%d)\n", strlen(szLine))); + continue; + } + + /* strip */ + psz = RTStrStrip(szLine); + if (!*psz) + continue; + + /* + * Interpret the line. + * (Ordered by normal occurrence.) + */ + char ch = psz[0]; + if (psz[1] != ':') + continue; + psz = RTStrStripL(psz + 3); +#define PREFIX(str) ( (pszValue = usbfsPrefix(psz, str, sizeof(str) - 1)) != NULL ) + switch (ch) + { + /* + * T: Bus=dd Lev=dd Prnt=dd Port=dd Cnt=dd Dev#=ddd Spd=ddd MxCh=dd + * | | | | | | | | |__MaxChildren + * | | | | | | | |__Device Speed in Mbps + * | | | | | | |__DeviceNumber + * | | | | | |__Count of devices at this level + * | | | | |__Connector/Port on Parent for this device + * | | | |__Parent DeviceNumber + * | | |__Level in topology for this bus + * | |__Bus number + * |__Topology info tag + */ + case 'T': + /* add */ + AssertMsg(cHits >= 3 || cHits == 0, ("cHits=%d\n", cHits)); + if (cHits >= 3) + vrc = usbfsAddDeviceToChain(&Dev, &pFirst, &ppNext, pszUsbfsRoot, fUnsupportedDevicesToo, vrc); + else + deviceFreeMembers(&Dev); + + /* Reset device state */ + RT_ZERO(Dev); + Dev.enmState = USBDEVICESTATE_UNUSED; + cHits = 1; + + /* parse the line. */ + while (*psz && RT_SUCCESS(vrc)) + { + if (PREFIX("Bus=")) + vrc = usbfsRead8(pszValue, 10, &Dev.bBus, &psz); + else if (PREFIX("Port=")) + vrc = usbfsRead8(pszValue, 10, &Dev.bPort, &psz); + else if (PREFIX("Spd=")) + vrc = usbfsReadSpeed(pszValue, &Dev.enmSpeed, &psz); + else if (PREFIX("Dev#=")) + vrc = usbfsRead8(pszValue, 10, &Dev.bDevNum, &psz); + else + psz = usbfsReadSkip(psz); + psz = RTStrStripL(psz); + } + break; + + /* + * Bandwidth info: + * B: Alloc=ddd/ddd us (xx%), #Int=ddd, #Iso=ddd + * | | | |__Number of isochronous requests + * | | |__Number of interrupt requests + * | |__Total Bandwidth allocated to this bus + * |__Bandwidth info tag + */ + case 'B': + break; + + /* + * D: Ver=x.xx Cls=xx(sssss) Sub=xx Prot=xx MxPS=dd #Cfgs=dd + * | | | | | | |__NumberConfigurations + * | | | | | |__MaxPacketSize of Default Endpoint + * | | | | |__DeviceProtocol + * | | | |__DeviceSubClass + * | | |__DeviceClass + * | |__Device USB version + * |__Device info tag #1 + */ + case 'D': + while (*psz && RT_SUCCESS(vrc)) + { + if (PREFIX("Ver=")) + vrc = usbfsReadBCD(pszValue, 16, &Dev.bcdUSB, &psz); + else if (PREFIX("Cls=")) + { + vrc = usbfsRead8(pszValue, 16, &Dev.bDeviceClass, &psz); + if (RT_SUCCESS(vrc) && Dev.bDeviceClass == 9 /* HUB */) + Dev.enmState = USBDEVICESTATE_UNSUPPORTED; + } + else if (PREFIX("Sub=")) + vrc = usbfsRead8(pszValue, 16, &Dev.bDeviceSubClass, &psz); + else if (PREFIX("Prot=")) + vrc = usbfsRead8(pszValue, 16, &Dev.bDeviceProtocol, &psz); + //else if (PREFIX("MxPS=")) + // vrc = usbRead16(pszValue, 10, &Dev.wMaxPacketSize, &psz); + else if (PREFIX("#Cfgs=")) + vrc = usbfsRead8(pszValue, 10, &Dev.bNumConfigurations, &psz); + else + psz = usbfsReadSkip(psz); + psz = RTStrStripL(psz); + } + cHits++; + break; + + /* + * P: Vendor=xxxx ProdID=xxxx Rev=xx.xx + * | | | |__Product revision number + * | | |__Product ID code + * | |__Vendor ID code + * |__Device info tag #2 + */ + case 'P': + while (*psz && RT_SUCCESS(vrc)) + { + if (PREFIX("Vendor=")) + vrc = usbfsRead16(pszValue, 16, &Dev.idVendor, &psz); + else if (PREFIX("ProdID=")) + vrc = usbfsRead16(pszValue, 16, &Dev.idProduct, &psz); + else if (PREFIX("Rev=")) + vrc = usbfsReadBCD(pszValue, 16, &Dev.bcdDevice, &psz); + else + psz = usbfsReadSkip(psz); + psz = RTStrStripL(psz); + } + cHits++; + break; + + /* + * String. + */ + case 'S': + if (PREFIX("Manufacturer=")) + vrc = usbfsReadStr(pszValue, &Dev.pszManufacturer); + else if (PREFIX("Product=")) + vrc = usbfsReadStr(pszValue, &Dev.pszProduct); + else if (PREFIX("SerialNumber=")) + { + vrc = usbfsReadStr(pszValue, &Dev.pszSerialNumber); + if (RT_SUCCESS(vrc)) + Dev.u64SerialHash = USBLibHashSerial(pszValue); + } + break; + + /* + * C:* #Ifs=dd Cfg#=dd Atr=xx MPwr=dddmA + * | | | | | |__MaxPower in mA + * | | | | |__Attributes + * | | | |__ConfiguratioNumber + * | | |__NumberOfInterfaces + * | |__ "*" indicates the active configuration (others are " ") + * |__Config info tag + */ + case 'C': + break; + + /* + * I: If#=dd Alt=dd #EPs=dd Cls=xx(sssss) Sub=xx Prot=xx Driver=ssss + * | | | | | | | |__Driver name + * | | | | | | | or "(none)" + * | | | | | | |__InterfaceProtocol + * | | | | | |__InterfaceSubClass + * | | | | |__InterfaceClass + * | | | |__NumberOfEndpoints + * | | |__AlternateSettingNumber + * | |__InterfaceNumber + * |__Interface info tag + */ + case 'I': + { + /* Check for thing we don't support. */ + while (*psz && RT_SUCCESS(vrc)) + { + if (PREFIX("Driver=")) + { + const char *pszDriver = NULL; + vrc = usbfsReadStr(pszValue, &pszDriver); + if ( !pszDriver + || !*pszDriver + || !strcmp(pszDriver, "(none)") + || !strcmp(pszDriver, "(no driver)")) + /* no driver */; + else if (!strcmp(pszDriver, "hub")) + Dev.enmState = USBDEVICESTATE_UNSUPPORTED; + else if (Dev.enmState == USBDEVICESTATE_UNUSED) + Dev.enmState = USBDEVICESTATE_USED_BY_HOST_CAPTURABLE; + RTStrFree((char *)pszDriver); + break; /* last attrib */ + } + else if (PREFIX("Cls=")) + { + uint8_t bInterfaceClass; + vrc = usbfsRead8(pszValue, 16, &bInterfaceClass, &psz); + if (RT_SUCCESS(vrc) && bInterfaceClass == 9 /* HUB */) + Dev.enmState = USBDEVICESTATE_UNSUPPORTED; + } + else + psz = usbfsReadSkip(psz); + psz = RTStrStripL(psz); + } + break; + } + + + /* + * E: Ad=xx(s) Atr=xx(ssss) MxPS=dddd Ivl=dddms + * | | | | |__Interval (max) between transfers + * | | | |__EndpointMaxPacketSize + * | | |__Attributes(EndpointType) + * | |__EndpointAddress(I=In,O=Out) + * |__Endpoint info tag + */ + case 'E': + break; + + } +#undef PREFIX + } /* parse loop */ + fclose(pFile); + + /* + * Add the current entry. + */ + AssertMsg(cHits >= 3 || cHits == 0, ("cHits=%d\n", cHits)); + if (cHits >= 3) + vrc = usbfsAddDeviceToChain(&Dev, &pFirst, &ppNext, pszUsbfsRoot, fUnsupportedDevicesToo, vrc); + + /* + * Success? + */ + if (RT_FAILURE(vrc)) + { + while (pFirst) + { + PUSBDEVICE pFree = pFirst; + pFirst = pFirst->pNext; + deviceFree(pFree); + } + } + } + if (RT_FAILURE(vrc)) + LogFlow(("USBProxyServiceLinux::getDevices: vrc=%Rrc\n", vrc)); + return pFirst; +} + +#endif /* VBOX_USB_WITH_USBFS */ +#ifdef VBOX_USB_WITH_SYSFS + +static void usbsysfsCleanupDevInfo(USBDeviceInfo *pSelf) +{ + RTStrFree(pSelf->mDevice); + RTStrFree(pSelf->mSysfsPath); + pSelf->mDevice = pSelf->mSysfsPath = NULL; + VEC_CLEANUP_PTR(&pSelf->mvecpszInterfaces); +} + + +static int usbsysfsInitDevInfo(USBDeviceInfo *pSelf, const char *aDevice, const char *aSystemID) +{ + pSelf->mDevice = aDevice ? RTStrDup(aDevice) : NULL; + pSelf->mSysfsPath = aSystemID ? RTStrDup(aSystemID) : NULL; + VEC_INIT_PTR(&pSelf->mvecpszInterfaces, char *, RTStrFree); + if ((aDevice && !pSelf->mDevice) || (aSystemID && ! pSelf->mSysfsPath)) + { + usbsysfsCleanupDevInfo(pSelf); + return 0; + } + return 1; +} + +# define USBDEVICE_MAJOR 189 + +/** + * Calculate the bus (a.k.a root hub) number of a USB device from it's sysfs + * path. + * + * sysfs nodes representing root hubs have file names of the form + * usb, where n is the bus number; other devices start with that number. + * See [http://www.linux-usb.org/FAQ.html#i6] and + * [http://www.kernel.org/doc/Documentation/usb/proc_usb_info.txt] for + * equivalent information about usbfs. + * + * @returns a bus number greater than 0 on success or 0 on failure. + */ +static unsigned usbsysfsGetBusFromPath(const char *pszPath) +{ + const char *pszFile = strrchr(pszPath, '/'); + if (!pszFile) + return 0; + unsigned bus = RTStrToUInt32(pszFile + 1); + if ( !bus + && pszFile[1] == 'u' && pszFile[2] == 's' && pszFile[3] == 'b') + bus = RTStrToUInt32(pszFile + 4); + return bus; +} + + +/** + * Calculate the device number of a USB device. + * + * See drivers/usb/core/hub.c:usb_new_device as of Linux 2.6.20. + */ +static dev_t usbsysfsMakeDevNum(unsigned bus, unsigned device) +{ + AssertReturn(bus > 0, 0); + AssertReturn(((device - 1) & ~127) == 0, 0); + AssertReturn(device > 0, 0); + return makedev(USBDEVICE_MAJOR, ((bus - 1) << 7) + device - 1); +} + + +/** + * If a file @a pszNode from /sys/bus/usb/devices is a device rather than an + * interface add an element for the device to @a pvecDevInfo. + */ +static int usbsysfsAddIfDevice(const char *pszDevicesRoot, const char *pszNode, VECTOR_OBJ(USBDeviceInfo) *pvecDevInfo) +{ + const char *pszFile = strrchr(pszNode, '/'); + if (!pszFile) + return VERR_INVALID_PARAMETER; + if (strchr(pszFile, ':')) + return VINF_SUCCESS; + + unsigned bus = usbsysfsGetBusFromPath(pszNode); + if (!bus) + return VINF_SUCCESS; + + int64_t device; + int vrc = RTLinuxSysFsReadIntFile(10, &device, "%s/devnum", pszNode); + if (RT_FAILURE(vrc)) + return VINF_SUCCESS; + + dev_t devnum = usbsysfsMakeDevNum(bus, (int)device); + if (!devnum) + return VINF_SUCCESS; + + char szDevPath[RTPATH_MAX]; + vrc = RTLinuxCheckDevicePath(devnum, RTFS_TYPE_DEV_CHAR, szDevPath, sizeof(szDevPath), + "%s/%.3d/%.3d", pszDevicesRoot, bus, device); + if (RT_FAILURE(vrc)) + return VINF_SUCCESS; + + USBDeviceInfo info; + if (usbsysfsInitDevInfo(&info, szDevPath, pszNode)) + { + vrc = VEC_PUSH_BACK_OBJ(pvecDevInfo, USBDeviceInfo, &info); + if (RT_SUCCESS(vrc)) + return VINF_SUCCESS; + } + usbsysfsCleanupDevInfo(&info); + return VERR_NO_MEMORY; +} + + +/** + * The logic for testing whether a sysfs address corresponds to an interface of + * a device. + * + * Both must be referenced by their canonical sysfs paths. This is not tested, + * as the test requires file-system interaction. + */ +static bool usbsysfsMuiIsAnInterfaceOf(const char *pszIface, const char *pszDev) +{ + size_t cchDev = strlen(pszDev); + + AssertPtr(pszIface); + AssertPtr(pszDev); + Assert(pszIface[0] == '/'); + Assert(pszDev[0] == '/'); + Assert(pszDev[cchDev - 1] != '/'); + + /* If this passes, pszIface is at least cchDev long */ + if (strncmp(pszIface, pszDev, cchDev)) + return false; + + /* If this passes, pszIface is longer than cchDev */ + if (pszIface[cchDev] != '/') + return false; + + /* In sysfs an interface is an immediate subdirectory of the device */ + if (strchr(pszIface + cchDev + 1, '/')) + return false; + + /* And it always has a colon in its name */ + if (!strchr(pszIface + cchDev + 1, ':')) + return false; + + /* And hopefully we have now elimitated everything else */ + return true; +} + + +# ifdef DEBUG +# ifdef __cplusplus +/** Unit test the logic in muiIsAnInterfaceOf in debug builds. */ +class testIsAnInterfaceOf +{ +public: + testIsAnInterfaceOf() + { + Assert(usbsysfsMuiIsAnInterfaceOf("/sys/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0", + "/sys/devices/pci0000:00/0000:00:1a.0/usb3")); + Assert(!usbsysfsMuiIsAnInterfaceOf("/sys/devices/pci0000:00/0000:00:1a.0/usb3/3-1", + "/sys/devices/pci0000:00/0000:00:1a.0/usb3")); + Assert(!usbsysfsMuiIsAnInterfaceOf("/sys/devices/pci0000:00/0000:00:1a.0/usb3/3-0:1.0/driver", + "/sys/devices/pci0000:00/0000:00:1a.0/usb3")); + } +}; +static testIsAnInterfaceOf testIsAnInterfaceOfInst; +# endif /* __cplusplus */ +# endif /* DEBUG */ + + +/** + * Tell whether a file in /sys/bus/usb/devices is an interface rather than a + * device. + */ +static int usbsysfsAddIfInterfaceOf(const char *pszNode, USBDeviceInfo *pInfo) +{ + if (!usbsysfsMuiIsAnInterfaceOf(pszNode, pInfo->mSysfsPath)) + return VINF_SUCCESS; + + char *pszDup = (char *)RTStrDup(pszNode); + if (pszDup) + { + int vrc = VEC_PUSH_BACK_PTR(&pInfo->mvecpszInterfaces, char *, pszDup); + if (RT_SUCCESS(vrc)) + return VINF_SUCCESS; + RTStrFree(pszDup); + } + return VERR_NO_MEMORY; +} + + +/** + * Helper for usbsysfsReadFilePaths(). + * + * Adds the entries from the open directory @a pDir to the vector @a pvecpchDevs + * using either the full path or the realpath() and skipping hidden files and + * files on which realpath() fails. + */ +static int usbsysfsReadFilePathsFromDir(const char *pszPath, DIR *pDir, VECTOR_PTR(char *) *pvecpchDevs) +{ + struct dirent entry, *pResult; + int err; + +#if RT_GNUC_PREREQ(4, 6) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + for (err = readdir_r(pDir, &entry, &pResult); pResult; + err = readdir_r(pDir, &entry, &pResult)) +#if RT_GNUC_PREREQ(4, 6) +# pragma GCC diagnostic pop +#endif + { + char szPath[RTPATH_MAX + 1]; + char szRealPath[RTPATH_MAX + 1]; + if (entry.d_name[0] == '.') + continue; + if (snprintf(szPath, sizeof(szPath), "%s/%s", pszPath, entry.d_name) < 0) + return RTErrConvertFromErrno(errno); /** @todo r=bird: snprintf isn't document to set errno. Also, wouldn't it be better to continue on errors? Finally, you don't need to copy pszPath each time... */ + if (!realpath(szPath, szRealPath)) + return RTErrConvertFromErrno(errno); + char *pszPathCopy = RTStrDup(szRealPath); + if (!pszPathCopy) + return VERR_NO_MEMORY; + int vrc = VEC_PUSH_BACK_PTR(pvecpchDevs, char *, pszPathCopy); + if (RT_FAILURE(vrc)) + return vrc; + } + return RTErrConvertFromErrno(err); +} + + +/** + * Dump the names of a directory's entries into a vector of char pointers. + * + * @returns zero on success or (positive) posix error value. + * @param pszPath the path to dump. + * @param pvecpchDevs an empty vector of char pointers - must be cleaned up + * by the caller even on failure. + * @param withRealPath whether to canonicalise the filename with realpath + */ +static int usbsysfsReadFilePaths(const char *pszPath, VECTOR_PTR(char *) *pvecpchDevs) +{ + AssertPtrReturn(pvecpchDevs, EINVAL); + AssertReturn(VEC_SIZE_PTR(pvecpchDevs) == 0, EINVAL); + AssertPtrReturn(pszPath, EINVAL); + + DIR *pDir = opendir(pszPath); + if (!pDir) + return RTErrConvertFromErrno(errno); + int vrc = usbsysfsReadFilePathsFromDir(pszPath, pDir, pvecpchDevs); + if (closedir(pDir) < 0 && RT_SUCCESS(vrc)) + vrc = RTErrConvertFromErrno(errno); + return vrc; +} + + +/** + * Logic for USBSysfsEnumerateHostDevices. + * + * @param pvecDevInfo vector of device information structures to add device + * information to + * @param pvecpchDevs empty scratch vector which will be freed by the caller, + * to simplify exit logic + */ +static int usbsysfsEnumerateHostDevicesWorker(const char *pszDevicesRoot, + VECTOR_OBJ(USBDeviceInfo) *pvecDevInfo, + VECTOR_PTR(char *) *pvecpchDevs) +{ + + AssertPtrReturn(pvecDevInfo, VERR_INVALID_POINTER); + LogFlowFunc (("pvecDevInfo=%p\n", pvecDevInfo)); + + int vrc = usbsysfsReadFilePaths("/sys/bus/usb/devices", pvecpchDevs); + if (RT_FAILURE(vrc)) + return vrc; + + char **ppszEntry; + VEC_FOR_EACH(pvecpchDevs, char *, ppszEntry) + { + vrc = usbsysfsAddIfDevice(pszDevicesRoot, *ppszEntry, pvecDevInfo); + if (RT_FAILURE(vrc)) + return vrc; + } + + USBDeviceInfo *pInfo; + VEC_FOR_EACH(pvecDevInfo, USBDeviceInfo, pInfo) + VEC_FOR_EACH(pvecpchDevs, char *, ppszEntry) + { + vrc = usbsysfsAddIfInterfaceOf(*ppszEntry, pInfo); + if (RT_FAILURE(vrc)) + return vrc; + } + return VINF_SUCCESS; +} + + +static int usbsysfsEnumerateHostDevices(const char *pszDevicesRoot, VECTOR_OBJ(USBDeviceInfo) *pvecDevInfo) +{ + VECTOR_PTR(char *) vecpchDevs; + + AssertReturn(VEC_SIZE_OBJ(pvecDevInfo) == 0, VERR_INVALID_PARAMETER); + LogFlowFunc(("entered\n")); + VEC_INIT_PTR(&vecpchDevs, char *, RTStrFree); + int vrc = usbsysfsEnumerateHostDevicesWorker(pszDevicesRoot, pvecDevInfo, &vecpchDevs); + VEC_CLEANUP_PTR(&vecpchDevs); + LogFlowFunc(("vrc=%Rrc\n", vrc)); + return vrc; +} + + +/** + * Helper function for extracting the port number on the parent device from + * the sysfs path value. + * + * The sysfs path is a chain of elements separated by forward slashes, and for + * USB devices, the last element in the chain takes the form + * -.[...].[:.] + * where the first is the port number on the root hub, and the following + * (optional) ones are the port numbers on any other hubs between the device + * and the root hub. The last part (:) is only present for + * interfaces, not for devices. This API should only be called for devices. + * For compatibility with usbfs, which enumerates from zero up, we subtract one + * from the port number. + * + * For root hubs, the last element in the chain takes the form + * usb + * and usbfs always returns port number zero. + * + * @returns VBox status code. pu8Port is set on success. + * @param pszPath The sysfs path to parse. + * @param pu8Port Where to store the port number. + */ +static int usbsysfsGetPortFromStr(const char *pszPath, uint8_t *pu8Port) +{ + AssertPtrReturn(pszPath, VERR_INVALID_POINTER); + AssertPtrReturn(pu8Port, VERR_INVALID_POINTER); + + /* + * This should not be possible until we get PCs with USB as their primary bus. + * Note: We don't assert this, as we don't expect the caller to validate the + * sysfs path. + */ + const char *pszLastComp = strrchr(pszPath, '/'); + if (!pszLastComp) + { + Log(("usbGetPortFromSysfsPath(%s): failed [1]\n", pszPath)); + return VERR_INVALID_PARAMETER; + } + pszLastComp++; /* skip the slash */ + + /* + * This API should not be called for interfaces, so the last component + * of the path should not contain a colon. We *do* assert this, as it + * might indicate a caller bug. + */ + AssertMsgReturn(strchr(pszLastComp, ':') == NULL, ("%s\n", pszPath), VERR_INVALID_PARAMETER); + + /* + * Look for the start of the last number. + */ + const char *pchDash = strrchr(pszLastComp, '-'); + const char *pchDot = strrchr(pszLastComp, '.'); + if (!pchDash && !pchDot) + { + /* No -/. so it must be a root hub. Check that it's usb. */ + if (strncmp(pszLastComp, RT_STR_TUPLE("usb")) != 0) + { + Log(("usbGetPortFromSysfsPath(%s): failed [2]\n", pszPath)); + return VERR_INVALID_PARAMETER; + } + return VERR_NOT_SUPPORTED; + } + + const char *pszLastPort = pchDot != NULL + ? pchDot + 1 + : pchDash + 1; + int vrc = RTStrToUInt8Full(pszLastPort, 10, pu8Port); + if (vrc != VINF_SUCCESS) + { + Log(("usbGetPortFromSysfsPath(%s): failed [3], vrc=%Rrc\n", pszPath, vrc)); + return VERR_INVALID_PARAMETER; + } + if (*pu8Port == 0) + { + Log(("usbGetPortFromSysfsPath(%s): failed [4]\n", pszPath)); + return VERR_INVALID_PARAMETER; + } + + /* usbfs compatibility, 0-based port number. */ + *pu8Port = (uint8_t)(*pu8Port - 1); + return VINF_SUCCESS; +} + + +/** + * Converts a sysfs BCD value into a uint16_t. + * + * In contrast to usbReadBCD() this function can handle BCD values without + * a decimal separator. This is necessary for parsing bcdDevice. + * + * @param pszBuf Pointer to the string buffer. + * @param pu15 Pointer to the return value. + * @returns IPRT status code. + */ +static int usbsysfsConvertStrToBCD(const char *pszBuf, uint16_t *pu16) +{ + char *pszNext; + int32_t i32; + + pszBuf = RTStrStripL(pszBuf); + int vrc = RTStrToInt32Ex(pszBuf, &pszNext, 16, &i32); + if ( RT_FAILURE(vrc) + || vrc == VWRN_NUMBER_TOO_BIG + || i32 < 0) + return VERR_NUMBER_TOO_BIG; + if (*pszNext == '.') + { + if (i32 > 255) + return VERR_NUMBER_TOO_BIG; + int32_t i32Lo; + vrc = RTStrToInt32Ex(pszNext+1, &pszNext, 16, &i32Lo); + if ( RT_FAILURE(vrc) + || vrc == VWRN_NUMBER_TOO_BIG + || i32Lo > 255 + || i32Lo < 0) + return VERR_NUMBER_TOO_BIG; + i32 = (i32 << 8) | i32Lo; + } + if ( i32 > 65535 + || (*pszNext != '\0' && *pszNext != ' ')) + return VERR_NUMBER_TOO_BIG; + + *pu16 = (uint16_t)i32; + return VINF_SUCCESS; +} + + +/** + * Returns the byte value for the given device property or sets the given default if an + * error occurs while obtaining it. + * + * @returns uint8_t value of the given property. + * @param uBase The base of the number in the sysfs property. + * @param fDef The default to set on error. + * @param pszFormat The format string for the property. + * @param ... Arguments for the format string. + */ +static uint8_t usbsysfsReadDevicePropertyU8Def(unsigned uBase, uint8_t fDef, const char *pszFormat, ...) +{ + int64_t i64Tmp = 0; + + va_list va; + va_start(va, pszFormat); + int vrc = RTLinuxSysFsReadIntFileV(uBase, &i64Tmp, pszFormat, va); + va_end(va); + if (RT_SUCCESS(vrc)) + return (uint8_t)i64Tmp; + return fDef; +} + + +/** + * Returns the uint16_t value for the given device property or sets the given default if an + * error occurs while obtaining it. + * + * @returns uint16_t value of the given property. + * @param uBase The base of the number in the sysfs property. + * @param u16Def The default to set on error. + * @param pszFormat The format string for the property. + * @param ... Arguments for the format string. + */ +static uint16_t usbsysfsReadDevicePropertyU16Def(unsigned uBase, uint16_t u16Def, const char *pszFormat, ...) +{ + int64_t i64Tmp = 0; + + va_list va; + va_start(va, pszFormat); + int vrc = RTLinuxSysFsReadIntFileV(uBase, &i64Tmp, pszFormat, va); + va_end(va); + if (RT_SUCCESS(vrc)) + return (uint16_t)i64Tmp; + return u16Def; +} + + +static void usbsysfsFillInDevice(USBDEVICE *pDev, USBDeviceInfo *pInfo) +{ + int vrc; + const char *pszSysfsPath = pInfo->mSysfsPath; + + /* Fill in the simple fields */ + pDev->enmState = USBDEVICESTATE_UNUSED; + pDev->bBus = (uint8_t)usbsysfsGetBusFromPath(pszSysfsPath); + pDev->bDeviceClass = usbsysfsReadDevicePropertyU8Def(16, 0, "%s/bDeviceClass", pszSysfsPath); + pDev->bDeviceSubClass = usbsysfsReadDevicePropertyU8Def(16, 0, "%s/bDeviceSubClass", pszSysfsPath); + pDev->bDeviceProtocol = usbsysfsReadDevicePropertyU8Def(16, 0, "%s/bDeviceProtocol", pszSysfsPath); + pDev->bNumConfigurations = usbsysfsReadDevicePropertyU8Def(10, 0, "%s/bNumConfigurations", pszSysfsPath); + pDev->idVendor = usbsysfsReadDevicePropertyU16Def(16, 0, "%s/idVendor", pszSysfsPath); + pDev->idProduct = usbsysfsReadDevicePropertyU16Def(16, 0, "%s/idProduct", pszSysfsPath); + pDev->bDevNum = usbsysfsReadDevicePropertyU8Def(10, 0, "%s/devnum", pszSysfsPath); + + /* Now deal with the non-numeric bits. */ + char szBuf[1024]; /* Should be larger than anything a sane device + * will need, and insane devices can be unsupported + * until further notice. */ + size_t cchRead; + + /* For simplicity, we just do strcmps on the next one. */ + vrc = RTLinuxSysFsReadStrFile(szBuf, sizeof(szBuf), &cchRead, "%s/speed", pszSysfsPath); + if (RT_FAILURE(vrc) || cchRead == sizeof(szBuf)) + pDev->enmState = USBDEVICESTATE_UNSUPPORTED; + else + pDev->enmSpeed = !strcmp(szBuf, "1.5") ? USBDEVICESPEED_LOW + : !strcmp(szBuf, "12") ? USBDEVICESPEED_FULL + : !strcmp(szBuf, "480") ? USBDEVICESPEED_HIGH + : !strcmp(szBuf, "5000") ? USBDEVICESPEED_SUPER + : USBDEVICESPEED_UNKNOWN; + + vrc = RTLinuxSysFsReadStrFile(szBuf, sizeof(szBuf), &cchRead, "%s/version", pszSysfsPath); + if (RT_FAILURE(vrc) || cchRead == sizeof(szBuf)) + pDev->enmState = USBDEVICESTATE_UNSUPPORTED; + else + { + vrc = usbsysfsConvertStrToBCD(szBuf, &pDev->bcdUSB); + if (RT_FAILURE(vrc)) + { + pDev->enmState = USBDEVICESTATE_UNSUPPORTED; + pDev->bcdUSB = UINT16_MAX; + } + } + + vrc = RTLinuxSysFsReadStrFile(szBuf, sizeof(szBuf), &cchRead, "%s/bcdDevice", pszSysfsPath); + if (RT_FAILURE(vrc) || cchRead == sizeof(szBuf)) + pDev->bcdDevice = UINT16_MAX; + else + { + vrc = usbsysfsConvertStrToBCD(szBuf, &pDev->bcdDevice); + if (RT_FAILURE(vrc)) + pDev->bcdDevice = UINT16_MAX; + } + + /* Now do things that need string duplication */ + vrc = RTLinuxSysFsReadStrFile(szBuf, sizeof(szBuf), &cchRead, "%s/product", pszSysfsPath); + if (RT_SUCCESS(vrc) && cchRead < sizeof(szBuf)) + { + USBLibPurgeEncoding(szBuf); + pDev->pszProduct = RTStrDup(szBuf); + } + + vrc = RTLinuxSysFsReadStrFile(szBuf, sizeof(szBuf), &cchRead, "%s/serial", pszSysfsPath); + if (RT_SUCCESS(vrc) && cchRead < sizeof(szBuf)) + { + USBLibPurgeEncoding(szBuf); + pDev->pszSerialNumber = RTStrDup(szBuf); + pDev->u64SerialHash = USBLibHashSerial(szBuf); + } + + vrc = RTLinuxSysFsReadStrFile(szBuf, sizeof(szBuf), &cchRead, "%s/manufacturer", pszSysfsPath); + if (RT_SUCCESS(vrc) && cchRead < sizeof(szBuf)) + { + USBLibPurgeEncoding(szBuf); + pDev->pszManufacturer = RTStrDup(szBuf); + } + + /* Work out the port number */ + if (RT_FAILURE(usbsysfsGetPortFromStr(pszSysfsPath, &pDev->bPort))) + pDev->enmState = USBDEVICESTATE_UNSUPPORTED; + + /* Check the interfaces to see if we can support the device. */ + char **ppszIf; + VEC_FOR_EACH(&pInfo->mvecpszInterfaces, char *, ppszIf) + { + vrc = RTLinuxSysFsGetLinkDest(szBuf, sizeof(szBuf), NULL, "%s/driver", *ppszIf); + if (RT_SUCCESS(vrc) && pDev->enmState != USBDEVICESTATE_UNSUPPORTED) + pDev->enmState = (strcmp(szBuf, "hub") == 0) + ? USBDEVICESTATE_UNSUPPORTED + : USBDEVICESTATE_USED_BY_HOST_CAPTURABLE; + if (usbsysfsReadDevicePropertyU8Def(16, 9 /* bDev */, "%s/bInterfaceClass", *ppszIf) == 9 /* hub */) + pDev->enmState = USBDEVICESTATE_UNSUPPORTED; + } + + /* We use a double slash as a separator in the pszAddress field. This is + * alright as the two paths can't contain a slash due to the way we build + * them. */ + char *pszAddress = NULL; + RTStrAPrintf(&pszAddress, "sysfs:%s//device:%s", pszSysfsPath, pInfo->mDevice); + pDev->pszAddress = pszAddress; + pDev->pszBackend = RTStrDup("host"); + + /* Work out from the data collected whether we can support this device. */ + pDev->enmState = usbDeterminState(pDev); + usbLogDevice(pDev); +} + + +/** + * USBProxyService::getDevices() implementation for sysfs. + */ +static PUSBDEVICE usbsysfsGetDevices(const char *pszDevicesRoot, bool fUnsupportedDevicesToo) +{ + /* Add each of the devices found to the chain. */ + PUSBDEVICE pFirst = NULL; + PUSBDEVICE pLast = NULL; + VECTOR_OBJ(USBDeviceInfo) vecDevInfo; + USBDeviceInfo *pInfo; + + VEC_INIT_OBJ(&vecDevInfo, USBDeviceInfo, usbsysfsCleanupDevInfo); + int vrc = usbsysfsEnumerateHostDevices(pszDevicesRoot, &vecDevInfo); + if (RT_FAILURE(vrc)) + return NULL; + VEC_FOR_EACH(&vecDevInfo, USBDeviceInfo, pInfo) + { + USBDEVICE *pDev = (USBDEVICE *)RTMemAllocZ(sizeof(USBDEVICE)); + if (!pDev) + vrc = VERR_NO_MEMORY; + if (RT_SUCCESS(vrc)) + usbsysfsFillInDevice(pDev, pInfo); + if ( RT_SUCCESS(vrc) + && ( pDev->enmState != USBDEVICESTATE_UNSUPPORTED + || fUnsupportedDevicesToo) + && pDev->pszAddress != NULL + ) + { + if (pLast != NULL) + { + pLast->pNext = pDev; + pLast = pLast->pNext; + } + else + pFirst = pLast = pDev; + } + else + deviceFree(pDev); + if (RT_FAILURE(vrc)) + break; + } + if (RT_FAILURE(vrc)) + deviceListFree(&pFirst); + + VEC_CLEANUP_OBJ(&vecDevInfo); + return pFirst; +} + +#endif /* VBOX_USB_WITH_SYSFS */ +#ifdef UNIT_TEST + +/* Set up mock functions for USBProxyLinuxCheckDeviceRoot - here dlsym and close + * for the inotify presence check. */ +static int testInotifyInitGood(void) { return 0; } +static int testInotifyInitBad(void) { return -1; } +static bool s_fHaveInotifyLibC = true; +static bool s_fHaveInotifyKernel = true; + +static void *testDLSym(void *handle, const char *symbol) +{ + RT_NOREF(handle, symbol); + Assert(handle == RTLD_DEFAULT); + Assert(!RTStrCmp(symbol, "inotify_init")); + if (!s_fHaveInotifyLibC) + return NULL; + if (s_fHaveInotifyKernel) + return (void *)(uintptr_t)testInotifyInitGood; + return (void *)(uintptr_t)testInotifyInitBad; +} + +void TestUSBSetInotifyAvailable(bool fHaveInotifyLibC, bool fHaveInotifyKernel) +{ + s_fHaveInotifyLibC = fHaveInotifyLibC; + s_fHaveInotifyKernel = fHaveInotifyKernel; +} +# define dlsym testDLSym +# define close(a) do {} while (0) + +#endif /* UNIT_TEST */ + +/** + * Is inotify available and working on this system? + * + * This is a requirement for using USB with sysfs + */ +static bool usbsysfsInotifyAvailable(void) +{ + int (*inotify_init)(void); + + *(void **)(&inotify_init) = dlsym(RTLD_DEFAULT, "inotify_init"); + if (!inotify_init) + return false; + int fd = inotify_init(); + if (fd == -1) + return false; + close(fd); + return true; +} + +#ifdef UNIT_TEST + +# undef dlsym +# undef close + +/** Unit test list of usbfs addresses of connected devices. */ +static const char **g_papszUsbfsDeviceAddresses = NULL; + +static PUSBDEVICE testGetUsbfsDevices(const char *pszUsbfsRoot, bool fUnsupportedDevicesToo) +{ + RT_NOREF(pszUsbfsRoot, fUnsupportedDevicesToo); + const char **psz; + PUSBDEVICE pList = NULL, pTail = NULL; + for (psz = g_papszUsbfsDeviceAddresses; psz && *psz; ++psz) + { + PUSBDEVICE pNext = (PUSBDEVICE)RTMemAllocZ(sizeof(USBDEVICE)); + if (pNext) + pNext->pszAddress = RTStrDup(*psz); + if (!pNext || !pNext->pszAddress) + { + if (pNext) + RTMemFree(pNext); + deviceListFree(&pList); + return NULL; + } + if (pTail) + pTail->pNext = pNext; + else + pList = pNext; + pTail = pNext; + } + return pList; +} +# define usbfsGetDevices testGetUsbfsDevices + +/** + * Specify the list of devices that will appear to be available through + * usbfs during unit testing (of USBProxyLinuxGetDevices) + * @param pacszDeviceAddresses NULL terminated array of usbfs device addresses + */ +void TestUSBSetAvailableUsbfsDevices(const char **papszDeviceAddresses) +{ + g_papszUsbfsDeviceAddresses = papszDeviceAddresses; +} + +/** Unit test list of files reported as accessible by access(3). We only do + * accessible or not accessible. */ +static const char **g_papszAccessibleFiles = NULL; + +static int testAccess(const char *pszPath, int mode) +{ + RT_NOREF(mode); + const char **psz; + for (psz = g_papszAccessibleFiles; psz && *psz; ++psz) + if (!RTStrCmp(pszPath, *psz)) + return 0; + return -1; +} +# define access testAccess + + +/** + * Specify the list of files that access will report as accessible (at present + * we only do accessible or not accessible) during unit testing (of + * USBProxyLinuxGetDevices) + * @param papszAccessibleFiles NULL terminated array of file paths to be + * reported accessible + */ +void TestUSBSetAccessibleFiles(const char **papszAccessibleFiles) +{ + g_papszAccessibleFiles = papszAccessibleFiles; +} + + +/** The path we pretend the usbfs root is located at, or NULL. */ +const char *s_pszTestUsbfsRoot; +/** Should usbfs be accessible to the current user? */ +bool s_fTestUsbfsAccessible; +/** The path we pretend the device node tree root is located at, or NULL. */ +const char *s_pszTestDevicesRoot; +/** Should the device node tree be accessible to the current user? */ +bool s_fTestDevicesAccessible; +/** The result of the usbfs/inotify-specific init */ +int s_vrcTestMethodInitResult; +/** The value of the VBOX_USB environment variable. */ +const char *s_pszTestEnvUsb; +/** The value of the VBOX_USB_ROOT environment variable. */ +const char *s_pszTestEnvUsbRoot; + + +/** Select which access methods will be available to the @a init method + * during unit testing, and (hack!) what return code it will see from + * the access method-specific initialisation. */ +void TestUSBSetupInit(const char *pszUsbfsRoot, bool fUsbfsAccessible, + const char *pszDevicesRoot, bool fDevicesAccessible, + int vrcMethodInitResult) +{ + s_pszTestUsbfsRoot = pszUsbfsRoot; + s_fTestUsbfsAccessible = fUsbfsAccessible; + s_pszTestDevicesRoot = pszDevicesRoot; + s_fTestDevicesAccessible = fDevicesAccessible; + s_vrcTestMethodInitResult = vrcMethodInitResult; +} + + +/** Specify the environment that the @a init method will see during unit + * testing. */ +void TestUSBSetEnv(const char *pszEnvUsb, const char *pszEnvUsbRoot) +{ + s_pszTestEnvUsb = pszEnvUsb; + s_pszTestEnvUsbRoot = pszEnvUsbRoot; +} + +/* For testing we redefine anything that accesses the outside world to + * return test values. */ +# define RTEnvGet(a) \ + ( !RTStrCmp(a, "VBOX_USB") ? s_pszTestEnvUsb \ + : !RTStrCmp(a, "VBOX_USB_ROOT") ? s_pszTestEnvUsbRoot \ + : NULL) +# define USBProxyLinuxCheckDeviceRoot(pszPath, fUseNodes) \ + ( ((fUseNodes) && s_fTestDevicesAccessible \ + && !RTStrCmp(pszPath, s_pszTestDevicesRoot)) \ + || (!(fUseNodes) && s_fTestUsbfsAccessible \ + && !RTStrCmp(pszPath, s_pszTestUsbfsRoot))) +# define RTDirExists(pszDir) \ + ( (pszDir) \ + && ( !RTStrCmp(pszDir, s_pszTestDevicesRoot) \ + || !RTStrCmp(pszDir, s_pszTestUsbfsRoot))) +# define RTFileExists(pszFile) \ + ( (pszFile) \ + && s_pszTestUsbfsRoot \ + && !RTStrNCmp(pszFile, s_pszTestUsbfsRoot, strlen(s_pszTestUsbfsRoot)) \ + && !RTStrCmp(pszFile + strlen(s_pszTestUsbfsRoot), "/devices")) + +#endif /* UNIT_TEST */ + +/** + * Use USBFS-like or sysfs/device node-like access method? + * + * Selects the access method that will be used to access USB devices based on + * what is available on the host and what if anything the user has specified + * in the environment. + * + * @returns iprt status value + * @param pfUsingUsbfsDevices on success this will be set to true if + * the prefered access method is USBFS-like and to + * false if it is sysfs/device node-like + * @param ppszDevicesRoot on success the root of the tree of USBFS-like + * device nodes will be stored here + */ +int USBProxyLinuxChooseMethod(bool *pfUsingUsbfsDevices, const char **ppszDevicesRoot) +{ + /* + * We have two methods available for getting host USB device data - using + * USBFS and using sysfs. The default choice is sysfs; if that is not + * available we fall back to USBFS. + * In the event of both failing, an appropriate error will be returned. + * The user may also specify a method and root using the VBOX_USB and + * VBOX_USB_ROOT environment variables. In this case we don't check + * the root they provide for validity. + */ + bool fUsbfsChosen = false; + bool fSysfsChosen = false; + const char *pszUsbFromEnv = RTEnvGet("VBOX_USB"); + const char *pszUsbRoot = NULL; + if (pszUsbFromEnv) + { + bool fValidVBoxUSB = true; + + pszUsbRoot = RTEnvGet("VBOX_USB_ROOT"); + if (!RTStrICmp(pszUsbFromEnv, "USBFS")) + { + LogRel(("Default USB access method set to \"usbfs\" from environment\n")); + fUsbfsChosen = true; + } + else if (!RTStrICmp(pszUsbFromEnv, "SYSFS")) + { + LogRel(("Default USB method set to \"sysfs\" from environment\n")); + fSysfsChosen = true; + } + else + { + LogRel(("Invalid VBOX_USB environment variable setting \"%s\"\n", pszUsbFromEnv)); + fValidVBoxUSB = false; + pszUsbFromEnv = NULL; + } + if (!fValidVBoxUSB && pszUsbRoot) + pszUsbRoot = NULL; + } + if (!pszUsbRoot) + { + if ( !fUsbfsChosen + && USBProxyLinuxCheckDeviceRoot("/dev/vboxusb", true)) + { + fSysfsChosen = true; + pszUsbRoot = "/dev/vboxusb"; + } + else if ( !fSysfsChosen + && USBProxyLinuxCheckDeviceRoot("/proc/bus/usb", false)) + { + fUsbfsChosen = true; + pszUsbRoot = "/proc/bus/usb"; + } + } + else if (!USBProxyLinuxCheckDeviceRoot(pszUsbRoot, fSysfsChosen)) + pszUsbRoot = NULL; + if (pszUsbRoot) + { + *pfUsingUsbfsDevices = fUsbfsChosen; + *ppszDevicesRoot = pszUsbRoot; + return VINF_SUCCESS; + } + /* else */ + return pszUsbFromEnv ? VERR_NOT_FOUND + : RTDirExists("/dev/vboxusb") ? VERR_VUSB_USB_DEVICE_PERMISSION + : RTFileExists("/proc/bus/usb/devices") ? VERR_VUSB_USBFS_PERMISSION + : VERR_NOT_FOUND; +} + +#ifdef UNIT_TEST +# undef RTEnvGet +# undef USBProxyLinuxCheckDeviceRoot +# undef RTDirExists +# undef RTFileExists +#endif + +/** + * Check whether a USB device tree root is usable. + * + * @param pszRoot the path to the root of the device tree + * @param fIsDeviceNodes whether this is a device node (or usbfs) tree + * @note returns a pointer into a static array so it will stay valid + */ +bool USBProxyLinuxCheckDeviceRoot(const char *pszRoot, bool fIsDeviceNodes) +{ + bool fOK = false; + if (!fIsDeviceNodes) /* usbfs */ + { +#ifdef VBOX_USB_WITH_USBFS + if (!access(pszRoot, R_OK | X_OK)) + { + fOK = true; + PUSBDEVICE pDevices = usbfsGetDevices(pszRoot, true); + if (pDevices) + { + PUSBDEVICE pDevice; + for (pDevice = pDevices; pDevice && fOK; pDevice = pDevice->pNext) + if (access(pDevice->pszAddress, R_OK | W_OK)) + fOK = false; + deviceListFree(&pDevices); + } + } +#endif + } +#ifdef VBOX_USB_WITH_SYSFS + /* device nodes */ + else if (usbsysfsInotifyAvailable() && !access(pszRoot, R_OK | X_OK)) + fOK = true; +#endif + return fOK; +} + +#ifdef UNIT_TEST +# undef usbfsGetDevices +# undef access +#endif + +/** + * Get the list of USB devices supported by the system. + * + * Result should be freed using #deviceFree or something equivalent. + * + * @param pszDevicesRoot the path to the root of the device tree + * @param fUseSysfs whether to use sysfs (or usbfs) for enumeration + */ +PUSBDEVICE USBProxyLinuxGetDevices(const char *pszDevicesRoot, bool fUseSysfs) +{ + if (!fUseSysfs) + { +#ifdef VBOX_USB_WITH_USBFS + return usbfsGetDevices(pszDevicesRoot, false); +#else + return NULL; +#endif + } + +#ifdef VBOX_USB_WITH_SYSFS + return usbsysfsGetDevices(pszDevicesRoot, false); +#else + return NULL; +#endif +} + diff --git a/src/VBox/Main/src-server/linux/USBProxyBackendLinux.cpp b/src/VBox/Main/src-server/linux/USBProxyBackendLinux.cpp new file mode 100644 index 00000000..bbfb7bed --- /dev/null +++ b/src/VBox/Main/src-server/linux/USBProxyBackendLinux.cpp @@ -0,0 +1,399 @@ +/* $Id: USBProxyBackendLinux.cpp $ */ +/** @file + * VirtualBox USB Proxy Service, Linux Specialization. + */ + +/* + * Copyright (C) 2005-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_MAIN_USBPROXYBACKEND +#include "USBProxyService.h" +#include "USBGetDevices.h" +#include "LoggingNew.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef VBOX_WITH_LINUX_COMPILER_H +# include +#endif +#include + + +/** + * Initialize data members. + */ +USBProxyBackendLinux::USBProxyBackendLinux() + : USBProxyBackend(), mhFile(NIL_RTFILE), mhWakeupPipeR(NIL_RTPIPE), mhWakeupPipeW(NIL_RTPIPE), mpWaiter(NULL) +{ + LogFlowThisFunc(("\n")); +} + + +/** + * Stop all service threads and free the device chain. + */ +USBProxyBackendLinux::~USBProxyBackendLinux() +{ + LogFlowThisFunc(("\n")); +} + +/** + * Initializes the object (called right after construction). + * + * @returns VBox status code. + */ +int USBProxyBackendLinux::init(USBProxyService *pUsbProxyService, const com::Utf8Str &strId, + const com::Utf8Str &strAddress, bool fLoadingSettings) +{ + USBProxyBackend::init(pUsbProxyService, strId, strAddress, fLoadingSettings); + + unconst(m_strBackend) = Utf8Str("host"); + + const char *pcszDevicesRoot; + int vrc = USBProxyLinuxChooseMethod(&mUsingUsbfsDevices, &pcszDevicesRoot); + if (RT_SUCCESS(vrc)) + { + mDevicesRoot = pcszDevicesRoot; + vrc = mUsingUsbfsDevices ? initUsbfs() : initSysfs(); + /* For the day when we have VBoxSVC release logging... */ + LogRel((RT_SUCCESS(vrc) ? "Successfully initialised host USB using %s\n" + : "Failed to initialise host USB using %s\n", + mUsingUsbfsDevices ? "USBFS" : "sysfs")); + } + + return vrc; +} + +void USBProxyBackendLinux::uninit() +{ + /* + * Stop the service. + */ + if (isActive()) + stop(); + + /* + * Free resources. + */ + doUsbfsCleanupAsNeeded(); +#ifdef VBOX_USB_WITH_SYSFS + if (mpWaiter) + delete mpWaiter; +#endif + + USBProxyBackend::uninit(); +} + + +/** + * Initialization routine for the usbfs based operation. + * + * @returns iprt status code. + */ +int USBProxyBackendLinux::initUsbfs(void) +{ + Assert(mUsingUsbfsDevices); + + /* + * Open the devices file. + */ + int vrc; + char *pszDevices = RTPathJoinA(mDevicesRoot.c_str(), "devices"); + if (pszDevices) + { + vrc = RTFileOpen(&mhFile, pszDevices, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_NONE); + if (RT_SUCCESS(vrc)) + { + vrc = RTPipeCreate(&mhWakeupPipeR, &mhWakeupPipeW, 0 /*fFlags*/); + if (RT_SUCCESS(vrc)) + { + /* + * Start the poller thread. + */ + vrc = start(); + if (RT_SUCCESS(vrc)) + { + RTStrFree(pszDevices); + LogFlowThisFunc(("returns successfully\n")); + return VINF_SUCCESS; + } + + RTPipeClose(mhWakeupPipeR); + RTPipeClose(mhWakeupPipeW); + mhWakeupPipeW = mhWakeupPipeR = NIL_RTPIPE; + } + else + Log(("USBProxyBackendLinux::USBProxyBackendLinux: RTFilePipe failed with vrc=%Rrc\n", vrc)); + RTFileClose(mhFile); + } + + RTStrFree(pszDevices); + } + else + { + vrc = VERR_NO_MEMORY; + Log(("USBProxyBackendLinux::USBProxyBackendLinux: out of memory!\n")); + } + + LogFlowThisFunc(("returns failure!!! (vrc=%Rrc)\n", vrc)); + return vrc; +} + + +/** + * Initialization routine for the sysfs based operation. + * + * @returns iprt status code + */ +int USBProxyBackendLinux::initSysfs(void) +{ + Assert(!mUsingUsbfsDevices); + +#ifdef VBOX_USB_WITH_SYSFS + try + { + mpWaiter = new VBoxMainHotplugWaiter(mDevicesRoot.c_str()); + } + catch(std::bad_alloc &e) + { + return VERR_NO_MEMORY; + } + int vrc = mpWaiter->getStatus(); + if (RT_SUCCESS(vrc) || vrc == VERR_TIMEOUT || vrc == VERR_TRY_AGAIN) + vrc = start(); + else if (vrc == VERR_NOT_SUPPORTED) + /* This can legitimately happen if hal or DBus are not running, but of + * course we can't start in this case. */ + vrc = VINF_SUCCESS; + return vrc; + +#else /* !VBOX_USB_WITH_SYSFS */ + return VERR_NOT_IMPLEMENTED; +#endif /* !VBOX_USB_WITH_SYSFS */ +} + + +/** + * If any Usbfs-related resources are currently allocated, then free them + * and mark them as freed. + */ +void USBProxyBackendLinux::doUsbfsCleanupAsNeeded() +{ + /* + * Free resources. + */ + if (mhFile != NIL_RTFILE) + RTFileClose(mhFile); + mhFile = NIL_RTFILE; + + if (mhWakeupPipeR != NIL_RTPIPE) + RTPipeClose(mhWakeupPipeR); + if (mhWakeupPipeW != NIL_RTPIPE) + RTPipeClose(mhWakeupPipeW); + mhWakeupPipeW = mhWakeupPipeR = NIL_RTPIPE; +} + + +int USBProxyBackendLinux::captureDevice(HostUSBDevice *aDevice) +{ + AssertReturn(aDevice, VERR_GENERAL_FAILURE); + AssertReturn(!aDevice->isWriteLockOnCurrentThread(), VERR_GENERAL_FAILURE); + + AutoReadLock devLock(aDevice COMMA_LOCKVAL_SRC_POS); + LogFlowThisFunc(("aDevice=%s\n", aDevice->i_getName().c_str())); + + /* + * Don't think we need to do anything when the device is held... fake it. + */ + Assert(aDevice->i_getUnistate() == kHostUSBDeviceState_Capturing); + devLock.release(); + interruptWait(); + + return VINF_SUCCESS; +} + + +int USBProxyBackendLinux::releaseDevice(HostUSBDevice *aDevice) +{ + AssertReturn(aDevice, VERR_GENERAL_FAILURE); + AssertReturn(!aDevice->isWriteLockOnCurrentThread(), VERR_GENERAL_FAILURE); + + AutoReadLock devLock(aDevice COMMA_LOCKVAL_SRC_POS); + LogFlowThisFunc(("aDevice=%s\n", aDevice->i_getName().c_str())); + + /* + * We're not really holding it atm., just fake it. + */ + Assert(aDevice->i_getUnistate() == kHostUSBDeviceState_ReleasingToHost); + devLock.release(); + interruptWait(); + + return VINF_SUCCESS; +} + + +/** + * A device was added, we need to adjust mUdevPolls. + */ +void USBProxyBackendLinux::deviceAdded(ComObjPtr &aDevice, PUSBDEVICE pDev) +{ + AssertReturnVoid(aDevice); + AssertReturnVoid(!aDevice->isWriteLockOnCurrentThread()); + AutoReadLock devLock(aDevice COMMA_LOCKVAL_SRC_POS); + if (pDev->enmState == USBDEVICESTATE_USED_BY_HOST) + { + LogRel(("USBProxyBackendLinux: Device %04x:%04x (%s) isn't accessible. giving udev a few seconds to fix this...\n", + pDev->idVendor, pDev->idProduct, pDev->pszAddress)); + mUdevPolls = 10; /* (10 * 500ms = 5s) */ + } + + devLock.release(); +} + + +bool USBProxyBackendLinux::isFakeUpdateRequired() +{ + return true; +} + +int USBProxyBackendLinux::wait(RTMSINTERVAL aMillies) +{ + int vrc; + if (mUsingUsbfsDevices) + vrc = waitUsbfs(aMillies); + else + vrc = waitSysfs(aMillies); + return vrc; +} + + +/** String written to the wakeup pipe. */ +#define WAKE_UP_STRING "WakeUp!" +/** Length of the string written. */ +#define WAKE_UP_STRING_LEN ( sizeof(WAKE_UP_STRING) - 1 ) + +int USBProxyBackendLinux::waitUsbfs(RTMSINTERVAL aMillies) +{ + struct pollfd PollFds[2]; + + /* Cap the wait interval if we're polling for udevd changing device permissions. */ + if (aMillies > 500 && mUdevPolls > 0) + { + mUdevPolls--; + aMillies = 500; + } + + RT_ZERO(PollFds); + PollFds[0].fd = (int)RTFileToNative(mhFile); + PollFds[0].events = POLLIN; + PollFds[1].fd = (int)RTPipeToNative(mhWakeupPipeR); + PollFds[1].events = POLLIN | POLLERR | POLLHUP; + + int iRc = poll(&PollFds[0], 2, aMillies); + if (iRc == 0) + return VERR_TIMEOUT; + if (iRc > 0) + { + /* drain the pipe */ + if (PollFds[1].revents & POLLIN) + { + char szBuf[WAKE_UP_STRING_LEN]; + int vrc2 = RTPipeReadBlocking(mhWakeupPipeR, szBuf, sizeof(szBuf), NULL); + AssertRC(vrc2); + } + return VINF_SUCCESS; + } + return RTErrConvertFromErrno(errno); +} + + +int USBProxyBackendLinux::waitSysfs(RTMSINTERVAL aMillies) +{ +#ifdef VBOX_USB_WITH_SYSFS + int vrc = mpWaiter->Wait(aMillies); + if (vrc == VERR_TRY_AGAIN) + { + RTThreadYield(); + vrc = VINF_SUCCESS; + } + return vrc; +#else /* !VBOX_USB_WITH_SYSFS */ + return USBProxyService::wait(aMillies); +#endif /* !VBOX_USB_WITH_SYSFS */ +} + + +int USBProxyBackendLinux::interruptWait(void) +{ + AssertReturn(!isWriteLockOnCurrentThread(), VERR_GENERAL_FAILURE); + + AutoReadLock alock(this COMMA_LOCKVAL_SRC_POS); +#ifdef VBOX_USB_WITH_SYSFS + LogFlowFunc(("mUsingUsbfsDevices=%d\n", mUsingUsbfsDevices)); + if (!mUsingUsbfsDevices) + { + mpWaiter->Interrupt(); + LogFlowFunc(("Returning VINF_SUCCESS\n")); + return VINF_SUCCESS; + } +#endif /* VBOX_USB_WITH_SYSFS */ + int vrc = RTPipeWriteBlocking(mhWakeupPipeW, WAKE_UP_STRING, WAKE_UP_STRING_LEN, NULL); + if (RT_SUCCESS(vrc)) + RTPipeFlush(mhWakeupPipeW); + LogFlowFunc(("returning %Rrc\n", vrc)); + return vrc; +} + + +PUSBDEVICE USBProxyBackendLinux::getDevices(void) +{ + return USBProxyLinuxGetDevices(mDevicesRoot.c_str(), !mUsingUsbfsDevices); +} diff --git a/src/VBox/Main/src-server/linux/vbox-libhal.cpp b/src/VBox/Main/src-server/linux/vbox-libhal.cpp new file mode 100644 index 00000000..f48182c2 --- /dev/null +++ b/src/VBox/Main/src-server/linux/vbox-libhal.cpp @@ -0,0 +1,105 @@ +/* $Id: vbox-libhal.cpp $ */ +/** @file + * + * Module to dynamically load libhal and libdbus and load all symbols + * which are needed by VirtualBox. + */ + +/* + * Copyright (C) 2006-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 . + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "vbox-libhal.h" + +#include +#include + +/** + * Whether we have tried to load libdbus and libhal yet. This flag should only be set + * to "true" after we have either loaded both libraries and all symbols which we need, + * or failed to load something and unloaded and set back to zero pLibDBus and pLibHal. + */ +static bool gCheckedForLibHal = false; +/** + * Pointer to the libhal shared object. This should only be set once all needed libraries + * and symbols have been successfully loaded. + */ +static RTLDRMOD ghLibHal = 0; + +/** The following are the symbols which we need from libdbus and libhal. */ +void (*gDBusErrorInit)(DBusError *); +DBusConnection *(*gDBusBusGet)(DBusBusType, DBusError *); +void (*gDBusErrorFree)(DBusError *); +void (*gDBusConnectionUnref)(DBusConnection *); +LibHalContext *(*gLibHalCtxNew)(void); +dbus_bool_t (*gLibHalCtxSetDBusConnection)(LibHalContext *, DBusConnection *); +dbus_bool_t (*gLibHalCtxInit)(LibHalContext *, DBusError *); +char **(*gLibHalFindDeviceStringMatch)(LibHalContext *, const char *, const char *, int *, + DBusError *); +char *(*gLibHalDeviceGetPropertyString)(LibHalContext *, const char *, const char *, DBusError *); +void (*gLibHalFreeString)(char *); +void (*gLibHalFreeStringArray)(char **); +dbus_bool_t (*gLibHalCtxShutdown)(LibHalContext *, DBusError *); +dbus_bool_t (*gLibHalCtxFree)(LibHalContext *); + +bool gLibHalCheckPresence(void) +{ + RTLDRMOD hLibHal; + + if (ghLibHal != 0 && gCheckedForLibHal == true) + return true; + if (gCheckedForLibHal == true) + return false; + if (!RT_SUCCESS(RTLdrLoad(LIB_HAL, &hLibHal))) + { + return false; + } + if ( RT_SUCCESS(RTLdrGetSymbol(hLibHal, "dbus_error_init", (void **) &gDBusErrorInit)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "dbus_bus_get", (void **) &gDBusBusGet)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "dbus_error_free", (void **) &gDBusErrorFree)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "dbus_connection_unref", + (void **) &gDBusConnectionUnref)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_ctx_new", (void **) &gLibHalCtxNew)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_ctx_set_dbus_connection", + (void **) &gLibHalCtxSetDBusConnection)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_ctx_init", (void **) &gLibHalCtxInit)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_manager_find_device_string_match", + (void **) &gLibHalFindDeviceStringMatch)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_device_get_property_string", + (void **) &gLibHalDeviceGetPropertyString)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_free_string", (void **) &gLibHalFreeString)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_free_string_array", + (void **) &gLibHalFreeStringArray)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_ctx_shutdown", (void **) &gLibHalCtxShutdown)) + && RT_SUCCESS(RTLdrGetSymbol(hLibHal, "libhal_ctx_free", (void **) &gLibHalCtxFree)) + ) + { + ghLibHal = hLibHal; + gCheckedForLibHal = true; + return true; + } + else + { + RTLdrClose(hLibHal); + gCheckedForLibHal = true; + return false; + } +} -- cgit v1.2.3