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
117 changes: 64 additions & 53 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,6 @@ jobs:
- name: Install
run: cmake --install cmake-build-ci --config ${{ env.CMAKE_BUILD_CONFIG }} --prefix cmake-build-ci/install

- name: Upload install artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: install-${{ matrix.name }}
path: cmake-build-ci/install
if-no-files-found: error

- name: Upload report artifact
if: >-
always() &&
Expand All @@ -483,6 +476,67 @@ jobs:
path: cmake-build-ci/reports
if-no-files-found: error

package_consumer:
name: Installed Package Consumer (Linux)
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
submodules: recursive

- name: Install package dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
cmake \
libevdev-dev \
libx11-dev \
libxtst-dev \
ninja-build \
pkg-config

# This intentionally uses a separate BUILD_TESTS=OFF configuration. Test
# builds add lizardbyte-common for test support and therefore cannot prove
# that a normal installed package is independently consumable. Debug keeps
# this packaging regression isolated from the open optimized-GCC P2; switch
# this job to Release when that warning is resolved.
- name: Configure tests-disabled package
run: |
cmake \
-DBUILD_DOCS=OFF \
-DBUILD_EXAMPLES=OFF \
-DBUILD_TESTS=OFF \
-DCMAKE_BUILD_TYPE:STRING=Debug \
-DCMAKE_INSTALL_PREFIX="${GITHUB_WORKSPACE}/cmake-build-package/install" \
-DLIBVIRTUALHID_BUILD_TOOLS=OFF \
-DLIBVIRTUALHID_ENABLE_PACKAGING=OFF \
-B cmake-build-package \
-G Ninja \
-S .

- name: Build and install package
run: |
cmake --build cmake-build-package --parallel 2
cmake --install cmake-build-package
test ! -d cmake-build-package/install/lib/cmake/lizardbyte-common

# A separate CMake project and real symbol reference force find_package,
# imported-target generation, compilation, and the final static-library link.
- name: Configure, compile, and link downstream consumer
run: |
cmake \
-DCMAKE_FIND_USE_PACKAGE_REGISTRY=OFF \
-DCMAKE_FIND_USE_SYSTEM_PACKAGE_REGISTRY=OFF \
-DCMAKE_PREFIX_PATH="${GITHUB_WORKSPACE}/cmake-build-package/install" \
-B cmake-build-package-consumer \
-G Ninja \
-S tests/package-consumer
cmake --build cmake-build-package-consumer --parallel 2

windows_driver:
name: Windows Driver Installer
needs: setup_release
Expand Down Expand Up @@ -759,65 +813,27 @@ jobs:
always() &&
needs.setup_release.outputs.publish_release == 'true' &&
needs.build.result == 'success' &&
needs.package_consumer.result == 'success' &&
needs.windows_driver.result == 'success' &&
startsWith(github.repository, 'LizardByte/')
needs:
- build
- package_consumer
- windows_driver
- setup_release
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- name: Download install artifact (Linux-GCC)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: install-Linux-GCC
path: install-Linux-GCC

- name: Download install artifact (Linux-Clang)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: install-Linux-Clang
path: install-Linux-Clang

- name: Download install artifact (macOS)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: install-macOS
path: install-macOS

- name: Download install artifact (Windows-MinGW-UCRT64)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: install-Windows-MinGW-UCRT64
path: install-Windows-MinGW-UCRT64

- name: Download install artifact (Windows-MSVC)
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: install-Windows-MSVC
path: install-Windows-MSVC

- name: Download Windows driver installer artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: windows-driver-installer
path: windows-driver-installer

