Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/app.pro
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ SOURCES += \
backend/usbforwardingtunnel.cpp \
backend/usbforwardingenvironment.cpp \
backend/usbforwardingbackend.cpp \
backend/usbforwardingelevation.cpp \
backend/usbforwardingmanager.cpp \
wm.cpp

HEADERS += \
Expand Down Expand Up @@ -254,7 +256,9 @@ HEADERS += \
backend/usbforwardingcapability.h \
backend/pairedcertificate.h \
backend/usbforwardingenvironment.h \
backend/usbforwardingbackend.h
backend/usbforwardingbackend.h \
backend/usbforwardingelevation.h \
backend/usbforwardingmanager.h

# Platform-specific renderers and decoders
ffmpeg {
Expand Down
181 changes: 160 additions & 21 deletions app/backend/usbforwardingbackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,38 @@
#include <QProcess>
#include <QRegularExpression>
#include <QTimer>
#include <mutex>
#include <thread>

#ifdef Q_OS_WIN
#include <windows.h>
#include <shellapi.h>
namespace {

// Elevation can sit behind a UAC prompt for a long time; the wait is on a
// worker thread, so a generous bound costs nothing.
constexpr int kElevationTimeoutMs = 2 * 60 * 1000;

constexpr int kElevationHardLimitMs = 10 * 60 * 1000;

#ifdef USBFWD_TESTING
std::mutex s_HooksMutex;
UsbForwardingBackend::TestHooks s_Hooks;

UsbForwardingBackend::TestHooks currentHooks()
{
std::lock_guard<std::mutex> lock(s_HooksMutex);
return s_Hooks;
}
#endif

namespace {
QString locateUsbipdOrHook()
{
#ifdef USBFWD_TESTING
const auto hooks = currentHooks();
if (hooks.locateUsbipd) {
return hooks.locateUsbipd();
}
#endif
return UsbForwardingEnvironment::locateUsbipd();
}

// Parses "USB\VID_054C&PID_0CE6\..." into "054c:0ce6".
QString vidPidFromInstanceId(const QString &instanceId)
Expand All @@ -32,10 +57,56 @@ QString vidPidFromInstanceId(const QString &instanceId)
}
return vidMatch.captured(1).toLower() + QLatin1Char(':') + pidMatch.captured(1).toLower();
}

} // namespace

UsbForwardingBackend::UsbForwardingBackend(QObject *parent) : QObject(parent) {}
#ifdef USBFWD_TESTING
void UsbForwardingBackend::setTestHooks(TestHooks hooks)
{
std::lock_guard<std::mutex> lock(s_HooksMutex);
s_Hooks = std::move(hooks);
}
#endif

struct UsbForwardingBackend::DeliveryLink
{
std::mutex mutex;
UsbForwardingBackend *backend = nullptr;
};

// Identity of a device across ports and reboots. A device with a real serial
// number gets "vid:pid#serial". A device WITHOUT one gets plain "vid:pid", so
// serial-less twins of the same model are indistinguishable and share it.
QString UsbForwardingBackend::identityFromInstanceId(const QString &instanceId)
{
const QString vidPid = vidPidFromInstanceId(instanceId);
if (vidPid.isEmpty()) {
return QString();
}
// "USB\VID_xxxx&PID_yyyy\<third>": a real serial number has no '&'.
// Windows invents ids such as "5&1a2b&0&2" for devices without a serial;
// those depend on the port, so they are not an identity.
const QStringList parts = instanceId.split(QLatin1Char('\\'));
if (parts.size() >= 3) {
static const QRegularExpression serialRe(QStringLiteral("\\A[0-9A-Za-z._-]{1,64}\\z"));
const QString serial = parts.at(2);
if (serialRe.match(serial).hasMatch()) {
return vidPid + QLatin1Char('#') + serial.toLower();
}
}
return vidPid;
}

UsbForwardingBackend::UsbForwardingBackend(QObject *parent)
: QObject(parent), m_Link(std::make_shared<DeliveryLink>())
{
m_Link->backend = this;
}

UsbForwardingBackend::~UsbForwardingBackend()
{
std::lock_guard<std::mutex> lock(m_Link->mutex);
m_Link->backend = nullptr;
}

