mirror of
https://github.com/joleuger/vuinputd.git
synced 2026-07-18 17:04:19 +00:00
Compare commits
75 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9b4d1a07b | ||
|
|
a563c442f8 | ||
|
|
ef060ce9d6 | ||
|
|
2c8d0f9af1 | ||
|
|
87694e1a96 | ||
|
|
341ba07523 | ||
|
|
aea990946d | ||
|
|
152ffd9711 | ||
|
|
6eb5a2985a | ||
|
|
e6c38be172 | ||
|
|
218fbdbcbf | ||
|
|
919b420e43 | ||
|
|
9e55cd84ab | ||
|
|
080a72906d | ||
|
|
8c40fdc120 | ||
|
|
5e79b0d1cb | ||
|
|
ab72e0bfc8 | ||
|
|
53eb3c7101 | ||
|
|
c5693d534f | ||
|
|
86c4e1c712 | ||
|
|
1cbd0540ab | ||
|
|
b75806cc09 | ||
|
|
8e0a00e817 | ||
|
|
0ce2034407 | ||
|
|
65802bfa63 | ||
|
|
0fda223d4e | ||
|
|
96719c0a9c | ||
|
|
5cffaec3da | ||
|
|
8d4a0c9413 | ||
|
|
6f60635ff2 | ||
|
|
33b0a016c3 | ||
|
|
21bcff7fa3 | ||
|
|
e4eb1d77da | ||
|
|
7182cac6fc | ||
|
|
88737be70b | ||
|
|
643c506a23 | ||
|
|
9070c4e836 | ||
|
|
b5889e6189 | ||
|
|
134b41fa98 | ||
|
|
4c4fefb0c6 | ||
|
|
8ada4a8624 | ||
|
|
8d1627ee42 | ||
|
|
e5bb81f86e | ||
|
|
62b5de7bf4 | ||
|
|
4d87dfea44 | ||
|
|
6e23a02e3f | ||
|
|
fd55bffddb | ||
|
|
350a80644a | ||
|
|
6bde733b09 | ||
|
|
b4c6c32431 | ||
|
|
0d0d1b489c | ||
|
|
d6423197bb | ||
|
|
3283aa0dc2 | ||
|
|
a1667bf4ba | ||
|
|
d139906834 | ||
|
|
11144bd694 | ||
|
|
0062a1741e | ||
|
|
5a675aac34 | ||
|
|
5277a13904 | ||
|
|
f8bdb04d7e | ||
|
|
a7a207e029 | ||
|
|
84df502d25 | ||
|
|
7fbb1616ed | ||
|
|
66d58f074f | ||
|
|
2c85f82448 | ||
|
|
ccd4adc5ec | ||
|
|
7c25eb2cfe | ||
|
|
00b80691e4 | ||
|
|
9b1b93d446 | ||
|
|
2704626d77 | ||
|
|
76ba60a614 | ||
|
|
0c91a05ef7 | ||
|
|
04bd88f179 | ||
|
|
77c3452bbb | ||
|
|
4fbb4d7389 |
83 changed files with 7358 additions and 320 deletions
101
.github/workflows/codeql.yml
vendored
Normal file
101
.github/workflows/codeql.yml
vendored
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL Advanced"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '30 12 * * 2'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze (${{ matrix.language }})
|
||||||
|
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||||
|
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||||
|
# - https://gh.io/supported-runners-and-hardware-resources
|
||||||
|
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||||
|
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||||
|
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||||
|
permissions:
|
||||||
|
# required for all workflows
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
# required to fetch internal or private CodeQL packs
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
# only required for workflows in private repositories
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- language: actions
|
||||||
|
build-mode: none
|
||||||
|
- language: rust
|
||||||
|
build-mode: none
|
||||||
|
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||||
|
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||||
|
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||||
|
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||||
|
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||||
|
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||||
|
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||||
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||||
|
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||||
|
# or others). This is typically only required for manual builds.
|
||||||
|
# - name: Setup runtime (example)
|
||||||
|
# uses: actions/setup-example@v1
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
build-mode: ${{ matrix.build-mode }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
# If the analyze step fails for one of the languages you are analyzing with
|
||||||
|
# "We were unable to automatically build your code", modify the matrix above
|
||||||
|
# to set the build mode to "manual" for that language. Then modify this step
|
||||||
|
# to build your code.
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
- name: Run manual build steps
|
||||||
|
if: matrix.build-mode == 'manual'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||||
|
'languages you are analyzing, replace this with the commands to build' \
|
||||||
|
'your code, for example:'
|
||||||
|
echo ' make bootstrap'
|
||||||
|
echo ' make release'
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
66
.github/workflows/debian-package.yml
vendored
Normal file
66
.github/workflows/debian-package.yml
vendored
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
name: Debian Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
DEB_BUILD_OPTIONS: nocheck
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-deb:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install build dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
# cargo deps
|
||||||
|
sudo apt install -y \
|
||||||
|
build-essential \
|
||||||
|
devscripts \
|
||||||
|
debhelper \
|
||||||
|
dh-cargo \
|
||||||
|
cargo \
|
||||||
|
rustc \
|
||||||
|
pkg-config \
|
||||||
|
libudev-dev \
|
||||||
|
libfuse3-dev \
|
||||||
|
fuse3 \
|
||||||
|
libclang-dev
|
||||||
|
# debian packages, if packages are not downloaded via cargo
|
||||||
|
sudo apt install -y librust-bindgen-dev librust-nix-dev librust-libc-dev librust-time-dev librust-log-dev librust-env-logger-dev librust-libudev-dev librust-regex-dev librust-async-channel-dev librust-futures-dev librust-async-io-dev librust-anyhow-dev librust-clap-dev librust-base64-dev librust-smallvec-dev librust-async-trait-dev
|
||||||
|
|
||||||
|
- name: Show versions (debug)
|
||||||
|
run: |
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
dpkg-buildpackage --version
|
||||||
|
|
||||||
|
- name: Build Debian package
|
||||||
|
run: |
|
||||||
|
dpkg-buildpackage -us -uc -b
|
||||||
|
|
||||||
|
- name: Collect artifacts
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts
|
||||||
|
mv ../*.deb artifacts/ || true
|
||||||
|
mv ../*.buildinfo artifacts/ || true
|
||||||
|
mv ../*.changes artifacts/ || true
|
||||||
|
|
||||||
|
- name: Upload Debian artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: vuinputd-debian-package
|
||||||
|
path: artifacts/
|
||||||
3
.github/workflows/rust.yml
vendored
3
.github/workflows/rust.yml
vendored
|
|
@ -6,6 +6,9 @@ on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "main" ]
|
branches: [ "main" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
|
|
||||||
1244
Cargo.lock
generated
Normal file
1244
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
10
README.md
10
README.md
|
|
@ -1,3 +1,5 @@
|
||||||
|
|
||||||
|
[](https://deepwiki.com/joleuger/vuinputd)
|
||||||
# vuinputd
|
# vuinputd
|
||||||
|
|
||||||
**Run Sunshine and other uinput-based apps inside containers — with full input isolation and zero kernel patches.**
|
**Run Sunshine and other uinput-based apps inside containers — with full input isolation and zero kernel patches.**
|
||||||
|
|
@ -75,8 +77,10 @@ sequenceDiagram
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
See [docs/BUILD.md](https://github.com/joleuger/vuinputd/blob/main/docs/BUILD.md) for a short build and installation guide.
|
See [docs/BUILD.md](https://github.com/joleuger/vuinputd/blob/main/docs/BUILD.md) for a short build and installation guide.
|
||||||
See [docs/DESIGN.md](https://github.com/joleuger/vuinputd/blob/main/docs/DESIGN.md) for a detailed overview of the architecture, design trade-offs, and security considerations.
|
See [docs/DESIGN.md](https://github.com/joleuger/vuinputd/blob/main/docs/DESIGN.md) for a detailed overview of the architecture, design trade-offs, and security considerations.
|
||||||
See [docs/USAGE.md](https://github.com/joleuger/vuinputd/blob/main/docs/USAGE.md) for a short usage guide.
|
See [docs/USAGE.md](https://github.com/joleuger/vuinputd/blob/main/docs/USAGE.md) for a short usage guide.
|
||||||
|
See [docs/USAGE-NIXOS.md](https://github.com/joleuger/vuinputd/blob/main/docs/USAGE-NIXOS.md) for a short usage guide for NixOS.
|
||||||
|
See [docs/DEBUG.md](https://github.com/joleuger/vuinputd/blob/main/docs/DEBUG.md) for a guide how to debug problems with containers.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -89,6 +93,8 @@ It reliably demonstrates the core concept — exposing `/dev/uinput` devices ins
|
||||||
|
|
||||||
### ✅ Goals for Production Readiness
|
### ✅ Goals for Production Readiness
|
||||||
|
|
||||||
|
* [ ] **Vibration/Force Feedback support:**
|
||||||
|
|
||||||
* [ ] **Steam input support:**
|
* [ ] **Steam input support:**
|
||||||
Steam input is not supported, yet. For some strange reasons, steam creates 16 virtual devices. Maybe a race.
|
Steam input is not supported, yet. For some strange reasons, steam creates 16 virtual devices. Maybe a race.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,8 @@ fn fuse_binding_filter(builder: bindgen::Builder) -> bindgen::Builder {
|
||||||
.allowlist_function("(?i)^fuse.*")
|
.allowlist_function("(?i)^fuse.*")
|
||||||
.allowlist_var("(?i)^fuse.*")
|
.allowlist_var("(?i)^fuse.*")
|
||||||
.blocklist_type("fuse_log_func_t")
|
.blocklist_type("fuse_log_func_t")
|
||||||
.blocklist_function("fuse_set_log_func");
|
.blocklist_function("fuse_set_log_func")
|
||||||
|
.allowlist_type("^libfuse_version$");
|
||||||
builder
|
builder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
30
debian/changelog
vendored
30
debian/changelog
vendored
|
|
@ -1,3 +1,32 @@
|
||||||
|
vuinputd (0.3.2-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Improve debugging support for container execution:
|
||||||
|
- Add documentation describing how to debug actions triggered by
|
||||||
|
vuinputd inside containers.
|
||||||
|
- Make it easier to use strace to debug child processes.
|
||||||
|
- Log action descriptions as base64-encoded JSON when executing
|
||||||
|
container actions (e.g. mknod), simplifying shell-based debugging.
|
||||||
|
|
||||||
|
-- Johannes Leupolz <vinput@leupolz.eu> Sat, 21 Dec 2025 10:30:00 +0000
|
||||||
|
|
||||||
|
|
||||||
|
vuinputd (0.3.1-1) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Improve Debian packaging and release process:
|
||||||
|
- Add release pipeline for Debian packages.
|
||||||
|
- Improve Debian build scripts.
|
||||||
|
- Fix issues in debian/rules.
|
||||||
|
* Improve startup robustness:
|
||||||
|
- Check the status of tty1 during startup.
|
||||||
|
- Add hints regarding vt-guard usage to the systemd service file.
|
||||||
|
* Documentation and usability improvements:
|
||||||
|
- Improve help messages.
|
||||||
|
- Document two approaches to mitigate the VT keyboard handler issue when
|
||||||
|
no graphical input session is active.
|
||||||
|
|
||||||
|
-- Johannes Leupolz <dev@leupolz.eu> Fri, 20 Dec 2025 22:00:00 +0000
|
||||||
|
|
||||||
|
|
||||||
vuinputd (0.3.0-1) unstable; urgency=medium
|
vuinputd (0.3.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Refactor container and namespace handling:
|
* Refactor container and namespace handling:
|
||||||
|
|
@ -19,6 +48,7 @@ vuinputd (0.3.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
-- Johannes Leupolz <dev@leupolz.eu> Thu, 19 Dec 2025 21:30:00 +0000
|
-- Johannes Leupolz <dev@leupolz.eu> Thu, 19 Dec 2025 21:30:00 +0000
|
||||||
|
|
||||||
|
|
||||||
vuinputd (0.2.0-1) unstable; urgency=medium
|
vuinputd (0.2.0-1) unstable; urgency=medium
|
||||||
|
|
||||||
* Register official USB vendor/product ID (VID 0x1209, PID 0x5020) under
|
* Register official USB vendor/product ID (VID 0x1209, PID 0x5020) under
|
||||||
|
|
|
||||||
13
debian/control
vendored
13
debian/control
vendored
|
|
@ -9,6 +9,9 @@ Build-Depends: debhelper (>= 12),
|
||||||
rustc:native,
|
rustc:native,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
libudev-dev,
|
libudev-dev,
|
||||||
|
libclang-dev,
|
||||||
|
libfuse3-dev,
|
||||||
|
libc6-dev,
|
||||||
librust-bindgen-dev,
|
librust-bindgen-dev,
|
||||||
librust-nix-dev,
|
librust-nix-dev,
|
||||||
librust-libc-dev,
|
librust-libc-dev,
|
||||||
|
|
@ -22,6 +25,9 @@ Build-Depends: debhelper (>= 12),
|
||||||
librust-async-io-dev,
|
librust-async-io-dev,
|
||||||
librust-anyhow-dev,
|
librust-anyhow-dev,
|
||||||
librust-clap-dev,
|
librust-clap-dev,
|
||||||
|
librust-base64-dev,
|
||||||
|
librust-smallvec-dev,
|
||||||
|
librust-async-trait-dev,
|
||||||
pkg-config,
|
pkg-config,
|
||||||
build-essential
|
build-essential
|
||||||
Standards-Version: 4.6.0
|
Standards-Version: 4.6.0
|
||||||
|
|
@ -30,5 +36,10 @@ Rules-Requires-Root: no
|
||||||
|
|
||||||
Package: vuinputd
|
Package: vuinputd
|
||||||
Architecture: amd64
|
Architecture: amd64
|
||||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
Depends:
|
||||||
|
${misc:Depends},
|
||||||
|
${shlibs:Depends},
|
||||||
|
systemd,
|
||||||
|
udev,
|
||||||
|
libfuse3-3
|
||||||
Description: Virtual input forwarder for Linux.
|
Description: Virtual input forwarder for Linux.
|
||||||
19
debian/rules
vendored
19
debian/rules
vendored
|
|
@ -8,6 +8,19 @@ override_dh_auto_build:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
override_dh_auto_install:
|
override_dh_auto_install:
|
||||||
install -D -m 0755 target/release/vuinputd debian/tmp/usr/bin/vuinputd
|
# install binary
|
||||||
install -D -m 0755 vuinputd/udev/90-vuinputd.hwdb debian/tmp/usr/lib/udev/hwdb.d/90-vuinputd.hwdb
|
install -D -m 0755 target/release/vuinputd \
|
||||||
install -D -m 0755 vuinputd/udev/90-vuinputd-protect.rules debian/tmp/usr/lib/udev/rules.d/90-vuinputd-protect.rules
|
debian/tmp/usr/bin/vuinputd
|
||||||
|
|
||||||
|
# patch systemd unit for Debian (/usr/local/bin -> /usr/bin)
|
||||||
|
mkdir -p debian/tmp/usr/lib/systemd/system
|
||||||
|
sed 's|/usr/local/bin/vuinputd|/usr/bin/vuinputd|g' \
|
||||||
|
vuinputd/systemd/vuinputd.service \
|
||||||
|
> debian/tmp/usr/lib/systemd/system/vuinputd.service
|
||||||
|
|
||||||
|
# install udev rules + hwdb
|
||||||
|
install -D -m 0644 vuinputd/udev/90-vuinputd-protect.rules \
|
||||||
|
debian/tmp/usr/lib/udev/rules.d/90-vuinputd-protect.rules
|
||||||
|
|
||||||
|
install -D -m 0644 vuinputd/udev/90-vuinputd.hwdb \
|
||||||
|
debian/tmp/usr/lib/udev/hwdb.d/90-vuinputd.hwdb
|
||||||
3
debian/vuinputd.install
vendored
3
debian/vuinputd.install
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
usr/bin/vuinputd
|
usr/bin/vuinputd
|
||||||
usr/lib/udev/hwdb.d/90-vuinputd.hwdb
|
usr/lib/udev/hwdb.d/90-vuinputd.hwdb
|
||||||
usr/lib/udev/rules.d/90-vuinputd-protect.rules
|
usr/lib/udev/rules.d/90-vuinputd-protect.rules
|
||||||
|
usr/lib/systemd/system/vuinputd.service
|
||||||
15
debian/vuinputd.postinst
vendored
Executable file
15
debian/vuinputd.postinst
vendored
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$1" = "configure" ]; then
|
||||||
|
if command -v systemd-hwdb >/dev/null 2>&1; then
|
||||||
|
systemd-hwdb update || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v udevadm >/dev/null 2>&1; then
|
||||||
|
udevadm control --reload-rules || true
|
||||||
|
udevadm trigger || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
60
distro-tests/cloud-init/create-cloud-init-iso.sh
Normal file
60
distro-tests/cloud-init/create-cloud-init-iso.sh
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
# How to do it is documented on https://cloudinit.readthedocs.io/en/latest/howto/launch_qemu.html
|
||||||
|
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PREPARED_DIR="${ROOT_DIR}/prepared"
|
||||||
|
IDENTITY_DIR="${PREPARED_DIR}/identity"
|
||||||
|
TMP_DIR="${ROOT_DIR}/tmp"
|
||||||
|
|
||||||
|
KEY_PRIV="${IDENTITY_DIR}/ssh_ed25519"
|
||||||
|
KEY_PUB="${IDENTITY_DIR}/ssh_ed25519.pub"
|
||||||
|
|
||||||
|
USER_DATA_SRC="${ROOT_DIR}/cloud-init/template/user-data.tmpl"
|
||||||
|
META_DATA_SRC="${ROOT_DIR}/cloud-init/template/meta-data.tmpl"
|
||||||
|
|
||||||
|
mkdir -p ${TMP_DIR}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# SSH identity (per user, stable)
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
if [ ! -f "${KEY_PRIV}" ]; then
|
||||||
|
echo "[*] Generating SSH keypair"
|
||||||
|
ssh-keygen -t ed25519 -N "" -f "${KEY_PRIV}" -C "distro-tests"
|
||||||
|
else
|
||||||
|
echo "[*] Reusing existing SSH key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SSH_KEY="$(cat "${KEY_PUB}")"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Build user-data with injected SSH key
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
USER_DATA_TMP="${TMP_DIR}/user-data"
|
||||||
|
META_DATA_TMP="${TMP_DIR}/meta-data"
|
||||||
|
|
||||||
|
awk -v key="${SSH_KEY}" '
|
||||||
|
/^users:/ { print; users=1; next }
|
||||||
|
users && /ssh_authorized_keys:/ {
|
||||||
|
print
|
||||||
|
print " - " key
|
||||||
|
next
|
||||||
|
}
|
||||||
|
{ print }
|
||||||
|
' "${USER_DATA_SRC}" > "${USER_DATA_TMP}"
|
||||||
|
|
||||||
|
cp "${META_DATA_SRC}" "${META_DATA_TMP}"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
# Create seed ISO
|
||||||
|
# --------------------------------------------------------------------
|
||||||
|
echo "[*] Creating cloud-init seed.iso"
|
||||||
|
cloud-localds \
|
||||||
|
"${PREPARED_DIR}/seed.iso" \
|
||||||
|
"${USER_DATA_TMP}" \
|
||||||
|
"${META_DATA_TMP}"
|
||||||
|
|
||||||
|
echo "[✓] cloud-init ISO ready: ${PREPARED_DIR}/seed.iso"
|
||||||
2
distro-tests/cloud-init/template/meta-data.tmpl
Normal file
2
distro-tests/cloud-init/template/meta-data.tmpl
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
instance-id: vuinputd-distro-tests
|
||||||
|
local-hostname: vuinputd-distro-tests
|
||||||
21
distro-tests/cloud-init/template/user-data.tmpl
Normal file
21
distro-tests/cloud-init/template/user-data.tmpl
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
#cloud-config
|
||||||
|
#
|
||||||
|
# Shared cloud-init configuration for distro-tests
|
||||||
|
# SSH keys are injected dynamically by create-cloud-init-iso.sh
|
||||||
|
#
|
||||||
|
|
||||||
|
users:
|
||||||
|
- name: testuser
|
||||||
|
groups: users, adm
|
||||||
|
shell: /bin/bash
|
||||||
|
sudo: ALL=(ALL) NOPASSWD:ALL
|
||||||
|
ssh_authorized_keys:
|
||||||
|
|
||||||
|
disable_root: true
|
||||||
|
ssh_pwauth: false
|
||||||
|
|
||||||
|
package_update: false
|
||||||
|
package_upgrade: false
|
||||||
|
|
||||||
|
runcmd:
|
||||||
|
- echo "cloud-init bootstrap complete" > /var/tmp/cloud-init-ok
|
||||||
272
distro-tests/nixos/README.md
Normal file
272
distro-tests/nixos/README.md
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
# Testing vuinputd on NixOS with Incus
|
||||||
|
|
||||||
|
This guide walks through setting up a reproducible NixOS test environment for `vuinputd`
|
||||||
|
using [Incus](https://linuxcontainers.org/incus/), an open-source system container and VM manager.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Script
|
||||||
|
|
||||||
|
If you are already familiar with Incus and NixOS, here is the full sequence at a glance.
|
||||||
|
The sections below explain each step in detail.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# --- Setup ---
|
||||||
|
# Download NixOS VM image (only needed once)
|
||||||
|
incus image copy images:nixos/25.11 local: --alias nixos/25.11 --vm
|
||||||
|
|
||||||
|
# Create and start the VM
|
||||||
|
incus launch local:nixos/25.11 nixos-vm --vm
|
||||||
|
|
||||||
|
# Adjust resources (requires a stop/start cycle)
|
||||||
|
incus stop nixos-vm
|
||||||
|
incus config set nixos-vm limits.memory 3GiB
|
||||||
|
incus config set nixos-vm security.secureboot false
|
||||||
|
incus start nixos-vm
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
# Extend the NixOS configuration to include the vuinputd test module
|
||||||
|
incus exec nixos-vm -- sed -i '/imports = \[/a\ ./vuinputd-test-automation.nix' \
|
||||||
|
/etc/nixos/configuration.nix
|
||||||
|
|
||||||
|
# Push the test module into the VM
|
||||||
|
incus file push vuinputd-test-automation.nix nixos-vm/etc/nixos/vuinputd-test-automation.nix
|
||||||
|
|
||||||
|
# Apply the configuration (takes a few minutes on first run)
|
||||||
|
incus exec nixos-vm -- nixos-rebuild switch --max-jobs 1
|
||||||
|
|
||||||
|
# --- Run the test ---
|
||||||
|
incus exec nixos-vm -- bwrap \
|
||||||
|
--unshare-net \
|
||||||
|
--ro-bind / / \
|
||||||
|
--tmpfs /tmp \
|
||||||
|
--tmpfs /run/udev \
|
||||||
|
--dev-bind /run/vuinputd/vuinput/dev-input /dev/input \
|
||||||
|
--dev-bind /dev/vuinput /dev/uinput \
|
||||||
|
/run/current-system/sw/bin/test-scenarios basic-keyboard
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why Incus?
|
||||||
|
|
||||||
|
Testing `vuinputd` requires a full Linux system stack: a running kernel, udev, `/dev/uinput`,
|
||||||
|
CUSE support, and a container runtime. A plain Docker container or unit test harness is not
|
||||||
|
sufficient because many of these subsystems only exist in a fully booted environment.
|
||||||
|
|
||||||
|
[Incus](https://linuxcontainers.org/incus/) is used here for several reasons:
|
||||||
|
|
||||||
|
- **Full VM support:** Incus can launch proper virtual machines (not just containers), which
|
||||||
|
gives each test environment its own kernel, udev tree, and device namespace — exactly what
|
||||||
|
`vuinputd` needs to operate.
|
||||||
|
- **Clean image lifecycle:** Incus pulls pre-built NixOS images from the
|
||||||
|
[Linux Containers image server](https://images.linuxcontainers.org/), so there is no need
|
||||||
|
to build a NixOS ISO or maintain a local image manually.
|
||||||
|
- **Easy configuration injection:** `incus file push` and `incus exec` allow pushing NixOS
|
||||||
|
configuration files into the VM and triggering a `nixos-rebuild switch` without requiring
|
||||||
|
SSH or manual setup.
|
||||||
|
- **Reproducibility:** Each test run can start from a fresh image, ensuring that leftover
|
||||||
|
state from a previous run does not affect results.
|
||||||
|
- **Isolation from the host:** The VM is fully isolated from the host system, so test runs
|
||||||
|
cannot accidentally interfere with the host's input devices or udev state.
|
||||||
|
|
||||||
|
> **Note:** While `vuinputd` is compatible with other container runtimes such as
|
||||||
|
> `systemd-nspawn`, `Docker`, `LXC`, and `Podman`, Incus VMs are the recommended environment
|
||||||
|
> for automated NixOS testing because they provide a fully booted NixOS system with minimal
|
||||||
|
> setup effort.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Incus** installed and initialized on the host (`incus admin init` completed).
|
||||||
|
- The `vuinputd-test-automation.nix` NixOS module available in your working directory.
|
||||||
|
This module configures `vuinputd` and the test tooling inside the VM.
|
||||||
|
- Sufficient disk space for the NixOS VM image (roughly 3–4 GiB).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Guide
|
||||||
|
|
||||||
|
### 1. Download the NixOS VM Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus image copy images:nixos/25.11 local: --alias nixos/25.11 --vm
|
||||||
|
```
|
||||||
|
|
||||||
|
This pulls the NixOS 25.11 image from the public Linux Containers image server and stores it
|
||||||
|
locally under the alias `nixos/25.11`. The `--vm` flag ensures the image is treated as a
|
||||||
|
full virtual machine image rather than a system container rootfs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Launch the VM
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus launch local:nixos/25.11 nixos-vm --vm
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates and starts a new VM instance named `nixos-vm` from the downloaded image.
|
||||||
|
At this point the VM boots NixOS with its default configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Adjust VM Resources
|
||||||
|
|
||||||
|
Stop the VM briefly to apply resource limits before running the NixOS rebuild, which is
|
||||||
|
memory-intensive:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus stop nixos-vm
|
||||||
|
incus config set nixos-vm limits.memory 3GiB
|
||||||
|
```
|
||||||
|
|
||||||
|
NixOS system builds (`nixos-rebuild switch`) evaluate a large Nix expression tree and compile
|
||||||
|
Rust code. Without sufficient memory, the build may be killed by the OOM reaper.
|
||||||
|
|
||||||
|
Secure Boot must also be disabled because the NixOS kernel modules required by `vuinputd`
|
||||||
|
(CUSE/FUSE) are not signed for Secure Boot by default:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus config set nixos-vm security.secureboot false
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart the VM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus start nixos-vm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Inject the Test Configuration
|
||||||
|
|
||||||
|
The NixOS configuration inside the VM needs to be extended to include the `vuinputd` test
|
||||||
|
module. Commands are run inside the VM via `incus exec`. The `--` separator tells Incus where
|
||||||
|
its own arguments end and where the command to execute inside the VM begins — everything after
|
||||||
|
`--` is passed verbatim to the VM shell.
|
||||||
|
|
||||||
|
First, append the import to the existing `configuration.nix`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus exec nixos-vm -- sed -i '/imports = \[/a\ ./vuinputd-test-automation.nix' \
|
||||||
|
/etc/nixos/configuration.nix
|
||||||
|
```
|
||||||
|
|
||||||
|
This uses `sed` to insert `./vuinputd-test-automation.nix` immediately after the `imports = [`
|
||||||
|
line in the NixOS configuration, so NixOS will pick it up during the next rebuild.
|
||||||
|
|
||||||
|
Next, push the test module file into the VM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus file push vuinputd-test-automation.nix nixos-vm/etc/nixos/vuinputd-test-automation.nix
|
||||||
|
```
|
||||||
|
|
||||||
|
The `vuinputd-test-automation.nix` module is responsible for:
|
||||||
|
- Installing and enabling `vuinputd` as a systemd service.
|
||||||
|
- Providing any additional packages needed by the test scenarios (e.g., `bwrap`, `test-scenarios`).
|
||||||
|
- Configuring udev rules for device isolation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Apply the NixOS Configuration
|
||||||
|
|
||||||
|
Trigger a NixOS system rebuild inside the VM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus exec nixos-vm -- nixos-rebuild switch --max-jobs 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--max-jobs 1` flag limits parallel build jobs to avoid exhausting VM memory during
|
||||||
|
compilation. This step may take several minutes on first run because Nix will fetch and
|
||||||
|
build all required dependencies.
|
||||||
|
|
||||||
|
After the rebuild completes, the VM is running a fully configured NixOS system with
|
||||||
|
`vuinputd` installed and active.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Run the Test Scenarios
|
||||||
|
|
||||||
|
Execute a test scenario inside a restricted sandbox using `bwrap` (Bubblewrap).
|
||||||
|
|
||||||
|
[Bubblewrap](https://github.com/containers/bubblewrap) was chosen as the sandboxing tool for
|
||||||
|
several reasons:
|
||||||
|
|
||||||
|
- **Minimal and universally available:** `bwrap` is a small, single binary with no daemon and
|
||||||
|
no runtime dependencies beyond the kernel. It is packaged in virtually every Linux
|
||||||
|
distribution, so there is no need to install a full container runtime just to run tests.
|
||||||
|
- **Reuses host binaries directly:** Because `bwrap` can bind-mount the host (or VM) filesystem
|
||||||
|
read-only into the sandbox, the test binary and all its dependencies are taken straight from
|
||||||
|
the running NixOS system — no separate rootfs or image needs to be prepared.
|
||||||
|
- **Good proxy for heavier runtimes:** The namespace isolation that `bwrap` provides (mount,
|
||||||
|
network, udev) is the same fundamental mechanism used by Docker, Podman, and systemd-nspawn.
|
||||||
|
If `vuinputd` works correctly inside a `bwrap` sandbox, the same behavior can be expected
|
||||||
|
from more heavyweight container runtimes, making `bwrap` a lightweight but representative
|
||||||
|
stand-in for integration testing.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus exec nixos-vm -- bwrap \
|
||||||
|
--unshare-net \
|
||||||
|
--ro-bind / / \
|
||||||
|
--tmpfs /tmp \
|
||||||
|
--tmpfs /run/udev \
|
||||||
|
--dev-bind /run/vuinputd/vuinput/dev-input /dev/input \
|
||||||
|
--dev-bind /dev/vuinput /dev/uinput \
|
||||||
|
/run/current-system/sw/bin/test-scenarios basic-keyboard
|
||||||
|
```
|
||||||
|
|
||||||
|
#### What this command does
|
||||||
|
|
||||||
|
| Flag | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `--unshare-net` | Removes network access from the sandbox, ensuring the test is self-contained. |
|
||||||
|
| `--ro-bind / /` | Mounts the entire VM filesystem read-only as the sandbox root. |
|
||||||
|
| `--tmpfs /tmp` | Provides a writable temporary directory. |
|
||||||
|
| `--tmpfs /run/udev` | Provides a writable udev runtime directory, isolating the sandbox from the VM's real udev socket. |
|
||||||
|
| `--dev-bind /run/vuinputd/vuinput/dev-input /dev/input` | Exposes the input devices managed by `vuinputd` (the container-scoped `/dev/input` subtree) at the expected path inside the sandbox. |
|
||||||
|
| `--dev-bind /dev/vuinput /dev/uinput` | Exposes the CUSE-backed virtual `/dev/uinput` device provided by `vuinputd` at the standard `/dev/uinput` path inside the sandbox. |
|
||||||
|
|
||||||
|
This sandbox mimics what a containerized application would see: it has access to the virtual
|
||||||
|
`/dev/uinput` provided by `vuinputd` and the corresponding `/dev/input` event devices, but
|
||||||
|
cannot reach the host's real uinput or other system resources.
|
||||||
|
|
||||||
|
The final argument `basic-keyboard` selects which test scenario to run. Additional scenarios
|
||||||
|
may be available; refer to the `test-scenarios` binary's help output for the full list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cleaning Up
|
||||||
|
|
||||||
|
To delete the VM and free disk space after testing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus delete --force nixos-vm
|
||||||
|
```
|
||||||
|
|
||||||
|
To also remove the cached image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
incus image delete nixos/25.11
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If the test fails or `vuinputd` does not appear to be running inside the VM, refer to
|
||||||
|
[docs/DEBUG.md](https://github.com/joleuger/vuinputd/blob/main/docs/DEBUG.md) for debugging
|
||||||
|
strategies applicable to container environments.
|
||||||
|
|
||||||
|
Common issues:
|
||||||
|
|
||||||
|
- **CUSE not available:** Ensure that the `cuse` kernel module is loaded inside the VM
|
||||||
|
(`modprobe cuse`). The test automation module should handle this automatically.
|
||||||
|
- **`nixos-rebuild` runs out of memory:** Increase the memory limit above 3 GiB or reduce
|
||||||
|
parallelism further with `--max-jobs 1 --cores 1`.
|
||||||
|
- **Secure Boot blocking the kernel module:** Verify that `security.secureboot` is set to
|
||||||
|
`false` on the VM instance.
|
||||||
|
- **`/dev/vuinput` not present:** Check that the `vuinputd` systemd service is active
|
||||||
|
(`systemctl status vuinputd`) and that CUSE is loaded.
|
||||||
90
distro-tests/nixos/vuinputd-test-automation.nix
Normal file
90
distro-tests/nixos/vuinputd-test-automation.nix
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
vuinputd = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "vuinputd";
|
||||||
|
version = "0.3.2-git";
|
||||||
|
|
||||||
|
buildType = "debug";
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.rustPlatform.bindgenHook
|
||||||
|
# breakpointHook
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [pkgs.udev pkgs.fuse3];
|
||||||
|
|
||||||
|
src = pkgs.fetchFromGitHub {
|
||||||
|
owner = "joleuger";
|
||||||
|
repo = "vuinputd";
|
||||||
|
rev = "8c40fdc12005319ea16dceb752a8822abfc6039a";
|
||||||
|
hash = "sha256-8Q34B04BngZqRLyixeFq8F1t5wFnk6JpaG3EEbgKRcU=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoHash = "sha256-nJw9bRh6Yn9g1H5SeoT6zxgZLCqV3AtAs9gMfE+P+CU=";
|
||||||
|
|
||||||
|
# Recent versions of fuse3 can also have libfuse_* types
|
||||||
|
postPatch = ''
|
||||||
|
substituteInPlace cuse-lowlevel/build.rs \
|
||||||
|
--replace-fail '.allowlist_type("(?i)^fuse.*")' '.allowlist_type("(?i)^(fuse|libfuse).*")'
|
||||||
|
'';
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mkdir -p $out/lib/udev/rules.d
|
||||||
|
mkdir $out/lib/udev/hwdb.d
|
||||||
|
cp vuinputd/udev/*.rules $out/lib/udev/rules.d/
|
||||||
|
cp vuinputd/udev/*.hwdb $out/lib/udev/hwdb.d/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
vim
|
||||||
|
pkg-config
|
||||||
|
udev
|
||||||
|
fuse3
|
||||||
|
git
|
||||||
|
rustc
|
||||||
|
cargo
|
||||||
|
bubblewrap # for testing
|
||||||
|
|
||||||
|
vuinputd
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.vuinputd = {
|
||||||
|
enable = true;
|
||||||
|
wantedBy = ["multi-user.target"];
|
||||||
|
unitConfig = {
|
||||||
|
Description = "Virtual input (/dev/vuinput) daemon";
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "exec";
|
||||||
|
ExecStartPre = pkgs.writeShellScript "mount-tmpfs-dev-input" ''
|
||||||
|
mkdir -p /run/vuinputd/vuinput/dev-input
|
||||||
|
${pkgs.util-linux}/bin/mount -t tmpfs -o rw,dev,nosuid tpmfs /run/vuinputd/vuinput/dev-input
|
||||||
|
'';
|
||||||
|
ExecStart = "${lib.getExe vuinputd} --major 120 --minor 414795 --placement on-host";
|
||||||
|
ExecStopPost = pkgs.writeShellScript "umount-dev-input" ''
|
||||||
|
${pkgs.util-linux}/bin/umount /run/vuinputd/vuinput/dev-input
|
||||||
|
'';
|
||||||
|
Restart = "on-failure";
|
||||||
|
DeviceAllow = "char-* rwm";
|
||||||
|
|
||||||
|
Environment = [
|
||||||
|
"RUST_LOG=debug"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
systemd.services.vuinputd-chmod = {
|
||||||
|
unitConfig.Description = "Chmod 666 the /dev/vuinput";
|
||||||
|
wantedBy = ["vuinputd.service"];
|
||||||
|
after = ["vuinputd.service"];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = pkgs.writeShellScript "chmod-vuinput" ''
|
||||||
|
sleep 2 && chmod 666 /dev/vuinput
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
28
distro-tests/ubuntu-24.04/download.sh
Normal file
28
distro-tests/ubuntu-24.04/download.sh
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
PREPARED_IMG_DIR="${ROOT_DIR}/prepared"
|
||||||
|
|
||||||
|
IMG_NAME="ubuntu-24.04-noble-base.qcow2"
|
||||||
|
IMG_PATH="${PREPARED_IMG_DIR}/${IMG_NAME}"
|
||||||
|
|
||||||
|
SRC_URL="https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
|
||||||
|
|
||||||
|
mkdir -p "${PREPARED_IMG_DIR}"
|
||||||
|
|
||||||
|
if [ -f "${IMG_PATH}" ]; then
|
||||||
|
echo "[*] Ubuntu 24.04 base image already present"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[*] Downloading Ubuntu 24.04 cloud image"
|
||||||
|
curl -L "${SRC_URL}" -o "${IMG_PATH}.tmp"
|
||||||
|
|
||||||
|
echo "[*] Converting to qcow2"
|
||||||
|
qemu-img convert -c -O qcow2 "${IMG_PATH}.tmp" "${IMG_PATH}"
|
||||||
|
|
||||||
|
rm -f "${IMG_PATH}.tmp"
|
||||||
|
|
||||||
|
echo "[✓] Ubuntu 24.04 base image ready"
|
||||||
109
docs/DEBUG.md
Normal file
109
docs/DEBUG.md
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
# DEBUG.md
|
||||||
|
|
||||||
|
## Debugging vuinputd
|
||||||
|
|
||||||
|
vuinputd performs low-level operations such as entering Linux namespaces and creating input device nodes. When something goes wrong, the root cause is often related to container lifetime, namespace visibility, or kernel security mechanisms.
|
||||||
|
|
||||||
|
This document describes how to debug common issues.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Enable debug logging
|
||||||
|
|
||||||
|
Run vuinputd with debug logging enabled:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
RUST_LOG=debug vuinputd ...
|
||||||
|
```
|
||||||
|
|
||||||
|
This will emit additional diagnostic output, especially around namespace entry and `mknod` execution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debugging `mknod` inside a container
|
||||||
|
|
||||||
|
Whenever vuinputd is about to execute `mknod` inside a container namespace, it prints a debug message similar to:
|
||||||
|
|
||||||
|
```
|
||||||
|
[2025-12-27T23:45:06Z DEBUG vuinputd::job_engine::job] Executing job: mknod input device in container
|
||||||
|
[2025-12-27T23:45:06Z DEBUG vuinputd::process_tools] In case you need to debug the system calls, call strace vuinputd --target-namespace /proc/103086/ns --action-base64 eyJhY3Rpb24iOiJta25vZC1kZXZpY2UiLCJwYXRoIjoiL2Rldi9pbnB1dC9ldmVudDEyIiwibWFqb3IiOjEzLCJtaW5vciI6NzZ9
|
||||||
|
```
|
||||||
|
|
||||||
|
The printed command can be used verbatim to trace the exact system calls involved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using `strace`
|
||||||
|
|
||||||
|
Run the suggested command under `strace`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
strace vuinputd --target-namespace /proc/103086/ns --action-base64 <BASE64_PAYLOAD>
|
||||||
|
```
|
||||||
|
|
||||||
|
Typical failure output may look like:
|
||||||
|
|
||||||
|
```text
|
||||||
|
strace target/debug/vuinputd --target-namespace /proc/102044/ns --action-base64 eyJhY3Rpb24iOiJta25vZC1kZXZpY2UiLCJwYXRoIjoiL2Rldi9pbnB1dC9ldmVudDEyIiwibWFqb3IiOjEzLCJtaW5vciI6NzZ9
|
||||||
|
...
|
||||||
|
statx(AT_FDCWD, "/proc/102044/ns", AT_STATX_SYNC_AS_STAT, STATX_ALL, 0x7ffeb6bea3c0) = -1 ENOENT (No such file or directory)
|
||||||
|
...
|
||||||
|
thread 'main' (103610) panicked at vuinputd/src/main.rs:170:84:
|
||||||
|
called Result::unwrap() on an Err value: the root process of the container whose namespaces we want to enter does not exist anymore
|
||||||
|
...
|
||||||
|
+++ exited with 101 +++
|
||||||
|
```
|
||||||
|
|
||||||
|
### Interpretation
|
||||||
|
|
||||||
|
In this example, `/proc/102044/ns` no longer exists, which means the container process has already terminated. Entering its namespaces is therefore impossible.
|
||||||
|
|
||||||
|
This usually indicates:
|
||||||
|
|
||||||
|
* the container exited before vuinputd ran,
|
||||||
|
* a race between container startup and vuinputd execution,
|
||||||
|
* or an incorrect PID being passed as `--target-namespace`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common causes of permission errors
|
||||||
|
|
||||||
|
If `mknod` fails with `EPERM` or similar errors, possible causes include:
|
||||||
|
|
||||||
|
* Missing `CAP_MKNOD` in the namespace where vuinputd is running
|
||||||
|
* seccomp filters blocking `mknod`
|
||||||
|
* SELinux or AppArmor policies
|
||||||
|
* eBPF-based LSM policies
|
||||||
|
* Read-only or improperly mounted `/dev`
|
||||||
|
* Missing permissions to create devices (systemd actually needs `DeviceAllow=char-* rwm` in service files)
|
||||||
|
|
||||||
|
Using `strace` usually makes these issues visible immediately.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `/run/udev` and libinput
|
||||||
|
|
||||||
|
libinput expects certain udev runtime files to exist, even if no udev daemon is running inside the container.
|
||||||
|
|
||||||
|
In minimal or containerized environments, make sure the following paths exist and are writable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /run/udev/data
|
||||||
|
touch /run/udev/control
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a known libinput behavior in containerized setups and not specific to vuinputd.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## When reporting issues
|
||||||
|
|
||||||
|
If you open an issue, please include:
|
||||||
|
|
||||||
|
* full debug logs (`RUST_LOG=debug`)
|
||||||
|
* the exact `strace` output, if available
|
||||||
|
* whether host and container share `/dev/input`
|
||||||
|
* whether vuinputd runs on the host or inside the container
|
||||||
|
* relevant security mechanisms (seccomp / SELinux / AppArmor)
|
||||||
|
|
||||||
|
This makes it much easier to reproduce and diagnose the problem.
|
||||||
216
docs/DESIGN.md
216
docs/DESIGN.md
|
|
@ -397,6 +397,14 @@ When mapping 32-bit compat input_event formats into 64-bit representation, copy
|
||||||
|
|
||||||
No high volume of events expected where we could benefit from multiple threads. But much of the code is already prepared for multithreading, if there is really demand.
|
No high volume of events expected where we could benefit from multiple threads. But much of the code is already prepared for multithreading, if there is really demand.
|
||||||
|
|
||||||
|
**Poll / event readiness handling**
|
||||||
|
|
||||||
|
- For operations that wait on host device readiness (e.g., force feedback, rumble, vibration, or reading back event state), the CUSE callback must **never block**.
|
||||||
|
- A **poll/wakeup watcher** in a background thread monitors underlying `/dev/uinput` FDs and updates per-handle readiness (`PollState`) in `VuInputState` (see section 3.13).
|
||||||
|
- FUSE poll callbacks may save the provided poll handle and immediately return; the background watcher later invokes `fuse_notify_poll()` to wake the kernel when data arrives.
|
||||||
|
|
||||||
|
*Why:* this separates the fast data-plane (CUSE callbacks) from the asynchronous event-plane (poll watcher) and prevents hanging the filesystem.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3.9 Overriding the type, vendor id, and product id
|
## 3.9 Overriding the type, vendor id, and product id
|
||||||
|
|
@ -485,6 +493,213 @@ Under these constraints, post-exec namespace switching provides a robust and pre
|
||||||
|
|
||||||
The chosen approach offers the best balance between correctness, portability, and operational simplicity.
|
The chosen approach offers the best balance between correctness, portability, and operational simplicity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **3.11 Fallback Graphical Session (`fallbackdm`)**
|
||||||
|
|
||||||
|
### **Problem Statement**
|
||||||
|
|
||||||
|
On systems without an active graphical session (X11 or Wayland), the kernel VT subsystem remains in **text mode (`KD_TEXT`)**, and the VT keyboard handler is active.
|
||||||
|
As a result:
|
||||||
|
|
||||||
|
* `getty` receives keyboard input on the active VT
|
||||||
|
* VT key handling (e.g. `Ctrl+Alt+Fn`) is enabled
|
||||||
|
* input devices may interact with the VT layer in unintended ways
|
||||||
|
|
||||||
|
When a graphical session is active, these issues do not occur:
|
||||||
|
the compositor, via `systemd-logind`, owns a VT, switches it to `KD_GRAPHICS` and `K_OFF`, and the VT keyboard handler is **suppressed**.
|
||||||
|
|
||||||
|
The missing piece is a **well-defined fallback** for the “no graphical session” case.
|
||||||
|
|
||||||
|
### **Effect of K_OFF in Linux VT subsystem**
|
||||||
|
|
||||||
|
- ioctl KDSKBMODE on /dev/ttyX leads to call of vt_do_kdskbmode
|
||||||
|
- kb->kbdmode = VC_OFF
|
||||||
|
|
||||||
|
This suppresses the following chains in [keyboard.c](https://github.com/torvalds/linux/blob/master/drivers/tty/vt/keyboard.c)
|
||||||
|
Lets take Console_1 via ALT+F2 as an example:
|
||||||
|
- kbd_event (Entry Point)
|
||||||
|
- kbd_keycode (Translation)
|
||||||
|
- Job: looks up the Keysym in the keymap based on the current modifier state (ALT+F2 is 0xf501 in [defkeymap.c_shipped](https://github.com/torvalds/linux/blob/master/drivers/tty/vt/defkeymap.c_shipped))
|
||||||
|
- type = KTYP(keysym) takes the first 16 bits (which is 0xf5).
|
||||||
|
- type -= 0xf0. (which is 0x05). Note hat the kernel uses the ** offset** `0xf0` to differentiate between characters and special handlers in the keymap. When `type -= 0xf0` is called, it "normalizes" the keysym into an index for the `k_handler` array.
|
||||||
|
- index = KVAL(keysym) takes the last 16 bits (which is 0x01)
|
||||||
|
- return if ((raw_mode || kbd->kbdmode == VC_OFF) && type != KT_SPEC && type != KT_SHIFT), so suppression happens here.
|
||||||
|
- Note that K_HANDLERS[type] == K_HANDLERS[0x05] == k_cons.
|
||||||
|
- if no suppression: call (*k_handler[type])(vc, KVAL(keysym), !down), which is k_cons(vc,0x01)
|
||||||
|
|
||||||
|
Lets take Decr_Console via ALT+Left as second example:
|
||||||
|
- kbd_event (Entry Point)
|
||||||
|
- kbd_keycode (Translation)
|
||||||
|
- Job: looks up the Keysym in the keymap based on the current modifier state (ALT+Left is 0xf210 in [defkeymap.c_shipped](https://github.com/torvalds/linux/blob/master/drivers/tty/vt/defkeymap.c_shipped))
|
||||||
|
- type = KTYP(keysym) takes the first 16 bits (which is 0xf2).
|
||||||
|
- type -= 0xf0. (which is 0x02)
|
||||||
|
- index = KVAL(keysym) takes the last 16 bits (which is 0x10)
|
||||||
|
- return if ((raw_mode || kbd->kbdmode == VC_OFF) && type != KT_SPEC && type != KT_SHIFT), so suppression does *not* happen here.
|
||||||
|
- Note that K_HANDLERS[type] == K_HANDLERS[0x02] == k_spec.
|
||||||
|
- call (*k_handler[type])(vc, KVAL(keysym), !down), which is k_spec(vc,0x10)
|
||||||
|
- k_spec (Handling)
|
||||||
|
- Condition if ((... || kbd->kbdmode == VC_OFF) && value != KVAL(K_SAK)) evaluates to false, so suppression happens here
|
||||||
|
- if no suppression: call fn_handler[value](...) which is fn_dec_console(...)
|
||||||
|
|
||||||
|
In the KT_SHIFT-case of "return if ((raw_mode || kbd->kbdmode == VC_OFF) && type != KT_SPEC && type != KT_SHIFT", nothing interesting happens in our case: it might enable and disable caps lock. This means uinput can still enable disable caps, which is a bit odd, but nothing tragic in our use cases.
|
||||||
|
|
||||||
|
sysrq has an own handler. It is not affected by K_OFF.
|
||||||
|
https://github.com/torvalds/linux/blob/master/drivers/tty/sysrq.c#L1048
|
||||||
|
|
||||||
|
Raw mode is not relevant.
|
||||||
|
|
||||||
|
### The Risk of Non-Standard or User-Loaded Keymaps
|
||||||
|
|
||||||
|
While `vuinputd` relies on the default kernel keymap logic for its internal filtering, it is important to note that the host's active keymap can be modified at runtime (e.g., via `loadkeys` or `systemd-vconsole-setup`). Because the kernel's `K_OFF` logic (triggered by `KDSKBMODE`) explicitly whitelists the `K_SAK` (Secure Attention Key) keysym, any user-defined key combination mapped to `SAK` will bypass the kernel's own input suppression.
|
||||||
|
|
||||||
|
To further mitigate these risks, `vuinputd` could be extended to parse the host's active keymap during startup, allowing the sanitizer to dynamically identify and filter any physical keycode mapped to a sensitive keysym like `K_SAK`. This is currently not planned. Alternatively, on systems dedicated to containerization where full host TTY access is not required, administrators can load a "hardened" keymap stripped of all `Console_N`, `Boot`, and `SAK` assignments. By combining a minimized host keymap with `vuinputd`'s CUSE-level filtering, the system achieves a robust "Defense in Depth" that protects against both accidental triggers and intentional container escapes.
|
||||||
|
|
||||||
|
### **Decision**
|
||||||
|
|
||||||
|
`fallbackdm` is implemented as a **logind-managed fallback graphical session**. It is available at https://github.com/joleuger/fallbackdm.
|
||||||
|
|
||||||
|
It runs only when **no other graphical session is active** on the seat and exists solely to:
|
||||||
|
|
||||||
|
* open a regular logind session **of class `greeter**`
|
||||||
|
* occupy the assigned VT
|
||||||
|
* let logind switch the VT to `KD_GRAPHICS` and `K_OFF` **and mute the keyboard handler**
|
||||||
|
|
||||||
|
`fallbackdm` itself does **not** manipulate VTs, perform `KDSETMODE` ioctls, or access `/dev/tty` directly.
|
||||||
|
All VT and seat handling is delegated to `systemd-logind` **via a standard PAM session**.
|
||||||
|
|
||||||
|
|
||||||
|
### **Behavior and Lifecycle**
|
||||||
|
|
||||||
|
* `fallbackdm` starts as a normal session on the seat
|
||||||
|
* while active:
|
||||||
|
* the VT is in `KD_GRAPHICS`
|
||||||
|
* **the kernel VT keyboard handler is muted (equivalent to `K_OFF` or `KDSKBMUTE`)**
|
||||||
|
* `getty` input is suppressed
|
||||||
|
* when a real graphical session (greeter or compositor) starts:
|
||||||
|
* logind deactivates `fallbackdm`
|
||||||
|
* VT ownership is transferred automatically
|
||||||
|
* when the graphical session ends:
|
||||||
|
* `fallbackdm` may be restarted to reclaim the fallback role
|
||||||
|
|
||||||
|
`fallbackdm` is non-interactive by design but may display **minimal status information** in the future. `fallbackdm` could also be extended to listen itself to the console switches from evdev devices that are actually connected to the seat and signal vuinputd to mute virtual devices accordingly.
|
||||||
|
|
||||||
|
|
||||||
|
### **Rationale**
|
||||||
|
|
||||||
|
This design:
|
||||||
|
|
||||||
|
* reuses existing, well-tested logind behavior
|
||||||
|
* avoids duplicating VT and seat logic
|
||||||
|
* guarantees compatibility with:
|
||||||
|
* Wayland compositors
|
||||||
|
* graphical login managers
|
||||||
|
* multi-seat setups
|
||||||
|
* keeps the implementation minimal and robust
|
||||||
|
|
||||||
|
Conceptually, `fallbackdm` acts as a **headless placeholder graphical session** that ensures consistent system behavior even when no real graphical environment is running.
|
||||||
|
|
||||||
|
|
||||||
|
### **Alternatives Considered**
|
||||||
|
|
||||||
|
* **Direct VT management (KDSETMODE, VT ioctls)**
|
||||||
|
Rejected due to complexity, fragility, and duplication of logind functionality.
|
||||||
|
* **Filtering input at the evdev / uinput layer**
|
||||||
|
Rejected as insufficient: it does not address VT keyboard handling or `getty` behavior.
|
||||||
|
* **Disabling gettys or VT switching globally**
|
||||||
|
Rejected to preserve emergency local access and standard Linux behavior. **The logind-managed approach allows physical VT switching to remain functional for debugging.**
|
||||||
|
* **Depending on a full display manager**
|
||||||
|
Rejected, as this may require dummy display devices in headless configurations and adds unnecessary complexity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.12 Device Policies & Input Sanitization
|
||||||
|
|
||||||
|
Exposing raw access to `/dev/uinput` inside a container introduces significant security risks. A malicious process could theoretically emulate a keyboard to execute "BadUSB"-style attacks, trigger kernel-level commands (Magic SysRq), or switch Virtual Terminals (VT) to escape the graphical session.
|
||||||
|
|
||||||
|
To mitigate this, `vuinputd` implements an **Active Filtering Layer** (CUSE middleware) that enforces strict device policies before requests reach the host kernel. This is controlled via the `--device-policy` flag.
|
||||||
|
|
||||||
|
The filtering operates on two levels (Defense in Depth):
|
||||||
|
|
||||||
|
1. **Capability Filtering (`ioctl`):** During device creation, `vuinputd` inspects `UI_SET_KEYBIT`, `UI_SET_RELBIT`, etc. If a container requests capabilities forbidden by the active policy (e.g., a gamepad trying to claim it has a SysRq key), the request is silently ignored or rejected. The resulting device on the host simply lacks those hardware capabilities. This hasn't been implemented, yet.
|
||||||
|
2. **Event Filtering (`write`):** At runtime, `vuinputd` inspects the stream of input events. It maintains internal state (tracking modifiers like `Alt` or `Ctrl`) to detect and drop dangerous sequences (e.g., `Alt` + `F1-F12` for VT switching) that the capability filter alone cannot block.
|
||||||
|
|
||||||
|
**Supported Policies:**
|
||||||
|
|
||||||
|
* **`strict-gamepad` (Whitelist):** Designed for console-like isolation. It strictly permits only Gamepad/Joystick events (`EV_KEY` buttons, `EV_ABS` axes). It proactively blocks `EV_REL` (mouse movement) and `ABS_MT` (multitouch), effectively "neutering" complex controllers (like DualSense or Wiimotes) so they cannot be used to hijack the host mouse cursor.
|
||||||
|
* **`sanitized` (Blacklist):** Designed for desktop gaming. It allows standard Keyboard and Mouse input but strictly filters dangerous keys (`KEY_SYSRQ`, `KEY_POWER`) and host-management shortcuts (VT switching, CAD), providing a safe "sandboxed keyboard."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.13 Polling & Readiness Watcher
|
||||||
|
|
||||||
|
**Purpose**
|
||||||
|
|
||||||
|
- Provide non-blocking detection of device readiness on `/dev/vuinput` for operations like force feedback / rumble / vibration.
|
||||||
|
- Ensure CUSE callbacks (`poll`, `read`) never block and the FUSE filesystem remains responsive.
|
||||||
|
|
||||||
|
**Core design**
|
||||||
|
|
||||||
|
- Each `VuInputState` includes a `PollState` struct:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct PollState {
|
||||||
|
/// A FUSE poll request is currently waiting to be woken.
|
||||||
|
pub waiting: bool,
|
||||||
|
|
||||||
|
/// Sticky readiness latch: true once evdev became readable, false after read/drain.
|
||||||
|
pub readable: bool,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* A **single background thread** watches all active uinput device file descriptors using **epoll** (or poll).
|
||||||
|
* When data arrives:
|
||||||
|
|
||||||
|
* The watcher locks the corresponding `VuInputState` via `VUINPUT_STATE`.
|
||||||
|
* Marks `poll.readable = true`.
|
||||||
|
* If `poll.waiting = true`, the watcher clears the flag and optionally calls `fuse_notify_poll()` to wake any waiting FUSE poll requests.
|
||||||
|
|
||||||
|
**Adding / removing devices**
|
||||||
|
|
||||||
|
* When creating a new uinput device, the thread performs `epoll_ctl(ADD)` for the file descriptor.
|
||||||
|
* On device close, it performs `epoll_ctl(DEL)` to remove the descriptor from monitoring.
|
||||||
|
* No separate registry is maintained; the **global `VUINPUT_STATE` HashMap** is the single source of truth.
|
||||||
|
|
||||||
|
**Poll callback behavior**
|
||||||
|
|
||||||
|
* FUSE poll callbacks **do not block**: they may store the poll handle and immediately return.
|
||||||
|
* The background watcher ensures that any pending poll handles are notified asynchronously when data is ready.
|
||||||
|
|
||||||
|
**Read handling**
|
||||||
|
|
||||||
|
* Reads from `/dev/vuinput` are non-blocking:
|
||||||
|
|
||||||
|
* `poll()` detects readiness.
|
||||||
|
* `read()` uses `O_NONBLOCK` and drains all available events.
|
||||||
|
* `EAGAIN` indicates the buffer is empty, at which point `poll.readable` is reset to false.
|
||||||
|
|
||||||
|
**Threading / shutdown**
|
||||||
|
|
||||||
|
* The background watcher uses a 500ms epoll_wait timeout to allow clean shutdown.
|
||||||
|
* It is safe to have a single watcher thread for all devices; epoll scales efficiently with multiple descriptors.
|
||||||
|
|
||||||
|
**Benefits**
|
||||||
|
|
||||||
|
* Fully non-blocking CUSE front-end.
|
||||||
|
* Lightweight: epoll manages file descriptors; no extra registry is needed.
|
||||||
|
* Correctly wakes FUSE poll requests for force feedback / rumble operations.
|
||||||
|
* Consistent with existing dispatcher rules: data-plane operations remain fast; control-plane updates scheduled as jobs if needed.
|
||||||
|
|
||||||
|
Poll/read race rule:
|
||||||
|
`PollState` is the single source of truth for readiness and pending poll waiters.
|
||||||
|
Both `poll()` and `read()` must update it only while holding the per-handle `VuInputState` mutex.
|
||||||
|
|
||||||
|
- `poll()` must first check `readable`; only if false may it enqueue a poll handle.
|
||||||
|
- the watcher sets `readable = true` and atomically drains pending waiters for notification.
|
||||||
|
- `read()` may clear `readable` only after draining the proxied evdev fd until `EAGAIN` (or equivalent proof of emptiness).
|
||||||
|
|
||||||
|
This prevents lost wakeups and stale readiness in the proxy.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -512,6 +727,7 @@ While this design is necessary for mediation, it introduces potential attack sur
|
||||||
* [ ] Use **seccomp** or `systemd` sandboxing (`ProtectSystem`, `ProtectKernelTunables`, `RestrictNamespaces`, etc.).
|
* [ ] Use **seccomp** or `systemd` sandboxing (`ProtectSystem`, `ProtectKernelTunables`, `RestrictNamespaces`, etc.).
|
||||||
* [ ] Eventually migrate to **Rust-native FUSE/Netlink** bindings to remove unsafe dependencies.
|
* [ ] Eventually migrate to **Rust-native FUSE/Netlink** bindings to remove unsafe dependencies.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 5. Background: How are input devices created by the kernel using uinput
|
## 5. Background: How are input devices created by the kernel using uinput
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,34 @@
|
||||||
|
|
||||||
## Integration tests
|
## Integration tests
|
||||||
|
|
||||||
|
Ensure, you have a `/run/vuinputd/vuinput-test`-folder that allows the usage of character devices:
|
||||||
|
```
|
||||||
|
mkdir -p /run/vuinputd/vuinput-test
|
||||||
|
mount -t tmpfs -o mode=755,size=1M tmpfs /run/vuinputd/vuinput-test
|
||||||
|
mkdir -p /run/vuinputd/vuinput-test/dev
|
||||||
|
mkdir -p /run/vuinputd/vuinput-test/dev-input
|
||||||
|
mkdir -p /run/vuinputd/vuinput-test/udev
|
||||||
|
```
|
||||||
|
|
||||||
|
### With bubblewrap
|
||||||
|
|
||||||
Install bubblewrap:
|
Install bubblewrap:
|
||||||
`apt-get install bubblewrap`.
|
`apt-get install bubblewrap`.
|
||||||
|
|
||||||
Run with `cargo test -p vuinputd-tests --features "requires-privileges requires-uinput requires-bwrap"`.
|
Run with `cargo test -p vuinputd-tests --features "requires-privileges requires-uinput requires-bwrap" -- --test-threads=1`.
|
||||||
|
|
||||||
|
### With podman
|
||||||
|
|
||||||
|
Install podman:
|
||||||
|
`apt-get install podman`.
|
||||||
|
|
||||||
|
Create test container for podman
|
||||||
|
```
|
||||||
|
cargo build -p vuinputd-tests
|
||||||
|
podman build --dns 1.1.1.1 -t vuinputd-tests -f vuinputd-tests/podman/Containerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
Run with `cargo test -p vuinputd-tests --features "requires-privileges requires-uinput requires-podman" -- --test-threads=1`.
|
||||||
|
|
||||||
## Performance tests
|
## Performance tests
|
||||||
|
|
||||||
|
|
|
||||||
67
docs/TROUBLESHOOTING.md
Normal file
67
docs/TROUBLESHOOTING.md
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
This document lists known error codes, their meaning, and how to resolve them.
|
||||||
|
|
||||||
|
Error codes are stable identifiers intended to help diagnose problems in
|
||||||
|
different environments such as bare metal, systemd services, and containers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to use this document
|
||||||
|
|
||||||
|
1. Locate the error code printed by the application
|
||||||
|
2. Search for it in this document
|
||||||
|
3. Follow the diagnostic steps
|
||||||
|
4. Apply the suggested resolution
|
||||||
|
|
||||||
|
Error messages may change over time; error codes do not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Code Index
|
||||||
|
|
||||||
|
| Code | Area | Summary |
|
||||||
|
|------|------|--------|
|
||||||
|
| VUI-UDEV-001 | udev | udev control socket not reachable |
|
||||||
|
| VUI-UDEV-002 | udev | could not write into /run/vuinputd/... |
|
||||||
|
| VUI-UDEV-003 | udev | could not remove udev data from ... |
|
||||||
|
| VUI-DEV-001 | ddev | could not remove device node ... |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Codes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### VUI-UDEV-001 — /run/udev/control/ not available. Keyboard or mouse might be unusable.
|
||||||
|
|
||||||
|
**Symptoms**
|
||||||
|
|
||||||
|
* No keyboard or mouse usable
|
||||||
|
|
||||||
|
**Cause**
|
||||||
|
This might be a problem when an application that uses libinput has already been started, because libinput only checks the file existance at startup.
|
||||||
|
|
||||||
|
**How to diagnose**
|
||||||
|
|
||||||
|
Check in container for file existence:
|
||||||
|
```sh
|
||||||
|
ls -l /run/udev/control
|
||||||
|
```
|
||||||
|
|
||||||
|
**Resolution**
|
||||||
|
|
||||||
|
* Create /run/udev/data directory and /run/udev/control file during startup. See [USAGE.md](USAGE.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting Issues
|
||||||
|
|
||||||
|
When reporting an issue, please include:
|
||||||
|
|
||||||
|
* The error code(s)
|
||||||
|
* Full command-line invocation
|
||||||
|
* Execution environment (host, container, systemd)
|
||||||
|
* Relevant debug logs (see [DEBUG.md](DEBUG.md))
|
||||||
|
|
||||||
|
### VUI-UDEV-002 - could not write into /run/vuinputd/...
|
||||||
285
docs/USAGE-NIXOS.md
Normal file
285
docs/USAGE-NIXOS.md
Normal file
|
|
@ -0,0 +1,285 @@
|
||||||
|
# Usage Guide for NixOS
|
||||||
|
|
||||||
|
This guide explains how to set up and use `vuinputd` on NixOS.
|
||||||
|
For general remarks about `vuinputd` and how it works, please refer to [USAGE.md](USAGE.md).
|
||||||
|
|
||||||
|
> **Status:** NixOS is one of the primary target platforms for `vuinputd`. Native packaging
|
||||||
|
> is planned; for now, the configuration below builds `vuinputd` directly from source as part
|
||||||
|
> of the NixOS system.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configurations in the Community
|
||||||
|
|
||||||
|
The following community members have shared their NixOS configurations including `vuinputd`:
|
||||||
|
|
||||||
|
* [ShaneTRS](https://github.com/ShaneTRS/nixos-config/)
|
||||||
|
* [griffi-gh](https://github.com/girl-pp-ua/nixos-infra/)
|
||||||
|
* [Markus328](https://github.com/joleuger/vuinputd/issues/14)
|
||||||
|
|
||||||
|
Feel free to open a GitHub issue or pull request to add your own configuration or remarks.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## NixOS Configuration
|
||||||
|
|
||||||
|
The example below is a self-contained NixOS module that:
|
||||||
|
|
||||||
|
- Builds `vuinputd` from source using `rustPlatform.buildRustPackage`
|
||||||
|
- Installs the required udev rules and hwdb entries
|
||||||
|
- Runs `vuinputd` as a systemd service with a tmpfs for the container-scoped `/dev/input` tree
|
||||||
|
- Applies the correct permissions to `/dev/vuinput` after the daemon starts
|
||||||
|
|
||||||
|
Add the module to your `configuration.nix` imports and run `nixos-rebuild switch`.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
vuinputd = pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "vuinputd";
|
||||||
|
version = "0.3.2-git";
|
||||||
|
|
||||||
|
buildType = "debug";
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.rustPlatform.bindgenHook
|
||||||
|
];
|
||||||
|
|
||||||
|
buildInputs = [ pkgs.udev pkgs.fuse3 ];
|
||||||
|
|
||||||
|
src = pkgs.fetchFromGitHub {
|
||||||
|
owner = "joleuger";
|
||||||
|
repo = "vuinputd";
|
||||||
|
rev = "8c40fdc12005319ea16dceb752a8822abfc6039a";
|
||||||
|
hash = "sha256-8Q34B04BngZqRLyixeFq8F1t5wFnk6JpaG3EEbgKRcU=";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargoHash = "sha256-nJw9bRh6Yn9g1H5SeoT6zxgZLCqV3AtAs9gMfE+P+CU=";
|
||||||
|
|
||||||
|
# Recent versions of fuse3 expose additional libfuse_* types that bindgen
|
||||||
|
# needs to allowlist alongside the standard fuse_* types.
|
||||||
|
postPatch = ''
|
||||||
|
substituteInPlace cuse-lowlevel/build.rs \
|
||||||
|
--replace-fail '.allowlist_type("(?i)^fuse.*")' '.allowlist_type("(?i)^(fuse|libfuse).*")'
|
||||||
|
'';
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
mkdir -p $out/lib/udev/rules.d
|
||||||
|
mkdir $out/lib/udev/hwdb.d
|
||||||
|
cp vuinputd/udev/*.rules $out/lib/udev/rules.d/
|
||||||
|
cp vuinputd/udev/*.hwdb $out/lib/udev/hwdb.d/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
vuinputd
|
||||||
|
bubblewrap # required for running containerized applications via bwrap
|
||||||
|
];
|
||||||
|
|
||||||
|
# Main vuinputd daemon.
|
||||||
|
# Before starting, a tmpfs is mounted at /run/vuinputd/vuinput/dev-input.
|
||||||
|
# This directory serves as the container-scoped /dev/input tree: input devices
|
||||||
|
# created by vuinputd are placed here instead of the host's /dev/input, so
|
||||||
|
# that containers see only their own devices.
|
||||||
|
systemd.services.vuinputd = {
|
||||||
|
enable = true;
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
unitConfig = {
|
||||||
|
Description = "Virtual input (/dev/vuinput) daemon";
|
||||||
|
};
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "exec";
|
||||||
|
ExecStartPre = pkgs.writeShellScript "mount-tmpfs-dev-input" ''
|
||||||
|
mkdir -p /run/vuinputd/vuinput/dev-input
|
||||||
|
${pkgs.util-linux}/bin/mount -t tmpfs -o rw,dev,nosuid tmpfs /run/vuinputd/vuinput/dev-input
|
||||||
|
'';
|
||||||
|
ExecStart = "${lib.getExe vuinputd} --major 120 --minor 414795 --placement on-host";
|
||||||
|
ExecStopPost = pkgs.writeShellScript "umount-dev-input" ''
|
||||||
|
${pkgs.util-linux}/bin/umount /run/vuinputd/vuinput/dev-input
|
||||||
|
'';
|
||||||
|
Restart = "on-failure";
|
||||||
|
# Required to allow vuinputd to access character devices (uinput, CUSE).
|
||||||
|
DeviceAllow = "char-* rwm";
|
||||||
|
Environment = [
|
||||||
|
"RUST_LOG=debug"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# vuinputd creates /dev/vuinput via CUSE. The device initially has restrictive
|
||||||
|
# permissions, so a one-shot service applies chmod 666 shortly after startup.
|
||||||
|
# A proper udev-based solution is planned to replace this workaround.
|
||||||
|
systemd.services.vuinputd-chmod = {
|
||||||
|
unitConfig.Description = "Chmod 666 /dev/vuinput";
|
||||||
|
wantedBy = [ "vuinputd.service" ];
|
||||||
|
after = [ "vuinputd.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ExecStart = pkgs.writeShellScript "chmod-vuinput" ''
|
||||||
|
sleep 2 && chmod 666 /dev/vuinput
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Configuration Notes
|
||||||
|
|
||||||
|
**`--major` and `--minor`**
|
||||||
|
These are the device numbers assigned to the virtual `/dev/uinput` character device exposed
|
||||||
|
inside the container. The values `120` and `414795` are chosen to avoid conflicts with
|
||||||
|
real devices on the host. Refer to [USAGE.md](USAGE.md) for details on choosing these values.
|
||||||
|
|
||||||
|
**`--placement on-host`**
|
||||||
|
Tells `vuinputd` to place the resulting `/dev/input/event*` devices on the host side (under
|
||||||
|
the tmpfs at `/run/vuinputd/vuinput/dev-input`) rather than directly in the host's `/dev/input`.
|
||||||
|
This is what enables per-container input isolation.
|
||||||
|
|
||||||
|
**`DeviceAllow = "char-* rwm"`**
|
||||||
|
`vuinputd` needs access to `/dev/uinput` (to create real input devices on the host) and to
|
||||||
|
the CUSE subsystem (to expose the virtual `/dev/uinput` inside containers). Both are character
|
||||||
|
devices, so this broad allowlist is currently required. Reducing the attack surface here is a
|
||||||
|
[planned hardening step](https://github.com/joleuger/vuinputd/blob/main/docs/DESIGN.md).
|
||||||
|
|
||||||
|
**`--device-policy`**
|
||||||
|
The `ExecStart` line can be extended with a `--device-policy` flag to control which input
|
||||||
|
capabilities and events the daemon exposes to containerized applications:
|
||||||
|
|
||||||
|
| Policy | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `none` | All capabilities allowed; no filtering. Useful for debugging. |
|
||||||
|
| `mute-sys-rq` | Blocks SysRq key handling. All other input passes through. **(default)** |
|
||||||
|
| `sanitized` | Keyboards and mice only; filters SysRq and VT-switching combos. Recommended for desktop/streaming workloads. |
|
||||||
|
| `strict-gamepad` | Gamepad-like devices only; blocks keyboards and mice entirely. |
|
||||||
|
|
||||||
|
For example, to use the recommended policy for a Sunshine streaming container:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
ExecStart = "${lib.getExe vuinputd} --major 120 --minor 414795 --placement on-host --device-policy sanitized";
|
||||||
|
```
|
||||||
|
|
||||||
|
See [USAGE.md](USAGE.md) for a full description of each policy.
|
||||||
|
|
||||||
|
**The `vuinputd-chmod` service**
|
||||||
|
The CUSE device `/dev/vuinput` is created by the kernel with root-only permissions. Until
|
||||||
|
a proper udev rule handles this, a small one-shot service applies `chmod 666` two seconds
|
||||||
|
after the daemon starts. This is a known rough edge and will be improved.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running a Containerized Application
|
||||||
|
|
||||||
|
Once `vuinputd` is running, start a containerized application by binding the virtual devices
|
||||||
|
into its namespace. The example below uses `bwrap` (Bubblewrap) as a lightweight container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bwrap \
|
||||||
|
--unshare-net \
|
||||||
|
--ro-bind / / \
|
||||||
|
--tmpfs /tmp \
|
||||||
|
--tmpfs /run/udev \
|
||||||
|
--dev-bind /run/vuinputd/vuinput/dev-input /dev/input \
|
||||||
|
--dev-bind /dev/vuinput /dev/uinput \
|
||||||
|
<your-application>
|
||||||
|
```
|
||||||
|
|
||||||
|
The two `--dev-bind` flags are the core of the integration:
|
||||||
|
|
||||||
|
| Bind | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `/run/vuinputd/vuinput/dev-input` → `/dev/input` | Gives the container its own isolated `/dev/input` tree populated by `vuinputd`. |
|
||||||
|
| `/dev/vuinput` → `/dev/uinput` | Exposes the CUSE-backed virtual `/dev/uinput` at the standard path the application expects. |
|
||||||
|
|
||||||
|
The `--tmpfs /run/udev` flag provides a writable but empty udev runtime directory inside the
|
||||||
|
sandbox. This is sufficient when using `--placement on-host`, because `vuinputd` forwards udev
|
||||||
|
events into the container directly. If you switch to `--placement in-container`, replace this
|
||||||
|
flag with a bind-mount of the actual udev runtime directory instead, and create the required
|
||||||
|
stubs inside the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /run/udev/data/
|
||||||
|
touch /run/udev/control
|
||||||
|
```
|
||||||
|
|
||||||
|
For instructions on testing this setup in an isolated VM, see
|
||||||
|
[Testing vuinputd on NixOS with Incus](https://github.com/joleuger/vuinputd/blob/main/distro-tests/nixos/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verifying Operation
|
||||||
|
|
||||||
|
To confirm that `vuinputd` and the container integration are working correctly, run the
|
||||||
|
following checks inside the container (install `libinput-tools` and `evtest` if needed):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Watch for device creation and input events
|
||||||
|
libinput debug-events
|
||||||
|
|
||||||
|
# Observe udev announcements in a second terminal
|
||||||
|
udevadm monitor -p
|
||||||
|
|
||||||
|
# Read raw events from the input device
|
||||||
|
evtest /dev/input/event*
|
||||||
|
```
|
||||||
|
|
||||||
|
Then trigger some input from within the container (e.g. run a test binary or move a virtual
|
||||||
|
mouse). You should see device creation reported by `libinput` and `udevadm`, and raw event
|
||||||
|
data in `evtest`. On the host, `journalctl -u vuinputd` should show corresponding log lines
|
||||||
|
about device creation and event forwarding.
|
||||||
|
|
||||||
|
For a more detailed walkthrough with example output, see the [Verifying Operation](USAGE.md#7-verifying-operation)
|
||||||
|
section in the main usage guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phantom Input Events and VT Handling
|
||||||
|
|
||||||
|
On headless NixOS systems (no active graphical session), the Linux kernel's virtual terminal
|
||||||
|
(VT) layer remains active and continues to process keyboard input. This can cause injected
|
||||||
|
input forwarded by `vuinputd` to reach `getty` login prompts or trigger kernel hotkeys such
|
||||||
|
as `Ctrl+Alt+Fn`.
|
||||||
|
|
||||||
|
The quickest mitigation is to start `vuinputd` with the `--vt-guard` flag:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
ExecStart = "${lib.getExe vuinputd} --major 120 --minor 414795 --placement on-host --vt-guard";
|
||||||
|
```
|
||||||
|
|
||||||
|
`--vt-guard` switches the active VT into graphics mode via a direct ioctl, which disables
|
||||||
|
the kernel keyboard handler for that VT without requiring a compositor or DRM device.
|
||||||
|
|
||||||
|
For a full discussion of all available approaches (including KMSCON and the experimental
|
||||||
|
`fallbackdm`), see the [Phantom Input Events](USAGE.md#8-handling-phantom-input-events-caused-by-vts)
|
||||||
|
section in the main usage guide.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
If `vuinputd` does not behave as expected, refer to
|
||||||
|
[DEBUG.md](https://github.com/joleuger/vuinputd/blob/main/docs/DEBUG.md) for general
|
||||||
|
debugging guidance.
|
||||||
|
|
||||||
|
Common NixOS-specific issues:
|
||||||
|
|
||||||
|
- **CUSE module not loaded:** NixOS should load `cuse` automatically via udev, but if
|
||||||
|
`/dev/vuinput` does not appear after the service starts, run `modprobe cuse` and restart
|
||||||
|
the service.
|
||||||
|
- **`/dev/vuinput` is not accessible:** The `vuinputd-chmod` service applies permissions
|
||||||
|
2 seconds after startup. If it fails, check `systemctl status vuinputd-chmod` and apply
|
||||||
|
`chmod 666 /dev/vuinput` manually for debugging.
|
||||||
|
- **Input devices not visible inside the container:** Verify that
|
||||||
|
`/run/vuinputd/vuinput/dev-input` is mounted as a tmpfs (`mount | grep vuinputd`) and that
|
||||||
|
the `bwrap` `--dev-bind` flags point to the correct paths.
|
||||||
|
- **Read-only filesystem error from `vuinputd`:** If the daemon logs an error like
|
||||||
|
`ReadOnlyFilesystem` when creating a device node, the directory where it tries to write
|
||||||
|
(typically `/dev/input` or `/run`) is not writable inside the container. Ensure the
|
||||||
|
`--dev-bind` and `--tmpfs` flags in your `bwrap` command cover all paths `vuinputd` writes
|
||||||
|
to, or switch to `--placement on-host` so writes happen on the host side instead.
|
||||||
|
|
||||||
|
```
|
||||||
|
Error creating input device /dev/input/event12: Read-only file system
|
||||||
|
```
|
||||||
|
- **Build failures due to bindgen/fuse3 mismatch:** Ensure the `postPatch` block in the
|
||||||
|
derivation is present; it is required for recent versions of `fuse3`.
|
||||||
273
docs/USAGE.md
273
docs/USAGE.md
|
|
@ -16,6 +16,25 @@ This guide shows how to:
|
||||||
2. Connect it to the host’s virtual `/dev/uinput`
|
2. Connect it to the host’s virtual `/dev/uinput`
|
||||||
3. Verify that device creation and input forwarding work correctly
|
3. Verify that device creation and input forwarding work correctly
|
||||||
|
|
||||||
|
### Runtime Artifact Placement
|
||||||
|
|
||||||
|
`vuinputd` supports different **placement modes** that control where runtime artifacts
|
||||||
|
(device nodes *and* associated udev data) are created.
|
||||||
|
|
||||||
|
This is configured via the `--placement` command-line option and affects:
|
||||||
|
|
||||||
|
* the virtual input device nodes
|
||||||
|
* the corresponding `/run/udev` runtime data used by libudev-based applications
|
||||||
|
|
||||||
|
### Device Policies
|
||||||
|
|
||||||
|
`vuinputd` can enforce **device policies** that control which input capabilities
|
||||||
|
and events are exposed to applications.
|
||||||
|
|
||||||
|
Policies are applied at device creation time and operate independently of
|
||||||
|
container runtime or placement mode.
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Prerequisites
|
## 2. Prerequisites
|
||||||
|
|
@ -90,7 +109,141 @@ The `vuinputd` daemon on the host should provide some logs. The following sectio
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Runtime-Specific Setup
|
## 4. Special command line settings
|
||||||
|
|
||||||
|
### Placement Modes
|
||||||
|
|
||||||
|
`vuinputd` can be configured to place runtime artifacts in different locations depending
|
||||||
|
on your container setup and isolation model.
|
||||||
|
|
||||||
|
#### `--placement in-container` (default)
|
||||||
|
|
||||||
|
* Device nodes and udev runtime data are created **inside the container**
|
||||||
|
* Requires writable `/dev` and `/run` inside the container
|
||||||
|
* No bind-mounts required
|
||||||
|
* Best suited for tightly integrated or ephemeral containers
|
||||||
|
|
||||||
|
#### `--placement on-host`
|
||||||
|
|
||||||
|
* Device nodes and udev runtime data are created **on the host** under:
|
||||||
|
* `/run/vuinputd/{devname}/dev-input`
|
||||||
|
* `/run/vuinputd/{devname}/udev`
|
||||||
|
* `/run/vuinputd/{devname}/dev-input` **must** have the mount option `dev`
|
||||||
|
* The user is expected to **bind-mount these directories** into the container
|
||||||
|
* Suitable for:
|
||||||
|
* read-only containers
|
||||||
|
* advanced sandboxing scenarios
|
||||||
|
|
||||||
|
#### `--placement none`
|
||||||
|
|
||||||
|
* No device nodes or udev runtime data are created
|
||||||
|
* Useful when:
|
||||||
|
* devices are managed externally
|
||||||
|
* running in dry-run or control-only mode
|
||||||
|
* debugging or testing non-input-related functionality
|
||||||
|
|
||||||
|
### Device Policies
|
||||||
|
|
||||||
|
Device policies define which input capabilities are allowed and which events
|
||||||
|
are filtered out for devices created by `vuinputd`.
|
||||||
|
|
||||||
|
They are configured using the `--device-policy` command-line option.
|
||||||
|
|
||||||
|
#### Available Policies
|
||||||
|
|
||||||
|
`--device-policy none`
|
||||||
|
* Allows **all device capabilities**
|
||||||
|
* No filtering is applied
|
||||||
|
* Useful for debugging or trusted environments
|
||||||
|
|
||||||
|
`--device-policy mute-sys-rq` (default)
|
||||||
|
|
||||||
|
* Blocks **SysRq** key handling
|
||||||
|
* Allows all other input events
|
||||||
|
* Prevents accidental or malicious kernel-level hotkeys
|
||||||
|
* Please read the section 'Handling Phantom Input Events Caused by VTs'
|
||||||
|
|
||||||
|
`--device-policy sanitized`
|
||||||
|
|
||||||
|
* Allows keyboards and mice
|
||||||
|
* Filters out dangerous key combinations, including:
|
||||||
|
* SysRq
|
||||||
|
* Virtual terminal switching (e.g. `Ctrl+Alt+Fn`)
|
||||||
|
* Recommended for most containerized desktop or streaming workloads
|
||||||
|
* Caution: This is **experimental**; in case there are combos that should be filtered as well, please post an issue
|
||||||
|
|
||||||
|
`--device-policy strict-gamepad`
|
||||||
|
|
||||||
|
* Only allows **gamepad-like devices**
|
||||||
|
* Blocks keyboards and mice entirely
|
||||||
|
* Intended for:
|
||||||
|
* gaming-focused containers
|
||||||
|
* sandboxed input forwarding
|
||||||
|
* untrusted workloads
|
||||||
|
|
||||||
|
### Multiple Independent `vuinputd` Instances
|
||||||
|
|
||||||
|
`vuinputd` supports running **multiple independent daemon instances**, each managing its **own virtual uinput device**.
|
||||||
|
This is achieved by explicitly configuring the device name and (optionally) the major/minor numbers.
|
||||||
|
|
||||||
|
This feature is primarily intended for:
|
||||||
|
|
||||||
|
* strong fault isolation between containers
|
||||||
|
* per-container `vuinputd` instances (especially with `--placement on-host`)
|
||||||
|
* development and testing,
|
||||||
|
* integration testing with multiple concurrent input stacks
|
||||||
|
|
||||||
|
#### Device Identification Options
|
||||||
|
|
||||||
|
The following command-line options control the identity of the virtual device created by `vuinputd`:
|
||||||
|
|
||||||
|
* `--devname <name>`
|
||||||
|
Name of the device node **without** the `/dev/` prefix
|
||||||
|
(e.g. `vuinput0` → `/dev/vuinput0`)
|
||||||
|
|
||||||
|
* `--major <number>`
|
||||||
|
Explicit major device number. Using 0 for both major and minor means auto assign.
|
||||||
|
|
||||||
|
* `--minor <number>`
|
||||||
|
Explicit minor device number. Using 0 for both major and minor means auto assign.
|
||||||
|
|
||||||
|
If not specified, `vuinputd` uses the default device identity `vuinput`.
|
||||||
|
|
||||||
|
#### Why This Matters
|
||||||
|
|
||||||
|
By default, all containers share the same virtual uinput endpoint.
|
||||||
|
While this is sufficient for many setups, it couples failure domains:
|
||||||
|
|
||||||
|
* a bug or crash in one workload may affect others
|
||||||
|
* reproducing issues becomes harder when state is shared
|
||||||
|
|
||||||
|
Using explicit device identities ensures failures and misbehaving clients are contained per instance.
|
||||||
|
|
||||||
|
#### Example: One `vuinputd` Instance per Container (Host Placement)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vuinputd --placement on-host --devname vuinput-container-a
|
||||||
|
```
|
||||||
|
|
||||||
|
The container would then bind-mount:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/run/vuinputd/vuinput/dev/vuinput-container-a → /dev/uinput
|
||||||
|
```
|
||||||
|
|
||||||
|
A second container can run its own instance with a different device:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vuinputd \
|
||||||
|
vuinputd --placement on-host --devname vuinput-container-b
|
||||||
|
```
|
||||||
|
|
||||||
|
No state, devices, or udev data are shared between the two instances.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Runtime-Specific Setup
|
||||||
|
|
||||||
### 🐳 Docker
|
### 🐳 Docker
|
||||||
|
|
||||||
|
|
@ -153,7 +306,7 @@ Then restart the container.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Inside the Container
|
## 6. Inside the Container
|
||||||
|
|
||||||
Once inside the container shell:
|
Once inside the container shell:
|
||||||
|
|
||||||
|
|
@ -165,13 +318,17 @@ apt-get update
|
||||||
apt-get install libinput-tools udev evtest tmux
|
apt-get install libinput-tools udev evtest tmux
|
||||||
|
|
||||||
# Prepare udev stubs
|
# Prepare udev stubs
|
||||||
|
# Note:
|
||||||
|
# The following steps are only required when using `--placement in-container`.
|
||||||
|
# When using `--placement on-host`, the udev runtime data is created on the host
|
||||||
|
# and must be bind-mounted into the container instead.
|
||||||
mkdir -p /run/udev/data/
|
mkdir -p /run/udev/data/
|
||||||
touch /run/udev/control
|
touch /run/udev/control
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Verifying Operation
|
## 7. Verifying Operation
|
||||||
|
|
||||||
To test everything, use multiple `tmux` windows for parallel monitoring.
|
To test everything, use multiple `tmux` windows for parallel monitoring.
|
||||||
|
|
||||||
|
|
@ -225,7 +382,111 @@ Sample output from `journalctl` showing vuinputd output:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Troubleshooting
|
## 8. Handling Phantom Input Events Caused by VTs
|
||||||
|
|
||||||
|
On Linux systems without an active graphical session (X11 or Wayland), **virtual terminals (VTs)** remain in text mode (`KD_TEXT`) and continue to process keyboard input via the kernel VT keyboard handler.
|
||||||
|
This can lead to *phantom input events*, where injected or forwarded input (e.g. via `vuinputd`) unintentionally reaches:
|
||||||
|
|
||||||
|
* `getty` login prompts
|
||||||
|
* inactive consoles
|
||||||
|
* kernel VT hotkeys (e.g. `Ctrl+Alt+Fn`)
|
||||||
|
|
||||||
|
The following approaches can be used to prevent or mitigate this behavior.
|
||||||
|
|
||||||
|
### Solution 1: Use KMSCON (DRM/KMS-based console)
|
||||||
|
|
||||||
|
A robust solution is to replace the kernel VT text console with a **DRM/KMS-based console** such as `kmscon`. This is very likely the solution that works natively with Fedora 44+ (see [phoronix.com](https://www.phoronix.com/news/Fedora-44-Considers-KMSCON)).
|
||||||
|
|
||||||
|
#### How it helps
|
||||||
|
|
||||||
|
* The kernel VT is no longer responsible for input handling
|
||||||
|
* Keyboard input is processed via evdev, not the VT layer
|
||||||
|
* Seat assignment is respected:
|
||||||
|
|
||||||
|
* devices on non-default seats (e.g. `seat_vuinput`) are ignored
|
||||||
|
* Phantom input events do not reach `getty`
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
|
||||||
|
* Requires DRM/KMS availability
|
||||||
|
* On most real GPUs, the DRM device remains available even when no monitor is connected and enters a hotplug-waiting state
|
||||||
|
* For headless systems, a virtual KMS device can be used:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
modprobe vkms
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Trade-offs
|
||||||
|
|
||||||
|
* Additional dependencies (DRM, kmscon)
|
||||||
|
* Not always desired for minimal or embedded systems
|
||||||
|
|
||||||
|
### Solution 2: VT Guard Mode (`--vt-guard`)
|
||||||
|
|
||||||
|
`vuinputd` can be started with the `--vt-guard` flag to explicitly neutralize VT input handling.
|
||||||
|
|
||||||
|
#### How it works
|
||||||
|
|
||||||
|
At startup, `vuinputd` performs a minimal VT operation such as:
|
||||||
|
|
||||||
|
* switching the active VT into graphics mode (`KD_GRAPHICS`), or
|
||||||
|
* disabling the kernel keyboard processing for that VT
|
||||||
|
|
||||||
|
This is done via direct VT ioctls (e.g. `KDSETMODE`), ensuring that:
|
||||||
|
|
||||||
|
* the kernel VT keyboard handler is inactive
|
||||||
|
* `getty` does not receive injected input events
|
||||||
|
|
||||||
|
#### Characteristics
|
||||||
|
|
||||||
|
* Very lightweight
|
||||||
|
* No DRM, compositor, or additional services required
|
||||||
|
* Effective even on fully headless systems
|
||||||
|
|
||||||
|
#### Caveats
|
||||||
|
|
||||||
|
* Relies on low-level VT ioctls
|
||||||
|
* Considered **hacky**, but intentionally minimal
|
||||||
|
* Bypasses higher-level session management
|
||||||
|
|
||||||
|
### Solution 3: fallbackdm (Work in Progress)
|
||||||
|
|
||||||
|
`fallbackdm` is an experimental, lightweight **logind-integrated fallback display manager**. `fallbackdm` is available at https://github.com/joleuger/fallbackdm.
|
||||||
|
|
||||||
|
#### Intended behavior
|
||||||
|
|
||||||
|
* Starts only when no graphical session is active
|
||||||
|
* Registers a proper `greeter` session with `systemd-logind`
|
||||||
|
* Takes ownership of a VT and switches it to `KD_GRAPHICS`
|
||||||
|
* Prevents `getty` and the VT keyboard handler from receiving input
|
||||||
|
* Leaves other VTs untouched for emergency local access
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
|
||||||
|
* Clean integration with `systemd-logind`
|
||||||
|
* No direct VT hacks
|
||||||
|
* Compatible with standard Linux session semantics
|
||||||
|
* Designed to coexist with real display managers
|
||||||
|
|
||||||
|
#### Status
|
||||||
|
|
||||||
|
* Currently under development
|
||||||
|
* Intended as the long-term, principled solution
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Solution | Headless | Lightweight | logind-aware | Recommended for |
|
||||||
|
| ------------ | --------- | ----------- | ------------ | ---------------------------- |
|
||||||
|
| KMSCON | ⚠️ (vkms) | ❌ | ✅ | Full console replacement |
|
||||||
|
| `--vt-guard` | ✅ | ✅ | ❌ | Minimal setups |
|
||||||
|
| fallbackdm | ✅ | ⚠️ | ✅ | Long-term, clean integration |
|
||||||
|
|
||||||
|
Choose the approach that best fits your system constraints and deployment model.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Troubleshooting
|
||||||
|
|
||||||
| Symptom | Possible Cause | Fix |
|
| Symptom | Possible Cause | Fix |
|
||||||
| --------------------------- | ------------------------------------ | ------------------------------------------------- |
|
| --------------------------- | ------------------------------------ | ------------------------------------------------- |
|
||||||
|
|
@ -246,7 +507,7 @@ Dez 14 21:33:17 wohnzimmer vuinputd[2172719]: called `Result::unwrap()` on an `E
|
||||||
Ensure /dev and /run are writable in the container. If in doubt, use tmpfs.
|
Ensure /dev and /run are writable in the container. If in doubt, use tmpfs.
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Notes and Advanced Topics
|
## 10. Notes and Advanced Topics
|
||||||
|
|
||||||
* You can safely run **multiple containers**.
|
* You can safely run **multiple containers**.
|
||||||
* Devices are automatically cleaned up when the container stops.
|
* Devices are automatically cleaned up when the container stops.
|
||||||
|
|
@ -258,7 +519,7 @@ Ensure /dev and /run are writable in the container. If in doubt, use tmpfs.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. References
|
## 11. References
|
||||||
|
|
||||||
* [mkosi manual](https://github.com/systemd/mkosi/blob/main/mkosi/resources/man/mkosi.1.md)
|
* [mkosi manual](https://github.com/systemd/mkosi/blob/main/mkosi/resources/man/mkosi.1.md)
|
||||||
* [Docker device rules documentation](https://docs.docker.com/engine/reference/run/#device-cgroup-rule)
|
* [Docker device rules documentation](https://docs.docker.com/engine/reference/run/#device-cgroup-rule)
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,17 @@ name = "vuinputd-tests"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "test-ipc"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "test-keyboard"
|
name = "test-keyboard"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "bwrap-ipc"
|
name = "test-ok"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "test-scenarios"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
uinput-ioctls = { path = "../uinput-ioctls" }
|
uinput-ioctls = { path = "../uinput-ioctls" }
|
||||||
|
|
@ -21,5 +27,7 @@ serde_json = "1.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
requires-privileges = []
|
requires-privileges = []
|
||||||
|
requires-rootless = []
|
||||||
requires-uinput = []
|
requires-uinput = []
|
||||||
requires-bwrap = []
|
requires-bwrap = []
|
||||||
|
requires-podman = []
|
||||||
9
vuinputd-tests/podman/Containerfile
Normal file
9
vuinputd-tests/podman/Containerfile
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Build from project root
|
||||||
|
# > cargo build -p vuinputd-tests
|
||||||
|
# > podman build --dns 1.1.1.1 -t vuinputd-tests -f vuinputd-tests/podman/Containerfile .
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
RUN apt-get update && apt-get install -yy strace && apt-get clean
|
||||||
|
COPY target/debug/test-ipc /test-ipc
|
||||||
|
COPY target/debug/test-keyboard /test-keyboard
|
||||||
|
COPY target/debug/test-ok /test-ok
|
||||||
|
|
@ -5,10 +5,10 @@
|
||||||
use core::panic;
|
use core::panic;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use vuinputd_tests::bwrap::SandboxChildIpc;
|
use vuinputd_tests::ipc::SandboxChildIpc;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("starting bwrap-ipc");
|
println!("starting test-ipc");
|
||||||
let ipc = unsafe { SandboxChildIpc::from_fd() };
|
let ipc = unsafe { SandboxChildIpc::from_fd() };
|
||||||
|
|
||||||
let incoming = ipc
|
let incoming = ipc
|
||||||
7
vuinputd-tests/src/bin/test-ok.rs
Normal file
7
vuinputd-tests/src/bin/test-ok.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("test-ok");
|
||||||
|
}
|
||||||
84
vuinputd-tests/src/bin/test-scenarios.rs
Normal file
84
vuinputd-tests/src/bin/test-scenarios.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use vuinputd_tests::scenarios::{
|
||||||
|
basic_keyboard::BasicKeyboard, basic_mouse::BasicMouse, basic_ps4_gamepad::BasicPs4Gamepad,
|
||||||
|
basic_xbox_gamepad::BasicXboxGamepad, ff_xbox_gamepad::FfXboxGamepad, BasicMouseAbsolute,
|
||||||
|
ScenarioArgs,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "test-scenarios")]
|
||||||
|
#[command(about = "Test scenarios for vuinputd", long_about = None)]
|
||||||
|
struct Cli {
|
||||||
|
/// Run scenarios in IPC mode (communicate with vuinputd daemon)
|
||||||
|
#[arg(short, long, default_value_t = false)]
|
||||||
|
ipc: bool,
|
||||||
|
|
||||||
|
/// Path to uinput device
|
||||||
|
#[arg(short, long, default_value = "/dev/uinput")]
|
||||||
|
dev_path: String,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Commands {
|
||||||
|
/// Basic keyboard test
|
||||||
|
BasicKeyboard,
|
||||||
|
|
||||||
|
/// Basic mouse test
|
||||||
|
BasicMouse,
|
||||||
|
|
||||||
|
/// Basic mouse (absolute) test
|
||||||
|
BasicMouseAbsolute,
|
||||||
|
|
||||||
|
/// Basic PS4 gamepad test
|
||||||
|
BasicPs4Gamepad,
|
||||||
|
|
||||||
|
/// Basic Xbox gamepad test
|
||||||
|
BasicXboxGamepad,
|
||||||
|
|
||||||
|
/// Force feedback / Vibration Xbox gamepad test
|
||||||
|
FfXboxGamepad,
|
||||||
|
/*
|
||||||
|
/// Reuse keyboard test (create, destroy, recreate)
|
||||||
|
ReuseKeyboard,
|
||||||
|
|
||||||
|
/// Reuse Xbox gamepad test (create, destroy, recreate)
|
||||||
|
ReuseXboxGamepad,
|
||||||
|
|
||||||
|
/// Stress test for keyboard (3000 events)
|
||||||
|
StressKeyboard,
|
||||||
|
|
||||||
|
/// Stress test for Xbox gamepad (3000 events)
|
||||||
|
StressXboxGamepad,
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), std::io::Error> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let args = ScenarioArgs {
|
||||||
|
ipc: cli.ipc,
|
||||||
|
dev_path: Some(cli.dev_path),
|
||||||
|
};
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Commands::BasicKeyboard => BasicKeyboard::run(&args),
|
||||||
|
Commands::BasicMouse => BasicMouse::run(&args),
|
||||||
|
Commands::BasicMouseAbsolute => BasicMouseAbsolute::run(&args),
|
||||||
|
Commands::BasicPs4Gamepad => BasicPs4Gamepad::run(&args),
|
||||||
|
Commands::BasicXboxGamepad => BasicXboxGamepad::run(&args),
|
||||||
|
Commands::FfXboxGamepad => FfXboxGamepad::run(&args),
|
||||||
|
/*
|
||||||
|
Commands::ReuseKeyboard => ReuseKeyboard::run(&args),
|
||||||
|
Commands::ReuseXboxGamepad => ReuseXboxGamepad::run(&args),
|
||||||
|
Commands::StressKeyboard => StressKeyboard::run(&args),
|
||||||
|
Commands::StressXboxGamepad => StressXboxGamepad::run(&args),
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,16 +3,17 @@
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd, RawFd};
|
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
|
||||||
use std::os::unix::net::UnixDatagram;
|
use std::os::unix::net::UnixDatagram;
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
use std::process::{Command, Output};
|
use std::process::{Command, Output};
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use nix::errno::Errno;
|
use nix::errno::Errno;
|
||||||
use nix::sys::socket::{socketpair, AddressFamily, SockFlag, SockType};
|
use nix::sys::socket::{socketpair, AddressFamily, SockFlag, SockType};
|
||||||
use nix::unistd::close;
|
use nix::unistd::close;
|
||||||
|
|
||||||
|
use crate::ipc::{SandboxChildIpc, SandboxIpc};
|
||||||
|
|
||||||
/// Check if bubblewrap is available.
|
/// Check if bubblewrap is available.
|
||||||
pub fn bwrap_available() -> bool {
|
pub fn bwrap_available() -> bool {
|
||||||
Command::new("bwrap")
|
Command::new("bwrap")
|
||||||
|
|
@ -22,56 +23,6 @@ pub fn bwrap_available() -> bool {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// IPC handle kept by the parent.
|
|
||||||
pub struct SandboxIpc {
|
|
||||||
sock: UnixDatagram,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SandboxIpc {
|
|
||||||
pub fn recv(&self, read_timeout: Option<Duration>) -> io::Result<Vec<u8>> {
|
|
||||||
let mut buf = vec![0u8; 4096];
|
|
||||||
self.sock.set_read_timeout(read_timeout)?;
|
|
||||||
let n = self.sock.recv(&mut buf)?;
|
|
||||||
buf.truncate(n);
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send(&self, data: &[u8]) -> io::Result<()> {
|
|
||||||
self.sock.send(data)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// IPC handle inside the container.
|
|
||||||
pub struct SandboxChildIpc {
|
|
||||||
sock: UnixDatagram,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SandboxChildIpc {
|
|
||||||
/// FD number is fixed and known.
|
|
||||||
pub const FD: RawFd = 3;
|
|
||||||
|
|
||||||
/// # Safety
|
|
||||||
/// Must only be called once in the child.
|
|
||||||
pub unsafe fn from_fd() -> Self {
|
|
||||||
let sock = UnixDatagram::from_raw_fd(Self::FD);
|
|
||||||
Self { sock }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn send(&self, data: &[u8]) -> io::Result<()> {
|
|
||||||
self.sock.send(data)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recv(&self, read_timeout: Option<Duration>) -> io::Result<Vec<u8>> {
|
|
||||||
let mut buf = vec![0u8; 4096];
|
|
||||||
self.sock.set_read_timeout(read_timeout)?;
|
|
||||||
let n = self.sock.recv(&mut buf)?;
|
|
||||||
buf.truncate(n);
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builder for bubblewrap invocations.
|
/// Builder for bubblewrap invocations.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct BwrapBuilder {
|
pub struct BwrapBuilder {
|
||||||
|
|
@ -107,8 +58,11 @@ impl BwrapBuilder {
|
||||||
// So, we mount a temporary directory that does not have this restrictions.
|
// So, we mount a temporary directory that does not have this restrictions.
|
||||||
self.args.extend([
|
self.args.extend([
|
||||||
"--dev-bind".into(),
|
"--dev-bind".into(),
|
||||||
"/dev/tmp/vuinputd-test".into(),
|
"/run/vuinputd/vuinput-test/dev".into(),
|
||||||
"/dev".into(),
|
"/dev".into(),
|
||||||
|
"--dev-bind".into(),
|
||||||
|
"/run/vuinputd/vuinput-test/dev-input".into(),
|
||||||
|
"/dev/input".into(),
|
||||||
]);
|
]);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
||||||
299
vuinputd-tests/src/devices/device_base.rs
Normal file
299
vuinputd-tests/src/devices/device_base.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::test_log::LoggedInputEvent;
|
||||||
|
use libc::{c_int, close, open, write, O_NONBLOCK, O_RDWR, O_WRONLY};
|
||||||
|
use libc::{input_event, timespec, uinput_setup, CLOCK_MONOTONIC};
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::mem::{size_of, zeroed};
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
use std::os::raw::{c_char, c_void};
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// Constants (same numeric values as in linux headers)
|
||||||
|
pub const EV_SYN: u16 = 0x00;
|
||||||
|
pub const EV_KEY: u16 = 0x01;
|
||||||
|
pub const EV_REL: u16 = 0x02;
|
||||||
|
pub const EV_ABS: u16 = 0x03;
|
||||||
|
pub const EV_FF: u16 = 0x15;
|
||||||
|
pub const SYN_REPORT: u16 = 0;
|
||||||
|
pub const BUS_USB: u16 = 0x03;
|
||||||
|
pub const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/";
|
||||||
|
|
||||||
|
// Absolute Axes
|
||||||
|
pub const ABS_X: u16 = 0x00;
|
||||||
|
pub const ABS_Y: u16 = 0x01;
|
||||||
|
pub const ABS_Z: u16 = 0x02;
|
||||||
|
pub const ABS_RX: u16 = 0x03;
|
||||||
|
pub const ABS_RY: u16 = 0x04;
|
||||||
|
pub const ABS_RZ: u16 = 0x05;
|
||||||
|
pub const ABS_HAT0X: u16 = 0x10;
|
||||||
|
pub const ABS_HAT0Y: u16 = 0x11;
|
||||||
|
|
||||||
|
/// Struct holding device state
|
||||||
|
pub struct DeviceState {
|
||||||
|
pub uinput_fd: i32,
|
||||||
|
pub sysname: String,
|
||||||
|
pub device_name: String,
|
||||||
|
pub event_device_node: String,
|
||||||
|
pub event_device_fd: i32,
|
||||||
|
pub events: Vec<LoggedInputEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for input devices
|
||||||
|
pub trait Device: Sized {
|
||||||
|
fn name() -> &'static str;
|
||||||
|
|
||||||
|
/// open uinput, configure keys, call ui_dev_setup, call ui_dev_create, get sysname and devnode
|
||||||
|
/// open event device
|
||||||
|
fn create(device: Option<&str>, name: &str) -> Result<Self, io::Error>;
|
||||||
|
|
||||||
|
/// call ui_dev_destroy and close fds
|
||||||
|
fn destroy(self);
|
||||||
|
|
||||||
|
/// Get the device state for internal operations
|
||||||
|
fn state(&self) -> &DeviceState;
|
||||||
|
|
||||||
|
/// Get mutable access to device state for updating events
|
||||||
|
fn state_mut(&mut self) -> &mut DeviceState;
|
||||||
|
|
||||||
|
/// Get the uinput file descriptor
|
||||||
|
fn uinput_fd(&self) -> i32 {
|
||||||
|
self.state().uinput_fd
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the sysname path
|
||||||
|
fn sysname(&self) -> &str {
|
||||||
|
&self.state().sysname
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the device name
|
||||||
|
fn device_name(&self) -> &str {
|
||||||
|
&self.state().device_name
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_event_device(&self) -> Result<c_int, io::Error>;
|
||||||
|
|
||||||
|
/// Emit an event to the device
|
||||||
|
fn emit(&self, ev_type: u16, code: u16, val: i32) -> io::Result<()> {
|
||||||
|
emit(self.uinput_fd(), ev_type, code, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an event from the event device
|
||||||
|
fn read_event(&self) -> io::Result<input_event> {
|
||||||
|
let event_device_fd = self.get_event_device()?;
|
||||||
|
read_event(event_device_fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit (to uinput) and read (from evdev) an event with logging
|
||||||
|
fn emit_read_and_log(
|
||||||
|
&mut self,
|
||||||
|
ev_type: u16,
|
||||||
|
code: u16,
|
||||||
|
val: i32,
|
||||||
|
) -> io::Result<LoggedInputEvent> {
|
||||||
|
let event_device_fd = self.get_event_device()?;
|
||||||
|
let event = emit_read_and_log(self.uinput_fd(), event_device_fd, ev_type, code, val, true)?;
|
||||||
|
self.state_mut().events.push(event.clone());
|
||||||
|
Ok(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit and read an event with logging
|
||||||
|
fn emit_to_evdev_read_from_uinput_and_log(
|
||||||
|
&mut self,
|
||||||
|
ev_type: u16,
|
||||||
|
code: u16,
|
||||||
|
val: i32,
|
||||||
|
) -> io::Result<LoggedInputEvent> {
|
||||||
|
let event_device_fd = self.get_event_device()?;
|
||||||
|
let event =
|
||||||
|
emit_read_and_log(event_device_fd, self.uinput_fd(), ev_type, code, val, false)?;
|
||||||
|
self.state_mut().events.push(event.clone());
|
||||||
|
Ok(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the event log
|
||||||
|
fn event_log(&self) -> &[LoggedInputEvent] {
|
||||||
|
&self.state().events
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the event log
|
||||||
|
fn reset_event_log(&mut self) {
|
||||||
|
self.state_mut().events.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the event log as mutable slice
|
||||||
|
fn event_log_mut(&mut self) -> &mut Vec<LoggedInputEvent> {
|
||||||
|
&mut self.state_mut().events
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup the uinput device (calls ui_dev_setup and ui_get_sysname)
|
||||||
|
fn setup_device(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
vendor: u16,
|
||||||
|
product: u16,
|
||||||
|
bustype: u16,
|
||||||
|
ff_effects_max: u32,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
unsafe {
|
||||||
|
let mut usetup: uinput_setup = zeroed();
|
||||||
|
usetup.id.bustype = bustype;
|
||||||
|
usetup.id.vendor = vendor;
|
||||||
|
usetup.id.product = product;
|
||||||
|
usetup.ff_effects_max = ff_effects_max;
|
||||||
|
|
||||||
|
let name_cstr = CString::new(name).unwrap();
|
||||||
|
let name_ptr = usetup.name.as_mut_ptr() as *mut c_char;
|
||||||
|
std::ptr::copy_nonoverlapping(
|
||||||
|
name_cstr.as_ptr(),
|
||||||
|
name_ptr,
|
||||||
|
name_cstr.to_bytes_with_nul().len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let usetup_ptr = &mut usetup as *mut uinput_setup;
|
||||||
|
ui_dev_setup(self.uinput_fd(), usetup_ptr).map_err(|e| {
|
||||||
|
eprintln!("ui_dev_setup failed: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the sysname from the uinput fd
|
||||||
|
fn get_sysname(&self) -> io::Result<String> {
|
||||||
|
unsafe {
|
||||||
|
let mut resultbuf: [c_char; 64] = [0; 64];
|
||||||
|
ui_get_sysname(self.uinput_fd(), resultbuf.as_mut_slice()).map_err(|e| {
|
||||||
|
eprintln!("ui_get_sysname failed: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
Ok(format!(
|
||||||
|
"{}{}",
|
||||||
|
SYS_INPUT_DIR,
|
||||||
|
CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit an event to the uinput device
|
||||||
|
pub fn emit(fd: c_int, ev_type: u16, code: u16, val: i32) -> io::Result<()> {
|
||||||
|
let mut ie: libc::input_event = unsafe { zeroed() };
|
||||||
|
|
||||||
|
ie.time.tv_sec = 0;
|
||||||
|
ie.time.tv_usec = 0;
|
||||||
|
|
||||||
|
ie.type_ = ev_type;
|
||||||
|
ie.code = code;
|
||||||
|
ie.value = val;
|
||||||
|
|
||||||
|
let buf_ptr = &ie as *const libc::input_event as *const c_void;
|
||||||
|
let bytes = size_of::<libc::input_event>();
|
||||||
|
|
||||||
|
let written = unsafe { write(fd, buf_ptr, bytes) };
|
||||||
|
if written as usize != bytes {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit event, sync, and read back with logging
|
||||||
|
pub fn emit_read_and_log(
|
||||||
|
emit_to: c_int,
|
||||||
|
read_from: c_int,
|
||||||
|
ev_type: u16,
|
||||||
|
code: u16,
|
||||||
|
val: i32,
|
||||||
|
emit_syn: bool,
|
||||||
|
) -> io::Result<LoggedInputEvent> {
|
||||||
|
let (time_sent_sec, time_sent_nsec) = monotonic_time();
|
||||||
|
emit(emit_to, ev_type, code, val)?;
|
||||||
|
if emit_syn {
|
||||||
|
emit(emit_to, EV_SYN, SYN_REPORT, 0)?;
|
||||||
|
}
|
||||||
|
let input_event_recv = read_event(read_from).unwrap();
|
||||||
|
if emit_syn {
|
||||||
|
let _syn_recv = read_event(read_from).unwrap();
|
||||||
|
}
|
||||||
|
let (time_recv_sec, time_recv_nsec) = monotonic_time();
|
||||||
|
let duration_usec =
|
||||||
|
(time_recv_sec - time_sent_sec) * 1_000_000 + (time_recv_nsec - time_sent_nsec) / 1000;
|
||||||
|
let send_and_receive_match = input_event_recv.type_ == ev_type
|
||||||
|
&& input_event_recv.code == code
|
||||||
|
&& input_event_recv.value == val;
|
||||||
|
|
||||||
|
Ok(LoggedInputEvent {
|
||||||
|
tv_sec: time_sent_sec,
|
||||||
|
tv_nsec: time_sent_nsec,
|
||||||
|
duration_usec,
|
||||||
|
type_: ev_type,
|
||||||
|
code,
|
||||||
|
value: val,
|
||||||
|
send_and_receive_match,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read an event from the event device
|
||||||
|
pub fn read_event(event_dev_fd: c_int) -> io::Result<input_event> {
|
||||||
|
let mut ev: input_event = unsafe { zeroed() };
|
||||||
|
let ret = unsafe {
|
||||||
|
libc::read(
|
||||||
|
event_dev_fd,
|
||||||
|
&mut ev as *mut _ as *mut c_void,
|
||||||
|
size_of::<input_event>(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if ret as usize != size_of::<input_event>() {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get monotonic time
|
||||||
|
pub fn monotonic_time() -> (i64, i64) {
|
||||||
|
let mut ts = timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::clock_gettime(CLOCK_MONOTONIC, &mut ts);
|
||||||
|
}
|
||||||
|
(ts.tv_sec, ts.tv_nsec)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open uinput device
|
||||||
|
pub fn open_uinput(device: Option<&str>) -> io::Result<i32> {
|
||||||
|
let device = match device {
|
||||||
|
Some(dev_path) => dev_path,
|
||||||
|
_ => "/dev/uinput",
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = std::ffi::CString::new(device).unwrap();
|
||||||
|
let fd = unsafe { open(path.as_ptr(), O_RDWR | O_NONBLOCK) };
|
||||||
|
if fd < 0 {
|
||||||
|
eprintln!("error opening uinput");
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
Ok(fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the event device node from the sysname path
|
||||||
|
pub fn fetch_device_node(sysname: &str) -> io::Result<String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
|
for entry in fs::read_dir(sysname)? {
|
||||||
|
let entry = entry?;
|
||||||
|
if let Some(name) = entry.file_name().to_str() {
|
||||||
|
if name.starts_with("event") {
|
||||||
|
return Ok(format!("/dev/input/{}", name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(io::Error::new(ErrorKind::NotFound, "no device found"))
|
||||||
|
}
|
||||||
345
vuinputd-tests/src/devices/keyboard.rs
Normal file
345
vuinputd-tests/src/devices/keyboard.rs
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::devices::device_base::{
|
||||||
|
fetch_device_node, open_uinput, Device, DeviceState, BUS_USB, SYS_INPUT_DIR,
|
||||||
|
};
|
||||||
|
use libc::{c_int, close, open};
|
||||||
|
use std::io;
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
/// Key codes. Those are used by udev to recognize a device as a keyboard.
|
||||||
|
pub const KEY_ESC: u16 = 1;
|
||||||
|
pub const KEY_1: u16 = 2;
|
||||||
|
pub const KEY_2: u16 = 3;
|
||||||
|
pub const KEY_3: u16 = 4;
|
||||||
|
pub const KEY_4: u16 = 5;
|
||||||
|
pub const KEY_5: u16 = 6;
|
||||||
|
pub const KEY_6: u16 = 7;
|
||||||
|
pub const KEY_7: u16 = 8;
|
||||||
|
pub const KEY_8: u16 = 9;
|
||||||
|
pub const KEY_9: u16 = 10;
|
||||||
|
pub const KEY_0: u16 = 11;
|
||||||
|
pub const KEY_MINUS: u16 = 12;
|
||||||
|
pub const KEY_EQUAL: u16 = 13;
|
||||||
|
pub const KEY_BACKSPACE: u16 = 14;
|
||||||
|
pub const KEY_TAB: u16 = 15;
|
||||||
|
pub const KEY_Q: u16 = 16;
|
||||||
|
pub const KEY_W: u16 = 17;
|
||||||
|
pub const KEY_E: u16 = 18;
|
||||||
|
pub const KEY_R: u16 = 19;
|
||||||
|
pub const KEY_T: u16 = 20;
|
||||||
|
pub const KEY_Y: u16 = 21;
|
||||||
|
pub const KEY_U: u16 = 22;
|
||||||
|
pub const KEY_I: u16 = 23;
|
||||||
|
pub const KEY_O: u16 = 24;
|
||||||
|
pub const KEY_P: u16 = 25;
|
||||||
|
pub const KEY_LEFTBRACE: u16 = 26;
|
||||||
|
pub const KEY_RIGHTBRACE: u16 = 27;
|
||||||
|
pub const KEY_ENTER: u16 = 28;
|
||||||
|
pub const KEY_LEFTCTRL: u16 = 29;
|
||||||
|
pub const KEY_A: u16 = 30;
|
||||||
|
pub const KEY_S: u16 = 31;
|
||||||
|
|
||||||
|
/// Space and other common keys
|
||||||
|
pub const KEY_D: u16 = 32;
|
||||||
|
pub const KEY_F: u16 = 33;
|
||||||
|
pub const KEY_G: u16 = 34;
|
||||||
|
pub const KEY_H: u16 = 35;
|
||||||
|
pub const KEY_J: u16 = 36;
|
||||||
|
pub const KEY_K: u16 = 37;
|
||||||
|
pub const KEY_L: u16 = 38;
|
||||||
|
pub const KEY_SEMICOLON: u16 = 39;
|
||||||
|
pub const KEY_APOSTROPHE: u16 = 40;
|
||||||
|
pub const KEY_GRAVE: u16 = 41;
|
||||||
|
pub const KEY_LEFTSHIFT: u16 = 42;
|
||||||
|
pub const KEY_BACKSLASH: u16 = 43;
|
||||||
|
pub const KEY_Z: u16 = 44;
|
||||||
|
pub const KEY_X: u16 = 45;
|
||||||
|
pub const KEY_C: u16 = 46;
|
||||||
|
pub const KEY_V: u16 = 47;
|
||||||
|
pub const KEY_B: u16 = 48;
|
||||||
|
pub const KEY_N: u16 = 49;
|
||||||
|
pub const KEY_M: u16 = 50;
|
||||||
|
pub const KEY_COMMA: u16 = 51;
|
||||||
|
pub const KEY_DOT: u16 = 52;
|
||||||
|
pub const KEY_SLASH: u16 = 53;
|
||||||
|
pub const KEY_RIGHTSHIFT: u16 = 54;
|
||||||
|
pub const KEY_KPASTERISK: u16 = 55;
|
||||||
|
pub const KEY_LEFTALT: u16 = 56;
|
||||||
|
pub const KEY_SPACE: u16 = 57;
|
||||||
|
pub const KEY_CAPSLOCK: u16 = 58;
|
||||||
|
|
||||||
|
/// Function keys
|
||||||
|
pub const KEY_F1: u16 = 59;
|
||||||
|
pub const KEY_F2: u16 = 60;
|
||||||
|
pub const KEY_F3: u16 = 61;
|
||||||
|
pub const KEY_F4: u16 = 62;
|
||||||
|
pub const KEY_F5: u16 = 63;
|
||||||
|
pub const KEY_F6: u16 = 64;
|
||||||
|
pub const KEY_F7: u16 = 65;
|
||||||
|
pub const KEY_F8: u16 = 66;
|
||||||
|
pub const KEY_F9: u16 = 67;
|
||||||
|
pub const KEY_F10: u16 = 68;
|
||||||
|
pub const KEY_NUMLOCK: u16 = 69;
|
||||||
|
pub const KEY_SCROLLLOCK: u16 = 70;
|
||||||
|
pub const KEY_KP7: u16 = 71;
|
||||||
|
pub const KEY_KP8: u16 = 72;
|
||||||
|
pub const KEY_KP9: u16 = 73;
|
||||||
|
pub const KEY_KPMINUS: u16 = 74;
|
||||||
|
pub const KEY_KP4: u16 = 75;
|
||||||
|
pub const KEY_KP5: u16 = 76;
|
||||||
|
pub const KEY_KP6: u16 = 77;
|
||||||
|
pub const KEY_KPPLUS: u16 = 78;
|
||||||
|
pub const KEY_KP1: u16 = 79;
|
||||||
|
pub const KEY_KP2: u16 = 80;
|
||||||
|
pub const KEY_KP3: u16 = 81;
|
||||||
|
pub const KEY_KP0: u16 = 82;
|
||||||
|
pub const KEY_KPDOT: u16 = 83;
|
||||||
|
|
||||||
|
/// Arrow keys and navigation
|
||||||
|
pub const KEY_ZENKAKUHANKAKU: u16 = 85;
|
||||||
|
pub const KEY_102ND: u16 = 86;
|
||||||
|
pub const KEY_F11: u16 = 87;
|
||||||
|
pub const KEY_F12: u16 = 88;
|
||||||
|
pub const KEY_RO: u16 = 89;
|
||||||
|
pub const KEY_KATAKANA: u16 = 90;
|
||||||
|
pub const KEY_HIRAGANA: u16 = 91;
|
||||||
|
pub const KEY_HENKAN: u16 = 92;
|
||||||
|
pub const KEY_KATAKANAHIRAGANA: u16 = 93;
|
||||||
|
pub const KEY_MUHENKAN: u16 = 94;
|
||||||
|
pub const KEY_KPJPCOMMA: u16 = 95;
|
||||||
|
pub const KEY_KPENTER: u16 = 96;
|
||||||
|
pub const KEY_RIGHTCTRL: u16 = 97;
|
||||||
|
pub const KEY_KPSLASH: u16 = 98;
|
||||||
|
pub const KEY_SYSRQ: u16 = 99;
|
||||||
|
pub const KEY_RIGHTALT: u16 = 100;
|
||||||
|
pub const KEY_LINEFEED: u16 = 101;
|
||||||
|
pub const KEY_HOME: u16 = 102;
|
||||||
|
pub const KEY_UP: u16 = 103;
|
||||||
|
pub const KEY_PAGEUP: u16 = 104;
|
||||||
|
pub const KEY_LEFT: u16 = 105;
|
||||||
|
pub const KEY_RIGHT: u16 = 106;
|
||||||
|
pub const KEY_END: u16 = 107;
|
||||||
|
pub const KEY_DOWN: u16 = 108;
|
||||||
|
pub const KEY_PAGEDOWN: u16 = 109;
|
||||||
|
pub const KEY_INSERT: u16 = 110;
|
||||||
|
pub const KEY_DELETE: u16 = 111;
|
||||||
|
|
||||||
|
/// Configure a full 101-key standard keyboard
|
||||||
|
unsafe fn set_standard_keyboard_keys(fd: c_int) -> Result<(), std::io::Error> {
|
||||||
|
// We need to set more bits so that systemd recognizes a keyboard as a keyboard.
|
||||||
|
// At least the first 32 bits are ESC, numbers, and Q to D, except KEY_RESERVED need to be considered.
|
||||||
|
// udev-builtin-input_id.c consideres the mask = 0xFFFFFFFE
|
||||||
|
|
||||||
|
// EV_KEY
|
||||||
|
ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?;
|
||||||
|
|
||||||
|
// All standard keys (1..101+)
|
||||||
|
let all_keys = [
|
||||||
|
// Modifier + main keys
|
||||||
|
KEY_ESC,
|
||||||
|
KEY_1,
|
||||||
|
KEY_2,
|
||||||
|
KEY_3,
|
||||||
|
KEY_4,
|
||||||
|
KEY_5,
|
||||||
|
KEY_6,
|
||||||
|
KEY_7,
|
||||||
|
KEY_8,
|
||||||
|
KEY_9,
|
||||||
|
KEY_0,
|
||||||
|
KEY_MINUS,
|
||||||
|
KEY_EQUAL,
|
||||||
|
KEY_BACKSPACE,
|
||||||
|
KEY_TAB,
|
||||||
|
KEY_Q,
|
||||||
|
KEY_W,
|
||||||
|
KEY_E,
|
||||||
|
KEY_R,
|
||||||
|
KEY_T,
|
||||||
|
KEY_Y,
|
||||||
|
KEY_U,
|
||||||
|
KEY_I,
|
||||||
|
KEY_O,
|
||||||
|
KEY_P,
|
||||||
|
KEY_LEFTBRACE,
|
||||||
|
KEY_RIGHTBRACE,
|
||||||
|
KEY_ENTER,
|
||||||
|
KEY_LEFTCTRL,
|
||||||
|
KEY_A,
|
||||||
|
KEY_S,
|
||||||
|
KEY_D,
|
||||||
|
KEY_F,
|
||||||
|
KEY_G,
|
||||||
|
KEY_H,
|
||||||
|
KEY_J,
|
||||||
|
KEY_K,
|
||||||
|
KEY_L,
|
||||||
|
KEY_SEMICOLON,
|
||||||
|
KEY_APOSTROPHE,
|
||||||
|
KEY_GRAVE,
|
||||||
|
KEY_LEFTSHIFT,
|
||||||
|
KEY_BACKSLASH,
|
||||||
|
KEY_Z,
|
||||||
|
KEY_X,
|
||||||
|
KEY_C,
|
||||||
|
KEY_V,
|
||||||
|
KEY_B,
|
||||||
|
KEY_N,
|
||||||
|
KEY_M,
|
||||||
|
KEY_COMMA,
|
||||||
|
KEY_DOT,
|
||||||
|
KEY_SLASH,
|
||||||
|
KEY_RIGHTSHIFT,
|
||||||
|
KEY_KPASTERISK,
|
||||||
|
KEY_LEFTALT,
|
||||||
|
KEY_SPACE,
|
||||||
|
KEY_CAPSLOCK,
|
||||||
|
// Function keys
|
||||||
|
KEY_F1,
|
||||||
|
KEY_F2,
|
||||||
|
KEY_F3,
|
||||||
|
KEY_F4,
|
||||||
|
KEY_F5,
|
||||||
|
KEY_F6,
|
||||||
|
KEY_F7,
|
||||||
|
KEY_F8,
|
||||||
|
KEY_F9,
|
||||||
|
KEY_F10,
|
||||||
|
KEY_F11,
|
||||||
|
KEY_F12,
|
||||||
|
KEY_NUMLOCK,
|
||||||
|
KEY_SCROLLLOCK,
|
||||||
|
// Keypad
|
||||||
|
KEY_KP7,
|
||||||
|
KEY_KP8,
|
||||||
|
KEY_KP9,
|
||||||
|
KEY_KPMINUS,
|
||||||
|
KEY_KP4,
|
||||||
|
KEY_KP5,
|
||||||
|
KEY_KP6,
|
||||||
|
KEY_KPPLUS,
|
||||||
|
KEY_KP1,
|
||||||
|
KEY_KP2,
|
||||||
|
KEY_KP3,
|
||||||
|
KEY_KP0,
|
||||||
|
KEY_KPDOT,
|
||||||
|
KEY_KPENTER,
|
||||||
|
KEY_KPSLASH,
|
||||||
|
KEY_KPJPCOMMA,
|
||||||
|
// Arrows / navigation
|
||||||
|
KEY_HOME,
|
||||||
|
KEY_UP,
|
||||||
|
KEY_PAGEUP,
|
||||||
|
KEY_LEFT,
|
||||||
|
KEY_RIGHT,
|
||||||
|
KEY_END,
|
||||||
|
KEY_DOWN,
|
||||||
|
KEY_PAGEDOWN,
|
||||||
|
KEY_INSERT,
|
||||||
|
KEY_DELETE,
|
||||||
|
KEY_RIGHTCTRL,
|
||||||
|
KEY_RIGHTALT,
|
||||||
|
// Optional Japanese / additional keys
|
||||||
|
KEY_ZENKAKUHANKAKU,
|
||||||
|
KEY_102ND,
|
||||||
|
KEY_RO,
|
||||||
|
KEY_KATAKANA,
|
||||||
|
KEY_HIRAGANA,
|
||||||
|
KEY_HENKAN,
|
||||||
|
KEY_KATAKANAHIRAGANA,
|
||||||
|
KEY_MUHENKAN,
|
||||||
|
KEY_LINEFEED,
|
||||||
|
KEY_SYSRQ,
|
||||||
|
];
|
||||||
|
|
||||||
|
for &key in all_keys.iter() {
|
||||||
|
ui_set_keybit(fd, key.try_into().unwrap())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct KeyboardDevice {
|
||||||
|
state: DeviceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device for KeyboardDevice {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"Keyboard"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> &DeviceState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_mut(&mut self) -> &mut DeviceState {
|
||||||
|
&mut self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_event_device(&self) -> Result<c_int, io::Error> {
|
||||||
|
Ok(self.state.event_device_fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(device: Option<&str>, name: &str) -> Result<Self, io::Error> {
|
||||||
|
let fd = open_uinput(device)?;
|
||||||
|
|
||||||
|
unsafe { set_standard_keyboard_keys(fd)? };
|
||||||
|
|
||||||
|
let temp_device = KeyboardDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname: String::new(),
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node: String::new(),
|
||||||
|
event_device_fd: -1,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 0)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ui_dev_create(fd).map_err(|e| {
|
||||||
|
eprintln!("ui_dev_create failed: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sysname = temp_device.get_sysname()?;
|
||||||
|
|
||||||
|
let event_device_node = fetch_device_node(&sysname)?;
|
||||||
|
let event_device_fd = unsafe {
|
||||||
|
open(
|
||||||
|
event_device_node.as_ptr() as *const i8,
|
||||||
|
libc::O_RDONLY | libc::O_NONBLOCK,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if event_device_fd < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(KeyboardDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname,
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node,
|
||||||
|
event_device_fd,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(self) {
|
||||||
|
unsafe {
|
||||||
|
ui_dev_destroy(self.state.uinput_fd).unwrap_or_else(|e| {
|
||||||
|
eprintln!("ui_dev_destroy failed: {:?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
close(self.state.uinput_fd);
|
||||||
|
close(self.state.event_device_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
vuinputd-tests/src/devices/mod.rs
Normal file
23
vuinputd-tests/src/devices/mod.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
pub mod device_base;
|
||||||
|
pub mod keyboard;
|
||||||
|
pub mod mouse;
|
||||||
|
pub mod mouse_absolute;
|
||||||
|
pub mod ps4_gamepad;
|
||||||
|
pub mod utils;
|
||||||
|
pub mod xbox_gamepad;
|
||||||
|
|
||||||
|
pub use device_base::Device;
|
||||||
|
// Keep DeviceState exported for backward compatibility
|
||||||
|
pub use device_base::DeviceState;
|
||||||
|
pub use keyboard::KeyboardDevice;
|
||||||
|
pub use mouse::MouseDevice;
|
||||||
|
pub use mouse_absolute::MouseAbsoluteDevice;
|
||||||
|
pub use ps4_gamepad::Ps4GamepadDevice;
|
||||||
|
pub use xbox_gamepad::XboxGamepadDevice;
|
||||||
|
|
||||||
|
// Re-export constants from device_base for backward compatibility
|
||||||
|
pub use device_base::{BUS_USB, EV_ABS, EV_FF, EV_KEY, EV_REL, EV_SYN, SYN_REPORT, SYS_INPUT_DIR};
|
||||||
114
vuinputd-tests/src/devices/mouse.rs
Normal file
114
vuinputd-tests/src/devices/mouse.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::devices::device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB};
|
||||||
|
use libc::{c_int, close, open};
|
||||||
|
use std::io;
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// Mouse codes
|
||||||
|
pub const BTN_LEFT: u16 = 272;
|
||||||
|
pub const BTN_RIGHT: u16 = 273;
|
||||||
|
pub const BTN_MIDDLE: u16 = 274;
|
||||||
|
pub const REL_X: u16 = 0;
|
||||||
|
pub const REL_Y: u16 = 1;
|
||||||
|
|
||||||
|
/// Setup mouse device
|
||||||
|
unsafe fn setup_mouse(fd: c_int) -> io::Result<()> {
|
||||||
|
// EV_SYN
|
||||||
|
ui_set_evbit(fd, super::EV_SYN.try_into().unwrap())?;
|
||||||
|
// EV_KEY
|
||||||
|
ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_LEFT.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_RIGHT.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_MIDDLE.try_into().unwrap())?;
|
||||||
|
// EV_REL
|
||||||
|
ui_set_evbit(fd, super::EV_REL.try_into().unwrap())?;
|
||||||
|
ui_set_relbit(fd, REL_X.try_into().unwrap())?;
|
||||||
|
ui_set_relbit(fd, REL_Y.try_into().unwrap())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MouseDevice {
|
||||||
|
state: DeviceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device for MouseDevice {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"Mouse"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> &DeviceState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_mut(&mut self) -> &mut DeviceState {
|
||||||
|
&mut self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_event_device(&self) -> Result<c_int, io::Error> {
|
||||||
|
Ok(self.state.event_device_fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(device: Option<&str>, name: &str) -> Result<Self, io::Error> {
|
||||||
|
let fd = open_uinput(device)?;
|
||||||
|
|
||||||
|
unsafe { setup_mouse(fd)? };
|
||||||
|
|
||||||
|
let temp_device = MouseDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname: String::new(),
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node: String::new(),
|
||||||
|
event_device_fd: -1,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 0)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ui_dev_create(fd).map_err(|e| {
|
||||||
|
eprintln!("ui_dev_create failed: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sysname = temp_device.get_sysname()?;
|
||||||
|
|
||||||
|
let event_device_node = fetch_device_node(&sysname)?;
|
||||||
|
let event_device_fd = unsafe {
|
||||||
|
open(
|
||||||
|
event_device_node.as_ptr() as *const i8,
|
||||||
|
libc::O_RDONLY | libc::O_NONBLOCK,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if event_device_fd < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(MouseDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname,
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node,
|
||||||
|
event_device_fd,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(self) {
|
||||||
|
unsafe {
|
||||||
|
ui_dev_destroy(self.state.uinput_fd).unwrap_or_else(|e| {
|
||||||
|
eprintln!("ui_dev_destroy failed: {:?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
close(self.state.uinput_fd);
|
||||||
|
close(self.state.event_device_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
161
vuinputd-tests/src/devices/mouse_absolute.rs
Normal file
161
vuinputd-tests/src/devices/mouse_absolute.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::devices::{
|
||||||
|
device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB},
|
||||||
|
utils::{ABS_X, ABS_Y},
|
||||||
|
};
|
||||||
|
use libc::{c_int, close, input_absinfo, open, uinput_abs_setup, INPUT_PROP_DIRECT};
|
||||||
|
use std::io;
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// Mouse codes
|
||||||
|
pub const BTN_LEFT: u16 = 272;
|
||||||
|
pub const BTN_RIGHT: u16 = 273;
|
||||||
|
pub const BTN_MIDDLE: u16 = 274;
|
||||||
|
pub const REL_X: u16 = 0;
|
||||||
|
pub const REL_Y: u16 = 1;
|
||||||
|
|
||||||
|
// non linux constants used this way in inputtino
|
||||||
|
pub const ABS_MAX_WIDTH: i32 = 19200;
|
||||||
|
pub const ABS_MAX_HEIGHT: i32 = 12000;
|
||||||
|
|
||||||
|
/// Setup absolute mouse device
|
||||||
|
unsafe fn setup_mouse_absolute(fd: c_int) -> io::Result<()> {
|
||||||
|
// EV_SYN (implicitly handled by libevdev, but required manually for uinput)
|
||||||
|
ui_set_evbit(fd, super::EV_SYN.try_into().unwrap())?;
|
||||||
|
|
||||||
|
// INPUT_PROP_DIRECT
|
||||||
|
ui_set_propbit(fd, INPUT_PROP_DIRECT.try_into().unwrap())?;
|
||||||
|
|
||||||
|
// EV_KEY
|
||||||
|
ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_LEFT.try_into().unwrap())?;
|
||||||
|
|
||||||
|
// EV_ABS
|
||||||
|
ui_set_evbit(fd, super::EV_ABS.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_X.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_Y.try_into().unwrap())?;
|
||||||
|
|
||||||
|
// Setup absolute axis parameters (min, max, fuzz, flat, resolution)
|
||||||
|
let abs_x_setup = uinput_abs_setup {
|
||||||
|
code: ABS_X,
|
||||||
|
absinfo: input_absinfo {
|
||||||
|
value: 0,
|
||||||
|
minimum: 0,
|
||||||
|
maximum: ABS_MAX_WIDTH,
|
||||||
|
fuzz: 1,
|
||||||
|
flat: 0,
|
||||||
|
resolution: 28,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ui_abs_setup(fd, &abs_x_setup).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("ui_abs_setup X failed: {:?}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let abs_y_setup = uinput_abs_setup {
|
||||||
|
code: ABS_Y,
|
||||||
|
absinfo: input_absinfo {
|
||||||
|
value: 0,
|
||||||
|
minimum: 0,
|
||||||
|
maximum: ABS_MAX_HEIGHT,
|
||||||
|
fuzz: 1,
|
||||||
|
flat: 0,
|
||||||
|
resolution: 28,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
ui_abs_setup(fd, &abs_y_setup).map_err(|e| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("ui_abs_setup Y failed: {:?}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MouseAbsoluteDevice {
|
||||||
|
state: DeviceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device for MouseAbsoluteDevice {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"Mouse Absolute"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> &DeviceState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_mut(&mut self) -> &mut DeviceState {
|
||||||
|
&mut self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_event_device(&self) -> Result<c_int, io::Error> {
|
||||||
|
Ok(self.state.event_device_fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(device: Option<&str>, name: &str) -> Result<Self, io::Error> {
|
||||||
|
let fd = open_uinput(device)?;
|
||||||
|
|
||||||
|
unsafe { setup_mouse_absolute(fd)? };
|
||||||
|
|
||||||
|
let temp_device = MouseAbsoluteDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname: String::new(),
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node: String::new(),
|
||||||
|
event_device_fd: -1,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 0)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ui_dev_create(fd).map_err(|e| {
|
||||||
|
eprintln!("ui_dev_create failed: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sysname = temp_device.get_sysname()?;
|
||||||
|
|
||||||
|
let event_device_node = fetch_device_node(&sysname)?;
|
||||||
|
let event_device_fd = unsafe {
|
||||||
|
open(
|
||||||
|
event_device_node.as_ptr() as *const i8,
|
||||||
|
libc::O_RDONLY | libc::O_NONBLOCK,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if event_device_fd < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(MouseAbsoluteDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname,
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node,
|
||||||
|
event_device_fd,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(self) {
|
||||||
|
unsafe {
|
||||||
|
ui_dev_destroy(self.state.uinput_fd).unwrap_or_else(|e| {
|
||||||
|
eprintln!("ui_dev_destroy failed: {:?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
close(self.state.uinput_fd);
|
||||||
|
close(self.state.event_device_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
180
vuinputd-tests/src/devices/ps4_gamepad.rs
Normal file
180
vuinputd-tests/src/devices/ps4_gamepad.rs
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
// this file is ai genrated and contains many mistake that need to be fixed manually
|
||||||
|
|
||||||
|
use crate::devices::device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB};
|
||||||
|
use libc::{c_int, close, open};
|
||||||
|
use std::io;
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// PS4 Gamepad codes
|
||||||
|
const BTN_SOUTH: u16 = 304; // Cross
|
||||||
|
const BTN_EAST: u16 = 305; // Circle
|
||||||
|
const BTN_NORTH: u16 = 306; // Square
|
||||||
|
const BTN_WEST: u16 = 307; // Triangle
|
||||||
|
const BTN_TOP: u16 = 310; // L1
|
||||||
|
const BTN_TOP2: u16 = 311; // R1
|
||||||
|
const BTN_BASE: u16 = 312; // Share
|
||||||
|
const BTN_BASE2: u16 = 313; // Options
|
||||||
|
const BTN_BASE3: u16 = 314; // L3
|
||||||
|
const BTN_BASE23: u16 = 315; // R3
|
||||||
|
const BTN_TL: u16 = 316; // L2
|
||||||
|
const BTN_TR: u16 = 317; // R2
|
||||||
|
const BTN_SELECT: u16 = 318;
|
||||||
|
const BTN_START: u16 = 319;
|
||||||
|
const BTN_THUMBL: u16 = 320;
|
||||||
|
const BTN_THUMBR: u16 = 321;
|
||||||
|
const BTN_TOUCH: u16 = 322;
|
||||||
|
const BTN_TR2: u16 = 323;
|
||||||
|
const BTN_DPAD_UP: u16 = 325;
|
||||||
|
const BTN_DPAD_DOWN: u16 = 326;
|
||||||
|
const BTN_DPAD_LEFT: u16 = 327;
|
||||||
|
const BTN_DPAD_RIGHT: u16 = 328;
|
||||||
|
|
||||||
|
const ABS_X: u16 = 0;
|
||||||
|
const ABS_Y: u16 = 1;
|
||||||
|
const ABS_Z: u16 = 2;
|
||||||
|
const ABS_RX: u16 = 3;
|
||||||
|
const ABS_RY: u16 = 4;
|
||||||
|
const ABS_RZ: u16 = 5;
|
||||||
|
const ABS_THROTTLE: u16 = 6;
|
||||||
|
const ABS_RUDDER: u16 = 7;
|
||||||
|
const ABS_PRESSURE: u16 = 24;
|
||||||
|
const ABS_DISTANCE: u16 = 32;
|
||||||
|
const ABS_MT_POSITION_X: u16 = 47;
|
||||||
|
const ABS_MT_POSITION_Y: u16 = 48;
|
||||||
|
const ABS_MT_TRACKING_ID: u16 = 57;
|
||||||
|
const ABS_MT_PRESSURE: u16 = 47;
|
||||||
|
const ABS_MT_TOOL_TYPE: u16 = 55;
|
||||||
|
const ABS_MT_WIDTH: u16 = 56;
|
||||||
|
|
||||||
|
// Xbox Gamepad codes
|
||||||
|
const BTN_A: u16 = 304; // A
|
||||||
|
const BTN_B: u16 = 305; // B
|
||||||
|
const BTN_X: u16 = 306; // X
|
||||||
|
const BTN_Y: u16 = 307; // Y
|
||||||
|
const BTN_TL2: u16 = 319; // LT
|
||||||
|
|
||||||
|
pub struct Ps4GamepadDevice {
|
||||||
|
state: DeviceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup PS4 gamepad device
|
||||||
|
unsafe fn setup_ps4_gamepad(fd: c_int) -> io::Result<()> {
|
||||||
|
// EV_SYN
|
||||||
|
ui_set_evbit(fd, super::EV_SYN.try_into().unwrap())?;
|
||||||
|
// EV_KEY
|
||||||
|
ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_SOUTH.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_EAST.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_NORTH.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_WEST.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TOP.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TOP2.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_BASE.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_BASE2.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_BASE3.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_BASE23.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TL.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TR.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_SELECT.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_START.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_THUMBL.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_THUMBR.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TOUCH.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TR2.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_DPAD_UP.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_DPAD_DOWN.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_DPAD_LEFT.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_DPAD_RIGHT.try_into().unwrap())?;
|
||||||
|
// EV_ABS
|
||||||
|
ui_set_evbit(fd, super::EV_ABS.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_X.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_Y.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_RX.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_RY.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_PRESSURE.try_into().unwrap())?;
|
||||||
|
ui_set_absbit(fd, ABS_DISTANCE.try_into().unwrap())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device for Ps4GamepadDevice {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"PS4 Gamepad"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> &DeviceState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_mut(&mut self) -> &mut DeviceState {
|
||||||
|
&mut self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_event_device(&self) -> Result<c_int, io::Error> {
|
||||||
|
Ok(self.state.event_device_fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(device: Option<&str>, name: &str) -> Result<Self, io::Error> {
|
||||||
|
let fd = open_uinput(device)?;
|
||||||
|
|
||||||
|
unsafe { setup_ps4_gamepad(fd)? };
|
||||||
|
|
||||||
|
let temp_device = Ps4GamepadDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname: String::new(),
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node: String::new(),
|
||||||
|
event_device_fd: -1,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 10)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ui_dev_create(fd).map_err(|e| {
|
||||||
|
eprintln!("ui_dev_create failed: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sysname = temp_device.get_sysname()?;
|
||||||
|
|
||||||
|
let event_device_node = fetch_device_node(&sysname)?;
|
||||||
|
let event_device_fd = unsafe {
|
||||||
|
open(
|
||||||
|
event_device_node.as_ptr() as *const i8,
|
||||||
|
libc::O_RDONLY | libc::O_NONBLOCK,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if event_device_fd < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Ps4GamepadDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname,
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node,
|
||||||
|
event_device_fd,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(self) {
|
||||||
|
unsafe {
|
||||||
|
ui_dev_destroy(self.state.uinput_fd).unwrap_or_else(|e| {
|
||||||
|
eprintln!("ui_dev_destroy failed: {:?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
close(self.state.uinput_fd);
|
||||||
|
close(self.state.event_device_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
vuinputd-tests/src/devices/utils.rs
Normal file
19
vuinputd-tests/src/devices/utils.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
pub use crate::devices::device_base::*;
|
||||||
|
use crate::test_log::{LoggedInputEvent, TestLog};
|
||||||
|
use libc::{c_int, close, open, write, O_NONBLOCK, O_WRONLY};
|
||||||
|
use libc::{input_event, timespec, uinput_setup, CLOCK_MONOTONIC};
|
||||||
|
use std::ffi::{CStr, CString};
|
||||||
|
use std::fs::{self, File, OpenOptions};
|
||||||
|
use std::io::{self, ErrorKind};
|
||||||
|
use std::mem::{self, size_of, zeroed};
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
use std::os::raw::{c_char, c_void};
|
||||||
|
use std::ptr;
|
||||||
|
pub use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// Re-export constants from device_base for backward compatibility
|
||||||
|
pub use crate::devices::device_base::{BUS_USB, EV_ABS, EV_FF, EV_KEY, EV_REL, EV_SYN, SYN_REPORT};
|
||||||
365
vuinputd-tests/src/devices/xbox_gamepad.rs
Normal file
365
vuinputd-tests/src/devices/xbox_gamepad.rs
Normal file
|
|
@ -0,0 +1,365 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::devices::{
|
||||||
|
device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB},
|
||||||
|
utils::{ABS_HAT0X, ABS_HAT0Y, ABS_RX, ABS_RY, ABS_RZ, ABS_X, ABS_Y, ABS_Z},
|
||||||
|
};
|
||||||
|
use libc::{c_int, close, ff_effect, input_event, open, uinput_ff_upload, EAGAIN};
|
||||||
|
use nix::{
|
||||||
|
ioctl_write_int, ioctl_write_ptr,
|
||||||
|
poll::{poll, PollFd, PollFlags, PollTimeout},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
os::fd::BorrowedFd,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
thread,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// Xbox Gamepad codes
|
||||||
|
// https://github.com/torvalds/linux/blob/master/Documentation/input/gamepad.rst
|
||||||
|
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h// Keys and Buttons
|
||||||
|
// https://github.com/torvalds/linux/blob/master/drivers/input/joystick/xpad.c
|
||||||
|
pub const BTN_SOUTH: u16 = 0x130;
|
||||||
|
pub const BTN_EAST: u16 = 0x131;
|
||||||
|
pub const BTN_NORTH: u16 = 0x133;
|
||||||
|
pub const BTN_WEST: u16 = 0x134;
|
||||||
|
pub const BTN_TL: u16 = 0x136;
|
||||||
|
pub const BTN_TR: u16 = 0x137;
|
||||||
|
pub const BTN_SELECT: u16 = 0x13a;
|
||||||
|
pub const BTN_START: u16 = 0x13b;
|
||||||
|
pub const BTN_MODE: u16 = 0x13c;
|
||||||
|
pub const BTN_THUMBL: u16 = 0x13d;
|
||||||
|
pub const BTN_THUMBR: u16 = 0x13e;
|
||||||
|
|
||||||
|
// Force Feedback
|
||||||
|
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input.h
|
||||||
|
pub const FF_RUMBLE: u16 = 0x50;
|
||||||
|
pub const FF_PERIODIC: u16 = 0x51;
|
||||||
|
pub const FF_CONSTANT: u16 = 0x52;
|
||||||
|
pub const FF_RAMP: u16 = 0x57;
|
||||||
|
pub const FF_SINE: u16 = 0x5a;
|
||||||
|
pub const FF_GAIN: u16 = 0x60;
|
||||||
|
|
||||||
|
const EV_UINPUT: u16 = 0x0101;
|
||||||
|
const UI_FF_UPLOAD: u16 = 1;
|
||||||
|
const UI_FF_ERASE: u16 = 2;
|
||||||
|
|
||||||
|
// EVIOCSFF ioctl command for Force Feedback Upload
|
||||||
|
ioctl_write_ptr!(eviocsff, b'E', 0x80, ff_effect);
|
||||||
|
// EVIOCRMFF ioctl command for Force Feedback erase
|
||||||
|
ioctl_write_int!(eviocrmff, b'E', 0x81);
|
||||||
|
|
||||||
|
/// Setup Xbox gamepad device
|
||||||
|
/// https://github.com/LizardByte/Sunshine/blob/master/src/platform/linux/input/inputtino_gamepad.cpp
|
||||||
|
/// https://github.com/games-on-whales/inputtino/blob/stable/src/uinput/joypad_xbox.cpp
|
||||||
|
unsafe fn setup_xbox_gamepad(fd: c_int) -> io::Result<()> {
|
||||||
|
// EV_SYN
|
||||||
|
ui_set_evbit(fd, super::EV_SYN.try_into().unwrap())?;
|
||||||
|
|
||||||
|
// EV_KEY
|
||||||
|
ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_WEST.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_EAST.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_NORTH.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_SOUTH.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_THUMBL.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_THUMBR.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TR.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_TL.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_SELECT.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_MODE.try_into().unwrap())?;
|
||||||
|
ui_set_keybit(fd, BTN_START.try_into().unwrap())?;
|
||||||
|
|
||||||
|
// EV_ABS
|
||||||
|
ui_set_evbit(fd, super::EV_ABS.try_into().unwrap())?;
|
||||||
|
// EV_ABS dpad
|
||||||
|
let abs_info_dpad = libc::input_absinfo {
|
||||||
|
value: 0,
|
||||||
|
minimum: -1,
|
||||||
|
maximum: 1,
|
||||||
|
fuzz: 0,
|
||||||
|
flat: 0,
|
||||||
|
resolution: 0,
|
||||||
|
};
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_HAT0Y,
|
||||||
|
absinfo: abs_info_dpad.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_HAT0X,
|
||||||
|
absinfo: abs_info_dpad.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
// EV_ABS stick
|
||||||
|
let abs_info_stick = libc::input_absinfo {
|
||||||
|
value: 0,
|
||||||
|
minimum: -32768,
|
||||||
|
maximum: 32767,
|
||||||
|
fuzz: 16,
|
||||||
|
flat: 128,
|
||||||
|
resolution: 0,
|
||||||
|
};
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_X,
|
||||||
|
absinfo: abs_info_stick.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_RX,
|
||||||
|
absinfo: abs_info_stick.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_Y,
|
||||||
|
absinfo: abs_info_stick.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_RY,
|
||||||
|
absinfo: abs_info_stick.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
// EV_ABS trigger
|
||||||
|
let abs_info_trigger = libc::input_absinfo {
|
||||||
|
value: 0,
|
||||||
|
minimum: 0,
|
||||||
|
maximum: 255,
|
||||||
|
fuzz: 0,
|
||||||
|
flat: 0,
|
||||||
|
resolution: 0,
|
||||||
|
};
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_Z,
|
||||||
|
absinfo: abs_info_trigger.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
ui_abs_setup(
|
||||||
|
fd,
|
||||||
|
&libc::uinput_abs_setup {
|
||||||
|
code: ABS_RZ,
|
||||||
|
absinfo: abs_info_trigger.clone(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// EV_FF
|
||||||
|
ui_set_evbit(fd, super::EV_FF.try_into().unwrap())?;
|
||||||
|
ui_set_ffbit(fd, FF_RUMBLE.try_into().unwrap())?;
|
||||||
|
ui_set_ffbit(fd, FF_CONSTANT.try_into().unwrap())?;
|
||||||
|
ui_set_ffbit(fd, FF_PERIODIC.try_into().unwrap())?;
|
||||||
|
ui_set_ffbit(fd, FF_SINE.try_into().unwrap())?;
|
||||||
|
ui_set_ffbit(fd, FF_RAMP.try_into().unwrap())?;
|
||||||
|
ui_set_ffbit(fd, FF_GAIN.try_into().unwrap())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the opaque `u` array for a rumble effect on 64-bit systems
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
pub fn create_rumble_array(strong_magnitude: u16, weak_magnitude: u16) -> [u64; 4] {
|
||||||
|
let mut u = [0u64; 4];
|
||||||
|
|
||||||
|
// Create an 8-byte array representing the memory of a u64
|
||||||
|
let mut bytes = [0u8; 8];
|
||||||
|
|
||||||
|
// Place the strong magnitude at offset 0 and weak at offset 2
|
||||||
|
// using native endianness to match exactly what the kernel expects.
|
||||||
|
bytes[0..2].copy_from_slice(&strong_magnitude.to_ne_bytes());
|
||||||
|
bytes[2..4].copy_from_slice(&weak_magnitude.to_ne_bytes());
|
||||||
|
|
||||||
|
// Convert those bytes back into a native u64 and place it in the union array
|
||||||
|
u[0] = u64::from_ne_bytes(bytes);
|
||||||
|
|
||||||
|
u
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Upload a force feedback effect to the device
|
||||||
|
/// Returns the effect id on success
|
||||||
|
pub fn upload_effect(fd: c_int, effect: *mut ff_effect) -> io::Result<i16> {
|
||||||
|
unsafe {
|
||||||
|
eviocsff(fd, effect).unwrap();
|
||||||
|
}
|
||||||
|
// Effect id is saved as effect.id
|
||||||
|
let id = unsafe { (*effect).id };
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct XboxGamepadDevice {
|
||||||
|
state: DeviceState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device for XboxGamepadDevice {
|
||||||
|
fn name() -> &'static str {
|
||||||
|
"Xbox Gamepad"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> &DeviceState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_mut(&mut self) -> &mut DeviceState {
|
||||||
|
&mut self.state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_event_device(&self) -> Result<c_int, io::Error> {
|
||||||
|
Ok(self.state.event_device_fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create(device: Option<&str>, name: &str) -> Result<Self, io::Error> {
|
||||||
|
let fd = open_uinput(device)?;
|
||||||
|
|
||||||
|
unsafe { setup_xbox_gamepad(fd)? };
|
||||||
|
|
||||||
|
let temp_device = XboxGamepadDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname: String::new(),
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node: String::new(),
|
||||||
|
event_device_fd: -1,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 10)?;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
ui_dev_create(fd).map_err(|e| {
|
||||||
|
eprintln!("ui_dev_create failed: {:?}", e);
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sysname = temp_device.get_sysname()?;
|
||||||
|
|
||||||
|
let event_device_node = fetch_device_node(&sysname)?;
|
||||||
|
let event_device_fd = unsafe {
|
||||||
|
open(
|
||||||
|
event_device_node.as_ptr() as *const i8,
|
||||||
|
libc::O_RDWR | libc::O_NONBLOCK,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if event_device_fd < 0 {
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(XboxGamepadDevice {
|
||||||
|
state: DeviceState {
|
||||||
|
uinput_fd: fd,
|
||||||
|
sysname,
|
||||||
|
device_name: name.to_string(),
|
||||||
|
event_device_node,
|
||||||
|
event_device_fd,
|
||||||
|
events: Vec::new(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy(self) {
|
||||||
|
unsafe {
|
||||||
|
ui_dev_destroy(self.state.uinput_fd).unwrap_or_else(|e| {
|
||||||
|
eprintln!("ui_dev_destroy failed: {:?}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
});
|
||||||
|
close(self.state.uinput_fd);
|
||||||
|
close(self.state.event_device_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl XboxGamepadDevice {
|
||||||
|
pub fn read_process_ff_event_from_uinput(&self, shutdown: Arc<AtomicBool>, use_poll: bool) {
|
||||||
|
// Copy the i32 file descriptor so we can move it into the thread safely
|
||||||
|
let fd = self.state().uinput_fd;
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
// Buffer for the raw bytes
|
||||||
|
let mut buffer = [0u8; 256];
|
||||||
|
|
||||||
|
let mut pollfds = [PollFd::new(
|
||||||
|
unsafe { BorrowedFd::borrow_raw(fd) },
|
||||||
|
PollFlags::POLLIN,
|
||||||
|
)];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if shutdown.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
println!("Loop in read_process_ff_event_from_uinput");
|
||||||
|
|
||||||
|
if use_poll {
|
||||||
|
let _ = poll(&mut pollfds, 500u16);
|
||||||
|
} else {
|
||||||
|
thread::sleep(Duration::from_millis(200));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calling C functions always requires an unsafe block
|
||||||
|
let result = unsafe {
|
||||||
|
libc::read(fd, buffer.as_mut_ptr() as *mut libc::c_void, buffer.len())
|
||||||
|
};
|
||||||
|
if result < 0 {
|
||||||
|
// result < 0 means an error occurred. We use std::io::Error::last_os_error()
|
||||||
|
// to get the correct OS error message based on the C `errno`.
|
||||||
|
let error = std::io::Error::last_os_error();
|
||||||
|
match error.kind() {
|
||||||
|
io::ErrorKind::WouldBlock => {
|
||||||
|
eprintln!("a read would block. waiting for the next real event");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
eprintln!("Error reading in thread: {}", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if result == 0 {
|
||||||
|
// 0 bytes usually means End-Of-File (EOF) or that the device was closed
|
||||||
|
println!("0 bytes (EOF) - Terminating thread");
|
||||||
|
return;
|
||||||
|
} else if result == 24 {
|
||||||
|
println!("read_process_ff_event_from_uinput: processing input event (read)");
|
||||||
|
let input_event = buffer.as_ptr() as *const libc::input_event;
|
||||||
|
let input_event = unsafe { *input_event };
|
||||||
|
if input_event.type_ == EV_UINPUT && input_event.code == UI_FF_UPLOAD {
|
||||||
|
let mut upload: uinput_ff_upload = unsafe { std::mem::zeroed() };
|
||||||
|
upload.request_id = input_event.value.try_into().unwrap();
|
||||||
|
unsafe {
|
||||||
|
let ptr = &mut upload as *mut uinput_ff_upload;
|
||||||
|
ui_begin_ff_upload(fd, ptr).unwrap();
|
||||||
|
println!("effect type: {}", upload.effect.type_);
|
||||||
|
ui_end_ff_upload(fd, ptr).unwrap();
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
"event: {} {} {}",
|
||||||
|
input_event.type_, input_event.code, input_event.value
|
||||||
|
);
|
||||||
|
//crate::devices::utils::emit(fd, input_event.type_,input_event.code,input_event.value).unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println!("Read {} bytes", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
64
vuinputd-tests/src/ipc.rs
Normal file
64
vuinputd-tests/src/ipc.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
// TODO: Use https://varlink.org/ which also supports bridges over ssh, which is nice
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
os::{
|
||||||
|
fd::{FromRawFd, RawFd},
|
||||||
|
unix::net::UnixDatagram,
|
||||||
|
},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// IPC handle kept by the parent.
|
||||||
|
pub struct SandboxIpc {
|
||||||
|
pub sock: UnixDatagram,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SandboxIpc {
|
||||||
|
pub fn recv(&self, read_timeout: Option<Duration>) -> io::Result<Vec<u8>> {
|
||||||
|
let mut buf = vec![0u8; 4096];
|
||||||
|
self.sock.set_read_timeout(read_timeout)?;
|
||||||
|
let n = self.sock.recv(&mut buf)?;
|
||||||
|
buf.truncate(n);
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, data: &[u8]) -> io::Result<()> {
|
||||||
|
self.sock.send(data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// IPC handle inside the container.
|
||||||
|
pub struct SandboxChildIpc {
|
||||||
|
sock: UnixDatagram,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SandboxChildIpc {
|
||||||
|
/// FD number is fixed and known.
|
||||||
|
pub const FD: RawFd = 3;
|
||||||
|
|
||||||
|
/// # Safety
|
||||||
|
/// Must only be called once in the child.
|
||||||
|
pub unsafe fn from_fd() -> Self {
|
||||||
|
let sock = UnixDatagram::from_raw_fd(Self::FD);
|
||||||
|
Self { sock }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, data: &[u8]) -> io::Result<()> {
|
||||||
|
self.sock.send(data)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recv(&self, read_timeout: Option<Duration>) -> io::Result<Vec<u8>> {
|
||||||
|
let mut buf = vec![0u8; 4096];
|
||||||
|
self.sock.set_read_timeout(read_timeout)?;
|
||||||
|
let n = self.sock.recv(&mut buf)?;
|
||||||
|
buf.truncate(n);
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,5 +3,9 @@
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
pub mod bwrap;
|
pub mod bwrap;
|
||||||
|
pub mod devices;
|
||||||
|
pub mod ipc;
|
||||||
|
pub mod podman;
|
||||||
pub mod run_vuinputd;
|
pub mod run_vuinputd;
|
||||||
|
pub mod scenarios;
|
||||||
pub mod test_log;
|
pub mod test_log;
|
||||||
|
|
|
||||||
209
vuinputd-tests/src/podman.rs
Normal file
209
vuinputd-tests/src/podman.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use nix::errno::Errno;
|
||||||
|
use nix::sys::socket::{AddressFamily, SockFlag, SockType};
|
||||||
|
use nix::unistd::close;
|
||||||
|
use std::io;
|
||||||
|
use std::os::fd::{FromRawFd, IntoRawFd, OwnedFd};
|
||||||
|
use std::os::unix::net::UnixDatagram;
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
use std::process::{Command, Output};
|
||||||
|
|
||||||
|
use crate::ipc::{SandboxChildIpc, SandboxIpc};
|
||||||
|
|
||||||
|
/// Check if podman is available.
|
||||||
|
pub fn podman_available() -> bool {
|
||||||
|
Command::new("podman")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for podman run invocations.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct PodmanBuilder {
|
||||||
|
args: Vec<String>,
|
||||||
|
ipc_child_fd: Option<OwnedFd>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PodmanBuilder {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `podman run`
|
||||||
|
pub fn run_cmd(mut self) -> Self {
|
||||||
|
self.args.push("run".into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rm(mut self) -> Self {
|
||||||
|
self.args.push("--rm".into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detach(mut self) -> Self {
|
||||||
|
self.args.push("--detach".into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(mut self, name: &str) -> Self {
|
||||||
|
self.args.push("--name".into());
|
||||||
|
self.args.push(name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tty(mut self) -> Self {
|
||||||
|
self.args.push("--tty".into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn interactive(mut self) -> Self {
|
||||||
|
self.args.push("--interactive".into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn device(mut self, spec: &str) -> Self {
|
||||||
|
self.args.push("--device".into());
|
||||||
|
self.args.push(spec.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allow_input_devices(mut self) -> Self {
|
||||||
|
self.args.push("--device-cgroup-rule=\"c 13:* rwm\"".into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn volume(mut self, spec: &str) -> Self {
|
||||||
|
self.args.push("-v".into());
|
||||||
|
self.args.push(spec.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn publish(mut self, spec: &str) -> Self {
|
||||||
|
self.args.push("--publish".into());
|
||||||
|
self.args.push(spec.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn env(mut self, key: &str, value: &str) -> Self {
|
||||||
|
self.args.push("-e".into());
|
||||||
|
self.args.push(format!("{key}={value}"));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn group_add(mut self, group: u32) -> Self {
|
||||||
|
self.args.push("--group-add".into());
|
||||||
|
self.args.push(group.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn security_opt(mut self, opt: &str) -> Self {
|
||||||
|
self.args.push("--security-opt".into());
|
||||||
|
self.args.push(opt.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn userns(mut self, mode: &str) -> Self {
|
||||||
|
self.args.push("--userns".into());
|
||||||
|
self.args.push(mode.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn uidmap(mut self, uidmap: &str) -> Self {
|
||||||
|
self.args.push("--uidmap".into());
|
||||||
|
self.args.push(uidmap.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable bidirectional IPC using a Unix seqpacket socketpair.
|
||||||
|
pub fn with_ipc(mut self) -> io::Result<(Self, SandboxIpc)> {
|
||||||
|
let (parent, child) = nix::sys::socket::socketpair(
|
||||||
|
AddressFamily::Unix,
|
||||||
|
SockType::SeqPacket,
|
||||||
|
None,
|
||||||
|
SockFlag::empty(),
|
||||||
|
)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||||
|
|
||||||
|
// Parent side
|
||||||
|
let parent_sock = unsafe { UnixDatagram::from_raw_fd(parent.into_raw_fd()) };
|
||||||
|
|
||||||
|
// Child side must become FD 3 inside container
|
||||||
|
self.ipc_child_fd = Some(child);
|
||||||
|
|
||||||
|
self.args.push("--preserve-fds=1".into());
|
||||||
|
|
||||||
|
Ok((self, SandboxIpc { sock: parent_sock }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Final image reference
|
||||||
|
pub fn image(mut self, image: &str) -> Self {
|
||||||
|
self.args.push(image.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Optional command override inside the container
|
||||||
|
pub fn command(mut self, cmd: &[&str]) -> Self {
|
||||||
|
self.args.extend(cmd.iter().map(|s| s.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(mut self) -> io::Result<Output> {
|
||||||
|
println!("Arguments for podman: {:?}", &self.args);
|
||||||
|
|
||||||
|
let mut cmd = Command::new("podman");
|
||||||
|
|
||||||
|
if let Some(fd) = self.ipc_child_fd.take() {
|
||||||
|
// give up ownership of ipc_child_fd in host process.
|
||||||
|
let fd = fd.into_raw_fd();
|
||||||
|
|
||||||
|
// Move child FD to 3. Note that the FD 3 needs to be linked at the
|
||||||
|
// beginning of the child program.
|
||||||
|
unsafe {
|
||||||
|
cmd.pre_exec(move || {
|
||||||
|
let res = libc::dup2(fd, SandboxChildIpc::FD);
|
||||||
|
Errno::result(res)
|
||||||
|
.map(drop)
|
||||||
|
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||||
|
close(fd).ok();
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.args(&self.args).output()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "requires-podman")]
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn podman_builder_smoke() {
|
||||||
|
if !podman_available() {
|
||||||
|
panic!("podman not available");
|
||||||
|
}
|
||||||
|
|
||||||
|
let out = PodmanBuilder::new()
|
||||||
|
.run_cmd()
|
||||||
|
.rm()
|
||||||
|
//.detach()
|
||||||
|
.name(&format!("vuinputd-podman-tests"))
|
||||||
|
.image("localhost/vuinputd-tests:latest")
|
||||||
|
.command(&["/test-ok"])
|
||||||
|
.run()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
os::unix::process::CommandExt,
|
os::unix::process::CommandExt,
|
||||||
process::{Child, Command},
|
process::{Child, Command},
|
||||||
sync::OnceLock,
|
sync::Mutex,
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
@ -14,33 +14,37 @@ use nix::sys::signal::{self, Signal};
|
||||||
use nix::unistd::Pid;
|
use nix::unistd::Pid;
|
||||||
|
|
||||||
/// Global singleton
|
/// Global singleton
|
||||||
static VUINPUTD: OnceLock<VuinputdGuard> = OnceLock::new();
|
static VUINPUTD_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
pub fn ensure_vuinputd_running() {
|
pub fn ensure_vuinputd_running(args: &[&str]) -> VuinputdGuard {
|
||||||
VUINPUTD.get_or_init(|| VuinputdGuard::start());
|
VuinputdGuard::start(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VuinputdGuard {
|
pub struct VuinputdGuard {
|
||||||
child: Child,
|
child: Child,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VuinputdGuard {
|
impl VuinputdGuard {
|
||||||
fn start() -> Self {
|
fn start(args: &[&str]) -> Self {
|
||||||
|
println!("Acquiring lock to ensure only one vuinputd test instance is running");
|
||||||
|
let _mutex = VUINPUTD_LOCK.lock().unwrap();
|
||||||
println!("Executing vuinputd located via cargo run");
|
println!("Executing vuinputd located via cargo run");
|
||||||
|
let mut concat_args = vec![
|
||||||
|
"run",
|
||||||
|
"-p",
|
||||||
|
"vuinputd",
|
||||||
|
"--",
|
||||||
|
"--major",
|
||||||
|
"120",
|
||||||
|
"--minor",
|
||||||
|
"414796",
|
||||||
|
"--devname",
|
||||||
|
"vuinput-test",
|
||||||
|
];
|
||||||
|
concat_args.extend(args);
|
||||||
let child = unsafe {
|
let child = unsafe {
|
||||||
Command::new("cargo")
|
Command::new("cargo")
|
||||||
.args([
|
.args(concat_args)
|
||||||
"run",
|
|
||||||
"-p",
|
|
||||||
"vuinputd",
|
|
||||||
"--",
|
|
||||||
"--major",
|
|
||||||
"120",
|
|
||||||
"--minor",
|
|
||||||
"414796",
|
|
||||||
"--devname",
|
|
||||||
"vuinputd-test",
|
|
||||||
])
|
|
||||||
.pre_exec(|| {
|
.pre_exec(|| {
|
||||||
// Last resort, if the parent just is killed.
|
// Last resort, if the parent just is killed.
|
||||||
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
|
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
|
||||||
|
|
@ -67,6 +71,7 @@ impl Drop for VuinputdGuard {
|
||||||
// Wait a bit
|
// Wait a bit
|
||||||
for _ in 0..10 {
|
for _ in 0..10 {
|
||||||
if let Ok(Some(_)) = self.child.try_wait() {
|
if let Ok(Some(_)) = self.child.try_wait() {
|
||||||
|
println!("vuinputd for tests shutdown gracefully");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(100));
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
|
@ -75,5 +80,6 @@ impl Drop for VuinputdGuard {
|
||||||
// Still alive → SIGKILL
|
// Still alive → SIGKILL
|
||||||
let _ = signal::kill(pid, Signal::SIGKILL);
|
let _ = signal::kill(pid, Signal::SIGKILL);
|
||||||
let _ = self.child.wait();
|
let _ = self.child.wait();
|
||||||
|
println!("vuinputd for tests killed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
vuinputd-tests/src/scenarios/basic_keyboard.rs
Normal file
40
vuinputd-tests/src/scenarios/basic_keyboard.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::devices::keyboard::KeyboardDevice;
|
||||||
|
use crate::devices::{Device, EV_KEY};
|
||||||
|
use crate::scenarios::ScenarioArgs;
|
||||||
|
use crate::test_log::TestLog;
|
||||||
|
|
||||||
|
const KEY_SPACE: u16 = 57;
|
||||||
|
|
||||||
|
pub struct BasicKeyboard;
|
||||||
|
|
||||||
|
impl BasicKeyboard {
|
||||||
|
pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> {
|
||||||
|
let device = args
|
||||||
|
.dev_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "/dev/uinput".to_string());
|
||||||
|
let mut keyboard = KeyboardDevice::create(Some(&device), "Example Keyboard")?;
|
||||||
|
eprintln!("sysname: {}", keyboard.sysname());
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
|
let _ev1 = keyboard.emit_read_and_log(EV_KEY, KEY_SPACE, 1)?;
|
||||||
|
let _ev2 = keyboard.emit_read_and_log(EV_KEY, KEY_SPACE, 0)?;
|
||||||
|
|
||||||
|
let eventlog = TestLog {
|
||||||
|
events: keyboard.event_log().to_vec(),
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&eventlog).unwrap();
|
||||||
|
println!("Event log: {}", serialized);
|
||||||
|
|
||||||
|
KeyboardDevice::destroy(keyboard);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
40
vuinputd-tests/src/scenarios/basic_mouse.rs
Normal file
40
vuinputd-tests/src/scenarios/basic_mouse.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::devices::mouse::MouseDevice;
|
||||||
|
use crate::devices::{Device, EV_KEY};
|
||||||
|
use crate::scenarios::ScenarioArgs;
|
||||||
|
use crate::test_log::{LoggedInputEvent, TestLog};
|
||||||
|
|
||||||
|
const BTN_LEFT: u16 = 272;
|
||||||
|
|
||||||
|
pub struct BasicMouse;
|
||||||
|
|
||||||
|
impl BasicMouse {
|
||||||
|
pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> {
|
||||||
|
let device = args
|
||||||
|
.dev_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "/dev/uinput".to_string());
|
||||||
|
|
||||||
|
let mut mouse = MouseDevice::create(Some(&device), "Example Mouse")?;
|
||||||
|
eprintln!("sysname: {}", mouse.sysname());
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
|
let _ev1 = mouse.emit_read_and_log(EV_KEY, BTN_LEFT, 1)?;
|
||||||
|
let _ev2 = mouse.emit_read_and_log(EV_KEY, BTN_LEFT, 0)?;
|
||||||
|
|
||||||
|
let eventlog = TestLog {
|
||||||
|
events: mouse.event_log().to_vec(),
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&eventlog).unwrap();
|
||||||
|
println!("Event log: {}", serialized);
|
||||||
|
|
||||||
|
MouseDevice::destroy(mouse);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
39
vuinputd-tests/src/scenarios/basic_mouse_absolute.rs
Normal file
39
vuinputd-tests/src/scenarios/basic_mouse_absolute.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::devices::{Device, MouseAbsoluteDevice, EV_KEY};
|
||||||
|
use crate::scenarios::ScenarioArgs;
|
||||||
|
use crate::test_log::{LoggedInputEvent, TestLog};
|
||||||
|
|
||||||
|
const BTN_LEFT: u16 = 272;
|
||||||
|
|
||||||
|
pub struct BasicMouseAbsolute;
|
||||||
|
|
||||||
|
impl BasicMouseAbsolute {
|
||||||
|
pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> {
|
||||||
|
let device = args
|
||||||
|
.dev_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "/dev/uinput".to_string());
|
||||||
|
|
||||||
|
let mut mouse = MouseAbsoluteDevice::create(Some(&device), "Example Mouse (absolute)")?;
|
||||||
|
eprintln!("sysname: {}", mouse.sysname());
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
|
let _ev1 = mouse.emit_read_and_log(EV_KEY, BTN_LEFT, 1)?;
|
||||||
|
let _ev2 = mouse.emit_read_and_log(EV_KEY, BTN_LEFT, 0)?;
|
||||||
|
|
||||||
|
let eventlog = TestLog {
|
||||||
|
events: mouse.event_log().to_vec(),
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&eventlog).unwrap();
|
||||||
|
println!("Event log: {}", serialized);
|
||||||
|
|
||||||
|
MouseAbsoluteDevice::destroy(mouse);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
40
vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs
Normal file
40
vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::devices::ps4_gamepad::Ps4GamepadDevice;
|
||||||
|
use crate::devices::{Device, EV_KEY};
|
||||||
|
use crate::scenarios::ScenarioArgs;
|
||||||
|
use crate::test_log::{LoggedInputEvent, TestLog};
|
||||||
|
|
||||||
|
const BTN_SOUTH: u16 = 304;
|
||||||
|
|
||||||
|
pub struct BasicPs4Gamepad;
|
||||||
|
|
||||||
|
impl BasicPs4Gamepad {
|
||||||
|
pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> {
|
||||||
|
let device = args
|
||||||
|
.dev_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "/dev/uinput".to_string());
|
||||||
|
|
||||||
|
let mut gamepad = Ps4GamepadDevice::create(Some(&device), "PS4 Gamepad")?;
|
||||||
|
eprintln!("sysname: {}", gamepad.sysname());
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
|
let _ev1 = gamepad.emit_read_and_log(EV_KEY, BTN_SOUTH, 1)?;
|
||||||
|
let _ev2 = gamepad.emit_read_and_log(EV_KEY, BTN_SOUTH, 0)?;
|
||||||
|
|
||||||
|
let eventlog = TestLog {
|
||||||
|
events: gamepad.event_log().to_vec(),
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&eventlog).unwrap();
|
||||||
|
println!("Event log: {}", serialized);
|
||||||
|
|
||||||
|
Ps4GamepadDevice::destroy(gamepad);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
41
vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs
Normal file
41
vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::devices::xbox_gamepad::XboxGamepadDevice;
|
||||||
|
use crate::devices::{Device, EV_KEY};
|
||||||
|
use crate::scenarios::ScenarioArgs;
|
||||||
|
use crate::test_log::{LoggedInputEvent, TestLog};
|
||||||
|
|
||||||
|
const BTN_A: u16 = 304;
|
||||||
|
|
||||||
|
pub struct BasicXboxGamepad;
|
||||||
|
|
||||||
|
impl BasicXboxGamepad {
|
||||||
|
pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> {
|
||||||
|
let device = args
|
||||||
|
.dev_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "/dev/uinput".to_string());
|
||||||
|
|
||||||
|
let mut gamepad = XboxGamepadDevice::create(Some(&device), "Xbox Gamepad")?;
|
||||||
|
eprintln!("sysname: {}", gamepad.sysname());
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
|
let _ev1 = gamepad.emit_read_and_log(EV_KEY, BTN_A, 1)?;
|
||||||
|
let _ev2 = gamepad.emit_read_and_log(EV_KEY, BTN_A, 0)?;
|
||||||
|
|
||||||
|
let eventlog = TestLog {
|
||||||
|
events: gamepad.event_log().to_vec(),
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&eventlog).unwrap();
|
||||||
|
println!("Event log: {}", serialized);
|
||||||
|
|
||||||
|
XboxGamepadDevice::destroy(gamepad);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
76
vuinputd-tests/src/scenarios/ff_xbox_gamepad.rs
Normal file
76
vuinputd-tests/src/scenarios/ff_xbox_gamepad.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::sync::atomic::AtomicBool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::devices::xbox_gamepad::{self, upload_effect, XboxGamepadDevice, FF_RUMBLE};
|
||||||
|
use crate::devices::{Device, EV_FF};
|
||||||
|
use crate::scenarios::ScenarioArgs;
|
||||||
|
use crate::test_log::{LoggedInputEvent, TestLog};
|
||||||
|
use libc::{self, ff_effect, ff_replay, ff_trigger};
|
||||||
|
|
||||||
|
//TODO: poll and erase
|
||||||
|
pub struct FfXboxGamepad;
|
||||||
|
|
||||||
|
impl FfXboxGamepad {
|
||||||
|
pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> {
|
||||||
|
let device = args
|
||||||
|
.dev_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "/dev/uinput".to_string());
|
||||||
|
|
||||||
|
let mut gamepad = XboxGamepadDevice::create(Some(&device), "Xbox Gamepad")?;
|
||||||
|
eprintln!("sysname: {}", gamepad.sysname());
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
|
eprintln!("upload a simple RUMBLE effect");
|
||||||
|
let mut effect: ff_effect = unsafe { std::mem::zeroed() };
|
||||||
|
effect.type_ = FF_RUMBLE;
|
||||||
|
effect.id = -1; // new effect
|
||||||
|
effect.direction = 0;
|
||||||
|
effect.trigger.button = 0;
|
||||||
|
effect.trigger.interval = 0;
|
||||||
|
effect.replay.length = 5000;
|
||||||
|
effect.replay.delay = 1000;
|
||||||
|
effect.u = xbox_gamepad::create_rumble_array(0x8000, 0x0);
|
||||||
|
|
||||||
|
// ensure uploaded effect gets processed
|
||||||
|
let shutdown = Arc::new(AtomicBool::new(false));
|
||||||
|
gamepad.read_process_ff_event_from_uinput(shutdown.clone(), false);
|
||||||
|
|
||||||
|
// Upload effect via ioctl
|
||||||
|
let effect_id = upload_effect(gamepad.state().event_device_fd, &mut effect)?;
|
||||||
|
|
||||||
|
eprintln!("Uploaded effect with id: {} {}", effect_id, effect.id);
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
|
// Play effect (value=1)
|
||||||
|
let _play_effect_event = gamepad.emit_to_evdev_read_from_uinput_and_log(
|
||||||
|
EV_FF,
|
||||||
|
effect_id.try_into().unwrap(),
|
||||||
|
1,
|
||||||
|
)?;
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
let _stop_effect_event = gamepad.emit_to_evdev_read_from_uinput_and_log(
|
||||||
|
EV_FF,
|
||||||
|
effect_id.try_into().unwrap(),
|
||||||
|
0,
|
||||||
|
)?;
|
||||||
|
thread::sleep(Duration::from_secs(1));
|
||||||
|
shutdown.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
|
||||||
|
let eventlog = TestLog {
|
||||||
|
events: gamepad.event_log().to_vec(),
|
||||||
|
};
|
||||||
|
let serialized = serde_json::to_string(&eventlog).unwrap();
|
||||||
|
println!("Event log: {}", serialized);
|
||||||
|
|
||||||
|
XboxGamepadDevice::destroy(gamepad);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
37
vuinputd-tests/src/scenarios/mod.rs
Normal file
37
vuinputd-tests/src/scenarios/mod.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
pub mod basic_keyboard;
|
||||||
|
pub mod basic_mouse;
|
||||||
|
pub mod basic_mouse_absolute;
|
||||||
|
pub mod basic_ps4_gamepad;
|
||||||
|
pub mod basic_xbox_gamepad;
|
||||||
|
pub mod ff_xbox_gamepad;
|
||||||
|
/*
|
||||||
|
pub mod reuse_keyboard;
|
||||||
|
pub mod reuse_xbox_gamepad;
|
||||||
|
pub mod stress_keyboard;
|
||||||
|
pub mod stress_xbox_gamepad;
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Re-exports for type checking
|
||||||
|
pub use basic_keyboard::BasicKeyboard;
|
||||||
|
pub use basic_mouse::BasicMouse;
|
||||||
|
pub use basic_mouse_absolute::BasicMouseAbsolute;
|
||||||
|
pub use basic_ps4_gamepad::BasicPs4Gamepad;
|
||||||
|
pub use basic_xbox_gamepad::BasicXboxGamepad;
|
||||||
|
pub use ff_xbox_gamepad::FfXboxGamepad;
|
||||||
|
/*
|
||||||
|
pub use reuse_keyboard::ReuseKeyboard;
|
||||||
|
pub use reuse_xbox_gamepad::ReuseXboxGamepad;
|
||||||
|
pub use stress_keyboard::StressKeyboard;
|
||||||
|
pub use stress_xbox_gamepad::StressXboxGamepad;
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// Common scenario arguments passed from CLI
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ScenarioArgs {
|
||||||
|
pub ipc: bool,
|
||||||
|
pub dev_path: Option<String>,
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct LoggedInputEvent {
|
pub struct LoggedInputEvent {
|
||||||
pub tv_sec: i64,
|
pub tv_sec: i64,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ fn test_bwrap_simple() {
|
||||||
#[cfg(all(feature = "requires-privileges", feature = "requires-bwrap"))]
|
#[cfg(all(feature = "requires-privileges", feature = "requires-bwrap"))]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_bwrap_ipc() {
|
fn test_bwrap_ipc() {
|
||||||
let bwrap_ipc = env!("CARGO_BIN_EXE_bwrap-ipc");
|
let bwrap_ipc = env!("CARGO_BIN_EXE_test-ipc");
|
||||||
|
|
||||||
let (builder, ipc) = bwrap::BwrapBuilder::new()
|
let (builder, ipc) = bwrap::BwrapBuilder::new()
|
||||||
.unshare_all()
|
.unshare_all()
|
||||||
|
|
@ -99,18 +99,14 @@ fn test_keyboard_on_host() {
|
||||||
fn test_keyboard_in_container_with_uinput() {
|
fn test_keyboard_in_container_with_uinput() {
|
||||||
let test_keyboard = env!("CARGO_BIN_EXE_test-keyboard");
|
let test_keyboard = env!("CARGO_BIN_EXE_test-keyboard");
|
||||||
|
|
||||||
let (builder, _ipc) = bwrap::BwrapBuilder::new()
|
let out = bwrap::BwrapBuilder::new()
|
||||||
.unshare_net()
|
.unshare_net()
|
||||||
.ro_bind("/", "/")
|
.ro_bind("/", "/")
|
||||||
.tmpfs("/tmp")
|
.tmpfs("/tmp")
|
||||||
.dev_bind("/dev/uinput", "/dev/uinput")
|
.dev_bind("/dev/uinput", "/dev/uinput")
|
||||||
.dev_bind("/dev/input", "/dev/input")
|
.dev_bind("/dev/input", "/dev/input")
|
||||||
.die_with_parent()
|
.die_with_parent()
|
||||||
.with_ipc()
|
.command(test_keyboard, &[])
|
||||||
.expect("failed to create IPC");
|
|
||||||
|
|
||||||
let out = builder
|
|
||||||
.command(test_keyboard, &["--ipc"])
|
|
||||||
.run()
|
.run()
|
||||||
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
|
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
|
||||||
|
|
||||||
|
|
@ -127,12 +123,12 @@ fn test_keyboard_in_container_with_uinput() {
|
||||||
feature = "requires-bwrap"
|
feature = "requires-bwrap"
|
||||||
))]
|
))]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_keyboard_in_container_with_vuinput() {
|
fn test_keyboard_in_container_with_vuinput_placement_in_container() {
|
||||||
run_vuinputd::ensure_vuinputd_running();
|
let _guard: run_vuinputd::VuinputdGuard = run_vuinputd::ensure_vuinputd_running(&[]);
|
||||||
|
|
||||||
let test_keyboard = env!("CARGO_BIN_EXE_test-keyboard");
|
let test_keyboard = env!("CARGO_BIN_EXE_test-keyboard");
|
||||||
|
|
||||||
let (builder, _ipc) = bwrap::BwrapBuilder::new()
|
let out = bwrap::BwrapBuilder::new()
|
||||||
.unshare_net()
|
.unshare_net()
|
||||||
.ro_bind("/", "/")
|
.ro_bind("/", "/")
|
||||||
.tmpfs("/tmp")
|
.tmpfs("/tmp")
|
||||||
|
|
@ -142,11 +138,103 @@ fn test_keyboard_in_container_with_vuinput() {
|
||||||
.tmpfs("/run")
|
.tmpfs("/run")
|
||||||
.dev_bind("/dev/vuinput-test", "/dev/uinput")
|
.dev_bind("/dev/vuinput-test", "/dev/uinput")
|
||||||
.die_with_parent()
|
.die_with_parent()
|
||||||
.with_ipc()
|
.command(test_keyboard, &[])
|
||||||
.expect("failed to create IPC");
|
.run()
|
||||||
|
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
|
||||||
let out = builder
|
|
||||||
.command(test_keyboard, &["--ipc"])
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "requires-privileges",
|
||||||
|
feature = "requires-uinput",
|
||||||
|
feature = "requires-bwrap"
|
||||||
|
))]
|
||||||
|
#[test]
|
||||||
|
fn test_keyboard_in_container_with_vuinput_placement_on_host() {
|
||||||
|
let _guard = run_vuinputd::ensure_vuinputd_running(&["--placement", "on-host"]);
|
||||||
|
|
||||||
|
let test_keyboard = env!("CARGO_BIN_EXE_test-keyboard");
|
||||||
|
|
||||||
|
let out = bwrap::BwrapBuilder::new()
|
||||||
|
.unshare_net()
|
||||||
|
.ro_bind("/", "/")
|
||||||
|
.tmpfs("/tmp")
|
||||||
|
// dev needs to be writable for the new devices
|
||||||
|
.dev()
|
||||||
|
// run needs to be writable for the udev devices
|
||||||
|
.tmpfs("/run")
|
||||||
|
.bind("/run/vuinputd/vuinput-test/udev", "/run/udev")
|
||||||
|
.dev_bind("/dev/vuinput-test", "/dev/uinput")
|
||||||
|
.die_with_parent()
|
||||||
|
.command(test_keyboard, &[])
|
||||||
|
.run()
|
||||||
|
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
|
||||||
|
|
||||||
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "requires-privileges",
|
||||||
|
feature = "requires-uinput",
|
||||||
|
feature = "requires-bwrap"
|
||||||
|
))]
|
||||||
|
#[test]
|
||||||
|
fn test_gamepad_with_ff_in_container() {
|
||||||
|
let _guard: run_vuinputd::VuinputdGuard = run_vuinputd::ensure_vuinputd_running(&[]);
|
||||||
|
|
||||||
|
let test_scenarios = env!("CARGO_BIN_EXE_test-scenarios");
|
||||||
|
|
||||||
|
let out = bwrap::BwrapBuilder::new()
|
||||||
|
.unshare_net()
|
||||||
|
.ro_bind("/", "/")
|
||||||
|
.tmpfs("/tmp")
|
||||||
|
// dev needs to be writable for the new devices
|
||||||
|
.dev()
|
||||||
|
// run needs to be writable for the udev devices
|
||||||
|
.tmpfs("/run")
|
||||||
|
.dev_bind("/dev/vuinput-test", "/dev/uinput")
|
||||||
|
.command(test_scenarios, &["ff-xbox-gamepad"])
|
||||||
|
.run()
|
||||||
|
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
|
||||||
|
|
||||||
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "requires-privileges",
|
||||||
|
feature = "requires-uinput",
|
||||||
|
feature = "requires-bwrap"
|
||||||
|
))]
|
||||||
|
#[test]
|
||||||
|
fn test_mouse_absolute_in_container() {
|
||||||
|
let _guard: run_vuinputd::VuinputdGuard = run_vuinputd::ensure_vuinputd_running(&[]);
|
||||||
|
|
||||||
|
let test_scenarios = env!("CARGO_BIN_EXE_test-scenarios");
|
||||||
|
|
||||||
|
let out = bwrap::BwrapBuilder::new()
|
||||||
|
.unshare_net()
|
||||||
|
.ro_bind("/", "/")
|
||||||
|
.tmpfs("/tmp")
|
||||||
|
// dev needs to be writable for the new devices
|
||||||
|
.dev()
|
||||||
|
// run needs to be writable for the udev devices
|
||||||
|
.tmpfs("/run")
|
||||||
|
.dev_bind("/dev/vuinput-test", "/dev/uinput")
|
||||||
|
.die_with_parent()
|
||||||
|
.command(test_scenarios, &["basic-mouse-absolute"])
|
||||||
.run()
|
.run()
|
||||||
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
|
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
|
||||||
|
|
||||||
|
|
|
||||||
127
vuinputd-tests/tests/podman_tests.rs
Normal file
127
vuinputd-tests/tests/podman_tests.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
use vuinputd_tests::podman;
|
||||||
|
use vuinputd_tests::run_vuinputd;
|
||||||
|
|
||||||
|
#[cfg(all(feature = "requires-privileges", feature = "requires-podman"))]
|
||||||
|
#[test]
|
||||||
|
fn test_podman_simple() {
|
||||||
|
let out = podman::PodmanBuilder::new()
|
||||||
|
.run_cmd()
|
||||||
|
.rm()
|
||||||
|
//.detach()
|
||||||
|
//.name(&format!("vuinputd-podman-tests"))
|
||||||
|
.image("localhost/vuinputd-tests:latest")
|
||||||
|
.command(&["/test-ok"])
|
||||||
|
.run()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(feature = "requires-privileges", feature = "requires-podman"))]
|
||||||
|
#[test]
|
||||||
|
fn test_podman_ipc() {
|
||||||
|
let (builder, ipc) = podman::PodmanBuilder::new()
|
||||||
|
.run_cmd()
|
||||||
|
.rm()
|
||||||
|
.with_ipc()
|
||||||
|
.expect("failed to create IPC");
|
||||||
|
let builder = builder
|
||||||
|
//.detach()
|
||||||
|
//.name(&format!("vuinputd-podman-tests"))
|
||||||
|
.image("localhost/vuinputd-tests:latest")
|
||||||
|
.command(&["/test-ipc"]);
|
||||||
|
|
||||||
|
// Note that builder.run() will block. Thus, the send needs to happen before the child process blocks
|
||||||
|
// the host process.
|
||||||
|
ipc.send("continue".as_bytes())
|
||||||
|
.unwrap_or_else(|e| panic!("failed to send data via ipc: {e}"));
|
||||||
|
|
||||||
|
let out = builder
|
||||||
|
.run()
|
||||||
|
.unwrap_or_else(|e| panic!("failed to run podman!: {e}"));
|
||||||
|
|
||||||
|
let result = ipc.recv(Some(Duration::from_secs(5)));
|
||||||
|
|
||||||
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
|
||||||
|
let result = result.expect("error receiving input from ipc as host within 5 seconds");
|
||||||
|
let result_str =
|
||||||
|
str::from_utf8(&result).expect("message received from ipc is not encoded as utf8");
|
||||||
|
println!("host received {}", result_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "requires-privileges",
|
||||||
|
feature = "requires-uinput",
|
||||||
|
feature = "requires-podman"
|
||||||
|
))]
|
||||||
|
#[test]
|
||||||
|
fn test_keyboard_in_container_with_vuinput() {
|
||||||
|
let _guard = run_vuinputd::ensure_vuinputd_running(&[]);
|
||||||
|
|
||||||
|
let (builder, _ipc) = podman::PodmanBuilder::new()
|
||||||
|
.run_cmd()
|
||||||
|
.rm()
|
||||||
|
.with_ipc()
|
||||||
|
.expect("failed to create IPC");
|
||||||
|
let builder = builder
|
||||||
|
//.detach()
|
||||||
|
//.name(&format!("vuinputd-podman-tests"))
|
||||||
|
.device("/dev/vuinput-test:/dev/uinput")
|
||||||
|
.allow_input_devices()
|
||||||
|
.image("localhost/vuinputd-tests:latest")
|
||||||
|
.command(&["/test-keyboard"]);
|
||||||
|
|
||||||
|
let out = builder
|
||||||
|
.run()
|
||||||
|
.unwrap_or_else(|e| panic!("failed to run podman!: {e}"));
|
||||||
|
|
||||||
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(
|
||||||
|
feature = "requires-rootless",
|
||||||
|
feature = "requires-uinput",
|
||||||
|
feature = "requires-podman"
|
||||||
|
))]
|
||||||
|
#[test]
|
||||||
|
fn test_keyboard_in_container_with_vuinput_rootless_with_userns() {
|
||||||
|
let _guard = run_vuinputd::ensure_vuinputd_running(&[]);
|
||||||
|
|
||||||
|
let (builder, _ipc) = podman::PodmanBuilder::new()
|
||||||
|
.run_cmd()
|
||||||
|
.rm()
|
||||||
|
.userns("auto")
|
||||||
|
.with_ipc()
|
||||||
|
.expect("failed to create IPC");
|
||||||
|
let builder = builder
|
||||||
|
//.detach()
|
||||||
|
//.name(&format!("vuinputd-podman-tests"))
|
||||||
|
.device("/dev/vuinput-test:/dev/uinput")
|
||||||
|
.allow_input_devices()
|
||||||
|
.image("localhost/vuinputd-tests:latest")
|
||||||
|
.command(&["/test-keyboard"]);
|
||||||
|
|
||||||
|
let out = builder
|
||||||
|
.run()
|
||||||
|
.unwrap_or_else(|e| panic!("failed to run podman!: {e}"));
|
||||||
|
|
||||||
|
println!("Output");
|
||||||
|
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
|
||||||
|
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
|
||||||
|
|
||||||
|
assert!(out.status.success());
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "vuinputd"
|
name = "vuinputd"
|
||||||
version = "0.3.0"
|
version = "0.3.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Johannes Leupolz <dev@leupolz.eu>"]
|
authors = ["Johannes Leupolz <dev@leupolz.eu>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
@ -29,3 +29,13 @@ anyhow = "1.0.100"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
|
base64 = "0.22"
|
||||||
|
smallvec = "1.15.1"
|
||||||
|
async-trait = "0.1.89"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
requires-privileges = []
|
||||||
|
requires-rootless = []
|
||||||
|
requires-uinput = []
|
||||||
|
requires-bwrap = []
|
||||||
|
requires-podman = []
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,18 @@ pub enum Action {
|
||||||
minor: u64,
|
minor: u64,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[serde(rename = "emit-udev-event")]
|
#[serde(rename = "write-udev-runtime-data")]
|
||||||
EmitUdevEvent {
|
WriteUdevRuntimeData {
|
||||||
netlink_message: HashMap<String, String>,
|
|
||||||
runtime_data: Option<String>,
|
runtime_data: Option<String>,
|
||||||
major: u64,
|
major: u64,
|
||||||
minor: u64,
|
minor: u64,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[serde(rename = "emit-netlink-message")]
|
||||||
|
EmitNetlinkMessage {
|
||||||
|
netlink_message: HashMap<String, String>,
|
||||||
|
},
|
||||||
|
|
||||||
#[serde(rename = "remove-device")]
|
#[serde(rename = "remove-device")]
|
||||||
RemoveDevice {
|
RemoveDevice {
|
||||||
path: String,
|
path: String,
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
use super::action::Action;
|
use super::action::Action;
|
||||||
use super::input_device;
|
use crate::input_realizer::input_device;
|
||||||
use super::netlink_message;
|
use crate::input_realizer::netlink_message;
|
||||||
use super::runtime_data;
|
use crate::input_realizer::runtime_data;
|
||||||
|
|
||||||
pub fn handle_cli_action(json: String) -> i32 {
|
pub fn handle_cli_action(json: String) -> i32 {
|
||||||
let action: Action = serde_json::from_str(&json).expect("invalid action JSON");
|
let action: Action = serde_json::from_str(&json).expect("invalid action JSON");
|
||||||
|
|
@ -21,20 +21,24 @@ fn handle_action(action: Action) -> anyhow::Result<()> {
|
||||||
input_device::ensure_input_device(path, major.into(), minor.into())?;
|
input_device::ensure_input_device(path, major.into(), minor.into())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Action::EmitUdevEvent {
|
Action::WriteUdevRuntimeData {
|
||||||
netlink_message,
|
|
||||||
runtime_data,
|
runtime_data,
|
||||||
major,
|
major,
|
||||||
minor,
|
minor,
|
||||||
} => {
|
} => {
|
||||||
netlink_message::send_udev_monitor_message_with_properties(netlink_message);
|
|
||||||
runtime_data::ensure_udev_structure()?;
|
runtime_data::ensure_udev_structure()?;
|
||||||
match runtime_data {
|
match runtime_data {
|
||||||
Some(data) => runtime_data::write_udev_data(&data, major.into(), minor.into())?,
|
Some(data) => {
|
||||||
None => runtime_data::delete_udev_data(major.into(), minor.into())?,
|
runtime_data::write_udev_data("/run", &data, major.into(), minor.into())?
|
||||||
|
}
|
||||||
|
None => runtime_data::delete_udev_data("/run", major.into(), minor.into())?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Action::EmitNetlinkMessage { netlink_message } => {
|
||||||
|
netlink_message::send_udev_monitor_message_with_properties(netlink_message);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Action::RemoveDevice { path, major, minor } => {
|
Action::RemoveDevice { path, major, minor } => {
|
||||||
input_device::remove_input_device(path, major.into(), minor.into())?;
|
input_device::remove_input_device(path, major.into(), minor.into())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,3 @@
|
||||||
|
|
||||||
pub mod action;
|
pub mod action;
|
||||||
pub mod handle_action;
|
pub mod handle_action;
|
||||||
pub mod input_device;
|
|
||||||
pub mod netlink_message;
|
|
||||||
pub mod runtime_data;
|
|
||||||
|
|
|
||||||
405
vuinputd/src/container_runtime/injection_strategy.rs
Normal file
405
vuinputd/src/container_runtime/injection_strategy.rs
Normal file
|
|
@ -0,0 +1,405 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
actions::action::Action,
|
||||||
|
global_config::{self, get_scope},
|
||||||
|
input_realizer::{input_device, runtime_data},
|
||||||
|
process_tools::{self, Pid, RequestingProcess},
|
||||||
|
};
|
||||||
|
pub static PLACEMENT_IN_CONTAINER: GenericPlacementInContainer = GenericPlacementInContainer {};
|
||||||
|
pub static PLACEMENT_ON_HOST: GenericPlacementOnHost = GenericPlacementOnHost {};
|
||||||
|
pub static SEND_NETLINK_ONLY: GenericSendNetlinkMessageOnly = GenericSendNetlinkMessageOnly {};
|
||||||
|
pub static INCUS: Incus = Incus {};
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait InjectionStrategy {
|
||||||
|
/// Create the device node.
|
||||||
|
async fn mknod_device_node(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// Remove device.
|
||||||
|
async fn remove_device_node(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// Write udev metadata.
|
||||||
|
async fn write_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
runtime_data: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// Remove runtime data.
|
||||||
|
async fn remove_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
|
||||||
|
/// Emit netlink message.
|
||||||
|
async fn emit_netlink_message(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
netlink_message: HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct GenericPlacementInContainer {}
|
||||||
|
pub struct GenericPlacementOnHost {}
|
||||||
|
pub struct GenericSendNetlinkMessageOnly {}
|
||||||
|
pub struct Incus {}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl InjectionStrategy for GenericPlacementInContainer {
|
||||||
|
async fn mknod_device_node(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mknod_device_action = Action::MknodDevice {
|
||||||
|
path: format!("/dev/input/{}", &devname),
|
||||||
|
major: major,
|
||||||
|
minor: minor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_pid =
|
||||||
|
process_tools::start_action(mknod_device_action, &requesting_process, false)
|
||||||
|
.expect("subprocess should work");
|
||||||
|
|
||||||
|
let _exit_info = process_tools::await_process(Pid::Pid(child_pid))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_device_node(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let dev_path = format!("/dev/input/{}", devname);
|
||||||
|
let remove_device_action = Action::RemoveDevice {
|
||||||
|
path: dev_path,
|
||||||
|
major: major,
|
||||||
|
minor: minor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_pid_1 =
|
||||||
|
process_tools::start_action(remove_device_action, &requesting_process, false)
|
||||||
|
.expect("subprocess should work");
|
||||||
|
|
||||||
|
let _exit_info = process_tools::await_process(Pid::Pid(child_pid_1)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
runtime_data: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let write_udev_runtime_data = Action::WriteUdevRuntimeData {
|
||||||
|
runtime_data: Some(runtime_data.to_string()),
|
||||||
|
major: major,
|
||||||
|
minor: minor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_pid =
|
||||||
|
process_tools::start_action(write_udev_runtime_data, &requesting_process, false)
|
||||||
|
.expect("subprocess should work");
|
||||||
|
|
||||||
|
let _exit_info = process_tools::await_process(Pid::Pid(child_pid))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let write_udev_runtime_data_action = Action::WriteUdevRuntimeData {
|
||||||
|
runtime_data: None,
|
||||||
|
major: major,
|
||||||
|
minor: minor,
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_pid_2 =
|
||||||
|
process_tools::start_action(write_udev_runtime_data_action, &requesting_process, false)
|
||||||
|
.expect("subprocess should work");
|
||||||
|
let _exit_info = process_tools::await_process(Pid::Pid(child_pid_2)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit netlink message.
|
||||||
|
async fn emit_netlink_message(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
netlink_message: HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let emit_netlink_message = Action::EmitNetlinkMessage {
|
||||||
|
netlink_message: netlink_message,
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_pid =
|
||||||
|
process_tools::start_action(emit_netlink_message, requesting_process, false)
|
||||||
|
.expect("subprocess should work");
|
||||||
|
|
||||||
|
let _exit_info = process_tools::await_process(Pid::Pid(child_pid)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl InjectionStrategy for GenericPlacementOnHost {
|
||||||
|
async fn mknod_device_node(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname());
|
||||||
|
let path = format!("{}/dev-input/{}", path_prefix, devname);
|
||||||
|
input_device::ensure_input_device(path.clone(), major, minor)
|
||||||
|
.expect(&format!("VUI-DEV-001: could not create {}", &path));
|
||||||
|
//TODO: somewhat costly
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_device_node(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname());
|
||||||
|
let devnode = format!("{}/dev-input/{}", path_prefix, devname);
|
||||||
|
input_device::remove_input_device(devnode.clone(), major, minor).expect(&format!(
|
||||||
|
"VUI-DEV-003: could not remove device node {}",
|
||||||
|
&devnode
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
runtime_data: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname());
|
||||||
|
runtime_data::write_udev_data(&path_prefix, &runtime_data, major.into(), minor.into())
|
||||||
|
.expect(&format!(
|
||||||
|
"VUI-UDEV-002: could not write into {}",
|
||||||
|
&path_prefix
|
||||||
|
)); //TODO: somewhat costly
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let path_prefix = format!("/run/vuinputd/{}", global_config::get_vudevname());
|
||||||
|
runtime_data::delete_udev_data(&path_prefix, major, minor).expect(&format!(
|
||||||
|
"VUI-UDEV-003: could not remove udev data from {}",
|
||||||
|
&path_prefix
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit netlink message.
|
||||||
|
async fn emit_netlink_message(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
netlink_message: HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
PLACEMENT_IN_CONTAINER
|
||||||
|
.emit_netlink_message(requesting_process, netlink_message)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl InjectionStrategy for GenericSendNetlinkMessageOnly {
|
||||||
|
async fn mknod_device_node(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
_devname: &str,
|
||||||
|
_major: u64,
|
||||||
|
_minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_device_node(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
_devname: &str,
|
||||||
|
_major: u64,
|
||||||
|
_minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
_runtime_data: &str,
|
||||||
|
_major: u64,
|
||||||
|
_minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
_major: u64,
|
||||||
|
_minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit netlink message.
|
||||||
|
async fn emit_netlink_message(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
netlink_message: HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
PLACEMENT_IN_CONTAINER
|
||||||
|
.emit_netlink_message(requesting_process, netlink_message)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl InjectionStrategy for Incus {
|
||||||
|
async fn mknod_device_node(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
_major: u64,
|
||||||
|
_minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let hostpath = format!("path=/dev/input/{}", devname);
|
||||||
|
let incuspath = format!("path=/dev/input/{}", devname);
|
||||||
|
let container_name = get_scope();
|
||||||
|
let container_name = match container_name {
|
||||||
|
global_config::Scope::Multi => bail!("no container name given"),
|
||||||
|
global_config::Scope::Single(container_name) => container_name,
|
||||||
|
};
|
||||||
|
let child = std::process::Command::new("/usr/bin/incus")
|
||||||
|
.args([
|
||||||
|
"config",
|
||||||
|
"device",
|
||||||
|
"add",
|
||||||
|
container_name,
|
||||||
|
devname,
|
||||||
|
"unix-char",
|
||||||
|
&incuspath,
|
||||||
|
&hostpath,
|
||||||
|
"mode=666",
|
||||||
|
])
|
||||||
|
.spawn()?;
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
println!("incus\n {}\n{}\n", stdout, stderr);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_device_node(
|
||||||
|
&self,
|
||||||
|
_requesting_process: &RequestingProcess,
|
||||||
|
devname: &str,
|
||||||
|
_major: u64,
|
||||||
|
_minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let container_name = get_scope();
|
||||||
|
let container_name = match container_name {
|
||||||
|
global_config::Scope::Multi => bail!("no container name given"),
|
||||||
|
global_config::Scope::Single(container_name) => container_name,
|
||||||
|
};
|
||||||
|
let child = std::process::Command::new("/usr/bin/incus")
|
||||||
|
.args(["config", "device", "remove", container_name, devname])
|
||||||
|
.spawn()?;
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
println!("incus\n {}\n{}\n", stdout, stderr);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
runtime_data: &str,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
PLACEMENT_IN_CONTAINER
|
||||||
|
.write_udev_runtime_data(requesting_process, runtime_data, major, minor)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_udev_runtime_data(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
major: u64,
|
||||||
|
minor: u64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
PLACEMENT_IN_CONTAINER
|
||||||
|
.remove_udev_runtime_data(requesting_process, major, minor)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emit netlink message.
|
||||||
|
/// Emit netlink message.
|
||||||
|
async fn emit_netlink_message(
|
||||||
|
&self,
|
||||||
|
requesting_process: &RequestingProcess,
|
||||||
|
netlink_message: HashMap<String, String>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let emit_netlink_message = Action::EmitNetlinkMessage {
|
||||||
|
netlink_message: netlink_message,
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_pid = process_tools::start_action(emit_netlink_message, requesting_process, true)
|
||||||
|
.expect("subprocess should work");
|
||||||
|
|
||||||
|
let _exit_info = process_tools::await_process(Pid::Pid(child_pid)).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
77
vuinputd/src/container_runtime/mod.rs
Normal file
77
vuinputd/src/container_runtime/mod.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
container_runtime::injection_strategy::{
|
||||||
|
GenericPlacementInContainer, GenericPlacementOnHost, GenericSendNetlinkMessageOnly, INCUS, InjectionStrategy, PLACEMENT_IN_CONTAINER, PLACEMENT_ON_HOST, SEND_NETLINK_ONLY
|
||||||
|
},
|
||||||
|
global_config::get_vudevname,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod injection_strategy;
|
||||||
|
|
||||||
|
/// Container runtime used for name resolution and lifecycle events
|
||||||
|
#[derive(Debug, Clone, clap::ValueEnum, Default, PartialEq, Eq)]
|
||||||
|
pub enum ContainerRuntime {
|
||||||
|
#[default]
|
||||||
|
/// Probe for installed runtimes (default). Currently just falls back to "generic placement in container"
|
||||||
|
Auto,
|
||||||
|
/// Generic linux namespaces. This technique uses nsenter and tries to create files and devices directly in the filesystem inside the container.
|
||||||
|
GenericPlacementInContainer,
|
||||||
|
/// Generic linux namespaces. This technique creates files and devices directly in the filesystem of the host. It is the job of the user to bind mount those devices to make them available in the container.
|
||||||
|
GenericPlacementOnHost,
|
||||||
|
/// Generic linux namespaces. This technique just sends the netlink message. Works if the user bind mounds the whole /dev/input and /var/run/udev-folder
|
||||||
|
GenericSendNetlinkMessageOnly,
|
||||||
|
/// Incus (incus info / incus list). Not implemented, yet.
|
||||||
|
Incus,
|
||||||
|
/// Docker (docker inspect / Docker socket). This currently falls back to GenericPlacementInContainer.
|
||||||
|
Docker,
|
||||||
|
/// Podman (podman inspect / Podman socket). This currently falls back to GenericPlacementOnHost
|
||||||
|
Podman,
|
||||||
|
/// systemd-nspawn via machinectl. This currently falls back to GenericPlacementInContainer.
|
||||||
|
Nspawn,
|
||||||
|
/// bubblewrap. This currently falls back to GenericPlacementOnHost
|
||||||
|
Bubblewrap,
|
||||||
|
/// Custom engine, please define a --strategie-file
|
||||||
|
CustomEngine,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerRuntime {
|
||||||
|
fn uses_run_folder(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
ContainerRuntime::Auto => false,
|
||||||
|
ContainerRuntime::GenericPlacementInContainer => false,
|
||||||
|
ContainerRuntime::GenericPlacementOnHost => true,
|
||||||
|
ContainerRuntime::GenericSendNetlinkMessageOnly => false,
|
||||||
|
ContainerRuntime::Incus => false,
|
||||||
|
ContainerRuntime::Docker => false,
|
||||||
|
ContainerRuntime::Podman => false,
|
||||||
|
ContainerRuntime::Nspawn => false,
|
||||||
|
ContainerRuntime::Bubblewrap => true,
|
||||||
|
ContainerRuntime::CustomEngine => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize(&self) {
|
||||||
|
if self.uses_run_folder() {
|
||||||
|
let path_prefix = format!("/run/vuinputd/{}", get_vudevname());
|
||||||
|
let _ = crate::input_realizer::host_fs::ensure_host_fs_structure(&path_prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn injection_strategy(&self) -> &'static dyn InjectionStrategy {
|
||||||
|
match self {
|
||||||
|
ContainerRuntime::Auto => &PLACEMENT_IN_CONTAINER,
|
||||||
|
ContainerRuntime::GenericPlacementInContainer => &PLACEMENT_IN_CONTAINER,
|
||||||
|
ContainerRuntime::GenericPlacementOnHost => &PLACEMENT_ON_HOST,
|
||||||
|
ContainerRuntime::GenericSendNetlinkMessageOnly => &SEND_NETLINK_ONLY,
|
||||||
|
ContainerRuntime::Incus => &INCUS,
|
||||||
|
ContainerRuntime::Docker => &PLACEMENT_IN_CONTAINER,
|
||||||
|
ContainerRuntime::Podman => &PLACEMENT_IN_CONTAINER,
|
||||||
|
ContainerRuntime::Nspawn => &PLACEMENT_IN_CONTAINER,
|
||||||
|
ContainerRuntime::Bubblewrap => &PLACEMENT_ON_HOST,
|
||||||
|
ContainerRuntime::CustomEngine => todo!("not implemented yet"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
163
vuinputd/src/cuse_device/device_policy.rs
Normal file
163
vuinputd/src/cuse_device/device_policy.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use libc::input_event;
|
||||||
|
|
||||||
|
// event types and codes from https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
|
||||||
|
|
||||||
|
const EV_SYN: u16 = 0x00;
|
||||||
|
const EV_KEY: u16 = 0x01;
|
||||||
|
const EV_REL: u16 = 0x02;
|
||||||
|
const EV_ABS: u16 = 0x03;
|
||||||
|
const EV_MSC: u16 = 0x04;
|
||||||
|
const EV_SW: u16 = 0x05;
|
||||||
|
const EV_LED: u16 = 0x11;
|
||||||
|
const EV_SND: u16 = 0x12;
|
||||||
|
const EV_REP: u16 = 0x14;
|
||||||
|
const EV_FF: u16 = 0x15;
|
||||||
|
const EV_PWR: u16 = 0x16;
|
||||||
|
const EV_FF_STATUS: u16 = 0x17;
|
||||||
|
const EV_MAX: u16 = 0x1f;
|
||||||
|
|
||||||
|
// special keyboard keys
|
||||||
|
const KEY_LEFTALT: u16 = 56;
|
||||||
|
const KEY_RIGHTALT: u16 = 100;
|
||||||
|
const KEY_LEFTCTRL: u16 = 29;
|
||||||
|
const KEY_RIGHTCTRL: u16 = 97;
|
||||||
|
const KEY_F1: u16 = 59;
|
||||||
|
const KEY_F10: u16 = 68;
|
||||||
|
const KEY_F11: u16 = 87;
|
||||||
|
const KEY_F12: u16 = 88;
|
||||||
|
const KEY_SYSRQ: u16 = 99;
|
||||||
|
const KEY_DELETE: u16 = 111;
|
||||||
|
const KEY_KPDOT: u16 = 83;
|
||||||
|
const KEY_POWER: u16 = 116;
|
||||||
|
const KEY_SLEEP: u16 = 142;
|
||||||
|
const KEY_WAKEUP: u16 = 143;
|
||||||
|
const KEY_BREAK: u16 = 0x19b;
|
||||||
|
const KEY_PAUSE: u16 = 119;
|
||||||
|
const KEY_RESTART: u16 = 0x198;
|
||||||
|
|
||||||
|
const KEY_FN: u16 = 0x1d0;
|
||||||
|
// TODO: Should we block range until KEY_FN_RIGHT_SHIFT?
|
||||||
|
|
||||||
|
// Gamepad keys from https://github.com/torvalds/linux/blob/master/Documentation/input/gamepad.rst
|
||||||
|
// First range
|
||||||
|
const BTN_SOUTH: u16 = 0x130;
|
||||||
|
const BTN_THUMBR: u16 = 0x13e;
|
||||||
|
// Second range
|
||||||
|
const BTN_DPAD_UP: u16 = 0x220;
|
||||||
|
const BTN_GRIPR2: u16 = 0x227;
|
||||||
|
|
||||||
|
use crate::{cuse_device::state::KeyTracker, global_config::DevicePolicy};
|
||||||
|
|
||||||
|
pub fn is_allowed(keytracker: &mut KeyTracker, policy: &DevicePolicy, event: &input_event) -> bool {
|
||||||
|
match policy {
|
||||||
|
DevicePolicy::None => true,
|
||||||
|
DevicePolicy::MuteSysRq => is_allowed_in_mute_sysrq(keytracker, event),
|
||||||
|
DevicePolicy::Sanitized => is_allowed_in_sanitized_mode(keytracker, event),
|
||||||
|
DevicePolicy::StrictGamepad => is_allowed_in_strict_gamepad_mode(keytracker, event),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_allowed_in_mute_sysrq(_keytracker: &mut KeyTracker, event: &input_event) -> bool {
|
||||||
|
if event.type_ == EV_KEY && event.code == KEY_SYSRQ {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_allowed_in_sanitized_mode(keytracker: &mut KeyTracker, event: &input_event) -> bool {
|
||||||
|
let type_ = event.type_;
|
||||||
|
let code = event.code;
|
||||||
|
let value = event.value;
|
||||||
|
|
||||||
|
if type_ == EV_KEY {
|
||||||
|
match code {
|
||||||
|
KEY_LEFTALT => keytracker.left_alt_down = value > 0,
|
||||||
|
KEY_RIGHTALT => keytracker.right_alt_down = value > 0,
|
||||||
|
KEY_LEFTCTRL => keytracker.left_ctrl_down = value > 0,
|
||||||
|
KEY_RIGHTCTRL => keytracker.right_ctrl_down = value > 0,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if type_ == EV_KEY {
|
||||||
|
// 1. Block SysRq in general
|
||||||
|
if code == KEY_SYSRQ {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let alt_down = keytracker.left_alt_down || keytracker.right_alt_down;
|
||||||
|
let ctrl_down = keytracker.left_ctrl_down || keytracker.right_ctrl_down;
|
||||||
|
|
||||||
|
// 2. Block VT Switching
|
||||||
|
// To block VT Switching, all CONSOLE_ actions need to be ignored.
|
||||||
|
// In standard Linux keymaps (defkeymap)
|
||||||
|
// https://github.com/torvalds/linux/blob/master/drivers/tty/vt/defkeymap.map
|
||||||
|
// - Left Alt + F1–F12 usually maps to Console_1 – Console_12.
|
||||||
|
// - Right Alt (AltGr) + F1–F12 usually maps to Console_13 – Console_24.
|
||||||
|
// Note: Alt + Left/Right (Decr_Console / Incr_Console) is still allowed. We assume
|
||||||
|
// this is blocked in any other way.
|
||||||
|
if alt_down && (code >= KEY_F1 && code <= KEY_F10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if alt_down && (code >= KEY_F11 && code <= KEY_F12) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// keycode 84 = Last_Console
|
||||||
|
// alt keycode 105 = Decr_Console
|
||||||
|
// alt keycode 106 = Incr_Console
|
||||||
|
|
||||||
|
// 3. Block CAD (Ctrl + Alt + Del).
|
||||||
|
// Block basically all Boot from defkeymap.map
|
||||||
|
if alt_down && ctrl_down && (code == KEY_DELETE || code == KEY_KPDOT) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Block SAK (CTRL+ALT+PAUSE). seems not to be mapped
|
||||||
|
// This one is crucial, because it is allowed even in K_OFF mode.
|
||||||
|
// https://github.com/torvalds/linux/blob/master/drivers/tty/vt/keyboard.c
|
||||||
|
// It seems not to be mapped by default, but close the potential hole if a
|
||||||
|
// distro follows the ancient https://www.kernel.org/doc/Documentation/SAK.txt
|
||||||
|
|
||||||
|
// 5. Block standalone dangerous keys
|
||||||
|
match code {
|
||||||
|
KEY_POWER | KEY_SLEEP | KEY_WAKEUP | KEY_FN | KEY_BREAK | KEY_PAUSE | KEY_RESTART => {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_allowed_in_strict_gamepad_mode(_keytracker: &mut KeyTracker, event: &input_event) -> bool {
|
||||||
|
match event.type_ {
|
||||||
|
EV_SYN => true,
|
||||||
|
|
||||||
|
// Analog sticks, triggers
|
||||||
|
EV_ABS => true,
|
||||||
|
|
||||||
|
// Force feedback
|
||||||
|
EV_FF => true,
|
||||||
|
|
||||||
|
// Digital buttons only
|
||||||
|
EV_KEY => match event.code {
|
||||||
|
// Standard gamepad face + shoulder + stick buttons
|
||||||
|
BTN_SOUTH..=BTN_THUMBR => true,
|
||||||
|
|
||||||
|
// D-Pad + extended gamepad buttons (triggers, paddles)
|
||||||
|
BTN_DPAD_UP..=BTN_GRIPR2 => true,
|
||||||
|
|
||||||
|
// Everything else is rejected (KEY_*, mouse buttons, etc.)
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Explicitly reject everything else (EV_REL, EV_MSC, etc.)
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
117
vuinputd/src/cuse_device/evdev_write_watcher.rs
Normal file
117
vuinputd/src/cuse_device/evdev_write_watcher.rs
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
os::fd::{AsFd, BorrowedFd},
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
Arc, Mutex, OnceLock,
|
||||||
|
},
|
||||||
|
thread::{self, JoinHandle},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
use cuse_lowlevel::fuse_lowlevel;
|
||||||
|
use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags};
|
||||||
|
|
||||||
|
use crate::cuse_device::state::{get_vuinput_state, PollPhase, VuFileHandle};
|
||||||
|
|
||||||
|
pub static EVDEV_WRITE_WATCHER: OnceLock<Mutex<EvdevWriteWatcher>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn initialize_evdev_write_watcher() -> anyhow::Result<()> {
|
||||||
|
EVDEV_WRITE_WATCHER
|
||||||
|
.set(Mutex::new(EvdevWriteWatcher::new()?)) // Convert the error from Mutex<T> to a simple string
|
||||||
|
.map_err(|_| anyhow::anyhow!("cell already full"))
|
||||||
|
// Now .context() works because &str is compatible
|
||||||
|
.context("failed to initialize evdev write watcher")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EvdevWriteWatcher {
|
||||||
|
epoll: Arc<Epoll>,
|
||||||
|
shutdown: Arc<AtomicBool>,
|
||||||
|
thread_handle: Option<JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EvdevWriteWatcher {
|
||||||
|
fn new() -> anyhow::Result<Self> {
|
||||||
|
let epoll = Arc::new(Epoll::new(EpollCreateFlags::empty())?);
|
||||||
|
let shutdown = Arc::new(AtomicBool::new(false));
|
||||||
|
let epoll_thread = epoll.clone();
|
||||||
|
let shutdown_thread = shutdown.clone();
|
||||||
|
let thread_handle = Some(thread::spawn(move || {
|
||||||
|
evdev_write_watch_loop(shutdown_thread, epoll_thread);
|
||||||
|
}));
|
||||||
|
Ok(Self {
|
||||||
|
thread_handle: thread_handle,
|
||||||
|
shutdown: shutdown,
|
||||||
|
epoll: epoll,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_device(&self, vu_fh: VuFileHandle) -> nix::Result<()> {
|
||||||
|
let VuFileHandle::Fh(fh) = vu_fh;
|
||||||
|
|
||||||
|
let vuinput_state_mutex = get_vuinput_state(&vu_fh).unwrap();
|
||||||
|
let vuinput_state = vuinput_state_mutex.lock().unwrap();
|
||||||
|
|
||||||
|
self.epoll.add(
|
||||||
|
&vuinput_state.file,
|
||||||
|
EpollEvent::new(EpollFlags::EPOLLIN | EpollFlags::EPOLLET, fh),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_device<Fd: AsFd>(&self, uinput_fd: Fd) -> nix::Result<()> {
|
||||||
|
self.epoll.delete(uinput_fd)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&mut self) {
|
||||||
|
self.shutdown.store(true, Ordering::SeqCst);
|
||||||
|
|
||||||
|
if let Some(handle) = self.thread_handle.take() {
|
||||||
|
let _ = handle.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_running(&self) -> bool {
|
||||||
|
self.thread_handle.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evdev_write_watch_loop(shutdown: Arc<AtomicBool>, epoll: Arc<Epoll>) {
|
||||||
|
let mut events = vec![EpollEvent::empty(); 64];
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if shutdown.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let n = match epoll.wait(&mut events, 500u16) {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("evdev_write_watcher: epoll_wait failed: {err}");
|
||||||
|
thread::sleep(Duration::from_millis(100));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for ev in &events[..n] {
|
||||||
|
let fh_val = ev.data() as u64;
|
||||||
|
let fh = VuFileHandle::Fh(fh_val);
|
||||||
|
let state = super::state::get_vuinput_state(&fh);
|
||||||
|
if let Ok(state) = state {
|
||||||
|
let mut state = state.lock().unwrap();
|
||||||
|
let handle = state.poll.take_waiters();
|
||||||
|
if let Some(mut handle) = handle {
|
||||||
|
handle.notify();
|
||||||
|
}
|
||||||
|
state.poll.pollphase = PollPhase::Readable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,13 @@
|
||||||
//
|
//
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
pub mod device_policy;
|
||||||
|
pub mod evdev_write_watcher;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod vuinput_ioctl;
|
pub mod vuinput_ioctl;
|
||||||
pub mod vuinput_open;
|
pub mod vuinput_open;
|
||||||
|
pub mod vuinput_poll;
|
||||||
|
pub mod vuinput_read;
|
||||||
pub mod vuinput_release;
|
pub mod vuinput_release;
|
||||||
pub mod vuinput_write;
|
pub mod vuinput_write;
|
||||||
|
|
||||||
|
|
@ -28,12 +32,12 @@ pub fn vuinput_make_cuse_ops() -> cuse_lowlevel::cuse_lowlevel_ops {
|
||||||
init_done: None,
|
init_done: None,
|
||||||
destroy: None,
|
destroy: None,
|
||||||
open: Some(vuinput_open::vuinput_open),
|
open: Some(vuinput_open::vuinput_open),
|
||||||
read: None,
|
read: Some(vuinput_read::vuinput_read),
|
||||||
write: Some(vuinput_write::vuinput_write),
|
write: Some(vuinput_write::vuinput_write),
|
||||||
flush: None,
|
flush: None,
|
||||||
release: Some(vuinput_release::vuinput_release),
|
release: Some(vuinput_release::vuinput_release),
|
||||||
fsync: None,
|
fsync: None,
|
||||||
ioctl: Some(vuinput_ioctl::vuinput_ioctl),
|
ioctl: Some(vuinput_ioctl::vuinput_ioctl),
|
||||||
poll: None,
|
poll: Some(vuinput_poll::vuinput_poll),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,156 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::ptr::NonNull;
|
||||||
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
||||||
|
|
||||||
use ::cuse_lowlevel::*;
|
use ::cuse_lowlevel::*;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
use crate::process_tools::RequestingProcess;
|
use crate::process_tools::RequestingProcess;
|
||||||
|
|
||||||
|
pub type PendingPollHandles = SmallVec<[*mut fuse_lowlevel::fuse_pollhandle; 1]>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VuInputDevice {
|
pub struct VuInputDevice {
|
||||||
pub major: u64,
|
pub major: u64,
|
||||||
pub minor: u64,
|
pub minor: u64,
|
||||||
pub syspath: String,
|
pub syspath: String,
|
||||||
|
pub devname: String,
|
||||||
pub devnode: String,
|
pub devnode: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct KeyTracker {
|
||||||
|
pub left_alt_down: bool,
|
||||||
|
pub right_alt_down: bool,
|
||||||
|
pub left_ctrl_down: bool,
|
||||||
|
pub right_ctrl_down: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyTracker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
left_alt_down: false,
|
||||||
|
right_alt_down: false,
|
||||||
|
left_ctrl_down: false,
|
||||||
|
right_ctrl_down: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// EMPTY -> READY -> READING -> { EMPTY | READY }
|
||||||
|
/// EMPTY -> READABLE (new data arrives / watcher can observe)
|
||||||
|
/// READABLE -> READING (read callback starts draining)
|
||||||
|
/// READING -> EMPTY (read drained everything)
|
||||||
|
/// READING -> READABLE (read finished, but more data still remains)
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum PollPhase {
|
||||||
|
Empty,
|
||||||
|
Readable,
|
||||||
|
Reading,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PollPhase {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PollHandle {
|
||||||
|
ptr: NonNull<fuse_lowlevel::fuse_pollhandle>,
|
||||||
|
has_been_completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PollHandle {
|
||||||
|
pub fn new(ptr: NonNull<fuse_lowlevel::fuse_pollhandle>) -> Self {
|
||||||
|
Self {
|
||||||
|
ptr: ptr,
|
||||||
|
has_been_completed: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn notify(&mut self) {
|
||||||
|
if !self.has_been_completed {
|
||||||
|
unsafe {
|
||||||
|
fuse_lowlevel::fuse_lowlevel_notify_poll(self.ptr.as_ptr());
|
||||||
|
fuse_lowlevel::fuse_pollhandle_destroy(self.ptr.as_ptr());
|
||||||
|
}
|
||||||
|
self.has_been_completed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PollHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.has_been_completed {
|
||||||
|
unsafe {
|
||||||
|
fuse_lowlevel::fuse_pollhandle_destroy(self.ptr.as_ptr());
|
||||||
|
}
|
||||||
|
self.has_been_completed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for PollHandle {}
|
||||||
|
|
||||||
|
/// this data structure ensures poll and read are synchronized.
|
||||||
|
/// poll() and read() must synchronize through one shared readines
|
||||||
|
/// state, and the state transitions must be done under the same per-handle mutex.
|
||||||
|
/// Ensure, we have no lost-wakeup races like:
|
||||||
|
/// 1) watcher sets readable
|
||||||
|
/// 2) read() drains and clears readable
|
||||||
|
/// 3) poll() stores waiter too late
|
||||||
|
/// 4) nobody wakes it anymore
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct PollState {
|
||||||
|
/// Sticky readiness latch:
|
||||||
|
/// true once evdev became readable, false again after read/drain.
|
||||||
|
pub pollphase: PollPhase,
|
||||||
|
|
||||||
|
/// Pending FUSE poll waiters for this device.
|
||||||
|
/// Optimized for the common case of 0 or 1 waiter, but supports
|
||||||
|
/// multiple concurrent poll() callers correctly.
|
||||||
|
pending: Option<PollHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PollState {
|
||||||
|
pub fn new() -> PollState {
|
||||||
|
PollState {
|
||||||
|
pollphase: PollPhase::Empty,
|
||||||
|
pending: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn has_waiters(&self) -> bool {
|
||||||
|
!self.pending.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_waiter(&mut self, handle: NonNull<fuse_lowlevel::fuse_pollhandle>) {
|
||||||
|
self.pending = Some(PollHandle::new(handle));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_waiters(&mut self) -> Option<PollHandle> {
|
||||||
|
std::mem::take(&mut self.pending)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PollState {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
//when the device closes, notify all pending waiters
|
||||||
|
let old_handle = self.take_waiters();
|
||||||
|
if let Some(mut old_handle) = old_handle {
|
||||||
|
old_handle.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct VuInputState {
|
pub struct VuInputState {
|
||||||
pub file: File,
|
pub file: File,
|
||||||
pub requesting_process: RequestingProcess,
|
pub requesting_process: RequestingProcess,
|
||||||
pub input_device: Option<VuInputDevice>,
|
pub input_device: Option<VuInputDevice>,
|
||||||
|
pub keytracker: KeyTracker,
|
||||||
|
pub poll: PollState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, Hash, PartialEq, Clone)]
|
#[derive(Debug, Eq, Hash, PartialEq, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -3,20 +3,19 @@
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
use ::cuse_lowlevel::*;
|
use ::cuse_lowlevel::*;
|
||||||
use libc::{iovec, size_t, EBADRQC};
|
use libc::{EBADRQC, input_absinfo, iovec, size_t};
|
||||||
use libc::{uinput_abs_setup, uinput_ff_erase, uinput_ff_upload, uinput_setup};
|
use libc::{uinput_abs_setup, uinput_ff_erase, uinput_ff_upload, uinput_setup};
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::ffi::CStr;
|
use std::ffi::CStr;
|
||||||
use std::io::Write;
|
|
||||||
use std::os::fd::AsRawFd;
|
use std::os::fd::AsRawFd;
|
||||||
use std::os::raw::{c_char, c_int, c_uint, c_void};
|
use std::os::raw::{c_char, c_int, c_uint, c_void};
|
||||||
use uinput_ioctls::*;
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
use crate::cuse_device::{get_vuinput_state, VuFileHandle};
|
use crate::cuse_device::{get_vuinput_state, VuFileHandle};
|
||||||
use crate::job_engine::JOB_DISPATCHER;
|
use crate::job_engine::JOB_DISPATCHER;
|
||||||
use crate::jobs::emit_udev_event_in_container_job::EmitUdevEventInContainerJob;
|
use crate::jobs::emit_udev_event_job::EmitUdevEventJob;
|
||||||
use crate::jobs::mknod_device_in_container_job::MknodDeviceInContainerJob;
|
use crate::jobs::mknod_device_job::MknodDeviceJob;
|
||||||
use crate::jobs::remove_from_container_job::RemoveFromContainerJob;
|
use crate::jobs::remove_device_job::RemoveDeviceJob;
|
||||||
use crate::process_tools::SELF_NAMESPACES;
|
use crate::process_tools::SELF_NAMESPACES;
|
||||||
use crate::{cuse_device::*, jobs};
|
use crate::{cuse_device::*, jobs};
|
||||||
|
|
||||||
|
|
@ -165,7 +164,7 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy()
|
CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy()
|
||||||
);
|
);
|
||||||
debug!("fh {}: syspath: {}", fh, sysname);
|
debug!("fh {}: syspath: {}", fh, sysname);
|
||||||
let devnode = fetch_device_node(&sysname).unwrap();
|
let (devname, devnode) = fetch_device_node(&sysname).unwrap();
|
||||||
debug!("fh {}: devnode: {}", fh, devnode);
|
debug!("fh {}: devnode: {}", fh, devnode);
|
||||||
let (major, minor) = fetch_major_minor(&devnode).unwrap();
|
let (major, minor) = fetch_major_minor(&devnode).unwrap();
|
||||||
debug!("fh {}: major: {} minor: {} ", fh, major, minor);
|
debug!("fh {}: major: {} minor: {} ", fh, major, minor);
|
||||||
|
|
@ -173,6 +172,7 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
major: major,
|
major: major,
|
||||||
minor: minor,
|
minor: minor,
|
||||||
syspath: sysname.clone(),
|
syspath: sysname.clone(),
|
||||||
|
devname: devname.clone(),
|
||||||
devnode: devnode.clone(),
|
devnode: devnode.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -182,9 +182,9 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
|
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
|
||||||
{
|
{
|
||||||
let mknod_job = MknodDeviceInContainerJob::new(
|
let mknod_job = MknodDeviceJob::new(
|
||||||
vuinput_state.requesting_process.clone(),
|
vuinput_state.requesting_process.clone(),
|
||||||
devnode.clone(),
|
devname.clone(),
|
||||||
sysname.clone(),
|
sysname.clone(),
|
||||||
major,
|
major,
|
||||||
minor,
|
minor,
|
||||||
|
|
@ -196,12 +196,12 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.dispatch(Box::new(mknod_job));
|
.dispatch(Box::new(mknod_job));
|
||||||
awaiter(&jobs::mknod_device_in_container_job::State::Finished);
|
awaiter(&jobs::mknod_device_job::State::Finished);
|
||||||
debug!("fh {}: mknod_device in container has been finished ", fh);
|
debug!("fh {}: mknod_device in container has been finished ", fh);
|
||||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||||
|
|
||||||
// we do not wait for the udev stuff
|
// we do not wait for the udev stuff
|
||||||
let emit_udev_event_job = EmitUdevEventInContainerJob::new(
|
let emit_udev_event_job = EmitUdevEventJob::new(
|
||||||
vuinput_state.requesting_process.clone(),
|
vuinput_state.requesting_process.clone(),
|
||||||
devnode.clone(),
|
devnode.clone(),
|
||||||
sysname.clone(),
|
sysname.clone(),
|
||||||
|
|
@ -230,9 +230,9 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
|
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
|
||||||
{
|
{
|
||||||
let input_device = input_device.unwrap();
|
let input_device = input_device.unwrap();
|
||||||
let remove_job = RemoveFromContainerJob::new(
|
let remove_job = RemoveDeviceJob::new(
|
||||||
vuinput_state.requesting_process.clone(),
|
vuinput_state.requesting_process.clone(),
|
||||||
input_device.devnode.clone(),
|
input_device.devname.clone(),
|
||||||
input_device.syspath.clone(),
|
input_device.syspath.clone(),
|
||||||
input_device.major,
|
input_device.major,
|
||||||
input_device.minor,
|
input_device.minor,
|
||||||
|
|
@ -244,7 +244,7 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.dispatch(Box::new(remove_job));
|
.dispatch(Box::new(remove_job));
|
||||||
awaiter(&jobs::remove_from_container_job::State::Finished);
|
awaiter(&jobs::remove_device_job::State::Finished);
|
||||||
debug!(
|
debug!(
|
||||||
"fh {}: removing dev-nodes from container has been finished ",
|
"fh {}: removing dev-nodes from container has been finished ",
|
||||||
fh
|
fh
|
||||||
|
|
@ -275,6 +275,10 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
//todo: i guess this needs to be reworked as this is variable size. i guess it is not reachable at all
|
//todo: i guess this needs to be reworked as this is variable size. i guess it is not reachable at all
|
||||||
debug!("fh {}: ioctl UI_ABS_SETUP", fh);
|
debug!("fh {}: ioctl UI_ABS_SETUP", fh);
|
||||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||||
|
|
||||||
|
let abs_setup_ptr = _in_buf as *const uinput_abs_setup;
|
||||||
|
ui_abs_setup(fd, abs_setup_ptr).unwrap();
|
||||||
|
|
||||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||||
}
|
}
|
||||||
UI_GET_SYSNAME_WITHOUT_SIZE => {
|
UI_GET_SYSNAME_WITHOUT_SIZE => {
|
||||||
|
|
@ -416,12 +420,12 @@ pub unsafe extern "C" fn vuinput_ioctl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fetch_device_node(path: &str) -> io::Result<String> {
|
pub fn fetch_device_node(path: &str) -> io::Result<(String, String)> {
|
||||||
for entry in fs::read_dir(path)? {
|
for entry in fs::read_dir(path)? {
|
||||||
let entry = entry?; // propagate per-entry errors
|
let entry = entry?; // propagate per-entry errors
|
||||||
if let Some(name) = entry.file_name().to_str() {
|
if let Some(name) = entry.file_name().to_str() {
|
||||||
if name.starts_with("event") {
|
if name.starts_with("event") {
|
||||||
return Ok(format!("/dev/input/{}", name));
|
return Ok((name.to_string(), format!("/dev/input/{}", name)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,16 @@
|
||||||
use ::cuse_lowlevel::*;
|
use ::cuse_lowlevel::*;
|
||||||
use libc::ENOENT;
|
use libc::ENOENT;
|
||||||
use libc::O_CLOEXEC;
|
use libc::O_CLOEXEC;
|
||||||
|
use libc::O_NONBLOCK;
|
||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
|
use std::os::fd::AsFd;
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use crate::cuse_device::evdev_write_watcher::EVDEV_WRITE_WATCHER;
|
||||||
use crate::cuse_device::*;
|
use crate::cuse_device::*;
|
||||||
use crate::process_tools::{get_requesting_process, Pid};
|
use crate::process_tools::{get_requesting_process, Pid};
|
||||||
|
|
||||||
|
|
@ -39,24 +42,33 @@ pub unsafe extern "C" fn vuinput_open(
|
||||||
debug!("fh {}: namespaces {}", fh, requesting_process);
|
debug!("fh {}: namespaces {}", fh, requesting_process);
|
||||||
// namespaces net:4026531840, uts:4026531838, ipc:4026531839, pid:4026531836, pid_for_children:4026531836, user:4026531837, mnt:4026531841, cgroup:4026531835, time:4026531834, time_for_children:4026531834
|
// namespaces net:4026531840, uts:4026531838, ipc:4026531839, pid:4026531836, pid_for_children:4026531836, user:4026531837, mnt:4026531841, cgroup:4026531835, time:4026531834, time_for_children:4026531834
|
||||||
(*_fi).fh = fh;
|
(*_fi).fh = fh;
|
||||||
// Open the path in read-only mode, returns `io::Result<File>`
|
// Open the path, returns `io::Result<File>`
|
||||||
let open_vuinput_result = OpenOptions::new()
|
let open_vuinput_result = OpenOptions::new()
|
||||||
.read(true)
|
.read(true)
|
||||||
.write(true)
|
.write(true)
|
||||||
//.custom_flags(O_NONBLOCK)
|
.custom_flags(O_NONBLOCK | O_CLOEXEC)
|
||||||
.custom_flags(O_CLOEXEC)
|
|
||||||
.open(Path::new("/dev/uinput"));
|
.open(Path::new("/dev/uinput"));
|
||||||
match open_vuinput_result {
|
match open_vuinput_result {
|
||||||
Ok(v) => {
|
Ok(v) => {
|
||||||
|
let vu_fh: VuFileHandle = VuFileHandle::Fh(fh);
|
||||||
insert_vuinput_state(
|
insert_vuinput_state(
|
||||||
&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap()),
|
&vu_fh,
|
||||||
VuInputState {
|
VuInputState {
|
||||||
file: v,
|
file: v,
|
||||||
requesting_process,
|
requesting_process,
|
||||||
input_device: None,
|
input_device: None,
|
||||||
|
keytracker: KeyTracker::new(),
|
||||||
|
poll: PollState::new(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
EVDEV_WRITE_WATCHER
|
||||||
|
.get()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.add_device(vu_fh)
|
||||||
|
.unwrap();
|
||||||
fuse_lowlevel::fuse_reply_open(_req, _fi);
|
fuse_lowlevel::fuse_reply_open(_req, _fi);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
59
vuinputd/src/cuse_device/vuinput_poll.rs
Normal file
59
vuinputd/src/cuse_device/vuinput_poll.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::cuse_device::*;
|
||||||
|
use crate::global_config::get_device_policy;
|
||||||
|
use ::cuse_lowlevel::*;
|
||||||
|
use libc::{__s32, __u16, input_event, POLLIN};
|
||||||
|
use libc::{off_t, size_t, EIO};
|
||||||
|
use libc::{uinput_abs_setup, uinput_setup};
|
||||||
|
use log::{debug, trace};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
use std::os::raw::c_char;
|
||||||
|
use std::ptr::NonNull;
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// https://github.com/libfuse/libfuse/blob/master/example/poll.c
|
||||||
|
// https://github.com/torvalds/linux/blob/f82b61de0f5dc58930fdb773b9e843573fcc374b/fs/fuse/file.c
|
||||||
|
|
||||||
|
// Note that poll in fuse blocks (because it calls fuse_simple_request, which is designed to block)
|
||||||
|
// until the handle is notified.
|
||||||
|
|
||||||
|
pub unsafe extern "C" fn vuinput_poll(
|
||||||
|
req: fuse_lowlevel::fuse_req_t,
|
||||||
|
fi: *mut fuse_lowlevel::fuse_file_info,
|
||||||
|
ph: *mut fuse_lowlevel::fuse_pollhandle,
|
||||||
|
) {
|
||||||
|
//fuse_lowlevel::fuse_reply_err(req, EIO);
|
||||||
|
//return;
|
||||||
|
|
||||||
|
let vuinput_state_mutex =
|
||||||
|
get_vuinput_state(&VuFileHandle::from_fuse_file_info(fi.as_ref().unwrap())).unwrap();
|
||||||
|
let mut vuinput_state = vuinput_state_mutex.lock().unwrap();
|
||||||
|
|
||||||
|
match vuinput_state.poll.pollphase {
|
||||||
|
PollPhase::Empty => {
|
||||||
|
if ph != std::ptr::null_mut() {
|
||||||
|
let ph = NonNull::<fuse_lowlevel::fuse_pollhandle>::new(ph);
|
||||||
|
vuinput_state.poll.set_waiter(ph.unwrap());
|
||||||
|
}
|
||||||
|
fuse_lowlevel::fuse_reply_poll(req, 0);
|
||||||
|
}
|
||||||
|
PollPhase::Readable => {
|
||||||
|
if ph != std::ptr::null_mut() {
|
||||||
|
fuse_lowlevel::fuse_lowlevel_notify_poll(ph);
|
||||||
|
fuse_lowlevel::fuse_pollhandle_destroy(ph);
|
||||||
|
}
|
||||||
|
fuse_lowlevel::fuse_reply_poll(req, POLLIN.try_into().unwrap());
|
||||||
|
}
|
||||||
|
PollPhase::Reading => {
|
||||||
|
if ph != std::ptr::null_mut() {
|
||||||
|
fuse_lowlevel::fuse_lowlevel_notify_poll(ph);
|
||||||
|
fuse_lowlevel::fuse_pollhandle_destroy(ph);
|
||||||
|
}
|
||||||
|
fuse_lowlevel::fuse_reply_poll(req, POLLIN.try_into().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
vuinputd/src/cuse_device/vuinput_read.rs
Normal file
76
vuinputd/src/cuse_device/vuinput_read.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::cuse_device::*;
|
||||||
|
use ::cuse_lowlevel::*;
|
||||||
|
use libc::{__s32, __u16, input_event, EAGAIN};
|
||||||
|
use libc::{off_t, size_t, EIO};
|
||||||
|
use log::{debug, trace};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::os::fd::AsRawFd;
|
||||||
|
use uinput_ioctls::*;
|
||||||
|
|
||||||
|
// TODO: compat-mode+ ensure sizeof(struct input_event)
|
||||||
|
pub unsafe extern "C" fn vuinput_read(
|
||||||
|
_req: fuse_lowlevel::fuse_req_t,
|
||||||
|
_size: size_t,
|
||||||
|
_off: off_t,
|
||||||
|
_fi: *mut fuse_lowlevel::fuse_file_info,
|
||||||
|
) {
|
||||||
|
assert!(
|
||||||
|
_off == 0,
|
||||||
|
"vuinput_read: offset needs to be 0 but is {}",
|
||||||
|
_off
|
||||||
|
);
|
||||||
|
//fuse_lowlevel::fuse_reply_err(_req, EIO);
|
||||||
|
//return;
|
||||||
|
|
||||||
|
let fh = &(*_fi).fh;
|
||||||
|
let vuinput_state_mutex =
|
||||||
|
get_vuinput_state(&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap())).unwrap();
|
||||||
|
let mut vuinput_state = vuinput_state_mutex.lock().unwrap();
|
||||||
|
|
||||||
|
const NORMAL_SIZE: usize = std::mem::size_of::<libc::input_event>();
|
||||||
|
let is_compat = vuinput_state.requesting_process.is_compat;
|
||||||
|
// TODO: ARM: && !compat_uses_64bit_time()
|
||||||
|
|
||||||
|
let mut buffer: [u8; 24] = [0; 24];
|
||||||
|
|
||||||
|
vuinput_state.poll.pollphase = PollPhase::Reading;
|
||||||
|
// read up to 24 bytes
|
||||||
|
//println!("vuinput_read: read");
|
||||||
|
let result = vuinput_state.file.read(&mut buffer);
|
||||||
|
|
||||||
|
//println!("vuinput_read: read finished");
|
||||||
|
match result {
|
||||||
|
Ok(NORMAL_SIZE) => {
|
||||||
|
if !is_compat {
|
||||||
|
let buffer = buffer.as_ptr() as *const i8;
|
||||||
|
fuse_lowlevel::fuse_reply_buf(_req, buffer, 24);
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"fh {}: error reading from uinput: not implemented yet for 32 bit users",
|
||||||
|
fh
|
||||||
|
);
|
||||||
|
// details how to implement it can be found in vuinput_write.rs
|
||||||
|
fuse_lowlevel::fuse_reply_err(_req, EIO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind() == io::ErrorKind::WouldBlock {
|
||||||
|
// EAGAIN / EWOULDBLOCK
|
||||||
|
//println!("Received EAGAIN: The read would block!");
|
||||||
|
vuinput_state.poll.pollphase = PollPhase::Empty;
|
||||||
|
fuse_lowlevel::fuse_reply_err(_req, EAGAIN);
|
||||||
|
} else {
|
||||||
|
debug!("fh {}: error reading from uinput: {e:?}", fh);
|
||||||
|
fuse_lowlevel::fuse_reply_err(_req, EIO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
debug!("fh {}: error reading from uinput: wrong size", fh);
|
||||||
|
fuse_lowlevel::fuse_reply_err(_req, EIO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,14 @@
|
||||||
//
|
//
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use crate::cuse_device::evdev_write_watcher::EVDEV_WRITE_WATCHER;
|
||||||
use crate::job_engine::JOB_DISPATCHER;
|
use crate::job_engine::JOB_DISPATCHER;
|
||||||
use crate::jobs::remove_from_container_job::RemoveFromContainerJob;
|
use crate::jobs::remove_device_job::RemoveDeviceJob;
|
||||||
use crate::process_tools::SELF_NAMESPACES;
|
use crate::process_tools::SELF_NAMESPACES;
|
||||||
use crate::{cuse_device::*, jobs};
|
use crate::{cuse_device::*, jobs};
|
||||||
use ::cuse_lowlevel::*;
|
use ::cuse_lowlevel::*;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
use std::os::fd::AsFd;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub unsafe extern "C" fn vuinput_release(
|
pub unsafe extern "C" fn vuinput_release(
|
||||||
|
|
@ -15,8 +17,8 @@ pub unsafe extern "C" fn vuinput_release(
|
||||||
_fi: *mut fuse_lowlevel::fuse_file_info,
|
_fi: *mut fuse_lowlevel::fuse_file_info,
|
||||||
) {
|
) {
|
||||||
let fh = &(*_fi).fh;
|
let fh = &(*_fi).fh;
|
||||||
let vuinput_state_mutex =
|
let vu_fh = VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap());
|
||||||
remove_vuinput_state(&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap())).unwrap();
|
let vuinput_state_mutex = remove_vuinput_state(&vu_fh).unwrap();
|
||||||
|
|
||||||
let mut vuinput_state = vuinput_state_mutex.lock().unwrap();
|
let mut vuinput_state = vuinput_state_mutex.lock().unwrap();
|
||||||
let input_device = vuinput_state.input_device.take();
|
let input_device = vuinput_state.input_device.take();
|
||||||
|
|
@ -32,9 +34,9 @@ pub unsafe extern "C" fn vuinput_release(
|
||||||
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
|
.equal_mnt_and_net(&vuinput_state.requesting_process.namespaces)
|
||||||
{
|
{
|
||||||
let input_device = input_device.unwrap();
|
let input_device = input_device.unwrap();
|
||||||
let remove_job = RemoveFromContainerJob::new(
|
let remove_job = RemoveDeviceJob::new(
|
||||||
vuinput_state.requesting_process.clone(),
|
vuinput_state.requesting_process.clone(),
|
||||||
input_device.devnode.clone(),
|
input_device.devname.clone(),
|
||||||
input_device.syspath.clone(),
|
input_device.syspath.clone(),
|
||||||
input_device.major,
|
input_device.major,
|
||||||
input_device.minor,
|
input_device.minor,
|
||||||
|
|
@ -46,9 +48,17 @@ pub unsafe extern "C" fn vuinput_release(
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.dispatch(Box::new(remove_job));
|
.dispatch(Box::new(remove_job));
|
||||||
awaiter(&jobs::remove_from_container_job::State::Finished);
|
awaiter(&jobs::remove_device_job::State::Finished);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EVDEV_WRITE_WATCHER
|
||||||
|
.get()
|
||||||
|
.unwrap()
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.remove_device(vuinput_state.file.as_fd())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
drop(vuinput_state);
|
drop(vuinput_state);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
use crate::cuse_device::*;
|
use crate::cuse_device::*;
|
||||||
|
use crate::global_config::get_device_policy;
|
||||||
use ::cuse_lowlevel::*;
|
use ::cuse_lowlevel::*;
|
||||||
use libc::{__s32, __u16, input_event};
|
use libc::{__s32, __u16, input_event};
|
||||||
use libc::{off_t, size_t, EIO};
|
use libc::{off_t, size_t, EIO};
|
||||||
|
|
@ -87,9 +88,15 @@ pub unsafe extern "C" fn vuinput_write(
|
||||||
let is_compat = vuinput_state.requesting_process.is_compat;
|
let is_compat = vuinput_state.requesting_process.is_compat;
|
||||||
// TODO: ARM: && !compat_uses_64bit_time()
|
// TODO: ARM: && !compat_uses_64bit_time()
|
||||||
|
|
||||||
|
let policy = get_device_policy();
|
||||||
|
|
||||||
if !is_compat {
|
if !is_compat {
|
||||||
while bytes + normal_size <= _size && result.is_ok() {
|
while bytes + normal_size <= _size && result.is_ok() {
|
||||||
result = vuinput_state.file.write(&slice[bytes..bytes + normal_size]);
|
let position = _buf.byte_add(bytes);
|
||||||
|
let input_event = position as *const input_event;
|
||||||
|
if device_policy::is_allowed(&mut vuinput_state.keytracker, policy, &*input_event) {
|
||||||
|
result = vuinput_state.file.write(&slice[bytes..bytes + normal_size]);
|
||||||
|
}
|
||||||
bytes += normal_size;
|
bytes += normal_size;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -99,7 +106,9 @@ pub unsafe extern "C" fn vuinput_write(
|
||||||
let normal = map_to_64_bit(&*compat);
|
let normal = map_to_64_bit(&*compat);
|
||||||
let normal_ptr = (&normal as *const libc::input_event) as *const u8;
|
let normal_ptr = (&normal as *const libc::input_event) as *const u8;
|
||||||
let slice = std::slice::from_raw_parts(normal_ptr, normal_size);
|
let slice = std::slice::from_raw_parts(normal_ptr, normal_size);
|
||||||
result = vuinput_state.file.write(&slice);
|
if device_policy::is_allowed(&mut vuinput_state.keytracker, policy, &normal) {
|
||||||
|
result = vuinput_state.file.write(&slice);
|
||||||
|
}
|
||||||
bytes += compat_size;
|
bytes += compat_size;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
122
vuinputd/src/global_config.rs
Normal file
122
vuinputd/src/global_config.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use clap::ValueEnum;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
|
use crate::container_runtime::ContainerRuntime;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GlobalConfig {
|
||||||
|
pub policy: DevicePolicy,
|
||||||
|
pub container_runtime: ContainerRuntime,
|
||||||
|
pub vudevname: String,
|
||||||
|
pub device_owner: DeviceOwner,
|
||||||
|
pub scope: Scope,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actual static variable. It starts empty and is set once in main().
|
||||||
|
pub static CONFIG: OnceLock<GlobalConfig> = OnceLock::new();
|
||||||
|
|
||||||
|
/// Defines the operational scope of the vuinputd instance
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
pub enum Scope {
|
||||||
|
#[default]
|
||||||
|
/// Watch all running containers of the configured runtime and manage lifecycle.
|
||||||
|
Multi,
|
||||||
|
/// Bind to a single named container. The name is passed directly to the engine's CLI/API.
|
||||||
|
Single(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device policy decides what events stay and what is filtered out.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum, Default)]
|
||||||
|
#[clap(rename_all = "kebab-case")] // This ensures StrictGamepad becomes "strict-gamepad"
|
||||||
|
pub enum DevicePolicy {
|
||||||
|
/// Allow all device capabilities
|
||||||
|
None,
|
||||||
|
#[default]
|
||||||
|
/// Default: Block SysRq
|
||||||
|
MuteSysRq,
|
||||||
|
/// Default: Allow keyboards/mice but block dangerous keys (SysRq, VT switching)
|
||||||
|
Sanitized,
|
||||||
|
/// Only allow Gamepad-like devices. Block mice and keyboards.
|
||||||
|
StrictGamepad,
|
||||||
|
}
|
||||||
|
/// Where to create runtime artifacts (device nodes + udev data)
|
||||||
|
/// Deprecated, use --container-runtime instead. Currently just maps to
|
||||||
|
/// --container-runtime
|
||||||
|
#[derive(Debug, Clone, ValueEnum, Default, PartialEq, Eq)]
|
||||||
|
pub enum Placement {
|
||||||
|
#[default]
|
||||||
|
/// Create inside the container
|
||||||
|
InContainer,
|
||||||
|
/// Create on the host (user is expected to bind-mount)
|
||||||
|
OnHost,
|
||||||
|
/// Do not create any artifacts (netlink message in container is unaffected)
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Device owner of the created devices
|
||||||
|
#[derive(Debug, Clone, ValueEnum, Default, PartialEq, Eq)]
|
||||||
|
pub enum DeviceOwner {
|
||||||
|
#[default]
|
||||||
|
/// Automatically derive useful settings (how might change in the future)
|
||||||
|
Auto,
|
||||||
|
/// Use the uid and gid of vuinputd
|
||||||
|
Vuinputd,
|
||||||
|
/// Same as dev folder in container
|
||||||
|
ContainerDevFolder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceOwner {
|
||||||
|
pub fn to_string_rep(&self) -> String {
|
||||||
|
match self {
|
||||||
|
DeviceOwner::Auto => "auto".to_string(),
|
||||||
|
DeviceOwner::Vuinputd => "vuinputd".to_string(),
|
||||||
|
DeviceOwner::ContainerDevFolder => "container-dev-folder".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize_global_config(
|
||||||
|
device_policy: &DevicePolicy,
|
||||||
|
container_runtime: &ContainerRuntime,
|
||||||
|
devname: &Option<String>,
|
||||||
|
device_owner: &DeviceOwner,
|
||||||
|
scope: &Scope,
|
||||||
|
) {
|
||||||
|
if CONFIG
|
||||||
|
.set(GlobalConfig {
|
||||||
|
policy: device_policy.clone(),
|
||||||
|
container_runtime: container_runtime.clone(),
|
||||||
|
vudevname: devname.clone().unwrap_or("vuinput".to_string()),
|
||||||
|
device_owner: device_owner.clone(),
|
||||||
|
scope: scope.clone(),
|
||||||
|
})
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
eprintln!("Failed to initialize global config");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device_policy<'a>() -> &'a DevicePolicy {
|
||||||
|
&CONFIG.get().unwrap().policy
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_container_runtime<'a>() -> &'a ContainerRuntime {
|
||||||
|
&CONFIG.get().unwrap().container_runtime
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vudevname<'a>() -> &'a String {
|
||||||
|
&CONFIG.get().unwrap().vudevname
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_device_owner<'a>() -> &'a DeviceOwner {
|
||||||
|
&CONFIG.get().unwrap().device_owner
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_scope<'a>() -> &'a Scope {
|
||||||
|
&CONFIG.get().unwrap().scope
|
||||||
|
}
|
||||||
80
vuinputd/src/input_realizer/host_fs.rs
Normal file
80
vuinputd/src/input_realizer/host_fs.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::io::{self, BufRead};
|
||||||
|
use std::{
|
||||||
|
fs::{self, File},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Ensure required dev-input, udev directories and files exist
|
||||||
|
pub fn ensure_host_fs_structure(path_prefix: &str) -> io::Result<()> {
|
||||||
|
let _ = check_if_path_allows_char_devs(&path_prefix);
|
||||||
|
let dev_input_dir = format!("{}/dev-input", path_prefix);
|
||||||
|
let dev_input_dir = Path::new(&dev_input_dir);
|
||||||
|
// Create directory like `mkdir -p`
|
||||||
|
if !dev_input_dir.exists() {
|
||||||
|
fs::create_dir_all(dev_input_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that this structure _must_ exist, before a service using libinput is run.
|
||||||
|
let data_dir = format!("{}/udev/data", path_prefix);
|
||||||
|
let data_dir = Path::new(&data_dir);
|
||||||
|
// Create directory like `mkdir -p`
|
||||||
|
if !data_dir.exists() {
|
||||||
|
fs::create_dir_all(data_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let control_file = format!("{}/udev/control", path_prefix);
|
||||||
|
let control_file = Path::new(&control_file);
|
||||||
|
// Ensure /run/udev/control exists, create empty if not
|
||||||
|
if !control_file.exists() {
|
||||||
|
File::create(control_file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// simple heuristic that checks whether path_prefix allows the hosting of character devices
|
||||||
|
/// This heuristic is not 100%, but a simple indicator
|
||||||
|
pub fn check_if_path_allows_char_devs(path: &str) -> io::Result<()> {
|
||||||
|
let file = File::open("/proc/self/mountinfo")?;
|
||||||
|
let reader = io::BufReader::new(file);
|
||||||
|
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line?;
|
||||||
|
|
||||||
|
let (left, _) = match line.split_once(" - ") {
|
||||||
|
Some(v) => v,
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fields: Vec<&str> = left.split_whitespace().collect();
|
||||||
|
|
||||||
|
// mount point is field 5
|
||||||
|
let mount_point = fields.get(4).copied().unwrap_or("");
|
||||||
|
// mount options are field 6
|
||||||
|
let options = fields.get(5).copied().unwrap_or("");
|
||||||
|
|
||||||
|
if mount_point.contains(path) {
|
||||||
|
if options.split(',').any(|o| o == "nodev") {
|
||||||
|
log::warn!(
|
||||||
|
"mount {} is present but mounted with nodev; device nodes will not work",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log::info!("mount {} is present and allows device nodes", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log::warn!(
|
||||||
|
"expected mount {} not found; user likely forgot to mount tmpfs with dev-option on it",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
8
vuinputd/src/input_realizer/mod.rs
Normal file
8
vuinputd/src/input_realizer/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
pub mod host_fs;
|
||||||
|
pub mod input_device;
|
||||||
|
pub mod netlink_message;
|
||||||
|
pub mod runtime_data;
|
||||||
|
|
@ -6,12 +6,16 @@ use std::fs::{self, File};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use log::{info, warn};
|
||||||
|
|
||||||
/// Ensure required udev directories and files exist
|
/// Ensure required udev directories and files exist
|
||||||
pub fn ensure_udev_structure() -> io::Result<()> {
|
pub fn ensure_udev_structure() -> io::Result<()> {
|
||||||
// TODO: this _must_ exist, before a service using libinput is run. The time of device creation might be too late
|
// Note that this structure _must_ exist, before a service using libinput is run. The time of device creation might be too late.
|
||||||
|
|
||||||
let data_dir = Path::new("/run/udev/data");
|
let data_dir = format!("/run/udev/data");
|
||||||
let control_file = Path::new("/run/udev/control");
|
let data_dir = Path::new(&data_dir);
|
||||||
|
let control_file = format!("/run/udev/control");
|
||||||
|
let control_file = Path::new(&control_file);
|
||||||
|
|
||||||
// Create directory like `mkdir -p`
|
// Create directory like `mkdir -p`
|
||||||
if !data_dir.exists() {
|
if !data_dir.exists() {
|
||||||
|
|
@ -20,6 +24,11 @@ pub fn ensure_udev_structure() -> io::Result<()> {
|
||||||
|
|
||||||
// Ensure /run/udev/control exists, create empty if not
|
// Ensure /run/udev/control exists, create empty if not
|
||||||
if !control_file.exists() {
|
if !control_file.exists() {
|
||||||
|
warn!(
|
||||||
|
"VUI-UDEV-001 — /run/udev/control/ not available. Keyboard or mouse might be unusable."
|
||||||
|
);
|
||||||
|
warn!("Visit https://github.com/joleuger/vuinputd/blob/main/docs/TROUBLESHOOTING.md for details");
|
||||||
|
info!("Creating file /run/udev/control anyway for subsequent runs.");
|
||||||
File::create(control_file)?;
|
File::create(control_file)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,7 +44,7 @@ pub fn ensure_udev_structure() -> io::Result<()> {
|
||||||
/// - remove all lines containing `seat_` references (G:, Q: lines)
|
/// - remove all lines containing `seat_` references (G:, Q: lines)
|
||||||
/// - replace ID_VUINPUT_* with ID_INPUT_*
|
/// - replace ID_VUINPUT_* with ID_INPUT_*
|
||||||
/// - write updated content to `/run/udev/data/c<major>:<minor>`
|
/// - write updated content to `/run/udev/data/c<major>:<minor>`
|
||||||
pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()> {
|
pub fn write_udev_data(path_prefix: &str, content: &str, major: u64, minor: u64) -> io::Result<()> {
|
||||||
let mut cleaned = String::new();
|
let mut cleaned = String::new();
|
||||||
|
|
||||||
for line in content.lines() {
|
for line in content.lines() {
|
||||||
|
|
@ -53,7 +62,7 @@ pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()>
|
||||||
cleaned.push('\n');
|
cleaned.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
let path = format!("/run/udev/data/c{}:{}", major, minor);
|
let path = format!("{}/udev/data/c{}:{}", path_prefix, major, minor);
|
||||||
let mut file = File::create(&path)?;
|
let mut file = File::create(&path)?;
|
||||||
file.write_all(cleaned.as_bytes())?;
|
file.write_all(cleaned.as_bytes())?;
|
||||||
|
|
||||||
|
|
@ -62,8 +71,8 @@ pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()>
|
||||||
|
|
||||||
/// Delete udev data for a given major/minor number
|
/// Delete udev data for a given major/minor number
|
||||||
/// - `major`, `minor` = device numbers
|
/// - `major`, `minor` = device numbers
|
||||||
pub fn delete_udev_data(major: u64, minor: u64) -> io::Result<()> {
|
pub fn delete_udev_data(path_prefix: &str, major: u64, minor: u64) -> io::Result<()> {
|
||||||
let path = format!("/run/udev/data/c{}:{}", major, minor);
|
let path = format!("{}/udev/data/c{}:{}", path_prefix, major, minor);
|
||||||
fs::remove_file(&path)?;
|
fs::remove_file(&path)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -14,7 +14,9 @@ use async_io::Timer;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::{action::Action, runtime_data::read_udev_data},
|
actions::action::Action,
|
||||||
|
global_config::get_container_runtime,
|
||||||
|
input_realizer::runtime_data,
|
||||||
job_engine::job::{Job, JobTarget},
|
job_engine::job::{Job, JobTarget},
|
||||||
jobs::monitor_udev_job::EVENT_STORE,
|
jobs::monitor_udev_job::EVENT_STORE,
|
||||||
process_tools::{self, await_process, Pid, RequestingProcess},
|
process_tools::{self, await_process, Pid, RequestingProcess},
|
||||||
|
|
@ -28,7 +30,7 @@ pub enum State {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct EmitUdevEventInContainerJob {
|
pub struct EmitUdevEventJob {
|
||||||
requesting_process: RequestingProcess,
|
requesting_process: RequestingProcess,
|
||||||
target: JobTarget,
|
target: JobTarget,
|
||||||
dev_path: String,
|
dev_path: String,
|
||||||
|
|
@ -38,7 +40,7 @@ pub struct EmitUdevEventInContainerJob {
|
||||||
sync_state: Arc<(Mutex<State>, Condvar)>,
|
sync_state: Arc<(Mutex<State>, Condvar)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmitUdevEventInContainerJob {
|
impl EmitUdevEventJob {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
requesting_process: RequestingProcess,
|
requesting_process: RequestingProcess,
|
||||||
dev_path: String,
|
dev_path: String,
|
||||||
|
|
@ -79,17 +81,17 @@ impl EmitUdevEventInContainerJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Job for EmitUdevEventInContainerJob {
|
impl Job for EmitUdevEventJob {
|
||||||
fn desc(&self) -> &str {
|
fn desc(&self) -> &str {
|
||||||
"emit udev event into container"
|
"emit udev event"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_after_cancellation(&self) -> bool {
|
fn execute_after_cancellation(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_task(self: &EmitUdevEventInContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
fn create_task(self: &EmitUdevEventJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
||||||
Box::pin(self.clone().inject_in_container())
|
Box::pin(self.clone().emit_udev_event())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn job_target(&self) -> JobTarget {
|
fn job_target(&self) -> JobTarget {
|
||||||
|
|
@ -97,8 +99,8 @@ impl Job for EmitUdevEventInContainerJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EmitUdevEventInContainerJob {
|
impl EmitUdevEventJob {
|
||||||
async fn inject_in_container(self) {
|
async fn emit_udev_event(self) {
|
||||||
// temporary hack that needs to be replaced. We try 50 times
|
// temporary hack that needs to be replaced. We try 50 times
|
||||||
// Should be: Wait for the device to be created, the runtime data to be written and the
|
// Should be: Wait for the device to be created, the runtime data to be written and the
|
||||||
// netlink message to be sent
|
// netlink message to be sent
|
||||||
|
|
@ -123,7 +125,7 @@ impl EmitUdevEventInContainerJob {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if runtime_data.is_none() {
|
if runtime_data.is_none() {
|
||||||
runtime_data = read_udev_data(self.major, self.minor).ok();
|
runtime_data = runtime_data::read_udev_data(self.major, self.minor).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
number_of_attempt += 1;
|
number_of_attempt += 1;
|
||||||
|
|
@ -143,20 +145,24 @@ impl EmitUdevEventInContainerJob {
|
||||||
|
|
||||||
let runtime_data = runtime_data.unwrap();
|
let runtime_data = runtime_data.unwrap();
|
||||||
let netlink_data = netlink_data.unwrap();
|
let netlink_data = netlink_data.unwrap();
|
||||||
let dev_path = self.dev_path.clone();
|
|
||||||
|
|
||||||
let emit_udev_event_action = Action::EmitUdevEvent {
|
let injector = get_container_runtime().injection_strategy();
|
||||||
netlink_message: netlink_data.clone(),
|
|
||||||
runtime_data: Some(runtime_data),
|
|
||||||
major: self.major,
|
|
||||||
minor: self.minor,
|
|
||||||
};
|
|
||||||
|
|
||||||
let child_pid =
|
injector
|
||||||
process_tools::start_action(emit_udev_event_action, &self.requesting_process)
|
.write_udev_runtime_data(
|
||||||
.expect("subprocess should work");
|
&self.requesting_process,
|
||||||
|
&runtime_data,
|
||||||
|
self.major,
|
||||||
|
self.minor,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
injector
|
||||||
|
.emit_netlink_message(&self.requesting_process, netlink_data)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let _exit_info = await_process(Pid::Pid(child_pid)).await.unwrap();
|
|
||||||
self.set_state(&State::Finished);
|
self.set_state(&State::Finished);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,20 +3,16 @@
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
future::Future,
|
future::Future,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{Arc, Condvar, Mutex},
|
sync::{Arc, Condvar, Mutex},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use async_io::Timer;
|
|
||||||
use log::debug;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::{action::Action, runtime_data::read_udev_data},
|
actions::action::Action,
|
||||||
|
global_config::{self, get_container_runtime, Placement},
|
||||||
|
input_realizer::input_device,
|
||||||
job_engine::job::{Job, JobTarget},
|
job_engine::job::{Job, JobTarget},
|
||||||
jobs::monitor_udev_job::EVENT_STORE,
|
|
||||||
process_tools::{self, await_process, Pid, RequestingProcess},
|
process_tools::{self, await_process, Pid, RequestingProcess},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -28,20 +24,20 @@ pub enum State {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct MknodDeviceInContainerJob {
|
pub struct MknodDeviceJob {
|
||||||
requesting_process: RequestingProcess,
|
requesting_process: RequestingProcess,
|
||||||
target: JobTarget,
|
target: JobTarget,
|
||||||
dev_path: String,
|
devname: String,
|
||||||
sys_path: String,
|
sys_path: String,
|
||||||
major: u64,
|
major: u64,
|
||||||
minor: u64,
|
minor: u64,
|
||||||
sync_state: Arc<(Mutex<State>, Condvar)>,
|
sync_state: Arc<(Mutex<State>, Condvar)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MknodDeviceInContainerJob {
|
impl MknodDeviceJob {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
requesting_process: RequestingProcess,
|
requesting_process: RequestingProcess,
|
||||||
dev_path: String,
|
devname: String,
|
||||||
sys_path: String,
|
sys_path: String,
|
||||||
major: u64,
|
major: u64,
|
||||||
minor: u64,
|
minor: u64,
|
||||||
|
|
@ -49,7 +45,7 @@ impl MknodDeviceInContainerJob {
|
||||||
Self {
|
Self {
|
||||||
requesting_process: requesting_process.clone(),
|
requesting_process: requesting_process.clone(),
|
||||||
target: JobTarget::Container(requesting_process),
|
target: JobTarget::Container(requesting_process),
|
||||||
dev_path: dev_path,
|
devname: devname,
|
||||||
sys_path: sys_path,
|
sys_path: sys_path,
|
||||||
major: major,
|
major: major,
|
||||||
minor: minor,
|
minor: minor,
|
||||||
|
|
@ -79,17 +75,17 @@ impl MknodDeviceInContainerJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Job for MknodDeviceInContainerJob {
|
impl Job for MknodDeviceJob {
|
||||||
fn desc(&self) -> &str {
|
fn desc(&self) -> &str {
|
||||||
"mknod input device in container"
|
"mknod input device"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_after_cancellation(&self) -> bool {
|
fn execute_after_cancellation(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_task(self: &MknodDeviceInContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
fn create_task(self: &MknodDeviceJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
||||||
Box::pin(self.clone().inject_in_container())
|
Box::pin(self.clone().mknod_device())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn job_target(&self) -> JobTarget {
|
fn job_target(&self) -> JobTarget {
|
||||||
|
|
@ -97,18 +93,20 @@ impl Job for MknodDeviceInContainerJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MknodDeviceInContainerJob {
|
impl MknodDeviceJob {
|
||||||
async fn inject_in_container(self) {
|
async fn mknod_device(self) {
|
||||||
let mknod_device_action = Action::MknodDevice {
|
let injector = get_container_runtime().injection_strategy();
|
||||||
path: self.dev_path.clone(),
|
|
||||||
major: self.major,
|
|
||||||
minor: self.minor,
|
|
||||||
};
|
|
||||||
|
|
||||||
let child_pid = process_tools::start_action(mknod_device_action, &self.requesting_process)
|
injector
|
||||||
.expect("subprocess should work");
|
.mknod_device_node(
|
||||||
|
&self.requesting_process,
|
||||||
|
&self.devname,
|
||||||
|
self.major,
|
||||||
|
self.minor,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let _exit_info = await_process(Pid::Pid(child_pid)).await.unwrap();
|
|
||||||
self.set_state(&State::Finished);
|
self.set_state(&State::Finished);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
pub mod emit_udev_event_in_container_job;
|
pub mod emit_udev_event_job;
|
||||||
pub mod mknod_device_in_container_job;
|
pub mod mknod_device_job;
|
||||||
pub mod monitor_udev_job;
|
pub mod monitor_udev_job;
|
||||||
pub mod remove_from_container_job;
|
pub mod remove_device_job;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ use log::debug;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
actions::action::Action,
|
actions::action::Action,
|
||||||
|
global_config::{self, get_container_runtime, Placement},
|
||||||
|
input_realizer::{input_device, runtime_data},
|
||||||
job_engine::job::{Job, JobTarget},
|
job_engine::job::{Job, JobTarget},
|
||||||
jobs::monitor_udev_job::EVENT_STORE,
|
jobs::monitor_udev_job::EVENT_STORE,
|
||||||
process_tools::{self, await_process, Pid, RequestingProcess},
|
process_tools::{self, await_process, Pid, RequestingProcess},
|
||||||
|
|
@ -25,20 +27,20 @@ pub enum State {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RemoveFromContainerJob {
|
pub struct RemoveDeviceJob {
|
||||||
requesting_process: RequestingProcess,
|
requesting_process: RequestingProcess,
|
||||||
target: JobTarget,
|
target: JobTarget,
|
||||||
dev_path: String,
|
dev_name: String,
|
||||||
sys_path: String,
|
sys_path: String,
|
||||||
major: u64,
|
major: u64,
|
||||||
minor: u64,
|
minor: u64,
|
||||||
sync_state: Arc<(Mutex<State>, Condvar)>,
|
sync_state: Arc<(Mutex<State>, Condvar)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoveFromContainerJob {
|
impl RemoveDeviceJob {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
requesting_process: RequestingProcess,
|
requesting_process: RequestingProcess,
|
||||||
dev_path: String,
|
dev_name: String,
|
||||||
sys_path: String,
|
sys_path: String,
|
||||||
major: u64,
|
major: u64,
|
||||||
minor: u64,
|
minor: u64,
|
||||||
|
|
@ -46,7 +48,7 @@ impl RemoveFromContainerJob {
|
||||||
Self {
|
Self {
|
||||||
requesting_process: requesting_process.clone(),
|
requesting_process: requesting_process.clone(),
|
||||||
target: JobTarget::Container(requesting_process),
|
target: JobTarget::Container(requesting_process),
|
||||||
dev_path: dev_path,
|
dev_name: dev_name,
|
||||||
sys_path: sys_path,
|
sys_path: sys_path,
|
||||||
major: major,
|
major: major,
|
||||||
minor: minor,
|
minor: minor,
|
||||||
|
|
@ -75,17 +77,17 @@ impl RemoveFromContainerJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Job for RemoveFromContainerJob {
|
impl Job for RemoveDeviceJob {
|
||||||
fn desc(&self) -> &str {
|
fn desc(&self) -> &str {
|
||||||
"Remove input device from container"
|
"Remove input device"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute_after_cancellation(&self) -> bool {
|
fn execute_after_cancellation(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_task(self: &RemoveFromContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
fn create_task(self: &RemoveDeviceJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
||||||
Box::pin(self.clone().remove_from_container())
|
Box::pin(self.clone().remove_device())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn job_target(&self) -> JobTarget {
|
fn job_target(&self) -> JobTarget {
|
||||||
|
|
@ -93,8 +95,8 @@ impl Job for RemoveFromContainerJob {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoveFromContainerJob {
|
impl RemoveDeviceJob {
|
||||||
async fn remove_from_container(self) {
|
async fn remove_device(self) {
|
||||||
self.set_state(&State::Started);
|
self.set_state(&State::Started);
|
||||||
|
|
||||||
let netlink_event = match EVENT_STORE
|
let netlink_event = match EVENT_STORE
|
||||||
|
|
@ -120,33 +122,31 @@ impl RemoveFromContainerJob {
|
||||||
let netlink_data = netlink_event.add_data;
|
let netlink_data = netlink_event.add_data;
|
||||||
|
|
||||||
let mut netlink_data = netlink_data.unwrap().clone();
|
let mut netlink_data = netlink_data.unwrap().clone();
|
||||||
let dev_path = self.dev_path.clone();
|
|
||||||
|
|
||||||
let _ = netlink_data.insert("ACTION".to_string(), "remove".to_string());
|
let _ = netlink_data.insert("ACTION".to_string(), "remove".to_string());
|
||||||
|
|
||||||
let remove_device_action = Action::RemoveDevice {
|
let injector = get_container_runtime().injection_strategy();
|
||||||
path: dev_path.clone(),
|
|
||||||
major: self.major,
|
|
||||||
minor: self.minor,
|
|
||||||
};
|
|
||||||
|
|
||||||
let child_pid_1 =
|
injector
|
||||||
process_tools::start_action(remove_device_action, &self.requesting_process)
|
.remove_device_node(
|
||||||
.expect("subprocess should work");
|
&self.requesting_process,
|
||||||
|
&self.dev_name,
|
||||||
|
self.major,
|
||||||
|
self.minor,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let emit_udev_event_action = Action::EmitUdevEvent {
|
injector
|
||||||
netlink_message: netlink_data.clone(),
|
.remove_udev_runtime_data(&self.requesting_process, self.major, self.minor)
|
||||||
runtime_data: None,
|
.await
|
||||||
major: self.major,
|
.unwrap();
|
||||||
minor: self.minor,
|
|
||||||
};
|
|
||||||
|
|
||||||
let child_pid_2 =
|
injector
|
||||||
process_tools::start_action(emit_udev_event_action, &self.requesting_process)
|
.emit_netlink_message(&self.requesting_process, netlink_data)
|
||||||
.expect("subprocess should work");
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let _exit_info = await_process(Pid::Pid(child_pid_1)).await;
|
|
||||||
let _exit_info = await_process(Pid::Pid(child_pid_2)).await;
|
|
||||||
self.set_state(&State::Finished);
|
self.set_state(&State::Finished);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,19 +16,28 @@
|
||||||
// distinguish between cleanup jobs that must not be cancelled and other jobs (especially background jobs)
|
// distinguish between cleanup jobs that must not be cancelled and other jobs (especially background jobs)
|
||||||
// naming: dev_path vs dev_node. I guess I mean the same.
|
// naming: dev_path vs dev_node. I guess I mean the same.
|
||||||
// Send warning, if udev monitor does not exist
|
// Send warning, if udev monitor does not exist
|
||||||
|
// Filter out Ctrl+Alt+Fx. "sysrq" keys or the low-level VT switching combos.
|
||||||
|
|
||||||
use ::cuse_lowlevel::*;
|
use ::cuse_lowlevel::*;
|
||||||
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
use base64::Engine as _;
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
use std::os::raw::c_char;
|
use std::os::raw::c_char;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::AtomicU64;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
pub mod cuse_device;
|
pub mod cuse_device;
|
||||||
|
|
||||||
|
use crate::container_runtime::ContainerRuntime;
|
||||||
|
use crate::cuse_device::evdev_write_watcher::{
|
||||||
|
initialize_evdev_write_watcher, EVDEV_WRITE_WATCHER,
|
||||||
|
};
|
||||||
use crate::cuse_device::state::{initialize_dedup_last_error, initialize_vuinput_state};
|
use crate::cuse_device::state::{initialize_dedup_last_error, initialize_vuinput_state};
|
||||||
use crate::cuse_device::vuinput_make_cuse_ops;
|
use crate::cuse_device::vuinput_make_cuse_ops;
|
||||||
use crate::cuse_device::vuinput_open::VUINPUT_COUNTER;
|
use crate::cuse_device::vuinput_open::VUINPUT_COUNTER;
|
||||||
|
use crate::global_config::{DeviceOwner, DevicePolicy, Placement, Scope};
|
||||||
use crate::jobs::monitor_udev_job::MonitorBackgroundLoop;
|
use crate::jobs::monitor_udev_job::MonitorBackgroundLoop;
|
||||||
|
|
||||||
pub mod process_tools;
|
pub mod process_tools;
|
||||||
|
|
@ -38,8 +47,12 @@ use crate::job_engine::{job::*, JOB_DISPATCHER};
|
||||||
use crate::process_tools::*;
|
use crate::process_tools::*;
|
||||||
|
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
|
pub mod input_realizer;
|
||||||
|
|
||||||
|
pub mod container_runtime;
|
||||||
|
pub mod global_config;
|
||||||
pub mod jobs;
|
pub mod jobs;
|
||||||
|
pub mod vt_tools;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
|
|
@ -65,50 +78,133 @@ struct Args {
|
||||||
#[arg(long, value_name = "JSON")]
|
#[arg(long, value_name = "JSON")]
|
||||||
pub action: Option<String>,
|
pub action: Option<String>,
|
||||||
|
|
||||||
/// Path to the target process's /proc/<pid>/ns directory used as namespace source.
|
/// Action to execute (base64-encoded JSON). Note that this excludes all other options.
|
||||||
|
#[arg(long = "action-base64", value_name = "BASE64")]
|
||||||
|
pub action_base64: Option<String>,
|
||||||
|
|
||||||
|
/// Process id that is used as the namespace source (e.g. 1234 is used to read the namespaces from /proc/1234/ns). Enters net and
|
||||||
|
/// mnt namespaces by default.
|
||||||
#[arg(
|
#[arg(
|
||||||
long = "target-namespace",
|
long = "target-pid",
|
||||||
value_name = "NS_PATH",
|
value_name = "PID",
|
||||||
help = "Path to /proc/<pid>/ns used as the namespace source (e.g. /proc/1234/ns or /proc/self/ns)"
|
help = "Process id that is used as the namespace source (e.g. 1234 is used to read the namespaces from /proc/1234/ns). Enters net and mnt namespaces by default."
|
||||||
)]
|
)]
|
||||||
pub target_namespace: Option<String>,
|
pub target_pid: Option<String>,
|
||||||
|
|
||||||
|
/// Enter also the user namespace. Used together with --target-pid.
|
||||||
|
#[arg(
|
||||||
|
long = "enter-user-namespace",
|
||||||
|
help = "Enter also the user namespace. Used together with --target-pid."
|
||||||
|
)]
|
||||||
|
pub enter_user_namespace: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long = "vt-guard",
|
||||||
|
help = "Prevent all keyboard input from reaching the VT by setting K_OFF on /dev/tty0.",
|
||||||
|
long_help = "Disable VT keyboard handling (K_OFF on /dev/tty0) to prevent uinput leakage.\n\
|
||||||
|
This disables all keyboard input on the virtual terminals, including physical keyboards.\n\
|
||||||
|
Loss of local access may require recovery via SSH or a rescue boot."
|
||||||
|
)]
|
||||||
|
pub vt_guard: bool,
|
||||||
|
|
||||||
|
/// Enforce a device policy on created devices
|
||||||
|
#[arg(long, value_enum, default_value_t)]
|
||||||
|
device_policy: DevicePolicy,
|
||||||
|
|
||||||
|
/// [DEPRECATED] Placement of device nodes and udev data. Maps to --container-runtime. Will be removed in a future version.
|
||||||
|
#[arg(long, value_enum)]
|
||||||
|
pub placement: Option<Placement>,
|
||||||
|
|
||||||
|
/// Owner of the created devices
|
||||||
|
#[arg(long = "device-owner", value_enum, default_value_t)]
|
||||||
|
pub device_owner: DeviceOwner,
|
||||||
|
|
||||||
|
/// Container runtime used for name resolution and lifecycle events
|
||||||
|
#[arg(long, default_value_t = ContainerRuntime::Auto, value_enum)]
|
||||||
|
pub container_runtime: ContainerRuntime,
|
||||||
|
|
||||||
|
/// Path to a custom strategy configuration file (used when runtime is 'custom-engine')
|
||||||
|
#[arg(long)]
|
||||||
|
pub strategy_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Bind to a single named container. If omitted, the daemon watches all running containers (Multi mode).
|
||||||
|
#[arg(long, value_name = "CONTAINER_NAME")]
|
||||||
|
pub target_container: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_args(args: &Args) -> Result<(), String> {
|
impl Args {
|
||||||
// action might only occur with target-namespace
|
pub fn get_scope(&self) -> Scope {
|
||||||
match (
|
match &self.target_container {
|
||||||
&args.major,
|
Some(name) => Scope::Single(name.clone()),
|
||||||
&args.minor,
|
None => Scope::Multi,
|
||||||
&args.devname,
|
|
||||||
&args.action,
|
|
||||||
&args.target_namespace,
|
|
||||||
) {
|
|
||||||
(None, None, None, Some(_), _) => {}
|
|
||||||
(_, _, _, None, None) => {}
|
|
||||||
_ => {
|
|
||||||
return Err("--action must not be used in combination with any other argument other than target-namespace".into());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// major/minor must appear together
|
pub fn resolve_runtime(&self) -> ContainerRuntime {
|
||||||
match (&args.major, &args.minor) {
|
if let Some(legacy_placement) = &self.placement {
|
||||||
(Some(_), Some(_)) | (None, None) => {}
|
return match legacy_placement {
|
||||||
_ => {
|
Placement::InContainer => ContainerRuntime::GenericPlacementInContainer,
|
||||||
return Err("--major and --minor must be specified together or not at all".into());
|
Placement::OnHost => ContainerRuntime::GenericPlacementOnHost,
|
||||||
|
Placement::None => ContainerRuntime::GenericSendNetlinkMessageOnly,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.container_runtime.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
// devname length constraint
|
fn validate_args(&self) -> Result<(), String> {
|
||||||
if let Some(devname) = &args.devname {
|
if self.placement.is_some() && self.container_runtime != ContainerRuntime::Auto {
|
||||||
if devname.len() >= DEVNAME_MAX_LEN {
|
return Err(
|
||||||
return Err(format!(
|
"Conflict: --placement and --container-runtime cannot be used together. \
|
||||||
"--devname must be shorter than {} bytes",
|
Please use only --container-runtime (the --placement flag is deprecated)."
|
||||||
DEVNAME_MAX_LEN
|
.into(),
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
let action: &Option<String> = match (&self.action, &self.action_base64) {
|
||||||
|
(None, None) => &None,
|
||||||
|
(None, Some(_)) => &self.action_base64,
|
||||||
|
(Some(_), None) => &self.action,
|
||||||
|
(Some(_), Some(_)) => {
|
||||||
|
return Err("--action and --action-base64 may not be used together".into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// action might only occur with target-pid
|
||||||
|
match (
|
||||||
|
self.major,
|
||||||
|
self.minor,
|
||||||
|
&self.devname,
|
||||||
|
action,
|
||||||
|
&self.target_pid,
|
||||||
|
) {
|
||||||
|
(None, None, None, Some(_), _) => {}
|
||||||
|
(_, _, _, None, None) => {}
|
||||||
|
_ => {
|
||||||
|
return Err("--action or --action-base64 must not be used in combination with any other argument other than target-pid".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// major/minor must appear together
|
||||||
|
match (self.major, self.minor) {
|
||||||
|
(Some(_), Some(_)) | (None, None) => {}
|
||||||
|
_ => {
|
||||||
|
return Err("--major and --minor must be specified together or not at all".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// devname length constraint
|
||||||
|
if let Some(devname) = &self.devname {
|
||||||
|
if devname.len() >= DEVNAME_MAX_LEN {
|
||||||
|
return Err(format!(
|
||||||
|
"--devname must be shorter than {} bytes",
|
||||||
|
DEVNAME_MAX_LEN
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
|
|
@ -119,21 +215,60 @@ fn main() -> std::io::Result<()> {
|
||||||
.next()
|
.next()
|
||||||
.expect("Couldn't retrieve program name");
|
.expect("Couldn't retrieve program name");
|
||||||
|
|
||||||
if let Err(e) = validate_args(&args) {
|
if let Err(e) = args.validate_args() {
|
||||||
eprintln!("Error: {e}");
|
eprintln!("Error: {e}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.action.is_some() {
|
let action = match (&args.action, &args.action_base64) {
|
||||||
if let Some(target_namespace) = args.target_namespace {
|
(Some(json), None) => Some(json.clone()),
|
||||||
process_tools::run_in_net_and_mnt_namespace(target_namespace.as_str()).unwrap();
|
(None, Some(b64)) => {
|
||||||
|
let decoded = BASE64_STANDARD
|
||||||
|
.decode(&b64)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
|
||||||
|
|
||||||
|
let decoded = String::from_utf8(decoded)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||||
|
Some(decoded)
|
||||||
}
|
}
|
||||||
let error_code = actions::handle_action::handle_cli_action(args.action.unwrap());
|
(None, None) => None,
|
||||||
|
_ => unreachable!("validate_args enforces mutual exclusion"),
|
||||||
|
};
|
||||||
|
|
||||||
|
if action.is_some() {
|
||||||
|
if let Some(target_pid) = args.target_pid {
|
||||||
|
process_tools::run_in_net_and_mnt_namespace(
|
||||||
|
target_pid.as_str(),
|
||||||
|
&args.device_owner,
|
||||||
|
args.enter_user_namespace,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let error_code = actions::handle_action::handle_cli_action(action.unwrap());
|
||||||
std::process::exit(error_code);
|
std::process::exit(error_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
check_permissions().expect("failed to read the capabilities of the vuinputd process");
|
if args.vt_guard {
|
||||||
|
vt_tools::mute_keyboard()?;
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
check_permissions().expect("failed to read the capabilities of the vuinputd process");
|
||||||
|
vt_tools::check_vt_status();
|
||||||
|
|
||||||
|
let container_runtime = args.resolve_runtime();
|
||||||
|
let scope = args.get_scope();
|
||||||
|
|
||||||
|
global_config::initialize_global_config(
|
||||||
|
&args.device_policy,
|
||||||
|
&container_runtime,
|
||||||
|
&args.devname,
|
||||||
|
&args.device_owner,
|
||||||
|
&scope,
|
||||||
|
);
|
||||||
|
initialize_evdev_write_watcher().expect(
|
||||||
|
"failed to initialize the watcher that watches for writes on the created evdev devices",
|
||||||
|
);
|
||||||
initialize_vuinput_state();
|
initialize_vuinput_state();
|
||||||
VUINPUT_COUNTER.set(AtomicU64::new(3)).expect(
|
VUINPUT_COUNTER.set(AtomicU64::new(3)).expect(
|
||||||
"failed to initialize the counter that provides the values of the CUSE file handles",
|
"failed to initialize the counter that provides the values of the CUSE file handles",
|
||||||
|
|
@ -142,7 +277,7 @@ fn main() -> std::io::Result<()> {
|
||||||
.set(Mutex::new(Dispatcher::new()))
|
.set(Mutex::new(Dispatcher::new()))
|
||||||
.expect("failed to initialize the job dispatcher");
|
.expect("failed to initialize the job dispatcher");
|
||||||
SELF_NAMESPACES
|
SELF_NAMESPACES
|
||||||
.set(get_namespace(Pid::SelfPid))
|
.set(get_self_namespace())
|
||||||
.expect("failed to retrieve the namespaces of the vuinputd process");
|
.expect("failed to retrieve the namespaces of the vuinputd process");
|
||||||
initialize_dedup_last_error();
|
initialize_dedup_last_error();
|
||||||
JOB_DISPATCHER
|
JOB_DISPATCHER
|
||||||
|
|
@ -160,6 +295,9 @@ fn main() -> std::io::Result<()> {
|
||||||
None => "vuinput",
|
None => "vuinput",
|
||||||
Some(devname) => devname,
|
Some(devname) => devname,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
container_runtime.initialize();
|
||||||
|
|
||||||
let vuinput_devicename = CString::new(format!("DEVNAME={}", vuinput_devicename)).unwrap();
|
let vuinput_devicename = CString::new(format!("DEVNAME={}", vuinput_devicename)).unwrap();
|
||||||
|
|
||||||
let mut dev_info_argv: Vec<*const c_char> = vec![
|
let mut dev_info_argv: Vec<*const c_char> = vec![
|
||||||
|
|
@ -215,5 +353,7 @@ fn main() -> std::io::Result<()> {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.wait_until_finished();
|
.wait_until_finished();
|
||||||
|
|
||||||
|
EVDEV_WRITE_WATCHER.get().unwrap().lock().unwrap().stop();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@
|
||||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
use async_io::Async;
|
use async_io::Async;
|
||||||
|
use base64::prelude::BASE64_STANDARD;
|
||||||
|
use base64::Engine as _;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::Read,
|
io::Read,
|
||||||
os::{
|
os::{
|
||||||
fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
|
||||||
unix::process::CommandExt,
|
unix::{fs::MetadataExt, process::CommandExt},
|
||||||
},
|
},
|
||||||
path::Path,
|
path::Path,
|
||||||
process::Command,
|
process::Command,
|
||||||
|
|
@ -19,23 +21,34 @@ use std::{
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crate::actions::action::Action;
|
use crate::{
|
||||||
|
actions::action::Action,
|
||||||
|
global_config::{get_device_owner, DeviceOwner},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod ns_fscreds;
|
||||||
|
|
||||||
pub static SELF_NAMESPACES: OnceLock<Namespaces> = OnceLock::new();
|
pub static SELF_NAMESPACES: OnceLock<Namespaces> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
|
||||||
pub enum Pid {
|
pub enum Pid {
|
||||||
SelfPid,
|
|
||||||
Pid(u32),
|
Pid(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pid {
|
impl Pid {
|
||||||
pub fn path(&self) -> String {
|
pub fn path(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
Pid::SelfPid => "/proc/self".to_string(),
|
|
||||||
Pid::Pid(pid_no) => format!("/proc/{}", pid_no),
|
Pid::Pid(pid_no) => format!("/proc/{}", pid_no),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn to_string_rep(&self) -> String {
|
||||||
|
let Pid::Pid(val) = self;
|
||||||
|
val.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enum PidOrSelf {
|
||||||
|
Pid(u32),
|
||||||
|
SelfPid,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||||
|
|
@ -78,15 +91,13 @@ pub fn is_compat_process(pid: Pid) -> Option<bool> {
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Pid::SelfPid => unreachable!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Rename to capture all relevant process information
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
|
||||||
pub struct RequestingProcess {
|
pub struct RequestingProcess {
|
||||||
pub nspath: String,
|
pub pid_requestor: Pid,
|
||||||
pub nsroot: String,
|
pub pid_requestor_root: Pid,
|
||||||
pub namespaces: Namespaces,
|
pub namespaces: Namespaces,
|
||||||
pub is_compat: bool,
|
pub is_compat: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -132,10 +143,19 @@ impl std::fmt::Display for RequestingProcess {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_self_namespace() -> Namespaces {
|
||||||
|
get_namespace_of_pid_or_self(PidOrSelf::SelfPid)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_namespace(pid: Pid) -> Namespaces {
|
pub fn get_namespace(pid: Pid) -> Namespaces {
|
||||||
let pid: String = match pid {
|
let Pid::Pid(pid) = pid;
|
||||||
Pid::Pid(pid) => pid.to_string(),
|
get_namespace_of_pid_or_self(PidOrSelf::Pid(pid))
|
||||||
Pid::SelfPid => "self".to_string(),
|
}
|
||||||
|
|
||||||
|
fn get_namespace_of_pid_or_self(pid_or_self: PidOrSelf) -> Namespaces {
|
||||||
|
let pid: String = match pid_or_self {
|
||||||
|
PidOrSelf::Pid(pid) => pid.to_string(),
|
||||||
|
PidOrSelf::SelfPid => "self".to_string(),
|
||||||
};
|
};
|
||||||
let nspath = format!("/proc/{}/ns", pid);
|
let nspath = format!("/proc/{}/ns", pid);
|
||||||
|
|
||||||
|
|
@ -179,7 +199,6 @@ pub fn get_namespace(pid: Pid) -> Namespaces {
|
||||||
|
|
||||||
fn get_ppid(pid: Pid) -> Option<Pid> {
|
fn get_ppid(pid: Pid) -> Option<Pid> {
|
||||||
let content = match pid {
|
let content = match pid {
|
||||||
Pid::SelfPid => fs::read_to_string(format!("/proc/self/status")).ok()?,
|
|
||||||
Pid::Pid(pid) => fs::read_to_string(format!("/proc/{}/status", pid)).ok()?,
|
Pid::Pid(pid) => fs::read_to_string(format!("/proc/{}/status", pid)).ok()?,
|
||||||
};
|
};
|
||||||
let ppid = content
|
let ppid = content
|
||||||
|
|
@ -237,64 +256,111 @@ pub fn get_requesting_process(pid: Pid) -> RequestingProcess {
|
||||||
pid.path()
|
pid.path()
|
||||||
);
|
);
|
||||||
|
|
||||||
let nspath = format!("{}/ns", pid.path());
|
|
||||||
let nsroot = format!("{}/ns", ppid.path());
|
|
||||||
RequestingProcess {
|
RequestingProcess {
|
||||||
nspath: nspath,
|
pid_requestor: pid,
|
||||||
nsroot: nsroot,
|
pid_requestor_root: ppid,
|
||||||
namespaces: nsinodes,
|
namespaces: nsinodes,
|
||||||
is_compat: is_compat,
|
is_compat: is_compat,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Pid::SelfPid => {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn print_debug_string(action: &str, ns: &RequestingProcess) {
|
||||||
|
let action_base64 = BASE64_STANDARD.encode(action);
|
||||||
|
let mut debugstring = String::new();
|
||||||
|
debugstring.push_str("In case you need to debug the system calls, call `strace vuinputd");
|
||||||
|
debugstring.push_str(" --target-pid ");
|
||||||
|
debugstring.push_str(&ns.pid_requestor_root.to_string_rep());
|
||||||
|
debugstring.push_str(" --action-base64 ");
|
||||||
|
debugstring.push_str(action_base64.as_str());
|
||||||
|
debugstring.push_str(" --device-owner ");
|
||||||
|
debugstring.push_str(get_device_owner().to_string_rep().as_str());
|
||||||
|
debugstring.push_str("`");
|
||||||
|
debug!("{}", debugstring);
|
||||||
|
}
|
||||||
|
|
||||||
/// Runs a function inside the given network and mount namespaces.
|
/// Runs a function inside the given network and mount namespaces.
|
||||||
/// Returns the child PID so the caller can `waitpid` on it.
|
/// Returns the child PID so the caller can `waitpid` on it.
|
||||||
pub fn start_action(action: Action, ns: &RequestingProcess) -> anyhow::Result<u32> {
|
pub fn start_action(
|
||||||
|
action: Action,
|
||||||
|
ns: &RequestingProcess,
|
||||||
|
enter_user_ns: bool,
|
||||||
|
) -> anyhow::Result<u32> {
|
||||||
let action_json = serde_json::to_string(&action).unwrap();
|
let action_json = serde_json::to_string(&action).unwrap();
|
||||||
|
print_debug_string(&action_json, &ns);
|
||||||
|
|
||||||
|
let device_owner = get_device_owner().to_string_rep();
|
||||||
|
|
||||||
let child = unsafe {
|
let child = unsafe {
|
||||||
Command::new("/proc/self/exe")
|
let mut cmd = Command::new("/proc/self/exe");
|
||||||
.args([
|
cmd.args([
|
||||||
"--action",
|
"--action",
|
||||||
action_json.as_str(),
|
action_json.as_str(),
|
||||||
"--target-namespace",
|
"--target-pid",
|
||||||
ns.nsroot.as_str(),
|
ns.pid_requestor_root.to_string_rep().as_str(),
|
||||||
])
|
"--device-owner",
|
||||||
.pre_exec(|| {
|
device_owner.as_str(),
|
||||||
// Last resort, if the parent just is killed.
|
]);
|
||||||
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
|
if enter_user_ns {
|
||||||
Ok(())
|
cmd.arg("--enter-user-namespace");
|
||||||
})
|
}
|
||||||
.spawn()
|
cmd.pre_exec(|| {
|
||||||
.expect("failed to start vuinputd")
|
// Last resort, if the parent just is killed.
|
||||||
|
libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL);
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.spawn()
|
||||||
|
.expect("failed to start vuinputd")
|
||||||
};
|
};
|
||||||
|
|
||||||
Result::Ok(child.id())
|
Result::Ok(child.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_in_net_and_mnt_namespace(target_namespace: &str) -> anyhow::Result<()> {
|
pub fn run_in_net_and_mnt_namespace(
|
||||||
|
target_pid: &str,
|
||||||
|
device_owner: &DeviceOwner,
|
||||||
|
enter_user_ns: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
debug!(
|
debug!(
|
||||||
"Entering namespaces of process {}. We assume this is the root process of the container.",
|
"Entering namespaces of process {}. We assume this is the root process of the container.",
|
||||||
target_namespace
|
target_pid
|
||||||
);
|
);
|
||||||
|
|
||||||
let path: &Path = Path::new(target_namespace);
|
let fs_uid_gid = if *device_owner == DeviceOwner::ContainerDevFolder {
|
||||||
|
let pid: u32 = target_pid.trim().parse()?;
|
||||||
|
let pid = Pid::Pid(pid);
|
||||||
|
let fs_uid = ns_fscreds::get_uid_in_container(pid, 0)?;
|
||||||
|
let fs_gid = ns_fscreds::get_gid_in_container(pid, 0)?;
|
||||||
|
Some((fs_uid, fs_gid))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let nspath = format!("/proc/{}/ns", target_pid);
|
||||||
|
let path: &Path = Path::new(&nspath);
|
||||||
if !fs::exists(path).unwrap() {
|
if !fs::exists(path).unwrap() {
|
||||||
return Err(anyhow!("the root process of the container whose namespaces we want to enter does not exist anymore"));
|
return Err(anyhow!("the root process of the container whose namespaces we want to enter does not exist anymore"));
|
||||||
}
|
}
|
||||||
let net = File::open(target_namespace.to_string() + "/net")?;
|
let user = File::open(nspath.to_string() + "/user")?;
|
||||||
let mnt = File::open(target_namespace.to_string() + "/mnt")?;
|
let net = File::open(nspath.to_string() + "/net")?;
|
||||||
|
let mnt = File::open(nspath.to_string() + "/mnt")?;
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// enter namespaces
|
// enter namespaces
|
||||||
|
if enter_user_ns {
|
||||||
|
libc::setns(user.as_raw_fd(), libc::CLONE_NEWUSER);
|
||||||
|
libc::setresgid(0, 0, 0);
|
||||||
|
libc::setresuid(0, 0, 0);
|
||||||
|
}
|
||||||
libc::setns(net.as_raw_fd(), libc::CLONE_NEWNET);
|
libc::setns(net.as_raw_fd(), libc::CLONE_NEWNET);
|
||||||
libc::setns(mnt.as_raw_fd(), libc::CLONE_NEWNS);
|
libc::setns(mnt.as_raw_fd(), libc::CLONE_NEWNS);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some((fs_uid, fs_gid)) = fs_uid_gid {
|
||||||
|
ns_fscreds::acquire_uid_and_gid(fs_uid, fs_gid)?;
|
||||||
|
}
|
||||||
|
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -323,9 +389,6 @@ pub async fn await_process(pid: Pid) -> io::Result<i32> {
|
||||||
Ok(si.si_status())
|
Ok(si.si_status())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Pid::SelfPid => {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
141
vuinputd/src/process_tools/ns_fscreds.rs
Normal file
141
vuinputd/src/process_tools/ns_fscreds.rs
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{self, BufRead};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::process_tools::Pid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
struct IdMapEntry {
|
||||||
|
pub inside_start: u64,
|
||||||
|
pub outside_start: u64,
|
||||||
|
pub length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_id_map(pid: u32, map_type: &str) -> io::Result<Vec<IdMapEntry>> {
|
||||||
|
let path = format!("/proc/{}/{}", pid, map_type);
|
||||||
|
let file = fs::File::open(&path)?;
|
||||||
|
let reader = io::BufReader::new(file);
|
||||||
|
|
||||||
|
Ok(reader
|
||||||
|
.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let line = line.ok()?;
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let inside_start = parts.next()?.parse().ok()?;
|
||||||
|
let outside_start = parts.next()?.parse().ok()?;
|
||||||
|
let length = parts.next()?.parse().ok()?;
|
||||||
|
Some(IdMapEntry {
|
||||||
|
inside_start,
|
||||||
|
outside_start,
|
||||||
|
length,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_host_id(entries: &[IdMapEntry], inside_id: u64) -> Option<u64> {
|
||||||
|
entries.iter().find_map(|e| {
|
||||||
|
if inside_id >= e.inside_start && inside_id < e.inside_start + e.length {
|
||||||
|
Some(e.outside_start + (inside_id - e.inside_start))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the host UID that corresponds to `ns_uid` (e.g. 0) inside the container.
|
||||||
|
pub fn get_uid_in_container(pid: Pid, ns_uid: u64) -> anyhow::Result<u32> {
|
||||||
|
let Pid::Pid(pid) = pid;
|
||||||
|
let entries = parse_id_map(pid, "uid_map")?;
|
||||||
|
to_host_id(&entries, ns_uid)
|
||||||
|
.map(|id| id as u32)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("uid {} is not mapped in /proc/{}/uid_map", ns_uid, pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the host GID that corresponds to `ns_gid` (e.g. 0) inside the container.
|
||||||
|
pub fn get_gid_in_container(pid: Pid, ns_gid: u64) -> anyhow::Result<u32> {
|
||||||
|
let Pid::Pid(pid) = pid;
|
||||||
|
let entries = parse_id_map(pid, "gid_map")?;
|
||||||
|
to_host_id(&entries, ns_gid)
|
||||||
|
.map(|id| id as u32)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("gid {} is not mapped in /proc/{}/gid_map", ns_gid, pid))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch filesystem UID/GID to the given host IDs.
|
||||||
|
/// GID must be set before UID — dropping UID=0 removes the ability to change GID.
|
||||||
|
pub fn acquire_uid_and_gid(target_uid: u32, target_gid: u32) -> anyhow::Result<()> {
|
||||||
|
unsafe {
|
||||||
|
libc::setfsgid(target_gid as libc::gid_t);
|
||||||
|
libc::setfsuid(target_uid as libc::uid_t);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Switch filesystem UID/GID to match whatever owner the given path has on the host.
|
||||||
|
pub fn acquire_uid_and_gid_of_path(path: &str) -> anyhow::Result<()> {
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
|
let metadata = fs::metadata(Path::new(path))?;
|
||||||
|
let target_uid = metadata.uid();
|
||||||
|
let target_gid = metadata.gid();
|
||||||
|
acquire_uid_and_gid(target_uid, target_gid)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn parse_str(s: &str) -> Vec<IdMapEntry> {
|
||||||
|
s.lines()
|
||||||
|
.filter_map(|line| {
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let inside_start = parts.next()?.parse().ok()?;
|
||||||
|
let outside_start = parts.next()?.parse().ok()?;
|
||||||
|
let length = parts.next()?.parse().ok()?;
|
||||||
|
Some(IdMapEntry {
|
||||||
|
inside_start,
|
||||||
|
outside_start,
|
||||||
|
length,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uid0_in_rootless_container_maps_to_host_uid() {
|
||||||
|
// Typical rootless setup: container root (0) → host uid 100000
|
||||||
|
let map = parse_str("0 100000 65536");
|
||||||
|
assert_eq!(to_host_id(&map, 0), Some(100000));
|
||||||
|
assert_eq!(to_host_id(&map, 1), Some(100001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uid_outside_range_returns_none() {
|
||||||
|
let map = parse_str("0 100000 65536");
|
||||||
|
assert_eq!(to_host_id(&map, 65536), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn identity_map_returns_same_id() {
|
||||||
|
// Process not in a user namespace: 0 0 4294967295
|
||||||
|
let map = parse_str("0 0 4294967295");
|
||||||
|
assert_eq!(to_host_id(&map, 0), Some(0));
|
||||||
|
assert_eq!(to_host_id(&map, 1000), Some(1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn proc_self_uid_is_parseable() {
|
||||||
|
let uid = unsafe { libc::getuid() } as u64;
|
||||||
|
let entries =
|
||||||
|
parse_id_map(std::process::id(), "uid_map").expect("failed to read /proc/self/uid_map");
|
||||||
|
assert!(
|
||||||
|
to_host_id(&entries, uid).is_some(),
|
||||||
|
"current uid {} not found in uid_map",
|
||||||
|
uid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
vuinputd/src/vt_tools.rs
Normal file
64
vuinputd/src/vt_tools.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
//
|
||||||
|
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||||
|
|
||||||
|
use log::{error, info, warn};
|
||||||
|
use std::fs::OpenOptions;
|
||||||
|
use std::io;
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
|
||||||
|
use libc::ioctl;
|
||||||
|
|
||||||
|
// see include/uapi/linux/kd.h
|
||||||
|
const KDSKBMODE: u64 = 0x4B45; // sets current keyboard mode
|
||||||
|
const KDGKBMODE: u64 = 0x4B44; // gets current keyboard mode
|
||||||
|
|
||||||
|
const K_OFF: u64 = 0x04;
|
||||||
|
|
||||||
|
pub fn check_vt_status() {
|
||||||
|
match OpenOptions::new().read(true).open("/dev/tty1") {
|
||||||
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||||
|
info!("/dev/tty1 not present — no VT-related input problem");
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to open /dev/tty1: {}", err);
|
||||||
|
}
|
||||||
|
Ok(tty) => {
|
||||||
|
let fd = tty.as_raw_fd();
|
||||||
|
let mut mode: u64 = 0;
|
||||||
|
|
||||||
|
let rc = unsafe { ioctl(fd, KDGKBMODE, &mut mode) };
|
||||||
|
if rc < 0 {
|
||||||
|
error!("KDGKBMODE ioctl failed: {}", io::Error::last_os_error());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode == K_OFF {
|
||||||
|
info!("tty1 keyboard mode is K_OFF — VT input is disabled");
|
||||||
|
} else {
|
||||||
|
warn!(
|
||||||
|
"tty1 keyboard mode is active (mode={}) — VT may consume input",
|
||||||
|
mode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mute_keyboard() -> std::io::Result<()> {
|
||||||
|
// 1. Open the TTY (usually TTY1 for a DM)
|
||||||
|
let file = OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open("/dev/tty1")?;
|
||||||
|
let fd = file.as_raw_fd();
|
||||||
|
|
||||||
|
// 2. Mute the keyboard
|
||||||
|
unsafe {
|
||||||
|
if libc::ioctl(fd, KDSKBMODE, K_OFF) < 0 {
|
||||||
|
panic!("Failed to mute keyboard. Are you root?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Keyboard muted.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,11 @@ After=systemd-udevd.service
|
||||||
Requires=systemd-udevd.service
|
Requires=systemd-udevd.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
|
# The Flag --vt-guard disables VT keyboard handling (K_OFF on /dev/tty0) to prevent uinput leakage.
|
||||||
|
# This disables all keyboard input on the virtual terminals, including physical keyboards.
|
||||||
|
# Loss of local access may require recovery via SSH or a rescue boot.
|
||||||
|
#ExecStartPre=/usr/local/bin/vuinputd --vt-guard
|
||||||
|
|
||||||
# major 120 is reserved for local/experimental use. I picked minor 414795 with the use
|
# major 120 is reserved for local/experimental use. I picked minor 414795 with the use
|
||||||
# of a random number generator to omit conflicts.
|
# of a random number generator to omit conflicts.
|
||||||
ExecStart=/usr/local/bin/vuinputd --major 120 --minor 414795
|
ExecStart=/usr/local/bin/vuinputd --major 120 --minor 414795
|
||||||
|
|
@ -16,6 +21,8 @@ Restart=on-failure
|
||||||
# we need the permission to create all sorts of devices
|
# we need the permission to create all sorts of devices
|
||||||
DeviceAllow=char-* rwm
|
DeviceAllow=char-* rwm
|
||||||
|
|
||||||
|
# Enable debug logs during preproduction phase
|
||||||
|
Environment=RUST_LOG=debug
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|
|
||||||
15
vuinputd/udev/install_udev.sh
Normal file
15
vuinputd/udev/install_udev.sh
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
RULES_DIR=/usr/lib/udev/rules.d
|
||||||
|
HWDB_DIR=/usr/lib/udev/hwdb.d
|
||||||
|
|
||||||
|
install -D -m 0644 90-vuinputd-protect.rules \
|
||||||
|
"$RULES_DIR/90-vuinputd-protect.rules"
|
||||||
|
|
||||||
|
install -D -m 0644 90-vuinputd.hwdb \
|
||||||
|
"$HWDB_DIR/90-vuinputd.hwdb"
|
||||||
|
|
||||||
|
systemd-hwdb update
|
||||||
|
udevadm control --reload-rules
|
||||||
|
udevadm trigger
|
||||||
Loading…
Add table
Add a link
Reference in a new issue