Add runtime tools and Red Bear service wiring
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -26,6 +26,7 @@ target
|
||||
wget-log
|
||||
/sysroot/
|
||||
local/docs/*.log
|
||||
local/docs/qt6-build-log.txt
|
||||
|
||||
# Explicitly track our OWN source code (recipes with path="source" where we wrote the code)
|
||||
# Only recipes under these categories contain our hand-written source:
|
||||
|
||||
Generated
+1
-1
@@ -855,7 +855,7 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rbos_cookbook"
|
||||
name = "redbear_cookbook"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi-to-tui",
|
||||
|
||||
@@ -13,8 +13,63 @@ filesystem_size = 10240
|
||||
# Red Bear OS branding (os-release, hostname, motd)
|
||||
redbear-release = {}
|
||||
|
||||
# Native Redox PCI/USB listing tools (lspci, lsusb)
|
||||
redbear-hwutils = {}
|
||||
|
||||
# Redox-native netctl compatibility command
|
||||
redbear-netctl = {}
|
||||
|
||||
# Terminal file manager (Midnight Commander port)
|
||||
mc = {}
|
||||
|
||||
# Package builder (cub -S/-B/-G CLI)
|
||||
cub = {}
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples/wired-dhcp"
|
||||
data = """
|
||||
Description='Red Bear wired DHCP profile'
|
||||
Interface=eth0
|
||||
Connection=ethernet
|
||||
IP=dhcp
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples/wired-static"
|
||||
data = """
|
||||
Description='Red Bear wired static profile'
|
||||
Interface=eth0
|
||||
Connection=ethernet
|
||||
IP=static
|
||||
Address=('192.168.1.10/24')
|
||||
Gateway='192.168.1.1'
|
||||
DNS=('1.1.1.1')
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/12_netctl.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Network profile application"
|
||||
requires_weak = [
|
||||
"10_smolnetd.service",
|
||||
"10_dhcpd.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "netctl"
|
||||
args = ["--boot"]
|
||||
type = "oneshot"
|
||||
"""
|
||||
|
||||
+229
-10
@@ -1,22 +1,29 @@
|
||||
# Red Bear OS Full Configuration
|
||||
# Complete desktop + RBOS branding + ext4 + input drivers
|
||||
# Note: GPU drivers (redox-driver-sys, linux-kpi, redox-drm, amdgpu)
|
||||
# are not included because they need custom build templates.
|
||||
# Build them separately with: ./local/scripts/build-amd.sh
|
||||
#
|
||||
# Desktop + RBOS branding + ext4 + input + Wayland + Qt6
|
||||
# Build: make all CONFIG_NAME=redbear-full
|
||||
# Live: make live CONFIG_NAME=redbear-full
|
||||
#
|
||||
# GPU driver stack (redox-driver-sys, linux-kpi, redox-drm, amdgpu): WIP crates
|
||||
# need custom template (library-only, cargo install fails). Re-enable when fixed.
|
||||
# KDE Frameworks + KWin: depend on qtdeclarative/qtwayland. Re-enable when ported.
|
||||
# libinput/libevdev: WIP meson builds, not yet validated. Re-enable when tested.
|
||||
# seatd: now builds; DRM lease/runtime validation is still open before enabling broadly.
|
||||
|
||||
include = ["desktop.toml"]
|
||||
|
||||
[general]
|
||||
# 2GB filesystem — plenty for full desktop + headroom
|
||||
filesystem_size = 2048
|
||||
|
||||
[packages]
|
||||
# Red Bear OS branding (os-release, hostname, motd)
|
||||
redbear-release = {}
|
||||
|
||||
# Native Redox PCI/USB listing tools (lspci, lsusb)
|
||||
redbear-hwutils = {}
|
||||
|
||||
# Redox-native netctl compatibility command
|
||||
redbear-netctl = {}
|
||||
|
||||
# Terminal file manager (Midnight Commander port)
|
||||
mc = {}
|
||||
|
||||
@@ -30,12 +37,224 @@ firmware-loader = {}
|
||||
evdevd = {}
|
||||
udev-shim = {}
|
||||
|
||||
# Package builder (cub -S/-B/-G CLI)
|
||||
cub = {}
|
||||
# Diagnostic tool
|
||||
redbear-info = {}
|
||||
|
||||
# RBOS meta-package (dependencies, default config)
|
||||
redbear-meta = {}
|
||||
# Process monitor
|
||||
htop = {}
|
||||
|
||||
# Wayland protocol
|
||||
libwayland = {}
|
||||
|
||||
# Keyboard support
|
||||
libxkbcommon = {}
|
||||
|
||||
# Qt6 base (Core+Concurrent+Xml+Gui+Widgets, software rendering)
|
||||
qtbase = {}
|
||||
|
||||
# RBOS meta-package — temporarily disabled (depends on GPU stack via redox-driver-sys)
|
||||
# redbear-meta = {}
|
||||
|
||||
# Workaround: bash fails to cross-compile (upstream mkbuiltins.c issue)
|
||||
# ion (from minimal) is the default shell anyway
|
||||
bash = "ignore"
|
||||
|
||||
# Firmware directory for AMD/Intel GPU blobs
|
||||
[[files]]
|
||||
path = "/usr/firmware/amdgpu"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
# ── Neutralize broken legacy scripts from upstream configs ─────────
|
||||
#
|
||||
# base.toml and desktop-minimal.toml ship legacy init scripts that use
|
||||
# "notify <service>" — but "notify" is NOT a keyword in the legacy script
|
||||
# parser (only "requires_weak" and "nowait" exist). This causes init to
|
||||
# try executing a binary called "notify" which doesn't exist.
|
||||
#
|
||||
# The base source package already ships proper .service/.target files in
|
||||
# its init.d/ directory. These get installed to /usr/lib/init.d/ during
|
||||
# the build. We just need to neutralize the conflicting legacy scripts
|
||||
# so only the proper .service/.target files remain active.
|
||||
#
|
||||
# Override each broken legacy script with an empty file. Init will still
|
||||
# find the .target and .service files from the base package.
|
||||
|
||||
# base.toml: "notify ipcd", "notify ptyd", "nowait sudo --daemon"
|
||||
# → Replaced by 00_base.target → 00_ipcd.service + 00_ptyd.service + 00_sudo.service
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_base"
|
||||
data = ""
|
||||
|
||||
# base.toml: "pcid-spawner" (blocking, no keyword)
|
||||
# → Replaced by 00_pcid-spawner.service (oneshot)
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_drivers"
|
||||
data = ""
|
||||
|
||||
# base.toml: "notify smolnetd", "nowait dhcpd"
|
||||
# → Replaced by 10_net.target → 10_smolnetd.service + 10_dhcpd.service
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/10_net"
|
||||
data = ""
|
||||
|
||||
# desktop-minimal.toml: "notify audiod", "nowait orbital orblogin launcher"
|
||||
# → audiod: 20_audiod.service (from base package, type=scheme)
|
||||
# → orbital: needs its own .service file since base doesn't ship one
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/20_orbital"
|
||||
data = ""
|
||||
|
||||
# ── Desktop services (not provided by base package) ────────────────
|
||||
|
||||
# Orbital display server + login + launcher
|
||||
# desktop-minimal.toml had "nowait VT=3 orbital orblogin launcher"
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/20_orbital.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Orbital display server"
|
||||
requires_weak = [
|
||||
"10_net.target",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "orbital"
|
||||
args = ["orblogin", "launcher"]
|
||||
envs = { VT = "3" }
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
# ── Red Bear OS custom service .service files ──────────────────────
|
||||
|
||||
# firmware-loader: scheme daemon serving /scheme/firmware
|
||||
# Uses SchemeDaemon which requires init to read the pipe (ServiceType::Scheme).
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/05_firmware-loader.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Firmware loading scheme"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "firmware-loader"
|
||||
type = { scheme = "firmware" }
|
||||
"""
|
||||
|
||||
# udev-shim: scheme daemon serving /scheme/udev
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/11_udev.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "udev compatibility shim"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "udev-shim"
|
||||
type = { scheme = "udev" }
|
||||
"""
|
||||
|
||||
# evdevd: self-registers scheme, no INIT_NOTIFY handshake needed
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/10_evdevd.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Evdev input daemon"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "evdevd"
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples/wired-dhcp"
|
||||
data = """
|
||||
Description='Red Bear wired DHCP profile'
|
||||
Interface=eth0
|
||||
Connection=ethernet
|
||||
IP=dhcp
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples/wired-static"
|
||||
data = """
|
||||
Description='Red Bear wired static profile'
|
||||
Interface=eth0
|
||||
Connection=ethernet
|
||||
IP=static
|
||||
Address=('192.168.1.10/24')
|
||||
Gateway='192.168.1.1'
|
||||
DNS=('1.1.1.1')
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/12_netctl.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Network profile application"
|
||||
requires_weak = [
|
||||
"10_smolnetd.service",
|
||||
"10_dhcpd.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "netctl"
|
||||
args = ["--boot"]
|
||||
type = "oneshot"
|
||||
"""
|
||||
|
||||
# desktop-minimal.toml: "inputd -A 2", "nowait getty 2", "nowait getty /scheme/debug/no-preserve -J"
|
||||
# Neutralize and replace with proper service files
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/30_console"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/30_console.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Console terminals"
|
||||
requires_weak = [
|
||||
"20_orbital.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "getty"
|
||||
args = ["2"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/31_debug_console.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Debug console"
|
||||
requires_weak = [
|
||||
"20_orbital.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "getty"
|
||||
args = ["/scheme/debug/no-preserve", "-J"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
# Red Bear OS KDE Plasma Desktop Configuration
|
||||
# Build: make all CONFIG_NAME=redbear-kde
|
||||
# Live: make live CONFIG_NAME=redbear-kde
|
||||
#
|
||||
# KDE Plasma 6 session with Wayland compositor
|
||||
# Requires: D-Bus, libinput, Mesa, Qt6, KF6, KWin, plasma-workspace
|
||||
|
||||
include = ["desktop.toml"]
|
||||
|
||||
[general]
|
||||
filesystem_size = 4096
|
||||
|
||||
[packages]
|
||||
# Red Bear OS branding
|
||||
redbear-release = {}
|
||||
|
||||
# ext4 filesystem support
|
||||
ext4d = {}
|
||||
|
||||
# Firmware loading
|
||||
firmware-loader = {}
|
||||
|
||||
# Input layer
|
||||
evdevd = {}
|
||||
udev-shim = {}
|
||||
|
||||
# D-Bus (session + system bus)
|
||||
dbus = {}
|
||||
|
||||
# Wayland protocol
|
||||
libwayland = {}
|
||||
wayland-protocols = {}
|
||||
|
||||
# Input
|
||||
libxkbcommon = {}
|
||||
libevdev = {}
|
||||
libinput = {}
|
||||
|
||||
# Seat management
|
||||
seatd = {}
|
||||
|
||||
# Qt6 stack
|
||||
qtbase = {}
|
||||
qtdeclarative = {}
|
||||
qtsvg = {}
|
||||
qtwayland = {}
|
||||
|
||||
# KF6 Frameworks — Tier 1 (no special deps)
|
||||
kf6-extra-cmake-modules = {}
|
||||
kf6-kcoreaddons = {}
|
||||
kf6-kwidgetsaddons = {}
|
||||
kf6-kconfig = {}
|
||||
kf6-ki18n = {}
|
||||
kf6-kcodecs = {}
|
||||
kf6-kguiaddons = {}
|
||||
kf6-kcolorscheme = {}
|
||||
kf6-kauth = {}
|
||||
kf6-kitemmodels = {}
|
||||
kf6-kitemviews = {}
|
||||
|
||||
# KF6 Frameworks — Tier 2
|
||||
kf6-karchive = {}
|
||||
kf6-kwindowsystem = {}
|
||||
kf6-knotifications = {}
|
||||
kf6-kjobwidgets = {}
|
||||
kf6-kconfigwidgets = {}
|
||||
|
||||
# KF6 Frameworks — Tier 3 (needs D-Bus)
|
||||
kf6-kcrash = {}
|
||||
kf6-kdbusaddons = {}
|
||||
kf6-kglobalaccel = {}
|
||||
kf6-kservice = {}
|
||||
kf6-kpackage = {}
|
||||
kf6-kiconthemes = {}
|
||||
kf6-kxmlgui = {}
|
||||
kf6-ktextwidgets = {}
|
||||
kf6-kirigami = {}
|
||||
kf6-solid = {}
|
||||
kf6-sonnet = {}
|
||||
|
||||
# KF6 Frameworks — Tier 4 (needs kio + kxmlgui)
|
||||
kf6-kio = {}
|
||||
kf6-kbookmarks = {}
|
||||
kf6-kcompletion = {}
|
||||
kf6-kdeclarative = {}
|
||||
kf6-kcmutils = {}
|
||||
plasma-framework = {}
|
||||
|
||||
# KDE Plasma
|
||||
kwin = {}
|
||||
plasma-workspace = {}
|
||||
plasma-desktop = {}
|
||||
breeze = {}
|
||||
kde-cli-tools = {}
|
||||
|
||||
# Graphics
|
||||
mesa = {}
|
||||
libdrm = {}
|
||||
|
||||
# Workaround: bash fails to cross-compile
|
||||
bash = "ignore"
|
||||
|
||||
# Firmware directory for AMD/Intel GPU blobs
|
||||
[[files]]
|
||||
path = "/usr/firmware/amdgpu"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
# ── Neutralize broken legacy scripts from upstream configs ─────────
|
||||
# base.toml uses "notify <service>" which is not a keyword in the legacy
|
||||
# script parser. Base source package ships proper .service/.target files.
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_base"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_drivers"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/10_net"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/20_orbital"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/30_console"
|
||||
data = ""
|
||||
|
||||
# ── Red Bear OS custom services ─────────────────────────────────────
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/05_firmware-loader.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Firmware loading scheme"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "firmware-loader"
|
||||
type = { scheme = "firmware" }
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/10_evdevd.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Evdev input daemon"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "evdevd"
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/11_udev.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "udev compatibility shim"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "udev-shim"
|
||||
type = { scheme = "udev" }
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/12_dbus.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "D-Bus system bus"
|
||||
requires_weak = [
|
||||
"00_base.target",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "dbus-daemon"
|
||||
args = ["--system"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/13_seatd.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "seatd seat management daemon"
|
||||
requires_weak = [
|
||||
"12_dbus.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "seatd"
|
||||
args = ["-l", "info"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/20_orbital.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Orbital display server (KDE session)"
|
||||
requires_weak = [
|
||||
"10_net.target",
|
||||
"12_dbus.service",
|
||||
"13_seatd.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "orbital"
|
||||
args = ["orbital-kde"]
|
||||
envs = { VT = "3" }
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/30_console.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Console terminals"
|
||||
requires_weak = [
|
||||
"20_orbital.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "getty"
|
||||
args = ["2"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/31_debug_console.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Debug console"
|
||||
requires_weak = [
|
||||
"20_orbital.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "getty"
|
||||
args = ["/scheme/debug/no-preserve", "-J"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
# KDE session launcher
|
||||
[[files]]
|
||||
path = "/usr/bin/orbital-kde"
|
||||
mode = 0o755
|
||||
data = """
|
||||
#!/usr/bin/env bash
|
||||
set -ex
|
||||
|
||||
export DISPLAY=""
|
||||
export WAYLAND_DISPLAY=wayland-0
|
||||
export XDG_RUNTIME_DIR=/tmp/run/user/0
|
||||
export XDG_SESSION_TYPE=wayland
|
||||
export KDE_FULL_SESSION=true
|
||||
export XDG_CURRENT_DESKTOP=KDE
|
||||
export HOME=/root
|
||||
export LIBSEAT_BACKEND=seatd
|
||||
export SEATD_SOCK=/run/seatd.sock
|
||||
|
||||
mkdir -p /tmp/run/user/0
|
||||
mkdir -p /var/lib/dbus
|
||||
mkdir -p /run/dbus
|
||||
|
||||
# Start D-Bus system bus (if not already running)
|
||||
if [ ! -S /run/dbus/system_bus_socket ]; then
|
||||
dbus-uuidgen --ensure
|
||||
dbus-daemon --system --fork
|
||||
fi
|
||||
|
||||
# Start D-Bus session bus
|
||||
eval $(dbus-launch --sh-syntax)
|
||||
|
||||
# Start KWin Wayland compositor
|
||||
kwin_wayland --replace &
|
||||
|
||||
sleep 2
|
||||
|
||||
# Start Plasma Shell
|
||||
plasmashell &
|
||||
"""
|
||||
+151
-7
@@ -12,15 +12,159 @@ filesystem_size = 512
|
||||
# Red Bear OS branding
|
||||
redbear-release = {}
|
||||
|
||||
# Native Redox PCI/USB listing tools (lspci, lsusb)
|
||||
redbear-hwutils = {}
|
||||
|
||||
# Redox-native netctl compatibility command
|
||||
redbear-netctl = {}
|
||||
|
||||
# Terminal file manager
|
||||
mc = {}
|
||||
|
||||
# Package builder
|
||||
cub = {}
|
||||
# Diagnostic tool
|
||||
redbear-info = {}
|
||||
|
||||
# Firmware loading
|
||||
firmware-loader = {}
|
||||
# ── Neutralize broken legacy scripts from base.toml ─────────────────
|
||||
# base.toml uses "notify <service>" which is not a keyword in the legacy
|
||||
# script parser. Base source package already ships proper .service/.target
|
||||
# files — we just need to suppress the conflicting legacy scripts.
|
||||
|
||||
# Input event handling
|
||||
evdevd = {}
|
||||
udev-shim = {}
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_base"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/00_drivers"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/10_net"
|
||||
data = ""
|
||||
|
||||
# minimal.toml: "inputd -A 2", "nowait getty 2", "nowait getty /scheme/debug/no-preserve -J"
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/30_console"
|
||||
data = ""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/30_console.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Console terminals"
|
||||
requires_weak = [
|
||||
"10_net.target",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "getty"
|
||||
args = ["2"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/31_debug_console.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Debug console"
|
||||
requires_weak = [
|
||||
"10_net.target",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "getty"
|
||||
args = ["/scheme/debug/no-preserve", "-J"]
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
# ── Red Bear OS custom services ─────────────────────────────────────
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/05_firmware-loader.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Firmware loading scheme"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "firmware-loader"
|
||||
type = { scheme = "firmware" }
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/11_udev.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "udev compatibility shim"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "udev-shim"
|
||||
type = { scheme = "udev" }
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/10_evdevd.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Evdev input daemon"
|
||||
requires_weak = [
|
||||
"00_pcid-spawner.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "evdevd"
|
||||
type = "oneshot_async"
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples"
|
||||
data = ""
|
||||
directory = true
|
||||
mode = 0o755
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples/wired-dhcp"
|
||||
data = """
|
||||
Description='Red Bear wired DHCP profile'
|
||||
Interface=eth0
|
||||
Connection=ethernet
|
||||
IP=dhcp
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/etc/netctl/examples/wired-static"
|
||||
data = """
|
||||
Description='Red Bear wired static profile'
|
||||
Interface=eth0
|
||||
Connection=ethernet
|
||||
IP=static
|
||||
Address=('192.168.1.10/24')
|
||||
Gateway='192.168.1.1'
|
||||
DNS=('1.1.1.1')
|
||||
"""
|
||||
|
||||
[[files]]
|
||||
path = "/usr/lib/init.d/12_netctl.service"
|
||||
data = """
|
||||
[unit]
|
||||
description = "Network profile application"
|
||||
requires_weak = [
|
||||
"10_smolnetd.service",
|
||||
"10_dhcpd.service",
|
||||
]
|
||||
|
||||
[service]
|
||||
cmd = "netctl"
|
||||
args = ["--boot"]
|
||||
type = "oneshot"
|
||||
"""
|
||||
|
||||
@@ -21,8 +21,8 @@ redox-drm = {}
|
||||
amdgpu = {}
|
||||
|
||||
# Input (Phase 3)
|
||||
evdevd = { path = "../../local/recipes/system/evdevd" }
|
||||
udev-shim = { path = "../../local/recipes/system/udev-shim" }
|
||||
evdevd = {}
|
||||
udev-shim = {}
|
||||
|
||||
# Wayland (Phase 4 — depends on P2+P3)
|
||||
# libwayland = {}
|
||||
@@ -62,5 +62,5 @@ nowait evdevd
|
||||
path = "/usr/lib/init.d/11_udev"
|
||||
data = """
|
||||
requires_weak 00_drivers
|
||||
nowait udev-shim
|
||||
nowait udev
|
||||
"""
|
||||
|
||||
@@ -68,5 +68,5 @@ nowait evdevd
|
||||
path = "/usr/lib/init.d/11_udev"
|
||||
data = """
|
||||
requires_weak 00_drivers
|
||||
nowait udev-shim
|
||||
nowait udev
|
||||
"""
|
||||
|
||||
@@ -54,5 +54,5 @@ nowait evdevd
|
||||
path = "/usr/lib/init.d/11_udev"
|
||||
data = """
|
||||
requires_weak 00_drivers
|
||||
nowait udev-shim
|
||||
nowait udev
|
||||
"""
|
||||
|
||||
@@ -8,3 +8,7 @@ redox-scheme = "0.1"
|
||||
syscall = { package = "redox_syscall", version = "0.4" }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
thiserror = "2"
|
||||
orbclient = { version = "=0.3.47", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "redox")'.dependencies]
|
||||
redox_event = "0.4"
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::types::{InputEvent, InputId, BUS_VIRTUAL};
|
||||
use crate::translate::{KEYBOARD_KEY_CODES, MOUSE_BUTTON_CODES, TOUCHPAD_KEY_CODES};
|
||||
use crate::types::{
|
||||
AbsInfo, InputId, ABS_MT_POSITION_X, ABS_MT_POSITION_Y, ABS_MT_SLOT, ABS_MT_TOUCH_MAJOR,
|
||||
ABS_MT_TRACKING_ID, ABS_PRESSURE, ABS_X, ABS_Y, BUS_VIRTUAL, EV_ABS, EV_KEY, EV_LED, EV_MSC,
|
||||
EV_REL, EV_REP, EV_SYN, INPUT_PROP_POINTER, KEY_MAX, LED_CAPSL, LED_MAX, LED_NUML, LED_SCROLLL,
|
||||
MSC_SCAN, REL_HWHEEL, REL_WHEEL, REL_X, REL_Y, REP_DELAY, REP_PERIOD,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum DeviceKind {
|
||||
@@ -10,17 +16,17 @@ pub enum DeviceKind {
|
||||
}
|
||||
|
||||
pub struct InputDevice {
|
||||
pub id: usize,
|
||||
pub kind: DeviceKind,
|
||||
pub name: String,
|
||||
pub input_id: InputId,
|
||||
pub event_buf: VecDeque<InputEvent>,
|
||||
pub key_state: [u8; KEY_MAX / 8 + 1],
|
||||
pub led_state: [u8; LED_MAX / 8 + 1],
|
||||
pub custom_abs: BTreeMap<u16, AbsInfo>,
|
||||
}
|
||||
|
||||
impl InputDevice {
|
||||
pub fn new_keyboard(id: usize) -> Self {
|
||||
InputDevice {
|
||||
id,
|
||||
kind: DeviceKind::Keyboard,
|
||||
name: format!("Redox Keyboard {}", id),
|
||||
input_id: InputId {
|
||||
@@ -29,13 +35,14 @@ impl InputDevice {
|
||||
product: id as u16,
|
||||
version: 1,
|
||||
},
|
||||
event_buf: VecDeque::new(),
|
||||
key_state: [0u8; KEY_MAX / 8 + 1],
|
||||
led_state: [0u8; LED_MAX / 8 + 1],
|
||||
custom_abs: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_mouse(id: usize) -> Self {
|
||||
InputDevice {
|
||||
id,
|
||||
kind: DeviceKind::Mouse,
|
||||
name: format!("Redox Mouse {}", id),
|
||||
input_id: InputId {
|
||||
@@ -44,13 +51,14 @@ impl InputDevice {
|
||||
product: (id + 0x10) as u16,
|
||||
version: 1,
|
||||
},
|
||||
event_buf: VecDeque::new(),
|
||||
key_state: [0u8; KEY_MAX / 8 + 1],
|
||||
led_state: [0u8; LED_MAX / 8 + 1],
|
||||
custom_abs: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_touchpad(id: usize) -> Self {
|
||||
InputDevice {
|
||||
id,
|
||||
kind: DeviceKind::Touchpad,
|
||||
name: format!("Redox Touchpad {}", id),
|
||||
input_id: InputId {
|
||||
@@ -59,37 +67,166 @@ impl InputDevice {
|
||||
product: (id + 0x20) as u16,
|
||||
version: 1,
|
||||
},
|
||||
event_buf: VecDeque::new(),
|
||||
key_state: [0u8; KEY_MAX / 8 + 1],
|
||||
led_state: [0u8; LED_MAX / 8 + 1],
|
||||
custom_abs: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_event(&mut self, event: InputEvent) {
|
||||
self.event_buf.push_back(event);
|
||||
}
|
||||
|
||||
pub fn push_events(&mut self, events: &[InputEvent]) {
|
||||
for &ev in events {
|
||||
self.event_buf.push_back(ev);
|
||||
pub fn supported_event_types(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Keyboard => bitmap_from_codes(&[EV_SYN, EV_KEY, EV_MSC, EV_LED, EV_REP]),
|
||||
DeviceKind::Mouse => bitmap_from_codes(&[EV_SYN, EV_KEY, EV_REL]),
|
||||
DeviceKind::Touchpad => bitmap_from_codes(&[EV_SYN, EV_KEY, EV_ABS]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_bytes(&mut self, buf: &mut [u8]) -> usize {
|
||||
let event_count = buf.len() / InputEvent::SIZE;
|
||||
let mut written = 0;
|
||||
for _ in 0..event_count {
|
||||
match self.event_buf.pop_front() {
|
||||
Some(ev) => {
|
||||
let bytes = ev.to_bytes();
|
||||
buf[written..written + InputEvent::SIZE].copy_from_slice(&bytes);
|
||||
written += InputEvent::SIZE;
|
||||
}
|
||||
None => break,
|
||||
pub fn supported_keys(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Keyboard => bitmap_from_codes(KEYBOARD_KEY_CODES),
|
||||
DeviceKind::Mouse => bitmap_from_codes(MOUSE_BUTTON_CODES),
|
||||
DeviceKind::Touchpad => bitmap_from_codes(TOUCHPAD_KEY_CODES),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supported_rel(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Mouse => bitmap_from_codes(&[REL_X, REL_Y, REL_WHEEL, REL_HWHEEL]),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supported_abs(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Touchpad => bitmap_from_codes(&[
|
||||
ABS_X,
|
||||
ABS_Y,
|
||||
ABS_PRESSURE,
|
||||
ABS_MT_SLOT,
|
||||
ABS_MT_TOUCH_MAJOR,
|
||||
ABS_MT_POSITION_X,
|
||||
ABS_MT_POSITION_Y,
|
||||
ABS_MT_TRACKING_ID,
|
||||
]),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supported_msc(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Keyboard => bitmap_from_codes(&[MSC_SCAN]),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supported_leds(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Keyboard => bitmap_from_codes(&[LED_NUML, LED_CAPSL, LED_SCROLLL]),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supported_rep(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Keyboard => bitmap_from_codes(&[REP_DELAY, REP_PERIOD]),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn supported_props(&self) -> Vec<u8> {
|
||||
match self.kind {
|
||||
DeviceKind::Mouse | DeviceKind::Touchpad => bitmap_from_codes(&[INPUT_PROP_POINTER]),
|
||||
DeviceKind::Keyboard => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn abs_info(&self, axis: u16) -> AbsInfo {
|
||||
if let Some(abs_info) = self.custom_abs.get(&axis) {
|
||||
return *abs_info;
|
||||
}
|
||||
|
||||
if self.kind != DeviceKind::Touchpad {
|
||||
return AbsInfo::default();
|
||||
}
|
||||
|
||||
match axis {
|
||||
ABS_X | ABS_MT_POSITION_X => AbsInfo {
|
||||
minimum: 0,
|
||||
maximum: 65_535,
|
||||
resolution: 1,
|
||||
..AbsInfo::default()
|
||||
},
|
||||
ABS_Y | ABS_MT_POSITION_Y => AbsInfo {
|
||||
minimum: 0,
|
||||
maximum: 65_535,
|
||||
resolution: 1,
|
||||
..AbsInfo::default()
|
||||
},
|
||||
ABS_PRESSURE => AbsInfo {
|
||||
minimum: 0,
|
||||
maximum: 255,
|
||||
resolution: 1,
|
||||
..AbsInfo::default()
|
||||
},
|
||||
ABS_MT_TOUCH_MAJOR => AbsInfo {
|
||||
minimum: 0,
|
||||
maximum: 255,
|
||||
resolution: 1,
|
||||
..AbsInfo::default()
|
||||
},
|
||||
ABS_MT_SLOT => AbsInfo {
|
||||
minimum: 0,
|
||||
maximum: 9,
|
||||
..AbsInfo::default()
|
||||
},
|
||||
ABS_MT_TRACKING_ID => AbsInfo {
|
||||
minimum: 0,
|
||||
maximum: i32::MAX,
|
||||
..AbsInfo::default()
|
||||
},
|
||||
_ => AbsInfo::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_abs_info(&mut self, axis: u16, abs_info: AbsInfo) {
|
||||
self.custom_abs.insert(axis, abs_info);
|
||||
}
|
||||
|
||||
pub fn update_key_state(&mut self, code: u16, pressed: bool) {
|
||||
let byte = (code / 8) as usize;
|
||||
let bit = code % 8;
|
||||
if byte < self.key_state.len() {
|
||||
if pressed {
|
||||
self.key_state[byte] |= 1 << bit;
|
||||
} else {
|
||||
self.key_state[byte] &= !(1 << bit);
|
||||
}
|
||||
}
|
||||
written
|
||||
}
|
||||
|
||||
pub fn has_events(&self) -> bool {
|
||||
!self.event_buf.is_empty()
|
||||
pub fn update_led_state(&mut self, code: u16, lit: bool) {
|
||||
let byte = (code / 8) as usize;
|
||||
let bit = code % 8;
|
||||
if byte < self.led_state.len() {
|
||||
if lit {
|
||||
self.led_state[byte] |= 1 << bit;
|
||||
} else {
|
||||
self.led_state[byte] &= !(1 << bit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn bitmap_from_codes(codes: &[u16]) -> Vec<u8> {
|
||||
let Some(max) = codes.iter().copied().max() else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut bitmap = vec![0u8; (usize::from(max) / 8) + 1];
|
||||
for &code in codes {
|
||||
let index = usize::from(code / 8);
|
||||
let bit = 1u8 << (code % 8);
|
||||
bitmap[index] |= bit;
|
||||
}
|
||||
bitmap
|
||||
}
|
||||
|
||||
@@ -3,16 +3,37 @@ mod scheme;
|
||||
mod translate;
|
||||
mod types;
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::env;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{ErrorKind, Read};
|
||||
use std::mem::{size_of, MaybeUninit};
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::process;
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
use std::thread;
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{error, info, LevelFilter, Metadata, Record};
|
||||
use redox_scheme::{SignalBehavior, Socket};
|
||||
use orbclient::{Event, EventOption};
|
||||
use redox_scheme::{Request, SignalBehavior, Socket};
|
||||
use syscall::error::EAGAIN;
|
||||
use syscall::flag::O_NONBLOCK;
|
||||
|
||||
use scheme::EvdevScheme;
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use event::{EventFlags as QueueEventFlags, RawEventQueue};
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
const SCHEME_QUEUE_TOKEN: usize = 1;
|
||||
#[cfg(target_os = "redox")]
|
||||
const INPUT_QUEUE_TOKEN: usize = 2;
|
||||
|
||||
struct StderrLogger {
|
||||
level: LevelFilter,
|
||||
}
|
||||
@@ -29,64 +50,256 @@ impl log::Log for StderrLogger {
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
fn read_input_events(scheme: &mut EvdevScheme) -> Result<(), String> {
|
||||
let mut input_file =
|
||||
File::open("/scheme/input").map_err(|e| format!("failed to open /scheme/input: {}", e))?;
|
||||
struct InputConsumer {
|
||||
file: File,
|
||||
partial: Vec<u8>,
|
||||
}
|
||||
|
||||
let mut buf = [0u8; 256];
|
||||
match input_file.read(&mut buf) {
|
||||
Ok(n) if n > 0 => {
|
||||
let data = &buf[..n];
|
||||
for &byte in data {
|
||||
let pressed = (byte & 0x80) == 0;
|
||||
let key = byte & 0x7F;
|
||||
scheme.feed_keyboard_event(key, pressed);
|
||||
impl InputConsumer {
|
||||
fn open() -> Result<Self, String> {
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(O_NONBLOCK as i32)
|
||||
.open("/scheme/input/consumer")
|
||||
.map_err(|e| format!("failed to open /scheme/input/consumer: {e}"))?;
|
||||
|
||||
Ok(Self {
|
||||
file,
|
||||
partial: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn fd(&self) -> usize {
|
||||
self.file.as_raw_fd() as usize
|
||||
}
|
||||
|
||||
fn read_available(&mut self, scheme: &mut EvdevScheme) -> Result<bool, String> {
|
||||
let event_size = size_of::<Event>();
|
||||
let mut buf = vec![0u8; event_size * 32];
|
||||
let mut progress = false;
|
||||
|
||||
loop {
|
||||
match self.file.read(&mut buf) {
|
||||
Ok(0) => break,
|
||||
Ok(count) => {
|
||||
progress = true;
|
||||
self.partial.extend_from_slice(&buf[..count]);
|
||||
|
||||
while self.partial.len() >= event_size {
|
||||
let event = read_event_from_bytes(&self.partial[..event_size]);
|
||||
self.partial.drain(..event_size);
|
||||
dispatch_input_event(event, scheme);
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == ErrorKind::WouldBlock => break,
|
||||
Err(err) => return Err(format!("failed to read /scheme/input/consumer: {err}")),
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!("evdevd: failed to read input: {}", e);
|
||||
|
||||
Ok(progress)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct SchemePoll {
|
||||
mounted: bool,
|
||||
progress: bool,
|
||||
}
|
||||
|
||||
fn read_event_from_bytes(bytes: &[u8]) -> Event {
|
||||
let mut event = MaybeUninit::<Event>::uninit();
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), event.as_mut_ptr() as *mut u8, bytes.len());
|
||||
event.assume_init()
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_input_event(event: Event, scheme: &mut EvdevScheme) {
|
||||
match event.to_option() {
|
||||
EventOption::Key(key) => scheme.feed_keyboard_event(key.scancode, key.pressed),
|
||||
EventOption::Mouse(mouse) => scheme.feed_touchpad_position(mouse.x, mouse.y),
|
||||
EventOption::MouseRelative(mouse) => scheme.feed_mouse_move(mouse.dx, mouse.dy),
|
||||
EventOption::Button(button) => {
|
||||
scheme.feed_mouse_buttons(button.left, button.middle, button.right)
|
||||
}
|
||||
EventOption::Scroll(scroll) => scheme.feed_mouse_scroll(scroll.x, scroll.y),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_would_block_socket(err: &syscall::Error) -> bool {
|
||||
err.errno == EAGAIN
|
||||
}
|
||||
|
||||
fn write_scheme_response(socket: &Socket, response: redox_scheme::Response) -> Result<(), String> {
|
||||
socket
|
||||
.write_response(response, SignalBehavior::Restart)
|
||||
.map_err(|e| format!("failed to write response: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_request(
|
||||
request: Request,
|
||||
scheme: &mut EvdevScheme,
|
||||
pending_requests: &mut VecDeque<Request>,
|
||||
socket: &Socket,
|
||||
) -> Result<bool, String> {
|
||||
match request.handle_scheme_block_mut(scheme) {
|
||||
Ok(response) => {
|
||||
write_scheme_response(socket, response)?;
|
||||
Ok(true)
|
||||
}
|
||||
Err(request) => {
|
||||
pending_requests.push_back(request);
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_pending_requests(
|
||||
scheme: &mut EvdevScheme,
|
||||
pending_requests: &mut VecDeque<Request>,
|
||||
socket: &Socket,
|
||||
) -> Result<bool, String> {
|
||||
let mut progress = false;
|
||||
let pending_len = pending_requests.len();
|
||||
|
||||
for _ in 0..pending_len {
|
||||
let Some(request) = pending_requests.pop_front() else {
|
||||
break;
|
||||
};
|
||||
|
||||
match request.handle_scheme_block_mut(scheme) {
|
||||
Ok(response) => {
|
||||
write_scheme_response(socket, response)?;
|
||||
progress = true;
|
||||
}
|
||||
Err(request) => pending_requests.push_back(request),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(progress)
|
||||
}
|
||||
|
||||
fn read_scheme_requests(
|
||||
socket: &Socket,
|
||||
scheme: &mut EvdevScheme,
|
||||
pending_requests: &mut VecDeque<Request>,
|
||||
) -> Result<SchemePoll, String> {
|
||||
let mut poll = SchemePoll {
|
||||
mounted: true,
|
||||
progress: false,
|
||||
};
|
||||
|
||||
loop {
|
||||
match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(request)) => {
|
||||
poll.progress =
|
||||
handle_request(request, scheme, pending_requests, socket)? || poll.progress;
|
||||
}
|
||||
Ok(None) => {
|
||||
poll.mounted = false;
|
||||
break;
|
||||
}
|
||||
Err(err) if is_would_block_socket(&err) => break,
|
||||
Err(err) => return Err(format!("failed to read scheme request: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(poll)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run_redox_event_loop(
|
||||
socket: &Socket,
|
||||
scheme: &mut EvdevScheme,
|
||||
input: &mut InputConsumer,
|
||||
pending_requests: &mut VecDeque<Request>,
|
||||
) -> Result<(), String> {
|
||||
let event_queue =
|
||||
RawEventQueue::new().map_err(|e| format!("failed to create event queue: {e}"))?;
|
||||
event_queue
|
||||
.subscribe(
|
||||
socket.inner().raw(),
|
||||
SCHEME_QUEUE_TOKEN,
|
||||
QueueEventFlags::READ,
|
||||
)
|
||||
.map_err(|e| format!("failed to subscribe scheme socket: {e}"))?;
|
||||
event_queue
|
||||
.subscribe(input.fd(), INPUT_QUEUE_TOKEN, QueueEventFlags::READ)
|
||||
.map_err(|e| format!("failed to subscribe input consumer: {e}"))?;
|
||||
|
||||
loop {
|
||||
let raw_event = event_queue
|
||||
.next_event()
|
||||
.map_err(|e| format!("failed to wait for events: {e}"))?;
|
||||
|
||||
match raw_event.user_data {
|
||||
SCHEME_QUEUE_TOKEN => {
|
||||
let poll = read_scheme_requests(socket, scheme, pending_requests)?;
|
||||
if !poll.mounted {
|
||||
info!("evdevd: scheme unmounted, exiting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
INPUT_QUEUE_TOKEN => {
|
||||
let _ = input.read_available(scheme)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let _ = flush_pending_requests(scheme, pending_requests, socket)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn run_host_event_loop(
|
||||
socket: &Socket,
|
||||
scheme: &mut EvdevScheme,
|
||||
input: &mut InputConsumer,
|
||||
pending_requests: &mut VecDeque<Request>,
|
||||
) -> Result<(), String> {
|
||||
loop {
|
||||
let mut progress = input.read_available(scheme)?;
|
||||
|
||||
let poll = read_scheme_requests(socket, scheme, pending_requests)?;
|
||||
if !poll.mounted {
|
||||
info!("evdevd: scheme unmounted, exiting");
|
||||
break;
|
||||
}
|
||||
progress |= poll.progress;
|
||||
progress |= flush_pending_requests(scheme, pending_requests, socket)?;
|
||||
|
||||
if !progress {
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let mut input = InputConsumer::open()?;
|
||||
let mut pending_requests = VecDeque::new();
|
||||
|
||||
let socket =
|
||||
Socket::create("evdev").map_err(|e| format!("failed to register evdev scheme: {}", e))?;
|
||||
Socket::nonblock("evdev").map_err(|e| format!("failed to register evdev scheme: {}", e))?;
|
||||
info!("evdevd: registered scheme:evdev");
|
||||
info!("evdevd: consuming orbclient::Event from /scheme/input/consumer");
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => {
|
||||
info!("evdevd: scheme unmounted, exiting");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("evdevd: failed to read scheme request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let response = match request.handle_scheme_block_mut(&mut scheme) {
|
||||
Ok(r) => r,
|
||||
Err(_req) => {
|
||||
error!("evdevd: failed to handle request");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
|
||||
error!("evdevd: failed to write response: {}", e);
|
||||
}
|
||||
|
||||
let _ = read_input_events(&mut scheme);
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
run_redox_event_loop(&socket, &mut scheme, &mut input, &mut pending_requests)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
run_host_event_loop(&socket, &mut scheme, &mut input, &mut pending_requests)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::mem::size_of;
|
||||
use std::mem::MaybeUninit;
|
||||
use std::ptr;
|
||||
|
||||
use syscall::data::Stat;
|
||||
use syscall::error::{Error, Result, EBADF, EINVAL, ENOENT, EROFS};
|
||||
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
|
||||
use syscall::error::{Error, Result, EBADF, EBUSY, EFAULT, EINVAL, ENOENT, ENOTTY, EROFS};
|
||||
use syscall::flag::{
|
||||
EventFlags, F_GETFD, F_GETFL, F_SETFD, F_SETFL, MODE_DIR, MODE_FILE, O_RDONLY, SEEK_CUR,
|
||||
SEEK_END, SEEK_SET,
|
||||
};
|
||||
|
||||
use crate::device::{DeviceKind, InputDevice};
|
||||
use crate::translate;
|
||||
use crate::types::{
|
||||
ioc_dir, ioc_nr, ioc_size, ioc_type, is_evdev_ioctl, AbsInfo, InputEvent, InputId,
|
||||
EVDEV_IOCTL_TYPE, EVIOCGABS, EVIOCGEFFECTS, EVIOCGID, EVIOCGKEY, EVIOCGLED, EVIOCGNAME,
|
||||
EVIOCGPROP, EVIOCGRAB, EVIOCGVERSION, EVIOCRMFF, EVIOCSABS, EVIOCSCLOCKID, EVIOCSFF, EV_ABS,
|
||||
EV_KEY, EV_LED, EV_MSC, EV_REL, EV_REP, EV_VERSION, IOC_READ,
|
||||
};
|
||||
|
||||
struct Handle {
|
||||
kind: HandleKind,
|
||||
@@ -14,13 +26,22 @@ struct Handle {
|
||||
|
||||
enum HandleKind {
|
||||
Root,
|
||||
Device(usize),
|
||||
Device {
|
||||
device_idx: usize,
|
||||
events: VecDeque<InputEvent>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct EvdevScheme {
|
||||
next_id: usize,
|
||||
handles: BTreeMap<usize, Handle>,
|
||||
devices: Vec<InputDevice>,
|
||||
grabbed_by: BTreeMap<usize, usize>,
|
||||
mouse_buttons: [bool; 3],
|
||||
touchpad_position: (i32, i32),
|
||||
touchpad_touching: bool,
|
||||
next_tracking_id: i32,
|
||||
current_tracking_id: i32,
|
||||
}
|
||||
|
||||
impl EvdevScheme {
|
||||
@@ -29,54 +50,241 @@ impl EvdevScheme {
|
||||
next_id: 0,
|
||||
handles: BTreeMap::new(),
|
||||
devices: Vec::new(),
|
||||
grabbed_by: BTreeMap::new(),
|
||||
mouse_buttons: [false; 3],
|
||||
touchpad_position: (0, 0),
|
||||
touchpad_touching: false,
|
||||
next_tracking_id: 1,
|
||||
current_tracking_id: -1,
|
||||
};
|
||||
scheme.devices.push(InputDevice::new_keyboard(0));
|
||||
scheme.devices.push(InputDevice::new_mouse(0));
|
||||
scheme.devices.push(InputDevice::new_mouse(1));
|
||||
scheme.devices.push(InputDevice::new_touchpad(2));
|
||||
scheme
|
||||
}
|
||||
|
||||
pub fn feed_keyboard_event(&mut self, key: u8, pressed: bool) {
|
||||
let events = translate::translate_keyboard(key, pressed);
|
||||
if !events.is_empty() {
|
||||
if let Some(dev) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|d| d.kind == DeviceKind::Keyboard)
|
||||
fn device_index(&self, kind: DeviceKind) -> Option<usize> {
|
||||
self.devices.iter().position(|d| d.kind == kind)
|
||||
}
|
||||
|
||||
fn current_tracking_id(&self) -> i32 {
|
||||
if self.touchpad_touching {
|
||||
self.current_tracking_id
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_device_events(&mut self, kind: DeviceKind, events: &[InputEvent]) {
|
||||
if events.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(device_idx) = self.device_index(kind) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for event in events {
|
||||
if event.event_type == EV_KEY {
|
||||
self.devices[device_idx].update_key_state(event.code, event.value != 0);
|
||||
} else if event.event_type == EV_LED {
|
||||
self.devices[device_idx].update_led_state(event.code, event.value != 0);
|
||||
}
|
||||
}
|
||||
|
||||
let grabbed_handle = self.grabbed_by.get(&device_idx).copied();
|
||||
|
||||
for (handle_id, handle) in self.handles.iter_mut() {
|
||||
if let HandleKind::Device {
|
||||
device_idx: handle_device_idx,
|
||||
events: handle_events,
|
||||
} = &mut handle.kind
|
||||
{
|
||||
dev.push_events(&events);
|
||||
if *handle_device_idx == device_idx {
|
||||
if let Some(grabbed_id) = grabbed_handle {
|
||||
if *handle_id != grabbed_id {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
handle_events.extend(events.iter().copied());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pop_handle_bytes(events: &mut VecDeque<InputEvent>, buf: &mut [u8]) -> usize {
|
||||
let event_count = buf.len() / InputEvent::SIZE;
|
||||
let mut written = 0;
|
||||
|
||||
for _ in 0..event_count {
|
||||
let Some(event) = events.pop_front() else {
|
||||
break;
|
||||
};
|
||||
|
||||
let bytes = event.to_bytes();
|
||||
buf[written..written + InputEvent::SIZE].copy_from_slice(&bytes);
|
||||
written += InputEvent::SIZE;
|
||||
}
|
||||
|
||||
written
|
||||
}
|
||||
|
||||
pub fn feed_keyboard_event(&mut self, scancode: u8, pressed: bool) {
|
||||
let events = translate::translate_keyboard(scancode, pressed);
|
||||
self.queue_device_events(DeviceKind::Keyboard, &events);
|
||||
}
|
||||
|
||||
pub fn feed_mouse_move(&mut self, dx: i32, dy: i32) {
|
||||
if let Some(dev) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|d| d.kind == DeviceKind::Mouse)
|
||||
{
|
||||
dev.push_events(&translate::translate_mouse_dx(dx));
|
||||
dev.push_events(&translate::translate_mouse_dy(dy));
|
||||
let events = translate::translate_mouse_motion(dx, dy);
|
||||
self.queue_device_events(DeviceKind::Mouse, &events);
|
||||
}
|
||||
|
||||
pub fn feed_mouse_scroll(&mut self, x: i32, y: i32) {
|
||||
let events = translate::translate_mouse_scroll(x, y);
|
||||
self.queue_device_events(DeviceKind::Mouse, &events);
|
||||
}
|
||||
|
||||
pub fn feed_mouse_buttons(&mut self, left: bool, middle: bool, right: bool) {
|
||||
let old_buttons = self.mouse_buttons;
|
||||
let new_buttons = [left, middle, right];
|
||||
for (index, (&old, &new)) in old_buttons.iter().zip(new_buttons.iter()).enumerate() {
|
||||
if old != new {
|
||||
let events = translate::translate_mouse_button(index, new);
|
||||
self.queue_device_events(DeviceKind::Mouse, &events);
|
||||
}
|
||||
}
|
||||
self.mouse_buttons = new_buttons;
|
||||
|
||||
let touching = left;
|
||||
if touching != self.touchpad_touching {
|
||||
if touching {
|
||||
self.current_tracking_id = self.next_tracking_id;
|
||||
self.next_tracking_id = self.next_tracking_id.saturating_add(1);
|
||||
}
|
||||
|
||||
self.touchpad_touching = touching;
|
||||
let (x, y) = self.touchpad_position;
|
||||
let tracking_id = self.current_tracking_id();
|
||||
let events = translate::translate_touchpad_contact(x, y, touching, tracking_id);
|
||||
self.queue_device_events(DeviceKind::Touchpad, &events);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn feed_mouse_scroll(&mut self, y: i32) {
|
||||
if let Some(dev) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|d| d.kind == DeviceKind::Mouse)
|
||||
{
|
||||
dev.push_events(&translate::translate_mouse_scroll(y));
|
||||
pub fn feed_touchpad_position(&mut self, x: i32, y: i32) {
|
||||
self.touchpad_position = (x, y);
|
||||
let touching = self.touchpad_touching;
|
||||
let tracking_id = self.current_tracking_id();
|
||||
let events = translate::translate_touchpad_motion(x, y, touching, tracking_id);
|
||||
self.queue_device_events(DeviceKind::Touchpad, &events);
|
||||
}
|
||||
|
||||
fn ioctl_name_len(cmd: u64) -> Option<usize> {
|
||||
if cmd == EVIOCGNAME || (ioc_type(cmd) == EVDEV_IOCTL_TYPE && ioc_nr(cmd) == 0x06) {
|
||||
let size = ioc_size(cmd);
|
||||
return Some(if size == 0 { 256 } else { size });
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn ioctl_bit_ev_and_len(cmd: u64) -> Option<(u8, usize)> {
|
||||
if !is_evdev_ioctl(cmd) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let nr = ioc_nr(cmd);
|
||||
if !(0x20..0x40).contains(&nr) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let size = ioc_size(cmd);
|
||||
Some(((nr - 0x20) as u8, size))
|
||||
}
|
||||
|
||||
fn ioctl_abs_axis(cmd: u64) -> Option<u16> {
|
||||
if !is_evdev_ioctl(cmd) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let nr = ioc_nr(cmd);
|
||||
if !(0x40..0x80).contains(&nr) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((nr - 0x40) as u16)
|
||||
}
|
||||
|
||||
fn device_bitmap(device: &InputDevice, ev: u8) -> Vec<u8> {
|
||||
match u16::from(ev) {
|
||||
0 => device.supported_event_types(),
|
||||
EV_KEY => device.supported_keys(),
|
||||
EV_REL => device.supported_rel(),
|
||||
EV_ABS => device.supported_abs(),
|
||||
EV_MSC => device.supported_msc(),
|
||||
EV_LED => device.supported_leds(),
|
||||
EV_REP => device.supported_rep(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn feed_mouse_button(&mut self, button: usize, pressed: bool) {
|
||||
if let Some(dev) = self
|
||||
.devices
|
||||
.iter_mut()
|
||||
.find(|d| d.kind == DeviceKind::Mouse)
|
||||
{
|
||||
dev.push_events(&translate::translate_mouse_button(button, pressed));
|
||||
unsafe fn write_value_to_user<T: Copy>(arg: usize, value: &T) -> Result<usize> {
|
||||
if arg == 0 {
|
||||
return Err(Error::new(EFAULT));
|
||||
}
|
||||
|
||||
ptr::copy_nonoverlapping(
|
||||
value as *const T as *const u8,
|
||||
arg as *mut u8,
|
||||
size_of::<T>(),
|
||||
);
|
||||
Ok(size_of::<T>())
|
||||
}
|
||||
|
||||
unsafe fn write_bytes_to_user(arg: usize, bytes: &[u8]) -> Result<usize> {
|
||||
if arg == 0 {
|
||||
return Err(Error::new(EFAULT));
|
||||
}
|
||||
|
||||
if !bytes.is_empty() {
|
||||
ptr::copy_nonoverlapping(bytes.as_ptr(), arg as *mut u8, bytes.len());
|
||||
}
|
||||
Ok(bytes.len())
|
||||
}
|
||||
|
||||
unsafe fn read_value_from_user<T: Copy>(arg: usize) -> Result<T> {
|
||||
if arg == 0 {
|
||||
return Err(Error::new(EFAULT));
|
||||
}
|
||||
|
||||
let mut value = MaybeUninit::<T>::uninit();
|
||||
ptr::copy_nonoverlapping(
|
||||
arg as *const u8,
|
||||
value.as_mut_ptr() as *mut u8,
|
||||
size_of::<T>(),
|
||||
);
|
||||
Ok(value.assume_init())
|
||||
}
|
||||
|
||||
fn ioctl_abs_set_axis(cmd: u64) -> Option<u16> {
|
||||
if !is_evdev_ioctl(cmd) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let nr = ioc_nr(cmd);
|
||||
if !(0xc0..0x100).contains(&nr) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((nr - 0xc0) as u16)
|
||||
}
|
||||
|
||||
fn device_prop_bitmap(device: &InputDevice) -> [u8; 64] {
|
||||
let bitmap = device.supported_props();
|
||||
let mut bytes = [0u8; 64];
|
||||
let copy_len = bitmap.len().min(bytes.len());
|
||||
if copy_len > 0 {
|
||||
bytes[..copy_len].copy_from_slice(&bitmap[..copy_len]);
|
||||
}
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +302,10 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
|
||||
if idx >= self.devices.len() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
HandleKind::Device(idx)
|
||||
HandleKind::Device {
|
||||
device_idx: idx,
|
||||
events: VecDeque::new(),
|
||||
}
|
||||
} else {
|
||||
return Err(Error::new(ENOENT));
|
||||
};
|
||||
@@ -108,22 +319,28 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
|
||||
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
|
||||
match &handle.kind {
|
||||
match &mut handle.kind {
|
||||
HandleKind::Root => {
|
||||
let mut listing = String::new();
|
||||
for (i, _dev) in self.devices.iter().enumerate() {
|
||||
listing.push_str(&format!("event{}\n", i));
|
||||
}
|
||||
let bytes = listing.as_bytes();
|
||||
if handle.offset >= bytes.len() {
|
||||
return Ok(Some(0));
|
||||
}
|
||||
let remaining = &bytes[handle.offset..];
|
||||
let to_copy = remaining.len().min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
|
||||
handle.offset += to_copy;
|
||||
Ok(Some(to_copy))
|
||||
}
|
||||
HandleKind::Device(idx) => {
|
||||
let dev = &mut self.devices[*idx];
|
||||
let written = dev.pop_bytes(buf);
|
||||
HandleKind::Device { events, .. } => {
|
||||
if !events.is_empty() && buf.len() < InputEvent::SIZE {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
|
||||
let written = Self::pop_handle_bytes(events, buf);
|
||||
handle.offset += written;
|
||||
Ok(if written == 0 { None } else { Some(written) })
|
||||
}
|
||||
@@ -156,7 +373,7 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
|
||||
HandleKind::Root => {
|
||||
stat.st_mode = MODE_DIR | 0o555;
|
||||
}
|
||||
HandleKind::Device(_) => {
|
||||
HandleKind::Device { .. } => {
|
||||
stat.st_mode = MODE_FILE | 0o444;
|
||||
}
|
||||
}
|
||||
@@ -164,6 +381,7 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
self.grabbed_by.retain(|_, grabbed_id| *grabbed_id != id);
|
||||
self.handles.remove(&id);
|
||||
Ok(Some(0))
|
||||
}
|
||||
@@ -172,7 +390,7 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
let path = match &handle.kind {
|
||||
HandleKind::Root => "evdev:".to_string(),
|
||||
HandleKind::Device(idx) => format!("evdev:event{}", idx),
|
||||
HandleKind::Device { device_idx, .. } => format!("evdev:event{}", device_idx),
|
||||
};
|
||||
let bytes = path.as_bytes();
|
||||
let to_copy = bytes.len().min(buf.len());
|
||||
@@ -180,13 +398,343 @@ impl redox_scheme::SchemeBlockMut for EvdevScheme {
|
||||
Ok(Some(to_copy))
|
||||
}
|
||||
|
||||
fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize) -> Result<Option<usize>> {
|
||||
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
Ok(Some(0))
|
||||
fn fcntl(&mut self, id: usize, cmd_raw: usize, arg: usize) -> Result<Option<usize>> {
|
||||
let device_idx = match &self.handles.get(&id).ok_or(Error::new(EBADF))?.kind {
|
||||
HandleKind::Root => None,
|
||||
HandleKind::Device { device_idx, .. } => Some(*device_idx),
|
||||
};
|
||||
|
||||
match cmd_raw {
|
||||
F_GETFL => return Ok(Some(O_RDONLY)),
|
||||
F_GETFD => return Ok(Some(0)),
|
||||
F_SETFL | F_SETFD => {
|
||||
return Ok(Some(0));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let Some(idx) = device_idx else {
|
||||
return Err(Error::new(EINVAL));
|
||||
};
|
||||
|
||||
if cmd_raw == EVIOCGRAB as usize {
|
||||
let grab = unsafe { Self::read_value_from_user::<i32>(arg)? };
|
||||
return match grab {
|
||||
0 => {
|
||||
if self.grabbed_by.get(&idx) == Some(&id) {
|
||||
self.grabbed_by.remove(&idx);
|
||||
}
|
||||
Ok(Some(0))
|
||||
}
|
||||
1 => match self.grabbed_by.get(&idx).copied() {
|
||||
Some(grabbed_id) if grabbed_id != id => Err(Error::new(EBUSY)),
|
||||
_ => {
|
||||
self.grabbed_by.insert(idx, id);
|
||||
Ok(Some(0))
|
||||
}
|
||||
},
|
||||
_ => Err(Error::new(EINVAL)),
|
||||
};
|
||||
}
|
||||
|
||||
if cmd_raw == EVIOCSCLOCKID as usize {
|
||||
return Ok(Some(0));
|
||||
}
|
||||
let cmd = cmd_raw as u64;
|
||||
|
||||
if matches!(cmd, EVIOCSFF | EVIOCRMFF | EVIOCGEFFECTS) {
|
||||
return Err(Error::new(ENOTTY));
|
||||
}
|
||||
|
||||
if cmd == EVIOCSABS || Self::ioctl_abs_set_axis(cmd).is_some() {
|
||||
let axis = Self::ioctl_abs_set_axis(cmd).unwrap_or(0);
|
||||
let abs_info = unsafe { Self::read_value_from_user::<AbsInfo>(arg)? };
|
||||
self.devices[idx].set_abs_info(axis, abs_info);
|
||||
return Ok(Some(0));
|
||||
}
|
||||
|
||||
let device = &self.devices[idx];
|
||||
|
||||
if cmd == EVIOCGVERSION {
|
||||
let version = EV_VERSION;
|
||||
return unsafe { Self::write_value_to_user(arg, &version).map(Some) };
|
||||
}
|
||||
|
||||
if cmd == EVIOCGID {
|
||||
let input_id: InputId = device.input_id;
|
||||
return unsafe { Self::write_value_to_user(arg, &input_id).map(Some) };
|
||||
}
|
||||
|
||||
if cmd == EVIOCGKEY {
|
||||
let key_state = device.key_state;
|
||||
return unsafe { Self::write_bytes_to_user(arg, &key_state).map(Some) };
|
||||
}
|
||||
|
||||
if cmd == EVIOCGLED {
|
||||
let led_state = device.led_state;
|
||||
return unsafe { Self::write_bytes_to_user(arg, &led_state).map(Some) };
|
||||
}
|
||||
|
||||
if cmd == EVIOCGPROP {
|
||||
let props = Self::device_prop_bitmap(device);
|
||||
return unsafe { Self::write_bytes_to_user(arg, &props).map(Some) };
|
||||
}
|
||||
|
||||
if let Some(name_len) = Self::ioctl_name_len(cmd) {
|
||||
let mut bytes = vec![0u8; name_len];
|
||||
let name = device.name.as_bytes();
|
||||
let copy_len = name.len().min(bytes.len().saturating_sub(1));
|
||||
if copy_len > 0 {
|
||||
bytes[..copy_len].copy_from_slice(&name[..copy_len]);
|
||||
}
|
||||
return unsafe { Self::write_bytes_to_user(arg, &bytes).map(Some) };
|
||||
}
|
||||
|
||||
if let Some((ev, len)) = Self::ioctl_bit_ev_and_len(cmd) {
|
||||
let bitmap = Self::device_bitmap(device, ev);
|
||||
let out_len = if len == 0 {
|
||||
bitmap.len()
|
||||
} else {
|
||||
len.max(bitmap.len()).min(len)
|
||||
};
|
||||
let mut bytes = vec![0u8; out_len];
|
||||
let copy_len = bitmap.len().min(bytes.len());
|
||||
if copy_len > 0 {
|
||||
bytes[..copy_len].copy_from_slice(&bitmap[..copy_len]);
|
||||
}
|
||||
return unsafe { Self::write_bytes_to_user(arg, &bytes).map(Some) };
|
||||
}
|
||||
|
||||
if cmd == EVIOCGABS || Self::ioctl_abs_axis(cmd).is_some() {
|
||||
let axis = Self::ioctl_abs_axis(cmd).unwrap_or(0);
|
||||
let abs_info: AbsInfo = device.abs_info(axis);
|
||||
return unsafe { Self::write_value_to_user(arg, &abs_info).map(Some) };
|
||||
}
|
||||
|
||||
if is_evdev_ioctl(cmd) && ioc_dir(cmd) == IOC_READ {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
|
||||
Err(Error::new(EINVAL))
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, flags: EventFlags) -> Result<Option<EventFlags>> {
|
||||
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
Ok(Some(flags))
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
|
||||
let readiness = match &handle.kind {
|
||||
HandleKind::Root => flags,
|
||||
HandleKind::Device { events, .. } if !events.is_empty() => {
|
||||
flags & EventFlags::EVENT_READ
|
||||
}
|
||||
HandleKind::Device { .. } => EventFlags::empty(),
|
||||
};
|
||||
|
||||
Ok(Some(readiness))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use redox_scheme::SchemeBlockMut;
|
||||
|
||||
use super::EvdevScheme;
|
||||
use crate::types::{
|
||||
AbsInfo, InputEvent, ABS_MT_SLOT, EVIOCGEFFECTS, EVIOCGPROP, EVIOCGRAB, EVIOCRMFF,
|
||||
EVIOCSABS, EVIOCSFF, INPUT_PROP_POINTER,
|
||||
};
|
||||
|
||||
fn open_device(scheme: &mut EvdevScheme, index: usize) -> usize {
|
||||
scheme
|
||||
.open(&format!("event{index}"), 0, 0, 0)
|
||||
.expect("open should succeed")
|
||||
.expect("device handle id")
|
||||
}
|
||||
|
||||
fn read_events(scheme: &mut EvdevScheme, id: usize) -> Option<usize> {
|
||||
let mut buf = vec![0u8; InputEvent::SIZE * 8];
|
||||
scheme.read(id, &mut buf).expect("read should succeed")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eviocgrab_routes_events_only_to_grabbing_handle() {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let first = open_device(&mut scheme, 0);
|
||||
let second = open_device(&mut scheme, 0);
|
||||
|
||||
let grab = 1i32;
|
||||
scheme
|
||||
.fcntl(first, EVIOCGRAB as usize, (&grab as *const i32) as usize)
|
||||
.expect("grab should succeed");
|
||||
|
||||
let err = scheme
|
||||
.fcntl(second, EVIOCGRAB as usize, (&grab as *const i32) as usize)
|
||||
.expect_err("second grab should fail");
|
||||
assert_eq!(err.errno, syscall::error::EBUSY);
|
||||
|
||||
scheme.feed_keyboard_event(0x1E, true);
|
||||
|
||||
assert!(read_events(&mut scheme, first).is_some());
|
||||
assert_eq!(read_events(&mut scheme, second), None);
|
||||
|
||||
let release = 0i32;
|
||||
scheme
|
||||
.fcntl(first, EVIOCGRAB as usize, (&release as *const i32) as usize)
|
||||
.expect("release should succeed");
|
||||
|
||||
scheme.feed_keyboard_event(0x30, true);
|
||||
|
||||
assert!(read_events(&mut scheme, first).is_some());
|
||||
assert!(read_events(&mut scheme, second).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_grabbed_handle_releases_grab() {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let first = open_device(&mut scheme, 0);
|
||||
let second = open_device(&mut scheme, 0);
|
||||
|
||||
let grab = 1i32;
|
||||
scheme
|
||||
.fcntl(first, EVIOCGRAB as usize, (&grab as *const i32) as usize)
|
||||
.expect("grab should succeed");
|
||||
scheme.close(first).expect("close should succeed");
|
||||
|
||||
scheme.feed_keyboard_event(0x1E, true);
|
||||
|
||||
assert!(read_events(&mut scheme, second).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eviocgrab_is_scoped_to_each_device() {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let keyboard = open_device(&mut scheme, 0);
|
||||
let mouse = open_device(&mut scheme, 1);
|
||||
|
||||
let grab = 1i32;
|
||||
|
||||
scheme
|
||||
.fcntl(keyboard, EVIOCGRAB as usize, (&grab as *const i32) as usize)
|
||||
.expect("keyboard grab should succeed");
|
||||
scheme
|
||||
.fcntl(mouse, EVIOCGRAB as usize, (&grab as *const i32) as usize)
|
||||
.expect("mouse grab should also succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eviocgprop_reports_pointer_capability_for_pointer_devices() {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let keyboard = open_device(&mut scheme, 0);
|
||||
let mouse = open_device(&mut scheme, 1);
|
||||
let touchpad = open_device(&mut scheme, 2);
|
||||
|
||||
let mut keyboard_props = [0u8; 64];
|
||||
let mut mouse_props = [0u8; 64];
|
||||
let mut touchpad_props = [0u8; 64];
|
||||
|
||||
let keyboard_len = scheme
|
||||
.fcntl(
|
||||
keyboard,
|
||||
EVIOCGPROP as usize,
|
||||
keyboard_props.as_mut_ptr() as usize,
|
||||
)
|
||||
.expect("keyboard props ioctl should succeed")
|
||||
.expect("keyboard props length");
|
||||
let mouse_len = scheme
|
||||
.fcntl(
|
||||
mouse,
|
||||
EVIOCGPROP as usize,
|
||||
mouse_props.as_mut_ptr() as usize,
|
||||
)
|
||||
.expect("mouse props ioctl should succeed")
|
||||
.expect("mouse props length");
|
||||
let touchpad_len = scheme
|
||||
.fcntl(
|
||||
touchpad,
|
||||
EVIOCGPROP as usize,
|
||||
touchpad_props.as_mut_ptr() as usize,
|
||||
)
|
||||
.expect("touchpad props ioctl should succeed")
|
||||
.expect("touchpad props length");
|
||||
|
||||
let pointer_mask = 1u8 << INPUT_PROP_POINTER;
|
||||
assert_eq!(keyboard_len, 64);
|
||||
assert_eq!(mouse_len, 64);
|
||||
assert_eq!(touchpad_len, 64);
|
||||
assert_eq!(keyboard_props[0] & pointer_mask, 0);
|
||||
assert_ne!(mouse_props[0] & pointer_mask, 0);
|
||||
assert_ne!(touchpad_props[0] & pointer_mask, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eviocsabs_overrides_default_abs_info() {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let touchpad = open_device(&mut scheme, 2);
|
||||
|
||||
let abs_info = AbsInfo {
|
||||
value: 7,
|
||||
minimum: -10,
|
||||
maximum: 1234,
|
||||
fuzz: 2,
|
||||
flat: 3,
|
||||
resolution: 4,
|
||||
};
|
||||
let mut reported = AbsInfo::default();
|
||||
|
||||
scheme
|
||||
.fcntl(
|
||||
touchpad,
|
||||
EVIOCSABS as usize,
|
||||
(&abs_info as *const AbsInfo) as usize,
|
||||
)
|
||||
.expect("set abs info should succeed");
|
||||
scheme
|
||||
.fcntl(
|
||||
touchpad,
|
||||
crate::types::eviocgabs(crate::types::ABS_X as u8) as usize,
|
||||
(&mut reported as *mut AbsInfo) as usize,
|
||||
)
|
||||
.expect("get abs info should succeed");
|
||||
|
||||
assert_eq!(reported.value, abs_info.value);
|
||||
assert_eq!(reported.minimum, abs_info.minimum);
|
||||
assert_eq!(reported.maximum, abs_info.maximum);
|
||||
assert_eq!(reported.fuzz, abs_info.fuzz);
|
||||
assert_eq!(reported.flat, abs_info.flat);
|
||||
assert_eq!(reported.resolution, abs_info.resolution);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multitouch_slot_abs_info_reports_nine_slots() {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let touchpad = open_device(&mut scheme, 2);
|
||||
let mut reported = AbsInfo::default();
|
||||
|
||||
scheme
|
||||
.fcntl(
|
||||
touchpad,
|
||||
crate::types::eviocgabs(ABS_MT_SLOT as u8) as usize,
|
||||
(&mut reported as *mut AbsInfo) as usize,
|
||||
)
|
||||
.expect("get mt slot abs info should succeed");
|
||||
|
||||
assert_eq!(reported.minimum, 0);
|
||||
assert_eq!(reported.maximum, 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn force_feedback_ioctls_return_enotty() {
|
||||
let mut scheme = EvdevScheme::new();
|
||||
let mouse = open_device(&mut scheme, 1);
|
||||
|
||||
for cmd in [
|
||||
EVIOCSFF as usize,
|
||||
EVIOCRMFF as usize,
|
||||
EVIOCGEFFECTS as usize,
|
||||
] {
|
||||
let err = scheme
|
||||
.fcntl(mouse, cmd, 0)
|
||||
.expect_err("force feedback ioctl should fail");
|
||||
assert_eq!(err.errno, syscall::error::ENOTTY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +1,226 @@
|
||||
use crate::types::*;
|
||||
|
||||
fn orb_key_to_evdev(orb_key: u8) -> Option<u16> {
|
||||
let mapped = match orb_key {
|
||||
b'1'..=b'9' => KEY_1 + (orb_key - b'1') as u16,
|
||||
b'0' => KEY_0,
|
||||
b'a'..=b'z' => KEY_A + (orb_key - b'a') as u16,
|
||||
b'\n' | b'\r' => KEY_ENTER,
|
||||
b'\t' => KEY_TAB,
|
||||
b' ' => KEY_SPACE,
|
||||
b'\x08' => KEY_BACKSPACE,
|
||||
b'\x1b' => KEY_ESC,
|
||||
b'-' => KEY_MINUS,
|
||||
b'=' => KEY_EQUAL,
|
||||
b'[' => KEY_LEFTBRACE,
|
||||
b']' => KEY_RIGHTBRACE,
|
||||
b'\\' => KEY_BACKSLASH,
|
||||
b';' => KEY_SEMICOLON,
|
||||
b'\'' => KEY_APOSTROPHE,
|
||||
b'`' => KEY_GRAVE,
|
||||
b',' => KEY_COMMA,
|
||||
b'.' => KEY_DOT,
|
||||
b'/' => KEY_SLASH,
|
||||
pub const KEYBOARD_KEY_CODES: &[u16] = &[
|
||||
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,
|
||||
KEY_F1,
|
||||
KEY_F2,
|
||||
KEY_F3,
|
||||
KEY_F4,
|
||||
KEY_F5,
|
||||
KEY_F6,
|
||||
KEY_F7,
|
||||
KEY_F8,
|
||||
KEY_F9,
|
||||
KEY_F10,
|
||||
KEY_NUMLOCK,
|
||||
KEY_SCROLLLOCK,
|
||||
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_F11,
|
||||
KEY_F12,
|
||||
KEY_KPENTER,
|
||||
KEY_RIGHTCTRL,
|
||||
KEY_KPSLASH,
|
||||
KEY_RIGHTALT,
|
||||
KEY_HOME,
|
||||
KEY_UP,
|
||||
KEY_PAGEUP,
|
||||
KEY_LEFT,
|
||||
KEY_RIGHT,
|
||||
KEY_END,
|
||||
KEY_DOWN,
|
||||
KEY_PAGEDOWN,
|
||||
KEY_INSERT,
|
||||
KEY_DELETE,
|
||||
KEY_LEFTMETA,
|
||||
KEY_RIGHTMETA,
|
||||
KEY_MENU,
|
||||
];
|
||||
|
||||
pub const MOUSE_BUTTON_CODES: &[u16] = &[BTN_LEFT, BTN_RIGHT, BTN_MIDDLE];
|
||||
pub const TOUCHPAD_KEY_CODES: &[u16] = &[BTN_TOUCH, BTN_TOOL_FINGER];
|
||||
|
||||
fn orb_key_to_evdev(scancode: u8) -> Option<u16> {
|
||||
Some(match scancode {
|
||||
0x01 => KEY_ESC,
|
||||
0x02 => KEY_1,
|
||||
0x03 => KEY_2,
|
||||
0x04 => KEY_3,
|
||||
0x05 => KEY_4,
|
||||
0x06 => KEY_5,
|
||||
0x07 => KEY_6,
|
||||
0x08 => KEY_7,
|
||||
0x09 => KEY_8,
|
||||
0x0A => KEY_9,
|
||||
0x0B => KEY_0,
|
||||
0x0C => KEY_MINUS,
|
||||
0x0D => KEY_EQUAL,
|
||||
0x0E => KEY_BACKSPACE,
|
||||
0x0F => KEY_TAB,
|
||||
0x10 => KEY_Q,
|
||||
0x11 => KEY_W,
|
||||
0x12 => KEY_E,
|
||||
0x13 => KEY_R,
|
||||
0x14 => KEY_T,
|
||||
0x15 => KEY_Y,
|
||||
0x16 => KEY_U,
|
||||
0x17 => KEY_I,
|
||||
0x18 => KEY_O,
|
||||
0x19 => KEY_P,
|
||||
0x1A => KEY_LEFTBRACE,
|
||||
0x1B => KEY_RIGHTBRACE,
|
||||
0x1C => KEY_ENTER,
|
||||
0x1D => KEY_LEFTCTRL,
|
||||
0x1E => KEY_A,
|
||||
0x1F => KEY_S,
|
||||
0x20 => KEY_D,
|
||||
0x21 => KEY_F,
|
||||
0x22 => KEY_G,
|
||||
0x23 => KEY_H,
|
||||
0x24 => KEY_J,
|
||||
0x25 => KEY_K,
|
||||
0x26 => KEY_L,
|
||||
0x27 => KEY_SEMICOLON,
|
||||
0x28 => KEY_APOSTROPHE,
|
||||
0x29 => KEY_GRAVE,
|
||||
0x2A => KEY_LEFTSHIFT,
|
||||
0x2B => KEY_BACKSLASH,
|
||||
0x2C => KEY_Z,
|
||||
0x2D => KEY_X,
|
||||
0x2E => KEY_C,
|
||||
0x2F => KEY_V,
|
||||
0x30 => KEY_B,
|
||||
0x31 => KEY_N,
|
||||
0x32 => KEY_M,
|
||||
0x33 => KEY_COMMA,
|
||||
0x34 => KEY_DOT,
|
||||
0x35 => KEY_SLASH,
|
||||
0x36 => KEY_RIGHTSHIFT,
|
||||
0x37 => KEY_KPASTERISK,
|
||||
0x38 => KEY_LEFTALT,
|
||||
0x39 => KEY_SPACE,
|
||||
0x3A => KEY_CAPSLOCK,
|
||||
0x3B => KEY_F1,
|
||||
0x3C => KEY_F2,
|
||||
0x3D => KEY_F3,
|
||||
0x3E => KEY_F4,
|
||||
0x3F => KEY_F5,
|
||||
0x40 => KEY_F6,
|
||||
0x41 => KEY_F7,
|
||||
0x42 => KEY_F8,
|
||||
0x43 => KEY_F9,
|
||||
0x44 => KEY_F10,
|
||||
0x45 => KEY_NUMLOCK,
|
||||
0x46 => KEY_SCROLLLOCK,
|
||||
0x47 => KEY_HOME,
|
||||
0x48 => KEY_UP,
|
||||
0x49 => KEY_PAGEUP,
|
||||
0x4B => KEY_LEFT,
|
||||
0x4D => KEY_RIGHT,
|
||||
0x4F => KEY_END,
|
||||
0x50 => KEY_DOWN,
|
||||
0x51 => KEY_PAGEDOWN,
|
||||
0x52 => KEY_INSERT,
|
||||
0x53 => KEY_DELETE,
|
||||
0x57 => KEY_F11,
|
||||
0x58 => KEY_F12,
|
||||
0x5B => KEY_LEFTMETA,
|
||||
0x5C => KEY_RIGHTMETA,
|
||||
0x5D => KEY_MENU,
|
||||
0x64 => KEY_RIGHTCTRL,
|
||||
0x70 => KEY_KP0,
|
||||
0x71 => KEY_KP1,
|
||||
0x72 => KEY_KP2,
|
||||
0x73 => KEY_KP3,
|
||||
0x74 => KEY_KP4,
|
||||
0x75 => KEY_KP5,
|
||||
0x76 => KEY_KP6,
|
||||
0x77 => KEY_KP7,
|
||||
0x78 => KEY_KP8,
|
||||
0x79 => KEY_KP9,
|
||||
0x7A => KEY_KPDOT,
|
||||
0x7B => KEY_KPMINUS,
|
||||
0x7C => KEY_KPPLUS,
|
||||
0x7D => KEY_KPASTERISK,
|
||||
0x7E => KEY_KPSLASH,
|
||||
0x7F => KEY_KPENTER,
|
||||
_ => return None,
|
||||
};
|
||||
Some(mapped)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn translate_keyboard(orb_key: u8, pressed: bool) -> Vec<InputEvent> {
|
||||
pub fn translate_keyboard(scancode: u8, pressed: bool) -> Vec<InputEvent> {
|
||||
let value = if pressed { 1 } else { 0 };
|
||||
match orb_key_to_evdev(orb_key) {
|
||||
match orb_key_to_evdev(scancode) {
|
||||
Some(code) => vec![
|
||||
InputEvent::new(EV_MSC, MSC_SCAN, i32::from(scancode)),
|
||||
InputEvent::new(EV_KEY, code, value),
|
||||
InputEvent::syn_report(),
|
||||
],
|
||||
@@ -37,19 +228,32 @@ pub fn translate_keyboard(orb_key: u8, pressed: bool) -> Vec<InputEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translate_mouse_dx(dx: i32) -> Vec<InputEvent> {
|
||||
vec![InputEvent::new(EV_REL, REL_X, dx), InputEvent::syn_report()]
|
||||
pub fn translate_mouse_motion(dx: i32, dy: i32) -> Vec<InputEvent> {
|
||||
let mut events = Vec::new();
|
||||
if dx != 0 {
|
||||
events.push(InputEvent::new(EV_REL, REL_X, dx));
|
||||
}
|
||||
if dy != 0 {
|
||||
events.push(InputEvent::new(EV_REL, REL_Y, dy));
|
||||
}
|
||||
if !events.is_empty() {
|
||||
events.push(InputEvent::syn_report());
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
pub fn translate_mouse_dy(dy: i32) -> Vec<InputEvent> {
|
||||
vec![InputEvent::new(EV_REL, REL_Y, dy), InputEvent::syn_report()]
|
||||
}
|
||||
|
||||
pub fn translate_mouse_scroll(y: i32) -> Vec<InputEvent> {
|
||||
vec![
|
||||
InputEvent::new(EV_REL, REL_WHEEL, y),
|
||||
InputEvent::syn_report(),
|
||||
]
|
||||
pub fn translate_mouse_scroll(x: i32, y: i32) -> Vec<InputEvent> {
|
||||
let mut events = Vec::new();
|
||||
if x != 0 {
|
||||
events.push(InputEvent::new(EV_REL, REL_HWHEEL, x));
|
||||
}
|
||||
if y != 0 {
|
||||
events.push(InputEvent::new(EV_REL, REL_WHEEL, y));
|
||||
}
|
||||
if !events.is_empty() {
|
||||
events.push(InputEvent::syn_report());
|
||||
}
|
||||
events
|
||||
}
|
||||
|
||||
pub fn translate_mouse_button(button: usize, pressed: bool) -> Vec<InputEvent> {
|
||||
@@ -68,10 +272,206 @@ pub fn translate_mouse_button(button: usize, pressed: bool) -> Vec<InputEvent> {
|
||||
]
|
||||
}
|
||||
|
||||
pub fn translate_touch(x: i32, y: i32, touching: bool) -> Vec<InputEvent> {
|
||||
let btn = InputEvent::new(EV_KEY, BTN_TOUCH, if touching { 1 } else { 0 });
|
||||
let abs_x = InputEvent::new(EV_ABS, ABS_X, x);
|
||||
let abs_y = InputEvent::new(EV_ABS, ABS_Y, y);
|
||||
let syn = InputEvent::syn_report();
|
||||
vec![btn, abs_x, abs_y, syn]
|
||||
pub fn translate_touchpad_motion(
|
||||
x: i32,
|
||||
y: i32,
|
||||
touching: bool,
|
||||
tracking_id: i32,
|
||||
) -> Vec<InputEvent> {
|
||||
let mut events = vec![
|
||||
InputEvent::new(EV_ABS, ABS_X, x),
|
||||
InputEvent::new(EV_ABS, ABS_Y, y),
|
||||
];
|
||||
|
||||
if touching {
|
||||
events.extend_from_slice(&[
|
||||
InputEvent::new(EV_ABS, ABS_MT_SLOT, 0),
|
||||
InputEvent::new(EV_ABS, ABS_MT_TRACKING_ID, tracking_id),
|
||||
InputEvent::new(EV_ABS, ABS_MT_POSITION_X, x),
|
||||
InputEvent::new(EV_ABS, ABS_MT_POSITION_Y, y),
|
||||
InputEvent::new(EV_ABS, ABS_PRESSURE, 255),
|
||||
InputEvent::new(EV_ABS, ABS_MT_TOUCH_MAJOR, 1),
|
||||
]);
|
||||
}
|
||||
|
||||
events.push(InputEvent::syn_report());
|
||||
events
|
||||
}
|
||||
|
||||
pub fn translate_touchpad_contact(
|
||||
x: i32,
|
||||
y: i32,
|
||||
touching: bool,
|
||||
tracking_id: i32,
|
||||
) -> Vec<InputEvent> {
|
||||
let mut events = vec![
|
||||
InputEvent::new(EV_ABS, ABS_X, x),
|
||||
InputEvent::new(EV_ABS, ABS_Y, y),
|
||||
InputEvent::new(EV_ABS, ABS_MT_SLOT, 0),
|
||||
];
|
||||
|
||||
if touching {
|
||||
events.extend_from_slice(&[
|
||||
InputEvent::new(EV_KEY, BTN_TOUCH, 1),
|
||||
InputEvent::new(EV_KEY, BTN_TOOL_FINGER, 1),
|
||||
InputEvent::new(EV_ABS, ABS_MT_TRACKING_ID, tracking_id),
|
||||
InputEvent::new(EV_ABS, ABS_MT_POSITION_X, x),
|
||||
InputEvent::new(EV_ABS, ABS_MT_POSITION_Y, y),
|
||||
InputEvent::new(EV_ABS, ABS_PRESSURE, 255),
|
||||
InputEvent::new(EV_ABS, ABS_MT_TOUCH_MAJOR, 1),
|
||||
]);
|
||||
} else {
|
||||
events.extend_from_slice(&[
|
||||
InputEvent::new(EV_ABS, ABS_MT_TRACKING_ID, -1),
|
||||
InputEvent::new(EV_ABS, ABS_PRESSURE, 0),
|
||||
InputEvent::new(EV_ABS, ABS_MT_TOUCH_MAJOR, 0),
|
||||
InputEvent::new(EV_KEY, BTN_TOUCH, 0),
|
||||
InputEvent::new(EV_KEY, BTN_TOOL_FINGER, 0),
|
||||
]);
|
||||
}
|
||||
|
||||
events.push(InputEvent::syn_report());
|
||||
events
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
translate_keyboard, translate_mouse_button, translate_mouse_motion, translate_mouse_scroll,
|
||||
translate_touchpad_motion,
|
||||
};
|
||||
use crate::types::*;
|
||||
|
||||
fn has_event(events: &[InputEvent], event_type: u16, code: u16, value: i32) -> bool {
|
||||
events.iter().any(|event| {
|
||||
event.event_type == event_type && event.code == code && event.value == value
|
||||
})
|
||||
}
|
||||
|
||||
fn has_event_code(events: &[InputEvent], event_type: u16, code: u16) -> bool {
|
||||
events
|
||||
.iter()
|
||||
.any(|event| event.event_type == event_type && event.code == code)
|
||||
}
|
||||
|
||||
fn event_index(events: &[InputEvent], event_type: u16, code: u16, value: i32) -> Option<usize> {
|
||||
events.iter().position(|event| {
|
||||
event.event_type == event_type && event.code == code && event.value == value
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyboard_press_translates_to_key_a_down() {
|
||||
let events = translate_keyboard(0x1E, true);
|
||||
|
||||
assert!(has_event(&events, EV_KEY, KEY_A, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyboard_release_translates_to_key_a_up() {
|
||||
let events = translate_keyboard(0x1E, false);
|
||||
|
||||
assert!(has_event(&events, EV_KEY, KEY_A, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyboard_events_include_scan_before_key() {
|
||||
let events = translate_keyboard(0x1E, true);
|
||||
let scan_index = event_index(&events, EV_MSC, MSC_SCAN, 0x1E).unwrap();
|
||||
let key_index = event_index(&events, EV_KEY, KEY_A, 1).unwrap();
|
||||
|
||||
assert!(scan_index < key_index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyboard_events_end_with_syn_report() {
|
||||
let events = translate_keyboard(0x1E, true);
|
||||
|
||||
let last = events
|
||||
.last()
|
||||
.expect("keyboard translation should emit events");
|
||||
assert_eq!(last.event_type, EV_SYN);
|
||||
assert_eq!(last.code, SYN_REPORT);
|
||||
assert_eq!(last.value, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_keyboard_scancode_returns_empty_events() {
|
||||
let events = translate_keyboard(0xFF, true);
|
||||
|
||||
assert!(events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_motion_x_only_emits_x_and_syn() {
|
||||
let events = translate_mouse_motion(10, 0);
|
||||
|
||||
assert!(has_event(&events, EV_REL, REL_X, 10));
|
||||
assert!(!has_event_code(&events, EV_REL, REL_Y));
|
||||
assert_eq!(
|
||||
events
|
||||
.last()
|
||||
.map(|event| (event.event_type, event.code, event.value)),
|
||||
Some((EV_SYN, SYN_REPORT, 0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_motion_x_and_y_emits_both_axes() {
|
||||
let events = translate_mouse_motion(5, -3);
|
||||
|
||||
assert!(has_event(&events, EV_REL, REL_X, 5));
|
||||
assert!(has_event(&events, EV_REL, REL_Y, -3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_motion_zero_returns_empty_events() {
|
||||
let events = translate_mouse_motion(0, 0);
|
||||
|
||||
assert!(events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_scroll_up_emits_vertical_wheel() {
|
||||
let events = translate_mouse_scroll(0, 1);
|
||||
|
||||
assert!(has_event(&events, EV_REL, REL_WHEEL, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_scroll_horizontal_emits_horizontal_wheel() {
|
||||
let events = translate_mouse_scroll(2, 0);
|
||||
|
||||
assert!(has_event(&events, EV_REL, REL_HWHEEL, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_button_left_press_emits_btn_left_down() {
|
||||
let events = translate_mouse_button(0, true);
|
||||
|
||||
assert!(has_event(&events, EV_KEY, BTN_LEFT, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_button_right_release_emits_btn_right_up() {
|
||||
let events = translate_mouse_button(2, false);
|
||||
|
||||
assert!(has_event(&events, EV_KEY, BTN_RIGHT, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_mouse_button_returns_empty_events() {
|
||||
let events = translate_mouse_button(10, true);
|
||||
|
||||
assert!(events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn touchpad_motion_emits_absolute_contact_details() {
|
||||
let events = translate_touchpad_motion(100, 200, true, 1);
|
||||
|
||||
assert!(has_event(&events, EV_ABS, ABS_X, 100));
|
||||
assert!(has_event(&events, EV_ABS, ABS_Y, 200));
|
||||
assert!(has_event(&events, EV_ABS, ABS_MT_TRACKING_ID, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// Linux-compatible evdev event types and constants.
|
||||
///
|
||||
/// These mirror the Linux kernel's `include/uapi/linux/input.h` definitions
|
||||
/// so that clients expecting evdev semantics can work on Redox.
|
||||
use std::mem::size_of;
|
||||
|
||||
// Event types
|
||||
pub const EV_SYN: u16 = 0x00;
|
||||
@@ -9,14 +12,51 @@ pub const EV_KEY: u16 = 0x01;
|
||||
pub const EV_REL: u16 = 0x02;
|
||||
pub const EV_ABS: u16 = 0x03;
|
||||
pub const EV_MSC: u16 = 0x04;
|
||||
pub const EV_SW: u16 = 0x05;
|
||||
pub const EV_LED: u16 = 0x11;
|
||||
pub const EV_SND: u16 = 0x12;
|
||||
pub const EV_REP: u16 = 0x14;
|
||||
pub const EV_FF: u16 = 0x15;
|
||||
|
||||
// Synchronization events
|
||||
pub const SYN_REPORT: u16 = 0;
|
||||
pub const SYN_CONFIG: u16 = 1;
|
||||
|
||||
// Misc events
|
||||
pub const MSC_SCAN: u16 = 0x04;
|
||||
|
||||
// Switch events
|
||||
pub const SW_LID: u16 = 0x00;
|
||||
pub const SW_TABLET_MODE: u16 = 0x01;
|
||||
pub const SW_HEADPHONE_INSERT: u16 = 0x02;
|
||||
pub const SW_RFKILL_ALL: u16 = 0x03;
|
||||
pub const SW_MICROPHONE_INSERT: u16 = 0x04;
|
||||
pub const SW_DOCK: u16 = 0x05;
|
||||
pub const SW_LINEOUT_INSERT: u16 = 0x06;
|
||||
pub const SW_JACK_PHYSICAL_INSERT: u16 = 0x07;
|
||||
pub const SW_VIDEOOUT_INSERT: u16 = 0x08;
|
||||
pub const SW_CAMERA_LENS_COVER: u16 = 0x09;
|
||||
pub const SW_KEYPAD_SLIDE: u16 = 0x0a;
|
||||
pub const SW_FRONT_PROXIMITY: u16 = 0x0b;
|
||||
pub const SW_ROTATE_LOCK: u16 = 0x0c;
|
||||
pub const SW_LINEIN_INSERT: u16 = 0x0d;
|
||||
pub const SW_MUTE_DEVICE: u16 = 0x0e;
|
||||
pub const SW_PEN_INSERTED: u16 = 0x0f;
|
||||
pub const SW_MACHINE_COVER: u16 = 0x10;
|
||||
|
||||
// Input properties
|
||||
pub const INPUT_PROP_POINTER: u16 = 0x00;
|
||||
pub const INPUT_PROP_DIRECT: u16 = 0x01;
|
||||
|
||||
// LEDs
|
||||
pub const LED_NUML: u16 = 0x00;
|
||||
pub const LED_CAPSL: u16 = 0x01;
|
||||
pub const LED_SCROLLL: u16 = 0x02;
|
||||
|
||||
// Repeat settings
|
||||
pub const REP_DELAY: u16 = 0x00;
|
||||
pub const REP_PERIOD: u16 = 0x01;
|
||||
|
||||
// Relative axes
|
||||
pub const REL_X: u16 = 0x00;
|
||||
pub const REL_Y: u16 = 0x01;
|
||||
@@ -107,8 +147,22 @@ 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;
|
||||
pub const KEY_F11: u16 = 87;
|
||||
pub const KEY_F12: u16 = 88;
|
||||
pub const KEY_KPENTER: u16 = 96;
|
||||
|
||||
pub const KEY_HOME: u16 = 102;
|
||||
pub const KEY_UP: u16 = 103;
|
||||
@@ -120,6 +174,8 @@ pub const KEY_DOWN: u16 = 108;
|
||||
pub const KEY_PAGEDOWN: u16 = 109;
|
||||
pub const KEY_INSERT: u16 = 110;
|
||||
pub const KEY_DELETE: u16 = 111;
|
||||
pub const KEY_KPSLASH: u16 = 98;
|
||||
pub const KEY_MENU: u16 = 139;
|
||||
|
||||
pub const KEY_LEFTMETA: u16 = 125;
|
||||
pub const KEY_RIGHTMETA: u16 = 126;
|
||||
@@ -145,6 +201,95 @@ pub const BUS_VIRTUAL: u16 = 0x06;
|
||||
// Evdev version
|
||||
pub const EV_VERSION: i32 = 0x010001;
|
||||
|
||||
// ioctl constants
|
||||
pub const EVIOCGVERSION: u64 = 0x80044501;
|
||||
pub const EVIOCGID: u64 = 0x80084502;
|
||||
pub const EVIOCGNAME: u64 = 0x80000000 | 0x45 << 8;
|
||||
pub const EVIOCGBIT: u64 = 0x80000000 | 0x45 << 8;
|
||||
pub const EVIOCGABS: u64 = 0x80184540;
|
||||
pub const EVIOCSABS: u64 = 0x401845c0;
|
||||
pub const EVIOCGRAB: u64 = 0x40044590;
|
||||
pub const EVIOCSCLOCKID: u64 = 0x400445a0;
|
||||
pub const EVIOCGPROP: u64 = 0x804045a0;
|
||||
pub const EVIOCSFF: u64 = 0x402c4580;
|
||||
pub const EVIOCRMFF: u64 = 0x40044581;
|
||||
pub const EVIOCGEFFECTS: u64 = 0x80044584;
|
||||
|
||||
// EVIOCGKEY returns the current state of all keys (bitmask of pressed keys)
|
||||
pub const EVIOCGKEY: u64 = (IOC_READ << IOC_DIRSHIFT)
|
||||
| ((KEY_MAX / 8 + 1) as u64) << IOC_SIZESHIFT
|
||||
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
|
||||
| 0x18;
|
||||
|
||||
// EVIOCGLED returns the current state of all LEDs (bitmask of lit LEDs)
|
||||
pub const EVIOCGLED: u64 = (IOC_READ << IOC_DIRSHIFT)
|
||||
| ((LED_MAX / 8 + 1) as u64) << IOC_SIZESHIFT
|
||||
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
|
||||
| 0x19;
|
||||
|
||||
// Key and LED state bitmaps
|
||||
pub const KEY_MAX: usize = 0x2FF; // 767 bits = 96 bytes
|
||||
pub const LED_MAX: usize = 0x0F; // 15 bits = 2 bytes
|
||||
|
||||
pub const IOC_NRBITS: u64 = 8;
|
||||
pub const IOC_TYPEBITS: u64 = 8;
|
||||
pub const IOC_SIZEBITS: u64 = 14;
|
||||
|
||||
pub const IOC_NRMASK: u64 = (1 << IOC_NRBITS) - 1;
|
||||
pub const IOC_TYPEMASK: u64 = (1 << IOC_TYPEBITS) - 1;
|
||||
pub const IOC_SIZEMASK: u64 = (1 << IOC_SIZEBITS) - 1;
|
||||
|
||||
pub const IOC_NRSHIFT: u64 = 0;
|
||||
pub const IOC_TYPESHIFT: u64 = IOC_NRSHIFT + IOC_NRBITS;
|
||||
pub const IOC_SIZESHIFT: u64 = IOC_TYPESHIFT + IOC_TYPEBITS;
|
||||
pub const IOC_DIRSHIFT: u64 = IOC_SIZESHIFT + IOC_SIZEBITS;
|
||||
|
||||
pub const IOC_NONE: u64 = 0;
|
||||
pub const IOC_WRITE: u64 = 1;
|
||||
pub const IOC_READ: u64 = 2;
|
||||
pub const EVDEV_IOCTL_TYPE: u64 = 0x45;
|
||||
|
||||
pub const fn ioc_dir(cmd: u64) -> u64 {
|
||||
cmd >> IOC_DIRSHIFT
|
||||
}
|
||||
|
||||
pub const fn ioc_type(cmd: u64) -> u64 {
|
||||
(cmd >> IOC_TYPESHIFT) & IOC_TYPEMASK
|
||||
}
|
||||
|
||||
pub const fn ioc_nr(cmd: u64) -> u64 {
|
||||
(cmd >> IOC_NRSHIFT) & IOC_NRMASK
|
||||
}
|
||||
|
||||
pub const fn ioc_size(cmd: u64) -> usize {
|
||||
((cmd >> IOC_SIZESHIFT) & IOC_SIZEMASK) as usize
|
||||
}
|
||||
|
||||
pub const fn is_evdev_ioctl(cmd: u64) -> bool {
|
||||
ioc_type(cmd) == EVDEV_IOCTL_TYPE
|
||||
}
|
||||
|
||||
pub const fn eviocgname(len: usize) -> u64 {
|
||||
(IOC_READ << IOC_DIRSHIFT)
|
||||
| ((len as u64) << IOC_SIZESHIFT)
|
||||
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
|
||||
| 0x06
|
||||
}
|
||||
|
||||
pub const fn eviocgbit(ev: u8, len: usize) -> u64 {
|
||||
(IOC_READ << IOC_DIRSHIFT)
|
||||
| ((len as u64) << IOC_SIZESHIFT)
|
||||
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
|
||||
| ((0x20 + ev as u64) << IOC_NRSHIFT)
|
||||
}
|
||||
|
||||
pub const fn eviocgabs(axis: u8) -> u64 {
|
||||
(IOC_READ << IOC_DIRSHIFT)
|
||||
| ((size_of::<AbsInfo>() as u64) << IOC_SIZESHIFT)
|
||||
| (EVDEV_IOCTL_TYPE << IOC_TYPESHIFT)
|
||||
| ((0x40 + axis as u64) << IOC_NRSHIFT)
|
||||
}
|
||||
|
||||
/// Linux `struct input_event` layout (24 bytes).
|
||||
///
|
||||
/// Matches the kernel binary layout:
|
||||
@@ -203,6 +348,17 @@ pub struct InputId {
|
||||
pub version: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct AbsInfo {
|
||||
pub value: i32,
|
||||
pub minimum: i32,
|
||||
pub maximum: i32,
|
||||
pub fuzz: i32,
|
||||
pub flat: i32,
|
||||
pub resolution: i32,
|
||||
}
|
||||
|
||||
fn now_timestamp() -> (u64, u64) {
|
||||
use std::time::SystemTime;
|
||||
let dur = SystemTime::now()
|
||||
|
||||
@@ -4,21 +4,7 @@
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "custom"
|
||||
script = """
|
||||
# Build the firmware-loader daemon
|
||||
COOKBOOK_CARGO_PATH=. cookbook_cargo
|
||||
|
||||
# Stage firmware blobs (copied by integrate-redbear.sh from local/firmware/amdgpu/)
|
||||
if [ -d "${COOKBOOK_SOURCE}/firmware/amdgpu" ]; then
|
||||
AMD_FW_COUNT=$(ls "${COOKBOOK_SOURCE}/firmware/amdgpu/"*.bin 2>/dev/null | wc -l)
|
||||
if [ "${AMD_FW_COUNT}" -gt 0 ]; then
|
||||
mkdir -p "${COOKBOOK_STAGE}/usr/lib/firmware/amdgpu"
|
||||
cp "${COOKBOOK_SOURCE}/firmware/amdgpu/"*.bin "${COOKBOOK_STAGE}/usr/lib/firmware/amdgpu/"
|
||||
echo "Staged ${AMD_FW_COUNT} AMD firmware blobs"
|
||||
fi
|
||||
fi
|
||||
"""
|
||||
template = "cargo"
|
||||
|
||||
[package.files]
|
||||
"/usr/lib/drivers/firmware-loader" = "firmware-loader"
|
||||
"/usr/bin/firmware-loader" = "firmware-loader"
|
||||
|
||||
@@ -4,8 +4,9 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
redox_syscall = { version = "0.7", features = ["std"] }
|
||||
syscall04 = { package = "redox_syscall", version = "0.4" }
|
||||
redox_scheme = { package = "redox-scheme", version = "0.1" }
|
||||
libc = "0.2"
|
||||
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
|
||||
redox_scheme = { package = "redox-scheme", version = "0.11" }
|
||||
libredox = "0.1"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
thiserror = "2"
|
||||
|
||||
@@ -103,6 +103,14 @@ impl FirmwareRegistry {
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.blobs.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.blobs.is_empty()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn list_keys(&self) -> Vec<&str> {
|
||||
self.blobs.keys().map(|s| s.as_str()).collect()
|
||||
|
||||
@@ -2,11 +2,12 @@ mod blob;
|
||||
mod scheme;
|
||||
|
||||
use std::env;
|
||||
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use log::{error, info, LevelFilter, Metadata, Record};
|
||||
use redox_scheme::{SignalBehavior, Socket};
|
||||
use redox_scheme::{scheme::SchemeSync, SignalBehavior, Socket};
|
||||
|
||||
use blob::FirmwareRegistry;
|
||||
use scheme::FirmwareScheme;
|
||||
@@ -40,52 +41,59 @@ fn default_firmware_dir() -> PathBuf {
|
||||
PathBuf::from("/usr/firmware/")
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let firmware_dir = env::var("FIRMWARE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| default_firmware_dir());
|
||||
unsafe fn get_init_notify_fd() -> RawFd {
|
||||
let fd: RawFd = env::var("INIT_NOTIFY")
|
||||
.expect("firmware-loader: INIT_NOTIFY not set")
|
||||
.parse()
|
||||
.expect("firmware-loader: INIT_NOTIFY is not a valid fd");
|
||||
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
|
||||
fd
|
||||
}
|
||||
|
||||
info!(
|
||||
"firmware-loader: starting with directory {}",
|
||||
firmware_dir.display()
|
||||
);
|
||||
fn notify_scheme_ready(notify_fd: RawFd, socket: &Socket, scheme: &mut FirmwareScheme) {
|
||||
let cap_id = scheme
|
||||
.scheme_root()
|
||||
.expect("firmware-loader: scheme_root failed");
|
||||
let cap_fd = socket
|
||||
.create_this_scheme_fd(0, cap_id, 0, 0)
|
||||
.expect("firmware-loader: create_this_scheme_fd failed");
|
||||
|
||||
let registry = FirmwareRegistry::new(&firmware_dir)
|
||||
.map_err(|e| format!("failed to initialize firmware registry: {e}"))?;
|
||||
syscall::call_wo(
|
||||
notify_fd as usize,
|
||||
&libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(),
|
||||
syscall::CallFlags::FD,
|
||||
&[],
|
||||
)
|
||||
.expect("firmware-loader: failed to notify init that scheme is ready");
|
||||
}
|
||||
|
||||
fn run_daemon(notify_fd: RawFd, registry: FirmwareRegistry) -> ! {
|
||||
let socket = Socket::create().expect("firmware-loader: failed to create scheme socket");
|
||||
let mut scheme = FirmwareScheme::new(registry);
|
||||
|
||||
notify_scheme_ready(notify_fd, &socket, &mut scheme);
|
||||
|
||||
let socket = Socket::create("firmware")
|
||||
.map_err(|e| format!("failed to register firmware scheme: {e}"))?;
|
||||
info!("firmware-loader: registered scheme:firmware");
|
||||
|
||||
let mut firmware_scheme = FirmwareScheme::new(registry);
|
||||
libredox::call::setrens(0, 0).expect("firmware-loader: failed to enter null namespace");
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => {
|
||||
info!("firmware-loader: scheme unmounted, exiting");
|
||||
break;
|
||||
while let Some(request) = socket
|
||||
.next_request(SignalBehavior::Restart)
|
||||
.expect("firmware-loader: failed to read scheme request")
|
||||
{
|
||||
match request.kind() {
|
||||
redox_scheme::RequestKind::Call(request) => {
|
||||
let mut state = redox_scheme::scheme::SchemeState::new();
|
||||
let response = request.handle_sync(&mut scheme, &mut state);
|
||||
socket
|
||||
.write_response(response, SignalBehavior::Restart)
|
||||
.expect("firmware-loader: failed to write response");
|
||||
}
|
||||
Err(e) => {
|
||||
error!("firmware-loader: failed to read scheme request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let response = match request.handle_scheme_block_mut(&mut firmware_scheme) {
|
||||
Ok(response) => response,
|
||||
Err(_request) => {
|
||||
error!("firmware-loader: failed to handle request");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
|
||||
error!("firmware-loader: failed to write response: {}", e);
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
process::exit(0);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -99,8 +107,26 @@ fn main() {
|
||||
|
||||
init_logging(log_level);
|
||||
|
||||
if let Err(e) = run() {
|
||||
error!("firmware-loader: fatal error: {}", e);
|
||||
let firmware_dir = env::var("FIRMWARE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| default_firmware_dir());
|
||||
|
||||
info!(
|
||||
"firmware-loader: starting with directory {}",
|
||||
firmware_dir.display()
|
||||
);
|
||||
|
||||
let registry = FirmwareRegistry::new(&firmware_dir).unwrap_or_else(|e| {
|
||||
error!("firmware-loader: fatal error: failed to initialize firmware registry: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
info!(
|
||||
"firmware-loader: indexed {} firmware blob(s) from {}",
|
||||
registry.len(),
|
||||
firmware_dir.display()
|
||||
);
|
||||
|
||||
let notify_fd = unsafe { get_init_notify_fd() };
|
||||
run_daemon(notify_fd, registry);
|
||||
}
|
||||
|
||||
@@ -2,17 +2,19 @@ use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::warn;
|
||||
use redox_scheme::SchemeBlockMut;
|
||||
use syscall04::data::Stat;
|
||||
use syscall04::error::{Error, Result, EBADF, EINVAL, EISDIR, ENOENT, EROFS};
|
||||
use syscall04::flag::{EventFlags, MapFlags, MunmapFlags, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
|
||||
use redox_scheme::scheme::SchemeSync;
|
||||
use redox_scheme::{CallerCtx, OpenResult};
|
||||
use syscall::error::*;
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
use syscall::{EventFlags, Stat, MODE_FILE};
|
||||
|
||||
use crate::blob::FirmwareRegistry;
|
||||
|
||||
const SCHEME_ROOT_ID: usize = 1;
|
||||
|
||||
struct Handle {
|
||||
blob_key: String,
|
||||
data: Arc<Vec<u8>>,
|
||||
offset: u64,
|
||||
map_count: usize,
|
||||
closed: bool,
|
||||
}
|
||||
@@ -27,10 +29,18 @@ impl FirmwareScheme {
|
||||
pub fn new(registry: FirmwareRegistry) -> Self {
|
||||
FirmwareScheme {
|
||||
registry,
|
||||
next_id: 0,
|
||||
next_id: SCHEME_ROOT_ID + 1,
|
||||
handles: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(&self, id: usize) -> Result<&Handle> {
|
||||
self.handles.get(&id).ok_or(Error::new(EBADF))
|
||||
}
|
||||
|
||||
fn handle_mut(&mut self, id: usize) -> Result<&mut Handle> {
|
||||
self.handles.get_mut(&id).ok_or(Error::new(EBADF))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_key(path: &str) -> Option<String> {
|
||||
@@ -65,8 +75,23 @@ fn resolve_key(path: &str) -> Option<String> {
|
||||
Some(key)
|
||||
}
|
||||
|
||||
impl SchemeBlockMut for FirmwareScheme {
|
||||
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
|
||||
impl SchemeSync for FirmwareScheme {
|
||||
fn scheme_root(&mut self) -> Result<usize> {
|
||||
Ok(SCHEME_ROOT_ID)
|
||||
}
|
||||
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
path: &str,
|
||||
_flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<OpenResult> {
|
||||
if dirfd != SCHEME_ROOT_ID {
|
||||
return Err(Error::new(EACCES));
|
||||
}
|
||||
|
||||
let key = resolve_key(path).ok_or(Error::new(EISDIR))?;
|
||||
|
||||
if !self.registry.contains(&key) {
|
||||
@@ -87,94 +112,95 @@ impl SchemeBlockMut for FirmwareScheme {
|
||||
Handle {
|
||||
blob_key: key,
|
||||
data,
|
||||
offset: 0,
|
||||
map_count: 0,
|
||||
closed: false,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Some(id))
|
||||
Ok(OpenResult::ThisScheme {
|
||||
number: id,
|
||||
flags: NewFdFlags::empty(),
|
||||
})
|
||||
}
|
||||
|
||||
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let len = handle.data.len() as i64;
|
||||
let new_offset = match whence {
|
||||
SEEK_SET => pos as i64,
|
||||
SEEK_CUR => handle.offset as i64 + pos as i64,
|
||||
SEEK_END => len + pos as i64,
|
||||
_ => return Err(Error::new(EINVAL)),
|
||||
};
|
||||
if new_offset < 0 {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
handle.offset = new_offset as u64;
|
||||
let new_offset = isize::try_from(new_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
Ok(Some(new_offset))
|
||||
}
|
||||
|
||||
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let offset = handle.offset as usize;
|
||||
fn read(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &mut [u8],
|
||||
offset: u64,
|
||||
_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let handle = self.handle(id)?;
|
||||
let offset = usize::try_from(offset).map_err(|_| Error::new(EINVAL))?;
|
||||
let data = &handle.data;
|
||||
|
||||
if offset >= data.len() {
|
||||
return Ok(Some(0));
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let available = data.len() - offset;
|
||||
let to_copy = available.min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&data[offset..offset + to_copy]);
|
||||
handle.offset += to_copy as u64;
|
||||
|
||||
Ok(Some(to_copy))
|
||||
Ok(to_copy)
|
||||
}
|
||||
|
||||
fn write(&mut self, id: usize, _buf: &[u8]) -> Result<Option<usize>> {
|
||||
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
fn write(
|
||||
&mut self,
|
||||
id: usize,
|
||||
_buf: &[u8],
|
||||
_offset: u64,
|
||||
_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let _ = self.handle(id)?;
|
||||
Err(Error::new(EROFS))
|
||||
}
|
||||
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
|
||||
let handle = self.handle(id)?;
|
||||
let path = format!("firmware:/{}.bin", handle.blob_key);
|
||||
let bytes = path.as_bytes();
|
||||
let len = bytes.len().min(buf.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(Some(len))
|
||||
Ok(len)
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> {
|
||||
let handle = self.handle(id)?;
|
||||
stat.st_mode = MODE_FILE | 0o444;
|
||||
stat.st_size = handle.data.len() as u64;
|
||||
stat.st_blksize = 4096;
|
||||
stat.st_blocks = (handle.data.len() as u64 + 511) / 512;
|
||||
Ok(Some(0))
|
||||
stat.st_nlink = 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fsync(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
if !self.handles.contains_key(&id) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
Ok(Some(0))
|
||||
fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> {
|
||||
let _ = self.handle(id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
|
||||
if !self.handles.contains_key(&id) {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
Ok(Some(EventFlags::empty()))
|
||||
fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result<usize> {
|
||||
let _ = self.handle(id)?;
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
handle.closed = true;
|
||||
let should_remove = handle.map_count == 0;
|
||||
if should_remove {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
Ok(Some(0))
|
||||
fn fsize(&mut self, id: usize, _ctx: &CallerCtx) -> Result<u64> {
|
||||
let handle = self.handle(id)?;
|
||||
Ok(handle.data.len() as u64)
|
||||
}
|
||||
|
||||
fn ftruncate(&mut self, id: usize, _len: u64, _ctx: &CallerCtx) -> Result<()> {
|
||||
let _ = self.handle(id)?;
|
||||
Err(Error::new(EROFS))
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result<EventFlags> {
|
||||
let _ = self.handle(id)?;
|
||||
Ok(EventFlags::empty())
|
||||
}
|
||||
|
||||
fn mmap_prep(
|
||||
@@ -182,9 +208,10 @@ impl SchemeBlockMut for FirmwareScheme {
|
||||
id: usize,
|
||||
offset: u64,
|
||||
size: usize,
|
||||
_flags: MapFlags,
|
||||
) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
_flags: syscall::MapFlags,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let handle = self.handle_mut(id)?;
|
||||
let data_len = handle.data.len() as u64;
|
||||
|
||||
if offset > data_len {
|
||||
@@ -196,7 +223,7 @@ impl SchemeBlockMut for FirmwareScheme {
|
||||
|
||||
let ptr = &handle.data[offset as usize] as *const u8;
|
||||
handle.map_count += 1;
|
||||
Ok(Some(ptr as usize))
|
||||
Ok(ptr as usize)
|
||||
}
|
||||
|
||||
fn munmap(
|
||||
@@ -204,9 +231,10 @@ impl SchemeBlockMut for FirmwareScheme {
|
||||
id: usize,
|
||||
_offset: u64,
|
||||
_size: usize,
|
||||
_flags: MunmapFlags,
|
||||
) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
_flags: syscall::MunmapFlags,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<()> {
|
||||
let handle = self.handle_mut(id)?;
|
||||
if handle.map_count > 0 {
|
||||
handle.map_count -= 1;
|
||||
}
|
||||
@@ -214,6 +242,20 @@ impl SchemeBlockMut for FirmwareScheme {
|
||||
if should_cleanup {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
Ok(Some(0))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_close(&mut self, id: usize) {
|
||||
if id == SCHEME_ROOT_ID {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(handle) = self.handles.get_mut(&id) {
|
||||
handle.closed = true;
|
||||
let should_remove = handle.map_count == 0;
|
||||
if should_remove {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
#TODO: IOMMU daemon — needs hardware validation with QEMU amd-iommu device
|
||||
# Provides scheme:iommu for DMA remapping and device isolation.
|
||||
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
dependencies = [
|
||||
"redox-driver-sys",
|
||||
]
|
||||
|
||||
[package.files]
|
||||
"/usr/lib/drivers/iommu" = "iommu"
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "iommu"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
redox-driver-sys = { version = "0.1", path = "../../../drivers/redox-driver-sys/source" }
|
||||
redox_scheme = { package = "redox-scheme", version = "0.1" }
|
||||
syscall = { package = "redox_syscall", version = "0.4" }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
|
||||
[patch.crates-io]
|
||||
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" }
|
||||
@@ -0,0 +1,524 @@
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
|
||||
const ACPI_HEADER_BYTES: usize = 36;
|
||||
const IVRS_HEADER_BYTES: usize = ACPI_HEADER_BYTES + 4;
|
||||
const IVHD_HEADER_BYTES: usize = 0x18;
|
||||
|
||||
const IVHD_TYPE_10: u8 = 0x10;
|
||||
const IVHD_TYPE_11: u8 = 0x11;
|
||||
const IVMD_TYPE_20: u8 = 0x20;
|
||||
const IVMD_TYPE_21: u8 = 0x21;
|
||||
|
||||
const IVHD_ALL: u8 = 0x00;
|
||||
const IVHD_SEL: u8 = 0x01;
|
||||
const IVHD_SOR: u8 = 0x02;
|
||||
const IVHD_EOR: u8 = 0x03;
|
||||
const IVHD_PAD4: u8 = 0x42;
|
||||
const IVHD_PAD8: u8 = 0x43;
|
||||
const IVHD_VAR: u8 = 0x44;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Bdf(pub u16);
|
||||
|
||||
impl Bdf {
|
||||
pub const fn new(bus: u8, device: u8, function: u8) -> Self {
|
||||
Self(((bus as u16) << 8) | (((device as u16) & 0x1F) << 3) | ((function as u16) & 0x7))
|
||||
}
|
||||
|
||||
pub const fn raw(self) -> u16 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub const fn bus(self) -> u8 {
|
||||
(self.0 >> 8) as u8
|
||||
}
|
||||
|
||||
pub const fn device(self) -> u8 {
|
||||
((self.0 >> 3) & 0x1F) as u8
|
||||
}
|
||||
|
||||
pub const fn function(self) -> u8 {
|
||||
(self.0 & 0x7) as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Bdf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:02x}:{:02x}.{}",
|
||||
self.bus(),
|
||||
self.device(),
|
||||
self.function()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_bdf(text: &str) -> Option<Bdf> {
|
||||
let trimmed = text.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(raw) = trimmed.strip_prefix("0x") {
|
||||
return u16::from_str_radix(raw, 16).ok().map(Bdf);
|
||||
}
|
||||
|
||||
if trimmed.contains('.') {
|
||||
let (head, function) = trimmed.rsplit_once('.')?;
|
||||
let function = u8::from_str_radix(function, 16)
|
||||
.or_else(|_| function.parse::<u8>())
|
||||
.ok()?;
|
||||
|
||||
let parts: Vec<&str> = head.split(':').collect();
|
||||
let (bus, device) = match parts.as_slice() {
|
||||
[bus, device] => (*bus, *device),
|
||||
[_, bus, device] => (*bus, *device),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let bus = u8::from_str_radix(bus, 16).ok()?;
|
||||
let device = u8::from_str_radix(device, 16).ok()?;
|
||||
return Some(Bdf::new(bus, device, function));
|
||||
}
|
||||
|
||||
u16::from_str_radix(trimmed, 16).ok().map(Bdf)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum IvhdEntry {
|
||||
All { flags: u8 },
|
||||
Select { bdf: Bdf, flags: u8 },
|
||||
StartRange { bdf: Bdf, flags: u8 },
|
||||
EndRange { bdf: Bdf },
|
||||
Padding { kind: u8, length: usize },
|
||||
Variable { kind: u8, payload: Vec<u8> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct IommuUnitInfo {
|
||||
pub entry_type: u8,
|
||||
pub flags: u8,
|
||||
pub length: u16,
|
||||
pub iommu_bdf: Bdf,
|
||||
pub capability_offset: u16,
|
||||
pub mmio_base: u64,
|
||||
pub pci_segment_group: u16,
|
||||
pub iommu_info: u16,
|
||||
pub iommu_efr: u32,
|
||||
pub device_entries: Vec<IvhdEntry>,
|
||||
}
|
||||
|
||||
impl IommuUnitInfo {
|
||||
pub fn unit_id(&self) -> u8 {
|
||||
((self.iommu_info >> 6) & 0x7F) as u8
|
||||
}
|
||||
|
||||
pub fn msi_number(&self) -> u8 {
|
||||
(self.iommu_info & 0x3F) as u8
|
||||
}
|
||||
|
||||
pub fn handles_device(&self, bdf: Bdf) -> bool {
|
||||
let mut all = false;
|
||||
let mut range_start: Option<u16> = None;
|
||||
|
||||
for entry in &self.device_entries {
|
||||
match *entry {
|
||||
IvhdEntry::All { .. } => all = true,
|
||||
IvhdEntry::Select { bdf: selected, .. } if selected == bdf => return true,
|
||||
IvhdEntry::StartRange { bdf: start, .. } => range_start = Some(start.raw()),
|
||||
IvhdEntry::EndRange { bdf: end } => {
|
||||
if let Some(start) = range_start.take() {
|
||||
let raw = bdf.raw();
|
||||
if (start..=end.raw()).contains(&raw) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
all
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct IvrsInfo {
|
||||
pub revision: u8,
|
||||
pub iv_info: u32,
|
||||
pub units: Vec<IommuUnitInfo>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum IvrsError {
|
||||
TooShort,
|
||||
InvalidSignature([u8; 4]),
|
||||
InvalidLength(u32),
|
||||
InvalidChecksum,
|
||||
TruncatedEntry { offset: usize },
|
||||
InvalidEntryLength { offset: usize, length: usize },
|
||||
InvalidIvhdLength { offset: usize, length: usize },
|
||||
InvalidVariableLength { offset: usize, length: usize },
|
||||
}
|
||||
|
||||
impl fmt::Display for IvrsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::TooShort => write!(f, "IVRS table is shorter than the ACPI header"),
|
||||
Self::InvalidSignature(sig) => write!(
|
||||
f,
|
||||
"invalid IVRS signature {:?}",
|
||||
String::from_utf8_lossy(sig)
|
||||
),
|
||||
Self::InvalidLength(length) => write!(f, "invalid IVRS table length {length}"),
|
||||
Self::InvalidChecksum => write!(f, "IVRS checksum validation failed"),
|
||||
Self::TruncatedEntry { offset } => {
|
||||
write!(f, "truncated IVRS entry at offset {offset:#x}")
|
||||
}
|
||||
Self::InvalidEntryLength { offset, length } => {
|
||||
write!(
|
||||
f,
|
||||
"invalid IVRS entry length {length} at offset {offset:#x}"
|
||||
)
|
||||
}
|
||||
Self::InvalidIvhdLength { offset, length } => {
|
||||
write!(
|
||||
f,
|
||||
"invalid IVHD entry length {length} at offset {offset:#x}"
|
||||
)
|
||||
}
|
||||
Self::InvalidVariableLength { offset, length } => {
|
||||
write!(
|
||||
f,
|
||||
"invalid IVHD variable-length entry {length} at offset {offset:#x}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for IvrsError {}
|
||||
|
||||
pub fn parse_ivrs(bytes: &[u8]) -> Result<IvrsInfo, IvrsError> {
|
||||
if bytes.len() < IVRS_HEADER_BYTES {
|
||||
return Err(IvrsError::TooShort);
|
||||
}
|
||||
|
||||
let signature = bytes[0..4].try_into().map_err(|_| IvrsError::TooShort)?;
|
||||
if signature != *b"IVRS" {
|
||||
return Err(IvrsError::InvalidSignature(signature));
|
||||
}
|
||||
|
||||
let length = read_u32(bytes, 4).ok_or(IvrsError::TooShort)?;
|
||||
if length < IVRS_HEADER_BYTES as u32 {
|
||||
return Err(IvrsError::InvalidLength(length));
|
||||
}
|
||||
if bytes.len() < length as usize {
|
||||
return Err(IvrsError::TooShort);
|
||||
}
|
||||
|
||||
let table = &bytes[..length as usize];
|
||||
if table.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte)) != 0 {
|
||||
return Err(IvrsError::InvalidChecksum);
|
||||
}
|
||||
|
||||
let revision = table[8];
|
||||
let iv_info = read_u32(table, ACPI_HEADER_BYTES).ok_or(IvrsError::TooShort)?;
|
||||
|
||||
let mut units = Vec::new();
|
||||
let mut offset = IVRS_HEADER_BYTES;
|
||||
while offset < table.len() {
|
||||
if offset + 4 > table.len() {
|
||||
return Err(IvrsError::TruncatedEntry { offset });
|
||||
}
|
||||
|
||||
let entry_type = table[offset];
|
||||
let entry_length =
|
||||
read_u16(table, offset + 2).ok_or(IvrsError::TruncatedEntry { offset })? as usize;
|
||||
|
||||
if entry_length < 4 {
|
||||
return Err(IvrsError::InvalidEntryLength {
|
||||
offset,
|
||||
length: entry_length,
|
||||
});
|
||||
}
|
||||
if offset + entry_length > table.len() {
|
||||
return Err(IvrsError::TruncatedEntry { offset });
|
||||
}
|
||||
|
||||
let entry = &table[offset..offset + entry_length];
|
||||
if matches!(entry_type, IVHD_TYPE_10 | IVHD_TYPE_11) {
|
||||
units.push(parse_ivhd(entry, offset)?);
|
||||
}
|
||||
|
||||
if matches!(entry_type, IVMD_TYPE_20 | IVMD_TYPE_21) {
|
||||
offset += entry_length;
|
||||
continue;
|
||||
}
|
||||
|
||||
offset += entry_length;
|
||||
}
|
||||
|
||||
Ok(IvrsInfo {
|
||||
revision,
|
||||
iv_info,
|
||||
units,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_ivhd(entry: &[u8], table_offset: usize) -> Result<IommuUnitInfo, IvrsError> {
|
||||
if entry.len() < IVHD_HEADER_BYTES {
|
||||
return Err(IvrsError::InvalidIvhdLength {
|
||||
offset: table_offset,
|
||||
length: entry.len(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut device_entries = Vec::new();
|
||||
let mut offset = IVHD_HEADER_BYTES;
|
||||
while offset < entry.len() {
|
||||
let kind = entry[offset];
|
||||
match kind {
|
||||
IVHD_ALL => {
|
||||
ensure_remaining(entry, offset, 4, table_offset)?;
|
||||
device_entries.push(IvhdEntry::All {
|
||||
flags: entry[offset + 1],
|
||||
});
|
||||
offset += 4;
|
||||
}
|
||||
IVHD_SEL => {
|
||||
ensure_remaining(entry, offset, 4, table_offset)?;
|
||||
device_entries.push(IvhdEntry::Select {
|
||||
bdf: Bdf(
|
||||
read_u16(entry, offset + 2).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset + offset,
|
||||
})?,
|
||||
),
|
||||
flags: entry[offset + 1],
|
||||
});
|
||||
offset += 4;
|
||||
}
|
||||
IVHD_SOR => {
|
||||
ensure_remaining(entry, offset, 4, table_offset)?;
|
||||
device_entries.push(IvhdEntry::StartRange {
|
||||
bdf: Bdf(
|
||||
read_u16(entry, offset + 2).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset + offset,
|
||||
})?,
|
||||
),
|
||||
flags: entry[offset + 1],
|
||||
});
|
||||
offset += 4;
|
||||
}
|
||||
IVHD_EOR => {
|
||||
ensure_remaining(entry, offset, 4, table_offset)?;
|
||||
device_entries.push(IvhdEntry::EndRange {
|
||||
bdf: Bdf(
|
||||
read_u16(entry, offset + 2).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset + offset,
|
||||
})?,
|
||||
),
|
||||
});
|
||||
offset += 4;
|
||||
}
|
||||
IVHD_PAD4 => {
|
||||
ensure_remaining(entry, offset, 8, table_offset)?;
|
||||
device_entries.push(IvhdEntry::Padding { kind, length: 8 });
|
||||
offset += 8;
|
||||
}
|
||||
IVHD_PAD8 => {
|
||||
ensure_remaining(entry, offset, 12, table_offset)?;
|
||||
device_entries.push(IvhdEntry::Padding { kind, length: 12 });
|
||||
offset += 12;
|
||||
}
|
||||
IVHD_VAR => {
|
||||
ensure_remaining(entry, offset, 2, table_offset)?;
|
||||
let variable_length = entry[offset + 1] as usize;
|
||||
if variable_length < 2 {
|
||||
return Err(IvrsError::InvalidVariableLength {
|
||||
offset: table_offset + offset,
|
||||
length: variable_length,
|
||||
});
|
||||
}
|
||||
ensure_remaining(entry, offset, variable_length, table_offset)?;
|
||||
device_entries.push(IvhdEntry::Variable {
|
||||
kind,
|
||||
payload: entry[offset + 2..offset + variable_length].to_vec(),
|
||||
});
|
||||
offset += variable_length;
|
||||
}
|
||||
_ => {
|
||||
ensure_remaining(entry, offset, 4, table_offset)?;
|
||||
device_entries.push(IvhdEntry::Variable {
|
||||
kind,
|
||||
payload: entry[offset + 1..offset + 4].to_vec(),
|
||||
});
|
||||
offset += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(IommuUnitInfo {
|
||||
entry_type: entry[0],
|
||||
flags: entry[1],
|
||||
length: read_u16(entry, 2).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset,
|
||||
})?,
|
||||
iommu_bdf: Bdf(read_u16(entry, 4).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset,
|
||||
})?),
|
||||
capability_offset: read_u16(entry, 6).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset,
|
||||
})?,
|
||||
mmio_base: read_u64(entry, 8).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset,
|
||||
})?,
|
||||
pci_segment_group: read_u16(entry, 16).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset,
|
||||
})?,
|
||||
iommu_info: read_u16(entry, 18).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset,
|
||||
})?,
|
||||
iommu_efr: read_u32(entry, 20).ok_or(IvrsError::TruncatedEntry {
|
||||
offset: table_offset,
|
||||
})?,
|
||||
device_entries,
|
||||
})
|
||||
}
|
||||
|
||||
fn ensure_remaining(
|
||||
entry: &[u8],
|
||||
offset: usize,
|
||||
length: usize,
|
||||
table_offset: usize,
|
||||
) -> Result<(), IvrsError> {
|
||||
if offset + length > entry.len() {
|
||||
return Err(IvrsError::TruncatedEntry {
|
||||
offset: table_offset + offset,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
|
||||
bytes
|
||||
.get(offset..offset + 2)?
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(u16::from_le_bytes)
|
||||
}
|
||||
|
||||
fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
|
||||
bytes
|
||||
.get(offset..offset + 4)?
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(u32::from_le_bytes)
|
||||
}
|
||||
|
||||
fn read_u64(bytes: &[u8], offset: usize) -> Option<u64> {
|
||||
bytes
|
||||
.get(offset..offset + 8)?
|
||||
.try_into()
|
||||
.ok()
|
||||
.map(u64::from_le_bytes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_bdf, parse_ivrs, Bdf, IommuUnitInfo, IvhdEntry, IVRS_HEADER_BYTES};
|
||||
|
||||
fn build_ivrs(units: &[Vec<u8>]) -> Vec<u8> {
|
||||
let length = (IVRS_HEADER_BYTES + units.iter().map(Vec::len).sum::<usize>()) as u32;
|
||||
let mut bytes = vec![0u8; length as usize];
|
||||
|
||||
bytes[0..4].copy_from_slice(b"IVRS");
|
||||
bytes[4..8].copy_from_slice(&length.to_le_bytes());
|
||||
bytes[8] = 3;
|
||||
bytes[10..16].copy_from_slice(b"RDBEAR");
|
||||
bytes[16..24].copy_from_slice(b"AMDVI ");
|
||||
bytes[36..40].copy_from_slice(&0x0123_4567u32.to_le_bytes());
|
||||
|
||||
let mut offset = IVRS_HEADER_BYTES;
|
||||
for unit in units {
|
||||
bytes[offset..offset + unit.len()].copy_from_slice(unit);
|
||||
offset += unit.len();
|
||||
}
|
||||
|
||||
let checksum =
|
||||
(!bytes.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte))).wrapping_add(1);
|
||||
bytes[9] = checksum;
|
||||
bytes
|
||||
}
|
||||
|
||||
fn build_ivhd(mmio_base: u64, iommu_bdf: Bdf, entries: &[u8]) -> Vec<u8> {
|
||||
let length = (0x18 + entries.len()) as u16;
|
||||
let mut bytes = vec![0u8; length as usize];
|
||||
bytes[0] = 0x11;
|
||||
bytes[1] = 0xA0;
|
||||
bytes[2..4].copy_from_slice(&length.to_le_bytes());
|
||||
bytes[4..6].copy_from_slice(&iommu_bdf.raw().to_le_bytes());
|
||||
bytes[6..8].copy_from_slice(&0x0040u16.to_le_bytes());
|
||||
bytes[8..16].copy_from_slice(&mmio_base.to_le_bytes());
|
||||
bytes[16..18].copy_from_slice(&0u16.to_le_bytes());
|
||||
bytes[18..20].copy_from_slice(&0x01c2u16.to_le_bytes());
|
||||
bytes[20..24].copy_from_slice(&0x00aa_5500u32.to_le_bytes());
|
||||
bytes[24..].copy_from_slice(entries);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_bdf_text_forms() {
|
||||
assert_eq!(parse_bdf("00:14.0"), Some(Bdf::new(0x00, 0x14, 0x0)));
|
||||
assert_eq!(parse_bdf("0000:02:00.1"), Some(Bdf::new(0x02, 0x00, 0x1)));
|
||||
assert_eq!(parse_bdf("0x1234"), Some(Bdf(0x1234)));
|
||||
assert_eq!(parse_bdf("zz:zz.z"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_ivrs_with_multiple_units() {
|
||||
let unit0_entries = [
|
||||
0x01, 0x11, 0x08, 0x00, // select 00:01.0
|
||||
0x02, 0x22, 0x10, 0x00, // start range 00:02.0
|
||||
0x03, 0x00, 0x17, 0x00, // end range 00:02.7
|
||||
];
|
||||
let unit1_entries = [0x00, 0x00, 0x00, 0x00];
|
||||
|
||||
let table = build_ivrs(&[
|
||||
build_ivhd(0xfee0_0000, Bdf::new(0, 0x18, 2), &unit0_entries),
|
||||
build_ivhd(0xfee1_0000, Bdf::new(0, 0x18, 3), &unit1_entries),
|
||||
]);
|
||||
|
||||
let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}"));
|
||||
assert_eq!(parsed.units.len(), 2);
|
||||
assert_eq!(parsed.units[0].mmio_base, 0xfee0_0000);
|
||||
assert_eq!(parsed.units[1].iommu_bdf, Bdf::new(0, 0x18, 3));
|
||||
|
||||
let unit = &parsed.units[0];
|
||||
assert!(unit.handles_device(Bdf::new(0, 1, 0)));
|
||||
assert!(unit.handles_device(Bdf::new(0, 2, 3)));
|
||||
assert!(!unit.handles_device(Bdf::new(0, 3, 0)));
|
||||
assert_eq!(unit.unit_id(), 7);
|
||||
assert_eq!(unit.msi_number(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_entry_covers_entire_bus_space() {
|
||||
let unit = IommuUnitInfo {
|
||||
entry_type: 0x11,
|
||||
flags: 0,
|
||||
length: 0x1c,
|
||||
iommu_bdf: Bdf::new(0, 0x18, 2),
|
||||
capability_offset: 0x40,
|
||||
mmio_base: 0xfee0_0000,
|
||||
pci_segment_group: 0,
|
||||
iommu_info: 0,
|
||||
iommu_efr: 0,
|
||||
device_entries: vec![IvhdEntry::All { flags: 0 }],
|
||||
};
|
||||
|
||||
assert!(unit.handles_device(Bdf::new(0x80, 0x1f, 7)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
use core::ptr::{read_volatile, write_volatile};
|
||||
|
||||
use log::{debug, warn};
|
||||
use redox_driver_sys::memory::{CacheType, MmioProt, MmioRegion};
|
||||
|
||||
use crate::acpi::{parse_ivrs, Bdf, IommuUnitInfo, IvrsError};
|
||||
use crate::command_buffer::{CommandBuffer, CommandEntry, EventLog, EventLogEntry};
|
||||
use crate::device_table::{DeviceTable, DeviceTableEntry, DEVICE_TABLE_ENTRIES};
|
||||
use crate::interrupt::InterruptRemapTable;
|
||||
use crate::mmio::{control, ext_feature, status, AmdViMmio, AMD_VI_MMIO_BYTES};
|
||||
use crate::page_table::DomainPageTables;
|
||||
|
||||
const CMD_BUF_LEN_ENCODING: u64 = 0x09;
|
||||
const EVT_LOG_LEN_ENCODING: u64 = 0x09;
|
||||
const DEV_TABLE_SIZE_ENCODING: u64 = 0x0F;
|
||||
const DEFAULT_CMD_ENTRIES: usize = 512;
|
||||
const DEFAULT_EVT_ENTRIES: usize = 512;
|
||||
const DEFAULT_IRT_ENTRIES: usize = 4096;
|
||||
const COMPLETION_TOKEN: u32 = 0xA11D_F00D;
|
||||
|
||||
struct MmioMapping {
|
||||
region: MmioRegion,
|
||||
base: *mut AmdViMmio,
|
||||
}
|
||||
|
||||
pub struct AmdViUnit {
|
||||
info: IommuUnitInfo,
|
||||
mmio: Option<MmioMapping>,
|
||||
device_table: Option<DeviceTable>,
|
||||
command_buffer: Option<CommandBuffer>,
|
||||
event_log: Option<EventLog>,
|
||||
interrupt_table: Option<InterruptRemapTable>,
|
||||
completion_store: Option<redox_driver_sys::dma::DmaBuffer>,
|
||||
command_tail: usize,
|
||||
event_head: usize,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct AmdViEvent {
|
||||
pub unit_id: u8,
|
||||
pub event_code: u16,
|
||||
pub event_flags: u16,
|
||||
pub device_id: Bdf,
|
||||
pub address: u64,
|
||||
}
|
||||
|
||||
impl AmdViUnit {
|
||||
pub fn detect(ivrs: &[u8]) -> Result<Vec<Self>, IvrsError> {
|
||||
let parsed = parse_ivrs(ivrs)?;
|
||||
Ok(parsed.units.into_iter().map(Self::from_info).collect())
|
||||
}
|
||||
|
||||
pub fn from_info(info: IommuUnitInfo) -> Self {
|
||||
Self {
|
||||
info,
|
||||
mmio: None,
|
||||
device_table: None,
|
||||
command_buffer: None,
|
||||
event_log: None,
|
||||
interrupt_table: None,
|
||||
completion_store: None,
|
||||
command_tail: 0,
|
||||
event_head: 0,
|
||||
initialized: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn info(&self) -> &IommuUnitInfo {
|
||||
&self.info
|
||||
}
|
||||
|
||||
pub fn initialized(&self) -> bool {
|
||||
self.initialized
|
||||
}
|
||||
|
||||
pub fn handles_device(&self, bdf: Bdf) -> bool {
|
||||
self.info.handles_device(bdf)
|
||||
}
|
||||
|
||||
pub fn init(&mut self) -> Result<(), String> {
|
||||
if self.initialized {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let region = MmioRegion::map(
|
||||
self.info.mmio_base,
|
||||
AMD_VI_MMIO_BYTES,
|
||||
CacheType::DeviceMemory,
|
||||
MmioProt::READ_WRITE,
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!(
|
||||
"failed to map AMD-Vi MMIO {:#x}: {err}",
|
||||
self.info.mmio_base
|
||||
)
|
||||
})?;
|
||||
let base = region.as_ptr() as *mut AmdViMmio;
|
||||
self.mmio = Some(MmioMapping { region, base });
|
||||
|
||||
self.disable_unit()?;
|
||||
|
||||
let device_table = DeviceTable::new().map_err(|err| err.to_string())?;
|
||||
let command_buffer =
|
||||
CommandBuffer::new(DEFAULT_CMD_ENTRIES).map_err(|err| err.to_string())?;
|
||||
let event_log = EventLog::new(DEFAULT_EVT_ENTRIES).map_err(|err| err.to_string())?;
|
||||
let interrupt_table =
|
||||
InterruptRemapTable::new(DEFAULT_IRT_ENTRIES).map_err(|err| err.to_string())?;
|
||||
|
||||
self.program_bars(&device_table, &command_buffer, &event_log)?;
|
||||
self.reset_ring_pointers()?;
|
||||
|
||||
self.device_table = Some(device_table);
|
||||
self.command_buffer = Some(command_buffer);
|
||||
self.event_log = Some(event_log);
|
||||
self.interrupt_table = Some(interrupt_table);
|
||||
|
||||
let ext = self.mmio_read_extended_feature()?;
|
||||
let mut control_value = control::EVENT_LOG_EN | control::CMD_BUF_EN;
|
||||
if ext & ext_feature::XT_SUP != 0 {
|
||||
control_value |= control::XT_EN;
|
||||
}
|
||||
if ext & ext_feature::NX_SUP != 0 {
|
||||
control_value |= control::NX_EN;
|
||||
}
|
||||
unsafe {
|
||||
AmdViMmio::write_control(self.mmio_base()?, control_value);
|
||||
}
|
||||
|
||||
self.flush_configuration()?;
|
||||
|
||||
unsafe {
|
||||
AmdViMmio::write_control(self.mmio_base()?, control_value | control::IOMMU_ENABLE);
|
||||
}
|
||||
self.wait_for_running(true)?;
|
||||
self.initialized = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn assign_device(&mut self, bdf: Bdf, domain: &DomainPageTables) -> Result<(), String> {
|
||||
if !self.initialized {
|
||||
return Err("AMD-Vi unit is not initialized".to_string());
|
||||
}
|
||||
if !self.handles_device(bdf) {
|
||||
return Err(format!(
|
||||
"AMD-Vi unit {} does not cover device {bdf}",
|
||||
self.info.unit_id()
|
||||
));
|
||||
}
|
||||
|
||||
let interrupt_table = self
|
||||
.interrupt_table
|
||||
.as_ref()
|
||||
.ok_or_else(|| "interrupt remap table not initialized".to_string())?;
|
||||
let device_table = self
|
||||
.device_table
|
||||
.as_mut()
|
||||
.ok_or_else(|| "device table not initialized".to_string())?;
|
||||
|
||||
let mut entry = DeviceTableEntry::new();
|
||||
entry.set_valid(true);
|
||||
entry.set_translation_valid(true);
|
||||
entry.set_read_permission(true);
|
||||
entry.set_write_permission(true);
|
||||
entry.set_mode(domain.levels());
|
||||
entry.set_page_table_root(domain.root_address());
|
||||
entry.set_interrupt_remap(true);
|
||||
entry.set_interrupt_write(true);
|
||||
entry.set_interrupt_control(0x02);
|
||||
entry.set_int_table_len(interrupt_table.len_encoding());
|
||||
entry.set_int_remap_table_ptr(interrupt_table.physical_address() as u64);
|
||||
|
||||
device_table.set_entry(bdf.raw(), &entry);
|
||||
self.submit_command(CommandEntry::invalidate_devtab_entry(bdf.raw()))?;
|
||||
self.submit_command(CommandEntry::invalidate_interrupt_table(bdf.raw()))?;
|
||||
self.wait_for_completion()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn drain_events(&mut self) -> Result<Vec<AmdViEvent>, String> {
|
||||
let mut drained = Vec::new();
|
||||
if !self.initialized {
|
||||
return Ok(drained);
|
||||
}
|
||||
|
||||
let base = self.mmio_base()?;
|
||||
let event_log = self
|
||||
.event_log
|
||||
.as_ref()
|
||||
.ok_or_else(|| "event log not initialized".to_string())?;
|
||||
let tail = unsafe { AmdViMmio::read_evt_log_tail(base) as usize % event_log.capacity() };
|
||||
|
||||
while self.event_head != tail {
|
||||
let event = event_log.read_entry(self.event_head);
|
||||
drained.push(self.decode_event(event));
|
||||
self.event_head = (self.event_head + 1) % event_log.capacity();
|
||||
}
|
||||
|
||||
unsafe {
|
||||
AmdViMmio::write_evt_log_head(base, self.event_head as u64);
|
||||
}
|
||||
Ok(drained)
|
||||
}
|
||||
|
||||
fn decode_event(&self, event: EventLogEntry) -> AmdViEvent {
|
||||
AmdViEvent {
|
||||
unit_id: self.info.unit_id(),
|
||||
event_code: event.event_type() as u16,
|
||||
event_flags: event.event_flags(),
|
||||
device_id: Bdf(event.device_id()),
|
||||
address: event.virtual_address(),
|
||||
}
|
||||
}
|
||||
|
||||
fn disable_unit(&mut self) -> Result<(), String> {
|
||||
let base = self.mmio_base()?;
|
||||
unsafe {
|
||||
AmdViMmio::write_control(base, 0);
|
||||
}
|
||||
self.wait_for_running(false)
|
||||
}
|
||||
|
||||
fn wait_for_running(&self, expected: bool) -> Result<(), String> {
|
||||
let base = self.mmio_base()?;
|
||||
for _ in 0..100_000 {
|
||||
let running = unsafe { AmdViMmio::read_status(base) } & status::IOMMU_RUNNING != 0;
|
||||
if running == expected {
|
||||
return Ok(());
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"timed out waiting for AMD-Vi unit {} running={expected}",
|
||||
self.info.unit_id()
|
||||
))
|
||||
}
|
||||
|
||||
fn program_bars(
|
||||
&mut self,
|
||||
device_table: &DeviceTable,
|
||||
command_buffer: &CommandBuffer,
|
||||
event_log: &EventLog,
|
||||
) -> Result<(), String> {
|
||||
let base = self.mmio_base()?;
|
||||
unsafe {
|
||||
AmdViMmio::write_dev_table_bar(
|
||||
base,
|
||||
(device_table.physical_address() as u64 & !0xFFF) | DEV_TABLE_SIZE_ENCODING,
|
||||
);
|
||||
AmdViMmio::write_cmd_buf_bar(
|
||||
base,
|
||||
(command_buffer.physical_address() as u64 & !0xFFF) | CMD_BUF_LEN_ENCODING,
|
||||
);
|
||||
AmdViMmio::write_evt_log_bar(
|
||||
base,
|
||||
(event_log.physical_address() as u64 & !0xFFF) | EVT_LOG_LEN_ENCODING,
|
||||
);
|
||||
AmdViMmio::write_exclusion_base(base, 0);
|
||||
AmdViMmio::write_exclusion_limit(base, 0);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_ring_pointers(&mut self) -> Result<(), String> {
|
||||
let base = self.mmio_base()?;
|
||||
unsafe {
|
||||
AmdViMmio::write_cmd_buf_head(base, 0);
|
||||
AmdViMmio::write_cmd_buf_tail(base, 0);
|
||||
AmdViMmio::write_evt_log_head(base, 0);
|
||||
}
|
||||
self.command_tail = 0;
|
||||
self.event_head = 0;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush_configuration(&mut self) -> Result<(), String> {
|
||||
let ext = self.mmio_read_extended_feature()?;
|
||||
if ext & ext_feature::IA_SUP != 0 {
|
||||
self.submit_command(CommandEntry::invalidate_all())?;
|
||||
} else if let Some(table) = self.device_table.as_ref() {
|
||||
let mut pending_invalidations = Vec::new();
|
||||
for device_id in 0..DEVICE_TABLE_ENTRIES {
|
||||
let entry = table.get_entry(device_id as u16);
|
||||
if entry.valid() {
|
||||
pending_invalidations.push(device_id as u16);
|
||||
}
|
||||
}
|
||||
for device_id in pending_invalidations {
|
||||
self.submit_command(CommandEntry::invalidate_devtab_entry(device_id))?;
|
||||
}
|
||||
} else {
|
||||
warn!("amd-vi: device table not yet allocated while flushing configuration");
|
||||
}
|
||||
self.wait_for_completion()
|
||||
}
|
||||
|
||||
fn submit_command(&mut self, command: CommandEntry) -> Result<(), String> {
|
||||
let base = self.mmio_base()?;
|
||||
let command_buffer = self
|
||||
.command_buffer
|
||||
.as_mut()
|
||||
.ok_or_else(|| "command buffer not initialized".to_string())?;
|
||||
|
||||
let head =
|
||||
unsafe { AmdViMmio::read_cmd_buf_head(base) as usize % command_buffer.capacity() };
|
||||
let next_tail = (self.command_tail + 1) % command_buffer.capacity();
|
||||
if next_tail == head {
|
||||
return Err("AMD-Vi command buffer is full".to_string());
|
||||
}
|
||||
|
||||
command_buffer.write_command(self.command_tail, &command);
|
||||
self.command_tail = next_tail;
|
||||
unsafe {
|
||||
AmdViMmio::write_cmd_buf_tail(base, self.command_tail as u64);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn wait_for_completion(&mut self) -> Result<(), String> {
|
||||
let completion_store = match self.completion_store.take() {
|
||||
Some(buffer) => buffer,
|
||||
None => redox_driver_sys::dma::DmaBuffer::allocate(8, 8)
|
||||
.map_err(|err| format!("failed to allocate completion wait store: {err}"))?,
|
||||
};
|
||||
|
||||
let completion_ptr = completion_store.as_ptr() as *const u32;
|
||||
let completion_mut = completion_store.as_ptr() as *mut u32;
|
||||
unsafe {
|
||||
write_volatile(completion_mut, 0);
|
||||
}
|
||||
let completion_phys = completion_store.physical_address() as u64;
|
||||
self.submit_command(CommandEntry::completion_wait(
|
||||
completion_phys,
|
||||
COMPLETION_TOKEN,
|
||||
))?;
|
||||
|
||||
for _ in 0..100_000 {
|
||||
if unsafe { read_volatile(completion_ptr) } == COMPLETION_TOKEN {
|
||||
self.completion_store = Some(completion_store);
|
||||
return Ok(());
|
||||
}
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
|
||||
self.completion_store = Some(completion_store);
|
||||
Err("timed out waiting for AMD-Vi command completion".to_string())
|
||||
}
|
||||
|
||||
fn mmio_read_extended_feature(&self) -> Result<u64, String> {
|
||||
let base = self.mmio_base()?;
|
||||
Ok(unsafe { AmdViMmio::read_extended_feature(base) })
|
||||
}
|
||||
|
||||
fn mmio_base(&self) -> Result<*mut AmdViMmio, String> {
|
||||
self.mmio
|
||||
.as_ref()
|
||||
.map(|mapping| mapping.base)
|
||||
.ok_or_else(|| "AMD-Vi MMIO is not mapped".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AmdViUnit {
|
||||
fn drop(&mut self) {
|
||||
if let Some(mapping) = &self.mmio {
|
||||
debug!(
|
||||
"amd-vi: dropping unit {} mapped at {:#x} ({:#x} bytes)",
|
||||
self.info.unit_id(),
|
||||
self.info.mmio_base,
|
||||
mapping.region.size()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::acpi::Bdf;
|
||||
|
||||
use super::AmdViUnit;
|
||||
|
||||
fn build_ivrs_with_unit() -> Vec<u8> {
|
||||
let mut table = vec![0u8; 40 + 28];
|
||||
table[0..4].copy_from_slice(b"IVRS");
|
||||
table[4..8].copy_from_slice(&(68u32).to_le_bytes());
|
||||
table[8] = 3;
|
||||
table[10..16].copy_from_slice(b"RDBEAR");
|
||||
table[16..24].copy_from_slice(b"AMDVI ");
|
||||
|
||||
let offset = 40;
|
||||
table[offset] = 0x11;
|
||||
table[offset + 1] = 0x20;
|
||||
table[offset + 2..offset + 4].copy_from_slice(&(28u16).to_le_bytes());
|
||||
table[offset + 4..offset + 6].copy_from_slice(&Bdf::new(0, 0x18, 2).raw().to_le_bytes());
|
||||
table[offset + 6..offset + 8].copy_from_slice(&0x40u16.to_le_bytes());
|
||||
table[offset + 8..offset + 16].copy_from_slice(&0xfee0_0000u64.to_le_bytes());
|
||||
table[offset + 16..offset + 18].copy_from_slice(&0u16.to_le_bytes());
|
||||
table[offset + 18..offset + 20].copy_from_slice(&0x0081u16.to_le_bytes());
|
||||
table[offset + 20..offset + 24].copy_from_slice(&0u32.to_le_bytes());
|
||||
table[offset + 24..offset + 28].copy_from_slice(&[0x00, 0, 0, 0]);
|
||||
|
||||
let checksum =
|
||||
(!table.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte))).wrapping_add(1);
|
||||
table[9] = checksum;
|
||||
table
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_builds_units_from_ivrs() {
|
||||
let units = AmdViUnit::detect(&build_ivrs_with_unit())
|
||||
.unwrap_or_else(|err| panic!("amd-vi detect failed: {err}"));
|
||||
assert_eq!(units.len(), 1);
|
||||
assert_eq!(units[0].info().mmio_base, 0xfee0_0000);
|
||||
assert!(units[0].handles_device(Bdf::new(0x80, 0x1f, 7)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
use core::mem::size_of;
|
||||
use core::slice;
|
||||
|
||||
use redox_driver_sys::dma::DmaBuffer;
|
||||
|
||||
pub const COMMAND_ENTRY_SIZE: usize = 16;
|
||||
pub const EVENT_LOG_ENTRY_SIZE: usize = 16;
|
||||
|
||||
const DMA_ALIGNMENT: usize = 4096;
|
||||
|
||||
pub const CMD_COMPLETION_WAIT: u32 = 0x01;
|
||||
pub const CMD_INVALIDATE_DEVTAB_ENTRY: u32 = 0x02;
|
||||
pub const CMD_INVALIDATE_IOMMU_PAGES: u32 = 0x03;
|
||||
pub const CMD_INVALIDATE_INTERRUPT_TABLE: u32 = 0x04;
|
||||
pub const CMD_INVALIDATE_IOMMU_ALL: u32 = 0x05;
|
||||
|
||||
pub const EVENT_IO_PAGE_FAULT: u32 = 0x01;
|
||||
pub const EVENT_INVALIDATE_DEVICE_TABLE: u32 = 0x02;
|
||||
|
||||
const COMPLETION_WAIT_STORE_BIT: u32 = 1 << 4;
|
||||
const COMPLETION_WAIT_INTERRUPT_BIT: u32 = 1 << 5;
|
||||
const INVALIDATE_PAGES_PDE_BIT: u32 = 1 << 12;
|
||||
const INVALIDATE_PAGES_SIZE_BIT: u32 = 1 << 13;
|
||||
|
||||
/// Command buffer entry (128 bits = 16 bytes = 4 × u32).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct CommandEntry {
|
||||
words: [u32; 4],
|
||||
}
|
||||
|
||||
impl CommandEntry {
|
||||
pub const fn new() -> Self {
|
||||
Self { words: [0; 4] }
|
||||
}
|
||||
|
||||
pub const fn from_words(words: [u32; 4]) -> Self {
|
||||
Self { words }
|
||||
}
|
||||
|
||||
pub fn words(&self) -> [u32; 4] {
|
||||
self.words
|
||||
}
|
||||
|
||||
pub fn opcode(&self) -> u32 {
|
||||
self.words[0] & 0xF
|
||||
}
|
||||
|
||||
/// COMPLETION_WAIT (opcode 0x01).
|
||||
pub fn completion_wait(store_addr: u64, store_data: u32) -> Self {
|
||||
debug_assert_eq!(
|
||||
store_addr & 0x7,
|
||||
0,
|
||||
"completion wait store address must be 8-byte aligned"
|
||||
);
|
||||
|
||||
Self {
|
||||
words: [
|
||||
CMD_COMPLETION_WAIT | COMPLETION_WAIT_STORE_BIT,
|
||||
store_addr as u32,
|
||||
(store_addr >> 32) as u32,
|
||||
store_data,
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/// INVALIDATE_DEVTAB_ENTRY (opcode 0x02).
|
||||
pub fn invalidate_devtab_entry(device_id: u16) -> Self {
|
||||
Self {
|
||||
words: [CMD_INVALIDATE_DEVTAB_ENTRY, device_id as u32, 0, 0],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_pages(domain_id: u16, addr: u64) -> Self {
|
||||
Self::invalidate_pages_with_flags(domain_id, addr, false, false)
|
||||
}
|
||||
|
||||
pub fn invalidate_pages_with_flags(domain_id: u16, addr: u64, pde: bool, size: bool) -> Self {
|
||||
let mut word0 = CMD_INVALIDATE_IOMMU_PAGES;
|
||||
if pde {
|
||||
word0 |= INVALIDATE_PAGES_PDE_BIT;
|
||||
}
|
||||
if size {
|
||||
word0 |= INVALIDATE_PAGES_SIZE_BIT;
|
||||
}
|
||||
|
||||
Self {
|
||||
words: [word0, domain_id as u32, addr as u32, (addr >> 32) as u32],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_interrupt_table(device_id: u16) -> Self {
|
||||
Self {
|
||||
words: [CMD_INVALIDATE_INTERRUPT_TABLE, device_id as u32, 0, 0],
|
||||
}
|
||||
}
|
||||
|
||||
/// INVALIDATE_IOMMU_ALL (opcode 0x05).
|
||||
pub fn invalidate_all() -> Self {
|
||||
Self {
|
||||
words: [CMD_INVALIDATE_IOMMU_ALL, 0, 0, 0],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completion_wait_store(&self) -> bool {
|
||||
self.words[0] & COMPLETION_WAIT_STORE_BIT != 0
|
||||
}
|
||||
|
||||
pub fn completion_wait_interrupt(&self) -> bool {
|
||||
self.words[0] & COMPLETION_WAIT_INTERRUPT_BIT != 0
|
||||
}
|
||||
|
||||
pub fn completion_wait_store_address(&self) -> u64 {
|
||||
(self.words[1] as u64) | ((self.words[2] as u64) << 32)
|
||||
}
|
||||
|
||||
pub fn completion_wait_store_data(&self) -> u32 {
|
||||
self.words[3]
|
||||
}
|
||||
|
||||
pub fn invalidate_device_id(&self) -> u16 {
|
||||
self.words[1] as u16
|
||||
}
|
||||
|
||||
pub fn invalidate_pages_pde(&self) -> bool {
|
||||
self.words[0] & INVALIDATE_PAGES_PDE_BIT != 0
|
||||
}
|
||||
|
||||
pub fn invalidate_pages_size(&self) -> bool {
|
||||
self.words[0] & INVALIDATE_PAGES_SIZE_BIT != 0
|
||||
}
|
||||
|
||||
pub fn invalidate_pages_address(&self) -> u64 {
|
||||
(self.words[2] as u64) | ((self.words[3] as u64) << 32)
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = assert!(size_of::<CommandEntry>() == COMMAND_ENTRY_SIZE);
|
||||
|
||||
pub struct CommandBuffer {
|
||||
buffer: DmaBuffer,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl CommandBuffer {
|
||||
pub fn new(entry_count: usize) -> Result<Self, &'static str> {
|
||||
if entry_count == 0 {
|
||||
return Err("IOMMU command buffer entry count must be non-zero");
|
||||
}
|
||||
|
||||
let byte_len = entry_count
|
||||
.checked_mul(COMMAND_ENTRY_SIZE)
|
||||
.ok_or("IOMMU command buffer size overflow")?;
|
||||
|
||||
let buffer = DmaBuffer::allocate(byte_len, DMA_ALIGNMENT)
|
||||
.map_err(|_| "failed to allocate IOMMU command buffer")?;
|
||||
|
||||
if buffer.len() < byte_len {
|
||||
return Err("IOMMU command buffer allocation was smaller than requested");
|
||||
}
|
||||
|
||||
if !buffer.is_physically_contiguous() {
|
||||
return Err("IOMMU command buffer allocation is not physically contiguous");
|
||||
}
|
||||
|
||||
if buffer.physical_address() & (DMA_ALIGNMENT - 1) != 0 {
|
||||
return Err("IOMMU command buffer allocation is not 4KiB-aligned");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
buffer,
|
||||
capacity: entry_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn physical_address(&self) -> usize {
|
||||
self.buffer.physical_address()
|
||||
}
|
||||
|
||||
/// Write a command at the given index.
|
||||
pub fn write_command(&mut self, index: usize, cmd: &CommandEntry) {
|
||||
assert!(index < self.capacity, "IOMMU command index out of bounds");
|
||||
self.commands_mut()[index] = *cmd;
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.capacity
|
||||
}
|
||||
|
||||
fn commands_mut(&mut self) -> &mut [CommandEntry] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(self.buffer.as_mut_ptr() as *mut CommandEntry, self.capacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Event log entry (128 bits = 16 bytes).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct EventLogEntry {
|
||||
words: [u32; 4],
|
||||
}
|
||||
|
||||
impl EventLogEntry {
|
||||
pub const fn new() -> Self {
|
||||
Self { words: [0; 4] }
|
||||
}
|
||||
|
||||
pub const fn from_words(words: [u32; 4]) -> Self {
|
||||
Self { words }
|
||||
}
|
||||
|
||||
pub fn words(&self) -> [u32; 4] {
|
||||
self.words
|
||||
}
|
||||
|
||||
pub fn event_type(&self) -> u32 {
|
||||
self.words[0] & 0xFFFF
|
||||
}
|
||||
|
||||
pub fn event_flags(&self) -> u16 {
|
||||
((self.words[0] >> 16) & 0xFFFF) as u16
|
||||
}
|
||||
|
||||
pub fn device_id(&self) -> u16 {
|
||||
self.words[1] as u16
|
||||
}
|
||||
|
||||
pub fn virtual_address(&self) -> u64 {
|
||||
((self.words[3] as u64) << 32) | (self.words[2] as u64)
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = assert!(size_of::<EventLogEntry>() == EVENT_LOG_ENTRY_SIZE);
|
||||
|
||||
pub struct EventLog {
|
||||
buffer: DmaBuffer,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl EventLog {
|
||||
pub fn new(entry_count: usize) -> Result<Self, &'static str> {
|
||||
if entry_count == 0 {
|
||||
return Err("IOMMU event log entry count must be non-zero");
|
||||
}
|
||||
|
||||
let byte_len = entry_count
|
||||
.checked_mul(EVENT_LOG_ENTRY_SIZE)
|
||||
.ok_or("IOMMU event log size overflow")?;
|
||||
|
||||
let buffer = DmaBuffer::allocate(byte_len, DMA_ALIGNMENT)
|
||||
.map_err(|_| "failed to allocate IOMMU event log")?;
|
||||
|
||||
if buffer.len() < byte_len {
|
||||
return Err("IOMMU event log allocation was smaller than requested");
|
||||
}
|
||||
|
||||
if !buffer.is_physically_contiguous() {
|
||||
return Err("IOMMU event log allocation is not physically contiguous");
|
||||
}
|
||||
|
||||
if buffer.physical_address() & (DMA_ALIGNMENT - 1) != 0 {
|
||||
return Err("IOMMU event log allocation is not 4KiB-aligned");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
buffer,
|
||||
capacity: entry_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn physical_address(&self) -> usize {
|
||||
self.buffer.physical_address()
|
||||
}
|
||||
|
||||
pub fn read_entry(&self, index: usize) -> EventLogEntry {
|
||||
assert!(index < self.capacity, "IOMMU event log index out of bounds");
|
||||
self.entries()[index]
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.capacity
|
||||
}
|
||||
|
||||
fn entries(&self) -> &[EventLogEntry] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(self.buffer.as_ptr() as *const EventLogEntry, self.capacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
CommandEntry, EventLogEntry, CMD_COMPLETION_WAIT, CMD_INVALIDATE_DEVTAB_ENTRY,
|
||||
CMD_INVALIDATE_IOMMU_ALL, CMD_INVALIDATE_IOMMU_PAGES, EVENT_IO_PAGE_FAULT,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_completion_wait_command() {
|
||||
let store_addr = 0x1234_5000_0000_1000;
|
||||
let store_data = 0xabcdefff;
|
||||
let cmd = CommandEntry::completion_wait(store_addr, store_data);
|
||||
let words = cmd.words();
|
||||
|
||||
assert_eq!(cmd.opcode(), CMD_COMPLETION_WAIT);
|
||||
assert!(cmd.completion_wait_store());
|
||||
assert!(!cmd.completion_wait_interrupt());
|
||||
assert_eq!(words[1], store_addr as u32);
|
||||
assert_eq!(words[2], (store_addr >> 32) as u32);
|
||||
assert_eq!(words[3], store_data);
|
||||
assert_eq!(cmd.completion_wait_store_address(), store_addr);
|
||||
assert_eq!(cmd.completion_wait_store_data(), store_data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalidate_devtab_command() {
|
||||
let device_id = 0x1234;
|
||||
let cmd = CommandEntry::invalidate_devtab_entry(device_id);
|
||||
let words = cmd.words();
|
||||
|
||||
assert_eq!(cmd.opcode(), CMD_INVALIDATE_DEVTAB_ENTRY);
|
||||
assert_eq!(cmd.invalidate_device_id(), device_id);
|
||||
assert_eq!(words[1], device_id as u32);
|
||||
assert_eq!(words[2], 0);
|
||||
assert_eq!(words[3], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalidate_pages_command() {
|
||||
let device_id = 0x4321;
|
||||
let addr = 0xfeed_cafe_b000;
|
||||
let cmd = CommandEntry::invalidate_pages(device_id, addr);
|
||||
let words = cmd.words();
|
||||
|
||||
assert_eq!(cmd.opcode(), CMD_INVALIDATE_IOMMU_PAGES);
|
||||
assert_eq!(cmd.invalidate_device_id(), device_id);
|
||||
assert!(!cmd.invalidate_pages_pde());
|
||||
assert!(!cmd.invalidate_pages_size());
|
||||
assert_eq!(words[1], device_id as u32);
|
||||
assert_eq!(cmd.invalidate_pages_address(), addr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalidate_all_command() {
|
||||
let cmd = CommandEntry::invalidate_all();
|
||||
let words = cmd.words();
|
||||
|
||||
assert_eq!(cmd.opcode(), CMD_INVALIDATE_IOMMU_ALL);
|
||||
assert_eq!(words[1], 0);
|
||||
assert_eq!(words[2], 0);
|
||||
assert_eq!(words[3], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_entry_parsing() {
|
||||
let device_id = 0x2468;
|
||||
let address = 0x0123_4567_89ab_cdef;
|
||||
let entry = EventLogEntry::from_words([
|
||||
EVENT_IO_PAGE_FAULT | ((0x5a as u32) << 16),
|
||||
device_id as u32,
|
||||
address as u32,
|
||||
(address >> 32) as u32,
|
||||
]);
|
||||
|
||||
assert_eq!(entry.event_type(), EVENT_IO_PAGE_FAULT);
|
||||
assert_eq!(entry.event_flags(), 0x5a);
|
||||
assert_eq!(entry.device_id(), device_id);
|
||||
assert_eq!(entry.virtual_address(), address);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
use core::mem::size_of;
|
||||
use core::slice;
|
||||
|
||||
use redox_driver_sys::dma::DmaBuffer;
|
||||
|
||||
/// AMD-Vi Device Table: 65536 entries × 32 bytes = 2 MiB.
|
||||
pub const DEVICE_TABLE_ENTRIES: usize = 65_536;
|
||||
pub const DTE_SIZE: usize = 32;
|
||||
|
||||
const DEVICE_TABLE_BYTES: usize = DEVICE_TABLE_ENTRIES * DTE_SIZE;
|
||||
|
||||
const DTE_VALID_BIT: u64 = 1 << 0;
|
||||
const DTE_TRANSLATION_VALID_BIT: u64 = 1 << 1;
|
||||
const DTE_WRITE_PERMISSION_BIT: u64 = 1 << 4;
|
||||
const DTE_READ_PERMISSION_BIT: u64 = 1 << 5;
|
||||
const DTE_SNOOP_ENABLE_BIT: u64 = 1 << 8;
|
||||
const DTE_MODE_SHIFT: u32 = 9;
|
||||
const DTE_MODE_MASK: u64 = 0x7 << DTE_MODE_SHIFT;
|
||||
const DTE_PAGE_TABLE_ROOT_MASK: u64 = ((1u64 << 40) - 1) << 12;
|
||||
const DTE_INTERRUPT_REMAP_BIT: u64 = 1 << 61;
|
||||
const DTE_INTERRUPT_WRITE_BIT: u64 = 1 << 62;
|
||||
|
||||
const DTE_INT_TABLE_LEN_MASK: u64 = 0xF;
|
||||
const DTE_INT_CONTROL_SHIFT: u32 = 4;
|
||||
const DTE_INT_CONTROL_MASK: u64 = 0x3 << DTE_INT_CONTROL_SHIFT;
|
||||
const DTE_INT_REMAP_TABLE_PTR_SHIFT: u32 = 6;
|
||||
const DTE_INT_REMAP_TABLE_PTR_MASK: u64 = ((1u64 << 46) - 1) << DTE_INT_REMAP_TABLE_PTR_SHIFT;
|
||||
|
||||
/// Device Table Entry (DTE) — 256 bits (32 bytes = 4 × u64).
|
||||
///
|
||||
/// Layout follows AMD IOMMU Spec 48882 Rev 3.10, Section 3.2.2.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct DeviceTableEntry {
|
||||
data: [u64; 4],
|
||||
}
|
||||
|
||||
impl DeviceTableEntry {
|
||||
pub const fn new() -> Self {
|
||||
Self { data: [0; 4] }
|
||||
}
|
||||
|
||||
pub fn valid(&self) -> bool {
|
||||
self.data[0] & DTE_VALID_BIT != 0
|
||||
}
|
||||
|
||||
pub fn set_valid(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= DTE_VALID_BIT;
|
||||
} else {
|
||||
self.data[0] &= !DTE_VALID_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn translation_valid(&self) -> bool {
|
||||
self.data[0] & DTE_TRANSLATION_VALID_BIT != 0
|
||||
}
|
||||
|
||||
pub fn set_translation_valid(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= DTE_TRANSLATION_VALID_BIT;
|
||||
} else {
|
||||
self.data[0] &= !DTE_TRANSLATION_VALID_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_permission(&self) -> bool {
|
||||
self.data[0] & DTE_WRITE_PERMISSION_BIT != 0
|
||||
}
|
||||
|
||||
pub fn set_write_permission(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= DTE_WRITE_PERMISSION_BIT;
|
||||
} else {
|
||||
self.data[0] &= !DTE_WRITE_PERMISSION_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_permission(&self) -> bool {
|
||||
self.data[0] & DTE_READ_PERMISSION_BIT != 0
|
||||
}
|
||||
|
||||
pub fn set_read_permission(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= DTE_READ_PERMISSION_BIT;
|
||||
} else {
|
||||
self.data[0] &= !DTE_READ_PERMISSION_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snoop_enable(&self) -> bool {
|
||||
self.data[0] & DTE_SNOOP_ENABLE_BIT != 0
|
||||
}
|
||||
|
||||
pub fn set_snoop_enable(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= DTE_SNOOP_ENABLE_BIT;
|
||||
} else {
|
||||
self.data[0] &= !DTE_SNOOP_ENABLE_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> u8 {
|
||||
((self.data[0] & DTE_MODE_MASK) >> DTE_MODE_SHIFT) as u8
|
||||
}
|
||||
|
||||
pub fn set_mode(&mut self, mode: u8) {
|
||||
self.data[0] = (self.data[0] & !DTE_MODE_MASK) | (((mode as u64) & 0x7) << DTE_MODE_SHIFT);
|
||||
}
|
||||
|
||||
/// Returns the full, 4KiB-aligned physical address stored in bits 12:51.
|
||||
pub fn page_table_root(&self) -> u64 {
|
||||
self.data[0] & DTE_PAGE_TABLE_ROOT_MASK
|
||||
}
|
||||
|
||||
pub fn set_page_table_root(&mut self, phys: u64) {
|
||||
self.data[0] =
|
||||
(self.data[0] & !DTE_PAGE_TABLE_ROOT_MASK) | (phys & DTE_PAGE_TABLE_ROOT_MASK);
|
||||
}
|
||||
|
||||
/// Interrupt remapping enable (bit 61 of word 0 in the AMD-Vi DTE).
|
||||
pub fn interrupt_remap(&self) -> bool {
|
||||
self.data[0] & DTE_INTERRUPT_REMAP_BIT != 0
|
||||
}
|
||||
|
||||
pub fn set_interrupt_remap(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= DTE_INTERRUPT_REMAP_BIT;
|
||||
} else {
|
||||
self.data[0] &= !DTE_INTERRUPT_REMAP_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
/// Interrupt write permission (bit 62 of word 0 in the AMD-Vi DTE).
|
||||
pub fn interrupt_write(&self) -> bool {
|
||||
self.data[0] & DTE_INTERRUPT_WRITE_BIT != 0
|
||||
}
|
||||
|
||||
pub fn set_interrupt_write(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= DTE_INTERRUPT_WRITE_BIT;
|
||||
} else {
|
||||
self.data[0] &= !DTE_INTERRUPT_WRITE_BIT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn int_table_len(&self) -> u8 {
|
||||
(self.data[1] & DTE_INT_TABLE_LEN_MASK) as u8
|
||||
}
|
||||
|
||||
pub fn set_int_table_len(&mut self, len: u8) {
|
||||
self.data[1] =
|
||||
(self.data[1] & !DTE_INT_TABLE_LEN_MASK) | ((len as u64) & DTE_INT_TABLE_LEN_MASK);
|
||||
}
|
||||
|
||||
pub fn interrupt_control(&self) -> u8 {
|
||||
((self.data[1] & DTE_INT_CONTROL_MASK) >> DTE_INT_CONTROL_SHIFT) as u8
|
||||
}
|
||||
|
||||
pub fn set_interrupt_control(&mut self, control: u8) {
|
||||
self.data[1] = (self.data[1] & !DTE_INT_CONTROL_MASK)
|
||||
| (((control as u64) & 0x3) << DTE_INT_CONTROL_SHIFT);
|
||||
}
|
||||
|
||||
/// Returns the interrupt remap table pointer bits stored in word 1.
|
||||
pub fn int_remap_table_ptr(&self) -> u64 {
|
||||
self.data[1] & DTE_INT_REMAP_TABLE_PTR_MASK
|
||||
}
|
||||
|
||||
pub fn set_int_remap_table_ptr(&mut self, phys: u64) {
|
||||
self.data[1] =
|
||||
(self.data[1] & !DTE_INT_REMAP_TABLE_PTR_MASK) | (phys & DTE_INT_REMAP_TABLE_PTR_MASK);
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = assert!(size_of::<DeviceTableEntry>() == DTE_SIZE);
|
||||
|
||||
/// Device Table — manages the 65536-entry device table.
|
||||
pub struct DeviceTable {
|
||||
buffer: DmaBuffer,
|
||||
}
|
||||
|
||||
impl DeviceTable {
|
||||
/// Allocate a new device table (65536 × 32 bytes = 2 MiB).
|
||||
pub fn new() -> Result<Self, &'static str> {
|
||||
let buffer = DmaBuffer::allocate(DEVICE_TABLE_BYTES, 4096)
|
||||
.map_err(|_| "failed to allocate IOMMU device table")?;
|
||||
|
||||
if buffer.len() < DEVICE_TABLE_BYTES {
|
||||
return Err("IOMMU device table allocation was smaller than requested");
|
||||
}
|
||||
|
||||
if !buffer.is_physically_contiguous() {
|
||||
return Err("IOMMU device table allocation is not physically contiguous");
|
||||
}
|
||||
|
||||
Ok(Self { buffer })
|
||||
}
|
||||
|
||||
pub fn get_entry(&self, device_id: u16) -> DeviceTableEntry {
|
||||
self.entries()[device_id as usize]
|
||||
}
|
||||
|
||||
pub fn set_entry(&mut self, device_id: u16, entry: &DeviceTableEntry) {
|
||||
self.entries_mut()[device_id as usize] = *entry;
|
||||
}
|
||||
|
||||
pub fn clear_entry(&mut self, device_id: u16) {
|
||||
self.entries_mut()[device_id as usize] = DeviceTableEntry::new();
|
||||
}
|
||||
|
||||
pub fn physical_address(&self) -> usize {
|
||||
self.buffer.physical_address()
|
||||
}
|
||||
|
||||
/// Convert PCI BDF to device ID.
|
||||
/// Bus: bits 8:15, Device: bits 3:7, Function: bits 0:2.
|
||||
pub fn bdf_to_device_id(bus: u8, device: u8, function: u8) -> u16 {
|
||||
((bus as u16) << 8) | ((device as u16) << 3) | (function as u16)
|
||||
}
|
||||
|
||||
fn entries(&self) -> &[DeviceTableEntry] {
|
||||
unsafe {
|
||||
slice::from_raw_parts(
|
||||
self.buffer.as_ptr() as *const DeviceTableEntry,
|
||||
DEVICE_TABLE_ENTRIES,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn entries_mut(&mut self) -> &mut [DeviceTableEntry] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(
|
||||
self.buffer.as_mut_ptr() as *mut DeviceTableEntry,
|
||||
DEVICE_TABLE_ENTRIES,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DeviceTable, DeviceTableEntry, DTE_PAGE_TABLE_ROOT_MASK};
|
||||
|
||||
fn try_allocate_table() -> Option<DeviceTable> {
|
||||
match DeviceTable::new() {
|
||||
Ok(table) => Some(table),
|
||||
Err(err) => {
|
||||
eprintln!("skipping DeviceTable allocation-dependent test: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dte_valid_bit() {
|
||||
let mut entry = DeviceTableEntry::new();
|
||||
assert!(!entry.valid());
|
||||
|
||||
entry.set_valid(true);
|
||||
assert!(entry.valid());
|
||||
|
||||
entry.set_valid(false);
|
||||
assert!(!entry.valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dte_translation_valid() {
|
||||
let mut entry = DeviceTableEntry::new();
|
||||
assert!(!entry.translation_valid());
|
||||
|
||||
entry.set_translation_valid(true);
|
||||
assert!(entry.translation_valid());
|
||||
|
||||
entry.set_translation_valid(false);
|
||||
assert!(!entry.translation_valid());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dte_mode_4level() {
|
||||
let mut entry = DeviceTableEntry::new();
|
||||
entry.set_mode(4);
|
||||
|
||||
assert_eq!(entry.mode(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dte_permissions_and_interrupt_control() {
|
||||
let mut entry = DeviceTableEntry::new();
|
||||
entry.set_read_permission(true);
|
||||
entry.set_write_permission(true);
|
||||
entry.set_snoop_enable(true);
|
||||
entry.set_interrupt_control(0x02);
|
||||
|
||||
assert!(entry.read_permission());
|
||||
assert!(entry.write_permission());
|
||||
assert!(entry.snoop_enable());
|
||||
assert_eq!(entry.interrupt_control(), 0x02);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dte_page_table_root() {
|
||||
let mut entry = DeviceTableEntry::new();
|
||||
entry.set_page_table_root(0x1234_5000);
|
||||
|
||||
assert_eq!(entry.page_table_root(), 0x1234_5000);
|
||||
assert_eq!(entry.data[0] & DTE_PAGE_TABLE_ROOT_MASK, 0x1234_5000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bdf_encoding() {
|
||||
assert_eq!(DeviceTable::bdf_to_device_id(0x12, 0x05, 0x03), 0x122b);
|
||||
assert_eq!(DeviceTable::bdf_to_device_id(0xff, 0x1f, 0x07), 0xffff);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_entry() -> Result<(), &'static str> {
|
||||
let Some(mut table) = try_allocate_table() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let device_id = DeviceTable::bdf_to_device_id(0x02, 0x00, 0x00);
|
||||
let mut entry = DeviceTableEntry::new();
|
||||
entry.set_valid(true);
|
||||
entry.set_translation_valid(true);
|
||||
entry.set_mode(4);
|
||||
entry.set_page_table_root(0x1234_5000);
|
||||
|
||||
table.set_entry(device_id, &entry);
|
||||
assert_eq!(table.get_entry(device_id), entry);
|
||||
|
||||
table.clear_entry(device_id);
|
||||
assert_eq!(table.get_entry(device_id), DeviceTableEntry::new());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
use core::mem::size_of;
|
||||
use core::slice;
|
||||
|
||||
use redox_driver_sys::dma::DmaBuffer;
|
||||
|
||||
pub const IRTE_SIZE: usize = 16;
|
||||
pub const MAX_INTERRUPT_REMAP_ENTRIES: usize = 4096;
|
||||
|
||||
const DMA_ALIGNMENT: usize = 4096;
|
||||
const IRTE_REMAP_ENABLE: u64 = 1 << 0;
|
||||
const IRTE_SUPPRESS_IOPF: u64 = 1 << 1;
|
||||
const IRTE_INT_TYPE_SHIFT: u64 = 2;
|
||||
const IRTE_INT_TYPE_MASK: u64 = 0x7 << IRTE_INT_TYPE_SHIFT;
|
||||
const IRTE_DEST_MODE: u64 = 1 << 8;
|
||||
const IRTE_DEST_LOW_SHIFT: u64 = 16;
|
||||
const IRTE_DEST_LOW_MASK: u64 = 0xFFFF << IRTE_DEST_LOW_SHIFT;
|
||||
const IRTE_VECTOR_SHIFT: u64 = 32;
|
||||
const IRTE_VECTOR_MASK: u64 = 0xFF << IRTE_VECTOR_SHIFT;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct AmdIrte {
|
||||
data: [u64; 2],
|
||||
}
|
||||
|
||||
impl AmdIrte {
|
||||
pub const fn new() -> Self {
|
||||
Self { data: [0; 2] }
|
||||
}
|
||||
|
||||
pub fn remap_enabled(&self) -> bool {
|
||||
self.data[0] & IRTE_REMAP_ENABLE != 0
|
||||
}
|
||||
|
||||
pub fn set_remap_enabled(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= IRTE_REMAP_ENABLE;
|
||||
} else {
|
||||
self.data[0] &= !IRTE_REMAP_ENABLE;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn suppress_io_page_faults(&self) -> bool {
|
||||
self.data[0] & IRTE_SUPPRESS_IOPF != 0
|
||||
}
|
||||
|
||||
pub fn set_suppress_io_page_faults(&mut self, value: bool) {
|
||||
if value {
|
||||
self.data[0] |= IRTE_SUPPRESS_IOPF;
|
||||
} else {
|
||||
self.data[0] &= !IRTE_SUPPRESS_IOPF;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_type(&self) -> u8 {
|
||||
((self.data[0] & IRTE_INT_TYPE_MASK) >> IRTE_INT_TYPE_SHIFT) as u8
|
||||
}
|
||||
|
||||
pub fn set_interrupt_type(&mut self, value: u8) {
|
||||
self.data[0] = (self.data[0] & !IRTE_INT_TYPE_MASK)
|
||||
| ((u64::from(value) & 0x7) << IRTE_INT_TYPE_SHIFT);
|
||||
}
|
||||
|
||||
pub fn destination_mode(&self) -> bool {
|
||||
self.data[0] & IRTE_DEST_MODE != 0
|
||||
}
|
||||
|
||||
pub fn set_destination_mode(&mut self, logical: bool) {
|
||||
if logical {
|
||||
self.data[0] |= IRTE_DEST_MODE;
|
||||
} else {
|
||||
self.data[0] &= !IRTE_DEST_MODE;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn destination(&self) -> u32 {
|
||||
(((self.data[1] & 0xFFFF_FFFF) as u32) << 16)
|
||||
| (((self.data[0] & IRTE_DEST_LOW_MASK) >> IRTE_DEST_LOW_SHIFT) as u32)
|
||||
}
|
||||
|
||||
pub fn set_destination(&mut self, apic_id: u32) {
|
||||
self.data[0] = (self.data[0] & !IRTE_DEST_LOW_MASK)
|
||||
| ((u64::from(apic_id & 0xFFFF)) << IRTE_DEST_LOW_SHIFT);
|
||||
self.data[1] = (self.data[1] & !0xFFFF_FFFF) | u64::from(apic_id >> 16);
|
||||
}
|
||||
|
||||
pub fn vector(&self) -> u8 {
|
||||
((self.data[0] & IRTE_VECTOR_MASK) >> IRTE_VECTOR_SHIFT) as u8
|
||||
}
|
||||
|
||||
pub fn set_vector(&mut self, vector: u8) {
|
||||
self.data[0] =
|
||||
(self.data[0] & !IRTE_VECTOR_MASK) | (u64::from(vector) << IRTE_VECTOR_SHIFT);
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = assert!(size_of::<AmdIrte>() == IRTE_SIZE);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct IrteConfig {
|
||||
pub vector: u8,
|
||||
pub destination: u32,
|
||||
pub logical_destination: bool,
|
||||
pub interrupt_type: u8,
|
||||
pub suppress_io_page_faults: bool,
|
||||
}
|
||||
|
||||
pub struct InterruptRemapTable {
|
||||
buffer: DmaBuffer,
|
||||
capacity: usize,
|
||||
}
|
||||
|
||||
impl InterruptRemapTable {
|
||||
pub fn new(entry_count: usize) -> Result<Self, &'static str> {
|
||||
if !(2..=MAX_INTERRUPT_REMAP_ENTRIES).contains(&entry_count) {
|
||||
return Err("interrupt remap table entry count must be between 2 and 4096");
|
||||
}
|
||||
if !entry_count.is_power_of_two() {
|
||||
return Err("interrupt remap table entry count must be a power of two");
|
||||
}
|
||||
|
||||
let byte_len = entry_count
|
||||
.checked_mul(IRTE_SIZE)
|
||||
.ok_or("interrupt remap table size overflow")?;
|
||||
let buffer = DmaBuffer::allocate(byte_len, DMA_ALIGNMENT)
|
||||
.map_err(|_| "failed to allocate interrupt remap table")?;
|
||||
if buffer.len() < byte_len {
|
||||
return Err("interrupt remap table allocation was smaller than requested");
|
||||
}
|
||||
if !buffer.is_physically_contiguous() {
|
||||
return Err("interrupt remap table allocation is not physically contiguous");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
buffer,
|
||||
capacity: entry_count,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> usize {
|
||||
self.capacity
|
||||
}
|
||||
|
||||
pub fn len_encoding(&self) -> u8 {
|
||||
self.capacity.ilog2() as u8 - 1
|
||||
}
|
||||
|
||||
pub fn physical_address(&self) -> usize {
|
||||
self.buffer.physical_address()
|
||||
}
|
||||
|
||||
pub fn entry(&self, index: usize) -> AmdIrte {
|
||||
assert!(
|
||||
index < self.capacity,
|
||||
"interrupt remap table index out of bounds"
|
||||
);
|
||||
self.entries()[index]
|
||||
}
|
||||
|
||||
pub fn set_entry(&mut self, index: usize, entry: AmdIrte) {
|
||||
assert!(
|
||||
index < self.capacity,
|
||||
"interrupt remap table index out of bounds"
|
||||
);
|
||||
self.entries_mut()[index] = entry;
|
||||
}
|
||||
|
||||
pub fn clear_entry(&mut self, index: usize) {
|
||||
self.set_entry(index, AmdIrte::new());
|
||||
}
|
||||
|
||||
pub fn configure(&mut self, index: usize, config: IrteConfig) {
|
||||
let mut entry = AmdIrte::new();
|
||||
entry.set_remap_enabled(true);
|
||||
entry.set_suppress_io_page_faults(config.suppress_io_page_faults);
|
||||
entry.set_interrupt_type(config.interrupt_type);
|
||||
entry.set_destination_mode(config.logical_destination);
|
||||
entry.set_destination(config.destination);
|
||||
entry.set_vector(config.vector);
|
||||
self.set_entry(index, entry);
|
||||
}
|
||||
|
||||
fn entries(&self) -> &[AmdIrte] {
|
||||
unsafe { slice::from_raw_parts(self.buffer.as_ptr().cast::<AmdIrte>(), self.capacity) }
|
||||
}
|
||||
|
||||
fn entries_mut(&mut self) -> &mut [AmdIrte] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(self.buffer.as_mut_ptr().cast::<AmdIrte>(), self.capacity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AmdIrte;
|
||||
|
||||
#[test]
|
||||
fn irte_accessors_round_trip() {
|
||||
let mut irte = AmdIrte::new();
|
||||
irte.set_remap_enabled(true);
|
||||
irte.set_suppress_io_page_faults(true);
|
||||
irte.set_interrupt_type(3);
|
||||
irte.set_destination_mode(true);
|
||||
irte.set_destination(0x1234_5678);
|
||||
irte.set_vector(0x52);
|
||||
|
||||
assert!(irte.remap_enabled());
|
||||
assert!(irte.suppress_io_page_faults());
|
||||
assert_eq!(irte.interrupt_type(), 3);
|
||||
assert!(irte.destination_mode());
|
||||
assert_eq!(irte.destination(), 0x1234_5678);
|
||||
assert_eq!(irte.vector(), 0x52);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,868 @@
|
||||
//! AMD-Vi-backed scheme:iommu implementation.
|
||||
|
||||
pub mod acpi;
|
||||
pub mod amd_vi;
|
||||
pub mod command_buffer;
|
||||
pub mod device_table;
|
||||
pub mod interrupt;
|
||||
pub mod mmio;
|
||||
pub mod page_table;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use acpi::{parse_bdf, Bdf};
|
||||
use amd_vi::AmdViUnit;
|
||||
use page_table::{DomainPageTables, MappingFlags};
|
||||
use redox_scheme::SchemeBlockMut;
|
||||
use syscall::data::Stat;
|
||||
use syscall::error::{Error, Result, EBADF, EINVAL, EIO, EISDIR, ENODEV, ENOENT};
|
||||
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
|
||||
|
||||
pub const IOMMU_PROTOCOL_VERSION: u16 = 1;
|
||||
|
||||
pub mod opcode {
|
||||
pub const QUERY: u16 = 0x0000;
|
||||
pub const CREATE_DOMAIN: u16 = 0x0001;
|
||||
pub const DESTROY_DOMAIN: u16 = 0x0002;
|
||||
pub const MAP: u16 = 0x0010;
|
||||
pub const UNMAP: u16 = 0x0011;
|
||||
pub const ASSIGN_DEVICE: u16 = 0x0020;
|
||||
pub const UNASSIGN_DEVICE: u16 = 0x0021;
|
||||
pub const DRAIN_EVENTS: u16 = 0x0030;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct IommuRequest {
|
||||
pub opcode: u16,
|
||||
pub version: u16,
|
||||
pub arg0: u32,
|
||||
pub arg1: u64,
|
||||
pub arg2: u64,
|
||||
pub arg3: u64,
|
||||
}
|
||||
|
||||
impl IommuRequest {
|
||||
pub const SIZE: usize = 32;
|
||||
|
||||
pub const fn new(opcode: u16, arg0: u32, arg1: u64, arg2: u64, arg3: u64) -> Self {
|
||||
Self {
|
||||
opcode,
|
||||
version: IOMMU_PROTOCOL_VERSION,
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
let header = bytes.get(..Self::SIZE)?;
|
||||
Some(Self {
|
||||
opcode: u16::from_le_bytes(header.get(0..2)?.try_into().ok()?),
|
||||
version: u16::from_le_bytes(header.get(2..4)?.try_into().ok()?),
|
||||
arg0: u32::from_le_bytes(header.get(4..8)?.try_into().ok()?),
|
||||
arg1: u64::from_le_bytes(header.get(8..16)?.try_into().ok()?),
|
||||
arg2: u64::from_le_bytes(header.get(16..24)?.try_into().ok()?),
|
||||
arg3: u64::from_le_bytes(header.get(24..32)?.try_into().ok()?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; Self::SIZE] {
|
||||
let mut bytes = [0u8; Self::SIZE];
|
||||
bytes[0..2].copy_from_slice(&self.opcode.to_le_bytes());
|
||||
bytes[2..4].copy_from_slice(&self.version.to_le_bytes());
|
||||
bytes[4..8].copy_from_slice(&self.arg0.to_le_bytes());
|
||||
bytes[8..16].copy_from_slice(&self.arg1.to_le_bytes());
|
||||
bytes[16..24].copy_from_slice(&self.arg2.to_le_bytes());
|
||||
bytes[24..32].copy_from_slice(&self.arg3.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct IommuResponse {
|
||||
pub status: i32,
|
||||
pub kind: u16,
|
||||
pub version: u16,
|
||||
pub arg0: u32,
|
||||
pub arg1: u64,
|
||||
pub arg2: u64,
|
||||
pub arg3: u64,
|
||||
}
|
||||
|
||||
impl IommuResponse {
|
||||
pub const SIZE: usize = 36;
|
||||
|
||||
pub const fn success(kind: u16, arg0: u32, arg1: u64, arg2: u64, arg3: u64) -> Self {
|
||||
Self {
|
||||
status: 0,
|
||||
kind,
|
||||
version: IOMMU_PROTOCOL_VERSION,
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn error(kind: u16, errno: i32) -> Self {
|
||||
Self {
|
||||
status: -errno,
|
||||
kind,
|
||||
version: IOMMU_PROTOCOL_VERSION,
|
||||
arg0: 0,
|
||||
arg1: 0,
|
||||
arg2: 0,
|
||||
arg3: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
let header = bytes.get(..Self::SIZE)?;
|
||||
Some(Self {
|
||||
status: i32::from_le_bytes(header.get(0..4)?.try_into().ok()?),
|
||||
kind: u16::from_le_bytes(header.get(4..6)?.try_into().ok()?),
|
||||
version: u16::from_le_bytes(header.get(6..8)?.try_into().ok()?),
|
||||
arg0: u32::from_le_bytes(header.get(8..12)?.try_into().ok()?),
|
||||
arg1: u64::from_le_bytes(header.get(12..20)?.try_into().ok()?),
|
||||
arg2: u64::from_le_bytes(header.get(20..28)?.try_into().ok()?),
|
||||
arg3: u64::from_le_bytes(header.get(28..36)?.try_into().ok()?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(self) -> [u8; Self::SIZE] {
|
||||
let mut bytes = [0u8; Self::SIZE];
|
||||
bytes[0..4].copy_from_slice(&self.status.to_le_bytes());
|
||||
bytes[4..6].copy_from_slice(&self.kind.to_le_bytes());
|
||||
bytes[6..8].copy_from_slice(&self.version.to_le_bytes());
|
||||
bytes[8..12].copy_from_slice(&self.arg0.to_le_bytes());
|
||||
bytes[12..20].copy_from_slice(&self.arg1.to_le_bytes());
|
||||
bytes[20..28].copy_from_slice(&self.arg2.to_le_bytes());
|
||||
bytes[28..36].copy_from_slice(&self.arg3.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum HandleKind {
|
||||
Root,
|
||||
Control,
|
||||
Domain(u16),
|
||||
Device(Bdf),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Handle {
|
||||
kind: HandleKind,
|
||||
offset: usize,
|
||||
response: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct IommuScheme {
|
||||
units: Vec<AmdViUnit>,
|
||||
next_id: usize,
|
||||
handles: BTreeMap<usize, Handle>,
|
||||
domains: BTreeMap<u16, DomainPageTables>,
|
||||
device_assignments: BTreeMap<Bdf, (u16, usize)>,
|
||||
}
|
||||
|
||||
impl IommuScheme {
|
||||
pub fn new() -> Self {
|
||||
Self::with_units(Vec::new())
|
||||
}
|
||||
|
||||
pub fn with_units(units: Vec<AmdViUnit>) -> Self {
|
||||
Self {
|
||||
units,
|
||||
next_id: 0,
|
||||
handles: BTreeMap::new(),
|
||||
domains: BTreeMap::new(),
|
||||
device_assignments: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unit_count(&self) -> usize {
|
||||
self.units.len()
|
||||
}
|
||||
|
||||
fn insert_handle(&mut self, kind: HandleKind) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id = self.next_id.saturating_add(1);
|
||||
self.handles.insert(
|
||||
id,
|
||||
Handle {
|
||||
kind,
|
||||
offset: 0,
|
||||
response: Vec::new(),
|
||||
},
|
||||
);
|
||||
id
|
||||
}
|
||||
|
||||
fn ensure_domain_exists(&mut self, domain_id: u16) -> core::result::Result<(), i32> {
|
||||
if self.domains.contains_key(&domain_id) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let domain = DomainPageTables::new(domain_id).map_err(|_| EIO as i32)?;
|
||||
self.domains.insert(domain_id, domain);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_domain_id(&self) -> Option<u16> {
|
||||
(1..u16::MAX).find(|domain_id| !self.domains.contains_key(domain_id))
|
||||
}
|
||||
|
||||
fn root_listing(&self) -> Vec<u8> {
|
||||
let mut listing = String::from("control\n");
|
||||
for (index, unit) in self.units.iter().enumerate() {
|
||||
let state = if unit.initialized() {
|
||||
"initialized"
|
||||
} else {
|
||||
"detected"
|
||||
};
|
||||
listing.push_str(&format!(
|
||||
"unit/{index} {} mmio={:#x} state={}\n",
|
||||
unit.info().iommu_bdf,
|
||||
unit.info().mmio_base,
|
||||
state
|
||||
));
|
||||
}
|
||||
for domain_id in self.domains.keys() {
|
||||
listing.push_str(&format!("domain/{domain_id}\n"));
|
||||
}
|
||||
for bdf in self.device_assignments.keys() {
|
||||
listing.push_str(&format!("device/{bdf}\n"));
|
||||
}
|
||||
listing.into_bytes()
|
||||
}
|
||||
|
||||
fn parse_domain_id(path: &str) -> Option<u16> {
|
||||
let trimmed = path.trim();
|
||||
trimmed
|
||||
.strip_prefix("0x")
|
||||
.and_then(|hex| u16::from_str_radix(hex, 16).ok())
|
||||
.or_else(|| trimmed.parse::<u16>().ok())
|
||||
.or_else(|| u16::from_str_radix(trimmed, 16).ok())
|
||||
}
|
||||
|
||||
fn map_flags(bits: u32) -> MappingFlags {
|
||||
let flags = MappingFlags {
|
||||
readable: bits & 0x1 != 0,
|
||||
writable: bits & 0x2 != 0,
|
||||
executable: bits & 0x4 != 0,
|
||||
force_coherent: bits & 0x8 != 0,
|
||||
user: bits & 0x10 != 0,
|
||||
};
|
||||
|
||||
if !flags.readable
|
||||
&& !flags.writable
|
||||
&& !flags.executable
|
||||
&& !flags.force_coherent
|
||||
&& !flags.user
|
||||
{
|
||||
MappingFlags::read_write()
|
||||
} else {
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_unit_for_device(
|
||||
&self,
|
||||
bdf: Bdf,
|
||||
requested_unit: Option<usize>,
|
||||
) -> core::result::Result<usize, i32> {
|
||||
if let Some(index) = requested_unit {
|
||||
let Some(unit) = self.units.get(index) else {
|
||||
return Err(ENODEV as i32);
|
||||
};
|
||||
if unit.handles_device(bdf) {
|
||||
return Ok(index);
|
||||
}
|
||||
return Err(ENODEV as i32);
|
||||
}
|
||||
|
||||
self.units
|
||||
.iter()
|
||||
.position(|unit| unit.handles_device(bdf))
|
||||
.ok_or(ENODEV as i32)
|
||||
}
|
||||
|
||||
fn dispatch_request(&mut self, kind: HandleKind, request: IommuRequest) -> IommuResponse {
|
||||
if request.version != IOMMU_PROTOCOL_VERSION {
|
||||
return IommuResponse::error(request.opcode, EINVAL as i32);
|
||||
}
|
||||
|
||||
match kind {
|
||||
HandleKind::Root => IommuResponse::error(request.opcode, EISDIR as i32),
|
||||
HandleKind::Control => self.handle_control_request(request),
|
||||
HandleKind::Domain(domain_id) => self.handle_domain_request(domain_id, request),
|
||||
HandleKind::Device(bdf) => self.handle_device_request(bdf, request),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_control_request(&mut self, request: IommuRequest) -> IommuResponse {
|
||||
match request.opcode {
|
||||
opcode::QUERY => IommuResponse::success(
|
||||
request.opcode,
|
||||
self.units.len() as u32,
|
||||
self.domains.len() as u64,
|
||||
self.device_assignments.len() as u64,
|
||||
self.units.iter().filter(|unit| unit.initialized()).count() as u64,
|
||||
),
|
||||
opcode::CREATE_DOMAIN => {
|
||||
let domain_id = if request.arg0 == 0 {
|
||||
match self.next_domain_id() {
|
||||
Some(domain_id) => domain_id,
|
||||
None => return IommuResponse::error(request.opcode, EIO as i32),
|
||||
}
|
||||
} else {
|
||||
request.arg0 as u16
|
||||
};
|
||||
|
||||
if let Err(errno) = self.ensure_domain_exists(domain_id) {
|
||||
return IommuResponse::error(request.opcode, errno);
|
||||
}
|
||||
let Some(domain) = self.domains.get(&domain_id) else {
|
||||
return IommuResponse::error(request.opcode, EIO as i32);
|
||||
};
|
||||
IommuResponse::success(
|
||||
request.opcode,
|
||||
domain_id as u32,
|
||||
domain.root_address(),
|
||||
domain.levels() as u64,
|
||||
domain.mapping_count() as u64,
|
||||
)
|
||||
}
|
||||
opcode::DESTROY_DOMAIN => {
|
||||
let domain_id = request.arg0 as u16;
|
||||
if self
|
||||
.device_assignments
|
||||
.values()
|
||||
.any(|(assigned_domain, _)| *assigned_domain == domain_id)
|
||||
{
|
||||
return IommuResponse::error(request.opcode, EINVAL as i32);
|
||||
}
|
||||
if self.domains.remove(&domain_id).is_none() {
|
||||
return IommuResponse::error(request.opcode, ENOENT as i32);
|
||||
}
|
||||
IommuResponse::success(request.opcode, domain_id as u32, 0, 0, 0)
|
||||
}
|
||||
opcode::DRAIN_EVENTS => {
|
||||
let requested_index = if request.arg0 == u32::MAX {
|
||||
None
|
||||
} else {
|
||||
Some(request.arg0 as usize)
|
||||
};
|
||||
|
||||
let mut count = 0u32;
|
||||
let mut first_code = 0u64;
|
||||
let mut first_device = 0u64;
|
||||
let mut first_address = 0u64;
|
||||
|
||||
for (index, unit) in self.units.iter_mut().enumerate() {
|
||||
if requested_index.is_some() && requested_index != Some(index) {
|
||||
continue;
|
||||
}
|
||||
match unit.drain_events() {
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.first() {
|
||||
if count == 0 {
|
||||
first_code = u64::from(event.event_code);
|
||||
first_device = u64::from(event.device_id.raw());
|
||||
first_address = event.address;
|
||||
}
|
||||
count = count.saturating_add(events.len() as u32);
|
||||
}
|
||||
}
|
||||
Err(_) => return IommuResponse::error(request.opcode, EIO as i32),
|
||||
}
|
||||
}
|
||||
|
||||
IommuResponse::success(
|
||||
request.opcode,
|
||||
count,
|
||||
first_code,
|
||||
first_device,
|
||||
first_address,
|
||||
)
|
||||
}
|
||||
_ => IommuResponse::error(request.opcode, EINVAL as i32),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_domain_request(&mut self, domain_id: u16, request: IommuRequest) -> IommuResponse {
|
||||
if let Err(errno) = self.ensure_domain_exists(domain_id) {
|
||||
return IommuResponse::error(request.opcode, errno);
|
||||
}
|
||||
|
||||
match request.opcode {
|
||||
opcode::QUERY => {
|
||||
let Some(domain) = self.domains.get(&domain_id) else {
|
||||
return IommuResponse::error(request.opcode, ENOENT as i32);
|
||||
};
|
||||
IommuResponse::success(
|
||||
request.opcode,
|
||||
domain_id as u32,
|
||||
domain.root_address(),
|
||||
domain.levels() as u64,
|
||||
domain.mapping_count() as u64,
|
||||
)
|
||||
}
|
||||
opcode::MAP => {
|
||||
let flags = Self::map_flags(request.arg0);
|
||||
let preferred_iova = if request.arg3 == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(request.arg3)
|
||||
};
|
||||
let Some(domain) = self.domains.get_mut(&domain_id) else {
|
||||
return IommuResponse::error(request.opcode, ENOENT as i32);
|
||||
};
|
||||
match domain.map_range(request.arg1, request.arg2, flags, preferred_iova) {
|
||||
Ok(iova) => IommuResponse::success(
|
||||
request.opcode,
|
||||
domain_id as u32,
|
||||
iova,
|
||||
request.arg2,
|
||||
0,
|
||||
),
|
||||
Err(_) => IommuResponse::error(request.opcode, EIO as i32),
|
||||
}
|
||||
}
|
||||
opcode::UNMAP => {
|
||||
let Some(domain) = self.domains.get_mut(&domain_id) else {
|
||||
return IommuResponse::error(request.opcode, ENOENT as i32);
|
||||
};
|
||||
match domain.unmap_range(request.arg1) {
|
||||
Ok(size) => IommuResponse::success(
|
||||
request.opcode,
|
||||
domain_id as u32,
|
||||
request.arg1,
|
||||
size,
|
||||
0,
|
||||
),
|
||||
Err(_) => IommuResponse::error(request.opcode, ENOENT as i32),
|
||||
}
|
||||
}
|
||||
_ => IommuResponse::error(request.opcode, EINVAL as i32),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_device_request(&mut self, bdf: Bdf, request: IommuRequest) -> IommuResponse {
|
||||
match request.opcode {
|
||||
opcode::QUERY => {
|
||||
let (domain_id, unit_index) = self
|
||||
.device_assignments
|
||||
.get(&bdf)
|
||||
.copied()
|
||||
.unwrap_or((0, usize::MAX));
|
||||
IommuResponse::success(
|
||||
request.opcode,
|
||||
domain_id as u32,
|
||||
if unit_index == usize::MAX {
|
||||
u64::MAX
|
||||
} else {
|
||||
unit_index as u64
|
||||
},
|
||||
u64::from(bdf.raw()),
|
||||
0,
|
||||
)
|
||||
}
|
||||
opcode::ASSIGN_DEVICE => {
|
||||
let domain_id = request.arg0 as u16;
|
||||
if let Err(errno) = self.ensure_domain_exists(domain_id) {
|
||||
return IommuResponse::error(request.opcode, errno);
|
||||
}
|
||||
|
||||
let requested_unit = if request.arg1 == u64::MAX {
|
||||
None
|
||||
} else {
|
||||
Some(request.arg1 as usize)
|
||||
};
|
||||
let unit_index = match self.choose_unit_for_device(bdf, requested_unit) {
|
||||
Ok(index) => index,
|
||||
Err(errno) => return IommuResponse::error(request.opcode, errno),
|
||||
};
|
||||
|
||||
let Some(domain) = self.domains.get(&domain_id) else {
|
||||
return IommuResponse::error(request.opcode, ENOENT as i32);
|
||||
};
|
||||
let Some(unit) = self.units.get_mut(unit_index) else {
|
||||
return IommuResponse::error(request.opcode, ENODEV as i32);
|
||||
};
|
||||
|
||||
match unit.assign_device(bdf, domain) {
|
||||
Ok(()) => {
|
||||
self.device_assignments.insert(bdf, (domain_id, unit_index));
|
||||
IommuResponse::success(
|
||||
request.opcode,
|
||||
domain_id as u32,
|
||||
unit_index as u64,
|
||||
u64::from(bdf.raw()),
|
||||
0,
|
||||
)
|
||||
}
|
||||
Err(_) => IommuResponse::error(request.opcode, EIO as i32),
|
||||
}
|
||||
}
|
||||
opcode::UNASSIGN_DEVICE => {
|
||||
if self.device_assignments.remove(&bdf).is_none() {
|
||||
return IommuResponse::error(request.opcode, ENOENT as i32);
|
||||
}
|
||||
IommuResponse::success(request.opcode, 0, u64::from(bdf.raw()), 0, 0)
|
||||
}
|
||||
_ => IommuResponse::error(request.opcode, EINVAL as i32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for IommuScheme {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemeBlockMut for IommuScheme {
|
||||
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
|
||||
let cleaned = path.trim_matches('/');
|
||||
|
||||
let kind = if cleaned.is_empty() {
|
||||
HandleKind::Root
|
||||
} else if cleaned == "control" {
|
||||
HandleKind::Control
|
||||
} else if let Some(rest) = cleaned.strip_prefix("domain/") {
|
||||
let domain_id = Self::parse_domain_id(rest).ok_or(Error::new(ENOENT))?;
|
||||
self.ensure_domain_exists(domain_id).map_err(Error::new)?;
|
||||
HandleKind::Domain(domain_id)
|
||||
} else if let Some(rest) = cleaned.strip_prefix("device/") {
|
||||
let bdf = parse_bdf(rest).ok_or(Error::new(ENOENT))?;
|
||||
HandleKind::Device(bdf)
|
||||
} else {
|
||||
return Err(Error::new(ENOENT));
|
||||
};
|
||||
|
||||
Ok(Some(self.insert_handle(kind)))
|
||||
}
|
||||
|
||||
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let (kind, offset, response) = {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
(handle.kind, handle.offset, handle.response.clone())
|
||||
};
|
||||
|
||||
let content = match kind {
|
||||
HandleKind::Root => self.root_listing(),
|
||||
_ => response,
|
||||
};
|
||||
|
||||
if offset >= content.len() {
|
||||
return Ok(Some(0));
|
||||
}
|
||||
|
||||
let to_copy = (content.len() - offset).min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&content[offset..offset + to_copy]);
|
||||
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
handle.offset = offset + to_copy;
|
||||
Ok(Some(to_copy))
|
||||
}
|
||||
|
||||
fn write(&mut self, id: usize, buf: &[u8]) -> Result<Option<usize>> {
|
||||
let kind = self
|
||||
.handles
|
||||
.get(&id)
|
||||
.map(|handle| handle.kind)
|
||||
.ok_or(Error::new(EBADF))?;
|
||||
if kind == HandleKind::Root {
|
||||
return Err(Error::new(EISDIR));
|
||||
}
|
||||
|
||||
let response = match IommuRequest::from_bytes(buf) {
|
||||
Some(request) => self.dispatch_request(kind, request),
|
||||
None => IommuResponse::error(0, EINVAL as i32),
|
||||
};
|
||||
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
handle.response = response.to_bytes().to_vec();
|
||||
handle.offset = 0;
|
||||
Ok(Some(buf.len()))
|
||||
}
|
||||
|
||||
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
|
||||
let (kind, current_offset, response_len) = {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
(handle.kind, handle.offset, handle.response.len())
|
||||
};
|
||||
|
||||
let content_len = match kind {
|
||||
HandleKind::Root => self.root_listing().len(),
|
||||
_ => response_len,
|
||||
};
|
||||
|
||||
let new_offset = match whence {
|
||||
SEEK_SET => pos,
|
||||
SEEK_CUR => current_offset as isize + pos,
|
||||
SEEK_END => content_len as isize + pos,
|
||||
_ => return Err(Error::new(EINVAL)),
|
||||
};
|
||||
if new_offset < 0 {
|
||||
return Err(Error::new(EINVAL));
|
||||
}
|
||||
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
handle.offset = new_offset as usize;
|
||||
Ok(Some(new_offset))
|
||||
}
|
||||
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let kind = self
|
||||
.handles
|
||||
.get(&id)
|
||||
.map(|handle| handle.kind)
|
||||
.ok_or(Error::new(EBADF))?;
|
||||
let path = match kind {
|
||||
HandleKind::Root => "iommu:".to_string(),
|
||||
HandleKind::Control => "iommu:control".to_string(),
|
||||
HandleKind::Domain(domain_id) => format!("iommu:domain/{domain_id}"),
|
||||
HandleKind::Device(bdf) => format!("iommu:device/{bdf}"),
|
||||
};
|
||||
let bytes = path.as_bytes();
|
||||
let to_copy = bytes.len().min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&bytes[..to_copy]);
|
||||
Ok(Some(to_copy))
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
|
||||
let kind = self
|
||||
.handles
|
||||
.get(&id)
|
||||
.map(|handle| handle.kind)
|
||||
.ok_or(Error::new(EBADF))?;
|
||||
match kind {
|
||||
HandleKind::Root => {
|
||||
stat.st_mode = MODE_DIR | 0o555;
|
||||
stat.st_size = self.root_listing().len() as u64;
|
||||
}
|
||||
_ => {
|
||||
let response_len = self
|
||||
.handles
|
||||
.get(&id)
|
||||
.map(|handle| handle.response.len())
|
||||
.ok_or(Error::new(EBADF))?;
|
||||
stat.st_mode = MODE_FILE | 0o666;
|
||||
stat.st_size = response_len as u64;
|
||||
}
|
||||
}
|
||||
stat.st_blksize = 4096;
|
||||
stat.st_blocks = stat.st_size.div_ceil(512);
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
|
||||
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
Ok(Some(EventFlags::empty()))
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
if self.handles.remove(&id).is_none() {
|
||||
return Err(Error::new(EBADF));
|
||||
}
|
||||
Ok(Some(0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, not(target_os = "redox")))]
|
||||
mod host_redox_stubs {
|
||||
use core::ptr;
|
||||
|
||||
use syscall::error::{EINVAL, ENOSYS};
|
||||
|
||||
fn error_result(errno: i32) -> usize {
|
||||
usize::wrapping_neg(errno as usize)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn redox_open_v1(
|
||||
_path_base: *const u8,
|
||||
_path_len: usize,
|
||||
_flags: u32,
|
||||
_mode: u16,
|
||||
) -> usize {
|
||||
error_result(ENOSYS)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn redox_openat_v1(
|
||||
_fd: usize,
|
||||
_buf: *const u8,
|
||||
_path_len: usize,
|
||||
_flags: u32,
|
||||
_fcntl_flags: u32,
|
||||
) -> usize {
|
||||
error_result(ENOSYS)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn redox_close_v1(_fd: usize) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn redox_mmap_v1(
|
||||
_addr: *mut (),
|
||||
_unaligned_len: usize,
|
||||
_prot: u32,
|
||||
_flags: u32,
|
||||
_fd: usize,
|
||||
_offset: u64,
|
||||
) -> usize {
|
||||
error_result(ENOSYS)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn redox_munmap_v1(_addr: *mut (), _unaligned_len: usize) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn redox_sys_call_v0(
|
||||
_fd: usize,
|
||||
_payload: *mut u8,
|
||||
_payload_len: usize,
|
||||
_flags: usize,
|
||||
_metadata: *const u64,
|
||||
_metadata_len: usize,
|
||||
) -> usize {
|
||||
error_result(ENOSYS)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn redox_strerror_v1(dst: *mut u8, dst_len: *mut usize, _error: u32) -> usize {
|
||||
if dst.is_null() || dst_len.is_null() {
|
||||
return error_result(EINVAL);
|
||||
}
|
||||
|
||||
let message = b"host test stub";
|
||||
unsafe {
|
||||
let writable = *dst_len;
|
||||
let count = writable.min(message.len());
|
||||
ptr::copy_nonoverlapping(message.as_ptr(), dst, count);
|
||||
*dst_len = count;
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{opcode, IommuRequest, IommuResponse, IommuScheme};
|
||||
use crate::page_table::PAGE_SIZE;
|
||||
use redox_scheme::SchemeBlockMut;
|
||||
|
||||
fn read_response(scheme: &mut IommuScheme, id: usize) -> IommuResponse {
|
||||
let mut bytes = [0u8; IommuResponse::SIZE];
|
||||
let count = scheme
|
||||
.read(id, &mut bytes)
|
||||
.unwrap_or_else(|err| panic!("read failed: {err}"))
|
||||
.unwrap_or_else(|| panic!("expected response bytes"));
|
||||
IommuResponse::from_bytes(&bytes[..count])
|
||||
.unwrap_or_else(|| panic!("invalid response bytes"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_round_trip_serialization() {
|
||||
let request = IommuRequest::new(opcode::MAP, 7, 0x1000, 0x2000, 0x3000);
|
||||
let encoded = request.to_bytes();
|
||||
let decoded = IommuRequest::from_bytes(&encoded)
|
||||
.unwrap_or_else(|| panic!("failed to deserialize request"));
|
||||
assert_eq!(decoded, request);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_lists_control_endpoint() {
|
||||
let mut scheme = IommuScheme::new();
|
||||
let root = scheme
|
||||
.open("", 0, 0, 0)
|
||||
.unwrap_or_else(|err| panic!("open root failed: {err}"))
|
||||
.unwrap_or_else(|| panic!("root open returned no handle"));
|
||||
|
||||
let mut bytes = [0u8; 128];
|
||||
let count = scheme
|
||||
.read(root, &mut bytes)
|
||||
.unwrap_or_else(|err| panic!("read root failed: {err}"))
|
||||
.unwrap_or_else(|| panic!("expected root bytes"));
|
||||
let listing = String::from_utf8_lossy(&bytes[..count]);
|
||||
assert!(listing.contains("control"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_can_create_and_query_domain() {
|
||||
let mut scheme = IommuScheme::new();
|
||||
let control = scheme
|
||||
.open("control", 0, 0, 0)
|
||||
.unwrap_or_else(|err| panic!("open control failed: {err}"))
|
||||
.unwrap_or_else(|| panic!("control open returned no handle"));
|
||||
|
||||
let request = IommuRequest::new(opcode::CREATE_DOMAIN, 7, 0, 0, 0);
|
||||
scheme
|
||||
.write(control, &request.to_bytes())
|
||||
.unwrap_or_else(|err| panic!("create domain write failed: {err}"));
|
||||
let response = read_response(&mut scheme, control);
|
||||
|
||||
assert_eq!(response.status, 0);
|
||||
assert_eq!(response.arg0, 7);
|
||||
assert_ne!(response.arg1, 0);
|
||||
|
||||
let query = IommuRequest::new(opcode::QUERY, 0, 0, 0, 0);
|
||||
scheme
|
||||
.write(control, &query.to_bytes())
|
||||
.unwrap_or_else(|err| panic!("control query failed: {err}"));
|
||||
let query_response = read_response(&mut scheme, control);
|
||||
assert_eq!(query_response.status, 0);
|
||||
assert_eq!(query_response.arg0, 0);
|
||||
assert_eq!(query_response.arg1, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_handle_can_map_pages() {
|
||||
let mut scheme = IommuScheme::new();
|
||||
let domain = scheme
|
||||
.open("domain/5", 0, 0, 0)
|
||||
.unwrap_or_else(|err| panic!("open domain failed: {err}"))
|
||||
.unwrap_or_else(|| panic!("domain open returned no handle"));
|
||||
|
||||
let map = IommuRequest::new(opcode::MAP, 0x3, 0x4000_0000, PAGE_SIZE * 2, 0);
|
||||
scheme
|
||||
.write(domain, &map.to_bytes())
|
||||
.unwrap_or_else(|err| panic!("domain map write failed: {err}"));
|
||||
let response = read_response(&mut scheme, domain);
|
||||
assert_eq!(response.status, 0);
|
||||
assert_eq!(response.arg0, 5);
|
||||
assert_ne!(response.arg1, 0);
|
||||
|
||||
let unmap = IommuRequest::new(opcode::UNMAP, 0, response.arg1, 0, 0);
|
||||
scheme
|
||||
.write(domain, &unmap.to_bytes())
|
||||
.unwrap_or_else(|err| panic!("domain unmap write failed: {err}"));
|
||||
let unmap_response = read_response(&mut scheme, domain);
|
||||
assert_eq!(unmap_response.status, 0);
|
||||
assert_eq!(unmap_response.arg2, PAGE_SIZE * 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assigning_without_detected_units_returns_error_response() {
|
||||
let mut scheme = IommuScheme::new();
|
||||
let device = scheme
|
||||
.open("device/00:14.0", 0, 0, 0)
|
||||
.unwrap_or_else(|err| panic!("open device failed: {err}"))
|
||||
.unwrap_or_else(|| panic!("device open returned no handle"));
|
||||
|
||||
let assign = IommuRequest::new(opcode::ASSIGN_DEVICE, 1, u64::MAX, 0, 0);
|
||||
scheme
|
||||
.write(device, &assign.to_bytes())
|
||||
.unwrap_or_else(|err| panic!("device assign write failed: {err}"));
|
||||
let response = read_response(&mut scheme, device);
|
||||
assert!(response.status < 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
//! IOMMU daemon — provides scheme:iommu for DMA remapping.
|
||||
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::process;
|
||||
|
||||
use iommu::amd_vi::AmdViUnit;
|
||||
#[cfg(target_os = "redox")]
|
||||
use iommu::IommuScheme;
|
||||
use log::{error, info, LevelFilter, Metadata, Record};
|
||||
#[cfg(target_os = "redox")]
|
||||
use redox_scheme::{SignalBehavior, Socket};
|
||||
|
||||
struct StderrLogger {
|
||||
level: LevelFilter,
|
||||
}
|
||||
|
||||
impl log::Log for StderrLogger {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
eprintln!("[{}] {}", record.level(), record.args());
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
fn init_logging(level: LevelFilter) {
|
||||
if log::set_boxed_logger(Box::new(StderrLogger { level })).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
log::set_max_level(level);
|
||||
}
|
||||
|
||||
fn detect_units_from_env() -> Result<Vec<AmdViUnit>, String> {
|
||||
let Some(path) = env::var_os("IOMMU_IVRS_PATH") else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let bytes = fs::read(&path).map_err(|err| {
|
||||
format!(
|
||||
"failed to read IVRS table from {}: {err}",
|
||||
path.to_string_lossy()
|
||||
)
|
||||
})?;
|
||||
let units = AmdViUnit::detect(&bytes).map_err(|err| format!("failed to parse IVRS: {err}"))?;
|
||||
Ok(units)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run() -> Result<(), String> {
|
||||
let mut units = detect_units_from_env()?;
|
||||
info!("iommu: detected {} AMD-Vi unit(s)", units.len());
|
||||
for (index, unit) in units.iter_mut().enumerate() {
|
||||
match unit.init() {
|
||||
Ok(()) => info!(
|
||||
"iommu: initialized unit {} at MMIO {:#x}",
|
||||
index,
|
||||
unit.info().mmio_base
|
||||
),
|
||||
Err(err) => error!(
|
||||
"iommu: failed to initialize unit {} at MMIO {:#x}: {}",
|
||||
index,
|
||||
unit.info().mmio_base,
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let socket =
|
||||
Socket::create("iommu").map_err(|e| format!("failed to register iommu scheme: {e}"))?;
|
||||
info!("iommu: registered scheme:iommu");
|
||||
|
||||
let mut scheme = IommuScheme::with_units(units);
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(request)) => request,
|
||||
Ok(None) => {
|
||||
info!("iommu: scheme unmounted, exiting");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("iommu: failed to read scheme request: {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let response = match request.handle_scheme_block_mut(&mut scheme) {
|
||||
Ok(response) => response,
|
||||
Err(_request) => {
|
||||
error!("iommu: failed to handle request");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
|
||||
error!("iommu: failed to write response: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn run() -> Result<(), String> {
|
||||
let units = detect_units_from_env()?;
|
||||
info!(
|
||||
"iommu: host build stub active; parsed {} AMD-Vi unit(s) from IOMMU_IVRS_PATH",
|
||||
units.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let log_level = match env::var("IOMMU_LOG").as_deref() {
|
||||
Ok("debug") => LevelFilter::Debug,
|
||||
Ok("trace") => LevelFilter::Trace,
|
||||
Ok("warn") => LevelFilter::Warn,
|
||||
Ok("error") => LevelFilter::Error,
|
||||
_ => LevelFilter::Info,
|
||||
};
|
||||
|
||||
init_logging(log_level);
|
||||
|
||||
if let Err(e) = run() {
|
||||
error!("iommu: fatal error: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
use core::mem::{offset_of, size_of};
|
||||
use core::ptr::{addr_of, addr_of_mut, read_volatile, write_volatile};
|
||||
|
||||
pub const AMD_VI_MMIO_BYTES: usize = 0x2038;
|
||||
|
||||
pub mod offsets {
|
||||
pub const DEV_TABLE_BAR: usize = 0x0000;
|
||||
pub const CMD_BUF_BAR: usize = 0x0008;
|
||||
pub const EVT_LOG_BAR: usize = 0x0010;
|
||||
pub const CONTROL: usize = 0x0018;
|
||||
pub const EXCLUSION_BASE: usize = 0x0020;
|
||||
pub const EXCLUSION_LIMIT: usize = 0x0028;
|
||||
pub const EXTENDED_FEATURE: usize = 0x0030;
|
||||
pub const PPR_LOG_BAR: usize = 0x0038;
|
||||
pub const CMD_BUF_HEAD: usize = 0x2000;
|
||||
pub const CMD_BUF_TAIL: usize = 0x2008;
|
||||
pub const EVT_LOG_HEAD: usize = 0x2010;
|
||||
pub const EVT_LOG_TAIL: usize = 0x2018;
|
||||
pub const STATUS: usize = 0x2020;
|
||||
pub const PPR_LOG_HEAD: usize = 0x2028;
|
||||
pub const PPR_LOG_TAIL: usize = 0x2030;
|
||||
}
|
||||
|
||||
pub mod control {
|
||||
pub const IOMMU_ENABLE: u32 = 1 << 0;
|
||||
pub const HT_TUN_EN: u32 = 1 << 1;
|
||||
pub const EVENT_LOG_EN: u32 = 1 << 2;
|
||||
pub const EVENT_INT_EN: u32 = 1 << 3;
|
||||
pub const COM_WAIT_INT_EN: u32 = 1 << 4;
|
||||
pub const CMD_BUF_EN: u32 = 1 << 5;
|
||||
pub const PPR_LOG_EN: u32 = 1 << 6;
|
||||
pub const PPR_INT_EN: u32 = 1 << 7;
|
||||
pub const PPR_EN: u32 = 1 << 8;
|
||||
pub const GT_EN: u32 = 1 << 9;
|
||||
pub const GA_EN: u32 = 1 << 10;
|
||||
pub const CRW: u32 = 1 << 12;
|
||||
pub const SMIF_EN: u32 = 1 << 13;
|
||||
pub const SLFW_EN: u32 = 1 << 14;
|
||||
pub const SMIF_LOG_EN: u32 = 1 << 15;
|
||||
pub const GAM_EN_0: u32 = 1 << 16;
|
||||
pub const GAM_EN_1: u32 = 1 << 17;
|
||||
pub const GAM_EN_2: u32 = 1 << 18;
|
||||
pub const XT_EN: u32 = 1 << 22;
|
||||
pub const NX_EN: u32 = 1 << 23;
|
||||
pub const IRQ_TABLE_LEN_EN: u32 = 1 << 24;
|
||||
}
|
||||
|
||||
pub mod status {
|
||||
pub const IOMMU_RUNNING: u32 = 1 << 0;
|
||||
pub const EVENT_OVERFLOW: u32 = 1 << 1;
|
||||
pub const EVENT_LOG_INT: u32 = 1 << 2;
|
||||
pub const COM_WAIT_INT: u32 = 1 << 3;
|
||||
pub const PPR_OVERFLOW: u32 = 1 << 4;
|
||||
pub const PPR_INT: u32 = 1 << 5;
|
||||
}
|
||||
|
||||
pub mod ext_feature {
|
||||
pub const PREF_SUP: u64 = 1 << 0;
|
||||
pub const PPR_SUP: u64 = 1 << 1;
|
||||
pub const XT_SUP: u64 = 1 << 2;
|
||||
pub const NX_SUP: u64 = 1 << 3;
|
||||
pub const GT_SUP: u64 = 1 << 4;
|
||||
pub const IA_SUP: u64 = 1 << 6;
|
||||
pub const GA_SUP: u64 = 1 << 7;
|
||||
pub const HE_SUP: u64 = 1 << 8;
|
||||
pub const PC_SUP: u64 = 1 << 9;
|
||||
pub const GI_SUP: u64 = 1 << 57;
|
||||
pub const HA_SUP: u64 = 1 << 58;
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct AmdViMmio {
|
||||
pub dev_table_bar: u64,
|
||||
pub cmd_buf_bar: u64,
|
||||
pub evt_log_bar: u64,
|
||||
pub control: u32,
|
||||
_reserved0: u32,
|
||||
pub exclusion_base: u64,
|
||||
pub exclusion_limit: u64,
|
||||
pub extended_feature: u64,
|
||||
pub ppr_log_bar: u64,
|
||||
_reserved1: [u8; 0x2000 - 0x40],
|
||||
pub cmd_buf_head: u64,
|
||||
pub cmd_buf_tail: u64,
|
||||
pub evt_log_head: u64,
|
||||
pub evt_log_tail: u64,
|
||||
pub status: u32,
|
||||
_reserved2: u32,
|
||||
pub ppr_log_head: u64,
|
||||
pub ppr_log_tail: u64,
|
||||
}
|
||||
|
||||
const _: () = assert!(size_of::<AmdViMmio>() == AMD_VI_MMIO_BYTES);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, dev_table_bar) == offsets::DEV_TABLE_BAR);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, cmd_buf_bar) == offsets::CMD_BUF_BAR);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, evt_log_bar) == offsets::EVT_LOG_BAR);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, control) == offsets::CONTROL);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, extended_feature) == offsets::EXTENDED_FEATURE);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, cmd_buf_head) == offsets::CMD_BUF_HEAD);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, cmd_buf_tail) == offsets::CMD_BUF_TAIL);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, evt_log_head) == offsets::EVT_LOG_HEAD);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, evt_log_tail) == offsets::EVT_LOG_TAIL);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, status) == offsets::STATUS);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, ppr_log_head) == offsets::PPR_LOG_HEAD);
|
||||
const _: () = assert!(offset_of!(AmdViMmio, ppr_log_tail) == offsets::PPR_LOG_TAIL);
|
||||
|
||||
impl AmdViMmio {
|
||||
pub unsafe fn read_dev_table_bar(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).dev_table_bar))
|
||||
}
|
||||
|
||||
pub unsafe fn write_dev_table_bar(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).dev_table_bar), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_cmd_buf_bar(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).cmd_buf_bar))
|
||||
}
|
||||
|
||||
pub unsafe fn write_cmd_buf_bar(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).cmd_buf_bar), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_evt_log_bar(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).evt_log_bar))
|
||||
}
|
||||
|
||||
pub unsafe fn write_evt_log_bar(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).evt_log_bar), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_control(base: *mut Self) -> u32 {
|
||||
read_volatile(addr_of!((*base).control))
|
||||
}
|
||||
|
||||
pub unsafe fn write_control(base: *mut Self, value: u32) {
|
||||
write_volatile(addr_of_mut!((*base).control), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_exclusion_base(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).exclusion_base))
|
||||
}
|
||||
|
||||
pub unsafe fn write_exclusion_base(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).exclusion_base), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_exclusion_limit(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).exclusion_limit))
|
||||
}
|
||||
|
||||
pub unsafe fn write_exclusion_limit(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).exclusion_limit), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_extended_feature(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).extended_feature))
|
||||
}
|
||||
|
||||
pub unsafe fn read_ppr_log_bar(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).ppr_log_bar))
|
||||
}
|
||||
|
||||
pub unsafe fn write_ppr_log_bar(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).ppr_log_bar), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_cmd_buf_head(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).cmd_buf_head))
|
||||
}
|
||||
|
||||
pub unsafe fn write_cmd_buf_head(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).cmd_buf_head), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_cmd_buf_tail(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).cmd_buf_tail))
|
||||
}
|
||||
|
||||
pub unsafe fn write_cmd_buf_tail(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).cmd_buf_tail), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_evt_log_head(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).evt_log_head))
|
||||
}
|
||||
|
||||
pub unsafe fn write_evt_log_head(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).evt_log_head), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_evt_log_tail(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).evt_log_tail))
|
||||
}
|
||||
|
||||
pub unsafe fn read_status(base: *mut Self) -> u32 {
|
||||
read_volatile(addr_of!((*base).status))
|
||||
}
|
||||
|
||||
pub unsafe fn read_ppr_log_head(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).ppr_log_head))
|
||||
}
|
||||
|
||||
pub unsafe fn write_ppr_log_head(base: *mut Self, value: u64) {
|
||||
write_volatile(addr_of_mut!((*base).ppr_log_head), value);
|
||||
}
|
||||
|
||||
pub unsafe fn read_ppr_log_tail(base: *mut Self) -> u64 {
|
||||
read_volatile(addr_of!((*base).ppr_log_tail))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use core::mem::MaybeUninit;
|
||||
|
||||
use super::{offsets, AmdViMmio};
|
||||
|
||||
#[test]
|
||||
fn register_accessors_use_expected_offsets() {
|
||||
let mut mmio = MaybeUninit::<AmdViMmio>::zeroed();
|
||||
let base = mmio.as_mut_ptr();
|
||||
|
||||
unsafe {
|
||||
AmdViMmio::write_control(base, 0xdead_beef);
|
||||
AmdViMmio::write_cmd_buf_head(base, 0x1122_3344_5566_7788);
|
||||
AmdViMmio::write_dev_table_bar(base, 0x2000);
|
||||
|
||||
assert_eq!(AmdViMmio::read_control(base), 0xdead_beef);
|
||||
assert_eq!(AmdViMmio::read_cmd_buf_head(base), 0x1122_3344_5566_7788);
|
||||
assert_eq!(AmdViMmio::read_dev_table_bar(base), 0x2000);
|
||||
|
||||
let byte_base = base.cast::<u8>();
|
||||
let control_ptr = byte_base.add(offsets::CONTROL).cast::<u32>();
|
||||
let head_ptr = byte_base.add(offsets::CMD_BUF_HEAD).cast::<u64>();
|
||||
|
||||
assert_eq!(core::ptr::read_volatile(control_ptr), 0xdead_beef);
|
||||
assert_eq!(core::ptr::read_volatile(head_ptr), 0x1122_3344_5566_7788);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
use core::alloc::Layout;
|
||||
use core::mem::size_of;
|
||||
use core::ptr::NonNull;
|
||||
use core::slice;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use redox_driver_sys::dma::DmaBuffer;
|
||||
|
||||
pub const PAGE_SIZE: u64 = 4096;
|
||||
pub const PTES_PER_PAGE: usize = 512;
|
||||
pub const DEFAULT_IOMMU_LEVELS: u8 = 4;
|
||||
pub const DEFAULT_IOVA_BASE: u64 = 0x1_0000_0000;
|
||||
pub const DEFAULT_IOVA_LIMIT: u64 = 0x0000_FFFF_FFFF_F000;
|
||||
|
||||
const PTE_PRESENT: u64 = 1 << 0;
|
||||
const PTE_USER: u64 = 1 << 1;
|
||||
const PTE_WRITE: u64 = 1 << 2;
|
||||
const PTE_READ: u64 = 1 << 3;
|
||||
const PTE_NEXT_LEVEL_SHIFT: u64 = 9;
|
||||
const PTE_NEXT_LEVEL_MASK: u64 = 0x7 << PTE_NEXT_LEVEL_SHIFT;
|
||||
const PTE_OUTPUT_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
|
||||
const PTE_FORCE_COHERENT: u64 = 1 << 59;
|
||||
const PTE_IRQ_REMAP: u64 = 1 << 61;
|
||||
const PTE_IRQ_WRITE: u64 = 1 << 62;
|
||||
const PTE_NO_EXECUTE: u64 = 1 << 63;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct AmdPte(pub u64);
|
||||
|
||||
impl AmdPte {
|
||||
pub const fn new() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
pub fn present(&self) -> bool {
|
||||
self.0 & PTE_PRESENT != 0
|
||||
}
|
||||
|
||||
pub fn set_present(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_PRESENT;
|
||||
} else {
|
||||
self.0 &= !PTE_PRESENT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user(&self) -> bool {
|
||||
self.0 & PTE_USER != 0
|
||||
}
|
||||
|
||||
pub fn set_user(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_USER;
|
||||
} else {
|
||||
self.0 &= !PTE_USER;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writable(&self) -> bool {
|
||||
self.0 & PTE_WRITE != 0
|
||||
}
|
||||
|
||||
pub fn set_writable(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_WRITE;
|
||||
} else {
|
||||
self.0 &= !PTE_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn readable(&self) -> bool {
|
||||
self.0 & PTE_READ != 0
|
||||
}
|
||||
|
||||
pub fn set_readable(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_READ;
|
||||
} else {
|
||||
self.0 &= !PTE_READ;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_level(&self) -> u8 {
|
||||
((self.0 & PTE_NEXT_LEVEL_MASK) >> PTE_NEXT_LEVEL_SHIFT) as u8
|
||||
}
|
||||
|
||||
pub fn set_next_level(&mut self, level: u8) {
|
||||
self.0 =
|
||||
(self.0 & !PTE_NEXT_LEVEL_MASK) | ((u64::from(level) & 0x7) << PTE_NEXT_LEVEL_SHIFT);
|
||||
}
|
||||
|
||||
pub fn output_addr(&self) -> u64 {
|
||||
self.0 & PTE_OUTPUT_ADDR_MASK
|
||||
}
|
||||
|
||||
pub fn set_output_addr(&mut self, addr: u64) {
|
||||
self.0 = (self.0 & !PTE_OUTPUT_ADDR_MASK) | (addr & PTE_OUTPUT_ADDR_MASK);
|
||||
}
|
||||
|
||||
pub fn force_coherent(&self) -> bool {
|
||||
self.0 & PTE_FORCE_COHERENT != 0
|
||||
}
|
||||
|
||||
pub fn set_force_coherent(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_FORCE_COHERENT;
|
||||
} else {
|
||||
self.0 &= !PTE_FORCE_COHERENT;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_remap(&self) -> bool {
|
||||
self.0 & PTE_IRQ_REMAP != 0
|
||||
}
|
||||
|
||||
pub fn set_interrupt_remap(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_IRQ_REMAP;
|
||||
} else {
|
||||
self.0 &= !PTE_IRQ_REMAP;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interrupt_write(&self) -> bool {
|
||||
self.0 & PTE_IRQ_WRITE != 0
|
||||
}
|
||||
|
||||
pub fn set_interrupt_write(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_IRQ_WRITE;
|
||||
} else {
|
||||
self.0 &= !PTE_IRQ_WRITE;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_execute(&self) -> bool {
|
||||
self.0 & PTE_NO_EXECUTE != 0
|
||||
}
|
||||
|
||||
pub fn set_no_execute(&mut self, value: bool) {
|
||||
if value {
|
||||
self.0 |= PTE_NO_EXECUTE;
|
||||
} else {
|
||||
self.0 &= !PTE_NO_EXECUTE;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leaf(addr: u64, flags: MappingFlags) -> Self {
|
||||
let mut entry = Self::new();
|
||||
entry.set_present(true);
|
||||
entry.set_output_addr(addr);
|
||||
entry.set_readable(flags.readable);
|
||||
entry.set_writable(flags.writable);
|
||||
entry.set_user(flags.user);
|
||||
entry.set_force_coherent(flags.force_coherent);
|
||||
entry.set_no_execute(!flags.executable);
|
||||
entry
|
||||
}
|
||||
|
||||
pub fn pointer(addr: u64, next_level: u8) -> Self {
|
||||
let mut entry = Self::new();
|
||||
entry.set_present(true);
|
||||
entry.set_next_level(next_level);
|
||||
entry.set_output_addr(addr);
|
||||
entry
|
||||
}
|
||||
}
|
||||
|
||||
const _: () = assert!(size_of::<AmdPte>() == 8);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct MappingFlags {
|
||||
pub readable: bool,
|
||||
pub writable: bool,
|
||||
pub executable: bool,
|
||||
pub force_coherent: bool,
|
||||
pub user: bool,
|
||||
}
|
||||
|
||||
impl Default for MappingFlags {
|
||||
fn default() -> Self {
|
||||
Self::read_write()
|
||||
}
|
||||
}
|
||||
|
||||
impl MappingFlags {
|
||||
pub const fn read_write() -> Self {
|
||||
Self {
|
||||
readable: true,
|
||||
writable: true,
|
||||
executable: false,
|
||||
force_coherent: false,
|
||||
user: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PageStorage {
|
||||
Dma(DmaBuffer),
|
||||
Host {
|
||||
ptr: NonNull<u8>,
|
||||
layout: Layout,
|
||||
len: usize,
|
||||
},
|
||||
}
|
||||
|
||||
struct PageBuffer {
|
||||
storage: PageStorage,
|
||||
phys_addr: usize,
|
||||
}
|
||||
|
||||
impl PageBuffer {
|
||||
fn allocate(len: usize, align: usize) -> Result<Self, &'static str> {
|
||||
match DmaBuffer::allocate(len, align) {
|
||||
Ok(buffer) => Ok(Self {
|
||||
phys_addr: buffer.physical_address(),
|
||||
storage: PageStorage::Dma(buffer),
|
||||
}),
|
||||
Err(_) => {
|
||||
let layout = Layout::from_size_align(len, align)
|
||||
.map_err(|_| "invalid page-table allocation layout")?;
|
||||
let ptr = unsafe { std::alloc::alloc_zeroed(layout) };
|
||||
let ptr = NonNull::new(ptr).ok_or("failed to allocate host page-table memory")?;
|
||||
Ok(Self {
|
||||
phys_addr: ptr.as_ptr() as usize,
|
||||
storage: PageStorage::Host { ptr, layout, len },
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn as_ptr(&self) -> *const u8 {
|
||||
match &self.storage {
|
||||
PageStorage::Dma(buffer) => buffer.as_ptr(),
|
||||
PageStorage::Host { ptr, .. } => ptr.as_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_mut_ptr(&mut self) -> *mut u8 {
|
||||
match &mut self.storage {
|
||||
PageStorage::Dma(buffer) => buffer.as_mut_ptr(),
|
||||
PageStorage::Host { ptr, .. } => ptr.as_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
fn physical_address(&self) -> usize {
|
||||
self.phys_addr
|
||||
}
|
||||
|
||||
fn len(&self) -> usize {
|
||||
match &self.storage {
|
||||
PageStorage::Dma(buffer) => buffer.len(),
|
||||
PageStorage::Host { len, .. } => *len,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PageBuffer {
|
||||
fn drop(&mut self) {
|
||||
if let PageStorage::Host { ptr, layout, .. } = &self.storage {
|
||||
unsafe {
|
||||
std::alloc::dealloc(ptr.as_ptr(), *layout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for PageBuffer {}
|
||||
unsafe impl Sync for PageBuffer {}
|
||||
|
||||
struct PageTablePage {
|
||||
buffer: PageBuffer,
|
||||
}
|
||||
|
||||
impl PageTablePage {
|
||||
fn new() -> Result<Self, &'static str> {
|
||||
let buffer = PageBuffer::allocate(PAGE_SIZE as usize, PAGE_SIZE as usize)?;
|
||||
if buffer.len() < PAGE_SIZE as usize {
|
||||
return Err("page-table allocation smaller than one page");
|
||||
}
|
||||
Ok(Self { buffer })
|
||||
}
|
||||
|
||||
fn physical_address(&self) -> u64 {
|
||||
self.buffer.physical_address() as u64
|
||||
}
|
||||
|
||||
fn entry(&self, index: usize) -> AmdPte {
|
||||
self.entries()[index]
|
||||
}
|
||||
|
||||
fn set_entry(&mut self, index: usize, entry: AmdPte) {
|
||||
self.entries_mut()[index] = entry;
|
||||
}
|
||||
|
||||
fn entries(&self) -> &[AmdPte] {
|
||||
unsafe { slice::from_raw_parts(self.buffer.as_ptr().cast::<AmdPte>(), PTES_PER_PAGE) }
|
||||
}
|
||||
|
||||
fn entries_mut(&mut self) -> &mut [AmdPte] {
|
||||
unsafe {
|
||||
slice::from_raw_parts_mut(self.buffer.as_mut_ptr().cast::<AmdPte>(), PTES_PER_PAGE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PageTableNode {
|
||||
page: PageTablePage,
|
||||
children: BTreeMap<usize, Box<PageTableNode>>,
|
||||
}
|
||||
|
||||
impl PageTableNode {
|
||||
fn new() -> Result<Self, &'static str> {
|
||||
Ok(Self {
|
||||
page: PageTablePage::new()?,
|
||||
children: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn phys_addr(&self) -> u64 {
|
||||
self.page.physical_address()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PageTable {
|
||||
levels: u8,
|
||||
root: Box<PageTableNode>,
|
||||
}
|
||||
|
||||
impl PageTable {
|
||||
pub fn new(levels: u8) -> Result<Self, &'static str> {
|
||||
if !(1..=6).contains(&levels) {
|
||||
return Err("AMD-Vi page tables support between 1 and 6 levels");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
levels,
|
||||
root: Box::new(PageTableNode::new()?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn levels(&self) -> u8 {
|
||||
self.levels
|
||||
}
|
||||
|
||||
pub fn root_address(&self) -> u64 {
|
||||
self.root.phys_addr()
|
||||
}
|
||||
|
||||
pub fn map_page(
|
||||
&mut self,
|
||||
iova: u64,
|
||||
phys: u64,
|
||||
flags: MappingFlags,
|
||||
) -> Result<(), &'static str> {
|
||||
if iova & (PAGE_SIZE - 1) != 0 || phys & (PAGE_SIZE - 1) != 0 {
|
||||
return Err("IOMMU mappings must be 4KiB-aligned");
|
||||
}
|
||||
|
||||
let mut node = self.root.as_mut();
|
||||
for level in (2..=self.levels).rev() {
|
||||
let index = page_table_index(level, iova);
|
||||
if !node.children.contains_key(&index) {
|
||||
let child = Box::new(PageTableNode::new()?);
|
||||
let child_phys = child.phys_addr();
|
||||
node.page
|
||||
.set_entry(index, AmdPte::pointer(child_phys, level - 1));
|
||||
node.children.insert(index, child);
|
||||
}
|
||||
let child = node
|
||||
.children
|
||||
.get_mut(&index)
|
||||
.ok_or("failed to descend page table")?;
|
||||
node = child.as_mut();
|
||||
}
|
||||
|
||||
let leaf_index = page_table_index(1, iova);
|
||||
node.page.set_entry(leaf_index, AmdPte::leaf(phys, flags));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unmap_page(&mut self, iova: u64) -> bool {
|
||||
Self::unmap_in_node(self.root.as_mut(), self.levels, iova)
|
||||
}
|
||||
|
||||
pub fn translate(&self, iova: u64) -> Option<u64> {
|
||||
let page_base = iova & !(PAGE_SIZE - 1);
|
||||
let page_offset = iova & (PAGE_SIZE - 1);
|
||||
let mut node = self.root.as_ref();
|
||||
|
||||
for level in (2..=self.levels).rev() {
|
||||
let index = page_table_index(level, page_base);
|
||||
let entry = node.page.entry(index);
|
||||
if !entry.present() {
|
||||
return None;
|
||||
}
|
||||
node = node.children.get(&index)?.as_ref();
|
||||
}
|
||||
|
||||
let leaf = node.page.entry(page_table_index(1, page_base));
|
||||
if !leaf.present() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(leaf.output_addr() + page_offset)
|
||||
}
|
||||
|
||||
fn unmap_in_node(node: &mut PageTableNode, level: u8, iova: u64) -> bool {
|
||||
if level == 1 {
|
||||
let index = page_table_index(1, iova);
|
||||
let present = node.page.entry(index).present();
|
||||
if present {
|
||||
node.page.set_entry(index, AmdPte::new());
|
||||
}
|
||||
return present;
|
||||
}
|
||||
|
||||
let index = page_table_index(level, iova);
|
||||
let Some(child) = node.children.get_mut(&index) else {
|
||||
return false;
|
||||
};
|
||||
Self::unmap_in_node(child.as_mut(), level - 1, iova)
|
||||
}
|
||||
}
|
||||
|
||||
fn page_table_index(level: u8, address: u64) -> usize {
|
||||
((address >> (12 + ((u64::from(level) - 1) * 9))) & 0x1FF) as usize
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DomainMapping {
|
||||
pub iova: u64,
|
||||
pub phys: u64,
|
||||
pub size: u64,
|
||||
pub flags: MappingFlags,
|
||||
}
|
||||
|
||||
pub struct IovaAllocator {
|
||||
base: u64,
|
||||
limit: u64,
|
||||
allocations: BTreeMap<u64, u64>,
|
||||
}
|
||||
|
||||
impl IovaAllocator {
|
||||
pub fn new(base: u64, limit: u64) -> Self {
|
||||
Self {
|
||||
base,
|
||||
limit,
|
||||
allocations: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocate(&mut self, size: u64, align: u64) -> Option<u64> {
|
||||
let size = align_up(size.max(PAGE_SIZE), PAGE_SIZE)?;
|
||||
let align = align.max(PAGE_SIZE).next_power_of_two();
|
||||
let mut cursor = align_up(self.base, align)?;
|
||||
|
||||
for (&start, &length) in &self.allocations {
|
||||
if cursor.checked_add(size)? <= start {
|
||||
self.allocations.insert(cursor, size);
|
||||
return Some(cursor);
|
||||
}
|
||||
cursor = align_up(start.checked_add(length)?, align)?;
|
||||
}
|
||||
|
||||
if cursor.checked_add(size)? > self.limit {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.allocations.insert(cursor, size);
|
||||
Some(cursor)
|
||||
}
|
||||
|
||||
pub fn reserve(&mut self, start: u64, size: u64) -> bool {
|
||||
let Some(end) = start.checked_add(size) else {
|
||||
return false;
|
||||
};
|
||||
if start < self.base || end > self.limit {
|
||||
return false;
|
||||
}
|
||||
|
||||
let prev = self.allocations.range(..=start).next_back();
|
||||
if let Some((&prev_start, &prev_len)) = prev {
|
||||
let Some(prev_end) = prev_start.checked_add(prev_len) else {
|
||||
return false;
|
||||
};
|
||||
if prev_end > start {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let next = self.allocations.range(start..).next();
|
||||
if let Some((&next_start, _)) = next {
|
||||
if next_start < end {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
self.allocations.insert(start, size);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn free(&mut self, start: u64) -> bool {
|
||||
self.allocations.remove(&start).is_some()
|
||||
}
|
||||
|
||||
pub fn allocated_size(&self, start: u64) -> Option<u64> {
|
||||
self.allocations.get(&start).copied()
|
||||
}
|
||||
|
||||
pub fn allocation_count(&self) -> usize {
|
||||
self.allocations.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DomainPageTables {
|
||||
domain_id: u16,
|
||||
page_table: PageTable,
|
||||
allocator: IovaAllocator,
|
||||
mappings: BTreeMap<u64, DomainMapping>,
|
||||
}
|
||||
|
||||
impl DomainPageTables {
|
||||
pub fn new(domain_id: u16) -> Result<Self, &'static str> {
|
||||
Self::with_range(domain_id, DEFAULT_IOVA_BASE, DEFAULT_IOVA_LIMIT)
|
||||
}
|
||||
|
||||
pub fn with_range(domain_id: u16, base: u64, limit: u64) -> Result<Self, &'static str> {
|
||||
Ok(Self {
|
||||
domain_id,
|
||||
page_table: PageTable::new(DEFAULT_IOMMU_LEVELS)?,
|
||||
allocator: IovaAllocator::new(base, limit),
|
||||
mappings: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn domain_id(&self) -> u16 {
|
||||
self.domain_id
|
||||
}
|
||||
|
||||
pub fn root_address(&self) -> u64 {
|
||||
self.page_table.root_address()
|
||||
}
|
||||
|
||||
pub fn levels(&self) -> u8 {
|
||||
self.page_table.levels()
|
||||
}
|
||||
|
||||
pub fn map_range(
|
||||
&mut self,
|
||||
phys: u64,
|
||||
size: u64,
|
||||
flags: MappingFlags,
|
||||
preferred_iova: Option<u64>,
|
||||
) -> Result<u64, &'static str> {
|
||||
if size == 0 {
|
||||
return Err("IOMMU map size must be non-zero");
|
||||
}
|
||||
if phys & (PAGE_SIZE - 1) != 0 {
|
||||
return Err("IOMMU physical mappings must be page-aligned");
|
||||
}
|
||||
|
||||
let size = align_up(size, PAGE_SIZE).ok_or("IOMMU map size overflow")?;
|
||||
let iova = if let Some(requested) = preferred_iova {
|
||||
if requested & (PAGE_SIZE - 1) != 0 {
|
||||
return Err("IOMMU IOVA mappings must be page-aligned");
|
||||
}
|
||||
if !self.allocator.reserve(requested, size) {
|
||||
return Err("requested IOVA range is unavailable");
|
||||
}
|
||||
requested
|
||||
} else {
|
||||
self.allocator
|
||||
.allocate(size, PAGE_SIZE)
|
||||
.ok_or("unable to allocate an IOVA range")?
|
||||
};
|
||||
|
||||
let mut mapped = 0u64;
|
||||
while mapped < size {
|
||||
if let Err(err) = self
|
||||
.page_table
|
||||
.map_page(iova + mapped, phys + mapped, flags)
|
||||
{
|
||||
let mut rollback = 0u64;
|
||||
while rollback < mapped {
|
||||
let _ = self.page_table.unmap_page(iova + rollback);
|
||||
rollback += PAGE_SIZE;
|
||||
}
|
||||
let _ = self.allocator.free(iova);
|
||||
return Err(err);
|
||||
}
|
||||
mapped += PAGE_SIZE;
|
||||
}
|
||||
|
||||
self.mappings.insert(
|
||||
iova,
|
||||
DomainMapping {
|
||||
iova,
|
||||
phys,
|
||||
size,
|
||||
flags,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(iova)
|
||||
}
|
||||
|
||||
pub fn unmap_range(&mut self, iova: u64) -> Result<u64, &'static str> {
|
||||
let mapping = self
|
||||
.mappings
|
||||
.remove(&iova)
|
||||
.ok_or("IOMMU mapping does not exist")?;
|
||||
|
||||
let mut offset = 0u64;
|
||||
while offset < mapping.size {
|
||||
let _ = self.page_table.unmap_page(mapping.iova + offset);
|
||||
offset += PAGE_SIZE;
|
||||
}
|
||||
let _ = self.allocator.free(mapping.iova);
|
||||
Ok(mapping.size)
|
||||
}
|
||||
|
||||
pub fn mapping(&self, iova: u64) -> Option<&DomainMapping> {
|
||||
self.mappings.get(&iova)
|
||||
}
|
||||
|
||||
pub fn mapping_count(&self) -> usize {
|
||||
self.mappings.len()
|
||||
}
|
||||
}
|
||||
|
||||
fn align_up(value: u64, align: u64) -> Option<u64> {
|
||||
let mask = align.checked_sub(1)?;
|
||||
value.checked_add(mask).map(|rounded| rounded & !mask)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{AmdPte, DomainPageTables, IovaAllocator, MappingFlags, PageTable, PAGE_SIZE};
|
||||
|
||||
#[test]
|
||||
fn amd_pte_leaf_sets_permissions() {
|
||||
let pte = AmdPte::leaf(0x1234_5000, MappingFlags::read_write());
|
||||
assert!(pte.present());
|
||||
assert!(pte.readable());
|
||||
assert!(pte.writable());
|
||||
assert!(pte.no_execute());
|
||||
assert_eq!(pte.output_addr(), 0x1234_5000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iova_allocator_finds_gap_and_reuses_freed_ranges() {
|
||||
let mut allocator = IovaAllocator::new(0x1000, 0x10_0000);
|
||||
let first = allocator.allocate(PAGE_SIZE, PAGE_SIZE).unwrap_or(0);
|
||||
let second = allocator.allocate(PAGE_SIZE * 2, PAGE_SIZE).unwrap_or(0);
|
||||
assert_eq!(first, 0x1000);
|
||||
assert_eq!(second, 0x2000);
|
||||
assert!(allocator.free(first));
|
||||
let reused = allocator.allocate(PAGE_SIZE, PAGE_SIZE).unwrap_or(0);
|
||||
assert_eq!(reused, first);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_table_translate_round_trips_mapping() {
|
||||
let mut table =
|
||||
PageTable::new(4).unwrap_or_else(|err| panic!("page table create failed: {err}"));
|
||||
table
|
||||
.map_page(0x4000, 0x2000_0000, MappingFlags::read_write())
|
||||
.unwrap_or_else(|err| panic!("page table map failed: {err}"));
|
||||
assert_eq!(table.translate(0x4123), Some(0x2000_0123));
|
||||
assert!(table.unmap_page(0x4000));
|
||||
assert_eq!(table.translate(0x4123), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn domain_page_tables_allocate_iova_and_unmap() {
|
||||
let mut domain = DomainPageTables::new(7)
|
||||
.unwrap_or_else(|err| panic!("domain page table create failed: {err}"));
|
||||
let iova = domain
|
||||
.map_range(0x3000_0000, PAGE_SIZE * 2, MappingFlags::read_write(), None)
|
||||
.unwrap_or_else(|err| panic!("domain mapping failed: {err}"));
|
||||
let mapping = domain
|
||||
.mapping(iova)
|
||||
.unwrap_or_else(|| panic!("mapping missing"));
|
||||
assert_eq!(mapping.size, PAGE_SIZE * 2);
|
||||
assert!(domain.unmap_range(iova).is_ok());
|
||||
assert!(domain.mapping(iova).is_none());
|
||||
}
|
||||
}
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
redbear-info
|
||||
@@ -0,0 +1,9 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
|
||||
[package.files]
|
||||
"/usr/bin/lspci" = "lspci"
|
||||
"/usr/bin/lsusb" = "lsusb"
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "redbear-hwutils"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "lspci"
|
||||
path = "src/bin/lspci.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "lsusb"
|
||||
path = "src/bin/lsusb.rs"
|
||||
|
||||
[dependencies]
|
||||
xhcid = { path = "../../../../../recipes/core/base/source/drivers/usb/xhcid" }
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::fs;
|
||||
use std::process;
|
||||
|
||||
use redbear_hwutils::{parse_args, parse_pci_location, PciLocation};
|
||||
|
||||
const USAGE: &str = "Usage: lspci\nList PCI devices exposed by /scheme/pci.";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
struct PciDeviceSummary {
|
||||
location: PciLocation,
|
||||
vendor_id: u16,
|
||||
device_id: u16,
|
||||
class_code: u8,
|
||||
subclass: u8,
|
||||
prog_if: u8,
|
||||
revision: u8,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match run() {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.is_empty() => {}
|
||||
Err(err) => {
|
||||
eprintln!("lspci: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args("lspci", USAGE, std::env::args())?;
|
||||
|
||||
let mut devices = collect_devices()?;
|
||||
devices.sort();
|
||||
|
||||
for device in devices {
|
||||
println!(
|
||||
"{} class {:02x}:{:02x}.{:02x} vendor {:04x} device {:04x} rev {:02x}",
|
||||
device.location,
|
||||
device.class_code,
|
||||
device.subclass,
|
||||
device.prog_if,
|
||||
device.vendor_id,
|
||||
device.device_id,
|
||||
device.revision,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_devices() -> Result<Vec<PciDeviceSummary>, String> {
|
||||
let entries =
|
||||
fs::read_dir("/scheme/pci").map_err(|err| format!("failed to read /scheme/pci: {err}"))?;
|
||||
|
||||
let mut devices = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let file_name = entry.file_name();
|
||||
let Some(file_name) = file_name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
let Some(location) = parse_pci_location(file_name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let config_path = format!("{}/config", location.scheme_path());
|
||||
let config = match fs::read(&config_path) {
|
||||
Ok(config) => config,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if config.len() < 16 {
|
||||
continue;
|
||||
}
|
||||
|
||||
devices.push(PciDeviceSummary {
|
||||
location,
|
||||
vendor_id: u16::from_le_bytes([config[0x00], config[0x01]]),
|
||||
device_id: u16::from_le_bytes([config[0x02], config[0x03]]),
|
||||
revision: config[0x08],
|
||||
prog_if: config[0x09],
|
||||
subclass: config[0x0A],
|
||||
class_code: config[0x0B],
|
||||
});
|
||||
}
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::fs;
|
||||
use std::process;
|
||||
|
||||
use redbear_hwutils::{describe_usb_device, parse_args};
|
||||
use xhcid_interface::{PortId, PortState, XhciClientHandle};
|
||||
|
||||
const USAGE: &str = "Usage: lsusb\nList USB devices exposed by native usb.* schemes.";
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct UsbDeviceSummary {
|
||||
controller: String,
|
||||
port: PortId,
|
||||
vendor_id: u16,
|
||||
product_id: u16,
|
||||
class: u8,
|
||||
subclass: u8,
|
||||
protocol: u8,
|
||||
usb_major: u8,
|
||||
usb_minor: u8,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct UsbPortStateSummary {
|
||||
controller: String,
|
||||
port: PortId,
|
||||
state: PortState,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match run() {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.is_empty() => {}
|
||||
Err(err) => {
|
||||
eprintln!("lsusb: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args("lsusb", USAGE, std::env::args())?;
|
||||
|
||||
let (mut devices, mut fallback_ports) = collect_usb_state()?;
|
||||
devices.sort_by(|left, right| {
|
||||
left.controller
|
||||
.cmp(&right.controller)
|
||||
.then(left.port.cmp(&right.port))
|
||||
});
|
||||
fallback_ports.sort_by(|left, right| {
|
||||
left.controller
|
||||
.cmp(&right.controller)
|
||||
.then(left.port.cmp(&right.port))
|
||||
});
|
||||
|
||||
for device in devices {
|
||||
println!(
|
||||
"{} {} ID {:04x}:{:04x} class {:02x}/{:02x}/{:02x} usb {}.{:02x} {}",
|
||||
device.controller,
|
||||
device.port,
|
||||
device.vendor_id,
|
||||
device.product_id,
|
||||
device.class,
|
||||
device.subclass,
|
||||
device.protocol,
|
||||
device.usb_major,
|
||||
device.usb_minor,
|
||||
device.description,
|
||||
);
|
||||
}
|
||||
|
||||
for fallback in fallback_ports {
|
||||
println!(
|
||||
"{} {} state {}",
|
||||
fallback.controller,
|
||||
fallback.port,
|
||||
fallback.state.as_str(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_usb_state() -> Result<(Vec<UsbDeviceSummary>, Vec<UsbPortStateSummary>), String> {
|
||||
let entries =
|
||||
fs::read_dir("/scheme").map_err(|err| format!("failed to read /scheme: {err}"))?;
|
||||
|
||||
let mut devices = Vec::new();
|
||||
let mut fallback_ports = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let file_name = entry.file_name();
|
||||
let Some(controller) = file_name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
if !controller.starts_with("usb.") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let controller_dir = format!("/scheme/{controller}");
|
||||
let ports = match fs::read_dir(&controller_dir) {
|
||||
Ok(ports) => ports,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
for port_entry in ports {
|
||||
let port_entry = match port_entry {
|
||||
Ok(port_entry) => port_entry,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let port_name = port_entry.file_name();
|
||||
let Some(port_name) = port_name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
let Some(raw_port_id) = port_name.strip_prefix("port") else {
|
||||
continue;
|
||||
};
|
||||
let Ok(port) = raw_port_id.parse::<PortId>() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let handle = match XhciClientHandle::new(controller.to_string(), port) {
|
||||
Ok(handle) => handle,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let state = handle.port_state().ok();
|
||||
|
||||
match handle.get_standard_descs() {
|
||||
Ok(descriptors) => {
|
||||
devices.push(UsbDeviceSummary {
|
||||
controller: controller.to_string(),
|
||||
port,
|
||||
vendor_id: descriptors.vendor,
|
||||
product_id: descriptors.product,
|
||||
class: descriptors.class,
|
||||
subclass: descriptors.sub_class,
|
||||
protocol: descriptors.protocol,
|
||||
usb_major: descriptors.major_version(),
|
||||
usb_minor: descriptors.minor_version(),
|
||||
description: describe_usb_device(
|
||||
descriptors.manufacturer_str.as_deref(),
|
||||
descriptors.product_str.as_deref(),
|
||||
),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
if let Some(state) =
|
||||
state.filter(|state| *state != PortState::EnabledOrDisabled)
|
||||
{
|
||||
fallback_ports.push(UsbPortStateSummary {
|
||||
controller: controller.to_string(),
|
||||
port,
|
||||
state,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((devices, fallback_ports))
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub struct PciLocation {
|
||||
pub segment: u16,
|
||||
pub bus: u8,
|
||||
pub device: u8,
|
||||
pub function: u8,
|
||||
}
|
||||
|
||||
impl PciLocation {
|
||||
pub fn scheme_path(&self) -> String {
|
||||
format!(
|
||||
"/scheme/pci/{:04x}--{:02x}--{:02x}.{}",
|
||||
self.segment, self.bus, self.device, self.function
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PciLocation {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{:04x}:{:02x}:{:02x}.{}",
|
||||
self.segment, self.bus, self.device, self.function
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_pci_location(name: &str) -> Option<PciLocation> {
|
||||
let (segment, rest) = name.split_once("--")?;
|
||||
let (bus, rest) = rest.split_once("--")?;
|
||||
let (device, function) = rest.split_once('.')?;
|
||||
|
||||
Some(PciLocation {
|
||||
segment: u16::from_str_radix(segment, 16).ok()?,
|
||||
bus: u8::from_str_radix(bus, 16).ok()?,
|
||||
device: u8::from_str_radix(device, 16).ok()?,
|
||||
function: function.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_args(
|
||||
program: &str,
|
||||
usage: &str,
|
||||
args: impl IntoIterator<Item = String>,
|
||||
) -> Result<(), String> {
|
||||
let extras: Vec<String> = args.into_iter().skip(1).collect();
|
||||
|
||||
if extras.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if extras.len() == 1 && matches!(extras[0].as_str(), "-h" | "--help") {
|
||||
println!("{usage}");
|
||||
return Err(String::new());
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"{program}: unsupported arguments: {}",
|
||||
extras.join(" ")
|
||||
))
|
||||
}
|
||||
|
||||
pub fn describe_usb_device(manufacturer: Option<&str>, product: Option<&str>) -> String {
|
||||
let mut parts = Vec::new();
|
||||
|
||||
if let Some(manufacturer) = manufacturer.filter(|value| !value.is_empty()) {
|
||||
parts.push(manufacturer);
|
||||
}
|
||||
if let Some(product) = product.filter(|value| !value.is_empty()) {
|
||||
parts.push(product);
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
"USB device".to_string()
|
||||
} else {
|
||||
parts.join(" ")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,9 @@ README
|
||||
# will ensure all of these are built and staged before this package
|
||||
dependencies = [
|
||||
"redbear-release",
|
||||
"redox-driver-sys",
|
||||
"linux-kpi",
|
||||
"redox-drm",
|
||||
"firmware-loader",
|
||||
"evdevd",
|
||||
"udev-shim",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "redbear-netctl"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "netctl"
|
||||
path = "src/main.rs"
|
||||
@@ -0,0 +1,365 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{self, Command};
|
||||
|
||||
const USAGE: &str = "Usage: netctl [--boot|list|status [profile]|start <profile>|stop <profile>|enable <profile>|disable [profile]|is-enabled [profile]]";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum ProfileIpMode {
|
||||
Dhcp,
|
||||
Static {
|
||||
address: String,
|
||||
gateway: Option<String>,
|
||||
dns: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Profile {
|
||||
name: String,
|
||||
interface: String,
|
||||
connection: String,
|
||||
ip_mode: ProfileIpMode,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("netctl: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let mut args = env::args().skip(1);
|
||||
let Some(command) = args.next() else {
|
||||
return Err(USAGE.into());
|
||||
};
|
||||
|
||||
match command.as_str() {
|
||||
"--boot" => run_boot_profile(),
|
||||
"list" => list_profiles(),
|
||||
"status" => status(args.next().as_deref()),
|
||||
"start" => start_profile(&required_profile(args.next())?, false),
|
||||
"stop" => stop_profile(&required_profile(args.next())?),
|
||||
"enable" => enable_profile(&required_profile(args.next())?),
|
||||
"disable" => disable_profile(args.next().as_deref()),
|
||||
"is-enabled" => is_enabled(args.next().as_deref()),
|
||||
"help" | "--help" | "-h" => {
|
||||
println!("{USAGE}");
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(USAGE.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn required_profile(profile: Option<String>) -> Result<String, String> {
|
||||
profile.ok_or_else(|| USAGE.to_string())
|
||||
}
|
||||
|
||||
fn run_boot_profile() -> Result<(), String> {
|
||||
let Some(active) = active_profile_name()? else {
|
||||
return Ok(());
|
||||
};
|
||||
start_profile(&active, true)
|
||||
}
|
||||
|
||||
fn list_profiles() -> Result<(), String> {
|
||||
let mut entries = profile_names()?;
|
||||
entries.sort();
|
||||
for entry in entries {
|
||||
println!("{entry}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn status(profile: Option<&str>) -> Result<(), String> {
|
||||
let active = active_profile_name()?;
|
||||
let selected = profile.map(str::to_string).or(active.clone());
|
||||
let address = current_addr().unwrap_or_else(|| "unconfigured".into());
|
||||
|
||||
match selected {
|
||||
Some(name) => {
|
||||
let enabled = active.as_deref() == Some(name.as_str());
|
||||
println!(
|
||||
"profile={} enabled={} address={}",
|
||||
name,
|
||||
if enabled { "yes" } else { "no" },
|
||||
address
|
||||
);
|
||||
}
|
||||
None => {
|
||||
println!("profile=none enabled=no address={address}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_profile(name: &str, boot: bool) -> Result<(), String> {
|
||||
ensure_runtime_surfaces()?;
|
||||
let profile = load_profile(name)?;
|
||||
apply_profile(&profile, boot)?;
|
||||
println!("started {}", profile.name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop_profile(name: &str) -> Result<(), String> {
|
||||
if active_profile_name()?.as_deref() == Some(name) {
|
||||
let _ = fs::remove_file(active_profile_path());
|
||||
}
|
||||
println!("stopped {}", name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn enable_profile(name: &str) -> Result<(), String> {
|
||||
let profile = load_profile(name)?;
|
||||
let active_path = active_profile_path();
|
||||
fs::write(&active_path, format!("{}\n", profile.name))
|
||||
.map_err(|err| format!("failed to write {}: {err}", active_path.display()))?;
|
||||
println!("enabled {}", profile.name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn disable_profile(profile: Option<&str>) -> Result<(), String> {
|
||||
if let Some(name) = profile {
|
||||
if active_profile_name()?.as_deref() != Some(name) {
|
||||
println!("disabled {}", name);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let _ = fs::remove_file(active_profile_path());
|
||||
println!("disabled {}", profile.unwrap_or("active"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_enabled(profile: Option<&str>) -> Result<(), String> {
|
||||
let active = active_profile_name()?;
|
||||
let enabled = match profile {
|
||||
Some(profile) => active.as_deref() == Some(profile),
|
||||
None => active.is_some(),
|
||||
};
|
||||
println!("{}", if enabled { "yes" } else { "no" });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_profile(profile: &Profile, boot: bool) -> Result<(), String> {
|
||||
if profile.connection != "ethernet" {
|
||||
return Err(format!(
|
||||
"unsupported Connection={} (only ethernet is supported)",
|
||||
profile.connection
|
||||
));
|
||||
}
|
||||
if profile.interface != "eth0" {
|
||||
return Err(format!(
|
||||
"unsupported Interface={} (only eth0 is supported)",
|
||||
profile.interface
|
||||
));
|
||||
}
|
||||
|
||||
match &profile.ip_mode {
|
||||
ProfileIpMode::Dhcp => {
|
||||
if boot
|
||||
|| current_addr().as_deref() == Some("Not configured")
|
||||
|| current_addr().is_none()
|
||||
{
|
||||
let _child = Command::new("dhcpd")
|
||||
.spawn()
|
||||
.map_err(|err| format!("failed to spawn dhcpd: {err}"))?;
|
||||
}
|
||||
}
|
||||
ProfileIpMode::Static {
|
||||
address,
|
||||
gateway,
|
||||
dns,
|
||||
} => {
|
||||
write_netcfg("ifaces/eth0/addr/set", address)?;
|
||||
if let Some(gateway) = gateway {
|
||||
write_netcfg("route/add", &format!("default via {gateway}"))?;
|
||||
}
|
||||
if let Some(dns) = dns {
|
||||
write_netcfg("resolv/nameserver", dns)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !boot && active_profile_name()?.as_deref() == Some(profile.name.as_str()) {
|
||||
let active_path = active_profile_path();
|
||||
fs::write(&active_path, format!("{}\n", profile.name))
|
||||
.map_err(|err| format!("failed to update {}: {err}", active_path.display()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_runtime_surfaces() -> Result<(), String> {
|
||||
let addr_path = format!("{}/ifaces/eth0/addr/list", netcfg_root().display());
|
||||
fs::read_to_string(&addr_path)
|
||||
.map(|_| ())
|
||||
.map_err(|err| format!("failed to access {addr_path}: {err}"))
|
||||
}
|
||||
|
||||
fn current_addr() -> Option<String> {
|
||||
fs::read_to_string(format!("{}/ifaces/eth0/addr/list", netcfg_root().display()))
|
||||
.ok()
|
||||
.map(|value| value.trim().to_string())
|
||||
}
|
||||
|
||||
fn write_netcfg(node: &str, value: &str) -> Result<(), String> {
|
||||
let path = format!("{}/{node}", netcfg_root().display());
|
||||
fs::write(&path, format!("{}\n", value.trim()))
|
||||
.map_err(|err| format!("failed to write {path}: {err}"))
|
||||
}
|
||||
|
||||
fn active_profile_name() -> Result<Option<String>, String> {
|
||||
let active_path = active_profile_path();
|
||||
match fs::read_to_string(&active_path) {
|
||||
Ok(value) => {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(value.to_string()))
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(err) => Err(format!("failed to read {}: {err}", active_path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
fn profile_names() -> Result<Vec<String>, String> {
|
||||
let profile_dir = profile_dir();
|
||||
let entries = fs::read_dir(&profile_dir)
|
||||
.map_err(|err| format!("failed to read {}: {err}", profile_dir.display()))?;
|
||||
let mut names = Vec::new();
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|err| format!("failed to read profile entry: {err}"))?;
|
||||
let path = entry.path();
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if name == "active" || name.starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
names.push(name.to_string());
|
||||
}
|
||||
|
||||
Ok(names)
|
||||
}
|
||||
|
||||
fn load_profile(name: &str) -> Result<Profile, String> {
|
||||
let path = profile_path(name);
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;
|
||||
parse_profile(name, &content)
|
||||
}
|
||||
|
||||
fn profile_path(name: &str) -> PathBuf {
|
||||
profile_dir().join(name)
|
||||
}
|
||||
|
||||
fn profile_dir() -> PathBuf {
|
||||
env::var_os("REDBEAR_NETCTL_PROFILE_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("/etc/netctl"))
|
||||
}
|
||||
|
||||
fn active_profile_path() -> PathBuf {
|
||||
env::var_os("REDBEAR_NETCTL_ACTIVE")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| profile_dir().join("active"))
|
||||
}
|
||||
|
||||
fn netcfg_root() -> PathBuf {
|
||||
env::var_os("REDBEAR_NETCFG_ROOT")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("/scheme/netcfg"))
|
||||
}
|
||||
|
||||
fn parse_profile(name: &str, content: &str) -> Result<Profile, String> {
|
||||
let mut interface = None;
|
||||
let mut connection = None;
|
||||
let mut ip = None;
|
||||
let mut address = None;
|
||||
let mut gateway = None;
|
||||
let mut dns = None;
|
||||
|
||||
for raw_line in content.lines() {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some((key, value)) = line.split_once('=') else {
|
||||
continue;
|
||||
};
|
||||
let key = key.trim();
|
||||
let value = value.trim();
|
||||
|
||||
match key {
|
||||
"Description" => {}
|
||||
"Interface" => interface = Some(parse_scalar(value)),
|
||||
"Connection" => connection = Some(parse_scalar(value)),
|
||||
"IP" => ip = Some(parse_scalar(value)),
|
||||
"Address" => address = parse_first_array_item(value),
|
||||
"Gateway" => gateway = Some(parse_scalar(value)),
|
||||
"DNS" => dns = parse_first_array_item(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let interface = interface.ok_or_else(|| format!("profile {name} is missing Interface="))?;
|
||||
let connection = connection.ok_or_else(|| format!("profile {name} is missing Connection="))?;
|
||||
let ip_mode = match ip
|
||||
.ok_or_else(|| format!("profile {name} is missing IP="))?
|
||||
.to_ascii_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
"dhcp" => ProfileIpMode::Dhcp,
|
||||
"static" => ProfileIpMode::Static {
|
||||
address: address.ok_or_else(|| format!("profile {name} is missing Address="))?,
|
||||
gateway,
|
||||
dns,
|
||||
},
|
||||
other => return Err(format!("unsupported IP={other}")),
|
||||
};
|
||||
|
||||
Ok(Profile {
|
||||
name: name.to_string(),
|
||||
interface,
|
||||
connection: connection.to_ascii_lowercase(),
|
||||
ip_mode,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_scalar(value: &str) -> String {
|
||||
let trimmed = value.trim();
|
||||
trimmed
|
||||
.trim_start_matches('(')
|
||||
.trim_end_matches(')')
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.trim_matches('\'')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn parse_first_array_item(value: &str) -> Option<String> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.starts_with('(') && trimmed.ends_with(')') {
|
||||
let inner = &trimmed[1..trimmed.len().saturating_sub(1)];
|
||||
inner
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(parse_scalar)
|
||||
.filter(|value| !value.is_empty())
|
||||
} else {
|
||||
let value = parse_scalar(trimmed);
|
||||
(!value.is_empty()).then_some(value)
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ path = "source"
|
||||
template = "cargo"
|
||||
|
||||
[package.files]
|
||||
"/usr/lib/drivers/udev-shim" = "udev-shim"
|
||||
"/usr/bin/udev" = "udev-shim"
|
||||
"/usr/lib/drivers/udev" = "udev-shim"
|
||||
|
||||
@@ -4,7 +4,9 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
redox-scheme = "0.1"
|
||||
syscall = { package = "redox_syscall", version = "0.4" }
|
||||
libc = "0.2"
|
||||
libredox = "0.1"
|
||||
redox-scheme = "0.11"
|
||||
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
thiserror = "2"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum Subsystem {
|
||||
Gpu,
|
||||
Network,
|
||||
@@ -9,8 +9,16 @@ pub enum Subsystem {
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum InputKind {
|
||||
Keyboard,
|
||||
Mouse,
|
||||
Generic,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeviceInfo {
|
||||
pub is_pci: bool,
|
||||
pub bus: u8,
|
||||
pub dev: u8,
|
||||
pub func: u8,
|
||||
@@ -19,15 +27,89 @@ pub struct DeviceInfo {
|
||||
pub class_code: u8,
|
||||
pub subclass: u8,
|
||||
pub subsystem: Subsystem,
|
||||
pub input_kind: Option<InputKind>,
|
||||
pub name: String,
|
||||
pub path: String,
|
||||
pub devpath: String,
|
||||
pub devnode: String,
|
||||
pub scheme_target: String,
|
||||
pub symlinks: Vec<String>,
|
||||
}
|
||||
|
||||
impl DeviceInfo {
|
||||
pub fn new_platform_input(
|
||||
name: &str,
|
||||
devpath: &str,
|
||||
input_kind: InputKind,
|
||||
devnode: &str,
|
||||
scheme_target: &str,
|
||||
) -> Self {
|
||||
Self {
|
||||
is_pci: false,
|
||||
bus: 0,
|
||||
dev: 0,
|
||||
func: 0,
|
||||
vendor_id: 0,
|
||||
device_id: 0,
|
||||
class_code: 0,
|
||||
subclass: 0,
|
||||
subsystem: Subsystem::Input,
|
||||
input_kind: Some(input_kind),
|
||||
name: name.to_string(),
|
||||
devpath: devpath.to_string(),
|
||||
devnode: devnode.to_string(),
|
||||
scheme_target: scheme_target.to_string(),
|
||||
symlinks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_node_metadata(
|
||||
&mut self,
|
||||
devnode: impl Into<String>,
|
||||
scheme_target: impl Into<String>,
|
||||
symlinks: Vec<String>,
|
||||
) {
|
||||
self.devnode = devnode.into();
|
||||
self.scheme_target = scheme_target.into();
|
||||
self.symlinks = symlinks;
|
||||
}
|
||||
|
||||
pub fn subsystem_name(&self) -> &'static str {
|
||||
match self.subsystem {
|
||||
Subsystem::Gpu => "drm",
|
||||
Subsystem::Network => "net",
|
||||
Subsystem::Storage => "block",
|
||||
Subsystem::Audio => "sound",
|
||||
Subsystem::Usb => "usb",
|
||||
Subsystem::Input => "input",
|
||||
Subsystem::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id_path(&self) -> String {
|
||||
if let Some(slot) = self.devpath.strip_prefix("/devices/pci/") {
|
||||
return format!("pci-{slot}");
|
||||
}
|
||||
|
||||
self.devpath
|
||||
.trim_start_matches("/devices/")
|
||||
.replace('/', "-")
|
||||
}
|
||||
|
||||
pub fn is_input_keyboard(&self) -> bool {
|
||||
self.input_kind == Some(InputKind::Keyboard)
|
||||
}
|
||||
|
||||
pub fn is_input_mouse(&self) -> bool {
|
||||
self.input_kind == Some(InputKind::Mouse)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo {
|
||||
let path = format!("/devices/pci/{:04x}:{:02x}:{:02x}.{}", bus, 0, dev, func);
|
||||
let devpath = format!("/devices/pci/{:04x}:{:02x}:{:02x}.{}", bus, 0, dev, func);
|
||||
|
||||
let config_path = format!("/scheme/pci/{}.{}.{}", bus, dev, func);
|
||||
let (vendor_id, device_id, class_code, subclass) = read_pci_config(&config_path);
|
||||
let input_kind = detect_input_kind(class_code, subclass);
|
||||
|
||||
let subsystem = match class_code {
|
||||
0x03 => Subsystem::Gpu,
|
||||
@@ -39,9 +121,10 @@ pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo {
|
||||
_ => Subsystem::Unknown,
|
||||
};
|
||||
|
||||
let name = format_device_name(vendor_id, device_id, class_code);
|
||||
let name = format_device_name(vendor_id, device_id, class_code, subclass, input_kind);
|
||||
|
||||
DeviceInfo {
|
||||
is_pci: true,
|
||||
bus,
|
||||
dev,
|
||||
func,
|
||||
@@ -50,8 +133,12 @@ pub fn classify_pci_device(bus: u8, dev: u8, func: u8) -> DeviceInfo {
|
||||
class_code,
|
||||
subclass,
|
||||
subsystem,
|
||||
input_kind,
|
||||
name,
|
||||
path,
|
||||
devpath,
|
||||
devnode: String::new(),
|
||||
scheme_target: String::new(),
|
||||
symlinks: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +155,40 @@ fn read_pci_config(path: &str) -> (u16, u16, u8, u8) {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_device_name(vendor_id: u16, device_id: u16, class_code: u8) -> String {
|
||||
fn detect_input_kind(class_code: u8, subclass: u8) -> Option<InputKind> {
|
||||
if class_code != 0x09 {
|
||||
return None;
|
||||
}
|
||||
|
||||
match subclass {
|
||||
0x00 => Some(InputKind::Keyboard),
|
||||
0x04 => Some(InputKind::Generic),
|
||||
_ => Some(InputKind::Generic),
|
||||
}
|
||||
}
|
||||
|
||||
fn format_device_name(
|
||||
vendor_id: u16,
|
||||
device_id: u16,
|
||||
class_code: u8,
|
||||
subclass: u8,
|
||||
input_kind: Option<InputKind>,
|
||||
) -> String {
|
||||
if class_code == 0x03 {
|
||||
if let Some(name) = gpu_device_name(vendor_id, device_id) {
|
||||
return format!("{name} [{vendor_id:04x}:{device_id:04x}]");
|
||||
}
|
||||
}
|
||||
|
||||
if class_code == 0x09 {
|
||||
let name = match (subclass, input_kind) {
|
||||
(0x00, Some(InputKind::Keyboard)) => "PS/2 Keyboard Controller",
|
||||
(0x04, _) => "USB HID Controller",
|
||||
_ => "Input Device",
|
||||
};
|
||||
return format!("{name} [{vendor_id:04x}:{device_id:04x}]");
|
||||
}
|
||||
|
||||
let vendor_name = match vendor_id {
|
||||
0x8086 => "Intel",
|
||||
0x1002 => "AMD",
|
||||
@@ -95,19 +215,93 @@ fn format_device_name(vendor_id: u16, device_id: u16, class_code: u8) -> String
|
||||
)
|
||||
}
|
||||
|
||||
pub fn format_device_info(dev: &DeviceInfo) -> String {
|
||||
let subsystem = match dev.subsystem {
|
||||
Subsystem::Gpu => "gpu",
|
||||
Subsystem::Network => "net",
|
||||
Subsystem::Storage => "block",
|
||||
Subsystem::Audio => "sound",
|
||||
Subsystem::Usb => "usb",
|
||||
Subsystem::Input => "input",
|
||||
Subsystem::Unknown => "unknown",
|
||||
};
|
||||
|
||||
format!(
|
||||
"P={}\nE=SUBSYSTEM={}\nE=PCI_VENDOR_ID={:#06x}\nE=PCI_DEVICE_ID={:#06x}\nE=PCI_CLASS={:#04x}{:02x}\nE=DEVNAME={}\n",
|
||||
dev.path, subsystem, dev.vendor_id, dev.device_id, dev.class_code, dev.subclass, dev.name
|
||||
)
|
||||
fn gpu_device_name(vendor_id: u16, device_id: u16) -> Option<&'static str> {
|
||||
match vendor_id {
|
||||
0x1002 => match device_id {
|
||||
0x73A3 => Some("AMD Radeon RX 6600 XT / 6650 XT (RDNA2)"),
|
||||
0x73BF => Some("AMD Radeon RX 6800 XT / 6900 XT (RDNA2)"),
|
||||
0x73DF => Some("AMD Radeon RX 6700 XT / 6750 XT (RDNA2)"),
|
||||
0x73EF => Some("AMD Radeon RX 6800 / 6850M XT (RDNA2)"),
|
||||
0x7422 => Some("AMD Radeon 780M (RDNA3)"),
|
||||
0x7448 => Some("AMD Radeon RX 7900 XT (RDNA3)"),
|
||||
0x744C => Some("AMD Radeon RX 7900 XTX (RDNA3)"),
|
||||
0x7480 => Some("AMD Radeon RX 7800 XT / 7700 XT (RDNA3)"),
|
||||
_ => Some("AMD Radeon GPU"),
|
||||
},
|
||||
0x8086 => match device_id {
|
||||
0x3E92 => Some("Intel UHD Graphics 630"),
|
||||
0x5912 => Some("Intel HD Graphics 630"),
|
||||
0x9A49 => Some("Intel Iris Xe Graphics (Tiger Lake)"),
|
||||
0x46A6 => Some("Intel Iris Xe Graphics (Alder Lake-P)"),
|
||||
0x56A0 => Some("Intel Arc Graphics (DG2)"),
|
||||
0x56A1 => Some("Intel Arc A380 (DG2)"),
|
||||
_ => Some("Intel Graphics"),
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn device_properties(dev: &DeviceInfo) -> Vec<(String, String)> {
|
||||
let mut props = Vec::new();
|
||||
props.push(("DEVPATH".to_string(), dev.devpath.clone()));
|
||||
props.push(("SUBSYSTEM".to_string(), dev.subsystem_name().to_string()));
|
||||
props.push(("ID_MODEL_FROM_DATABASE".to_string(), dev.name.clone()));
|
||||
|
||||
if !dev.devnode.is_empty() {
|
||||
props.push(("DEVNAME".to_string(), dev.devnode.clone()));
|
||||
}
|
||||
|
||||
let id_path = dev.id_path();
|
||||
if !id_path.is_empty() {
|
||||
props.push(("ID_PATH".to_string(), id_path));
|
||||
}
|
||||
|
||||
if dev.is_pci {
|
||||
props.push((
|
||||
"PCI_VENDOR_ID".to_string(),
|
||||
format!("0x{:04x}", dev.vendor_id),
|
||||
));
|
||||
props.push((
|
||||
"PCI_DEVICE_ID".to_string(),
|
||||
format!("0x{:04x}", dev.device_id),
|
||||
));
|
||||
props.push((
|
||||
"PCI_CLASS".to_string(),
|
||||
format!("0x{:02x}{:02x}", dev.class_code, dev.subclass),
|
||||
));
|
||||
}
|
||||
|
||||
if dev.subsystem == Subsystem::Input {
|
||||
props.push(("ID_INPUT".to_string(), "1".to_string()));
|
||||
match dev.input_kind {
|
||||
Some(InputKind::Keyboard) => {
|
||||
props.push(("ID_INPUT_KEYBOARD".to_string(), "1".to_string()));
|
||||
}
|
||||
Some(InputKind::Mouse) => {
|
||||
props.push(("ID_INPUT_MOUSE".to_string(), "1".to_string()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
props
|
||||
}
|
||||
|
||||
pub fn format_device_info(dev: &DeviceInfo) -> String {
|
||||
let mut info = format!("P={}\n", dev.devpath);
|
||||
for (key, value) in device_properties(dev) {
|
||||
info.push_str(&format!("E={key}={value}\n"));
|
||||
}
|
||||
for link in &dev.symlinks {
|
||||
info.push_str(&format!("S={}\n", link.trim_start_matches('/')));
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
pub fn format_uevent_info(dev: &DeviceInfo) -> String {
|
||||
let mut info = String::from("ACTION=add\n");
|
||||
for (key, value) in device_properties(dev) {
|
||||
info.push_str(&format!("{key}={value}\n"));
|
||||
}
|
||||
info
|
||||
}
|
||||
|
||||
@@ -2,10 +2,13 @@ mod device_db;
|
||||
mod scheme;
|
||||
|
||||
use std::env;
|
||||
use std::process;
|
||||
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
|
||||
|
||||
use log::{error, info, LevelFilter, Metadata, Record};
|
||||
use redox_scheme::{SignalBehavior, Socket};
|
||||
use redox_scheme::{
|
||||
scheme::{SchemeState, SchemeSync},
|
||||
SignalBehavior, Socket,
|
||||
};
|
||||
|
||||
use scheme::UdevScheme;
|
||||
|
||||
@@ -25,45 +28,35 @@ impl log::Log for StderrLogger {
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let mut scheme = UdevScheme::new();
|
||||
|
||||
match scheme.scan_pci_devices() {
|
||||
Ok(n) => info!("udev-shim: enumerated {} PCI device(s)", n),
|
||||
Err(e) => error!("udev-shim: PCI scan failed: {}", e),
|
||||
fn init_logging(level: LevelFilter) {
|
||||
if log::set_boxed_logger(Box::new(StderrLogger { level })).is_err() {
|
||||
return;
|
||||
}
|
||||
log::set_max_level(level);
|
||||
}
|
||||
|
||||
let socket =
|
||||
Socket::create("udev").map_err(|e| format!("failed to register udev scheme: {}", e))?;
|
||||
info!("udev-shim: registered scheme:udev");
|
||||
unsafe fn get_init_notify_fd() -> RawFd {
|
||||
let fd: RawFd = env::var("INIT_NOTIFY")
|
||||
.expect("udev-shim: INIT_NOTIFY not set")
|
||||
.parse()
|
||||
.expect("udev-shim: INIT_NOTIFY is not a valid fd");
|
||||
libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
|
||||
fd
|
||||
}
|
||||
|
||||
loop {
|
||||
let request = match socket.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(r)) => r,
|
||||
Ok(None) => {
|
||||
info!("udev-shim: scheme unmounted, exiting");
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("udev-shim: failed to read scheme request: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
fn notify_scheme_ready(notify_fd: RawFd, socket: &Socket, scheme: &mut UdevScheme) {
|
||||
let cap_id = scheme.scheme_root().expect("udev-shim: scheme_root failed");
|
||||
let cap_fd = socket
|
||||
.create_this_scheme_fd(0, cap_id, 0, 0)
|
||||
.expect("udev-shim: create_this_scheme_fd failed");
|
||||
|
||||
let response = match request.handle_scheme_block_mut(&mut scheme) {
|
||||
Ok(r) => r,
|
||||
Err(_req) => {
|
||||
error!("udev-shim: failed to handle request");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = socket.write_response(response, SignalBehavior::Restart) {
|
||||
error!("udev-shim: failed to write response: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
syscall::call_wo(
|
||||
notify_fd as usize,
|
||||
&libredox::Fd::new(cap_fd).into_raw().to_ne_bytes(),
|
||||
syscall::CallFlags::FD,
|
||||
&[],
|
||||
)
|
||||
.expect("udev-shim: failed to notify init that scheme is ready");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -72,11 +65,40 @@ fn main() {
|
||||
Ok("trace") => LevelFilter::Trace,
|
||||
_ => LevelFilter::Info,
|
||||
};
|
||||
let _ = log::set_boxed_logger(Box::new(StderrLogger { level: log_level }));
|
||||
log::set_max_level(log_level);
|
||||
|
||||
if let Err(e) = run() {
|
||||
error!("udev-shim: fatal error: {}", e);
|
||||
process::exit(1);
|
||||
init_logging(log_level);
|
||||
|
||||
let mut scheme = UdevScheme::new();
|
||||
|
||||
match scheme.scan_pci_devices() {
|
||||
Ok(n) => info!("udev-shim: enumerated {} PCI device(s)", n),
|
||||
Err(e) => error!("udev-shim: PCI scan failed: {}", e),
|
||||
}
|
||||
|
||||
let notify_fd = unsafe { get_init_notify_fd() };
|
||||
let socket = Socket::create().expect("udev-shim: failed to create udev scheme");
|
||||
let mut state = SchemeState::new();
|
||||
|
||||
notify_scheme_ready(notify_fd, &socket, &mut scheme);
|
||||
|
||||
libredox::call::setrens(0, 0).expect("udev-shim: failed to enter null namespace");
|
||||
|
||||
info!("udev-shim: registered scheme:udev");
|
||||
|
||||
while let Some(request) = socket
|
||||
.next_request(SignalBehavior::Restart)
|
||||
.expect("udev-shim: failed to read scheme request")
|
||||
{
|
||||
match request.kind() {
|
||||
redox_scheme::RequestKind::Call(request) => {
|
||||
let response = request.handle_sync(&mut scheme, &mut state);
|
||||
socket
|
||||
.write_response(response, SignalBehavior::Restart)
|
||||
.expect("udev-shim: failed to write response");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
@@ -1,170 +1,633 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use syscall::data::Stat;
|
||||
use syscall::error::{Error, Result, EBADF, EINVAL, ENOENT, EROFS};
|
||||
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, SEEK_CUR, SEEK_END, SEEK_SET};
|
||||
use redox_scheme::scheme::SchemeSync;
|
||||
use redox_scheme::{CallerCtx, OpenResult};
|
||||
use syscall::error::{Error, Result, EACCES, EBADF, ENOENT, EROFS};
|
||||
use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE};
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
|
||||
use crate::device_db::{classify_pci_device, format_device_info, DeviceInfo, Subsystem};
|
||||
use crate::device_db::{
|
||||
classify_pci_device, format_device_info, format_uevent_info, DeviceInfo, InputKind, Subsystem,
|
||||
};
|
||||
|
||||
struct Handle {
|
||||
kind: HandleKind,
|
||||
offset: usize,
|
||||
}
|
||||
const SCHEME_ROOT_ID: usize = 1;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum HandleKind {
|
||||
Root,
|
||||
Devices,
|
||||
Device(usize),
|
||||
Dev,
|
||||
DevInputDir,
|
||||
DevInput(usize),
|
||||
DevInputMice,
|
||||
DevDriDir,
|
||||
DevDri(usize),
|
||||
DevLinks,
|
||||
LinksInputDir,
|
||||
LinksInputByPathDir,
|
||||
LinksDriDir,
|
||||
LinksDriByPathDir,
|
||||
Link(usize),
|
||||
Uevent,
|
||||
}
|
||||
|
||||
pub struct UdevScheme {
|
||||
next_id: usize,
|
||||
handles: BTreeMap<usize, Handle>,
|
||||
handles: BTreeMap<usize, HandleKind>,
|
||||
devices: Vec<DeviceInfo>,
|
||||
}
|
||||
|
||||
impl UdevScheme {
|
||||
pub fn new() -> Self {
|
||||
UdevScheme {
|
||||
next_id: 0,
|
||||
Self {
|
||||
next_id: SCHEME_ROOT_ID + 1,
|
||||
handles: BTreeMap::new(),
|
||||
devices: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scan_pci_devices(&mut self) -> Result<usize> {
|
||||
let dir = match std::fs::read_dir("/scheme/pci") {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::warn!("udev-shim: failed to read /scheme/pci: {e}");
|
||||
return Ok(0);
|
||||
self.devices.clear();
|
||||
|
||||
let mut pci_slots = Vec::new();
|
||||
match std::fs::read_dir("/scheme/pci") {
|
||||
Ok(dir) => {
|
||||
for entry in dir {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
let name = match entry.file_name().to_str() {
|
||||
Some(name) => name.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
if let Some(slot) = parse_pci_slot(&name) {
|
||||
pci_slots.push(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut count = 0;
|
||||
for entry in dir {
|
||||
let entry = match entry {
|
||||
Ok(e) => e,
|
||||
Err(_) => continue,
|
||||
};
|
||||
let name = match entry.file_name().to_str() {
|
||||
Some(n) => n.to_string(),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let parts: Vec<&str> = name.split('.').collect();
|
||||
if parts.len() < 3 {
|
||||
continue;
|
||||
Err(err) => {
|
||||
log::warn!("udev-shim: failed to read /scheme/pci: {err}");
|
||||
}
|
||||
|
||||
let bus: u8 = parts[0].parse().unwrap_or(0);
|
||||
let dev: u8 = parts[1].parse().unwrap_or(0);
|
||||
let func: u8 = parts[2].parse().unwrap_or(0);
|
||||
|
||||
let info = classify_pci_device(bus, dev, func);
|
||||
self.devices.push(info);
|
||||
count += 1;
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
pci_slots.sort_unstable();
|
||||
for (bus, dev, func) in pci_slots {
|
||||
self.devices.push(classify_pci_device(bus, dev, func));
|
||||
}
|
||||
|
||||
if path_exists("/scheme/input") {
|
||||
self.devices.push(DeviceInfo::new_platform_input(
|
||||
"Redox Keyboard Input",
|
||||
"/devices/platform/keyboard0",
|
||||
InputKind::Keyboard,
|
||||
"",
|
||||
"",
|
||||
));
|
||||
}
|
||||
|
||||
if path_exists("/scheme/pointer") || path_exists("/scheme/mouse") {
|
||||
self.devices.push(DeviceInfo::new_platform_input(
|
||||
"Redox Mouse Input",
|
||||
"/devices/platform/mouse0",
|
||||
InputKind::Mouse,
|
||||
"",
|
||||
"",
|
||||
));
|
||||
}
|
||||
|
||||
self.assign_virtual_nodes();
|
||||
|
||||
Ok(self.devices.len())
|
||||
}
|
||||
|
||||
fn assign_virtual_nodes(&mut self) {
|
||||
for dev in &mut self.devices {
|
||||
if dev.subsystem == Subsystem::Gpu || (dev.subsystem == Subsystem::Input && !dev.is_pci)
|
||||
{
|
||||
dev.set_node_metadata("", "", Vec::new());
|
||||
} else {
|
||||
dev.symlinks.clear();
|
||||
}
|
||||
}
|
||||
|
||||
let mut gpu_indices: Vec<usize> = self
|
||||
.devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, dev)| (dev.subsystem == Subsystem::Gpu).then_some(idx))
|
||||
.collect();
|
||||
gpu_indices.sort_by_key(|idx| {
|
||||
let dev = &self.devices[*idx];
|
||||
(gpu_priority(dev), dev.bus, dev.dev, dev.func)
|
||||
});
|
||||
|
||||
for (card_idx, device_idx) in gpu_indices.into_iter().enumerate() {
|
||||
let devnode = format!("/dev/dri/card{card_idx}");
|
||||
let scheme_target = format!("drm/card{card_idx}");
|
||||
let symlink = format!(
|
||||
"/links/dri/by-path/{}-card",
|
||||
self.devices[device_idx].id_path()
|
||||
);
|
||||
self.devices[device_idx].set_node_metadata(devnode, scheme_target, vec![symlink]);
|
||||
}
|
||||
|
||||
let mut input_indices: Vec<usize> = self
|
||||
.devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, dev)| {
|
||||
(dev.subsystem == Subsystem::Input && !dev.is_pci).then_some(idx)
|
||||
})
|
||||
.collect();
|
||||
input_indices.sort_by_key(|idx| {
|
||||
let dev = &self.devices[*idx];
|
||||
(input_priority(dev), dev.devpath.clone())
|
||||
});
|
||||
|
||||
for (event_idx, device_idx) in input_indices.into_iter().enumerate() {
|
||||
let devnode = format!("/dev/input/event{event_idx}");
|
||||
let scheme_target = format!("evdev/event{event_idx}");
|
||||
let suffix = match self.devices[device_idx].input_kind {
|
||||
Some(InputKind::Keyboard) => "event-kbd",
|
||||
Some(InputKind::Mouse) => "event-mouse",
|
||||
Some(InputKind::Generic) | None => "event",
|
||||
};
|
||||
let symlink = format!(
|
||||
"/links/input/by-path/{}-{}",
|
||||
self.devices[device_idx].id_path(),
|
||||
suffix
|
||||
);
|
||||
self.devices[device_idx].set_node_metadata(devnode, scheme_target, vec![symlink]);
|
||||
}
|
||||
}
|
||||
|
||||
fn find_device_by_devnode(&self, devnode: &str) -> Option<usize> {
|
||||
self.devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, dev)| (dev.devnode == devnode).then_some(idx))
|
||||
}
|
||||
|
||||
fn find_device_by_link(&self, prefix: &str, tail: &str) -> Option<usize> {
|
||||
let expected = format!("{prefix}{tail}");
|
||||
self.devices.iter().enumerate().find_map(|(idx, dev)| {
|
||||
dev.symlinks
|
||||
.iter()
|
||||
.any(|link| link == &expected)
|
||||
.then_some(idx)
|
||||
})
|
||||
}
|
||||
|
||||
fn mouse_device_index(&self) -> Option<usize> {
|
||||
self.devices.iter().enumerate().find_map(|(idx, dev)| {
|
||||
(dev.is_input_mouse() && !dev.scheme_target.is_empty()).then_some(idx)
|
||||
})
|
||||
}
|
||||
|
||||
fn input_event_indices(&self) -> Vec<usize> {
|
||||
self.devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, dev)| {
|
||||
(dev.devnode.starts_with("/dev/input/event") && !dev.scheme_target.is_empty())
|
||||
.then_some(idx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn dri_card_indices(&self) -> Vec<usize> {
|
||||
self.devices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, dev)| {
|
||||
(dev.devnode.starts_with("/dev/dri/card") && !dev.scheme_target.is_empty())
|
||||
.then_some(idx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn directory_listing<I>(&self, entries: I) -> String
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let mut listing = String::new();
|
||||
for entry in entries {
|
||||
listing.push_str(&entry);
|
||||
listing.push('\n');
|
||||
}
|
||||
listing
|
||||
}
|
||||
|
||||
fn link_listing(&self, prefix: &str) -> String {
|
||||
self.directory_listing(
|
||||
self.devices
|
||||
.iter()
|
||||
.flat_map(|dev| dev.symlinks.iter())
|
||||
.filter_map(|link| {
|
||||
link.strip_prefix(prefix).and_then(|tail| {
|
||||
(!tail.is_empty() && !tail.contains('/')).then(|| tail.to_string())
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn uevent_content(&self) -> String {
|
||||
let mut content = String::new();
|
||||
for (idx, dev) in self.devices.iter().enumerate() {
|
||||
if idx > 0 {
|
||||
content.push('\n');
|
||||
}
|
||||
content.push_str(&format_uevent_info(dev));
|
||||
}
|
||||
content
|
||||
}
|
||||
|
||||
fn content_for_handle(&self, kind: &HandleKind) -> Result<String> {
|
||||
match kind {
|
||||
HandleKind::Root => Ok(self.directory_listing(
|
||||
["devices", "dev", "links", "uevent"]
|
||||
.into_iter()
|
||||
.map(String::from),
|
||||
)),
|
||||
HandleKind::Devices => {
|
||||
Ok(self.directory_listing((0..self.devices.len()).map(|idx| idx.to_string())))
|
||||
}
|
||||
HandleKind::Device(idx) => self
|
||||
.devices
|
||||
.get(*idx)
|
||||
.map(format_device_info)
|
||||
.ok_or_else(|| Error::new(ENOENT)),
|
||||
HandleKind::Dev => {
|
||||
Ok(self.directory_listing(["input", "dri"].into_iter().map(String::from)))
|
||||
}
|
||||
HandleKind::DevInputDir => {
|
||||
let mut entries: Vec<String> = self
|
||||
.input_event_indices()
|
||||
.into_iter()
|
||||
.filter_map(|idx| basename(&self.devices[idx].devnode))
|
||||
.collect();
|
||||
if self.mouse_device_index().is_some() {
|
||||
entries.push("mice".to_string());
|
||||
}
|
||||
Ok(self.directory_listing(entries))
|
||||
}
|
||||
HandleKind::DevInput(idx) => {
|
||||
let dev = self.devices.get(*idx).ok_or_else(|| Error::new(ENOENT))?;
|
||||
if dev.scheme_target.is_empty() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
let mut info = format_device_info(dev);
|
||||
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
|
||||
Ok(info)
|
||||
}
|
||||
HandleKind::DevDri(idx) => {
|
||||
let dev = self.devices.get(*idx).ok_or_else(|| Error::new(ENOENT))?;
|
||||
if dev.scheme_target.is_empty() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
let mut info = format_device_info(dev);
|
||||
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
|
||||
Ok(info)
|
||||
}
|
||||
HandleKind::Link(idx) => {
|
||||
let dev = self.devices.get(*idx).ok_or_else(|| Error::new(ENOENT))?;
|
||||
if dev.scheme_target.is_empty() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
let mut info = format_device_info(dev);
|
||||
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
|
||||
Ok(info)
|
||||
}
|
||||
HandleKind::DevInputMice => {
|
||||
let idx = self
|
||||
.mouse_device_index()
|
||||
.ok_or_else(|| Error::new(ENOENT))?;
|
||||
let dev = &self.devices[idx];
|
||||
let mut info = format_device_info(dev);
|
||||
info.push_str(&format!("SCHEME_TARGET={}\n", dev.scheme_target));
|
||||
Ok(info)
|
||||
}
|
||||
HandleKind::DevDriDir => Ok(self.directory_listing(
|
||||
self.dri_card_indices()
|
||||
.into_iter()
|
||||
.filter_map(|idx| basename(&self.devices[idx].devnode)),
|
||||
)),
|
||||
HandleKind::DevLinks => {
|
||||
Ok(self.directory_listing(["input", "dri"].into_iter().map(String::from)))
|
||||
}
|
||||
HandleKind::LinksInputDir => {
|
||||
Ok(self.directory_listing(["by-path"].into_iter().map(String::from)))
|
||||
}
|
||||
HandleKind::LinksInputByPathDir => Ok(self.link_listing("/links/input/by-path/")),
|
||||
HandleKind::LinksDriDir => {
|
||||
Ok(self.directory_listing(["by-path"].into_iter().map(String::from)))
|
||||
}
|
||||
HandleKind::LinksDriByPathDir => Ok(self.link_listing("/links/dri/by-path/")),
|
||||
HandleKind::Uevent => Ok(self.uevent_content()),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_directory(kind: &HandleKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
HandleKind::Root
|
||||
| HandleKind::Devices
|
||||
| HandleKind::Dev
|
||||
| HandleKind::DevInputDir
|
||||
| HandleKind::DevDriDir
|
||||
| HandleKind::DevLinks
|
||||
| HandleKind::LinksInputDir
|
||||
| HandleKind::LinksInputByPathDir
|
||||
| HandleKind::LinksDriDir
|
||||
| HandleKind::LinksDriByPathDir
|
||||
)
|
||||
}
|
||||
|
||||
fn kind_for_id(&self, id: usize) -> Result<HandleKind> {
|
||||
if id == SCHEME_ROOT_ID {
|
||||
return Ok(HandleKind::Root);
|
||||
}
|
||||
|
||||
self.handles
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::new(EBADF))
|
||||
}
|
||||
|
||||
fn kind_for_path(&self, path: &str) -> Result<HandleKind> {
|
||||
let cleaned = path.trim_matches('/');
|
||||
|
||||
match cleaned {
|
||||
"" => Ok(HandleKind::Root),
|
||||
"devices" => Ok(HandleKind::Devices),
|
||||
"dev" => Ok(HandleKind::Dev),
|
||||
"dev/input" => Ok(HandleKind::DevInputDir),
|
||||
"dev/input/mice" => {
|
||||
if self.mouse_device_index().is_none() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
Ok(HandleKind::DevInputMice)
|
||||
}
|
||||
"dev/dri" => Ok(HandleKind::DevDriDir),
|
||||
"links" => Ok(HandleKind::DevLinks),
|
||||
"links/input" => Ok(HandleKind::LinksInputDir),
|
||||
"links/input/by-path" => Ok(HandleKind::LinksInputByPathDir),
|
||||
"links/dri" => Ok(HandleKind::LinksDriDir),
|
||||
"links/dri/by-path" => Ok(HandleKind::LinksDriByPathDir),
|
||||
"uevent" => Ok(HandleKind::Uevent),
|
||||
_ => {
|
||||
if let Some(rest) = cleaned.strip_prefix("devices/") {
|
||||
let idx = rest.parse::<usize>().map_err(|_| Error::new(ENOENT))?;
|
||||
if idx >= self.devices.len() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
Ok(HandleKind::Device(idx))
|
||||
} else if let Some(rest) = cleaned.strip_prefix("dev/input/") {
|
||||
let devnode = format!("/dev/input/{rest}");
|
||||
let idx = self
|
||||
.find_device_by_devnode(&devnode)
|
||||
.ok_or_else(|| Error::new(ENOENT))?;
|
||||
Ok(HandleKind::DevInput(idx))
|
||||
} else if let Some(rest) = cleaned.strip_prefix("dev/dri/") {
|
||||
let devnode = format!("/dev/dri/{rest}");
|
||||
let idx = self
|
||||
.find_device_by_devnode(&devnode)
|
||||
.ok_or_else(|| Error::new(ENOENT))?;
|
||||
Ok(HandleKind::DevDri(idx))
|
||||
} else if let Some(rest) = cleaned.strip_prefix("links/input/by-path/") {
|
||||
let idx = self
|
||||
.find_device_by_link("/links/input/by-path/", rest)
|
||||
.ok_or_else(|| Error::new(ENOENT))?;
|
||||
Ok(HandleKind::Link(idx))
|
||||
} else if let Some(rest) = cleaned.strip_prefix("links/dri/by-path/") {
|
||||
let idx = self
|
||||
.find_device_by_link("/links/dri/by-path/", rest)
|
||||
.ok_or_else(|| Error::new(ENOENT))?;
|
||||
Ok(HandleKind::Link(idx))
|
||||
} else {
|
||||
Err(Error::new(ENOENT))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn allocate_handle(&mut self, kind: HandleKind) -> usize {
|
||||
let id = self.next_id;
|
||||
self.next_id = self.next_id.saturating_add(1);
|
||||
self.handles.insert(id, kind);
|
||||
id
|
||||
}
|
||||
|
||||
fn path_for_handle(&self, kind: &HandleKind) -> Result<String> {
|
||||
match kind {
|
||||
HandleKind::Root => Ok("/scheme/udev".to_string()),
|
||||
HandleKind::Devices => Ok("/scheme/udev/devices".to_string()),
|
||||
HandleKind::Device(idx) => {
|
||||
if *idx >= self.devices.len() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
Ok(format!("/scheme/udev/devices/{idx}"))
|
||||
}
|
||||
HandleKind::Dev => Ok("/scheme/udev/dev".to_string()),
|
||||
HandleKind::DevInputDir => Ok("/scheme/udev/dev/input".to_string()),
|
||||
HandleKind::DevInput(idx) => self
|
||||
.devices
|
||||
.get(*idx)
|
||||
.filter(|dev| !dev.devnode.is_empty())
|
||||
.map(|dev| format!("/scheme/udev{}", dev.devnode))
|
||||
.ok_or_else(|| Error::new(ENOENT)),
|
||||
HandleKind::DevInputMice => Ok("/scheme/udev/dev/input/mice".to_string()),
|
||||
HandleKind::DevDriDir => Ok("/scheme/udev/dev/dri".to_string()),
|
||||
HandleKind::DevDri(idx) => self
|
||||
.devices
|
||||
.get(*idx)
|
||||
.filter(|dev| !dev.devnode.is_empty())
|
||||
.map(|dev| format!("/scheme/udev{}", dev.devnode))
|
||||
.ok_or_else(|| Error::new(ENOENT)),
|
||||
HandleKind::DevLinks => Ok("/scheme/udev/links".to_string()),
|
||||
HandleKind::LinksInputDir => Ok("/scheme/udev/links/input".to_string()),
|
||||
HandleKind::LinksInputByPathDir => Ok("/scheme/udev/links/input/by-path".to_string()),
|
||||
HandleKind::LinksDriDir => Ok("/scheme/udev/links/dri".to_string()),
|
||||
HandleKind::LinksDriByPathDir => Ok("/scheme/udev/links/dri/by-path".to_string()),
|
||||
HandleKind::Link(idx) => self
|
||||
.devices
|
||||
.get(*idx)
|
||||
.and_then(|dev| dev.symlinks.first())
|
||||
.map(|link| format!("/scheme/udev{link}"))
|
||||
.ok_or_else(|| Error::new(ENOENT)),
|
||||
HandleKind::Uevent => Ok("/scheme/udev/uevent".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl redox_scheme::SchemeBlockMut for UdevScheme {
|
||||
fn open(&mut self, path: &str, _flags: usize, _uid: u32, _gid: u32) -> Result<Option<usize>> {
|
||||
let cleaned = path.trim_matches('/');
|
||||
|
||||
let kind = if cleaned.is_empty() {
|
||||
HandleKind::Root
|
||||
} else if cleaned == "devices" || cleaned == "devices/" {
|
||||
HandleKind::Root
|
||||
} else if let Some(rest) = cleaned.strip_prefix("devices/") {
|
||||
let idx: usize = rest
|
||||
.trim_end_matches('/')
|
||||
.parse()
|
||||
.map_err(|_| Error::new(ENOENT))?;
|
||||
if idx >= self.devices.len() {
|
||||
return Err(Error::new(ENOENT));
|
||||
}
|
||||
HandleKind::Device(idx)
|
||||
} else {
|
||||
return Err(Error::new(ENOENT));
|
||||
};
|
||||
|
||||
let id = self.next_id;
|
||||
self.next_id += 1;
|
||||
self.handles.insert(id, Handle { kind, offset: 0 });
|
||||
Ok(Some(id))
|
||||
impl SchemeSync for UdevScheme {
|
||||
fn scheme_root(&mut self) -> Result<usize> {
|
||||
Ok(SCHEME_ROOT_ID)
|
||||
}
|
||||
|
||||
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
path: &str,
|
||||
_flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<OpenResult> {
|
||||
if dirfd != SCHEME_ROOT_ID {
|
||||
return Err(Error::new(EACCES));
|
||||
}
|
||||
|
||||
let content = match &handle.kind {
|
||||
HandleKind::Root => {
|
||||
let mut listing = String::new();
|
||||
for (i, dev) in self.devices.iter().enumerate() {
|
||||
listing.push_str(&format!("devices/{}\n", i));
|
||||
}
|
||||
listing
|
||||
}
|
||||
HandleKind::Device(idx) => {
|
||||
let dev = &self.devices[*idx];
|
||||
format_device_info(dev)
|
||||
}
|
||||
let kind = self.kind_for_path(path)?;
|
||||
let id = if matches!(kind, HandleKind::Root) {
|
||||
SCHEME_ROOT_ID
|
||||
} else {
|
||||
self.allocate_handle(kind)
|
||||
};
|
||||
|
||||
Ok(OpenResult::ThisScheme {
|
||||
number: id,
|
||||
flags: NewFdFlags::empty(),
|
||||
})
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
id: usize,
|
||||
buf: &mut [u8],
|
||||
offset: u64,
|
||||
_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let kind = self.kind_for_id(id)?;
|
||||
let content = self.content_for_handle(&kind)?;
|
||||
let bytes = content.as_bytes();
|
||||
let remaining = &bytes[handle.offset..];
|
||||
|
||||
if offset >= bytes.len() as u64 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let start = offset as usize;
|
||||
let remaining = &bytes[start..];
|
||||
let to_copy = remaining.len().min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&remaining[..to_copy]);
|
||||
handle.offset += to_copy;
|
||||
Ok(Some(to_copy))
|
||||
Ok(to_copy)
|
||||
}
|
||||
|
||||
fn write(&mut self, id: usize, _buf: &[u8]) -> Result<Option<usize>> {
|
||||
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
fn write(
|
||||
&mut self,
|
||||
id: usize,
|
||||
_buf: &[u8],
|
||||
_offset: u64,
|
||||
_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> Result<usize> {
|
||||
let _kind = self.kind_for_id(id)?;
|
||||
Err(Error::new(EROFS))
|
||||
}
|
||||
|
||||
fn seek(&mut self, id: usize, pos: isize, whence: usize) -> Result<Option<isize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or(Error::new(EBADF))?;
|
||||
let len = match &handle.kind {
|
||||
HandleKind::Root => self.devices.len() * 20,
|
||||
HandleKind::Device(idx) => format_device_info(&self.devices[*idx]).len(),
|
||||
fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result<usize> {
|
||||
let kind = self.kind_for_id(id)?;
|
||||
let path = self.path_for_handle(&kind)?;
|
||||
let bytes = path.as_bytes();
|
||||
let to_copy = bytes.len().min(buf.len());
|
||||
buf[..to_copy].copy_from_slice(&bytes[..to_copy]);
|
||||
Ok(to_copy)
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut syscall::Stat, _ctx: &CallerCtx) -> Result<()> {
|
||||
let kind = self.kind_for_id(id)?;
|
||||
let size = self.content_for_handle(&kind)?.len() as u64;
|
||||
|
||||
stat.st_mode = if Self::is_directory(&kind) {
|
||||
MODE_DIR | 0o555
|
||||
} else {
|
||||
MODE_FILE | 0o444
|
||||
};
|
||||
let new_offset = match whence {
|
||||
SEEK_SET => pos as isize,
|
||||
SEEK_CUR => handle.offset as isize + pos,
|
||||
SEEK_END => len as isize + pos,
|
||||
_ => return Err(Error::new(EINVAL)),
|
||||
};
|
||||
if new_offset < 0 {
|
||||
return Err(Error::new(EINVAL));
|
||||
stat.st_size = size;
|
||||
stat.st_blocks = size.div_ceil(512);
|
||||
stat.st_blksize = 4096;
|
||||
stat.st_nlink = 1;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> {
|
||||
let _kind = self.kind_for_id(id)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result<usize> {
|
||||
let _kind = self.kind_for_id(id)?;
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
fn fsize(&mut self, id: usize, _ctx: &CallerCtx) -> Result<u64> {
|
||||
let kind = self.kind_for_id(id)?;
|
||||
Ok(self.content_for_handle(&kind)?.len() as u64)
|
||||
}
|
||||
|
||||
fn ftruncate(&mut self, id: usize, _len: u64, _ctx: &CallerCtx) -> Result<()> {
|
||||
let _kind = self.kind_for_id(id)?;
|
||||
Err(Error::new(EROFS))
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result<EventFlags> {
|
||||
let _kind = self.kind_for_id(id)?;
|
||||
Ok(EventFlags::empty())
|
||||
}
|
||||
|
||||
fn on_close(&mut self, id: usize) {
|
||||
if id != SCHEME_ROOT_ID {
|
||||
self.handles.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn path_exists(path: &str) -> bool {
|
||||
std::fs::metadata(path).is_ok()
|
||||
}
|
||||
|
||||
fn parse_pci_slot(name: &str) -> Option<(u8, u8, u8)> {
|
||||
let mut parts = name.split('.');
|
||||
let bus = parts.next()?.parse::<u8>().ok()?;
|
||||
let dev = parts.next()?.parse::<u8>().ok()?;
|
||||
let func = parts.next()?.parse::<u8>().ok()?;
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
Some((bus, dev, func))
|
||||
}
|
||||
|
||||
fn basename(path: &str) -> Option<String> {
|
||||
path.rsplit('/').next().and_then(|part| {
|
||||
if part.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(part.to_string())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn gpu_priority(dev: &DeviceInfo) -> u8 {
|
||||
match dev.vendor_id {
|
||||
0x1002 => 0,
|
||||
0x8086 => 1,
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
fn input_priority(dev: &DeviceInfo) -> u8 {
|
||||
if dev.is_input_keyboard() {
|
||||
0
|
||||
} else {
|
||||
match dev.input_kind {
|
||||
Some(InputKind::Mouse) => 1,
|
||||
Some(InputKind::Generic) | None => 2,
|
||||
Some(InputKind::Keyboard) => 0,
|
||||
}
|
||||
handle.offset = new_offset as usize;
|
||||
Ok(Some(new_offset))
|
||||
}
|
||||
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
match &handle.kind {
|
||||
HandleKind::Root => {
|
||||
stat.st_mode = MODE_DIR | 0o555;
|
||||
}
|
||||
HandleKind::Device(_) => {
|
||||
stat.st_mode = MODE_FILE | 0o444;
|
||||
}
|
||||
}
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
|
||||
let _ = self.handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
Ok(Some(EventFlags::empty()))
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
self.handles.remove(&id);
|
||||
Ok(Some(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,10 +83,22 @@ mkdir -p recipes/gpu
|
||||
symlink "../../local/recipes/gpu/amdgpu" "recipes/gpu/amdgpu"
|
||||
symlink "../../local/recipes/gpu/redox-drm" "recipes/gpu/redox-drm"
|
||||
|
||||
# Library stubs / custom libs
|
||||
mkdir -p recipes/libs
|
||||
symlink "../../local/recipes/libs/libepoxy-stub" "recipes/libs/libepoxy-stub"
|
||||
symlink "../../local/recipes/libs/libudev-stub" "recipes/libs/libudev-stub"
|
||||
symlink "../../local/recipes/libs/lcms2-stub" "recipes/libs/lcms2-stub"
|
||||
symlink "../../local/recipes/libs/libdisplay-info-stub" "recipes/libs/libdisplay-info-stub"
|
||||
symlink "../../local/recipes/libs/libxcvt-stub" "recipes/libs/libxcvt-stub"
|
||||
|
||||
# System
|
||||
mkdir -p recipes/system
|
||||
symlink "../../local/recipes/system/cub" "recipes/system/cub"
|
||||
symlink "../../local/recipes/system/evdevd" "recipes/system/evdevd"
|
||||
symlink "../../local/recipes/system/firmware-loader" "recipes/system/firmware-loader"
|
||||
symlink "../../local/recipes/system/iommu" "recipes/system/iommu"
|
||||
symlink "../../local/recipes/system/redbear-hwutils" "recipes/system/redbear-hwutils"
|
||||
symlink "../../local/recipes/system/redbear-netctl" "recipes/system/redbear-netctl"
|
||||
symlink "../../local/recipes/system/redbear-meta" "recipes/system/redbear-meta"
|
||||
symlink "../../local/recipes/system/udev-shim" "recipes/system/udev-shim"
|
||||
|
||||
|
||||
@@ -133,13 +133,6 @@ echo "Root: ${PROJECT_ROOT##*/}"
|
||||
echo "Tag: $REDBEAR_TAG"
|
||||
echo ""
|
||||
|
||||
section "Ensuring local recipe aliases..."
|
||||
if [ ! -e "local/recipes/system/rbos-info" ] && [ -d "local/recipes/system/redbear-info" ]; then
|
||||
symlink "redbear-info" "local/recipes/system/rbos-info"
|
||||
fi
|
||||
status "Local recipe aliases ready"
|
||||
echo ""
|
||||
|
||||
section "Ensuring custom recipe symlinks..."
|
||||
symlink "../../local/recipes/branding/redbear-release" "recipes/branding/redbear-release"
|
||||
symlink "../../local/recipes/drivers/linux-kpi" "recipes/drivers/linux-kpi"
|
||||
@@ -148,8 +141,9 @@ symlink "../../local/recipes/gpu/amdgpu" "recipes/gpu/amdgpu"
|
||||
symlink "../../local/recipes/gpu/redox-drm" "recipes/gpu/redox-drm"
|
||||
symlink "../../local/recipes/system/evdevd" "recipes/system/evdevd"
|
||||
symlink "../../local/recipes/system/firmware-loader" "recipes/system/firmware-loader"
|
||||
symlink "../../local/recipes/system/rbos-info" "recipes/system/rbos-info"
|
||||
symlink "../../local/recipes/system/redbear-info" "recipes/system/redbear-info"
|
||||
symlink "../../local/recipes/system/redbear-hwutils" "recipes/system/redbear-hwutils"
|
||||
symlink "../../local/recipes/system/redbear-netctl" "recipes/system/redbear-netctl"
|
||||
symlink "../../local/recipes/system/redbear-meta" "recipes/system/redbear-meta"
|
||||
symlink "../../local/recipes/system/udev-shim" "recipes/system/udev-shim"
|
||||
symlink "../../local/recipes/core/ext4d" "recipes/core/ext4d"
|
||||
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# test-iommu-qemu.sh - Launch QEMU with AMD IOMMU device for hardware testing
|
||||
#
|
||||
# This wrapper adds the AMD IOMMU device to QEMU for testing IOMMU/vIOMMU
|
||||
# functionality on AMD hardware. It forwards any additional QEMU flags to the
|
||||
# make qemu invocation.
|
||||
#
|
||||
# Usage:
|
||||
# ./local/scripts/test-iommu-qemu.sh [--help]
|
||||
# ./local/scripts/test-iommu-qemu.sh [extra QEMU flags...]
|
||||
#
|
||||
# Examples:
|
||||
# ./local/scripts/test-iommu-qemu.sh # Basic IOMMU test
|
||||
# ./local/scripts/test-iommu-qemu.sh -display sdl # With SDL display
|
||||
# ./local/scripts/test-iommu-qemu.sh -m 4G # With 4GB RAM
|
||||
|
||||
set -e
|
||||
|
||||
# Print usage information
|
||||
usage() {
|
||||
cat << USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Launch QEMU with AMD IOMMU device for hardware testing.
|
||||
|
||||
Options:
|
||||
--help Show this help message
|
||||
|
||||
Any additional arguments are passed as extra QEMU flags.
|
||||
|
||||
Environment:
|
||||
QEMUFLAGS Additional flags (prepended to device amd-iommu)
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") -display sdl -m 4G
|
||||
QEMUFLAGS="-smp 8" $(basename "$0")
|
||||
|
||||
USAGE
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Parse --help before anything else
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--help|-h|help)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Trap to handle Ctrl+C gracefully
|
||||
# Kill any background QEMU process if interrupted
|
||||
cleanup() {
|
||||
echo "Interrupted, cleaning up..."
|
||||
# The make qemu process will be killed by the signal
|
||||
exit 130
|
||||
}
|
||||
trap cleanup SIGINT SIGTERM
|
||||
|
||||
# Build QEMUFLAGS with AMD IOMMU device
|
||||
# Prepend user QEMUFLAGS if set, then add the amd-iommu device
|
||||
IOMMU_FLAGS="-device amd-iommu"
|
||||
if [[ -n "${QEMUFLAGS:-}" ]]; then
|
||||
QEMUFLAGS="${QEMUFLAGS} ${IOMMU_FLAGS} $@"
|
||||
else
|
||||
QEMUFLAGS="${IOMMU_FLAGS} $@"
|
||||
fi
|
||||
|
||||
# Launch QEMU via make
|
||||
exec make qemu QEMUFLAGS="$QEMUFLAGS"
|
||||
@@ -68,7 +68,7 @@ esac
|
||||
rm -rf "${COOKBOOK_BUILD}/initfs"
|
||||
mkdir -p "${COOKBOOK_BUILD}/initfs/lib/init.d"
|
||||
|
||||
cp "${COOKBOOK_SOURCE}/init.d"/* "${COOKBOOK_BUILD}/initfs/lib/init.d/"
|
||||
cp "${COOKBOOK_SOURCE}/init.initfs.d"/* "${COOKBOOK_BUILD}/initfs/lib/init.d/"
|
||||
|
||||
mkdir -pv "${COOKBOOK_BUILD}/initfs/lib/pcid.d"
|
||||
cp -v "${COOKBOOK_SOURCE}/drivers/initfs.toml" "${COOKBOOK_BUILD}/initfs/lib/pcid.d/initfs.toml"
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
[source]
|
||||
git = "https://gitlab.redox-os.org/redox-os/base.git"
|
||||
patches = ["redox.patch"]
|
||||
|
||||
[build]
|
||||
template = "custom"
|
||||
script = """
|
||||
mkdir -pv "${COOKBOOK_STAGE}/usr/bin"
|
||||
for package in audiod ipcd ptyd; do
|
||||
for package in audiod ipcd ptyd dhcpd; do
|
||||
"${COOKBOOK_CARGO}" build \
|
||||
--manifest-path "${COOKBOOK_SOURCE}/${package}/Cargo.toml" \
|
||||
${build_flags}
|
||||
@@ -73,4 +74,7 @@ do
|
||||
driver="$(basename "$(dirname "$conf")")"
|
||||
cp -v "$conf" "${COOKBOOK_STAGE}/lib/pcid.d/$driver.toml"
|
||||
done
|
||||
|
||||
mkdir -pv "${COOKBOOK_STAGE}/usr/lib/init.d"
|
||||
cp -v "${COOKBOOK_SOURCE}/init.d"/* "${COOKBOOK_STAGE}/usr/lib/init.d/"
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[source]
|
||||
git = "https://gitlab.redox-os.org/redox-os/installer.git"
|
||||
patches = ["redox.patch"]
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[source]
|
||||
git = "https://gitlab.redox-os.org/redox-os/kernel.git"
|
||||
patches = ["redox.patch"]
|
||||
|
||||
[build]
|
||||
template = "custom"
|
||||
|
||||
@@ -18,6 +18,12 @@ COOKBOOK_CONFIGURE_FLAGS+=(
|
||||
--enable-static-link # This ensures loadables are not built, which will fail
|
||||
)
|
||||
COOKBOOK_MAKE_JOBS=1 # workaround for parallel make bugs
|
||||
|
||||
# mkbuiltins.c uses K&R-style empty parameter declarations (e.g. `static char *xmalloc ();`).
|
||||
# GCC 15+ defaults to C23 where () means (void), breaking calls with arguments.
|
||||
# Force C17 to restore the old behavior for the host-built code generator.
|
||||
export CFLAGS_FOR_BUILD="-g -O2 -std=gnu17"
|
||||
|
||||
cookbook_configure
|
||||
ln -s "bash" "${COOKBOOK_STAGE}/usr/bin/sh"
|
||||
cp -r "${COOKBOOK_RECIPE}/etc" "${COOKBOOK_STAGE}/etc"
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../local/recipes/system/iommu
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../local/recipes/system/rbos-info
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../local/recipes/system/redbear-hwutils
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../local/recipes/system/redbear-netctl
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Build Red Bear OS live ISO
|
||||
# Usage: ./scripts/build-iso.sh [CONFIG_NAME] [ARCH]
|
||||
# CONFIG_NAME - build config (default: redbear-full)
|
||||
# ARCH - target architecture (default: x86_64)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONFIG_NAME="${1:-redbear-full}"
|
||||
ARCH="${2:-x86_64}"
|
||||
|
||||
echo "Building Red Bear OS ISO"
|
||||
echo " config: ${CONFIG_NAME}"
|
||||
echo " arch: ${ARCH}"
|
||||
|
||||
make live CONFIG_NAME="${CONFIG_NAME}" ARCH="${ARCH}"
|
||||
|
||||
echo ""
|
||||
echo "Done: redbear-live.iso"
|
||||
+296
-18
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# fetch-all-sources.sh — Download ALL Redox OS + Red Bear OS package sources.
|
||||
#
|
||||
# Smart re-download: skips sources whose local checksum matches the recipe's
|
||||
# blake3. Falls back to file-size comparison when no blake3 is recorded.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/fetch-all-sources.sh # Fetch for default desktop config
|
||||
# ./scripts/fetch-all-sources.sh redbear-full # Fetch for a specific config
|
||||
@@ -8,9 +11,11 @@
|
||||
# ./scripts/fetch-all-sources.sh --recipe kernel # Fetch a single recipe
|
||||
# ./scripts/fetch-all-sources.sh --list # List recipes that would be fetched
|
||||
# ./scripts/fetch-all-sources.sh --status # Show which sources already exist
|
||||
# ./scripts/fetch-all-sources.sh --preflight # Smart checksum/size check (no download)
|
||||
#
|
||||
# Prerequisites: rustup + nightly, git, wget, tar. The script builds the
|
||||
# cookbook `repo` binary if not already built.
|
||||
# Prerequisites: rustup + nightly, git, wget, tar, curl, b3sum.
|
||||
# The script builds the cookbook `repo` binary if not already built.
|
||||
# If b3sum is not installed, it will be installed via cargo.
|
||||
#
|
||||
# Sources are placed in recipes/<category>/<name>/source/ for git/tar recipes,
|
||||
# and are left in-place for local/recipes/ (path-based sources).
|
||||
@@ -25,6 +30,14 @@ REPO_BIN="./target/release/repo"
|
||||
CONFIG_NAME="${1:-desktop}"
|
||||
ACTION="fetch"
|
||||
|
||||
# ── Colors (disabled when not a terminal) ───────────────────────────
|
||||
if [ -t 1 ]; then
|
||||
C_GREEN="\033[0;32m" C_YELLOW="\033[0;33m" C_RED="\033[0;31m"
|
||||
C_CYAN="\033[0;36m" C_BOLD="\033[1m" C_RESET="\033[0m"
|
||||
else
|
||||
C_GREEN="" C_YELLOW="" C_RED="" C_CYAN="" C_BOLD="" C_RESET=""
|
||||
fi
|
||||
|
||||
# ── Argument parsing ────────────────────────────────────────────────
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTIONS] [CONFIG_NAME]"
|
||||
@@ -36,6 +49,8 @@ usage() {
|
||||
echo " --recipe NAME Fetch a single recipe by name"
|
||||
echo " --list List recipes that would be fetched (no download)"
|
||||
echo " --status Show which sources already exist locally"
|
||||
echo " --preflight Smart blake3/size check — show what needs updating"
|
||||
echo " --force Force re-download even if checksums match"
|
||||
echo " --help Show this help"
|
||||
echo ""
|
||||
echo "Configs: desktop, redbear-full, redbear-minimal, server, minimal, wayland, x11"
|
||||
@@ -44,6 +59,7 @@ usage() {
|
||||
|
||||
ALL_CONFIGS=0
|
||||
SINGLE_RECIPE=""
|
||||
FORCE_FETCH=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--all-configs)
|
||||
@@ -62,6 +78,14 @@ while [[ $# -gt 0 ]]; do
|
||||
ACTION="status"
|
||||
shift
|
||||
;;
|
||||
--preflight)
|
||||
ACTION="preflight"
|
||||
shift
|
||||
;;
|
||||
--force)
|
||||
FORCE_FETCH=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
@@ -99,6 +123,203 @@ resolve_config() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Checksum / size utilities ───────────────────────────────────────
|
||||
|
||||
# Ensure b3sum is available
|
||||
ensure_b3sum() {
|
||||
if ! command -v b3sum &>/dev/null; then
|
||||
echo " Installing b3sum (blake3 CLI tool)..."
|
||||
cargo install b3sum 2>&1 | tail -1
|
||||
if ! command -v b3sum &>/dev/null; then
|
||||
echo " WARNING: b3sum not available. Size-based fallback only."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Compute blake3 of a file (returns empty string if b3sum unavailable)
|
||||
compute_blake3() {
|
||||
local file="$1"
|
||||
if command -v b3sum &>/dev/null && [ -f "$file" ]; then
|
||||
b3sum --no-names "$file" | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
# Get remote file size via HTTP HEAD (follows redirects)
|
||||
get_remote_size() {
|
||||
local url="$1"
|
||||
# -sI: silent, HEAD only, -L: follow redirects, --max-time: timeout
|
||||
curl -sI -L --max-time 15 "$url" 2>/dev/null \
|
||||
| grep -i '^content-length:' \
|
||||
| tail -1 \
|
||||
| awk '{print $2}' \
|
||||
| tr -d '\r\n'
|
||||
}
|
||||
|
||||
# Get local file size (portable across Linux/macOS)
|
||||
get_local_size() {
|
||||
local file="$1"
|
||||
if [ -f "$file" ]; then
|
||||
stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
# ── TOML field extraction (simple, no dependencies) ─────────────────
|
||||
|
||||
# Extract a quoted string field from recipe.toml: field = "value"
|
||||
recipe_str_field() {
|
||||
local file="$1" field="$2"
|
||||
grep "^${field} *= *\"" "$file" 2>/dev/null | head -1 | sed 's/^[^"]*"\([^"]*\)".*/\1/'
|
||||
}
|
||||
|
||||
# ── Per-recipe smart check ──────────────────────────────────────────
|
||||
#
|
||||
# Returns one of: "cached" | "missing" | "mismatch" | "no-checksum"
|
||||
# Prints reason to stdout.
|
||||
|
||||
check_recipe_source() {
|
||||
local recipe_dir="$1"
|
||||
local recipe_toml="$recipe_dir/recipe.toml"
|
||||
local source_dir="$recipe_dir/source"
|
||||
local source_tar="$recipe_dir/source.tar"
|
||||
|
||||
# No recipe file
|
||||
[ -f "$recipe_toml" ] || { echo "no-recipe"; return; }
|
||||
|
||||
# Path-based sources — always cached
|
||||
if grep -q '^path *= *"source"' "$recipe_toml" 2>/dev/null; then
|
||||
echo "cached:path"
|
||||
return
|
||||
fi
|
||||
|
||||
# ── Tar source ──────────────────────────────────────────────
|
||||
local tar_url
|
||||
tar_url=$(recipe_str_field "$recipe_toml" "tar")
|
||||
if [ -n "$tar_url" ]; then
|
||||
# No local tar at all
|
||||
if [ ! -f "$source_tar" ]; then
|
||||
# source dir might exist from a previous extract — check blake3 of
|
||||
# the recipe against nothing: we just need to download
|
||||
echo "missing"
|
||||
return
|
||||
fi
|
||||
|
||||
# Tar exists — check blake3
|
||||
local blake3_expected
|
||||
blake3_expected=$(recipe_str_field "$recipe_toml" "blake3")
|
||||
|
||||
if [ -n "$blake3_expected" ]; then
|
||||
local blake3_local
|
||||
blake3_local=$(compute_blake3 "$source_tar")
|
||||
if [ -n "$blake3_local" ] && [ "$blake3_local" = "$blake3_expected" ]; then
|
||||
echo "cached:blake3"
|
||||
return
|
||||
else
|
||||
echo "mismatch:blake3"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# No blake3 in recipe — fall back to size comparison
|
||||
local local_size remote_size
|
||||
local_size=$(get_local_size "$source_tar")
|
||||
remote_size=$(get_remote_size "$tar_url")
|
||||
|
||||
if [ -n "$remote_size" ] && [ -n "$local_size" ] && [ "$local_size" = "$remote_size" ]; then
|
||||
echo "cached:size"
|
||||
return
|
||||
else
|
||||
echo "mismatch:size"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Git source ──────────────────────────────────────────────
|
||||
if grep -q '^git *= *"' "$recipe_toml" 2>/dev/null; then
|
||||
if [ -d "$source_dir/.git" ]; then
|
||||
echo "cached:git"
|
||||
return
|
||||
elif [ -d "$source_dir" ]; then
|
||||
echo "cached:git-dir"
|
||||
return
|
||||
else
|
||||
echo "missing"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── same_as source ──────────────────────────────────────────
|
||||
if grep -q '^same_as *= *"' "$recipe_toml" 2>/dev/null; then
|
||||
echo "cached:same_as"
|
||||
return
|
||||
fi
|
||||
|
||||
# Unknown — let repo handle it
|
||||
echo "missing"
|
||||
}
|
||||
|
||||
# ── Preflight: scan all recipes and report status ───────────────────
|
||||
|
||||
preflight_scan() {
|
||||
local label="${1:-all recipes}"
|
||||
local total=0 cached=0 missing=0 mismatch=0 no_checksum=0
|
||||
local missing_list=() mismatch_list=()
|
||||
|
||||
echo ""
|
||||
printf "${C_BOLD}==> Smart preflight scan: %s${C_RESET}\n" "$label"
|
||||
echo " Checking blake3 checksums and file sizes..."
|
||||
echo ""
|
||||
|
||||
while IFS= read -r recipe_toml; do
|
||||
local recipe_dir recipe_name category
|
||||
recipe_dir="$(dirname "$recipe_toml")"
|
||||
recipe_name="$(basename "$recipe_dir")"
|
||||
category="$(basename "$(dirname "$recipe_dir")")"
|
||||
|
||||
# Skip recipes without a [source] section
|
||||
grep -q '^\[source\]' "$recipe_toml" 2>/dev/null || continue
|
||||
|
||||
total=$((total + 1))
|
||||
local status reason
|
||||
status=$(check_recipe_source "$recipe_dir")
|
||||
reason="${status#*:}"
|
||||
status="${status%%:*}"
|
||||
|
||||
case "$status" in
|
||||
cached)
|
||||
cached=$((cached + 1))
|
||||
;;
|
||||
missing)
|
||||
missing=$((missing + 1))
|
||||
printf " ${C_YELLOW}MISSING %-30s %s${C_RESET}\n" "$category/$recipe_name" "$reason"
|
||||
missing_list+=("$category/$recipe_name")
|
||||
;;
|
||||
mismatch)
|
||||
mismatch=$((mismatch + 1))
|
||||
printf " ${C_RED}CHANGED %-30s %s${C_RESET}\n" "$category/$recipe_name" "$reason"
|
||||
mismatch_list+=("$category/$recipe_name")
|
||||
;;
|
||||
*)
|
||||
# no-recipe, same_as, etc. — skip
|
||||
;;
|
||||
esac
|
||||
done < <(find recipes local/recipes -name "recipe.toml" -not -path "*/source/*" 2>/dev/null | sort)
|
||||
|
||||
echo ""
|
||||
printf " ${C_BOLD}Total recipes:${C_RESET} %3d\n" "$total"
|
||||
printf " ${C_GREEN}Cached (skip):${C_RESET} %3d\n" "$cached"
|
||||
printf " ${C_YELLOW}Missing:${C_RESET} %3d\n" "$missing"
|
||||
printf " ${C_RED}Changed:${C_RESET} %3d\n" "$mismatch"
|
||||
echo ""
|
||||
|
||||
if [ "$((missing + mismatch))" -eq 0 ]; then
|
||||
printf " ${C_GREEN}✓ All sources are up to date.${C_RESET}\n"
|
||||
return 1 # nothing to do
|
||||
else
|
||||
printf " ${C_BOLD}%d source(s) need downloading.${C_RESET}\n" "$((missing + mismatch))"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Fetch sources for a config ──────────────────────────────────────
|
||||
fetch_for_config() {
|
||||
local config_name="$1"
|
||||
@@ -120,6 +341,21 @@ fetch_for_config() {
|
||||
# ── Fetch a single recipe ──────────────────────────────────────────
|
||||
fetch_single_recipe() {
|
||||
local recipe_name="$1"
|
||||
|
||||
# Find recipe directory
|
||||
local recipe_dir=""
|
||||
for d in $(find recipes local/recipes -maxdepth 2 -name "$recipe_name" -type d 2>/dev/null); do
|
||||
if [ -f "$d/recipe.toml" ]; then
|
||||
recipe_dir="$d"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$recipe_dir" ]; then
|
||||
echo "ERROR: recipe '$recipe_name' not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Fetching single recipe: $recipe_name"
|
||||
echo ""
|
||||
@@ -150,12 +386,25 @@ list_for_config() {
|
||||
|
||||
# ── Status: show which sources exist ────────────────────────────────
|
||||
show_status() {
|
||||
echo "==> Source status for all recipes"
|
||||
local config_filter="${1:-}"
|
||||
echo "==> Source status${config_filter:+ for config: $config_filter}"
|
||||
echo ""
|
||||
|
||||
local total=0 fetched=0 local_src=0 missing=0
|
||||
|
||||
while IFS= read -r recipe_toml; do
|
||||
local recipe_list
|
||||
if [ -n "$config_filter" ] && [ -x "$REPO_BIN" ]; then
|
||||
local config_file
|
||||
config_file="$(resolve_config "$config_filter")" 2>/dev/null || {
|
||||
config_filter=""
|
||||
}
|
||||
if [ -n "$config_filter" ]; then
|
||||
recipe_list=$("$REPO_BIN" cook-tree "--filesystem=$config_file" --with-package-deps 2>/dev/null | grep -v '^=' | grep -v '^$')
|
||||
fi
|
||||
fi
|
||||
|
||||
check_one_recipe() {
|
||||
local recipe_toml="$1"
|
||||
recipe_dir="$(dirname "$recipe_toml")"
|
||||
recipe_name="$(basename "$recipe_dir")"
|
||||
category="$(basename "$(dirname "$recipe_dir")")"
|
||||
@@ -163,28 +412,44 @@ show_status() {
|
||||
total=$((total + 1))
|
||||
|
||||
if [ -d "$recipe_dir/source" ]; then
|
||||
# Check if it's a symlink (local recipe)
|
||||
if [ -L "$recipe_dir/source" ] || grep -q '^path = "source"' "$recipe_toml" 2>/dev/null; then
|
||||
if [ -L "$recipe_dir/source" ] || grep -q '^path *= *"source"' "$recipe_toml" 2>/dev/null; then
|
||||
local_src=$((local_src + 1))
|
||||
else
|
||||
fetched=$((fetched + 1))
|
||||
fi
|
||||
else
|
||||
# Check if source section exists
|
||||
if grep -q '^\[source\]' "$recipe_toml" 2>/dev/null; then
|
||||
missing=$((missing + 1))
|
||||
echo " MISSING $category/$recipe_name"
|
||||
fi
|
||||
fi
|
||||
done < <(find recipes -name "recipe.toml" -not -path "*/source/*" | sort)
|
||||
}
|
||||
|
||||
# Also check local recipes
|
||||
while IFS= read -r recipe_toml; do
|
||||
recipe_dir="$(dirname "$recipe_toml")"
|
||||
recipe_name="$(basename "$recipe_dir")"
|
||||
total=$((total + 1))
|
||||
local_src=$((local_src + 1))
|
||||
done < <(find local/recipes -name "recipe.toml" -not -path "*/source/*" 2>/dev/null | sort)
|
||||
if [ -n "${recipe_list:-}" ]; then
|
||||
while IFS= read -r recipe_name; do
|
||||
local found=0
|
||||
while IFS= read -r recipe_toml; do
|
||||
check_one_recipe "$recipe_toml"
|
||||
found=1
|
||||
break
|
||||
done < <(find recipes local/recipes -path "*/${recipe_name}/recipe.toml" -not -path "*/source/*" 2>/dev/null | head -1)
|
||||
if [ "$found" -eq 0 ]; then
|
||||
total=$((total + 1))
|
||||
missing=$((missing + 1))
|
||||
echo " MISSING $recipe_name (no recipe.toml found)"
|
||||
fi
|
||||
done <<< "$recipe_list"
|
||||
else
|
||||
while IFS= read -r recipe_toml; do
|
||||
check_one_recipe "$recipe_toml"
|
||||
done < <(find recipes -name "recipe.toml" -not -path "*/source/*" | sort)
|
||||
|
||||
while IFS= read -r recipe_toml; do
|
||||
recipe_dir="$(dirname "$recipe_toml")"
|
||||
total=$((total + 1))
|
||||
local_src=$((local_src + 1))
|
||||
done < <(find local/recipes -name "recipe.toml" -not -path "*/source/*" 2>/dev/null | sort)
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Total recipes: $total"
|
||||
@@ -201,9 +466,22 @@ show_status() {
|
||||
|
||||
# ── Main ────────────────────────────────────────────────────────────
|
||||
|
||||
# Ensure b3sum is available for checksum-based checking
|
||||
ensure_b3sum
|
||||
|
||||
case "$ACTION" in
|
||||
status)
|
||||
show_status
|
||||
show_status ""
|
||||
;;
|
||||
preflight)
|
||||
build_repo
|
||||
if [ "$ALL_CONFIGS" -eq 1 ]; then
|
||||
for cfg in desktop redbear-full redbear-minimal server minimal wayland x11; do
|
||||
preflight_scan "$cfg" || true
|
||||
done
|
||||
else
|
||||
preflight_scan "$CONFIG_NAME"
|
||||
fi
|
||||
;;
|
||||
list)
|
||||
build_repo
|
||||
@@ -231,11 +509,11 @@ case "$ACTION" in
|
||||
done
|
||||
echo ""
|
||||
echo "==> All sources fetched. Summary:"
|
||||
show_status
|
||||
show_status ""
|
||||
else
|
||||
fetch_for_config "$CONFIG_NAME"
|
||||
echo ""
|
||||
show_status
|
||||
show_status "$CONFIG_NAME"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
CONFIG_NAME="redbear-full"
|
||||
ARCH="$(uname -m)"
|
||||
BUILD=0
|
||||
QEMU_EXTRA_ARGS=()
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
|
||||
Red Bear OS — build and run in QEMU.
|
||||
|
||||
Options:
|
||||
-b, --build Build full OS before running
|
||||
-c, --config NAME Config name (default: redbear-full)
|
||||
-a, --arch ARCH Target architecture (default: host arch)
|
||||
-- ARGS Pass remaining args to make qemu (e.g. -- QEMUFLAGS="-m 8G")
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # Run existing image
|
||||
$(basename "$0") --build # Build + run
|
||||
$(basename "$0") -b -c redbear-minimal # Build minimal + run
|
||||
$(basename "$0") -- QEMUFLAGS="-m 8G" # Run with 8G RAM
|
||||
$(basename "$0") -b -- serial=yes # Build + run with serial console
|
||||
$(basename "$0") -b -- gpu=virtio kvm=no # Build + run with virtio GPU, no KVM
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-b|--build) BUILD=1 ;;
|
||||
-c|--config) CONFIG_NAME="$2"; shift ;;
|
||||
-a|--arch) ARCH="$2"; shift ;;
|
||||
-h|--help) usage ;;
|
||||
--) shift; QEMU_EXTRA_ARGS=("$@"); break ;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
if [ "$BUILD" -eq 1 ]; then
|
||||
echo "==> Ensuring .config is set for native build..."
|
||||
if ! grep -q 'PODMAN_BUILD?=0' .config 2>/dev/null; then
|
||||
echo 'PODMAN_BUILD?=0' > .config
|
||||
fi
|
||||
|
||||
echo "==> Applying Red Bear OS patches..."
|
||||
if [ -f local/scripts/apply-patches.sh ]; then
|
||||
bash local/scripts/apply-patches.sh
|
||||
fi
|
||||
|
||||
echo "==> Building cookbook..."
|
||||
cargo build --release
|
||||
|
||||
echo "==> Building Red Bear OS ($CONFIG_NAME, $ARCH)..."
|
||||
CI=1 make all "CONFIG_NAME=$CONFIG_NAME" ARCH="$ARCH"
|
||||
echo "==> Build complete."
|
||||
fi
|
||||
|
||||
BUILD_DIR="build/$ARCH/$CONFIG_NAME"
|
||||
if [ ! -f "$BUILD_DIR/harddrive.img" ]; then
|
||||
echo "ERROR: $BUILD_DIR/harddrive.img not found. Run with --build first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Launching Red Bear OS in QEMU ($CONFIG_NAME, $ARCH)..."
|
||||
echo ""
|
||||
|
||||
exec make qemu "CONFIG_NAME=$CONFIG_NAME" ARCH="$ARCH" "${QEMU_EXTRA_ARGS[@]}"
|
||||
Reference in New Issue
Block a user