UsbForwardingBackend *UsbForwardingBackend::get()
{
Expand Down Expand Up @@ -112,6 +183,7 @@ QVariantList UsbForwardingBackend::parseUsbipdState(const QByteArray &stateJson,
device.insert(QStringLiteral("description"), description);
device.insert(QStringLiteral("instanceId"), instanceId);
device.insert(QStringLiteral("vidPid"), vidPidFromInstanceId(instanceId));
device.insert(QStringLiteral("identity"), identityFromInstanceId(instanceId));
device.insert(QStringLiteral("isBound"), isBound);
device.insert(QStringLiteral("isConnected"), isConnected);
device.insert(QStringLiteral("isAttached"), isAttached);
Expand Down Expand Up @@ -146,8 +218,17 @@ void UsbForwardingBackend::refresh()
if (m_Busy) {
return;
}
#ifdef USBFWD_TESTING
{
const auto hooks = currentHooks();
if (hooks.refresh) {
hooks.refresh();
return;
}
}
#endif
#ifdef Q_OS_WIN
const QString exe = UsbForwardingEnvironment::locateUsbipd();
const QString exe = locateUsbipdOrHook();
if (exe.isEmpty()) {
m_Devices.clear();
emit devicesChanged();
Expand Down Expand Up @@ -207,32 +288,90 @@ void UsbForwardingBackend::refresh()
void UsbForwardingBackend::runElevated(const QString &arguments, const QString &successMessage)
{
#ifdef Q_OS_WIN
const QString exe = UsbForwardingEnvironment::locateUsbipd();
if (m_Busy) {
emit operationFinished(false, tr("Another USB operation is still running."));
return;
}
const QString exe = locateUsbipdOrHook();
if (exe.isEmpty()) {
emit operationFinished(false, tr("usbipd-win is not installed."));
return;
}
// "exe" is the validated absolute path; "arguments" came from
// bindArguments()/unbindArguments(). Nothing else reaches the UAC prompt.
const std::wstring wideExe = QDir::toNativeSeparators(exe).toStdWString();
const std::wstring wideArgs = arguments.toStdWString();
const std::wstring wideDir = QDir::toNativeSeparators(QFileInfo(exe).absolutePath()).toStdWString();
const HINSTANCE result = ShellExecuteW(nullptr, L"runas", wideExe.c_str(), wideArgs.c_str(),
wideDir.c_str(), SW_HIDE);
const bool ok = reinterpret_cast<INT_PTR>(result) > 32;
if (ok) {
emit operationFinished(true, successMessage);
QTimer::singleShot(1500, this, [this] { refresh(); });
} else {
emit operationFinished(false, tr("The elevation was cancelled or failed."));
const QString workDir = QFileInfo(exe).absolutePath();
UsbForwarding::ElevatedLauncher launcher = UsbForwarding::launchElevated;
int timeoutMs = kElevationTimeoutMs;
int hardLimitMs = kElevationHardLimitMs;
#ifdef USBFWD_TESTING
{
const auto hooks = currentHooks();
if (hooks.launcher) launcher = hooks.launcher;
if (hooks.elevationTimeoutMs > 0) timeoutMs = hooks.elevationTimeoutMs;
if (hooks.elevationHardLimitMs > 0) hardLimitMs = hooks.elevationHardLimitMs;
}
#endif

setBusy(true);
m_SlowReported = false;
// A plain detached thread, not a thread pool: the application must be able
// to quit while a UAC prompt is still open, and a pool would make exit wait
// for it. Results come back through the link, which the destructor clears
// under the same lock, so delivery can never touch a deleted backend.
const std::shared_ptr<DeliveryLink> link = m_Link;
const auto deliver = [link](std::function<void(UsbForwardingBackend *)> call) {
std::lock_guard<std::mutex> lock(link->mutex);
if (UsbForwardingBackend *backend = link->backend) {
QMetaObject::invokeMethod(backend, [backend, call] { call(backend); }, Qt::QueuedConnection);
}
};
std::thread([deliver, launcher, exe, arguments, workDir, timeoutMs, hardLimitMs, successMessage] {
// Blocks this thread for as long as the UAC prompt and usbipd take.
const UsbForwarding::ElevationResult result = launcher(
exe, arguments, workDir, timeoutMs, hardLimitMs,
[deliver] { deliver([](UsbForwardingBackend *b) { b->reportSlow(); }); });
deliver([result, successMessage](UsbForwardingBackend *b) { b->finishElevated(result, successMessage); });
}).detach();
#else
Q_UNUSED(arguments);
Q_UNUSED(successMessage);
emit operationFinished(false, tr("USB forwarding is not supported on this platform."));
#endif
}

void UsbForwardingBackend::reportSlow()
{
// The process may still change usbipd's state: stay busy (no second
// elevated operation can start) until it really exits.
m_SlowReported = true;
emit operationFinished(false, tr("usbipd is still running; the device list will update when it finishes."));
}

void UsbForwardingBackend::finishElevated(const UsbForwarding::ElevationResult &result,
const QString &successMessage)
{
const bool slowReported = m_SlowReported;
m_SlowReported = false;
setBusy(false);
if (result.timedOut) {
if (!slowReported) {
emit operationFinished(false, tr("usbipd is still running; the device list will update when it finishes."));
}
// It exited late, or we gave up waiting: either way look at the real state now.
refresh();
} else if (result.succeeded()) {
emit operationFinished(true, successMessage);
// The list reflects what usbipd now says, not a guess at how long it takes.
refresh();
} else if (result.launched) {
emit operationFinished(false, tr("usbipd failed (exit %1).").arg(result.exitCode));
} else if (result.cancelled) {
emit operationFinished(false, tr("The elevation was cancelled."));
} else {
emit operationFinished(false, tr("The elevation was cancelled or failed."));
}
}

void UsbForwardingBackend::bind(const QString &busId)
{
if (busId.isEmpty()) {
Expand All @@ -243,7 +382,7 @@ void UsbForwardingBackend::bind(const QString &busId)
emit operationFinished(false, tr("This USB device cannot be shared."));
return;
}
runElevated(arguments, tr("Requested sharing. Refreshing device list..."));
runElevated(arguments, tr("The device is now shared."));
}

void UsbForwardingBackend::unbind(const QString &busId, const QString &persistedGuid)
Expand All @@ -256,5 +395,5 @@ void UsbForwardingBackend::unbind(const QString &busId, const QString &persisted
emit operationFinished(false, tr("This USB device cannot be unshared."));
return;
}
runElevated(arguments, tr("Requested stop sharing. Refreshing device list..."));
runElevated(arguments, tr("The device is no longer shared."));
}
45 changes: 44 additions & 1 deletion app/backend/usbforwardingbackend.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@

// Derived from qiin2333/moonlight-qt (GPL-3.0).

#include "usbforwardingelevation.h"

#include <QObject>
#include <QString>
#include <QVariantList>
#include <functional>
#include <memory>

// USB device sharing orchestration (Windows only; usbipd-win does the
// sharing). Lists devices from `usbipd state` JSON, and binds or unbinds
Expand All @@ -23,6 +27,9 @@
// description device description
// instanceId Windows instance ID (USB\VID_XXXX&PID_YYYY\...)
// vidPid "xxxx:yyyy"
// identity stable identity for the settings list: "xxxx:yyyy", or
// "xxxx:yyyy#serial" when the device has a real serial number.
// Unlike busId it does not change when the device moves port.
// isBound bool, PersistedGuid is not empty
// isConnected bool, present in the current listing
// isAttached bool, in use by a client right now
Expand Down Expand Up @@ -53,6 +60,30 @@ class UsbForwardingBackend : public QObject
// validated bus id or GUID. Return an empty string for invalid input.
static QString bindArguments(const QString &busId);
static QString unbindArguments(const QString &busId, const QString &persistedGuid);
// Stable device identity from a Windows instance ID (see "identity" above).
// Empty when the instance ID has no usable VID/PID.
// NOTE for the settings UI: two devices of the same model that have no
// serial number share the identity "vid:pid". Selecting one selects all
// such twins, and the UI has to say so.
static QString identityFromInstanceId(const QString &instanceId);

#ifdef USBFWD_TESTING
// Test seams, compiled only into the unit tests (USBFWD_TESTING is defined
// in their .pro files, never in the application). Hooks are process wide;
// pass a default-constructed value to restore production behaviour.
struct TestHooks
{
std::function<QString()> locateUsbipd; // replaces the usbipd.exe lookup
UsbForwarding::ElevatedLauncher launcher; // replaces the elevated launch
std::function<void()> refresh; // replaces refresh()
int elevationTimeoutMs = 0; // 0 = production value
int elevationHardLimitMs = 0; // 0 = production value
};
static void setTestHooks(TestHooks hooks);
// Tests make their own instances (the application uses get()).
explicit UsbForwardingBackend(QObject *parent = nullptr);
#endif
~UsbForwardingBackend() override;

Q_INVOKABLE void refresh();
Q_INVOKABLE void bind(const QString &busId);
Expand All @@ -69,14 +100,26 @@ class UsbForwardingBackend : public QObject
void operationFinished(bool success, const QString &message);

private:
#ifndef USBFWD_TESTING
explicit UsbForwardingBackend(QObject *parent = nullptr);
#endif
struct DeliveryLink;
void reportSlow();

void setBusy(bool busy);
void setError(const QString &error);
// Runs the validated usbipd.exe elevated with the given fixed arguments.
// Runs the validated usbipd.exe elevated with the given fixed arguments on
// a worker thread (the UAC prompt and the wait never block the caller) and
// reports the real result through operationFinished(). On success the
// device list is refreshed from usbipd; on failure it is left alone.
void runElevated(const QString &arguments, const QString &successMessage);

void finishElevated(const UsbForwarding::ElevationResult &result, const QString &successMessage);

QVariantList m_Devices;
bool m_Busy = false;
bool m_SlowReported = false;
QString m_Error;
// Lets the elevation worker post its result without racing our deletion.
std::shared_ptr<DeliveryLink> m_Link;
};
Loading
Loading