- name: Package install artifacts
- name: Prepare release artifact
run: |
mkdir -p artifacts
for name in Linux-GCC Linux-Clang macOS Windows-MinGW-UCRT64 Windows-MSVC; do
release_name="${name}"
case "${name}" in
Windows-MinGW-UCRT64) release_name="Windows-AMD64-MinGW-UCRT64" ;;
Windows-MSVC) release_name="Windows-AMD64-MSVC" ;;
esac
zip -r \
"artifacts/libvirtualhid-${release_name}.zip" \
"install-${name}"
done
cp windows-driver-installer/*.msi artifacts/

- name: Validate release metadata
Expand All @@ -829,12 +845,7 @@ jobs:
test -n "${RELEASE_TAG}"
test -n "${RELEASE_VERSION}"
test "${RELEASE_COMMIT}" = "${GITHUB_SHA}"
test -s "artifacts/libvirtualhid-Linux-GCC.zip"
test -s "artifacts/libvirtualhid-Linux-Clang.zip"
test -s "artifacts/libvirtualhid-macOS.zip"
test -s "artifacts/libvirtualhid-Windows-AMD64-driver-installer.msi"
test -s "artifacts/libvirtualhid-Windows-AMD64-MinGW-UCRT64.zip"
test -s "artifacts/libvirtualhid-Windows-AMD64-MSVC.zip"

- name: Create/Update GitHub Release
if: needs.setup_release.outputs.publish_release == 'true'
Expand Down
17 changes: 15 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ set(LIBVIRTUALHID_USES_THREADS OFF)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LIBVIRTUALHID_USES_THREADS ON)
endif()
set(LIBVIRTUALHID_USES_XTEST OFF)

#
# Additional setup for coverage
Expand All @@ -71,7 +72,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND BUILD_TESTS AND NOT CMAKE_CXX_CO
endif()

set(LIBVIRTUALHID_USES_LIZARDBYTE_COMMON OFF)
if(WIN32)
if(WIN32 AND LIBVIRTUALHID_BUILD_WINDOWS_BROKER)
set(LIBVIRTUALHID_USES_LIZARDBYTE_COMMON ON)
endif()

Expand All @@ -80,7 +81,7 @@ if(LIBVIRTUALHID_USES_LIZARDBYTE_COMMON OR BUILD_TESTS)
${BUILD_TESTS}
CACHE BOOL "Build lizardbyte-common GoogleTest support helpers" FORCE)
set(LIZARDBYTE_COMMON_INSTALL
${LIBVIRTUALHID_INSTALL}
OFF
CACHE BOOL "Install lizardbyte-common targets and package configuration" FORCE)
if(NOT TARGET lizardbyte::common)
add_subdirectory(third-party/lizardbyte-common)
Expand Down Expand Up @@ -157,6 +158,18 @@ endif()
# Package config
#
if(LIBVIRTUALHID_INSTALL)
set(LIBVIRTUALHID_CONFIG_DEPENDENCIES "")
if(LIBVIRTUALHID_USES_THREADS)
string(APPEND LIBVIRTUALHID_CONFIG_DEPENDENCIES
"find_dependency(Threads)\n"
"find_dependency(PkgConfig)\n"
"pkg_check_modules(LIBEVDEV REQUIRED IMPORTED_TARGET libevdev)\n")
endif()
if(LIBVIRTUALHID_USES_XTEST)
string(APPEND LIBVIRTUALHID_CONFIG_DEPENDENCIES
"find_dependency(X11 COMPONENTS Xtst)\n")
endif()

install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE"
DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/libvirtualhid"
COMPONENT library)
Expand Down
8 changes: 1 addition & 7 deletions cmake/libvirtualhid-config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,7 @@

include(CMakeFindDependencyMacro)

if(@LIBVIRTUALHID_USES_THREADS@)
find_dependency(Threads)
find_dependency(PkgConfig)
pkg_check_modules(LIBEVDEV REQUIRED IMPORTED_TARGET libevdev)
endif()

find_dependency(lizardbyte-common)
@LIBVIRTUALHID_CONFIG_DEPENDENCIES@

include("${CMAKE_CURRENT_LIST_DIR}/libvirtualhid-targets.cmake")

Expand Down
21 changes: 17 additions & 4 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,23 @@ normalized gamepad output such as rumble, RGB LED, adaptive trigger, trigger
rumble, and raw report events delivered through the normal callback path. Button
controls are momentary by default, so they behave like physical gamepad buttons;
on Windows, the UI also displays broker license status and can activate,
refresh, or deactivate a machine license without elevation. Every Windows UMDF
gamepad creation requires a current successful license validation response and
there is no offline grace period. Purchase and account-management buttons use
the compiled URLs in
refresh, or deactivate a machine license without elevation. Windows UMDF
gamepad creation requires a current machine authorization, but does not perform
an online request per controller. The broker validates in the background at
startup and once per day. If Polar cannot be reached, it retries every 60
seconds. Existing gamepads are retained for one hour, but a new gamepad can be
created only when no licensed gamepad is active. When the outage reaches one
hour, the broker removes excess licensed gamepads and retains at most one.
If the broker service restarts, it removes gamepads left by the previous broker
instance before accepting new creation requests. Failed removals are retried.
Yearly licenses stop at their exact `expires_at`; this outage behavior never
extends expiration. Lifetime licenses have no expiration. Polar server time,
Windows uptime, and a per-boot marker track time without relying on the
user-adjustable Windows date. After Windows restarts, a yearly license must
reconnect to Polar before gamepad creation; lifetime licenses can use the
one-gamepad outage fallback. A confirmed missing, revoked, disabled, expired, or
mismatched entitlement invalidates the license and removes all licensed gamepads. Purchase and
account-management buttons use the compiled URLs in
`src/platform/windows/shared/lvh_windows_broker_config.hpp`.
Enable `Lock buttons` to click-to-toggle behavior for held inputs.
The resizable window supports a compact width. Its device and control panels
Expand Down
61 changes: 47 additions & 14 deletions docs/windows-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@ submit/destroy requests include that token so stale or unrelated clients cannot
control devices they did not create. Input reports are submitted through VHF,
and HID output writes are normalized back to the C++ output callback path.

The driver rejects gamepad create and destroy IOCTLs unless the requestor token
contains the `NT SERVICE\libvirtualhid_broker` service SID. On the first boot
after installation, before Windows applies a newly configured service SID to
the process token, the driver instead requires the requestor PID to match the
SCM-registered, currently running broker service. Administrators still control
installation, repair, replacement, and service diagnostics through the normal
Windows service and driver-management tools, but they are not a separate runtime
bypass for creating or destroying virtual devices.
The driver rejects gamepad create, destroy, and broker-instance reset IOCTLs
unless the requestor token contains the `NT SERVICE\libvirtualhid_broker`
service SID. On the first boot after installation, before Windows applies a
newly configured service SID to the process token, the driver instead requires
the requestor PID to match the SCM-registered, currently running broker service.
Administrators still control installation, repair, replacement, and service
diagnostics through the normal Windows service and driver-management tools, but
they are not a separate runtime bypass for creating or destroying virtual
devices.

The library and installed driver must use the same control-protocol version.
Protocol version 2 expands the report-descriptor capacity to 2048 bytes for the
Expand All @@ -94,7 +95,10 @@ parents that target to the control-file handle that created it. If the creating
process exits or crashes, Windows cleans up gamepads that were not explicitly
destroyed. In brokered driver packages, the broker owns that control-file handle.
The broker tracks the requesting client process for each created device and
destroys broker-owned devices when that client process exits unexpectedly.
destroys broker-owned devices when that client process exits unexpectedly. A new
broker process first asks the driver to remove every gamepad left by the previous
broker instance and refuses new creation until that reset succeeds. Clients must
recreate their gamepads after the broker service restarts.

The backend reports `requires_installed_driver = true` and only advertises
gamepad/output-report support when the broker is reachable and the control
Expand Down Expand Up @@ -276,8 +280,29 @@ opens the shared persistent Polar Checkout Link. Account management opens the
[LizardByte LLC Polar customer portal](https://polar.sh/lizardbyte-llc/portal),
where customers can manage their five allowed machine activations.

Normal Windows UMDF gamepad creation requires a current successful license
validation response before the broker calls the driver. The sole exception is
Normal Windows UMDF gamepad creation requires a current machine authorization,
but controller creation itself does not contact Polar. The broker validates the
saved activation immediately after service startup and then once per day in the
background. If validation cannot complete because of a temporary network or
provider failure, the broker retries every 60 seconds. Controllers that already
exist are retained for one hour unless the broker service restarts, but no
additional controller can be created while at least one licensed controller
remains active. When the outage reaches one hour, the broker removes excess
licensed controllers and retains at most one until online validation succeeds.
Failed driver destruction requests remain tracked and are retried instead of
being treated as successful revocations.

Polar's HTTPS `Date` response header supplies trusted time when a new
authorization is issued. A yearly license ends exactly at its reported
`expires_at`; the one-hour outage retention does not extend that expiration. A
lifetime license has no calendar expiration. The broker advances Polar's trusted
timestamp using Windows uptime and stores a random marker in a volatile registry
key for the current boot session. This works across broker service restarts and
includes sleep or hibernation, but never consults the user-adjustable Windows
date. After Windows restarts, the marker changes, so a yearly license must
reconnect to Polar before gamepad creation; lifetime licenses can use the
one-gamepad outage fallback. Explicit validation requests always contact the
provider. The sole exception to normal licensing is
for CI runners where the broker service itself has the `GITHUB_ACTIONS`
environment marker. That environment receives one machine-scoped five-minute
evaluation window beginning with its first unlicensed creation attempt. The
Expand All @@ -290,7 +315,13 @@ Polar's `limit_activations` value is the machine limit and is configured as `5`
on both license-key benefits. The broker gives yearly and lifetime licenses the
same full local access when the provider reports the key status as `granted`. Polar
revokes a subscription benefit when its entitlement ends. Licensed access has
no local active-device cap, and there is no production offline grace period.
no local active-device cap after successful validation. A definitive missing
activation, revoked or disabled key, activation mismatch, disallowed benefit,
explicit deactivation, or exact yearly expiration prevents new gamepads and
causes the broker to destroy existing licensed gamepads. A timeout or other
transient provider failure starts the one-hour retention period and one-gamepad
creation limit instead of immediately revoking existing controllers. WinHTTP resolve, connect, send, and receive
operations have explicit timeouts of 5, 5, 5, and 10 seconds respectively.

## Profile Compatibility

Expand Down Expand Up @@ -338,8 +369,10 @@ label because VHF does not provide a product/manufacturer string callback.
- The published Windows driver installer is AMD64-only. Windows ARM64 release
packages require a Microsoft dashboard signing path that is not part of the
current Azure Trusted Signing workflow.
- Every production gamepad creation requires a successful online license
validation response. There is no offline grace period.
- A temporary Polar outage limits a previously activated machine to one active
licensed gamepad until validation succeeds. Yearly licenses receive no
post-expiration grace. Definitive invalidation or exact expiration prevents
new gamepads and removes active licensed gamepads.

## Signing

Expand Down
16 changes: 7 additions & 9 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if(LIBVIRTUALHID_USES_THREADS)
pkg_check_modules(LIBEVDEV REQUIRED IMPORTED_TARGET libevdev)
endif()
if(LIBVIRTUALHID_ENABLE_XTEST)
find_package(X11 QUIET)
find_package(X11 QUIET COMPONENTS Xtst)
endif()

target_sources(${PROJECT_NAME}
Expand All @@ -34,26 +34,25 @@ if(LIBVIRTUALHID_USES_THREADS)
PRIVATE
PkgConfig::LIBEVDEV)
if(LIBVIRTUALHID_ENABLE_XTEST AND X11_FOUND AND X11_XTest_FOUND)
set(LIBVIRTUALHID_USES_XTEST ON PARENT_SCOPE)
target_compile_definitions(${PROJECT_NAME}
PRIVATE
LIBVIRTUALHID_HAVE_XTEST=1)
target_include_directories(${PROJECT_NAME}
PRIVATE
${X11_INCLUDE_DIR}
${X11_XTest_INCLUDE_PATH})
target_link_libraries(${PROJECT_NAME}
PRIVATE
${X11_LIBRARIES}
${X11_XTest_LIB})
X11::X11
X11::Xtst)
endif()
elseif(WIN32)
target_sources(${PROJECT_NAME}
PRIVATE
"${PROJECT_SOURCE_DIR}/third-party/lizardbyte-common/src/common/env.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/windows_backend.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/windows_broker_client.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/windows_license.cpp")
target_include_directories(${PROJECT_NAME}
PRIVATE
"${PROJECT_SOURCE_DIR}/third-party/lizardbyte-common/src/include"
"${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/shared")
target_compile_definitions(${PROJECT_NAME}
PRIVATE
Expand All @@ -63,8 +62,7 @@ elseif(WIN32)
target_link_libraries(${PROJECT_NAME}
PRIVATE
advapi32
setupapi
lizardbyte::common)
setupapi)
elseif(APPLE)
target_sources(${PROJECT_NAME}
PRIVATE
Expand Down
Loading
Loading