Advance redbear-full Wayland, greeter, and Qt integration
Consolidate the active desktop path around redbear-full while landing the greeter/session stack and the runtime fixes needed to keep Wayland and KWin bring-up moving forward.
This commit is contained in:
+98
-59
@@ -11,11 +11,20 @@ Red Bear OS relates to Redox OS in the same way Ubuntu relates to Debian:
|
||||
- The `local/` directory is our overlay — untouched by upstream updates
|
||||
- First-class configs use `redbear-*` naming (not `my-*`, which is gitignored)
|
||||
|
||||
## FREE/LIBRE SOFTWARE POLICY
|
||||
|
||||
Red Bear OS must remain a free/libre project.
|
||||
|
||||
- Prefer components that are open-source, freely available to all users, or built in-tree by Red Bear.
|
||||
- Do not introduce proprietary, source-unavailable, paywalled, or redistributability-restricted dependencies into the tracked system surface.
|
||||
- When a dependency is dual-licensed under multiple free/open licenses, choose and document the option that is compatible with the Red Bear project surface.
|
||||
- For the greeter/login stack specifically, the current SHA-crypt verifier path is the pure-Rust `sha-crypt` crate, licensed `MIT OR Apache-2.0`; Red Bear treats it under the MIT option for compatibility with the project's free-software policy.
|
||||
|
||||
Build flow:
|
||||
```
|
||||
make all CONFIG_NAME=redbear-kde
|
||||
→ mk/config.mk resolves to config/redbear-kde.toml
|
||||
→ Config includes desktop.toml (mainline) + Red Bear packages
|
||||
make all CONFIG_NAME=redbear-full
|
||||
→ mk/config.mk resolves to the active desktop/graphics compile target
|
||||
→ Desktop/graphics are available only on redbear-full and redbear-live-full
|
||||
→ repo cook builds all packages including our custom ones
|
||||
→ mk/disk.mk creates harddrive.img with Red Bear branding
|
||||
```
|
||||
@@ -23,9 +32,24 @@ make all CONFIG_NAME=redbear-kde
|
||||
Update flow:
|
||||
```
|
||||
./local/scripts/sync-upstream.sh # Rebase onto upstream Redox + verify symlinks
|
||||
make all CONFIG_NAME=redbear-kde # Rebuild with latest
|
||||
make all CONFIG_NAME=redbear-full # Rebuild the active desktop/graphics target
|
||||
```
|
||||
|
||||
## ACTIVE COMPILE TARGETS
|
||||
|
||||
The supported compile targets are exactly:
|
||||
|
||||
- `redbear-mini`
|
||||
- `redbear-live-mini`
|
||||
- `redbear-full`
|
||||
- `redbear-live-full`
|
||||
|
||||
Desktop/graphics are available only on `redbear-full` and `redbear-live-full`.
|
||||
|
||||
Names such as `redbear-kde`, `redbear-wayland`, and `redbear-minimal` may still appear in older
|
||||
docs, legacy validation notes, or in-repo staging configs, but they should not be treated as the
|
||||
current supported compile targets.
|
||||
|
||||
## TRACKING UPSTREAM (SYNC WITH REDOX OS)
|
||||
|
||||
Red Bear OS tracks the Redox OS build system as upstream. The `local/` directory
|
||||
@@ -133,9 +157,10 @@ redox-master/ ← git pull updates mainline Redox
|
||||
├── config/
|
||||
│ ├── desktop.toml ← mainline configs (untouched)
|
||||
│ ├── minimal.toml
|
||||
│ ├── redbear-desktop.toml ← RED BEAR OS configs (first-class, tracked)
|
||||
│ ├── redbear-minimal.toml
|
||||
│ └── redbear-live.toml
|
||||
│ ├── redbear-full.toml ← Active desktop/graphics target
|
||||
│ ├── redbear-live-full.toml ← Live desktop/graphics target
|
||||
│ ├── redbear-mini*.toml ← Minimal target surface (legacy/staging naming may still vary in-tree)
|
||||
│ └── redbear-greeter-services.toml ← Greeter/auth/session-launch wiring fragment
|
||||
├── recipes/ ← mainline package recipes (untouched)
|
||||
├── mk/ ← mainline build system (untouched)
|
||||
├── local/ ← RED BEAR OS custom work
|
||||
@@ -147,8 +172,11 @@ redox-master/ ← git pull updates mainline Redox
|
||||
│ │ ├── drivers/ ← redox-driver-sys, linux-kpi (GPU/Wi-Fi compat only — NOT USB)
|
||||
│ │ ├── gpu/ ← redox-drm (AMD + Intel display drivers), amdgpu (C port)
|
||||
│ │ ├── system/ ← cub, evdevd, udev-shim, redbear-firmware, firmware-loader, redbear-hwutils, redbear-info, redbear-netctl, redbear-quirks, redbear-meta
|
||||
│ │ │ ├── redbear-sessiond ← org.freedesktop.login1 D-Bus session broker (zbus-based Rust daemon)
|
||||
│ │ │ ├── redbear-dbus-services ← D-Bus .service activation files + XML policies
|
||||
│ │ │ ├── redbear-sessiond ← org.freedesktop.login1 D-Bus session broker (zbus-based Rust daemon)
|
||||
│ │ │ ├── redbear-authd ← local-user authentication daemon (`/etc/passwd` + `/etc/shadow` + `/etc/group`)
|
||||
│ │ │ ├── redbear-session-launch ← session bootstrap helper (uid/gid/env/runtime-dir handoff)
|
||||
│ │ │ ├── redbear-greeter ← greeter orchestrator package (`redbear-greeterd`, UI, compositor wrapper, staged assets)
|
||||
│ │ │ ├── redbear-dbus-services ← D-Bus .service activation files + XML policies
|
||||
│ │ ├── wayland/ ← Wayland compositor (v2.0 Phase 2)
|
||||
│ │ └── kde/ ← KDE Plasma (v2.0 Phases 3–4)
|
||||
│ ├── patches/
|
||||
@@ -185,25 +213,27 @@ redox-master/ ← git pull updates mainline Redox
|
||||
│ │ ├── test-ps2-qemu.sh ← QEMU launcher for the bounded PS/2 + serio runtime proof
|
||||
│ │ ├── test-timer-qemu.sh ← QEMU launcher for the bounded monotonic timer runtime proof
|
||||
│ │ ├── test-lowlevel-controllers-qemu.sh ← Sequential wrapper for bounded low-level controller proofs
|
||||
│ │ └── test-usb-maturity-qemu.sh ← Sequential wrapper for bounded USB maturity proofs
|
||||
│ │ ├── test-usb-maturity-qemu.sh ← Sequential wrapper for bounded USB maturity proofs
|
||||
│ │ └── test-greeter-qemu.sh ← Bounded QEMU proof for the Red Bear greeter/auth/session surface
|
||||
│ └── docs/ ← Integration docs
|
||||
```
|
||||
|
||||
## HOW TO BUILD RED BEAR OS
|
||||
|
||||
```bash
|
||||
# Tracked KWin Wayland desktop target
|
||||
./local/scripts/build-redbear.sh redbear-kde
|
||||
# Active desktop/graphics target
|
||||
./local/scripts/build-redbear.sh redbear-full
|
||||
|
||||
# Minimal server variant
|
||||
./local/scripts/build-redbear.sh redbear-minimal
|
||||
# Minimal non-desktop target
|
||||
./local/scripts/build-redbear.sh redbear-mini
|
||||
|
||||
# Live ISO
|
||||
./local/scripts/build-redbear.sh redbear-live && make live CONFIG_NAME=redbear-live
|
||||
# Live images
|
||||
./local/scripts/build-redbear.sh redbear-live-full && make live CONFIG_NAME=redbear-live-full
|
||||
./local/scripts/build-redbear.sh redbear-live-mini && make live CONFIG_NAME=redbear-live-mini
|
||||
|
||||
# VM-network baseline validation helpers
|
||||
./local/scripts/validate-vm-network-baseline.sh
|
||||
./local/scripts/test-vm-network-qemu.sh redbear-minimal
|
||||
./local/scripts/test-vm-network-qemu.sh redbear-mini
|
||||
# Then run inside the guest:
|
||||
# ./local/scripts/test-vm-network-runtime.sh
|
||||
|
||||
@@ -212,7 +242,8 @@ redox-master/ ← git pull updates mainline Redox
|
||||
./local/scripts/test-phase1-desktop-substrate.sh --qemu redbear-wayland
|
||||
|
||||
# Legacy Phase 3 runtime-substrate validation (historical P0-P6 numbering; script still works)
|
||||
./local/scripts/test-phase3-runtime-substrate.sh --qemu redbear-kde
|
||||
# Use the active desktop target when adapting historical validation flows.
|
||||
./local/scripts/test-phase3-runtime-substrate.sh --qemu redbear-full
|
||||
|
||||
# Low-level controller validation
|
||||
./local/scripts/test-xhci-irq-qemu.sh --check
|
||||
@@ -249,6 +280,13 @@ redox-master/ ← git pull updates mainline Redox
|
||||
# Then run inside the guest:
|
||||
# redbear-phase5-network-check
|
||||
|
||||
# Experimental Red Bear greeter/login validation
|
||||
./local/scripts/build-redbear.sh redbear-full
|
||||
./local/scripts/test-greeter-qemu.sh --check
|
||||
# Then run inside the guest:
|
||||
# redbear-greeter-check
|
||||
# redbear-greeter-check --invalid root wrong
|
||||
|
||||
# Bounded Intel Wi-Fi runtime validation (real target or passthrough guest)
|
||||
# Host preparation for VFIO-backed guests:
|
||||
# sudo ./local/scripts/validate-wifi-vfio-host.sh --host-pci 0000:xx:yy.z --expect-driver iwlwifi
|
||||
@@ -265,7 +303,7 @@ redox-master/ ← git pull updates mainline Redox
|
||||
# ./local/scripts/finalize-wifi-validation-run.sh ./wifi-passthrough-capture.json ./wifi-passthrough-artifacts.tar.gz
|
||||
|
||||
# Legacy Phase 6 KDE session-surface validation (historical P0-P6 numbering; script still works)
|
||||
./local/scripts/build-redbear.sh redbear-kde
|
||||
./local/scripts/build-redbear.sh redbear-full
|
||||
./local/scripts/test-phase6-kde-qemu.sh --check
|
||||
# Then run inside the guest:
|
||||
# redbear-phase6-kde-check
|
||||
@@ -274,7 +312,7 @@ redox-master/ ← git pull updates mainline Redox
|
||||
redbear-netctl --help
|
||||
|
||||
# Or manually:
|
||||
make all CONFIG_NAME=redbear-kde
|
||||
make all CONFIG_NAME=redbear-full
|
||||
|
||||
# Single custom recipe:
|
||||
./target/release/repo cook local/recipes/branding/redbear-release
|
||||
@@ -313,13 +351,16 @@ When mainline updates affect our work:
|
||||
- `docs/07-RED-BEAR-OS-IMPLEMENTATION-PLAN.md` is the canonical public execution plan.
|
||||
- `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` (v2.0) is the canonical desktop path plan from console to
|
||||
hardware-accelerated KDE Plasma on Wayland, using a three-track Phase 1–5 model.
|
||||
- `local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` is the canonical Wayland subsystem plan beneath the
|
||||
desktop path. Use it for Wayland-specific stability, completeness, ownership, and runtime-proof
|
||||
sequencing.
|
||||
- `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` is the current DRM-focused execution plan beneath
|
||||
the canonical desktop path. It keeps Intel and AMD at the same evidence bar while separating
|
||||
display/KMS maturity from render/3D maturity.
|
||||
- Older GPU-specific docs such as `local/docs/AMD-FIRST-INTEGRATION.md`,
|
||||
`local/docs/P2-AMD-GPU-DISPLAY.md`, `local/docs/HARDWARE-3D-ASSESSMENT.md`, and
|
||||
`local/docs/DMA-BUF-IMPROVEMENT-PLAN.md` remain useful reference material, but they are not the
|
||||
planning authority when sequencing or acceptance criteria differ.
|
||||
`local/docs/HARDWARE-3D-ASSESSMENT.md`, and `local/docs/DMA-BUF-IMPROVEMENT-PLAN.md` remain
|
||||
useful reference material, but they are not the planning authority when sequencing or acceptance
|
||||
criteria differ.
|
||||
- `local/docs/AMD-FIRST-INTEGRATION.md` remains the deeper AMD-specific technical roadmap, but AMD
|
||||
and Intel machines are now equal-priority Red Bear OS targets.
|
||||
- The earlier Phase 0–3 reassessment bridge has been retired. Its reconciliation role is now
|
||||
@@ -338,6 +379,7 @@ When mainline updates affect our work:
|
||||
- `local/docs/QUIRKS-IMPROVEMENT-PLAN.md` is the current follow-up plan for removing quirks drift,
|
||||
integrating quirks into real drivers, and converging on one source of truth.
|
||||
- `local/docs/DBUS-INTEGRATION-PLAN.md` is the canonical D-Bus architecture and implementation plan for KDE Plasma 6 on Wayland. It defines the phased approach to D-Bus service integration, the `redbear-sessiond` login1-compatible session broker, and the gap analysis for desktop-facing D-Bus services.
|
||||
- `local/docs/GREETER-LOGIN-IMPLEMENTATION-PLAN.md` is the canonical Red Bear-native greeter/login design and current implementation plan for the `redbear-full` desktop path. It defines the `redbear-authd` / `redbear-session-launch` / `redbear-greeter` split, service wiring, validation surface, and the current boundary between the active greeter path and the older `redbear-validation-session` helper flows.
|
||||
|
||||
The current execution order for these subsystem plans is:
|
||||
|
||||
@@ -511,35 +553,34 @@ local/Assets/
|
||||
|
||||
## RED BEAR OS CONFIG HIERARCHY
|
||||
|
||||
Active compile targets:
|
||||
|
||||
- `redbear-mini`
|
||||
- `redbear-live-mini`
|
||||
- `redbear-full`
|
||||
- `redbear-live-full`
|
||||
|
||||
Desktop/graphics are available only on the `full` targets. Older names such as `redbear-kde`,
|
||||
`redbear-wayland`, `redbear-minimal`, and `redbear-live-minimal` may still exist in the tree as
|
||||
legacy or staging artifacts, but they are not the supported compile-target surface.
|
||||
|
||||
```
|
||||
redbear-live.toml
|
||||
└── redbear-kde.toml
|
||||
redbear-live-full.toml
|
||||
└── redbear-full.toml
|
||||
├── desktop.toml (mainline)
|
||||
├── redbear-legacy-base.toml ← Neutralize broken base legacy init scripts
|
||||
├── redbear-legacy-desktop.toml ← Neutralize broken desktop legacy init scripts
|
||||
├── redbear-device-services.toml ← Shared firmware-loader / evdevd / udev service wiring
|
||||
├── redbear-netctl.toml ← Shared Red Bear network profile files + netctl boot service
|
||||
├── desktop.toml (mainline)
|
||||
│ ├── desktop-minimal.toml
|
||||
│ │ └── minimal.toml
|
||||
│ │ └── base.toml
|
||||
│ └── server.toml
|
||||
│ └── minimal.toml
|
||||
│ └── base.toml
|
||||
├── redbear-greeter-services.toml ← Greeter/auth/session-launch wiring for desktop targets
|
||||
└── [packages] redbear-release, redbear-hwutils, redbear-netctl,
|
||||
firmware-loader, evdevd, udev-shim, redbear-info,
|
||||
mc, cub
|
||||
NOTE: ext4d is inherited from desktop.toml (mainline package)
|
||||
NOTE: cub is treated as an essential Red Bear utility and is included through the tracked
|
||||
flavor configs; it still depends on the custom recipe symlink
|
||||
(recipes/system/cub → local/recipes/system/cub) being created by
|
||||
integrate-redbear.sh or apply-patches.sh before building.
|
||||
NOTE: redbear-netctl provides a Redox-native `netctl` command with profiles
|
||||
in /etc/netctl and a boot-time `netctl --boot` service.
|
||||
NOTE: redbear-info is the canonical runtime integration report. Keep it updated when
|
||||
Red Bear adds new tools, schemes, services, or hardware integration paths.
|
||||
NOTE: redbear-live inherits cub through redbear-kde.toml.
|
||||
NOTE: redbear-meta is explicitly included in redbear-full.toml. Keep any broader inclusion
|
||||
deliberate because its dependency surface is much heavier than the core utility layer.
|
||||
redbear-sessiond, redbear-authd, redbear-session-launch,
|
||||
redbear-greeter, redbear-meta, cub
|
||||
NOTE: Desktop/graphics are available only on redbear-full and redbear-live-full.
|
||||
NOTE: ext4d is inherited from desktop.toml (mainline package).
|
||||
NOTE: redbear-meta is explicitly included in redbear-full.toml; keep broader inclusion deliberate.
|
||||
NOTE: redbear-live-full inherits from redbear-full.toml.
|
||||
|
||||
redbear-full.toml
|
||||
└── desktop.toml (mainline)
|
||||
@@ -547,20 +588,17 @@ redbear-full.toml
|
||||
└── redbear-legacy-desktop.toml ← Neutralize broken desktop legacy init scripts
|
||||
└── redbear-device-services.toml ← Shared firmware-loader / evdevd / udev service wiring
|
||||
└── redbear-netctl.toml ← Shared Red Bear network profile files + netctl boot service
|
||||
└── redbear-greeter-services.toml ← Greeter/auth/session-launch wiring
|
||||
|
||||
redbear-wayland.toml
|
||||
└── wayland.toml (mainline-derived Wayland profile)
|
||||
└── bounded validation runtime surface
|
||||
└── validation entrypoints: test-phase4-wayland-qemu.sh + redbear-phase4-wayland-check
|
||||
redbear-live-mini.toml
|
||||
└── minimal non-desktop live target
|
||||
└── desktop/graphics intentionally absent
|
||||
|
||||
redbear-kde.toml
|
||||
└── desktop.toml (mainline)
|
||||
└── redbear-legacy-base.toml ← Neutralize broken base legacy init scripts
|
||||
└── redbear-legacy-desktop.toml ← Neutralize broken desktop legacy init scripts
|
||||
└── redbear-device-services.toml ← Shared firmware-loader / evdevd / udev service wiring
|
||||
└── redbear-netctl.toml ← Shared Red Bear network profile files + netctl boot service
|
||||
redbear-mini
|
||||
└── legacy/staging config files in-tree still use the older `redbear-minimal*` names
|
||||
in some places; do not treat those names as the supported compile-target surface
|
||||
|
||||
redbear-minimal.toml
|
||||
redbear-minimal.toml (legacy/staging naming still present in tree)
|
||||
└── minimal.toml (mainline)
|
||||
└── base.toml
|
||||
└── redbear-legacy-base.toml ← Neutralize broken base legacy init scripts
|
||||
@@ -573,9 +611,10 @@ redbear-minimal.toml
|
||||
Config comparison:
|
||||
| Config | GPU Stack | Desktop | Branding | ext4d | filesystem_size |
|
||||
|--------|-----------|---------|----------|-------|-----------------|
|
||||
| redbear-desktop | Full | Supplementary integration support | Yes | ✅ (via desktop.toml) | 10240 MiB |
|
||||
| redbear-minimal | None | None | Yes | ❌ | 512 MiB |
|
||||
| redbear-live | Full | KWin Wayland target | Yes | ✅ (via desktop.toml) | 12288 MiB |
|
||||
| redbear-full | Full | Yes | Yes | ✅ (via desktop.toml) | 4096 MiB |
|
||||
| redbear-live-full | Full | Yes | Yes | ✅ (via redbear-full.toml) | 4096 MiB |
|
||||
| redbear-mini | None | None | Yes | legacy/staging naming in tree still maps through `redbear-minimal*` files | legacy/staging |
|
||||
| redbear-live-mini | None | None | Yes | legacy/staging naming in tree still maps through `redbear-live-minimal*` files | legacy/staging |
|
||||
|
||||
## ANTI-PATTERNS (COMMIT POLICY)
|
||||
|
||||
|
||||
@@ -103,12 +103,16 @@ bounded-hardware, or release-grade completeness.
|
||||
- MCFG handling was removed from `acpid` and replaced with the `pcid /config` path.
|
||||
- Shutdown eventing via `/scheme/kernel.acpi/kstop` is implemented and consumed by
|
||||
`redbear-sessiond`.
|
||||
- `acpid` now models `S1` / `S3` / `S4` / `S5` explicitly in userspace, and the current `_S5`
|
||||
shutdown path routes through that model instead of a special-case magic value.
|
||||
|
||||
### Weak today
|
||||
|
||||
- Sleep-state transitions beyond `\_S5` are unsupported.
|
||||
- Sleep eventing is unsupported.
|
||||
- `SLP_TYPb` remains incomplete for broader sleep-state handling.
|
||||
- Non-`S5` sleep targets are now represented explicitly, but they remain groundwork-only and do not
|
||||
imply implemented suspend/resume support yet.
|
||||
- AML init order is still tied to PCI FD registration timing.
|
||||
- Some physmem / opregion failure paths are still not explicit enough.
|
||||
- DMAR remains orphaned in `acpid` source: present, not wired, not fully transferred.
|
||||
|
||||
@@ -44,7 +44,7 @@ take 5+ years.
|
||||
| x2APIC | ✅ Works | Auto-detected via CPUID, APIC/SMP functional |
|
||||
| HPET | ✅ Works | Timer initialized from ACPI |
|
||||
| IOMMU | 🚧 In progress | `iommu` daemon now builds, auto-discovers common IVRS table paths, reaches unit detection plus `scheme:iommu` registration in the QEMU/AMD-IOMMU validation path, and now has a guest-driven first-use self-test that initializes both discovered units and drains events successfully in QEMU; real hardware validation is still missing |
|
||||
| AMD GPU | 🚧 In progress | MMIO mapped, DC port compiles, MSI-X wired, no hardware validation yet |
|
||||
| AMD GPU | 🚧 In progress | MMIO mapped, bounded Red Bear display glue path builds, MSI-X wired; imported Linux AMD DC/TTM/core remain under compile triage; no hardware validation yet |
|
||||
| Wi-Fi/BT | 🚧 In progress | Repo now carries bounded wireless scaffolding: one experimental in-tree Bluetooth slice exists, and a bounded Intel Wi-Fi scaffold exists elsewhere, but validated wireless connectivity support is still incomplete |
|
||||
| USB | ⚠️ Variable | Some USB controllers work, others don't |
|
||||
|
||||
@@ -259,22 +259,27 @@ ONLY the display/modesetting portion first, using linux-kpi headers.
|
||||
| MSI-X interrupt support | ✅ | `local/recipes/gpu/redox-drm/source/src/drivers/interrupt.rs` — shared MSI-X/MSI/legacy abstraction with quirk-aware fallback |
|
||||
| Intel pcid-spawner config | ✅ | `local/config/pcid.d/intel_gpu.toml` — auto-detect Intel GPUs |
|
||||
|
||||
### P2: AMD GPU Display — COMPLETE (compiles, no HW validation)
|
||||
### P2: AMD GPU Display — BOUNDED PATH BUILDS (imported Linux AMD DC/TTM/core still under compile triage)
|
||||
|
||||
| Component | Status | Files |
|
||||
|-----------|--------|-------|
|
||||
| redox-drm daemon | ✅ | `local/recipes/gpu/redox-drm/source/` — DRM scheme daemon |
|
||||
| AMD driver (Rust) | ✅ | `local/recipes/gpu/redox-drm/source/src/drivers/amd/mod.rs` |
|
||||
| AMD DisplayCore (FFI) | ✅ | `local/recipes/gpu/redox-drm/source/src/drivers/amd/display.rs` |
|
||||
| AMD PCI stubs (dynamic) | ✅ | `local/recipes/gpu/amdgpu/source/redox_stubs.c` — populated from Rust via FFI |
|
||||
| AMD DC init (C) | ✅ | `local/recipes/gpu/amdgpu/source/amdgpu_redox_main.c` — modesetting, connector detect |
|
||||
| AMD glue headers | ✅ | `local/recipes/gpu/amdgpu/source/redox_glue.h` — Linux compat surface |
|
||||
| AMD DisplayCore (FFI surface) | ✅ bounded | `local/recipes/gpu/redox-drm/source/src/drivers/amd/display.rs` |
|
||||
| AMD PCI stubs (dynamic) | ✅ bounded | `local/recipes/gpu/amdgpu/source/redox_stubs.c` — populated from Rust via FFI |
|
||||
| AMD DC init / modeset glue (C) | ✅ bounded | `local/recipes/gpu/amdgpu/source/amdgpu_redox_main.c` — modesetting, connector detect |
|
||||
| AMD glue headers | ✅ bounded | `local/recipes/gpu/amdgpu/source/redox_glue.h` — Linux compat surface for the retained path |
|
||||
| GTT manager | ✅ | `local/recipes/gpu/redox-drm/source/src/drivers/amd/gtt.rs` |
|
||||
| Ring buffer | ✅ | `local/recipes/gpu/redox-drm/source/src/drivers/amd/ring.rs` |
|
||||
| GEM buffer mgmt | ✅ | `local/recipes/gpu/redox-drm/source/src/gem.rs` |
|
||||
| DMA-BUF | ✅ | `local/recipes/gpu/redox-drm/source/src/scheme.rs` (PRIME export/import via opaque tokens) |
|
||||
| Intel driver | ✅ | `local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs` + `display.rs` |
|
||||
|
||||
The current retained AMD build path now produces the `amdgpu` recipe from the Red Bear glue layer
|
||||
plus Rust-side driver/runtime pieces. The broad imported Linux AMD display, TTM, and amdgpu core
|
||||
trees are no longer treated as compile-complete deliverables; they remain under compile triage until
|
||||
the bounded path proves a concrete need to re-introduce them.
|
||||
|
||||
For bounded runtime display validation, Red Bear now uses the shared
|
||||
`local/scripts/test-drm-display-runtime.sh` harness, with `local/scripts/test-amd-gpu.sh` as the
|
||||
AMD wrapper.
|
||||
@@ -282,6 +287,27 @@ AMD wrapper.
|
||||
Human-readable PCI naming for AMD/Intel devices now comes from the shipped `pciids` database rather
|
||||
than from hand-maintained GPU name tables in local runtime tools.
|
||||
|
||||
#### Historical P2 implementation snapshot
|
||||
|
||||
The old standalone `P2-AMD-GPU-DISPLAY.md` milestone record is now folded into this AMD-specific
|
||||
reference.
|
||||
|
||||
Important historical P2 details that still matter:
|
||||
|
||||
- **Architecture:** `userspace apps -> scheme:drm -> redox-drm daemon -> AMD DC (C code,
|
||||
linux-kpi) -> MMIO`
|
||||
- **Build integration:** the Red Bear GPU path is rooted in `local/recipes/gpu/redox-drm/` and
|
||||
`local/recipes/gpu/amdgpu/`, with PCI auto-detection from `local/config/pcid.d/amd_gpu.toml`
|
||||
and the imported Linux AMD driver tree in `local/recipes/gpu/amdgpu-source/`
|
||||
- **Historical P2 boot sequence:** kernel PCI init -> `pcid` AMD GPU detection -> `redox-drm`
|
||||
launch -> BAR/MMIO mapping -> firmware load via `scheme:firmware` -> AMD DC init -> connector
|
||||
detect / EDID -> `scheme:drm/card0` registration
|
||||
- **Historical implementation closure:** the scoped P2 implementation task was compile-complete for
|
||||
display-side bring-up, but hardware validation remained and still remains a separate evidence gate
|
||||
|
||||
That milestone should now be read through the current GPU/DRM plan and current desktop status docs
|
||||
rather than as a standalone execution authority.
|
||||
|
||||
### Build Verification
|
||||
|
||||
All crates compile with `cargo check` (0 errors):
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
# AMDGPU DC Compile Triage Plan
|
||||
|
||||
**Date:** 2026-04-18
|
||||
**Scope:** Triage of the current Red Bear amdgpu AMD Display Core compile path, specifically the
|
||||
decision between growing the Linux compatibility surface and narrowing the imported display/DC
|
||||
source set to the bounded path actually needed for first display bring-up.
|
||||
|
||||
> **Planning authority note (2026-04-18):** this file is a focused amdgpu/DC compile-triage and
|
||||
> execution document. It does not replace `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` as the
|
||||
> canonical GPU/DRM plan. Use the DRM modernization plan for overall execution order, Intel/AMD
|
||||
> parity criteria, and broader acceptance gates. Use this file for the specific question of how to
|
||||
> triage the current amdgpu DC compile break without drifting into open-ended compatibility work.
|
||||
|
||||
> **Status update (2026-04-18):** Phase 1B has now been carried out in bounded form. The `amdgpu`
|
||||
> recipe builds successfully on the retained Red Bear glue path (`amdgpu_redox_main.c` +
|
||||
> `redox_stubs.c`), while the imported Linux AMD display, TTM, and amdgpu core trees remain
|
||||
> explicitly outside the retained compile surface and still under compile triage.
|
||||
|
||||
## Title and intent
|
||||
|
||||
Red Bear currently compiles the imported AMD display tree too broadly for the evidence-backed goal
|
||||
it actually has today.
|
||||
|
||||
The immediate goal is **not** to prove that the full imported AMD Display Core tree compiles on
|
||||
Redox. The immediate goal is to unblock the bounded display path needed for first display-side
|
||||
bring-up while preserving a maintainable route toward broader DC closure later.
|
||||
|
||||
This document exists to prevent two failure modes:
|
||||
|
||||
1. treating the first compile error as if it justifies unconstrained `linux-kpi` expansion, and
|
||||
2. claiming progress from a narrowed compile path without documenting exactly what was excluded and
|
||||
why.
|
||||
|
||||
## Current grounded state
|
||||
|
||||
### Bottom line
|
||||
|
||||
The original broad-tree failure was **not** a `freesync.c`-specific logic bug. It exposed a broader
|
||||
mismatch between the imported AMD DC / TTM / amdgpu trees and the current Red Bear compatibility
|
||||
strategy.
|
||||
|
||||
After narrowing the recipe to the actual retained first-display path, the `amdgpu` recipe now
|
||||
builds successfully from the Red Bear glue layer alone. That is the current truthful state: the
|
||||
bounded retained path builds, while the imported Linux trees remain under compile triage rather than
|
||||
being claimed as compile-complete.
|
||||
|
||||
### Confirmed evidence
|
||||
|
||||
| Area | Current evidence | Repo grounding |
|
||||
|---|---|---|
|
||||
| Historical broad-path rule | The old recipe compiled all `display/*.c` files and failed in optional AMD DC code before the retained path was proven | historical recipe state + `local/recipes/gpu/amdgpu/target/x86_64-unknown-redox/build/freesync.o.log` |
|
||||
| Current retained build rule | The current recipe compiles only the bounded Red Bear glue path and links `libamdgpu_dc_redox.so` from that retained surface | `local/recipes/gpu/amdgpu/recipe.toml` |
|
||||
| Historical first hard failure | `freesync.c -> dm_services.h -> dm_services_types.h -> os_types.h -> linux/kgdb.h` | `local/recipes/gpu/amdgpu/target/x86_64-unknown-redox/build/freesync.o.log` |
|
||||
| Current shim posture | Compatibility surface is partial, not absent | `local/recipes/drivers/linux-kpi/source/src/c_headers/`, `local/recipes/gpu/amdgpu/source/redox_glue.h` |
|
||||
| Small retained-path shim probes attempted | Added minimal `linux/export.h` and `linux/refcount.h` while testing whether imported TTM belonged on the retained path | `local/recipes/drivers/linux-kpi/source/src/c_headers/linux/export.h`, `.../linux/refcount.h` |
|
||||
| Switch criterion outcome | Imported TTM immediately fanned into broader Linux-kernel surfaces (`__cond_acquires`, `iosys-map`, and related header fallout), so the retained path was narrowed again instead of growing shims further | retained build logs during TTM probe |
|
||||
| Current Red Bear need | First display bring-up needs a bounded display path, not proof that all optional AMD DC subtrees compile | `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md`, `local/docs/AMD-FIRST-INTEGRATION.md` |
|
||||
|
||||
### Why the current approach is unstable
|
||||
|
||||
The current amdgpu recipe uses a broad compile rule that effectively says:
|
||||
|
||||
> compile the imported display tree first, then see what breaks.
|
||||
|
||||
That is useful for discovery, but it is a poor default execution strategy for bounded bring-up.
|
||||
|
||||
It pulls optional and advanced display code into the same compile surface as the first modeset path,
|
||||
which means a failure in a module such as FreeSync can block the entire experiment even when that
|
||||
module is not yet proven necessary for the first Red Bear display target.
|
||||
|
||||
## Triage question
|
||||
|
||||
Red Bear needs an explicit answer to this question before continuing:
|
||||
|
||||
> Should the repo first grow the Linux compatibility layer until the full imported AMD display tree
|
||||
> compiles further, or should it first narrow the imported source set to the display path Red Bear
|
||||
> actually needs today?
|
||||
|
||||
This document answers:
|
||||
|
||||
- **Start with Strategy B** — narrow the DC source set.
|
||||
- Use **Strategy A** — minimal shim additions — only as a controlled fallback when the retained,
|
||||
bounded display path still proves a small required compatibility gap.
|
||||
|
||||
## Strategy comparison
|
||||
|
||||
| Strategy | What it does | Best when | Success criteria | Main failure mode |
|
||||
|---|---|---|---|---|
|
||||
| **A. Minimal shim additions** | Add the smallest Linux compatibility surface needed to expose the next blocker | The real retained display path is already known, and the missing API surface stays small and generic | Each shim advances the build by one blocker class without broadening scope dramatically | Header whack-a-mole grows into de facto kernel-environment emulation |
|
||||
| **B. Narrow the DC source set** | Replace broad full-tree compile with an explicit bounded file list aligned to the actual first display goal | Optional or advanced modules are being pulled into the build before their necessity is proven | The reduced source set compiles further or reveals the next blocker on the true bring-up path | False confidence if the narrowed claim is not documented precisely |
|
||||
|
||||
## Recommendation
|
||||
|
||||
### Recommendation summary
|
||||
|
||||
Start with **B: narrow the compiled DC source set to the bounded display path Red Bear actually
|
||||
uses today**.
|
||||
|
||||
That recommendation has now been implemented in bounded form. The retained path was narrowed far
|
||||
enough to prove that the current Red Bear bring-up surface does not need the imported Linux AMD
|
||||
display, TTM, or amdgpu core trees in order to build the shipped `amdgpu` recipe.
|
||||
|
||||
The current evidence supports that recommendation because:
|
||||
|
||||
1. the recipe compiles the entire imported display tree,
|
||||
2. the first blocker sits in a dependency cone that likely contains several more Linux/DRM header
|
||||
and semantic assumptions, and
|
||||
3. Red Bear's current need is bounded display bring-up, not immediate proof that every imported
|
||||
AMD DC subsystem compiles under Redox.
|
||||
|
||||
### Why A is not the first move
|
||||
|
||||
The first hard failure (`linux/kgdb.h`) is shallow enough to tempt a quick shim fix. That is useful
|
||||
only if the retained path is already known. Right now it is not. Without narrowing the source set
|
||||
first, each new shim risks paying compatibility cost for files Red Bear may not need for first
|
||||
bring-up.
|
||||
|
||||
That is the main hidden cost of Strategy A at this stage: it can create real maintenance debt before
|
||||
the repo has proven that the affected code is on the first bring-up path at all.
|
||||
|
||||
## ULW execution plan
|
||||
|
||||
## Phase 0 — Freeze the baseline
|
||||
|
||||
### Goal
|
||||
|
||||
Create one canonical failure snapshot that all later triage work can refer back to.
|
||||
|
||||
### Actions
|
||||
|
||||
- Record the current broad display compile rule in the amdgpu recipe.
|
||||
- Record the first failing translation unit and full include chain.
|
||||
- Record the current bounded Red Bear display objective and the currently targeted ASIC/runtime
|
||||
surface.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
One written baseline exists showing:
|
||||
|
||||
- the current full-tree compile behavior,
|
||||
- the current first hard failure at `linux/kgdb.h`, and
|
||||
- the current bounded display objective.
|
||||
|
||||
### Current status
|
||||
|
||||
- complete enough to proceed
|
||||
|
||||
## Phase 1B — Narrow-source probe
|
||||
|
||||
### Goal
|
||||
|
||||
Identify the minimum imported display/DC source set required for current Red Bear display bring-up.
|
||||
|
||||
### Required mindset
|
||||
|
||||
The question in this phase is not “what can Linux build?”
|
||||
|
||||
The question is:
|
||||
|
||||
> what does Red Bear actually need compiled now to support its present display-side target?
|
||||
|
||||
### Actions
|
||||
|
||||
- Replace broad `find .../display -name '*.c'` behavior with an explicit bounded file list.
|
||||
- Treat the first retained file list as a **probe hypothesis**, not as a proven final minimum.
|
||||
- Keep only the C sources required for the current Red Bear bring-up surface hypothesis:
|
||||
- device initialization,
|
||||
- connector detection and mode enumeration,
|
||||
- bounded modeset path,
|
||||
- cleanup,
|
||||
- and the currently targeted ASIC families.
|
||||
- Exclude obvious scope inflators first unless the call graph proves they are required:
|
||||
- `modules/freesync/*`,
|
||||
- untargeted DCN generations,
|
||||
- `amdgpu_dm/*`,
|
||||
- optional feature modules not on the first display path.
|
||||
|
||||
### Verification
|
||||
|
||||
- The reduced file list is explicit and reviewable.
|
||||
- The reduced build is re-run.
|
||||
- The next failure is checked to confirm that it occurs on the retained bounded path rather than in
|
||||
an excluded optional subtree.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
One of the following becomes true:
|
||||
|
||||
1. the narrowed set compiles meaningfully further than the current build, or
|
||||
2. the next blocker appears on the real retained path and is therefore a justified compatibility
|
||||
problem.
|
||||
|
||||
### Failure signal
|
||||
|
||||
If the narrowed set cannot be described cleanly because the retained path immediately drags in broad
|
||||
optional subsystems, stop and move to the decision gate rather than continuing to guess.
|
||||
|
||||
### Current status
|
||||
|
||||
- complete — the retained path is now explicit and builds
|
||||
|
||||
## Phase 1A — Minimal-shim probe
|
||||
|
||||
### Goal
|
||||
|
||||
Expose the next blocker with the smallest justified compatibility addition.
|
||||
|
||||
### Entry condition
|
||||
|
||||
Only do this after Phase 1B has established a retained bounded path, or after the narrowed path
|
||||
proves that a small missing Linux primitive is genuinely required.
|
||||
|
||||
### Allowed shim order
|
||||
|
||||
Add one shim family at a time, in this rough priority order:
|
||||
|
||||
1. `linux/kgdb.h`
|
||||
2. `asm/byteorder.h`
|
||||
3. `linux/vmalloc.h`
|
||||
4. `ktime_get_raw_ns` / timekeeping support
|
||||
5. `div64_u64` / `div64_u64_rem`
|
||||
6. `linux/refcount.h`
|
||||
|
||||
### Rules
|
||||
|
||||
- One shim family per change.
|
||||
- No speculative shim batches.
|
||||
- No ad hoc amdgpu-only workaround when the gap clearly belongs in `linux-kpi`.
|
||||
- If a shim exposes a large new Linux subsystem expectation rather than a narrow primitive, stop and
|
||||
reconsider the strategy.
|
||||
|
||||
### Verification
|
||||
|
||||
- Re-run the build after each shim family.
|
||||
- Confirm that the build advances by one blocker class.
|
||||
- Confirm that the next failure remains on the retained bounded path.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
- The build advances by exactly one blocker class, and
|
||||
- the next failure still belongs to the retained bounded path.
|
||||
|
||||
### Failure signal
|
||||
|
||||
If one shim immediately reveals several unrelated Linux subsystem requirements, stop and return to
|
||||
Strategy B.
|
||||
|
||||
## Phase 2 — Decision gate
|
||||
|
||||
### Stay on Strategy B if
|
||||
|
||||
- the blocker sits in optional or advanced code such as FreeSync,
|
||||
- narrowing quickly reduces the blocker surface,
|
||||
- failures outside the retained path disappear,
|
||||
- or the retained path becomes understandable and controllable.
|
||||
|
||||
### Switch from B to A if
|
||||
|
||||
- **all** of the following are true:
|
||||
- an explicit retained file list has been written down,
|
||||
- the failure reproduces on that retained path after the narrowing pass,
|
||||
- the missing piece is a small generic primitive or header family rather than a broad subsystem
|
||||
expectation,
|
||||
- and the same compatibility gap is visible across multiple retained core files or one retained
|
||||
shared include chain.
|
||||
|
||||
### Abort A and return to B if
|
||||
|
||||
- more than one or two unrelated shim families are required before reaching a meaningful compile
|
||||
milestone,
|
||||
- missing APIs are dominated by files outside the retained runtime path,
|
||||
- or the work starts resembling unconstrained kernel-environment emulation.
|
||||
|
||||
## Phase 3 — Continue on the chosen path
|
||||
|
||||
### If B wins
|
||||
|
||||
- Keep the bounded file list explicit.
|
||||
- Document exactly what the bounded claim covers.
|
||||
- Do not quietly re-expand the tree.
|
||||
- Add excluded modules back only behind explicit proof of need.
|
||||
- Treat success here as **compile-triage progress only**. It does not imply full DC feature closure,
|
||||
optional-module completeness, or runtime readiness.
|
||||
|
||||
### If A wins
|
||||
|
||||
- Expand `linux-kpi` deliberately rather than scattering shims through amdgpu-local code.
|
||||
- Keep each new shim family generic and reusable where possible.
|
||||
- Track each new compatibility family as maintenance debt that must justify itself.
|
||||
|
||||
## Commit slicing
|
||||
|
||||
Recommended commit order:
|
||||
|
||||
1. narrow source set only,
|
||||
2. first shim family only,
|
||||
3. one blocker family per follow-up change.
|
||||
|
||||
Never mix broad source pruning and broad compatibility growth in the same commit.
|
||||
|
||||
## Red / Green / Refactor loop
|
||||
|
||||
### Red
|
||||
|
||||
The historical full-tree display build failed at `linux/kgdb.h` while compiling `freesync.c`.
|
||||
|
||||
### Green
|
||||
|
||||
Either:
|
||||
|
||||
- the narrowed source set compiles further, or
|
||||
- one small shim advances the retained path to the next blocker.
|
||||
|
||||
Current green state:
|
||||
|
||||
- the bounded retained path now builds successfully,
|
||||
- and the imported Linux AMD display / TTM / amdgpu trees remain explicitly excluded pending proven
|
||||
need.
|
||||
|
||||
### Refactor
|
||||
|
||||
Codify the smallest proven source set and execution path before adding more compatibility surface.
|
||||
|
||||
## Hidden failure modes
|
||||
|
||||
### Strategy B hidden failure mode
|
||||
|
||||
Strategy B can produce false confidence if the repo narrows the file list but does not write down
|
||||
what functionality is now intentionally out of scope.
|
||||
|
||||
That is why every narrowing step must be paired with an explicit bounded claim.
|
||||
|
||||
### Strategy A hidden failure mode
|
||||
|
||||
Strategy A can feel productive because each header addition removes one hard stop. But that can hide
|
||||
the fact that the repo is drifting into long-term Linux-environment emulation for code that the
|
||||
current Red Bear target may not even need.
|
||||
|
||||
That is why A must stay subordinate to a retained, justified source set.
|
||||
|
||||
## Definition of done
|
||||
|
||||
This triage plan is complete when:
|
||||
|
||||
- the repo has an explicit choice between bounded source narrowing and compatibility expansion,
|
||||
- the choice is backed by compile evidence,
|
||||
- optional AMD DC modules are not silently treated as required for first bring-up,
|
||||
- and compatibility growth, if needed, is happening in the right long-term layer.
|
||||
|
||||
For clarity, done here means the compile-triage path is explicit and justified. It does **not** mean
|
||||
that the full AMD DC tree is complete, that excluded optional modules are unnecessary in all future
|
||||
phases, or that runtime display validation is closed.
|
||||
|
||||
## Immediate next action
|
||||
|
||||
Do this next:
|
||||
|
||||
1. keep the retained `amdgpu` build path explicit and bounded,
|
||||
2. do not quietly re-introduce imported Linux AMD display / TTM / core sources,
|
||||
3. re-introduce imported subsystems only behind concrete runtime or feature evidence,
|
||||
4. if a future re-introduction attempt fans into broad Linux-kernel compatibility work again,
|
||||
treat that as a new triage pass rather than as proof that the broader tree belongs in the
|
||||
default retained build.
|
||||
@@ -9,13 +9,15 @@
|
||||
This is the single authoritative plan for the Red Bear OS path from console boot to a
|
||||
hardware-accelerated KDE Plasma desktop running on Wayland.
|
||||
|
||||
It consolidates and replaces the planning role previously held by:
|
||||
It consolidates and replaces the top-level planning role previously held by:
|
||||
|
||||
- `docs/03-WAYLAND-ON-REDOX.md` (historical Wayland rationale)
|
||||
- `docs/05-KDE-PLASMA-ON-REDOX.md` (historical KDE rationale)
|
||||
- `local/docs/AMD-FIRST-INTEGRATION.md` (AMD-specific hardware detail)
|
||||
- Prior revisions of this document (v1, which used a different Phase 1–5 breakdown)
|
||||
|
||||
`local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` now serves as the canonical Wayland subsystem plan
|
||||
beneath this top-level desktop path.
|
||||
|
||||
Those documents remain useful for subsystem detail, porting history, and design rationale.
|
||||
The earlier reassessment bridge is now retired, and its reconciliation role is covered here together
|
||||
with `local/docs/DESKTOP-STACK-CURRENT-STATUS.md` and `docs/07-RED-BEAR-OS-IMPLEMENTATION-PLAN.md`.
|
||||
@@ -71,12 +73,12 @@ Rules:
|
||||
| Area | State | Evidence | Notes |
|
||||
|---|---|---|---|
|
||||
| AMD bare-metal boot | validated | Boot, ACPI, SMP, x2APIC all work | Bounded to current tested hardware |
|
||||
| relibc Wayland/Qt unblockers | builds | signalfd, timerfd, eventfd, open_memstream, F_DUPFD_CLOEXEC, MSG_NOSIGNAL, bounded waitid, bounded RLIMIT, bounded eth0 networking, shm_open, bounded sem_open, bounded sys/ipc.h, bounded sys/shm.h | Runtime pressure from real consumers still untested |
|
||||
| relibc Wayland/Qt unblockers | builds + targeted runtime proof | signalfd, timerfd, eventfd, open_memstream, F_DUPFD_CLOEXEC, MSG_NOSIGNAL, bounded waitid, bounded RLIMIT, bounded eth0 networking, shm_open, bounded sem_open, bounded sys/ipc.h, bounded sys/shm.h | Strict relibc Redox-target runtime proof now exists for the fd-event slice; broader real-consumer semantics still need confirmation |
|
||||
| redox-driver-sys | builds | Driver substrate | |
|
||||
| linux-kpi | builds | Linux kernel API compatibility layer | |
|
||||
| firmware-loader | builds, boots | scheme:firmware registers at boot | |
|
||||
| redox-drm (AMD + Intel) | builds | DRM scheme daemon | No hardware runtime validation |
|
||||
| amdgpu C port | builds | AMD DC + TTM + linux-kpi compat | No hardware runtime validation |
|
||||
| amdgpu retained C path | builds | Red Bear display glue retained path + linux-kpi compat; imported Linux AMD DC/TTM/core remain under compile triage | No hardware runtime validation |
|
||||
| evdevd | builds, boots | scheme:evdev registers at boot | |
|
||||
| udev-shim | builds, boots | scheme:udev registers at boot | |
|
||||
| libwayland 1.24.0 | builds | No compositor proof yet | |
|
||||
@@ -578,7 +580,7 @@ This is the canonical document for the desktop path. It does not replace subsyst
|
||||
| `local/docs/DESKTOP-STACK-CURRENT-STATUS.md` | Short current-state desktop truth summary |
|
||||
| `local/docs/RELIBC-COMPLETENESS-AND-ENHANCEMENT-PLAN.md` | relibc completeness detail + patch ownership |
|
||||
| `local/docs/INPUT-SCHEME-ENHANCEMENT.md` | Input-path design if structural cleanup needed |
|
||||
| `local/docs/P2-AMD-GPU-DISPLAY.md` | AMD display status + validation targets |
|
||||
| `local/docs/AMDGPU-DC-COMPILE-TRIAGE-PLAN.md` | AMD DC compile-triage + bounded source-set strategy |
|
||||
| `local/docs/DMA-BUF-IMPROVEMENT-PLAN.md` | DMA-BUF scheme detail |
|
||||
| `local/docs/IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` | Controller/IRQ/IOMMU quality work |
|
||||
| `local/docs/PROFILE-MATRIX.md` | Profile roles + support-language reference |
|
||||
@@ -600,8 +602,8 @@ continuity, not as future work.
|
||||
|---|---|---|
|
||||
| AMD bare-metal boot (ACPI, SMP, x2APIC) | ✅ Boot-baseline complete | Prior to this plan; see `local/docs/ACPI-IMPROVEMENT-PLAN.md` for ongoing ownership and robustness work |
|
||||
| Driver infrastructure (redox-driver-sys, linux-kpi, firmware-loader) | ✅ Builds complete | Prior to this plan |
|
||||
| AMD GPU display (redox-drm, amdgpu C port) | ✅ Builds complete | Prior to this plan |
|
||||
| relibc POSIX unblockers (signalfd, timerfd, eventfd, etc.) | ✅ Builds complete | Prior to this plan |
|
||||
| AMD GPU display (redox-drm + bounded amdgpu retained path) | 🚧 Partial build completion | Imported Linux AMD DC/TTM/core remain under compile triage; no hardware runtime validation yet |
|
||||
| relibc POSIX unblockers (signalfd, timerfd, eventfd, etc.) | ✅ Builds + targeted runtime proof complete | Prior to this plan |
|
||||
| Qt6 base stack (qtbase, qtdeclarative, qtsvg, qtwayland) | ✅ Builds complete | Prior to this plan |
|
||||
| D-Bus 1.16.2 | ✅ Builds + bounded runtime | Prior to this plan |
|
||||
| All 32 KF6 frameworks | ✅ Builds complete | Prior to this plan |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Red Bear OS Desktop Stack — Current Status
|
||||
|
||||
**Last updated:** 2026-04-18
|
||||
**Last updated:** 2026-04-19
|
||||
**Canonical plan:** `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` (v2.0)
|
||||
|
||||
## Purpose
|
||||
@@ -15,7 +15,7 @@ Its job is to answer:
|
||||
- and what still blocks a trustworthy Wayland/KDE session claim.
|
||||
|
||||
For the execution plan (phases, timelines, acceptance criteria), see the canonical plan above.
|
||||
For historical design rationale, see `docs/03-WAYLAND-ON-REDOX.md` and `docs/05-KDE-PLASMA-ON-REDOX.md`.
|
||||
For subsystem planning detail, see `local/docs/WAYLAND-IMPLEMENTATION-PLAN.md`; for historical KDE rationale, see `docs/05-KDE-PLASMA-ON-REDOX.md`.
|
||||
|
||||
## Where We Are in the Plan
|
||||
|
||||
@@ -25,15 +25,18 @@ The canonical desktop plan uses a three-track model:
|
||||
- **Track B (Phase 3–4):** KWin Session → KDE Plasma — **blocked on Track A**
|
||||
- **Track C (Phase 5):** Hardware GPU — **can start after Phase 1**
|
||||
|
||||
**Current position:** Build-side gates are crossed. Phase 1 (Runtime Substrate Validation) is the
|
||||
next work target. The repo has not yet started systematic runtime validation.
|
||||
**Current position:** Build-side gates are crossed. Phase 1 (Runtime Substrate Validation) is still
|
||||
the next broad desktop target, but the repo now also carries an experimental Red Bear-native
|
||||
greeter/auth/session-launch stack on the `redbear-full` desktop path.
|
||||
|
||||
## Tracked Default and Evidence Boundary
|
||||
## Active Target Surface and Evidence Boundary
|
||||
|
||||
- The tracked default build now resolves to `CONFIG_NAME?=redbear-kde`.
|
||||
- the bounded Phase 2 validation profile remains available.
|
||||
- `redbear-kde` is the tracked KWin Wayland desktop direction, but runtime-proven compositor/
|
||||
session claims still remain incomplete.
|
||||
- The supported compile targets are `redbear-mini`, `redbear-live-mini`, `redbear-full`, and `redbear-live-full`.
|
||||
- Desktop/graphics are available only on `redbear-full` and `redbear-live-full`.
|
||||
- Older names such as `redbear-kde`, `redbear-wayland`, and `redbear-minimal*` still appear in
|
||||
historical or staging material, but they are not the supported compile-target surface.
|
||||
- The greeter/login path is currently an **experimental build/integration surface** on `redbear-full`;
|
||||
it is not yet a runtime-validated end-to-end desktop-login claim.
|
||||
|
||||
## Status Matrix
|
||||
|
||||
@@ -48,8 +51,15 @@ next work target. The repo has not yet started systematic runtime validation.
|
||||
| Mesa EGL+GBM+GLES2 | **builds** | Software path via LLVMpipe proven in QEMU; hardware path not proven |
|
||||
| libdrm amdgpu | **builds** | Package-level success only |
|
||||
| Input stack | **builds, enumerates** | evdevd, libevdev, libinput, seatd present; evdevd registers scheme at boot |
|
||||
| D-Bus | **builds, usable (bounded)** | System bus wired in `redbear-full` and `redbear-kde`; D-Bus plan + sessiond complete (DB-1), Qt 6.11 D-Bus coverage documented (Section 14), DB-2/3/4 service daemons implemented as stubs (notifications, upower, udisks, polkit) |
|
||||
| redbear-sessiond | ✅ Scaffold | org.freedesktop.login1 D-Bus session broker — Rust daemon (zbus 5), config wired in redbear-kde.toml, acpi_watcher with edge detection |
|
||||
| D-Bus | **builds, usable (bounded)** | System bus wired in `redbear-full`; D-Bus plan + sessiond complete (DB-1), Qt 6.11 D-Bus coverage documented (Section 14), DB-2/3/4 service daemons implemented as stubs (notifications, upower, udisks, polkit) |
|
||||
| redbear-sessiond | **builds, scaffold** | org.freedesktop.login1 D-Bus session broker — Rust daemon (zbus 5), wired on the `redbear-full` desktop path; now includes runtime control updates used by the greeter/auth session handoff |
|
||||
| redbear-authd | **builds** | Privileged local-user auth daemon; `/etc/passwd`/`/etc/shadow`/`/etc/group` parsing, SHA-256/SHA-512 crypt verification, bounded lockout, target-side recipe build proven |
|
||||
| redbear-session-launch | **builds** | User-session bootstrap tool; runtime-dir/env setup, uid/gid handoff, dbus-run-session → `redbear-kde-session`, target-side recipe build proven |
|
||||
| redbear-greeterd | **builds, experimental** | Root-owned greeter orchestrator; UI/auth socket protocol, bounded restart policy, return-to-greeter daemon logic, crate tests pass; end-to-end runtime proof still pending |
|
||||
| redbear-greeter UI | **builds, experimental** | Qt6/QML unprivileged login surface now ships in-tree; bounded runtime proof remains narrower than a full trusted KDE desktop-login claim |
|
||||
| redbear-validation-session | **builds, bounded helper** | Still staged as a validation launcher/helper, but no longer the primary `redbear-full` display-service owner |
|
||||
| Greeter runtime checker | ✅ implemented (bounded checker) | `redbear-greeter-check` asserts greeter binaries, assets, service files, socket reachability, hello protocol, invalid-login handling, and a validation-only successful-login/session-return loop inside the guest; current graphical runtime proof is still blocked below the greeter slice by guest-side Qt shared-plugin parsing |
|
||||
| Greeter QEMU harness | ✅ implemented (bounded harness) | `test-greeter-qemu.sh` boots `redbear-full`, logs in on the fallback console, and runs the in-guest greeter checker for hello, invalid-login, and bounded successful-login return-to-greeter proof; the compositor leg is presently blocked by guest-side Qt plugin loader failure rather than missing greeter artifacts |
|
||||
| redbear-notifications | ✅ Scaffold | org.freedesktop.Notifications — logs to stderr, no display integration yet |
|
||||
| redbear-upower | ✅ bounded real | org.freedesktop.UPower — enumerates real AC adapters/batteries from `/scheme/acpi/power`; desktop machines with no battery report line power only |
|
||||
| redbear-udisks | ✅ bounded real | org.freedesktop.UDisks2 — enumerates real `disk.*` schemes and partitions into read-only D-Bus objects; no fabricated mount/serial metadata |
|
||||
@@ -61,36 +71,37 @@ next work target. The repo has not yet started systematic runtime validation.
|
||||
| GPU acceleration | **blocked** | PRIME/DMA-BUF ioctls and bounded private CS surface implemented; real vendor render CS/fence path still missing |
|
||||
| validation compositor runtime | **experimental** | Reaches early init in QEMU; no complete session |
|
||||
| validation profile | **builds, boots** | Bounded Wayland runtime profile |
|
||||
| `redbear-full` profile | **builds, boots** | Broader desktop plumbing profile |
|
||||
| `redbear-kde` profile | **builds** | Tracked KWin desktop-direction profile |
|
||||
| `redbear-live` profile | **builds** | Live image following the tracked KWin desktop target |
|
||||
| `redbear-full` profile | **builds, boots** | Active desktop/graphics compile surface; now owns the experimental greeter/auth/session-launch integration path |
|
||||
| `redbear-live-full` profile | **builds** | Live image following the active desktop/graphics target |
|
||||
| `redbear-mini` profile | **builds** | Minimal non-desktop compile target |
|
||||
| `redbear-live-mini` profile | **builds** | Minimal live image target |
|
||||
|
||||
## Profile View
|
||||
|
||||
### Validation profile
|
||||
|
||||
- **Role:** Phase 2 Wayland compositor validation target
|
||||
- **Current truth:** Builds and boots in QEMU; bounded compositor initialization reaches early init but no complete session
|
||||
- **Use for:** Compositor/runtime regression testing, not broad desktop claims
|
||||
|
||||
### `redbear-full`
|
||||
|
||||
- **Role:** Broader desktop/network/session plumbing
|
||||
- **Current truth:** Carries D-Bus and broader integration pieces; VirtIO networking works in QEMU, and the bounded Phase 5 network/session checker is evidence-backed there
|
||||
- **Use for:** Desktop integration testing beyond the narrow Wayland slice
|
||||
- **Role:** Active desktop/graphics compile target and current greeter-integration surface
|
||||
- **Current truth:** Carries D-Bus, sessiond, broader integration pieces, and the experimental Red Bear-native greeter/auth/session-launch stack; VirtIO networking works in QEMU, the bounded Phase 5 network/session checker is evidence-backed there, and the repo now includes a bounded greeter checker/harness for the login surface. `redbear-validation-session` remains staged only as a bounded helper, not the active `20_display.service` owner on this target.
|
||||
- **Use for:** Desktop integration testing, greeter/login bring-up, and bounded desktop/network plumbing validation
|
||||
- **Do not overclaim:** This profile proves bounded QEMU desktop/network plumbing only. It does not by itself close the Wi-Fi implementation plan's later real-hardware Phase W5 reporting/recovery gate.
|
||||
|
||||
### `redbear-kde`
|
||||
### `redbear-live-full`
|
||||
|
||||
- **Role:** Phase 3–4 KDE/Plasma session bring-up
|
||||
- **Current truth:** Carries KWin/session wiring and KDE-facing package set; experimental but selected as the tracked default desktop target
|
||||
- **Use for:** KDE session surface testing once Phase 2 completes
|
||||
- **Role:** Live/demo/recovery image layered on the active desktop target
|
||||
- **Current truth:** Follows `redbear-full`; desktop/graphics-capable live image, but the greeter/login surface remains experimental until end-to-end proof exists
|
||||
- **Use for:** Demo, install, and bounded live-media validation on the current desktop surface
|
||||
|
||||
### `redbear-live`
|
||||
### `redbear-mini`
|
||||
|
||||
- **Role:** Live/demo/recovery image layered on the tracked desktop profile
|
||||
- **Current truth:** Inherits `redbear-kde`, so live media now follows the tracked KWin desktop target
|
||||
- **Use for:** Demo, install, and recovery workflows based on the current shipped desktop surface
|
||||
- **Role:** Minimal non-desktop target
|
||||
- **Current truth:** No desktop/graphics path; recovery and non-desktop integration surface only
|
||||
- **Use for:** Minimal runtime bring-up, subsystem validation, and non-desktop packaging checks
|
||||
|
||||
### `redbear-live-mini`
|
||||
|
||||
- **Role:** Minimal live image target
|
||||
- **Current truth:** No desktop/graphics path; live/recovery-oriented minimal image surface
|
||||
- **Use for:** Minimal live boot and recovery workflows
|
||||
|
||||
## Current Blockers
|
||||
|
||||
@@ -104,7 +115,31 @@ Phase 1 exists specifically to close this gap.
|
||||
A bounded compositor initialization reaches early startup but does not complete a usable Wayland compositor session.
|
||||
This blocks all desktop session work.
|
||||
|
||||
### 3. KWin reduced build is now dependency-honest, but runtime proof is still missing (Phase 3 gate)
|
||||
### 3. Greeter/login path now exists, but runtime proof is still missing (desktop-login gate)
|
||||
|
||||
The repo now carries the main non-visual pieces of the Red Bear-native greeter/login plan:
|
||||
|
||||
- `redbear-authd`
|
||||
- `redbear-session-launch`
|
||||
- `redbear-greeterd`
|
||||
- `redbear-greeter-services.toml`
|
||||
- `redbear-greeter-check`
|
||||
- `test-greeter-qemu.sh`
|
||||
|
||||
Current truth for that slice:
|
||||
|
||||
| Piece | Current state | Remaining limitation |
|
||||
|---|---|---|
|
||||
| `redbear-authd` | Target-side recipe build proven; unit tests cover passwd/shadow parsing, SHA-crypt verification, lockout, approval checks | No bounded in-guest login proof yet |
|
||||
| `redbear-session-launch` | Target-side recipe build proven; unit tests cover env/runtime-dir/argument handling | Real session handoff still depends on full greeter/runtime proof |
|
||||
| `redbear-greeterd` | Crate tests cover protocol-facing state strings, installed asset paths, bounded restart policy, and now own successful-login session launch directly after response delivery | Full desktop-login trust still depends on wider KDE runtime proof plus the unresolved guest-side Qt plugin-loader defect |
|
||||
| Greeter validation helpers | `redbear-greeter-check` + `test-greeter-qemu.sh` exist and are wired for bounded runtime proof | The successful-login path is validation-only and does not replace broader KDE session proof; current graphical proof is blocked by guest-side Qt plugin parsing rather than by greeter protocol/packaging gaps |
|
||||
| `redbear-greeter` packaging | Builds in-tree | Qt/QML UI binary, compositor wrapper, and branded assets are packaged; broader runtime trust still remains experimental because the guest-side Qt plugin loader currently rejects shared platform plugins (`libqminimal.so`, KWin QPA) as invalid ELF during metadata scan |
|
||||
|
||||
This means Red Bear now has a credible **build-visible login boundary**, but not yet a runtime-trusted
|
||||
graphical login surface.
|
||||
|
||||
### 4. KWin reduced build is now dependency-honest, but runtime proof is still missing (desktop-session gate)
|
||||
|
||||
The reduced KWin path now builds with honest provider linkage for `libepoxy`, `lcms2`, `libudev`,
|
||||
and `libdisplay-info`.
|
||||
@@ -121,7 +156,7 @@ Current truth for that slice:
|
||||
Additionally, two packages still need more honest session-ready treatment: kirigami (stub-only),
|
||||
kf6-kio (heavy shim).
|
||||
|
||||
### 4. Hardware acceleration missing GPU CS ioctl (Phase 5 gate)
|
||||
### 5. Hardware acceleration missing GPU CS ioctl (Phase 5 gate)
|
||||
|
||||
PRIME/DMA-BUF buffer sharing is implemented at the scheme level, and a bounded private CS
|
||||
surface now exists for shared-contract work. Real vendor render command submission and shared
|
||||
@@ -139,9 +174,10 @@ exercised on real Intel and AMD hardware.
|
||||
|---|---|
|
||||
| `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Canonical desktop path plan (v2.0, Phase 1–5) |
|
||||
| This document | Current build/runtime truth summary |
|
||||
| `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` | Canonical GPU/DRM execution plan beneath the desktop path |
|
||||
| `local/docs/QT6-PORT-STATUS.md` | Qt/KF6/KWin package-level build status |
|
||||
| `local/docs/AMD-FIRST-INTEGRATION.md` | AMD-specific hardware/driver detail |
|
||||
| `docs/03-WAYLAND-ON-REDOX.md` | Historical Wayland design rationale |
|
||||
| `local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` | Canonical Wayland subsystem plan |
|
||||
| `docs/05-KDE-PLASMA-ON-REDOX.md` | Historical KDE design rationale |
|
||||
| `local/docs/PROFILE-MATRIX.md` | Profile roles and support-language reference |
|
||||
|
||||
@@ -149,8 +185,9 @@ exercised on real Intel and AMD hardware.
|
||||
|
||||
The Red Bear desktop stack has crossed major build-side gates:
|
||||
- All Qt6 core modules, all 32 KF6 frameworks, Mesa EGL/GBM/GLES2, and D-Bus build
|
||||
- Three tracked desktop profiles exist and at least boot in QEMU
|
||||
- Four supported compile targets exist, with desktop/graphics on `redbear-full` and `redbear-live-full`
|
||||
- the non-visual Red Bear-native greeter/login pieces now build and test
|
||||
- relibc compatibility is materially stronger than before
|
||||
|
||||
The remaining work is **runtime validation, session assembly, and the remaining KDE session/runtime proof work**.
|
||||
Phase 1 (Runtime Substrate Validation) remains the immediate next target, while the KWin reduced path still lacks runtime compositor/session proof.
|
||||
The remaining work is **runtime validation, greeter/UI completion, session assembly, and the remaining KDE session/runtime proof work**.
|
||||
Phase 1 (Runtime Substrate Validation) remains the immediate broad target, while the new greeter/login path and the KWin reduced path both still need bounded runtime proof before stronger claims are safe.
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
**Supersedes as planning authority:**
|
||||
|
||||
- `local/docs/AMD-FIRST-INTEGRATION.md` for forward execution order
|
||||
- `local/docs/P2-AMD-GPU-DISPLAY.md` for future-task sequencing
|
||||
- `local/docs/HARDWARE-3D-ASSESSMENT.md` for roadmap ordering
|
||||
- `local/docs/DMA-BUF-IMPROVEMENT-PLAN.md` for PRIME/render dependency ordering
|
||||
|
||||
@@ -54,7 +53,7 @@ The repo has real progress in shared DRM/KMS, GEM, PRIME, firmware plumbing, int
|
||||
| KMS ioctl surface | Implemented in shared scheme layer | `local/recipes/gpu/redox-drm/source/src/scheme.rs` |
|
||||
| GEM allocation and mapping | Implemented in shared scheme and GEM manager | `local/recipes/gpu/redox-drm/source/src/gem.rs`, `local/recipes/gpu/redox-drm/source/src/scheme.rs` |
|
||||
| PRIME and DMA-BUF style sharing | Implemented at scheme level | `local/docs/HARDWARE-3D-ASSESSMENT.md`, `local/docs/DMA-BUF-IMPROVEMENT-PLAN.md`, `local/recipes/gpu/redox-drm/source/src/scheme.rs` |
|
||||
| AMD display backend | Build-visible, firmware-aware, interrupt-aware | `local/recipes/gpu/redox-drm/source/src/drivers/amd/mod.rs`, `local/recipes/gpu/amdgpu/source/amdgpu_redox_main.c` |
|
||||
| AMD display backend | Build-visible on the bounded retained path, firmware-aware, interrupt-aware | `local/recipes/gpu/redox-drm/source/src/drivers/amd/mod.rs`, `local/recipes/gpu/amdgpu/source/amdgpu_redox_main.c` |
|
||||
| Intel display backend | Build-visible, GGTT and ring scaffolding present | `local/recipes/gpu/redox-drm/source/src/drivers/intel/mod.rs`, `.../intel/ring.rs` |
|
||||
| Mesa userland base | Builds with EGL, GBM, OSMesa, software Gallium path | `recipes/libs/mesa/recipe.toml` |
|
||||
|
||||
@@ -255,6 +254,8 @@ quirk path. Do not use the Linux quirk extractor as a substitute for PCI naming
|
||||
- the raw `(crtc_id, vblank_count)` IRQ tuple path has been replaced with a small shared driver-event model for internal driver → main loop → scheme transport.
|
||||
- `scheme.rs` now owns event ingestion through a shared helper, so page-flip retirement remains tied to explicit vblank events while non-vblank events do not pretend to be render completion.
|
||||
- both Intel and AMD now forward shared hotplug events through the same internal event path instead of backend-specific side handling.
|
||||
- `scheme.rs` now turns shared hotplug and vblank events into a queued scheme-visible `EVENT_READ` surface for `card0`, and hotplug also targets the matching connector handle.
|
||||
- unit tests now cover card-level hotplug readiness, connector-targeted hotplug readiness, queued vblank delivery, and event draining, while preserving the rule that non-vblank events do not retire pending page flips.
|
||||
- this is structural groundwork only; real fence objects, sync waits, and backend-proven render completion semantics are still not implemented.
|
||||
|
||||
### Workstream C, Intel backend maturation
|
||||
@@ -276,7 +277,7 @@ quirk path. Do not use the Linux quirk extractor as a substitute for PCI naming
|
||||
|
||||
### Workstream D, AMD backend maturation
|
||||
|
||||
**Goal:** Turn the AMD path from code-complete display work plus amdgpu port scaffolding into an evidence-backed modern AMD track.
|
||||
**Goal:** Turn the AMD path from a bounded retained display build plus broader imported amdgpu/DC triage into an evidence-backed modern AMD track.
|
||||
|
||||
**Tasks:**
|
||||
|
||||
|
||||
@@ -0,0 +1,876 @@
|
||||
# Red Bear OS Greeter / Login Implementation Plan
|
||||
|
||||
**Version:** 1.0 — 2026-04-19
|
||||
**Status:** Active plan with experimental implementation in progress on `redbear-full`
|
||||
**Scope:** Red Bear-native graphical greeter, authentication boundary, and session handoff for the KDE-on-Wayland desktop path
|
||||
**Parent plans:** `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` (v2.0), `local/docs/DBUS-INTEGRATION-PLAN.md`
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Executive Summary](#1-executive-summary)
|
||||
2. [Scope and Non-Goals](#2-scope-and-non-goals)
|
||||
3. [Evidence Model](#3-evidence-model)
|
||||
4. [Current State Assessment](#4-current-state-assessment)
|
||||
5. [Decision Record: Login-Manager Direction](#5-decision-record-login-manager-direction)
|
||||
6. [Architecture Principles](#6-architecture-principles)
|
||||
7. [Architecture Design](#7-architecture-design)
|
||||
8. [Component Specifications](#8-component-specifications)
|
||||
9. [Protocols and Session Contracts](#9-protocols-and-session-contracts)
|
||||
10. [Phased Implementation](#10-phased-implementation)
|
||||
11. [Testing and Validation](#11-testing-and-validation)
|
||||
12. [Risks and Mitigations](#12-risks-and-mitigations)
|
||||
13. [Relationship to Other Plans](#13-relationship-to-other-plans)
|
||||
14. [File and Recipe Inventory](#14-file-and-recipe-inventory)
|
||||
15. [Open Questions](#15-open-questions)
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive Summary
|
||||
|
||||
Red Bear OS currently has enough session substrate to start **one fixed KDE Wayland session**, but it does
|
||||
not yet have a real graphical login path.
|
||||
|
||||
What exists today:
|
||||
|
||||
- `dbus-daemon` on the system bus
|
||||
- `redbear-sessiond` exposing a minimal `org.freedesktop.login1` subset for KWin
|
||||
- `seatd` as the seat/libseat backend
|
||||
- a direct session launcher (`redbear-kde-session`) that starts `kwin_wayland`
|
||||
- fallback text `getty` surfaces on VT2 and `/scheme/debug/no-preserve`
|
||||
|
||||
What does **not** exist today:
|
||||
|
||||
- no display manager
|
||||
- no graphical greeter
|
||||
- no authentication daemon
|
||||
- no session-launch privilege boundary
|
||||
- no PAM-backed or systemd-logind-shaped login stack
|
||||
|
||||
This plan defines the forward path for the missing layer:
|
||||
|
||||
1. **Do not adopt SDDM first.** Upstream KDE convention points to SDDM, but the current Red Bear
|
||||
session/auth substrate is not yet shaped like a conventional Linux desktop-login environment.
|
||||
2. **Build a Red Bear-native minimal greeter/login path first.** The system should present one
|
||||
graphical login surface for one session only: **KDE on Wayland**.
|
||||
3. **Keep the architecture narrow.** Separate:
|
||||
- `redbear-sessiond` → login1/session compatibility for KWin
|
||||
- `redbear-greeter` → login UX and session orchestration
|
||||
- `redbear-authd` → credential verification and privilege boundary
|
||||
- `redbear-session-launch` → user-session bootstrap only
|
||||
|
||||
This plan intentionally avoids generic display-manager scope. Red Bear wants **one desktop direction**,
|
||||
not a multi-session desktop-manager framework.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope and Non-Goals
|
||||
|
||||
### 2.1 In Scope
|
||||
|
||||
- One graphical login surface for the Red Bear KDE-on-Wayland desktop path
|
||||
- A Red Bear-native greeter daemon, greeter UI, authentication daemon, and session launcher
|
||||
- Integration with existing `dbus-daemon`, `redbear-sessiond`, `seatd`, `inputd`, and `redbear-kde-session`
|
||||
- Explicit VT ownership and handoff on the desktop VT
|
||||
- A narrow local-user authentication model backed by `/etc/passwd`, `/etc/shadow`, and `/etc/group`
|
||||
- Branding integration using Red Bear assets from `local/Assets/`
|
||||
- Packaging and config wiring under `local/recipes/system/` and tracked `config/redbear-*.toml`
|
||||
|
||||
This plan applies only to the **graphical desktop path**. It does **not** replace console-first or
|
||||
minimal non-desktop configurations. Existing text and debug console surfaces remain part of the
|
||||
recovery model.
|
||||
|
||||
### 2.2 Out of Scope
|
||||
|
||||
- X11 login surfaces
|
||||
- multiple desktop environments
|
||||
- session chooser UI
|
||||
- remote authentication
|
||||
- PAM/NSS plugin ecosystems
|
||||
- LDAP/SSO/smartcard/fingerprint login
|
||||
- graphical lock screen / unlock manager
|
||||
- full Plasma session-manager semantics (`ksmserver`, multi-user desktop switching)
|
||||
|
||||
### 2.3 Policy Assumption
|
||||
|
||||
This plan assumes the Red Bear desktop direction converges on **one KDE-on-Wayland path**.
|
||||
|
||||
Current implementation answer: the first tracked owner is `redbear-full` (and therefore
|
||||
`redbear-live-full` for live media). Older names such as `redbear-kde` may still appear in
|
||||
historical or staging material, but they are not the supported compile-target surface for this plan.
|
||||
|
||||
---
|
||||
|
||||
## 3. Evidence Model
|
||||
|
||||
This plan uses the same evidence language as the canonical desktop plan.
|
||||
|
||||
| Class | Meaning | Safe to say | Not safe to say |
|
||||
|---|---|---|---|
|
||||
| **builds** | Package compiles and stages | "builds" | "works" |
|
||||
| **boots** | Image reaches prompt or known runtime surface | "boots" | "desktop works" |
|
||||
| **enumerates** | Service/register surface appears and answers basic queries | "enumerates" | "usable end to end" |
|
||||
| **usable** | Bounded runtime path performs its intended task | "usable for this path" | "broadly stable" |
|
||||
| **validated** | Repeated proof on the intended target class | "validated" | "complete everywhere" |
|
||||
| **experimental** | Partial, scaffolded, or unproven | "experimental" | "done" |
|
||||
|
||||
Rules:
|
||||
|
||||
- A greeter binary that compiles is only **builds**.
|
||||
- A VM image that reaches a graphical login surface is only **boots**.
|
||||
- A greeter that hands off to KDE on Wayland in bounded QEMU proof is **usable (bounded)**.
|
||||
- Nothing is **validated** until it repeats reliably on the intended target class.
|
||||
|
||||
---
|
||||
|
||||
## 4. Current State Assessment
|
||||
|
||||
### 4.1 What Exists and Works
|
||||
|
||||
| Component | Location | Status | Detail |
|
||||
|---|---|---|---|
|
||||
| system D-Bus | `config/redbear-full.toml` | ✅ usable (bounded) | `12_dbus.service` starts `dbus-daemon --system` on the active desktop target |
|
||||
| login1 compatibility | `local/recipes/system/redbear-sessiond/` | ✅ scaffold | Minimal `org.freedesktop.login1` broker for KWin |
|
||||
| seat backend | `config/redbear-full.toml` | ✅ builds, wired | `13_seatd.service`; session env exports `LIBSEAT_BACKEND=seatd` |
|
||||
| display VT activation | `29_activate_console.service` in desktop configs | ✅ usable (bounded) | `inputd -A 3` activates desktop VT |
|
||||
| fallback text login | `30_console.service` | ✅ boots | `getty 2` on VT2 |
|
||||
| debug console | `31_debug_console.service` | ✅ boots | `getty /scheme/debug/no-preserve -J` |
|
||||
| direct KDE session launcher | `/usr/bin/redbear-kde-session` | ✅ builds, experimental | Starts session bus if needed, then `exec kwin_wayland --replace` |
|
||||
| authentication daemon | `local/recipes/system/redbear-authd/` | ✅ builds, experimental | Local-user auth boundary with `/etc/passwd` / `/etc/shadow` / `/etc/group` parsing and SHA-crypt verification |
|
||||
| session launcher boundary | `local/recipes/system/redbear-session-launch/` | ✅ builds, experimental | User-session bootstrap with bounded environment/runtime-dir setup |
|
||||
| greeter daemon scaffold | `local/recipes/system/redbear-greeter/` | ✅ builds, experimental | Root-owned greeter orchestrator, socket protocol, bounded restart policy |
|
||||
| greeter config fragment | `config/redbear-greeter-services.toml` | ✅ builds, experimental | Adds `19_redbear-authd.service`, `20_greeter.service`, compatibility `20_display.service`, and fallback console dependencies |
|
||||
| bounded validation launcher | `/usr/bin/redbear-validation-session` | ✅ retained helper | Still available for older bounded validation flows, but no longer the primary `redbear-full` display-service path |
|
||||
| branding assets | `local/Assets/images/` | ✅ present | `Red Bear OS loading background.png`, `Red Bear OS icon.png` |
|
||||
|
||||
### 4.2 What Exists But Is Incomplete
|
||||
|
||||
| Component | Status | Gap |
|
||||
|---|---|---|
|
||||
| `redbear-sessiond` seat switching | ⚠️ scaffold | `Seat.SwitchTo` is currently logged/delegated externally to `inputd -A` |
|
||||
| KDE runtime services | ⚠️ partial | D-Bus substrate exists, but broader Plasma session services remain incomplete |
|
||||
| `redbear-full` greeter flow | ⚠️ experimental | Non-visual pieces are implemented, but final packaged UI and bounded runtime proof are still pending |
|
||||
| greeter runtime validation | ⚠️ partial | `redbear-greeter-check` + `test-greeter-qemu.sh` exist, but final proof still depends on the packaged greeter UI |
|
||||
|
||||
### 4.3 What Does Not Exist
|
||||
|
||||
| Missing piece | Why it matters |
|
||||
|---|---|
|
||||
| packaged graphical greeter UI | no complete user-visible graphical login surface is staged yet |
|
||||
| bounded end-to-end login proof | build-side pieces exist, but runtime-trusted login/session handoff is not proven yet |
|
||||
| shared login protocol extraction | current protocol is encoded directly in the first-cut daemon/checker implementations |
|
||||
| display-manager package integration | no SDDM/greetd/lightdm/ly path in repo |
|
||||
|
||||
### 4.4 Baseline Conclusion
|
||||
|
||||
The current Red Bear desktop path can **start a session**, but it cannot yet **own a login flow**.
|
||||
|
||||
The missing work is not “port more KDE packages.” The missing work is the **login boundary**:
|
||||
|
||||
1. graphical greeter surface,
|
||||
2. authentication boundary,
|
||||
3. session-launch privilege drop,
|
||||
4. clean handoff into the existing KDE Wayland session path.
|
||||
|
||||
That login boundary must be added **without** replacing the current seat/session substrate and
|
||||
without removing existing console recovery paths.
|
||||
|
||||
---
|
||||
|
||||
## 5. Decision Record: Login-Manager Direction
|
||||
|
||||
### 5.1 Recommendation
|
||||
|
||||
The best fit for Red Bear OS **today** is a **Red Bear-native minimal single-session greeter/launcher**.
|
||||
|
||||
This is closer in class to **greetd-style minimal orchestration** than to **SDDM-style full desktop
|
||||
manager behavior**, but the forward path should be **Red Bear-specific**, not a generic Linux deployment.
|
||||
|
||||
### 5.2 Why Not SDDM First
|
||||
|
||||
SDDM is the standard answer for a conventional KDE distribution, but Red Bear is not yet a conventional
|
||||
Linux-shaped session/auth environment.
|
||||
|
||||
The repo evidence today shows:
|
||||
|
||||
- no SDDM integration,
|
||||
- no PAM path,
|
||||
- no mature general-purpose display-manager substrate,
|
||||
- a deliberately minimal `login1` compatibility layer,
|
||||
- a fixed single desktop direction.
|
||||
|
||||
Adopting SDDM first would force Red Bear to emulate a broader environment before the current narrower
|
||||
session path is runtime-trusted.
|
||||
|
||||
### 5.3 Ranked Direction
|
||||
|
||||
| Rank | Direction | Verdict |
|
||||
|---|---|---|
|
||||
| 1 | Red Bear-native minimal greeter | **Primary** |
|
||||
| 2 | Current direct session launcher | bring-up baseline only |
|
||||
| 3 | SDDM-class integration | future option after session/auth substrate matures |
|
||||
| 4 | GDM/LightDM/elogind-shaped path | reject |
|
||||
|
||||
### 5.4 Future Revisit Trigger
|
||||
|
||||
Revisit SDDM-class integration only if Red Bear later decides it needs:
|
||||
|
||||
- richer multi-user semantics,
|
||||
- session chooser behavior,
|
||||
- broader desktop-manager policy surface,
|
||||
- significantly fuller login/session accounting than the current `redbear-sessiond` contract.
|
||||
|
||||
---
|
||||
|
||||
## 6. Architecture Principles
|
||||
|
||||
### 6.1 One Desktop, One Session Path
|
||||
|
||||
The greeter must launch exactly one session target:
|
||||
|
||||
- **KDE on Wayland**
|
||||
|
||||
There is no session chooser in v1.
|
||||
|
||||
### 6.2 Keep the Existing Session Substrate
|
||||
|
||||
Reuse existing pieces rather than replacing them:
|
||||
|
||||
- `dbus-daemon`
|
||||
- `redbear-sessiond`
|
||||
- `seatd`
|
||||
- `inputd`
|
||||
- `redbear-kde-session`
|
||||
|
||||
The greeter layer sits **above** them.
|
||||
|
||||
`seatd` remains the **seat/device authority** for this design. The greeter stack consumes the existing
|
||||
seat/libseat path; it does not introduce a second seat/session-manager authority.
|
||||
|
||||
### 6.3 Separate Login UX, Authentication, and Session Bootstrap
|
||||
|
||||
Do not collapse these roles into one process.
|
||||
|
||||
| Component | Responsibility |
|
||||
|---|---|
|
||||
| `redbear-sessiond` | login1/session compatibility for KWin |
|
||||
| `redbear-greeterd` | login flow orchestration |
|
||||
| `redbear-greeter-ui` | graphical login UX |
|
||||
| `redbear-authd` | credential verification and privilege boundary |
|
||||
| `redbear-session-launch` | drop privileges, set env, start user session |
|
||||
|
||||
`redbear-sessiond` is therefore **not** the login/auth/session-launch authority. It remains the
|
||||
KWin-facing session compatibility broker defined by the D-Bus plan.
|
||||
|
||||
### 6.4 Avoid a PAM Clone
|
||||
|
||||
Red Bear should not build a new generic PAM/NSS plugin ecosystem merely to satisfy display-manager
|
||||
expectations. For this path, use a narrow local account model first.
|
||||
|
||||
### 6.5 Stop-and-Start Handoff Is Acceptable
|
||||
|
||||
The greeter and the user session do not need an in-place seamless transition in v1.
|
||||
|
||||
It is acceptable to:
|
||||
|
||||
1. stop the greeter UI,
|
||||
2. start the user session cleanly,
|
||||
3. return to the greeter after session exit.
|
||||
|
||||
### 6.6 Branding Is Part of the Product Surface
|
||||
|
||||
Use committed Red Bear assets as the default greeter look.
|
||||
|
||||
Source-of-truth art files in the repo are:
|
||||
|
||||
- background: `local/Assets/images/Red Bear OS loading background.png`
|
||||
- icon: `local/Assets/images/Red Bear OS icon.png`
|
||||
|
||||
At runtime, the greeter must use **installed asset paths**, not source-tree paths.
|
||||
|
||||
---
|
||||
|
||||
## 7. Architecture Design
|
||||
|
||||
### 7.1 Stack Overview
|
||||
|
||||
```text
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ KDE Wayland user session │
|
||||
│ redbear-kde-session → kwin_wayland → later Plasma services │
|
||||
├────────────────────────────────────────────────────────────────────┤
|
||||
│ redbear-session-launch │
|
||||
│ drop privileges, set env, start session bus, exec session │
|
||||
├────────────────────────────────────────────────────────────────────┤
|
||||
│ redbear-authd redbear-greeter-ui │
|
||||
│ local auth + privilege boundary Qt6/QML login surface │
|
||||
├────────────────────────────────────────────────────────────────────┤
|
||||
│ redbear-greeterd │
|
||||
│ login state machine, VT3 ownership, auth/session orchestration │
|
||||
├────────────────────────────────────────────────────────────────────┤
|
||||
│ dbus-daemon --system redbear-sessiond seatd inputd │
|
||||
├────────────────────────────────────────────────────────────────────┤
|
||||
│ Redox schemes / system services │
|
||||
│ scheme:input, scheme:acpi, scheme:drm, debug scheme, etc. │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 7.2 Boot-to-Login Sequence
|
||||
|
||||
```text
|
||||
boot
|
||||
→ 12_dbus.service (system D-Bus)
|
||||
→ 13_redbear-sessiond.service (login1 subset)
|
||||
→ 13_seatd.service (seat backend)
|
||||
→ 20_greeter.service (start redbear-greeterd on VT3)
|
||||
→ 29_activate_console.service (inputd -A 3)
|
||||
→ 30_console.service (fallback getty 2 on VT2)
|
||||
→ 31_debug_console.service (debug getty)
|
||||
→ redbear-greeter-ui shows login surface on VT3
|
||||
→ successful login
|
||||
→ redbear-session-launch
|
||||
→ dbus-run-session -- redbear-kde-session
|
||||
→ kwin_wayland
|
||||
```
|
||||
|
||||
### 7.3 Session Return Path
|
||||
|
||||
```text
|
||||
user session exits or crashes
|
||||
→ redbear-greeterd observes session root exit
|
||||
→ greeter-specific cleanup
|
||||
→ reactivate VT3
|
||||
→ respawn redbear-greeter-ui
|
||||
→ return to login surface
|
||||
```
|
||||
|
||||
### 7.4 Why This Shape Fits the Repo
|
||||
|
||||
- matches existing `VT=3` display path
|
||||
- preserves fallback text login on VT2
|
||||
- reuses `redbear-sessiond` instead of replacing it
|
||||
- does not assume a broader Linux-style session-manager stack than the repo currently has
|
||||
- avoids dead-end graphical boot behavior by preserving text/debug fallback paths
|
||||
|
||||
---
|
||||
|
||||
## 8. Component Specifications
|
||||
|
||||
### 8.1 `redbear-greeterd`
|
||||
|
||||
**Type:** root-owned orchestrator daemon
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- own the login state machine,
|
||||
- own greeter/UI lifecycle,
|
||||
- talk to `redbear-authd`,
|
||||
- start the user session via `redbear-session-launch`,
|
||||
- monitor the session root process,
|
||||
- return to greeter after logout/session crash.
|
||||
|
||||
**Must not do:**
|
||||
|
||||
- parse or verify passwords directly unless `redbear-authd` is intentionally collapsed into it,
|
||||
- render the login UI,
|
||||
- absorb generic session-manager policy.
|
||||
|
||||
### 8.2 `redbear-greeter-ui`
|
||||
|
||||
**Type:** unprivileged Qt6/QML frontend
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- render Red Bear background and icon,
|
||||
- collect username/password,
|
||||
- present Login / Shutdown / Reboot,
|
||||
- show bounded status (`Authenticating`, `Login failed`, `Starting session`).
|
||||
|
||||
**Must not do:**
|
||||
|
||||
- read `/etc/shadow`,
|
||||
- own power/device/session policy,
|
||||
- choose alternate desktop sessions.
|
||||
|
||||
### 8.3 `redbear-authd`
|
||||
|
||||
**Type:** privileged authentication daemon
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- read local user data,
|
||||
- verify password hashes,
|
||||
- check lock/disable rules,
|
||||
- perform narrow privileged actions (`login`, optional `shutdown`, optional `reboot`),
|
||||
- spawn `redbear-session-launch` for a verified user.
|
||||
|
||||
**Must not do:**
|
||||
|
||||
- own the greeter UI,
|
||||
- own compositor startup policy,
|
||||
- become a general identity platform.
|
||||
|
||||
`redbear-authd` is the **only** component in this plan allowed to read password-hash data
|
||||
(` /etc/shadow`-equivalent runtime content). Neither UI nor session launcher may touch it.
|
||||
|
||||
### 8.4 `redbear-session-launch`
|
||||
|
||||
**Type:** small bootstrap tool
|
||||
|
||||
**Responsibilities:**
|
||||
|
||||
- create/fix `XDG_RUNTIME_DIR`,
|
||||
- drop to target uid/gid and supplementary groups,
|
||||
- construct a minimal KDE/Wayland environment,
|
||||
- launch the user session bus,
|
||||
- exec `redbear-kde-session`.
|
||||
|
||||
`redbear-session-launch` is intentionally thin. It must not duplicate KDE session policy already owned
|
||||
by `redbear-kde-session`.
|
||||
|
||||
### 8.5 `redbear-sessiond`
|
||||
|
||||
This plan does **not** replace `redbear-sessiond`.
|
||||
|
||||
It remains responsible for:
|
||||
|
||||
- `org.freedesktop.login1` subset for KWin,
|
||||
- session/seat compatibility surface,
|
||||
- bounded power/sleep integration already assigned in the D-Bus plan.
|
||||
|
||||
---
|
||||
|
||||
## 9. Protocols and Session Contracts
|
||||
|
||||
### 9.1 UI ↔ Greeter Daemon Protocol
|
||||
|
||||
Transport:
|
||||
|
||||
- Unix socket at `/run/redbear-greeterd.sock`
|
||||
- JSON messages, versioned
|
||||
|
||||
Minimum message set:
|
||||
|
||||
```json
|
||||
{ "type": "hello", "version": 1 }
|
||||
{ "type": "submit_login", "username": "alice", "password": "secret" }
|
||||
{ "type": "request_shutdown" }
|
||||
{ "type": "request_reboot" }
|
||||
```
|
||||
|
||||
Example reply:
|
||||
|
||||
```json
|
||||
{ "type": "hello_ok", "background": "/usr/share/redbear/greeter/background.png", "icon": "/usr/share/redbear/greeter/icon.png", "session_name": "KDE on Wayland" }
|
||||
```
|
||||
|
||||
### 9.2 Greeter Daemon ↔ Auth Daemon Protocol
|
||||
|
||||
Transport:
|
||||
|
||||
- Unix socket at `/run/redbear-authd.sock`
|
||||
- JSON messages, versioned
|
||||
|
||||
Minimum message set:
|
||||
|
||||
```json
|
||||
{ "type": "authenticate", "request_id": 17, "username": "alice", "password": "secret", "vt": 3 }
|
||||
{ "type": "start_session", "request_id": 17, "username": "alice", "session": "kde-wayland" }
|
||||
```
|
||||
|
||||
### 9.3 State Machine
|
||||
|
||||
`redbear-greeterd` uses this state set:
|
||||
|
||||
1. `Starting`
|
||||
2. `GreeterReady`
|
||||
3. `Authenticating`
|
||||
4. `LaunchingSession`
|
||||
5. `SessionRunning`
|
||||
6. `ReturningToGreeter`
|
||||
7. `PowerAction`
|
||||
8. `FatalError`
|
||||
|
||||
Rules:
|
||||
|
||||
- one greeter UI process at a time,
|
||||
- one session launch in flight,
|
||||
- one supported session only: `kde-wayland`,
|
||||
- greeter UI never survives into `SessionRunning`.
|
||||
|
||||
### 9.4 Local Account Storage Contract
|
||||
|
||||
Use a simple Unix-like model first:
|
||||
|
||||
- `/etc/passwd`
|
||||
- `/etc/shadow`
|
||||
- `/etc/group`
|
||||
|
||||
This plan explicitly rejects inventing a new account database format for v1.
|
||||
|
||||
The local-account model is a **runtime contract**. Source-tree examples or provisioning helpers may live
|
||||
elsewhere, but the greeter/auth path must interact only with installed runtime account files.
|
||||
|
||||
### 9.5 Session-Launch Environment
|
||||
|
||||
`redbear-session-launch` should set a minimal explicit environment:
|
||||
|
||||
- `HOME`
|
||||
- `USER`
|
||||
- `LOGNAME`
|
||||
- `SHELL`
|
||||
- `PATH=/usr/bin:/bin`
|
||||
- `XDG_RUNTIME_DIR=/run/user/$UID`
|
||||
- `XDG_SESSION_TYPE=wayland`
|
||||
- `XDG_CURRENT_DESKTOP=KDE`
|
||||
- `KDE_FULL_SESSION=true`
|
||||
- `WAYLAND_DISPLAY=wayland-0`
|
||||
- `XDG_SEAT=seat0`
|
||||
- `XDG_VTNR=3`
|
||||
- `LIBSEAT_BACKEND=seatd`
|
||||
- `SEATD_SOCK=/run/seatd.sock`
|
||||
|
||||
Preferred launch form:
|
||||
|
||||
```text
|
||||
dbus-run-session -- redbear-kde-session
|
||||
```
|
||||
|
||||
If `dbus-run-session` proves unreliable on Red Bear, use the current `dbus-launch` pattern as a bounded
|
||||
fallback.
|
||||
|
||||
### 9.6 Branding Contract
|
||||
|
||||
Stage the current assets at stable runtime paths:
|
||||
|
||||
- `/usr/share/redbear/greeter/background.png`
|
||||
- `/usr/share/redbear/greeter/icon.png`
|
||||
|
||||
Use:
|
||||
|
||||
- `Red Bear OS loading background.png` as the full-screen wallpaper
|
||||
- `Red Bear OS icon.png` above the login form
|
||||
|
||||
The greeter runtime must reference only the installed `/usr/share/redbear/greeter/*` paths.
|
||||
`local/Assets/...` remains the source-of-truth location in the repo, not a runtime lookup path.
|
||||
|
||||
### 9.7 Failure and Fallback Contract
|
||||
|
||||
Greeter failure must never create a dead-end boot surface.
|
||||
|
||||
Required behavior:
|
||||
|
||||
- VT2 `getty` remains available as text recovery,
|
||||
- debug `getty` remains available,
|
||||
- greeter failures return control to a recoverable state,
|
||||
- repeated greeter/UI restart failures must stop escalating after a bounded retry count,
|
||||
- the system must prefer a reachable fallback console over an infinite graphical restart loop.
|
||||
|
||||
---
|
||||
|
||||
## 10. Phased Implementation
|
||||
|
||||
### Phase G0 — Scope Freeze and Wiring Baseline
|
||||
|
||||
**Goal:** Freeze the architectural split and identify the tracked desktop profile(s) that will own the
|
||||
greeter path.
|
||||
|
||||
| # | Task | Acceptance criteria |
|
||||
|---|---|---|
|
||||
| G0.1 | Freeze component boundaries | `sessiond`, `greeterd`, `authd`, `session-launch` responsibilities documented without overlap |
|
||||
| G0.2 | Freeze single-session policy | Only `kde-wayland` is named as the supported graphical session |
|
||||
| G0.3 | Freeze branding inputs | Runtime asset paths and source asset files documented |
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- architecture split is documented,
|
||||
- session policy is explicit,
|
||||
- asset source of truth is explicit.
|
||||
|
||||
### Phase G1 — Service Skeleton and Boot Wiring
|
||||
|
||||
**Goal:** Add daemon/package skeletons and init wiring without claiming a usable login flow.
|
||||
|
||||
| # | Task | Acceptance criteria |
|
||||
|---|---|---|
|
||||
| G1.1 | Create recipe skeletons | `redbear-greeter`, `redbear-authd`, `redbear-session-launch`, optional `redbear-login-protocol` build and stage |
|
||||
| G1.2 | Add config fragment | A tracked config fragment wires `20_greeter.service` and supporting files |
|
||||
| G1.3 | Replace direct display launch in the chosen profile | Desktop profile starts `redbear-greeterd` instead of directly starting `redbear-kde-session` |
|
||||
| G1.4 | Keep text/debug recovery path | VT2 `getty` and debug `getty` still boot |
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- packages build,
|
||||
- boot wiring is in place,
|
||||
- image still boots,
|
||||
- fallback text surfaces remain reachable.
|
||||
|
||||
### Phase G2 — Auth Foundation
|
||||
|
||||
**Goal:** Prove the local account/authentication boundary independent of the full greeter UI.
|
||||
|
||||
| # | Task | Acceptance criteria |
|
||||
|---|---|---|
|
||||
| G2.1 | Implement passwd/shadow parsing | Local users can be parsed from the chosen account files |
|
||||
| G2.2 | Implement password verification | Valid and invalid credentials are distinguished correctly in tests |
|
||||
| G2.3 | Implement lock/disable rules | Locked/disabled users are rejected predictably |
|
||||
| G2.4 | Implement session-spawn authorization boundary | Only `redbear-authd` can approve session launch |
|
||||
| G2.5 | Implement bounded failure handling | Retry throttling / lockout policy is documented and covered by tests |
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- auth parser tests pass,
|
||||
- credential checks pass,
|
||||
- negative cases pass,
|
||||
- no UI process reads auth data,
|
||||
- repeated auth failure behavior is bounded and explicit.
|
||||
|
||||
### Phase G3 — Greeter UI and Daemon State Machine
|
||||
|
||||
**Goal:** Bring up the graphical greeter surface and daemon orchestration.
|
||||
|
||||
| # | Task | Acceptance criteria |
|
||||
|---|---|---|
|
||||
| G3.1 | Start greeter UI on VT3 | QEMU image reaches a Red Bear-branded graphical greeter surface |
|
||||
| G3.2 | Implement UI/daemon socket protocol | UI can submit login and power requests |
|
||||
| G3.3 | Implement daemon state machine | State transitions are test-covered for success and failure paths |
|
||||
| G3.4 | Implement bounded login error UX | Invalid credentials return cleanly to `GreeterReady` |
|
||||
| G3.5 | Implement failure fallback behavior | Greeter/UI restart failure yields reachable fallback behavior rather than infinite restart |
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- greeter surface boots,
|
||||
- UI/daemon protocol works,
|
||||
- failure returns to the login screen,
|
||||
- no session starts yet without auth success,
|
||||
- fallback console path remains reachable under greeter failure.
|
||||
|
||||
### Phase G4 — Session Handoff to KDE on Wayland
|
||||
|
||||
**Goal:** Replace direct session startup with authenticated session launch.
|
||||
|
||||
| # | Task | Acceptance criteria |
|
||||
|---|---|---|
|
||||
| G4.1 | Implement `redbear-session-launch` env/bootstrap path | Session runs with correct uid/gid/groups/runtime dir/env |
|
||||
| G4.2 | Implement greeter teardown before session launch | Greeter UI exits before KDE session becomes active |
|
||||
| G4.3 | Implement session-monitor return path | Session exit returns to the greeter |
|
||||
| G4.4 | Keep bounded D-Bus/sessiond compatibility intact | KWin still sees the required login1 subset |
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- successful login reaches `redbear-kde-session`,
|
||||
- session uses intended env/runtime dir,
|
||||
- session exit returns to greeter,
|
||||
- fallback VT2 login still works.
|
||||
|
||||
### Phase G5 — Desktop Integration and Product Surface Hardening
|
||||
|
||||
**Goal:** Move from “bounded login proof” to a product-quality Red Bear login surface.
|
||||
|
||||
| # | Task | Acceptance criteria |
|
||||
|---|---|---|
|
||||
| G5.1 | Implement reboot/shutdown path | Greeter can trigger bounded power actions |
|
||||
| G5.2 | Hardening | rate limiting, buffer clearing, socket permission checks, retry behavior |
|
||||
| G5.3 | Packaging and profile cleanup | Target desktop profile wiring is canonical and documented |
|
||||
| G5.4 | Validation tooling | scripted QEMU/runtime proof exists for greeter boot/login/logout loop |
|
||||
|
||||
**Exit criteria:**
|
||||
|
||||
- login loop is repeatable,
|
||||
- power actions are bounded and explicit,
|
||||
- hardening checks pass,
|
||||
- documentation matches shipped surface.
|
||||
|
||||
### Critical Path
|
||||
|
||||
```text
|
||||
G0 (scope)
|
||||
→ G1 (wiring)
|
||||
→ G2 (auth boundary)
|
||||
→ G3 (greeter surface)
|
||||
→ G4 (session handoff)
|
||||
→ G5 (product hardening)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Testing and Validation
|
||||
|
||||
### 11.1 Unit and Component Tests
|
||||
|
||||
| Component | Tests |
|
||||
|---|---|
|
||||
| `redbear-login-protocol` | message encoding/decoding, version checks |
|
||||
| `redbear-authd` | passwd parsing, shadow parsing, hash verification, lockout logic |
|
||||
| `redbear-session-launch` | env construction, runtime-dir creation, argument validation |
|
||||
| `redbear-greeterd` | state transitions, socket protocol handling, session-monitor behavior |
|
||||
| `redbear-greeter-ui` | smoke only; no auth logic in UI tests |
|
||||
|
||||
### 11.2 Integration Checks
|
||||
|
||||
The first bounded integration proofs should answer these questions in order:
|
||||
|
||||
1. does the image boot to a graphical greeter surface on VT3?
|
||||
2. does invalid login return to the greeter surface?
|
||||
3. does valid login reach `redbear-kde-session`?
|
||||
4. does session exit return to the greeter?
|
||||
5. do VT2 and debug login remain available as recovery paths?
|
||||
6. does greeter failure still leave a recoverable console path instead of looping forever?
|
||||
|
||||
### 11.3 Suggested Validation Commands / Harnesses
|
||||
|
||||
This plan expects a bounded QEMU harness similar in style to existing Red Bear runtime proofs.
|
||||
|
||||
Expected future surfaces:
|
||||
|
||||
- `local/scripts/test-greeter-qemu.sh`
|
||||
- in-target checker such as `redbear-greeter-check`
|
||||
|
||||
The exact script names are implementation details, but the proof style should match existing bounded
|
||||
runtime validation patterns already used elsewhere in the repo.
|
||||
|
||||
### 11.4 Definition of Done
|
||||
|
||||
This plan is only substantially complete when **all** of the following are true:
|
||||
|
||||
- a Red Bear-branded graphical greeter boots on the tracked KDE desktop path,
|
||||
- credentials are verified through a narrow privileged boundary,
|
||||
- valid login reaches KDE on Wayland,
|
||||
- invalid login returns cleanly to the greeter,
|
||||
- session exit returns to the greeter,
|
||||
- VT2 fallback and debug console remain available,
|
||||
- greeter/UI failure does not trap the machine in an unrecoverable restart loop,
|
||||
- the bounded login/logout proof repeats reliably on the intended target class.
|
||||
|
||||
---
|
||||
|
||||
## 12. Risks and Mitigations
|
||||
|
||||
| ID | Risk | Likelihood | Impact | Mitigation |
|
||||
|---|---|---:|---:|---|
|
||||
| R1 | `redbear-sessiond` login1 subset proves too thin for stable KWin session ownership | Medium | High | keep greeter plan explicitly dependent on D-Bus/sessiond validation; widen only the needed contract |
|
||||
| R2 | Auth layer grows into a PAM replacement by accident | Medium | High | freeze v1 to local users + passwd/shadow only |
|
||||
| R3 | Greeter UI becomes privileged by convenience | Medium | High | keep UI unprivileged and enforce daemon/auth socket boundary |
|
||||
| R4 | VT/session handoff is flaky on real targets | Medium | High | keep VT2 fallback path and validate QEMU before broader claims |
|
||||
| R5 | Profile ownership confusion (`redbear-kde` vs `redbear-full`) delays integration | High | Medium | keep profile naming a policy question separate from greeter architecture |
|
||||
| R6 | Branding/assets are staged inconsistently | Low | Medium | stage stable runtime paths under `/usr/share/redbear/greeter/` |
|
||||
| R7 | Session launch inherits too much ambient environment | Medium | Medium | start from a clean explicit environment in `redbear-session-launch` |
|
||||
| R8 | Greeter restart policy creates boot loops | Medium | High | bound retries and prefer console fallback after repeated failures |
|
||||
|
||||
---
|
||||
|
||||
## 13. Relationship to Other Plans
|
||||
|
||||
| Document | Role relative to this plan |
|
||||
|---|---|
|
||||
| `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` | Parent desktop-path authority; this plan fills the graphical login boundary beneath it |
|
||||
| `local/docs/DBUS-INTEGRATION-PLAN.md` | Parent session/D-Bus authority for `redbear-sessiond` and related service model |
|
||||
| `local/docs/DESKTOP-STACK-CURRENT-STATUS.md` | Current truth source for what the desktop stack actually builds/boots today |
|
||||
| `local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` | Wayland/compositor subsystem plan beneath the desktop path |
|
||||
| `docs/07-RED-BEAR-OS-IMPLEMENTATION-PLAN.md` | Repo-wide product/profile/workstream framing |
|
||||
|
||||
This document does **not** replace any of the above. It fills a missing subsystem-planning gap:
|
||||
the login/greeter boundary between a booted desktop substrate and a real KDE session surface.
|
||||
|
||||
---
|
||||
|
||||
## 14. File and Recipe Inventory
|
||||
|
||||
### 14.1 Existing Files This Plan Builds On
|
||||
|
||||
- `config/redbear-full.toml`
|
||||
- `config/redbear-greeter-services.toml`
|
||||
- `local/recipes/system/redbear-sessiond/`
|
||||
- `local/recipes/system/redbear-dbus-services/`
|
||||
- `local/Assets/images/Red Bear OS loading background.png`
|
||||
- `local/Assets/images/Red Bear OS icon.png`
|
||||
|
||||
### 14.2 Proposed New Recipe Layout
|
||||
|
||||
```text
|
||||
local/recipes/system/
|
||||
├── redbear-authd/
|
||||
├── redbear-session-launch/
|
||||
└── redbear-greeter/
|
||||
```
|
||||
|
||||
Current implementation status:
|
||||
|
||||
- `redbear-authd/` — implemented (experimental, target-side recipe build proven)
|
||||
- `redbear-session-launch/` — implemented (experimental, target-side recipe build proven)
|
||||
- `redbear-greeter/` — implemented as an experimental bounded surface; daemon, Qt/QML UI, compositor wrapper, staged assets, and bounded runtime checks now exist, while broader KDE runtime trust still remains open
|
||||
- Current blocker after the greeter/UI packaging work: guest-side Qt shared-plugin loading on Red Bear still rejects platform plugins during metadata scan (`libqminimal.so`, `qwayland-org.kde.kwin.qpa.so`) even though the plugin files are present in the image and their on-disk ELF headers read correctly via non-Qt tools. This blocks the bounded graphical compositor proof below the greeter slice.
|
||||
- `redbear-login-protocol/` — optional follow-up extraction, not required for the first bounded runtime proof
|
||||
|
||||
### 14.3 Proposed New Runtime Files
|
||||
|
||||
```text
|
||||
/usr/bin/redbear-greeterd
|
||||
/usr/bin/redbear-greeter-ui
|
||||
/usr/bin/redbear-authd
|
||||
/usr/bin/redbear-session-launch
|
||||
/usr/share/redbear/greeter/background.png
|
||||
/usr/share/redbear/greeter/icon.png
|
||||
/run/redbear-greeterd.sock
|
||||
/run/redbear-authd.sock
|
||||
/usr/bin/redbear-greeter-check
|
||||
```
|
||||
|
||||
Bounded validation helper currently landed:
|
||||
|
||||
```text
|
||||
local/scripts/test-greeter-qemu.sh
|
||||
```
|
||||
|
||||
### 14.4 Proposed Config Fragment
|
||||
|
||||
This plan expects a tracked config include fragment such as:
|
||||
|
||||
```text
|
||||
config/redbear-greeter-services.toml
|
||||
```
|
||||
|
||||
That fragment should own:
|
||||
|
||||
- package inclusions for greeter/auth/session-launch,
|
||||
- `20_greeter.service`,
|
||||
- any bounded init-service overrides needed to replace direct session startup.
|
||||
|
||||
The greeter **recipe**, not the config fragment, should own staged runtime artifacts such as:
|
||||
|
||||
- `/usr/bin/redbear-greeter-ui`
|
||||
- `/usr/share/redbear/greeter/background.png`
|
||||
- `/usr/share/redbear/greeter/icon.png`
|
||||
- compositor/helper payloads that the greeter package installs under `/usr/share/redbear/greeter/`
|
||||
|
||||
---
|
||||
|
||||
## 15. Open Questions
|
||||
|
||||
1. Which tracked profile should own the canonical desktop greeter path first:
|
||||
`redbear-kde`, `redbear-full`, or a unified future target?
|
||||
2. Which password-hash scheme should Red Bear standardize on for v1 local users?
|
||||
3. Should reboot/shutdown requests go through `redbear-authd` or a separate narrow power helper?
|
||||
4. Is `dbus-run-session` reliable enough on Red Bear, or should the current `dbus-launch` path remain the first shipped session-bus strategy?
|
||||
5. At what point should the project consider SDDM-class integration again, if ever?
|
||||
|
||||
Current answer to (1): **`redbear-full` first**, with `redbear-live-full` inheriting that path for
|
||||
live media.
|
||||
|
||||
Current answer to (2): **traditional `/etc/shadow` SHA-512-crypt / SHA-256-crypt first** (`$6$` / `$5$`),
|
||||
with narrower support preferred over premature multi-format sprawl.
|
||||
|
||||
Free/libre policy note for (2): the current verifier path uses the pure-Rust `sha-crypt` crate,
|
||||
which is licensed `MIT OR Apache-2.0`; for Red Bear policy purposes it is treated under the MIT
|
||||
option, keeping the greeter/login stack within a free/open-source dependency surface. The intended
|
||||
implementation direction remains **pure-Rust verification crates first**, not `crypt(3)` FFI.
|
||||
|
||||
Current answer to (3): **through `redbear-authd` in the first cut**, to preserve one narrow privileged
|
||||
boundary until runtime evidence justifies a separate helper.
|
||||
|
||||
Current answer to (4): **`dbus-run-session` remains the preferred first shipped path**, with fallback
|
||||
conservatism retained in validation/docs until broader runtime proof exists.
|
||||
|
||||
Current answer to (5): **not before the Red Bear-native greeter path is runtime-trusted and the
|
||||
session/auth substrate is materially stronger than it is today.**
|
||||
@@ -42,7 +42,7 @@ GPU hardware (AMD RDNA / Intel Gen)
|
||||
| Component | Status | Lines | What's Implemented |
|
||||
|-----------|--------|-------|-------------------|
|
||||
| DRM/KMS modesetting | ✅ Code complete | ~500 | 16 KMS ioctls, CRTC/connector/encoder/plane |
|
||||
| AMD Display Core | ✅ Compiles | ~1400 | DC init, CRTC programming, firmware loading, HPD |
|
||||
| AMD display backend (bounded retained path) | ✅ Builds | ~2 C glue files + Rust FFI surface | Red Bear display glue (`amdgpu_redox_main.c`, `redox_stubs.c`) plus the Rust FFI consumer build; imported Linux AMD DC/TTM/core remain under compile triage |
|
||||
| Intel Display Driver | ✅ Compiles | ~800 | Display pipe, GGTT, forcewake |
|
||||
| GEM buffer management | ✅ Full | ~350 | create/close/mmap with DmaBuffer |
|
||||
| GEM scheme ioctls | ✅ Wired | ~100 | GEM_CREATE, GEM_CLOSE, GEM_MMAP |
|
||||
@@ -167,5 +167,5 @@ platform priority.
|
||||
|
||||
- `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` — Phase 5 covers hardware GPU enablement
|
||||
- `local/docs/AMD-FIRST-INTEGRATION.md` — AMD-specific GPU driver details
|
||||
- `local/docs/P2-AMD-GPU-DISPLAY.md` — Display driver code-complete status
|
||||
- `local/docs/AMDGPU-DC-COMPILE-TRIAGE-PLAN.md` — AMD DC compile-triage and bounded source-set strategy
|
||||
- `docs/04-LINUX-DRIVER-COMPAT.md` — linux-kpi architecture reference
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**Purpose**: Implementation-ready hardware register and data structure reference for Red Bear OS IOMMU support. Based on AMD IOMMU Specification 48882 Rev 3.10 and Intel Virtualization Technology for Directed I/O (VT-d) Rev 5.0.
|
||||
|
||||
**Status**: The `iommu` daemon now builds in-tree, but hardware validation is still missing in the AMD-first integration plan (see `AMD-FIRST-INTEGRATION.md`). This document provides the register and data-structure reference for finishing AMD-Vi and Intel VT-d bring-up.
|
||||
**Status**: The `iommu` daemon now builds in-tree, owns AMD-Vi runtime initialization, and also detects the presence of a kernel ACPI `DMAR` table so Intel VT-d runtime ownership can converge here instead of remaining conceptually stranded in `acpid`. Hardware validation is still missing in the AMD-first integration plan (see `AMD-FIRST-INTEGRATION.md`). This document provides the register and data-structure reference for finishing AMD-Vi and Intel VT-d bring-up.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -78,6 +78,9 @@ Red Bear OS already has a meaningful low-level controller and interrupt foundati
|
||||
including PCI scheme-entry parsing bounds, I/O BAR port conversion safety, and MSI-X BAR window
|
||||
helper validation. This should be treated as **source + host-test evidence**, not as runtime
|
||||
controller proof.
|
||||
- `redox-driver-sys` fast PCI enumeration now preserves capability-chain data from config-space
|
||||
bytes instead of returning empty capability lists, and exposes a quirk-aware interrupt-support
|
||||
summary (`none` / `legacy` / `msi` / `msix`) for downstream policy convergence.
|
||||
- `redox-drm` already contains a shared interrupt abstraction with MSI-X-first and legacy-IRQ
|
||||
fallback paths for GPU drivers.
|
||||
- The AMD-Vi / Intel VT-d reference material and the in-tree `iommu` daemon establish a serious
|
||||
@@ -328,6 +331,9 @@ Open enhancement items:
|
||||
- the repo now has a bounded PS/2 runtime-proof path via `redbear-phase-ps2-check` and
|
||||
`local/scripts/test-ps2-qemu.sh --check`, which proves serio node presence and a successful
|
||||
handoff into the existing Phase 3 input-path checker inside a guest
|
||||
- `ps2d` controller init now also drains stale controller output during probe and around the core
|
||||
init/self-test path, which is the current bounded Red Bear-native equivalent of Linux i8042 flush
|
||||
discipline before broader PS/2 suspend/resume work exists.
|
||||
|
||||
### USB xHCI controller interrupt path
|
||||
|
||||
|
||||
@@ -12,6 +12,43 @@ This document answers a specific Red Bear question:
|
||||
|
||||
This plan is intentionally **Red Bear-native**. It does **not** propose importing Linux subsystem architecture into Red Bear.
|
||||
|
||||
## Current implementation status snapshot (2026-04-18)
|
||||
|
||||
The software-only, bounded slices from this plan that are now implemented in code are:
|
||||
|
||||
- **Phase A — PCI / IRQ substrate**
|
||||
- shared PCI config-space parsing now preserves capability chains in `redox-driver-sys`
|
||||
- shared quirk-aware interrupt support summary exists (`none` / `legacy` / `msi` / `msix`)
|
||||
- `pcid` now consumes the shared PCI parser in its header path for interrupt-support reporting,
|
||||
which starts the planned downstream convergence onto the shared substrate instead of keeping all
|
||||
capability interpretation local.
|
||||
- **Phase B — ACPI / IOMMU groundwork**
|
||||
- `acpid` now has an explicit userspace sleep-target model for `S1` / `S3` / `S4` / `S5`
|
||||
- `_S5` shutdown routes through that model, while non-`S5` targets remain groundwork-only
|
||||
- `iommu` now detects kernel ACPI `DMAR` presence, establishing the Intel VT-d ownership seam
|
||||
- **Phase C — PS/2 / USB / storage**
|
||||
- `ps2d` now flushes stale controller output during probe and around core init/self-test
|
||||
- `xhcid` now tracks active alternate settings and resolves endpoint descriptors through that map
|
||||
- `usbscsid` now has a bounded `SYNCHRONIZE CACHE(10/16)` heuristic behind `needs_sync_cache`
|
||||
- **Phase D — Wi-Fi / DRM shared-core**
|
||||
- `redbear-wifictl` transport probing now uses the shared PCI parser and interrupt-support summary
|
||||
- `redox-drm` now exposes queued shared hotplug/vblank events through a real scheme `EVENT_READ` surface
|
||||
|
||||
The work that still remains is the larger **vendor/backend maturation and hardware-validation** side:
|
||||
|
||||
- full ACPI sleep/resume implementation beyond groundwork
|
||||
- full Intel VT-d runtime support beyond DMAR ownership discovery
|
||||
- deeper PCI / `pcid` convergence on shared helpers
|
||||
- broader PS/2 resume/wake policy
|
||||
- broader USB architecture/runtime maturation beyond the bounded helper slices already implemented
|
||||
- deeper Wi-Fi transport/helper extraction beyond probing
|
||||
- Intel and AMD DRM backend maturation and real hardware validation
|
||||
|
||||
This document should therefore be read as:
|
||||
|
||||
- **implemented now** for the bounded shared-core and software-only slices listed above
|
||||
- **still in progress** for backend maturation and hardware-backed acceptance phases
|
||||
|
||||
## Hard rules
|
||||
|
||||
1. **Linux suspend/resume is reference-only.** Red Bear should study Linux ordering and edge cases, but implement its own suspend/resume support in the Red Bear architecture.
|
||||
@@ -285,6 +322,15 @@ Keep only:
|
||||
- unit tests for malformed capability chains and BAR layout
|
||||
- interrupt mode logged deterministically
|
||||
|
||||
**Current implementation progress (2026-04-18)**
|
||||
- `redox-driver-sys` fast PCI enumeration now parses capability chains from config bytes in the
|
||||
read-only path, so enumerated `PciDeviceInfo` records no longer default to empty capability
|
||||
lists.
|
||||
- `PciDeviceInfo` now exposes a quirk-aware interrupt support summary (`none`, `legacy`, `msi`,
|
||||
`msix`) that can serve as the common policy input for future `pcid`/driver convergence.
|
||||
- Host-runnable unit coverage exists for capability-chain parsing, malformed next-pointer handling,
|
||||
and interrupt-support selection behavior.
|
||||
|
||||
### Phase B — ACPI / suspend / IOMMU
|
||||
|
||||
**Primary targets**
|
||||
@@ -304,6 +350,19 @@ Keep only:
|
||||
- explicit sleep phase machine exists
|
||||
- IOMMU ownership clarified and moved out of `acpid`
|
||||
|
||||
**Current implementation progress (2026-04-18)**
|
||||
- `acpid` now has an explicit `SleepTarget` / `SleepPhase` model in userspace, covering `S1`, `S3`,
|
||||
`S4`, and `S5` as named Red Bear sleep targets.
|
||||
- The real shutdown path now routes through that target model, while non-`S5` targets are
|
||||
recognized but reported as groundwork-only rather than silently ignored.
|
||||
- Unit coverage exists for sleep-target parsing, AML sleep-object naming, and the current
|
||||
Red Bear-native rule that only `S5` is treated as an implemented soft-off path today.
|
||||
- This is still groundwork only: there is no claim of full suspend/resume or sleep eventing yet,
|
||||
and Linux suspend sequencing remains reference material rather than imported structure.
|
||||
- The `iommu` daemon now also detects the presence of a kernel ACPI `DMAR` table and reports that
|
||||
Intel VT-d runtime ownership should converge there instead of remaining conceptually attached to
|
||||
the old transitional `acpid` DMAR code.
|
||||
|
||||
### Phase C — PS/2 / USB / storage
|
||||
|
||||
**Primary targets**
|
||||
@@ -321,6 +380,33 @@ Keep only:
|
||||
- xHCI and USB maturity proofs remain green
|
||||
- no Linux USB/input-core structure imported
|
||||
|
||||
**Current implementation progress (2026-04-18)**
|
||||
- `xhcid` now tracks active alternate settings per interface and resolves endpoint descriptors using
|
||||
that active-alternate map instead of flattening all interface descriptors in a configuration.
|
||||
- Direct unit coverage exists for default-alternate endpoint selection and alternate-setting-aware
|
||||
endpoint remapping, closing the most explicit in-tree USB interface-selection TODO without
|
||||
importing Linux USB-core structure.
|
||||
- `xhcid` now also preserves previously selected alternates on the same configuration and applies a
|
||||
requested interface/alternate override before endpoint planning, so alternate-setting
|
||||
reconfiguration no longer silently falls back to all-zero defaults.
|
||||
- `xhcid` endpoint-direction lookup now also follows the active interface/alternate selection state
|
||||
instead of reading from the first configuration/interface pair unconditionally.
|
||||
- `xhcid` driver spawning now also follows the selected configuration and active alternate map
|
||||
instead of hardcoding the first configuration and ignoring non-zero alternates.
|
||||
- `xhcid` now also has a preserve-and-grow event-ring path in the IRQ reactor, so `EventRingFull`
|
||||
recovery no longer drops unread event TRBs while resizing the primary event ring.
|
||||
- `usbhubd` and `xhcid` now propagate USB 2 hub TT Think Time from the parent hub descriptor into
|
||||
the xHCI Slot Context TT information bits using a bounded Linux-compatible encoding path.
|
||||
- `xhcid` endpoint-context calculations are now protocol-speed-aware for SuperSpeedPlus, so
|
||||
interval and ESIT-payload selection distinguish SSP paths from generic SuperSpeed using the
|
||||
resolved port protocol speed rather than only endpoint companion presence.
|
||||
- `usbscsid` now has a bounded native `SYNCHRONIZE CACHE(10/16)` heuristic gated by the existing
|
||||
`needs_sync_cache` storage quirk, directly reflecting the planned Linux `sd.c` donor usage without
|
||||
importing Linux SCSI midlayer structure.
|
||||
- `ps2d` now performs an explicit controller-output flush during probe and at the key controller
|
||||
reinitialization boundaries in `Ps2::init()`, matching the Linux `i8042_flush()` discipline in a
|
||||
bounded Red Bear-native way without importing Linux input-core structure.
|
||||
|
||||
### Phase D — Wi-Fi and GPU/DRM
|
||||
|
||||
**Primary targets**
|
||||
@@ -337,6 +423,16 @@ Keep only:
|
||||
- DRM display-vs-render boundary remains explicit
|
||||
- no claim of full AMDGPU rewrite or Linux wireless-architecture import
|
||||
|
||||
**Current implementation progress (2026-04-18)**
|
||||
- `redbear-wifictl` transport probing now uses the shared `redox-driver-sys` PCI parser and the
|
||||
shared quirk-aware interrupt-support summary instead of relying only on local raw-config logic.
|
||||
- This is a bounded helper extraction only: the native Wi-Fi control plane remains authoritative,
|
||||
and there is still no import of Linux wireless subsystem structure.
|
||||
- `redox-drm` now turns shared hotplug and vblank events into a queued scheme-visible
|
||||
`EVENT_READ` surface for `card0`, with hotplug also reaching the matching connector handle.
|
||||
That makes shared DRM event delivery observable without conflating it with render-fence
|
||||
semantics.
|
||||
|
||||
## 4. Subsystem-specific code guidelines
|
||||
|
||||
### ACPI / suspend
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
# Historical Phase P2: AMD GPU Display Output
|
||||
|
||||
## Status: Historical implementation milestone complete — hardware validation pending
|
||||
|
||||
> **Planning authority note (2026-04-18):** this file is an AMD display implementation/status
|
||||
> reference. For current GPU/DRM execution order, Intel/AMD parity criteria, and future task
|
||||
> sequencing, use `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md`.
|
||||
|
||||
The original P2 implementation task was completed and compile-validated for its scoped deliverables.
|
||||
This file is now a historical AMD display implementation/status reference rather than the canonical
|
||||
GPU planning document. Hardware validation remains a separate milestone requiring physical AMD GPU
|
||||
hardware.
|
||||
|
||||
## Goal
|
||||
Enable AMD GPU display output (modesetting) on Redox OS via a DRM scheme daemon
|
||||
that ports the AMD Display Core (DC) from Linux kernel 7.0-rc7.
|
||||
|
||||
## Architecture
|
||||
|
||||
Userspace apps → scheme:drm → redox-drm daemon → AMD DC (C code, linux-kpi) → MMIO
|
||||
|
||||
## Components
|
||||
|
||||
### redox-drm (local/recipes/gpu/redox-drm/)
|
||||
DRM scheme daemon. Registers scheme:drm/card0.
|
||||
- PCI enumeration for AMD GPUs (vendor 0x1002)
|
||||
- MMIO register mapping via redox-driver-sys
|
||||
- KMS: connector detection, mode getting, CRTC programming
|
||||
- GEM: buffer object create/mmap/close
|
||||
- Dispatches to AMD driver backend
|
||||
|
||||
### amdgpu C port (local/recipes/gpu/amdgpu/source/)
|
||||
AMD GPU driver source extracted from Linux 7.0-rc7:
|
||||
- drivers/gpu/drm/amd/ — full AMD driver (269k lines)
|
||||
- drivers/gpu/drm/ttm/ — TTM memory manager
|
||||
- include/drm/ — DRM core headers
|
||||
- include/linux/ — Linux kernel headers (reference)
|
||||
|
||||
### amdgpu build recipe (local/recipes/gpu/amdgpu/)
|
||||
Compiles AMD DC display code against linux-kpi headers with -D__redox__:
|
||||
- recipe.toml — custom build template
|
||||
- redox_glue.h — type compatibility, function stubs, macro replacements
|
||||
- redox_stubs.c — C implementations of Linux kernel API stubs
|
||||
- amdgpu_redox_main.c — daemon entry point replacing module_init
|
||||
|
||||
## Build Integration
|
||||
|
||||
Config: config/redbear-desktop.toml (includes desktop.toml + Red Bear GPU packages)
|
||||
- Includes redox-drm and amdgpu packages
|
||||
- filesystem_size = 8196 (8GB, needs space for firmware blobs)
|
||||
|
||||
pcid: local/config/pcid.d/amd_gpu.toml
|
||||
- Auto-detects AMD GPU (vendor 0x1002, class 0x03)
|
||||
- Launches redox-drm with PCI device location
|
||||
|
||||
## Boot Sequence (P2)
|
||||
|
||||
1. Kernel boots, initializes PCI subsystem
|
||||
2. pcid detects AMD GPU (vendor 0x1002)
|
||||
3. pcid-spawner launches: redox-drm $BUS $DEV $FUNC
|
||||
4. redox-drm opens PCI device, verifies AMD GPU
|
||||
5. redox-drm maps MMIO BAR0 (GPU registers)
|
||||
6. redox-drm loads PSP firmware via scheme:firmware
|
||||
7. redox-drm initializes AMD DC (Display Core)
|
||||
8. AMD DC detects connectors, reads EDID
|
||||
9. scheme:drm/card0 registered
|
||||
10. Userspace can begin bounded display-side probing (for example `modetest`) once runtime validation is available; this is not yet a hardware-backed support claim by itself
|
||||
|
||||
## Verification
|
||||
|
||||
### Code Complete (P2 implementation task)
|
||||
- [x] scheme:drm/card0 daemon compiles and registers scheme
|
||||
- [x] KMS ioctl dispatch handles all 15 DRM ioctls
|
||||
- [x] GEM buffer lifecycle: create/mmap/close with ownership tracking
|
||||
- [x] FB lifecycle: ADDFB/RMFB with size validation, per-fd ownership
|
||||
- [x] Page flip: one outstanding per CRTC, vblank-gated retirement
|
||||
- [x] Firmware: Rust cache validates blob availability at startup; C code loads via request_firmware() from scheme:firmware at runtime
|
||||
- [x] GTT page tables: free-list reuse, TLB-safe error rollback
|
||||
- [x] Original implementation pass received repeated review of resource lifecycle, ownership, GTT, and page-flip behavior
|
||||
- [x] All 4 Rust crates build with zero errors, zero warnings
|
||||
- [x] C glue files pass gcc -fsyntax-only
|
||||
- [x] Build symlinks and config files in place
|
||||
|
||||
### Hardware Validation (requires physical AMD GPU)
|
||||
- [ ] modetest -M amd shows connector info and modes
|
||||
- [ ] modetest -M amd -s 0:1920x1080 sets mode and shows test pattern
|
||||
- [ ] Works on real AMD hardware (RDNA2/RDNA3)
|
||||
|
||||
Current bounded runtime harness:
|
||||
- `redbear-drm-display-check` is now the in-guest bounded display checker.
|
||||
- `local/scripts/test-amd-gpu.sh` now wraps the shared `local/scripts/test-drm-display-runtime.sh` harness.
|
||||
- The checker now proves connector/mode enumeration directly and can perform a bounded direct modeset proof.
|
||||
- A successful harness run is still display-only evidence, not render proof.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| local/recipes/gpu/redox-drm/ | DRM scheme daemon |
|
||||
| local/recipes/gpu/amdgpu/ | Build recipe + integration glue |
|
||||
| local/recipes/gpu/amdgpu/source/ | AMD driver C port source (from Linux 7.0-rc7) |
|
||||
| config/redbear-desktop.toml | Build config |
|
||||
| local/config/pcid.d/amd_gpu.toml | PCI auto-detection (AMD) |
|
||||
| local/recipes/gpu/redox-drm/source/src/drivers/interrupt.rs | MSI-X/legacy interrupt abstraction |
|
||||
| local/config/pcid.d/intel_gpu.toml | Intel GPU PCI auto-detection |
|
||||
| local/patches/base/P0-pcid-config-endpoint.patch | pcid /config file endpoint |
|
||||
| local/scripts/build-redbear.sh | Canonical build wrapper |
|
||||
| local/scripts/test-amd-gpu.sh | Test script |
|
||||
|
||||
## Dependencies (P1)
|
||||
|
||||
| Crate | Status | Provides |
|
||||
|-------|--------|----------|
|
||||
| redox-driver-sys | ✅ | MmioRegion, PciDevice, IrqHandle, DmaBuffer |
|
||||
| linux-kpi | ✅ | C headers, FFI stubs (kmalloc, mutex, spinlock...) |
|
||||
| firmware-loader | ✅ | scheme:firmware daemon |
|
||||
|
||||
## P1/P2 Changes Since Initial Implementation
|
||||
|
||||
### pcid /config endpoint (T1)
|
||||
- Added `Config { addr: PciAddress }` handle to pcid scheme
|
||||
- Routes `/scheme/pci/{addr}/config` to raw PCI config space read/write
|
||||
- Enables redox-driver-sys PciDevice to access config space for MSI-X, BAR parsing
|
||||
|
||||
### MSI-X interrupt support (T2-T4)
|
||||
- Created shared `InterruptHandle` enum in `redox-drm/src/drivers/interrupt.rs`
|
||||
- Tries MSI-X first (find capability → parse → map table → mask_all → enable → request_vector)
|
||||
- Falls back through MSI and then legacy IRQ, with `NO_MSIX`, `NO_MSI`, and `FORCE_LEGACY_IRQ` quirk gates applied before transport selection
|
||||
- Both AMD and Intel drivers use `InterruptHandle::setup()`
|
||||
|
||||
### Dynamic PCI device info (T6)
|
||||
- Replaced hardcoded `redox_pci_find_amd_gpu()` stub with `redox_pci_set_device_info()`
|
||||
- Rust side passes real PciDeviceInfo (vendor, device, revision, IRQ, BAR0/BAR2) to C via FFI
|
||||
- C layer validates the struct is populated before `amdgpu_redox_init()` uses it
|
||||
|
||||
### linux-kpi quirk consumption (current)
|
||||
- `redox-drm` now also passes the real PCI BDF into the amdgpu C glue so linux-kpi quirk lookups resolve against the actual GPU, not a guessed location
|
||||
- `amdgpu_redox_main.c` now calls `pci_get_quirk_flags()` / `pci_has_quirk()` in the live Redox init path
|
||||
- firmware gating now stays at the Rust-side GPU driver boundary, while the AMD C backend logs quirk-driven IRQ expectations (`NO_MSI`, `NO_MSIX`, `FORCE_LEGACY`) via linux-kpi lookups on the real GPU BDF
|
||||
|
||||
### Intel GPU support (T4-T5)
|
||||
- Intel driver switched to shared `InterruptHandle` (MSI-X / MSI / legacy with quirk-aware fallback)
|
||||
- Added `local/config/pcid.d/intel_gpu.toml` for auto-detection (vendor 0x8086, class 0x03)
|
||||
@@ -258,7 +258,7 @@ Plus: QML debug plugins, QtQuick/QML modules staged.
|
||||
|
||||
Recent relibc implementation progress in this repo now also includes:
|
||||
|
||||
- source-visible `signalfd`, `timerfd`, `eventfd`, `open_memstream`, `F_DUPFD_CLOEXEC`, and `MSG_NOSIGNAL`
|
||||
- source-visible plus strict Redox-target runtime-tested `signalfd`, `timerfd`, `eventfd`, `open_memstream`, `F_DUPFD_CLOEXEC`, and `MSG_NOSIGNAL`
|
||||
- a bounded `waitid()` path in relibc, replacing the old Qt-side waitid stub workaround
|
||||
- a bounded `eth0`-backed `net_if` / `ifaddrs` path in relibc
|
||||
- a minimal source-visible `resolv.h` surface in relibc
|
||||
@@ -307,7 +307,7 @@ Current downstream build proof in this repo now includes:
|
||||
- Non-udev shim (libudev stub for HAVE_UDEV=0)
|
||||
- Vendored Linux input.h selection for __redox__
|
||||
- strtod_l() fallback
|
||||
- timerfd fallback (tracks expiry without timerfd fd)
|
||||
- timerfd path still warrants broader consumer-confidence review, but relibc now has strict Redox-target runtime proof for the bounded timerfd harness
|
||||
- Linux-only tool binaries skipped on Redox
|
||||
|
||||
### Phase 3 — KF6 Frameworks (✅ ALL 32 BUILT)
|
||||
|
||||
@@ -49,7 +49,8 @@ The Red Bear USB stack consists of:
|
||||
- native USB observability (`lsusb`, `usbctl`, `redbear-info`)
|
||||
- a low-level userspace client API through `xhcid_interface`
|
||||
- a hardware quirks system that applies USB device-specific workarounds at runtime
|
||||
- three QEMU validation harnesses covering interrupt delivery, full stack, and storage autospawn
|
||||
- four QEMU validation harnesses covering interrupt delivery, bounded device lifecycle hotplug,
|
||||
full stack, and storage autospawn
|
||||
- an in-guest scheme-tree checker (`redbear-usb-check`)
|
||||
|
||||
### Red Bear xHCI Patch Layer
|
||||
@@ -104,12 +105,9 @@ source:
|
||||
Even with the Red Bear patch applied:
|
||||
|
||||
- HID is still wired through the legacy mixed-stream `inputd` path
|
||||
- SuperSpeedPlus differentiation requires Extended Port Status (not yet implemented)
|
||||
- TTT (Think Time) in Slot Context hardcoded to 0 — needs parent hub descriptor propagation
|
||||
- Composite devices and non-default alternate settings use first-match only
|
||||
(`//TODO: USE ENDPOINTS FROM ALL INTERFACES`)
|
||||
- `grow_event_ring()` swaps to a new ring but does not copy pending TRBs from the old one; under
|
||||
sustained event-ring-full conditions this may lose in-flight events
|
||||
- Any remaining USB composite/device-model issues now sit above the bounded helper fixes already
|
||||
landed for active alternates, endpoint direction, real interface/alternate hub configuration, and
|
||||
SSP-aware endpoint-context calculations.
|
||||
- ~57 TODO/FIXME comments remain across xHCI driver files
|
||||
- usbhubd: interrupt-driven change detection implemented; 1-second polling retained as fallback
|
||||
- usbscsid: `ReadCapacity16` now implemented with automatic fallback from `ReadCapacity10`
|
||||
@@ -168,6 +166,7 @@ Key files and their sizes:
|
||||
|
||||
| Script | What it tests | Limitations |
|
||||
|---|---|---|
|
||||
| `test-xhci-device-lifecycle-qemu.sh --check` | Bounded xHCI hotplug lifecycle proof: runtime attach → configure → driver spawn → detach for HID and storage devices | QEMU-only; monitor-driven hotplug; not a broad hardware stress test |
|
||||
| `test-usb-qemu.sh --check` | Full stack: xHCI interrupt mode, HID spawn, SCSI spawn, bounded sector-0 readback, BOS processing, crash errors | QEMU-only; log-grep based; no guest-side write proof |
|
||||
| `test-usb-storage-qemu.sh` | USB mass storage autospawn + sector-0 readback + crash patterns | No guest-side write proof yet; no multi-LUN; no UAS |
|
||||
| `test-xhci-irq-qemu.sh --check` | xHCI interrupt delivery mode (MSI/MSI-X/INTx) | No devices attached during check; single log grep |
|
||||
@@ -262,6 +261,12 @@ hardware; controller enumerates attached devices reliably across repeated boot c
|
||||
- USB 3 hub endpoint configuration stall handled
|
||||
- `endp_direction` off-by-one fixed (`checked_sub(1)`)
|
||||
- `cfg_idx` assigned after validation
|
||||
- xHCI lifecycle gating prevents new I/O from entering while a port is detaching
|
||||
- `attach_device()` no longer leaves a published partially-enumerated `PortState` on attach failure
|
||||
- `detach_device()` now waits for in-flight lifecycle operations before removing the port state
|
||||
- `configure_endpoints_once()` is transactional: endpoint state is staged locally, input-context
|
||||
mutations are snapshotted, and rollback is attempted if `CONFIGURE_ENDPOINT` or
|
||||
`SET_CONFIGURATION` fails
|
||||
- `CLEAR_FEATURE` uses correct USB endpoint address from descriptor
|
||||
- `usbhubd` status_change_buf sizing and bitmap parsing fixed
|
||||
- Hub interrupt EP1 status change detection replacing polling
|
||||
@@ -314,6 +319,9 @@ replug do not collapse all USB HID into one anonymous stream.
|
||||
- `usbscsid` SCSI layer: `plain::from_bytes().unwrap()` replaced with typed `ScsiError` and fallible `parse_bytes`/`parse_mut_bytes` helpers
|
||||
- `usbscsid` main.rs: fallible `run()` helper, event loop continues on individual failures
|
||||
- `ReadCapacity16` implemented with automatic fallback when `ReadCapacity10` returns max LBA (0xFFFFFFFF)
|
||||
- `usbscsid` now issues bounded `SYNCHRONIZE CACHE(10/16)` commands when the runtime storage quirk
|
||||
set includes `needs_sync_cache`, using Linux `sd.c` sync-cache behavior as a donor reference for
|
||||
command selection and tolerant error handling.
|
||||
|
||||
**Remaining** (all require hardware or design decisions):
|
||||
- Runtime I/O validation: prove stall recovery works under real device I/O (requires hardware)
|
||||
@@ -359,6 +367,7 @@ scope.
|
||||
- `test-usb-qemu.sh` — full USB stack validation harness (6 checks)
|
||||
- `test-usb-storage-qemu.sh` — USB mass storage autospawn check
|
||||
- `test-xhci-irq-qemu.sh` — xHCI interrupt delivery mode check
|
||||
- `test-xhci-device-lifecycle-qemu.sh` — bounded xHCI attach/configure/detach hotplug proof
|
||||
- `USB-VALIDATION-RUNBOOK.md` — operator documentation with Paths A and B
|
||||
- `redbear-usb-check` — in-guest scheme-tree checker (now installed in image)
|
||||
- `lsusb` — full USB scheme walk with descriptor parsing and quirks integration
|
||||
@@ -367,7 +376,7 @@ scope.
|
||||
**Remaining** (all require hardware):
|
||||
- Add hardware-matrix coverage for target controllers and class families
|
||||
- Add USB storage data I/O validation (read/write to block device)
|
||||
- Add hot-plug stress testing harness
|
||||
- Add repeated hardware hot-plug stress testing beyond the bounded QEMU lifecycle slice
|
||||
|
||||
**Exit criteria**: at least one profile can honestly claim a validated USB baseline for named
|
||||
controller/class scope; USB support language in docs matches real test evidence.
|
||||
@@ -613,24 +622,49 @@ zero `unwrap()`/`expect()` panics), interrupt-driven hub change detection, `Read
|
||||
for large disk support, and a USB quirk table expanded from 8 to 146 entries with 22 quirk flags
|
||||
mined from Linux 7.0.
|
||||
|
||||
Recent bounded maturity progress:
|
||||
|
||||
- `xhcid` now tracks active alternate settings per interface in `PortState` and resolves endpoint
|
||||
descriptors through that active-alternate map instead of flattening all interface descriptors
|
||||
indiscriminately.
|
||||
- Direct unit coverage now exists for both default-alternate endpoint selection and
|
||||
alternate-setting-aware endpoint remapping.
|
||||
- `xhcid` now also preserves previously selected alternates on the same configuration and applies a
|
||||
requested interface/alternate override before endpoint planning, so alternate-setting
|
||||
reconfiguration no longer silently falls back to all-zero defaults.
|
||||
- `xhcid` endpoint-direction lookup now also follows the active interface/alternate selection state
|
||||
instead of reading from the first configuration/interface pair unconditionally.
|
||||
- `xhcid` driver spawning now also follows the selected configuration and active alternate map
|
||||
instead of hardcoding the first configuration and ignoring non-zero alternates.
|
||||
- `xhcid` now keeps per-port lifecycle state so detach blocks new transfer/configure/suspend/resume
|
||||
work, waits for in-flight operations to drain, and removes the published port state only after
|
||||
slot disable succeeds.
|
||||
- `xhcid` endpoint configuration is now transactional: software endpoint bookkeeping stays staged
|
||||
until `CONFIGURE_ENDPOINT` and optional `SET_CONFIGURATION` succeed, and the input context is
|
||||
restored with an explicit rollback attempt on failure.
|
||||
- the xHCI IRQ reactor now replaces the old `TODO: grow event ring` stub with a preserve-and-grow
|
||||
path that copies unread event TRBs into a larger event ring and reprograms ERST registers instead
|
||||
of dropping pending events during `EventRingFull` recovery.
|
||||
- `usbhubd` now derives USB 2 hub TT Think Time from the hub descriptor using the same bounded
|
||||
Linux-compatible encoding and passes it through `ConfigureEndpointsReq`, and `xhcid` now writes
|
||||
that value into the Slot Context TT information bits for hub devices.
|
||||
- xHCI endpoint-context calculations are now protocol-speed-aware for SuperSpeedPlus, so interval
|
||||
and ESIT-payload selection use the resolved port protocol speed instead of relying only on
|
||||
endpoint companion presence.
|
||||
|
||||
All validation is QEMU-only. No real hardware USB testing exists.
|
||||
|
||||
The remaining gaps fall into three categories:
|
||||
The remaining gaps now fall into two categories:
|
||||
|
||||
**Still-open software work (implementable without hardware):**
|
||||
- Composite-device endpoint selection across interfaces (xHCI scheme.rs — `//TODO: USE ENDPOINTS FROM ALL INTERFACES`)
|
||||
- Non-default configuration and alternate-setting support (xHCI scheme.rs)
|
||||
- SuperSpeedPlus differentiation via Extended Port Status
|
||||
- TTT (Think Time) propagation from parent hub descriptor into Slot Context
|
||||
- Event ring growth does not copy pending TRBs from old ring (may lose events under sustained load)
|
||||
|
||||
**Architectural redesign (cross-cutting, not USB-internal):**
|
||||
**Broader architectural work (cross-cutting, not a small bounded USB-only fix):**
|
||||
- Any remaining USB composite/device-model issues now belong to wider device-model/design cleanup
|
||||
rather than one more isolated helper patch.
|
||||
- HID producer modernization: per-device streams, hotplug add/remove (requires inputd redesign)
|
||||
- Userspace USB API: `libusb` WIP, no coherent native story
|
||||
|
||||
**Hardware-dependent or design decisions:**
|
||||
- Real hardware validation: no controller tested outside QEMU
|
||||
- Hot-plug stress testing
|
||||
- Hot-plug stress testing beyond the new bounded QEMU lifecycle harness
|
||||
- Storage write validation (bounded sector-0 readback proof now exists in QEMU via `test-usb-storage-qemu.sh`, but guest-side write verification to the USB-backed block device is still open)
|
||||
- usbhubd 1-second polling fallback (only exercisable with real hub hardware)
|
||||
- Modern USB scope decision: device mode / USB-C / PD
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
# Red Bear OS USB Storage, Speed, and Device Integration
|
||||
|
||||
## Purpose
|
||||
|
||||
This document covers USB subsystem areas that the main USB implementation plan
|
||||
(`USB-IMPLEMENTATION-PLAN.md`) treats at a higher level: mass storage quality, filesystem
|
||||
integration, device speed handling, backwards compatibility, and integrated USB device paths.
|
||||
|
||||
It is a companion document, not a replacement. Read both together for the complete USB picture.
|
||||
|
||||
## Current Headline
|
||||
|
||||
USB mass storage is **present in the codebase but disabled**. The driver table entry for
|
||||
`usbscsid` is commented out in `drivers.toml` with the note "#TODO: causes XHCI errors". HID
|
||||
(keyboard/mouse) and hub handling are enabled and functional in QEMU.
|
||||
|
||||
## USB Mass Storage
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
USB device → xhcid (scheme:usb) → usbscsid → driver_block::DiskScheme → /scheme/disk.usb-*
|
||||
→ filesystem (redoxfs/ext4d)
|
||||
```
|
||||
|
||||
| Layer | Component | Status |
|
||||
|---|---|---|
|
||||
| Transport | BOT (Bulk-Only Transport) | ✅ Implemented — CBW/CSW signatures validated, tag matching, stall recovery |
|
||||
| Protocol | SCSI SBC | ⚠️ Partial — READ(16)/WRITE(16) only, missing READ(10)/WRITE(10) |
|
||||
| Block device | driver_block::DiskScheme | ✅ Functional — registers as `/scheme/disk.usb-{scheme}+{port}-scsi` |
|
||||
| Partitions | partitionlib (MBR/GPT) | ✅ Parsed on init — exposes `0p0`, `0p1`, etc. |
|
||||
| Filesystems | redoxfs, ext4d | ✅ Can mount USB block devices via scheme path |
|
||||
|
||||
### BOT Transport Quality
|
||||
|
||||
The BOT transport in `usbscsid/src/protocol/bot.rs` is well-implemented:
|
||||
|
||||
- **CBW handling**: Correct signature (`0x43425355`), per-command tag increment, direction bit, LUN field
|
||||
- **CSW handling**: Signature validation, tag matching, residue tracking, short packet tolerance
|
||||
- **Stall recovery**: `ClearFeature(ENDPOINT_HALT)` on both endpoints, `Bulk-Only Mass Storage Reset`
|
||||
class request (0xFF) for full reset recovery, re-check for persistent stalls
|
||||
- **Phase errors**: Detected and reported via `ProtocolError`
|
||||
|
||||
### SCSI Command Completeness
|
||||
|
||||
| Command | CDB Size | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| INQUIRY | 6 | ✅ | Standard + vendor inquiry data |
|
||||
| REQUEST SENSE | 6 | ✅ | Fixed-format sense data |
|
||||
| READ CAPACITY(10) | 10 | ✅ | 32-bit LBA, used as first probe |
|
||||
| READ CAPACITY(16) | 16 | ✅ | 64-bit LBA, auto-fallback from RC(10) max |
|
||||
| READ(16) | 16 | ✅ | Primary read path |
|
||||
| WRITE(16) | 16 | ✅ | Primary write path |
|
||||
| MODE SENSE(6) | 6 | ✅ | Block descriptor fallback |
|
||||
| MODE SENSE(10) | 10 | ✅ | Primary block size/count source |
|
||||
| READ(10) | 10 | ❌ **Missing** | Required for older/simpler devices |
|
||||
| WRITE(10) | 10 | ❌ **Missing** | Required for older/simpler devices |
|
||||
| SYNCHRONIZE CACHE | 10 | ⚠️ Opcode only | Never issued |
|
||||
| START STOP UNIT | 6 | ⚠️ Opcode only | Never issued |
|
||||
| TEST UNIT READY | 6 | ⚠️ Opcode only | Never issued |
|
||||
| REPORT LUNS | 12 | ❌ **Missing** | Needed for multi-LUN devices |
|
||||
| MODE SELECT(6/10) | 6/10 | ❌ **Missing** | Needed for parameter negotiation |
|
||||
| FORMAT UNIT | 6 | ❌ **Missing** | Rarely needed |
|
||||
|
||||
**Critical gap**: READ(10)/WRITE(10) are not implemented. The daemon uses 16-byte CDBs exclusively.
|
||||
Devices that only support 10-byte SCSI commands (some older USB flash drives, embedded firmware
|
||||
devices) will fail. Adding READ(10)/WRITE(10) with automatic fallback (similar to how
|
||||
READ CAPACITY(10)→(16) already works) is a concrete, bounded improvement.
|
||||
|
||||
### LUN Support
|
||||
|
||||
`get_max_lun()` reads the device's max LUN count via class-specific request, but the daemon
|
||||
hardcodes `cbw.lun = 0` for all commands. Multi-LUN devices (card readers, multi-slot adapters)
|
||||
will only expose the first LUN. Supporting multiple LUNs requires:
|
||||
|
||||
1. Iterating from 0 to max_lun
|
||||
2. Creating separate `UsbDisk` instances per LUN
|
||||
3. Registering separate `DiskScheme` paths per LUN
|
||||
|
||||
### UAS (USB Attached SCSI)
|
||||
|
||||
UAS is not implemented. The protocol factory in `protocol/mod.rs` only matches protocol 0x50 (BOT).
|
||||
Protocol 0x62 (UAS) is absent. UAS provides:
|
||||
|
||||
- Multiple simultaneous command pipes (vs BOT's single serialize-execute-wait)
|
||||
- Stream-based transfers for higher throughput
|
||||
- Better error recovery semantics
|
||||
|
||||
For USB 3.0 SuperSpeed devices, UAS can provide significantly higher throughput than BOT.
|
||||
|
||||
### Transfer Size Limitations
|
||||
|
||||
- BOT max transfer: 64KB per command (driver_interface.rs hard limit)
|
||||
- Stream transfer chunk: 32KB per iteration (hardcoded in TransferStream)
|
||||
- No scatter-gather: all data must fit in a single buffer (explicit TODO in scsi/mod.rs)
|
||||
|
||||
### Why usbscsid is Disabled
|
||||
|
||||
The comment says "#TODO: causes XHCI errors". This likely relates to:
|
||||
|
||||
1. Bulk endpoint configuration issues in the xHCI driver
|
||||
2. The 64KB transfer limit causing multi-block reads to fragment incorrectly
|
||||
3. Missing endpoint stall handling during initial enumeration
|
||||
|
||||
**Re-enabling usbscsid is a prerequisite for USB storage validation.**
|
||||
|
||||
## USB Speed Handling
|
||||
|
||||
### Speed Detection
|
||||
|
||||
The xHCI driver detects device speed via PORTSC register bits 10–13. The `ProtocolSpeed` struct
|
||||
(in `xhci/extended.rs`) classifies speeds:
|
||||
|
||||
| Speed | Bitrate | Detection | Status |
|
||||
|---|---|---|---|
|
||||
| Low Speed | 1.5 Mbps | `is_lowspeed()` | ✅ Detected |
|
||||
| Full Speed | 12 Mbps | `is_fullspeed()` | ✅ Detected |
|
||||
| High Speed | 480 Mbps | `is_highspeed()` | ✅ Detected |
|
||||
| SuperSpeed Gen1 x1 | 5 Gbps | `is_superspeed_gen1x1()` | ✅ Detected |
|
||||
| SuperSpeedPlus Gen2 x1 | 10 Gbps | `is_superspeedplus_gen2x1()` | ✅ Detected |
|
||||
| SuperSpeedPlus Gen1 x2 | 10 Gbps x2 | `is_superspeedplus_gen1x2()` | ✅ Detected |
|
||||
| SuperSpeedPlus Gen2 x2 | 20 Gbps x2 | `is_superspeedplus_gen2x2()` | ✅ Detected |
|
||||
|
||||
### Default Control Pipe Max Packet Size
|
||||
|
||||
The driver sets the default control pipe max packet size based on speed:
|
||||
|
||||
| Speed | Max Packet Size | Location |
|
||||
|---|---|---|
|
||||
| Low/Full Speed | 8 bytes | mod.rs:1128 |
|
||||
| High Speed | 64 bytes | mod.rs:1131 |
|
||||
| SuperSpeed | 512 bytes | mod.rs:1134 |
|
||||
|
||||
### Transfer Type Support
|
||||
|
||||
| Transfer Type | USB Role | Status | Notes |
|
||||
|---|---|---|---|
|
||||
| Control | Configuration, enumeration | ✅ Works | Endpoint 0 only |
|
||||
| Bulk | Mass storage, network | ✅ Works | Used by usbscsid |
|
||||
| Interrupt | HID, hub status | ✅ Works | Used by usbhubd, usbhidd |
|
||||
| Isochronous | Audio, video | ❌ ENOSYS | `scheme.rs` explicitly returns `ENOSYS` |
|
||||
|
||||
Isochronous transfers are required for USB audio devices, webcams, and streaming applications.
|
||||
The driver returns `ENOSYS` (function not implemented) for all isochronous endpoint requests.
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
### Transaction Translator (TT) Handling — STUBBED
|
||||
|
||||
USB 1.x Low Speed and Full Speed devices connected behind USB 2.0 High Speed hubs require a
|
||||
Transaction Translator (TT) to convert between USB 1.x and USB 2.0 protocols. The xHCI
|
||||
specification handles TT internally in the controller, but the driver must provide correct
|
||||
parent hub information in the Slot Context during device addressing.
|
||||
|
||||
**Current state**: All TT-related fields are hardcoded:
|
||||
|
||||
| Field | Value | Should Be | Location |
|
||||
|---|---|---|---|
|
||||
| `mtt` (Multi-TT) | `false` | Read from parent hub descriptor | mod.rs:1057 |
|
||||
| `ttt` (TT Think Time) | `0` | Encoded from parent hub descriptor | mod.rs:1114 |
|
||||
| `needs_parent_info` | `true` (forced) | Based on actual device speed topology | mod.rs:1070 |
|
||||
|
||||
The TODOs at mod.rs:1066–1068 explicitly state the values need to be determined from actual
|
||||
device speed and hub topology. Without correct TT information:
|
||||
|
||||
- Low Speed devices (1.5 Mbps) behind USB 2.0 hubs may not enumerate correctly on real hardware
|
||||
- Full Speed devices (12 Mbps) behind USB 2.0 hubs may fail during bulk transfers
|
||||
- Multi-TT hubs with multiple LS/FS devices attached may have timing violations
|
||||
|
||||
### USB 1.x Compatibility
|
||||
|
||||
- **Low Speed (1.5 Mbps)**: Speed detection works. Default control pipe size correct (8 bytes).
|
||||
TT handling stubbed — may fail on real hardware behind HS hubs.
|
||||
- **Full Speed (12 Mbps)**: Speed detection works. Default control pipe size correct (8 bytes).
|
||||
Same TT limitation.
|
||||
|
||||
### USB 2.0 Compatibility
|
||||
|
||||
- **High Speed (480 Mbps)**: Primary tested speed in QEMU. Bulk, Interrupt, Control all functional.
|
||||
Max packet size 64 bytes correctly set.
|
||||
|
||||
### USB 3.x Compatibility
|
||||
|
||||
- **SuperSpeed (5 Gbps)**: Protocol speed detection works. BOS descriptor fetching implemented.
|
||||
Max packet size 512 bytes correctly set. SuperSpeed Companion Descriptor parsed.
|
||||
- **SuperSpeedPlus (10–20 Gbps)**: Protocol speed detection works. SuperSpeedPlus Isochronous
|
||||
Companion Descriptor parsed. No functional testing.
|
||||
|
||||
### Speed-Specific Gaps
|
||||
|
||||
- No speed-specific timeout or retry tuning — all controller-level timeouts are 1-second hardcoded
|
||||
- No burst transaction support for SuperSpeed bulk endpoints (burst field parsed but not used in
|
||||
transfer scheduling)
|
||||
- No streams support for USB 3.0 bulk endpoints (Stream ID capability parsed but not exercised)
|
||||
|
||||
## Integrated USB Devices
|
||||
|
||||
### Device Autospawn Flow
|
||||
|
||||
```
|
||||
1. Device plugs in
|
||||
2. xhcid detects port status change
|
||||
3. xhcid resets port, enumerates device, reads config descriptor
|
||||
4. spawn_drivers() iterates interfaces:
|
||||
- For each interface with alternate_setting == 0:
|
||||
- Match class code (+ optional subclass) against drivers.toml
|
||||
- If match: spawn daemon with $SCHEME, $PORT, $IF_NUM/$IF_PROTO
|
||||
5. Each spawned daemon opens its USB interface via xhcid_interface
|
||||
6. Daemon registers its own scheme or connects to existing schemes
|
||||
```
|
||||
|
||||
### Driver Table (`drivers.toml`)
|
||||
|
||||
```toml
|
||||
# Mass Storage — DISABLED (#TODO: causes XHCI errors)
|
||||
#[[drivers]]
|
||||
#name = "SCSI over USB"
|
||||
#class = 8; subclass = 6
|
||||
#command = ["usbscsid", "$SCHEME", "$PORT", "$IF_PROTO"]
|
||||
|
||||
[[drivers]]
|
||||
name = "USB HUB"; class = 9; subclass = -1
|
||||
command = ["usbhubd", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
|
||||
[[drivers]]
|
||||
name = "USB HID"; class = 3; subclass = -1
|
||||
command = ["usbhidd", "$SCHEME", "$PORT", "$IF_NUM"]
|
||||
```
|
||||
|
||||
### Supported Device Paths
|
||||
|
||||
| Device Class | Daemon | Scheme Path | Integration |
|
||||
|---|---|---|---|
|
||||
| **Hub** (class 9) | `usbhubd` | Manages child ports via xhci scheme | Triggers nested device enumeration |
|
||||
| **HID** (class 3) | `usbhidd` | Writes to `/scheme/input/producer` via inputd | Legacy display/input consumers read `/scheme/input` |
|
||||
| **Mass Storage** (class 8) | `usbscsid` | Registers `disk.usb-{scheme}+{port}-scsi` | Filesystems mount via scheme path |
|
||||
|
||||
### HID Integration Detail
|
||||
|
||||
```
|
||||
USB keyboard/mouse → xhcid → usbhidd → inputd (scheme:input) → display/input consumer
|
||||
↑
|
||||
/scheme/input/producer (drivers write here)
|
||||
/scheme/input/consumer (display server reads here)
|
||||
/scheme/input/handle/{name} (per-device handles)
|
||||
```
|
||||
|
||||
- `usbhidd` implements boot protocol HID (keyboard, mouse, scroll, button)
|
||||
- Events: `orbclient::KeyEvent` for keyboards, `orbclient::MouseEvent`/`ButtonEvent`/`ScrollEvent`
|
||||
- The `inputd` multiplexer collects from all input producers (USB HID, PS/2 via `ps2d`, etc.)
|
||||
|
||||
### Storage Integration Detail
|
||||
|
||||
```
|
||||
USB flash drive → xhcid → usbscsid → DiskScheme → /scheme/disk.usb-usb+1-scsi
|
||||
↓
|
||||
redoxfs/ext4d mount
|
||||
↓
|
||||
/scheme/file/{mount-point}
|
||||
```
|
||||
|
||||
- `DiskScheme` from `driver-block` provides block I/O via scheme
|
||||
- Partition table parsing via `partitionlib` (MBR + GPT)
|
||||
- Partitions exposed as `0p0`, `0p1`, etc. under the disk scheme
|
||||
|
||||
### Composite Device Handling
|
||||
|
||||
Composite USB devices (e.g., keyboard+mouse combo, keyboard+trackpad) are partially supported:
|
||||
|
||||
- xhcid iterates **all interfaces** in the first configuration
|
||||
- Each interface matching a `drivers.toml` entry spawns its own daemon process
|
||||
- Alternate settings (`alternate_setting != 0`) are explicitly skipped
|
||||
- No vendor/product ID matching — class code only
|
||||
|
||||
**What works**: A keyboard+mouse combo with two HID interfaces will spawn two `usbhidd` processes,
|
||||
each handling one interface. Both produce input events through inputd.
|
||||
|
||||
**What doesn't work**: Devices requiring alternate settings for full functionality. Devices needing
|
||||
vendor-specific drivers.
|
||||
|
||||
### Unsupported Device Classes
|
||||
|
||||
These USB device classes have no driver in Red Bear OS:
|
||||
|
||||
| Class | Name | Use Case | Blocker |
|
||||
|---|---|---|---|
|
||||
| 0x01 | Audio | USB headsets, speakers | Isochronous transfers not implemented (ENOSYS) |
|
||||
| 0x0E | Video | Webcams | Isochronous transfers not implemented |
|
||||
| 0x02 | CDC/ACM | USB serial, modems | No driver written |
|
||||
| 0x0A | CDC-Data | USB networking | No driver written |
|
||||
| 0x0B | Chip Card | Smart card readers | No driver written |
|
||||
| 0x0D | Content Security | Conditional access | No driver written |
|
||||
| 0x0F | Personal Healthcare | Medical devices | No driver written |
|
||||
| 0x06 | Still Image | Cameras (PTP/MTP) | No driver written |
|
||||
| 0x07 | Printer | USB printers | No driver written |
|
||||
| 0x10 | Audio/Video | AV devices | Isochronous required |
|
||||
| 0x11 | Billboard | USB-C alternate mode | No driver written |
|
||||
| 0x12 | USB Type-C Bridge | USB-C muxes | No driver written |
|
||||
| 0xDC | Diagnostic | USB debug | No driver written |
|
||||
| 0xE0 | Wireless Controller | Bluetooth, Wi-Fi dongles | Separate (redbear-btusb) |
|
||||
| 0xEF | Miscellaneous | Firmware update, etc. | No driver written |
|
||||
| 0xFF | Vendor Specific | Custom devices | No driver written |
|
||||
|
||||
## Implementation Priorities
|
||||
|
||||
### Priority 1: Re-enable USB Mass Storage
|
||||
|
||||
The most impactful single change. Requires diagnosing and fixing the "causes XHCI errors" issue.
|
||||
Likely causes:
|
||||
|
||||
1. Bulk endpoint configuration in scheme.rs — endpoint type mismatch during configuration
|
||||
2. Transfer size handling — the 64KB limit may fragment BOT CBW/CSW sequences
|
||||
3. Missing stall recovery during initial BOT reset
|
||||
|
||||
### Priority 2: Add READ(10)/WRITE(10)
|
||||
|
||||
Implement 10-byte CDB variants with automatic fallback. Pattern already exists for READ CAPACITY.
|
||||
Required for device compatibility:
|
||||
|
||||
```rust
|
||||
// Proposed fallback pattern (matching existing RC10→RC16 pattern):
|
||||
pub fn read(&mut self, lba: u64, buf: &mut [u8], protocol: &mut dyn Protocol) -> Result<()> {
|
||||
if lba <= u32::MAX as u64 && buf.len() <= u32::MAX as usize {
|
||||
// Try READ(10) first — wider device compatibility
|
||||
let cmd = self.cmd_read10()?;
|
||||
*cmd = cmds::Read10::new(lba as u32, ...);
|
||||
...
|
||||
}
|
||||
// Fall back to READ(16) for large addresses
|
||||
}
|
||||
```
|
||||
|
||||
### Priority 3: Fix Transaction Translator Handling
|
||||
|
||||
Replace hardcoded TT values with actual parent hub descriptor data. This is required for real
|
||||
hardware where LS/FS devices sit behind HS hubs.
|
||||
|
||||
### Priority 4: Multi-LUN Support
|
||||
|
||||
Iterate device LUNs and create separate disk scheme instances per LUN. Required for card readers
|
||||
and multi-slot adapters.
|
||||
|
||||
### Priority 5: Isochronous Transfers
|
||||
|
||||
Implement the Isoch TRB path in scheme.rs to enable USB audio and video device classes.
|
||||
|
||||
### Priority 6: UAS Transport
|
||||
|
||||
Add USB Attached SCSI protocol support for SuperSpeed storage devices. Higher throughput than BOT
|
||||
but requires stream ID support in the xHCI driver.
|
||||
|
||||
## Summary
|
||||
|
||||
USB mass storage exists in the codebase with a well-implemented BOT transport, proper SCSI command
|
||||
set (with gaps in READ/WRITE(10)), and functional block device integration — but it is **disabled**
|
||||
due to xHCI errors during device configuration. The most impactful work is diagnosing and fixing
|
||||
that issue, then adding READ(10)/WRITE(10) for wider device compatibility.
|
||||
|
||||
Speed handling covers the full range from Low Speed (1.5 Mbps) to SuperSpeedPlus (20 Gbps) at the
|
||||
detection level, but TT handling is stubbed and isochronous transfers return ENOSYS. Backwards
|
||||
compatibility for USB 1.x devices behind USB 2.0 hubs requires TT fix work.
|
||||
|
||||
Device integration supports hubs and HID via autospawn. Composite devices get all interfaces
|
||||
handled. No vendor/product matching exists. No audio, video, serial, or networking USB device
|
||||
classes have drivers.
|
||||
@@ -0,0 +1,353 @@
|
||||
# Red Bear OS Wayland Implementation Plan
|
||||
|
||||
**Version:** 1.0 (2026-04-19)
|
||||
**Status:** Canonical Wayland subsystem plan
|
||||
**Supersedes:** `docs/03-WAYLAND-ON-REDOX.md` as the active Wayland planning document
|
||||
|
||||
## Purpose
|
||||
|
||||
This is the single authoritative Red Bear Wayland subsystem plan.
|
||||
|
||||
It replaces the planning role previously held by `docs/03-WAYLAND-ON-REDOX.md` and consolidates the
|
||||
current Wayland story into one document that answers four questions clearly:
|
||||
|
||||
1. what in the Wayland stack actually builds,
|
||||
2. what has runtime proof,
|
||||
3. what still blocks a trustworthy compositor/session claim,
|
||||
4. and what work must happen next, in what order, to close those gaps.
|
||||
|
||||
This plan is subordinate to the canonical desktop path in
|
||||
`local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` and to the current build/runtime truth in
|
||||
`local/docs/DESKTOP-STACK-CURRENT-STATUS.md`, but it is the canonical subsystem plan for the
|
||||
Wayland layer beneath that desktop path.
|
||||
|
||||
## Truth Statement
|
||||
|
||||
Red Bear Wayland is **partially complete and still experimental**.
|
||||
|
||||
What is true today:
|
||||
|
||||
- the base package stack is substantially build-visible: `libwayland`, `wayland-protocols`, Mesa
|
||||
EGL/GBM/GLES2, Qt Wayland, libinput, seatd, and KWin-related package surfaces all build in some
|
||||
form,
|
||||
- the tracked Wayland validation profile, `redbear-wayland`, builds and boots in QEMU,
|
||||
- the bounded validation path reaches compositor early init, xkbcommon initialization, and Redox EGL
|
||||
platform selection,
|
||||
- `qt6-wayland-smoke` is a real bounded client-side proof target,
|
||||
- but there is still **no complete Wayland compositor session**, **no runtime-trusted input/session
|
||||
path**, and **no hardware-accelerated Wayland proof**.
|
||||
|
||||
This means Wayland is no longer blocked mainly by package absence. It is blocked by the gap between
|
||||
**build-visible packaging** and **runtime-trusted compositor/session behavior**.
|
||||
|
||||
## Scope
|
||||
|
||||
This plan covers the Red Bear Wayland subsystem from protocol/runtime substrate up to a bounded
|
||||
working compositor session, and then its handoff into the KWin desktop path.
|
||||
|
||||
In scope:
|
||||
|
||||
- `libwayland`, `wayland-protocols`, protocol generation, and residual patch reduction,
|
||||
- the `redbear-wayland` validation profile,
|
||||
- compositor runtime validation,
|
||||
- evdevd / udev-shim / libinput / seatd integration as they affect Wayland,
|
||||
- Mesa/GBM/EGL software-path proof and the Wayland-facing graphics runtime,
|
||||
- KWin as the intended production Wayland compositor path,
|
||||
- local overlay ownership decisions for Wayland components and validation harnesses.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- full KDE Plasma session assembly beyond its Wayland-facing dependencies,
|
||||
- hardware GPU render enablement strategy in detail (owned by the DRM plan),
|
||||
- Wi-Fi, Bluetooth, USB, and low-level controller work except where they directly block Wayland
|
||||
runtime trust.
|
||||
|
||||
## Authority Chain
|
||||
|
||||
Use the doc set in this order:
|
||||
|
||||
1. `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` — top-level desktop sequencing authority
|
||||
2. `local/docs/DESKTOP-STACK-CURRENT-STATUS.md` — current desktop/Wayland truth
|
||||
3. `local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` — Wayland subsystem plan beneath the desktop path
|
||||
4. `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` — GPU/DRM execution detail
|
||||
5. `local/docs/QT6-PORT-STATUS.md` — Qt/KF6/KWin package-level build status
|
||||
|
||||
The following are historical or reference-only after this plan:
|
||||
|
||||
- `docs/05-KDE-PLASMA-ON-REDOX.md` — historical KDE rationale
|
||||
- older WIP compositor notes such as the `smallvil` path — historical bounded validation references
|
||||
|
||||
## Evidence Model
|
||||
|
||||
This plan uses the same strict evidence classes as the canonical desktop path:
|
||||
|
||||
| Class | Meaning | Safe to say | Not safe to say |
|
||||
|---|---|---|---|
|
||||
| **builds** | package compiles and stages | “builds” | “works” |
|
||||
| **boots** | image reaches prompt or known runtime surface | “boots” | “desktop works” |
|
||||
| **enumerates** | scheme/device node appears and answers bounded queries | “enumerates” | “usable end to end” |
|
||||
| **usable** | bounded runtime path performs intended task | “usable for this path” | “broadly stable” |
|
||||
| **validated** | repeated proof on intended target class | “validated” | “complete everywhere” |
|
||||
| **experimental** | partial, scaffolded, or runtime-untrusted | “experimental” | “done” |
|
||||
|
||||
Rules:
|
||||
|
||||
- compile-only success is still only **builds**,
|
||||
- QEMU-only success stays QEMU-bounded,
|
||||
- a compositor that reaches early init but never completes a session is still **experimental**,
|
||||
- KWin and Plasma build success does not imply Wayland session viability.
|
||||
|
||||
## Current State Assessment
|
||||
|
||||
### Stable enough to rely on for planning
|
||||
|
||||
| Area | Current state | Notes |
|
||||
|---|---|---|
|
||||
| `redbear-wayland` profile | builds, boots | bounded validation profile only |
|
||||
| `libwayland` | builds | still carries Redox-specific recipe/source rewriting and residual patching |
|
||||
| `wayland-protocols` | builds | protocol packaging is not the blocker |
|
||||
| Qt6 Wayland client path | builds, partial runtime | `qt6-wayland-smoke` is installed, runs in the bounded harness, and leaves runtime markers; visible in-compositor window proof is still open |
|
||||
| Mesa EGL + GBM + GLES2 | builds | software path via LLVMpipe proven in QEMU |
|
||||
| evdevd / udev-shim / firmware-loader / redox-drm | builds, boots, enumerate | runtime trust still bounded |
|
||||
| libinput | builds | udev disabled in recipe; runtime integration still open |
|
||||
| seatd | builds | runtime trust still open; lease path still unproven |
|
||||
| KWin reduced path | experimental build | honest reduced dependency path, but runtime/session proof missing |
|
||||
|
||||
### What remains incomplete
|
||||
|
||||
| Area | Current gap |
|
||||
|---|---|
|
||||
| Compositor runtime | no complete Wayland compositor session |
|
||||
| Input path | no end-to-end proof that evdevd → libinput → compositor is trustworthy |
|
||||
| Session path | no runtime-trusted seat/session proof for KWin path |
|
||||
| Hardware graphics | no hardware-accelerated Wayland proof |
|
||||
| KWin truthfulness | build is reduced and partially dependency-honest, but still not a runtime-ready session |
|
||||
| WIP ownership | upstream WIP recipes and local overlays are mixed; forward path is not always explicit |
|
||||
|
||||
## Stability / Completeness Verdict
|
||||
|
||||
### Stability
|
||||
|
||||
Wayland is **not stable enough** for a broad support claim.
|
||||
|
||||
Reason:
|
||||
|
||||
- runtime proof is still limited to a bounded QEMU validation harness,
|
||||
- the compositor path reaches early init but not a complete session,
|
||||
- input/session integration is not yet runtime-trusted,
|
||||
- the intended production path (KWin) is still runtime-incomplete.
|
||||
|
||||
### Completeness
|
||||
|
||||
Wayland is **build-substantially-complete but runtime-incomplete**.
|
||||
|
||||
The stack is no longer missing its main package layers. It is missing:
|
||||
|
||||
- complete compositor runtime proof,
|
||||
- complete input/session integration proof,
|
||||
- hardware-path proof,
|
||||
- and a cleaner local ownership story for the forward path versus historical references.
|
||||
|
||||
## Main Gaps and Blockers
|
||||
|
||||
### G1. Runtime trust trails build success
|
||||
|
||||
This is the biggest real blocker.
|
||||
|
||||
Current examples:
|
||||
|
||||
- `libwayland` builds, but runtime behavior is not yet trusted as a full compositor foundation,
|
||||
- libinput builds, but its runtime path through evdevd/udev-shim is still open,
|
||||
- seatd builds, but the compositor/session path still lacks runtime proof,
|
||||
- `redox-drm` enumerates and supports bounded display tooling, but Wayland compositor runtime is not
|
||||
yet trusted on top of it.
|
||||
|
||||
### G2. No complete compositor session
|
||||
|
||||
The bounded validation compositor path is still an **early-init harness**, not a working session.
|
||||
|
||||
Current proof stops at:
|
||||
|
||||
- launch surface present,
|
||||
- xkbcommon init reached,
|
||||
- Redox EGL platform selected,
|
||||
- Qt smoke markers present.
|
||||
|
||||
That is useful, but it is still not the same thing as:
|
||||
|
||||
- a visible, durable Wayland session,
|
||||
- a client that connects and stays usable,
|
||||
- input routing proven through the compositor,
|
||||
- or a trustworthy handoff into KWin session work.
|
||||
|
||||
### G3. KWin remains the intended path, but it is still runtime-incomplete
|
||||
|
||||
KWin is the forward compositor direction, not smallvil or COSMIC.
|
||||
|
||||
Current truth:
|
||||
|
||||
- the recipe exists,
|
||||
- the reduced path is more honest than before,
|
||||
- but it still carries disabled features and incomplete runtime/session proof,
|
||||
- therefore it must not yet be described as a working compositor path.
|
||||
|
||||
### G4. The input/session stack is build-visible but still operationally incomplete
|
||||
|
||||
Key issues:
|
||||
|
||||
- libinput is still built with udev disabled,
|
||||
- seatd runtime proof is still open,
|
||||
- compositor-side device discovery and hotplug behavior remain bounded or incomplete,
|
||||
- `seatd-redox` remains a live local TODO and not a closed runtime path.
|
||||
|
||||
### G5. Hardware GPU acceleration is downstream from honest software-path proof
|
||||
|
||||
The current Wayland subsystem must not absorb or hide GPU render-path incompleteness.
|
||||
|
||||
Current truth:
|
||||
|
||||
- software-path Mesa/GBM/EGL is the valid bounded proof path,
|
||||
- hardware acceleration remains blocked on shared GPU/DRM work outside the Wayland package layer,
|
||||
- therefore hardware claims must stay in the DRM plan, not be implied by Wayland package success.
|
||||
|
||||
## Ownership and Forward Path
|
||||
|
||||
### Red Bear-owned forward path
|
||||
|
||||
The forward path is now:
|
||||
|
||||
- `redbear-wayland` for bounded compositor/runtime validation,
|
||||
- `redbear-kde` for the intended KWin Wayland desktop direction,
|
||||
- local overlay ownership for validation harnesses and any shipping-critical Wayland recipe deltas.
|
||||
|
||||
### Historical or non-forward references
|
||||
|
||||
These should not be treated as the forward path:
|
||||
|
||||
- `smallvil` — historical bounded validation compositor reference only,
|
||||
- the generic upstream WIP compositor set (`wlroots`, `sway`, `hyprland`, etc.) — useful inputs, not
|
||||
trusted Red Bear shipping surfaces,
|
||||
- `docs/03-WAYLAND-ON-REDOX.md` — retired as a planning document.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
This plan keeps Wayland aligned with the canonical desktop path, but narrows the work specifically to
|
||||
Wayland subsystem needs.
|
||||
|
||||
### Wave 1 — Runtime substrate closure for Wayland consumers
|
||||
|
||||
**Goal:** turn the Wayland substrate from build-visible into runtime-trusted.
|
||||
|
||||
**Must prove:**
|
||||
|
||||
1. `libwayland` runtime behavior against the current relibc event/fd surfaces,
|
||||
2. evdevd → libinput → compositor-facing input viability,
|
||||
3. udev-shim enumeration sufficient for current Wayland-facing consumers,
|
||||
4. firmware-loader + `redox-drm` + bounded KMS/display evidence adequate for the validation path.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- bounded relibc/libwayland runtime smoke is repeatable,
|
||||
- bounded input path reaches compositor-facing consumers without hand-wavy assumptions,
|
||||
- bounded display path still passes the current runtime harness after the input/session wiring is
|
||||
tightened,
|
||||
- no current claim depends on a package merely compiling.
|
||||
|
||||
### Wave 2 — Complete the bounded compositor validation path
|
||||
|
||||
**Goal:** convert the current early-init harness into a real bounded software compositor proof.
|
||||
|
||||
**What success means:**
|
||||
|
||||
- compositor runs for a bounded interval without crashing,
|
||||
- `WAYLAND_DISPLAY` is live,
|
||||
- a client connects and survives,
|
||||
- the current `qt6-wayland-smoke` path remains a visible bounded proof target,
|
||||
- input is proven through the active compositor surface, not just through lower-layer scheme checks.
|
||||
|
||||
**Important rule:**
|
||||
|
||||
This wave is still a **validation compositor** wave, not a claim that KWin or Plasma is working.
|
||||
|
||||
### Wave 3 — KWin runtime truthfulness
|
||||
|
||||
**Goal:** turn the current reduced KWin build into an honest runtime target.
|
||||
|
||||
**Required work:**
|
||||
|
||||
- keep dependency honesty explicit,
|
||||
- prove which remaining stubs/shims are still acceptable for bounded runtime work,
|
||||
- establish one bounded KWin session proof before any Plasma support claim,
|
||||
- keep disabled features and bounded providers visible in the support language.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- KWin starts as the compositor on the tracked path,
|
||||
- the runtime session survives for a bounded interval,
|
||||
- session/login1/D-Bus surfaces needed by KWin are observable,
|
||||
- support claims still remain profile-scoped and bounded.
|
||||
|
||||
### Wave 4 — Ownership cleanup and stale-path retirement
|
||||
|
||||
**Goal:** make the doc/recipe story match the real forward path.
|
||||
|
||||
**Required work:**
|
||||
|
||||
- retire old planning authority from historical Wayland docs,
|
||||
- demote or remove stale historical compositor references from the active guidance path,
|
||||
- make the WIP recipe guidance reflect current truth instead of older partial states,
|
||||
- keep local overlay ownership explicit wherever Red Bear is still the effective shipping owner.
|
||||
|
||||
**Acceptance criteria:**
|
||||
|
||||
- one canonical Wayland subsystem plan exists,
|
||||
- stale planning references are removed,
|
||||
- historical references are clearly marked historical,
|
||||
- no active doc suggests that smallvil or generic upstream WIP compositor recipes are the forward
|
||||
Red Bear desktop path.
|
||||
|
||||
## What This Plan Supersedes
|
||||
|
||||
This plan supersedes the active planning role previously held by:
|
||||
|
||||
- `docs/03-WAYLAND-ON-REDOX.md`
|
||||
|
||||
It also reduces ambiguity in these adjacent surfaces:
|
||||
|
||||
- `recipes/wip/AGENTS.md` Wayland status notes,
|
||||
- `docs/02-GAP-ANALYSIS.md` Wayland references,
|
||||
- current-status and canonical-plan references that still pointed to the old Wayland roadmap.
|
||||
|
||||
## Docs To Keep vs. Retire
|
||||
|
||||
### Keep
|
||||
|
||||
- `local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` — canonical Wayland subsystem plan
|
||||
- `local/docs/DESKTOP-STACK-CURRENT-STATUS.md` — current truth summary
|
||||
- `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` — canonical desktop path
|
||||
- `local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` — GPU/DRM execution detail
|
||||
- `local/docs/QT6-PORT-STATUS.md` — Qt/KF6/KWin package build status
|
||||
|
||||
### Retire or demote
|
||||
|
||||
- `docs/03-WAYLAND-ON-REDOX.md` — remove as an active planning document
|
||||
- stale WIP Wayland status text that still implies `smallvil` is current or that package build status
|
||||
equals runtime viability
|
||||
|
||||
## Definition of Done
|
||||
|
||||
Wayland can be called substantially complete for the current subsystem scope only when all of the
|
||||
following are true:
|
||||
|
||||
- the bounded Wayland runtime path completes a usable software compositor session,
|
||||
- runtime input/session/device-enumeration behavior is trusted enough to support that claim,
|
||||
- KWin has at least one honest bounded runtime proof path,
|
||||
- current docs describe the same truth with no stale forward-path confusion,
|
||||
- hardware acceleration remains either separately proven or explicitly outside the claim.
|
||||
|
||||
## Current Bottom Line
|
||||
|
||||
Red Bear Wayland is no longer blocked primarily by package absence. It is blocked by runtime trust,
|
||||
compositor completion, session/input integration, and honest ownership of the forward path.
|
||||
|
||||
That is the real work. This plan makes that explicit.
|
||||
@@ -278,6 +278,14 @@ The best Red Bear Wi-Fi path is **native-first**:
|
||||
- Narrow `linux-kpi` glue only where useful (93 tests, 17 modules)
|
||||
- Native `smolnetd` / `netcfg` / `dhcpd` reused after association
|
||||
|
||||
Current bounded extraction progress:
|
||||
|
||||
- `redbear-wifictl` transport probing now consumes the shared `redox-driver-sys` PCI parser
|
||||
instead of relying only on ad hoc raw-config interpretation.
|
||||
- Transport status now reports quirk-aware interrupt support (`none` / `legacy` / `msi` / `msix`)
|
||||
from the shared substrate, which is the intended convergence direction for future GPU/Wi-Fi-only
|
||||
donor usage under `linux-kpi`.
|
||||
|
||||
The codebase has 119 tests passing (93 linux-kpi + 8 redbear-iwlwifi + 18 redbear-wifictl), no production `unwrap()` in the Wi-Fi daemon request loop (startup uses `expect()`), atomic command
|
||||
handling, proper timer cancellation, honest timeout reporting, and real 802.11 frame parsing.
|
||||
The structural skeleton is solid. The next required step is **real hardware validation** with an
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
# xhcid Device-Level Improvement Plan
|
||||
|
||||
## Purpose
|
||||
|
||||
This document defines the implementation sequence for hardening `xhcid` at the device level in
|
||||
Red Bear OS.
|
||||
|
||||
It is a focused companion to `local/docs/USB-IMPLEMENTATION-PLAN.md`. The USB plan remains the
|
||||
subsystem-wide authority; this document narrows scope to the `xhcid` device lifecycle,
|
||||
configuration, teardown, PM behavior, enumerator robustness, and bounded proof coverage.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- `recipes/core/base/source/drivers/usb/xhcid/src/xhci/device_enumerator.rs`
|
||||
- `recipes/core/base/source/drivers/usb/xhcid/src/xhci/mod.rs`
|
||||
- `recipes/core/base/source/drivers/usb/xhcid/src/xhci/scheme.rs`
|
||||
- `recipes/core/base/source/drivers/usb/xhcid/src/xhci/irq_reactor.rs`
|
||||
- bounded QEMU validation scripts under `local/scripts/`
|
||||
- canonical USB documentation under `local/docs/`
|
||||
|
||||
Out of scope:
|
||||
|
||||
- generic USB redesign
|
||||
- unrelated class-driver feature work
|
||||
- hardware-validation claims beyond what the repo can currently prove
|
||||
|
||||
## Repo-Fit Note
|
||||
|
||||
Technical implementation targets live in upstream-owned source under
|
||||
`recipes/core/base/source/...`, but durable Red Bear preservation belongs in
|
||||
`local/patches/base/`. This plan names the technical work locations, not a recommendation to leave
|
||||
work stranded only in upstream-owned trees.
|
||||
|
||||
## Current Audited Findings
|
||||
|
||||
The current `xhcid` tree has already improved materially:
|
||||
|
||||
- lifecycle gating exists through `PortLifecycle` and `PortOperationGuard`
|
||||
- `configure_endpoints_once()` is now transactional relative to earlier behavior
|
||||
- detach waits before removing published state
|
||||
- a bounded QEMU lifecycle proof exists
|
||||
|
||||
Remaining risks:
|
||||
|
||||
- partial attach visibility still exists around publication timing
|
||||
- detach can still depend on bounded-but-incomplete purge semantics
|
||||
- suspend/resume is still mainly software gating
|
||||
- rollback failure is not yet a fully hardened degraded-state path
|
||||
- enumerator logic still relies on timing- and assumption-heavy behavior
|
||||
- proof coverage is still QEMU-bounded and misses key interleavings
|
||||
|
||||
## Design Invariants
|
||||
|
||||
The implementation should satisfy these invariants:
|
||||
|
||||
1. No half-attached device is publicly usable.
|
||||
2. No new work is admitted after detach begins.
|
||||
3. Detach always reaches a bounded terminal outcome.
|
||||
4. Failed configure leaves either the old config intact or the device explicitly
|
||||
degraded/reset-required.
|
||||
5. PM transitions reflect actual usable state, not only software policy.
|
||||
6. Enumerator behavior is bounded and diagnosable, not panic-driven.
|
||||
7. Validation claims match what scripts actually prove.
|
||||
|
||||
## Phase 1 — Proof-First Expansion
|
||||
|
||||
### Goal
|
||||
|
||||
Make the current blind spots reproducible before changing behavior.
|
||||
|
||||
### Work
|
||||
|
||||
- extend `test-xhci-device-lifecycle-qemu.sh`
|
||||
- extend `test-usb-qemu.sh`
|
||||
- extend `test-xhci-irq-qemu.sh`
|
||||
- add bounded injection hooks in `xhcid` for configure-failure and attach/detach timing cases
|
||||
|
||||
### Required Cases
|
||||
|
||||
- repeated attach/detach
|
||||
- detach during storage startup
|
||||
- transfer-during-detach surrogate
|
||||
- configure failure injection
|
||||
- suspend/resume admission checks
|
||||
- rapid event ordering cases
|
||||
|
||||
### Per-File Focus
|
||||
|
||||
#### `local/scripts/test-xhci-device-lifecycle-qemu.sh`
|
||||
|
||||
- add repeated HID/storage attach-detach loops
|
||||
- add detach-during-driver-start for storage
|
||||
- add storage attach long enough to exercise startup/read activity before unplug
|
||||
- require explicit attach-entered, attach-finished, detach-completed evidence
|
||||
|
||||
#### `local/scripts/test-usb-qemu.sh`
|
||||
|
||||
- separate boot progress from proof failure
|
||||
- keep result lines distinct for xHCI init, HID spawn, SCSI spawn, bounded readback, and crash scan
|
||||
- add repeated full-stack run mode or bounded loop count if needed for ordering-sensitive regressions
|
||||
|
||||
#### `local/scripts/test-xhci-irq-qemu.sh`
|
||||
|
||||
- verify interrupt-mode evidence still holds under actual attached-device pressure, not only empty-controller boot
|
||||
|
||||
#### `xhci` test hooks
|
||||
|
||||
- add bounded test-only failure hooks in `scheme.rs` / `mod.rs` for:
|
||||
- fail after `CONFIGURE_ENDPOINT`
|
||||
- fail after `SET_CONFIGURATION`
|
||||
- optional delay before final attach commit
|
||||
- current bounded implementation uses one-shot guest-side commands written to
|
||||
`/tmp/xhcid-test-hook`, consumed by `xhcid` on the next matching lifecycle point
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- scripts are syntax-clean
|
||||
- new cases fail meaningfully on current gaps
|
||||
- failures identify the specific missed milestone
|
||||
|
||||
## Phase 2 — Atomic Attach Publication
|
||||
|
||||
### Goal
|
||||
|
||||
Prevent half-built devices from becoming publicly reachable.
|
||||
|
||||
### Work
|
||||
|
||||
- refactor `Xhci::attach_device`
|
||||
- split attach staging from published `PortState`
|
||||
- narrow lifecycle exposure so scheme paths cannot reach a device before final commit
|
||||
- make attach cleanup direct for prepublication failure
|
||||
|
||||
### Key Targets
|
||||
|
||||
- `xhci/mod.rs::Xhci::attach_device`
|
||||
- `xhci/mod.rs::PortLifecycle::*`
|
||||
- `xhci/device_enumerator.rs::DeviceEnumerator::run`
|
||||
|
||||
### Per-File Focus
|
||||
|
||||
#### `xhci/mod.rs`
|
||||
|
||||
- stop inserting into `port_states` before all attach substeps complete
|
||||
- keep slot, input context, EP0 ring, quirks, and descriptors in a private staging carrier
|
||||
- commit published `PortState` in one final block
|
||||
- keep prepublication cleanup separate from `detach_device()` where possible
|
||||
|
||||
#### `xhci/device_enumerator.rs`
|
||||
|
||||
- ensure duplicate connect handling still treats `EAGAIN` or equivalent as "already published" rather than "half-built staging state"
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- no public state before attach commit
|
||||
- attach failure leaves no published device and no child driver
|
||||
|
||||
## Phase 3 — Bounded Detach and Purge
|
||||
|
||||
### Goal
|
||||
|
||||
Make teardown bounded, dominant, and safe against stale completions.
|
||||
|
||||
### Work
|
||||
|
||||
- bound `PortLifecycle::begin_detaching()`
|
||||
- reject all new work immediately once detach starts
|
||||
- purge or tombstone pending transfer/reactor state
|
||||
- separate graceful drain from forced teardown
|
||||
- preserve correct slot-disable/remove ordering
|
||||
- ensure child-driver shutdown cannot wedge detach
|
||||
|
||||
### Key Targets
|
||||
|
||||
- `xhci/mod.rs`
|
||||
- `xhci/irq_reactor.rs`
|
||||
- transfer bookkeeping in `xhci/scheme.rs`
|
||||
|
||||
### Per-File Focus
|
||||
|
||||
#### `xhci/mod.rs`
|
||||
|
||||
- add timeout or bounded wait to detach drain logic
|
||||
- distinguish graceful drain from forced teardown
|
||||
- keep `port_states.remove(...)` after terminal teardown outcome
|
||||
|
||||
#### `xhci/irq_reactor.rs`
|
||||
|
||||
- add per-port invalidation or tombstone behavior so stale completions cannot target removed state
|
||||
|
||||
#### `xhci/scheme.rs`
|
||||
|
||||
- ensure operation-entry helpers fail immediately once detach starts
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- detach cannot hang forever
|
||||
- no stale completion can target removed device state
|
||||
- unload-under-activity proof passes
|
||||
|
||||
## Phase 4 — Configure Rollback Hardening
|
||||
|
||||
### Goal
|
||||
|
||||
Make configuration changes fully transactional and recoverable.
|
||||
|
||||
### Work
|
||||
|
||||
- formalize stage/program/commit boundaries
|
||||
- ensure snapshots cover all mutated controller-facing state
|
||||
- promote rollback failure into explicit degraded-state handling
|
||||
- define deterministic behavior for post-`SET_CONFIGURATION` failure
|
||||
- keep alternate/config bookkeeping coherent after rollback
|
||||
- quarantine or reset on unrecoverable ambiguity
|
||||
|
||||
### Key Targets
|
||||
|
||||
- `xhci/scheme.rs::configure_endpoints_once`
|
||||
- `restore_configure_input_context`
|
||||
- `configure_endpoints`
|
||||
- `set_configuration`
|
||||
- `set_interface`
|
||||
|
||||
### Per-File Focus
|
||||
|
||||
#### `xhci/scheme.rs`
|
||||
|
||||
- keep endpoint/ring state staged until commit
|
||||
- verify snapshots cover every mutated slot/endpoint field
|
||||
- treat rollback failure as a first-class degraded state
|
||||
- ensure post-failure descriptor and alternate bookkeeping still reflect live state
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- injected configure failure preserves old state or explicitly degrades/resets device
|
||||
- no staged endpoint state leaks into live software state
|
||||
|
||||
## Phase 5 — Real PM Sequencing
|
||||
|
||||
### Goal
|
||||
|
||||
Replace software-only PM gating with meaningful quiesce/resume semantics.
|
||||
|
||||
### Work
|
||||
|
||||
- define richer PM transition states
|
||||
- quiesce before suspend
|
||||
- tie resume to controller/device validity
|
||||
- define PM interaction with detach
|
||||
- define PM interaction with configure
|
||||
- add bounded PM proof cases
|
||||
|
||||
### Key Targets
|
||||
|
||||
- `xhci/scheme.rs::suspend_device`
|
||||
- `xhci/scheme.rs::resume_device`
|
||||
- `xhci/scheme.rs::ensure_port_active`
|
||||
- supporting helpers in `xhci/mod.rs`
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- suspend blocks new I/O only after quiesce starts
|
||||
- resume only returns success from a genuinely usable state
|
||||
- PM/detach/configure interleavings are deterministic
|
||||
|
||||
## Phase 6 — Enumerator Cleanup and Timing Hardening
|
||||
|
||||
### Goal
|
||||
|
||||
Remove panic-style and magic-delay behavior from the enumerator path.
|
||||
|
||||
### Work
|
||||
|
||||
- remove panic-class assumptions from `DeviceEnumerator::run`
|
||||
- replace fixed sleeps with bounded readiness checks
|
||||
- make duplicate/out-of-order event handling explicit
|
||||
- align enumerator decisions with the new attach/detach state machine
|
||||
- improve logging for reset/attach/detach milestones
|
||||
|
||||
### Key Targets
|
||||
|
||||
- `xhci/device_enumerator.rs`
|
||||
- supporting interactions in `xhci/mod.rs`
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- no ordinary event path panics
|
||||
- no unnecessary fixed sleep remains
|
||||
- rapid event-order tests pass in QEMU
|
||||
|
||||
## Phase 7 — Final Validation, Docs, and Preservation
|
||||
|
||||
### Goal
|
||||
|
||||
Close the loop with evidence, canonical docs, and durable patch carriers.
|
||||
|
||||
### Work
|
||||
|
||||
- rerun the full bounded proof matrix on a rebuilt image
|
||||
- run source-level verification (`lsp_diagnostics`, `cargo check`, `cargo test`)
|
||||
- update canonical docs:
|
||||
- `local/docs/USB-IMPLEMENTATION-PLAN.md`
|
||||
- `local/docs/USB-VALIDATION-RUNBOOK.md`
|
||||
- refresh durable patch carriers under `local/patches/base/`
|
||||
- delete only clearly stale, superseded docs after link sweep
|
||||
|
||||
### Exit Criteria
|
||||
|
||||
- all bounded USB/xHCI proofs pass on a fresh image
|
||||
- changed files are diagnostics-clean
|
||||
- canonical docs match actual proof scope
|
||||
- patch carrier is refreshed and reapplicable
|
||||
|
||||
## Validation Matrix
|
||||
|
||||
Required final proofs:
|
||||
|
||||
- `bash ./local/scripts/test-xhci-device-lifecycle-qemu.sh --check <tracked-target>`
|
||||
- `bash ./local/scripts/test-usb-qemu.sh --check <tracked-target>`
|
||||
- `bash ./local/scripts/test-xhci-irq-qemu.sh --check`
|
||||
- `bash ./local/scripts/test-usb-maturity-qemu.sh <tracked-target>`
|
||||
|
||||
Required source checks:
|
||||
|
||||
- `lsp_diagnostics` on all changed files
|
||||
- `cargo check` / `cargo test` for `xhcid`
|
||||
- `cargo check` for any touched class daemon or helper crate
|
||||
|
||||
## Commit Strategy
|
||||
|
||||
1. proof/harness expansion
|
||||
2. atomic attach publication
|
||||
3. bounded detach and purge
|
||||
4. configure rollback hardening
|
||||
5. PM sequencing
|
||||
6. enumerator cleanup
|
||||
7. docs, patch preservation, stale-doc cleanup
|
||||
|
||||
## Canonical Doc Authority
|
||||
|
||||
Authoritative docs after cleanup:
|
||||
|
||||
- `local/docs/USB-IMPLEMENTATION-PLAN.md`
|
||||
- `local/docs/USB-VALIDATION-RUNBOOK.md`
|
||||
|
||||
This xhcid plan is a focused implementation document beneath those subsystem-level authorities.
|
||||
|
||||
## Completion Standard
|
||||
|
||||
This work is complete only when:
|
||||
|
||||
- all seven phases are done in order
|
||||
- no changed-file diagnostics remain
|
||||
- `xhcid` builds/tests cleanly
|
||||
- bounded QEMU proof matrix passes on a rebuilt image
|
||||
- canonical docs are synchronized
|
||||
- durable patch carrier is refreshed
|
||||
- remaining gaps, if any, are explicitly documented as future or hardware-only work
|
||||
File diff suppressed because it is too large
Load Diff
+3250
-97
File diff suppressed because it is too large
Load Diff
@@ -493,3 +493,367 @@ index 94519448..0db1de53 100644
|
||||
page_count,
|
||||
))
|
||||
}
|
||||
diff --git a/src/event.rs b/src/event.rs
|
||||
index 7398145a..92e5793c 100644
|
||||
--- a/src/event.rs
|
||||
+++ b/src/event.rs
|
||||
@@ -8,13 +8,14 @@ use crate::{
|
||||
context,
|
||||
scheme::{self, SchemeExt, SchemeId},
|
||||
sync::{
|
||||
- CleanLockToken, LockToken, RwLock, RwLockReadGuard, RwLockWriteGuard, WaitQueue, L0, L1, L2,
|
||||
+ CleanLockToken, LockToken, Mutex, RwLock, RwLockReadGuard, RwLockWriteGuard,
|
||||
+ WaitCondition, WaitQueue, L0, L1, L2,
|
||||
},
|
||||
syscall::{
|
||||
data::Event,
|
||||
- error::{Error, Result, EBADF},
|
||||
- flag::EventFlags,
|
||||
- usercopy::UserSliceWo,
|
||||
+ error::{Error, Result, EAGAIN, EBADF, EINVAL, EINTR},
|
||||
+ flag::{EVENT_READ, EVENT_WRITE, EventFlags},
|
||||
+ usercopy::{UserSliceRo, UserSliceWo},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -25,6 +26,17 @@ pub struct EventQueue {
|
||||
queue: WaitQueue<Event>,
|
||||
}
|
||||
|
||||
+const EVENTFD_COUNTER_MAX: u64 = u64::MAX - 1;
|
||||
+const EVENTFD_TAG_BIT: usize = 1usize << (usize::BITS - 1);
|
||||
+
|
||||
+pub struct EventCounter {
|
||||
+ id: usize,
|
||||
+ counter: Mutex<L1, u64>,
|
||||
+ read_condition: WaitCondition,
|
||||
+ write_condition: WaitCondition,
|
||||
+ semaphore: bool,
|
||||
+}
|
||||
+
|
||||
impl EventQueue {
|
||||
pub fn new(id: EventQueueId) -> EventQueue {
|
||||
EventQueue {
|
||||
@@ -91,19 +103,146 @@ impl EventQueue {
|
||||
}
|
||||
}
|
||||
|
||||
+impl EventCounter {
|
||||
+ pub fn new(id: usize, init: u64, semaphore: bool) -> EventCounter {
|
||||
+ EventCounter {
|
||||
+ id,
|
||||
+ counter: Mutex::new(init),
|
||||
+ read_condition: WaitCondition::new(),
|
||||
+ write_condition: WaitCondition::new(),
|
||||
+ semaphore,
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ pub fn is_readable(&self, token: &mut CleanLockToken) -> bool {
|
||||
+ *self.counter.lock(token.token()) > 0
|
||||
+ }
|
||||
+
|
||||
+ pub fn is_writable(&self, token: &mut CleanLockToken) -> bool {
|
||||
+ *self.counter.lock(token.token()) < EVENTFD_COUNTER_MAX
|
||||
+ }
|
||||
+
|
||||
+ pub fn read(&self, buf: UserSliceWo, block: bool, token: &mut CleanLockToken) -> Result<usize> {
|
||||
+ if buf.len() < core::mem::size_of::<u64>() {
|
||||
+ return Err(Error::new(EINVAL));
|
||||
+ }
|
||||
+
|
||||
+ loop {
|
||||
+ let counter = self.counter.lock(token.token());
|
||||
+ let (mut counter, mut token) = counter.into_split();
|
||||
+
|
||||
+ if *counter > 0 {
|
||||
+ let value = if self.semaphore {
|
||||
+ *counter -= 1;
|
||||
+ 1
|
||||
+ } else {
|
||||
+ let value = *counter;
|
||||
+ *counter = 0;
|
||||
+ value
|
||||
+ };
|
||||
+
|
||||
+ buf.limit(core::mem::size_of::<u64>())
|
||||
+ .ok_or(Error::new(EINVAL))?
|
||||
+ .copy_from_slice(&value.to_ne_bytes())?;
|
||||
+
|
||||
+ trigger_locked(
|
||||
+ GlobalSchemes::Event.scheme_id(),
|
||||
+ self.id,
|
||||
+ EVENT_WRITE,
|
||||
+ token.token(),
|
||||
+ );
|
||||
+ self.write_condition.notify_locked(token.token());
|
||||
+
|
||||
+ return Ok(core::mem::size_of::<u64>());
|
||||
+ }
|
||||
+
|
||||
+ if !block {
|
||||
+ return Err(Error::new(EAGAIN));
|
||||
+ }
|
||||
+
|
||||
+ if !self
|
||||
+ .read_condition
|
||||
+ .wait(counter, "EventCounter::read", &mut token)
|
||||
+ {
|
||||
+ return Err(Error::new(EINTR));
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ pub fn write(&self, buf: UserSliceRo, block: bool, token: &mut CleanLockToken) -> Result<usize> {
|
||||
+ if buf.len() != core::mem::size_of::<u64>() {
|
||||
+ return Err(Error::new(EINVAL));
|
||||
+ }
|
||||
+
|
||||
+ let value = unsafe { buf.read_exact::<u64>()? };
|
||||
+ if value == u64::MAX {
|
||||
+ return Err(Error::new(EINVAL));
|
||||
+ }
|
||||
+
|
||||
+ loop {
|
||||
+ let counter = self.counter.lock(token.token());
|
||||
+ let (mut counter, mut token) = counter.into_split();
|
||||
+
|
||||
+ if EVENTFD_COUNTER_MAX - *counter >= value {
|
||||
+ let was_zero = *counter == 0;
|
||||
+ *counter += value;
|
||||
+
|
||||
+ if was_zero && value != 0 {
|
||||
+ trigger_locked(
|
||||
+ GlobalSchemes::Event.scheme_id(),
|
||||
+ self.id,
|
||||
+ EVENT_READ,
|
||||
+ token.token(),
|
||||
+ );
|
||||
+ self.read_condition.notify_locked(token.token());
|
||||
+ }
|
||||
+
|
||||
+ return Ok(core::mem::size_of::<u64>());
|
||||
+ }
|
||||
+
|
||||
+ if !block {
|
||||
+ return Err(Error::new(EAGAIN));
|
||||
+ }
|
||||
+
|
||||
+ if !self
|
||||
+ .write_condition
|
||||
+ .wait(counter, "EventCounter::write", &mut token)
|
||||
+ {
|
||||
+ return Err(Error::new(EINTR));
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ pub fn into_drop(self, _token: LockToken<'_, L1>) {
|
||||
+ drop(self);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
pub type EventQueueList = HashMap<EventQueueId, Arc<EventQueue>>;
|
||||
+pub type EventCounterList = HashMap<usize, Arc<EventCounter>>;
|
||||
|
||||
// Next queue id
|
||||
static NEXT_QUEUE_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
+static NEXT_COUNTER_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
/// Get next queue id
|
||||
pub fn next_queue_id() -> EventQueueId {
|
||||
EventQueueId::from(NEXT_QUEUE_ID.fetch_add(1, Ordering::SeqCst))
|
||||
}
|
||||
|
||||
+pub fn next_counter_id() -> usize {
|
||||
+ EVENTFD_TAG_BIT | NEXT_COUNTER_ID.fetch_add(1, Ordering::SeqCst)
|
||||
+}
|
||||
+
|
||||
+pub fn is_counter_id(id: usize) -> bool {
|
||||
+ id & EVENTFD_TAG_BIT != 0
|
||||
+}
|
||||
+
|
||||
// Current event queues
|
||||
static QUEUES: RwLock<L2, EventQueueList> =
|
||||
RwLock::new(EventQueueList::with_hasher(DefaultHashBuilder::new()));
|
||||
+static COUNTERS: RwLock<L2, EventCounterList> =
|
||||
+ RwLock::new(EventCounterList::with_hasher(DefaultHashBuilder::new()));
|
||||
|
||||
/// Get the event queues list, const
|
||||
pub fn queues(token: LockToken<'_, L0>) -> RwLockReadGuard<'_, L2, EventQueueList> {
|
||||
@@ -115,6 +254,14 @@ pub fn queues_mut(token: LockToken<'_, L0>) -> RwLockWriteGuard<'_, L2, EventQue
|
||||
QUEUES.write(token)
|
||||
}
|
||||
|
||||
+pub fn counters(token: LockToken<'_, L0>) -> RwLockReadGuard<'_, L2, EventCounterList> {
|
||||
+ COUNTERS.read(token)
|
||||
+}
|
||||
+
|
||||
+pub fn counters_mut(token: LockToken<'_, L0>) -> RwLockWriteGuard<'_, L2, EventCounterList> {
|
||||
+ COUNTERS.write(token)
|
||||
+}
|
||||
+
|
||||
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct RegKey {
|
||||
pub scheme: SchemeId,
|
||||
diff --git a/src/scheme/event.rs b/src/scheme/event.rs
|
||||
index 36efe5b2..c64b6bd0 100644
|
||||
--- a/src/scheme/event.rs
|
||||
+++ b/src/scheme/event.rs
|
||||
@@ -1,9 +1,12 @@
|
||||
-use alloc::sync::Arc;
|
||||
+use alloc::{sync::Arc, vec::Vec};
|
||||
use syscall::{EventFlags, O_NONBLOCK};
|
||||
|
||||
use crate::{
|
||||
context::file::InternalFlags,
|
||||
- event::{next_queue_id, queues, queues_mut, EventQueue, EventQueueId},
|
||||
+ event::{
|
||||
+ EventCounter, EventQueue, EventQueueId, counters, counters_mut, is_counter_id,
|
||||
+ next_counter_id, next_queue_id, queues, queues_mut,
|
||||
+ },
|
||||
sync::CleanLockToken,
|
||||
syscall::{
|
||||
data::Event,
|
||||
@@ -25,7 +28,7 @@ impl KernelScheme for EventScheme {
|
||||
fn kopenat(
|
||||
&self,
|
||||
id: usize,
|
||||
- _user_buf: StrOrBytes,
|
||||
+ user_buf: StrOrBytes,
|
||||
_flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: CallerCtx,
|
||||
@@ -34,13 +37,53 @@ impl KernelScheme for EventScheme {
|
||||
if id != SCHEME_ROOT_ID {
|
||||
return Err(Error::new(EACCES));
|
||||
}
|
||||
- let id = next_queue_id();
|
||||
- queues_mut(token.token()).insert(id, Arc::new(EventQueue::new(id)));
|
||||
|
||||
- Ok(OpenResult::SchemeLocal(id.get(), InternalFlags::empty()))
|
||||
+ let path = user_buf.as_str().or(Err(Error::new(EINVAL)))?;
|
||||
+ let path = path.trim_matches('/');
|
||||
+
|
||||
+ if path.is_empty() {
|
||||
+ let id = next_queue_id();
|
||||
+ queues_mut(token.token()).insert(id, Arc::new(EventQueue::new(id)));
|
||||
+ return Ok(OpenResult::SchemeLocal(id.get(), InternalFlags::empty()));
|
||||
+ }
|
||||
+
|
||||
+ let parts: Vec<&str> = path.split('/').collect();
|
||||
+ if matches!(parts.first(), Some(&"eventfd")) {
|
||||
+ let init = match parts.get(1) {
|
||||
+ Some(value) => value.parse::<u64>().map_err(|_| Error::new(EINVAL))?,
|
||||
+ None => 0_u64,
|
||||
+ };
|
||||
+ if init > u32::MAX as u64 {
|
||||
+ return Err(Error::new(EINVAL));
|
||||
+ }
|
||||
+ let semaphore = match parts.get(2) {
|
||||
+ Some(value) => match *value {
|
||||
+ "0" => Ok(false),
|
||||
+ "1" => Ok(true),
|
||||
+ _ => Err(Error::new(EINVAL)),
|
||||
+ }?,
|
||||
+ None => false,
|
||||
+ };
|
||||
+
|
||||
+ let id = next_counter_id();
|
||||
+ counters_mut(token.token()).insert(id, Arc::new(EventCounter::new(id, init, semaphore)));
|
||||
+ return Ok(OpenResult::SchemeLocal(id, InternalFlags::empty()));
|
||||
+ }
|
||||
+
|
||||
+ Err(Error::new(ENOENT))
|
||||
}
|
||||
|
||||
fn close(&self, id: usize, token: &mut CleanLockToken) -> Result<()> {
|
||||
+ if is_counter_id(id) {
|
||||
+ let counter = counters_mut(token.token())
|
||||
+ .remove(&id)
|
||||
+ .ok_or(Error::new(EBADF))?;
|
||||
+ if let Some(counter) = Arc::into_inner(counter) {
|
||||
+ counter.into_drop(token.downgrade());
|
||||
+ }
|
||||
+ return Ok(());
|
||||
+ }
|
||||
+
|
||||
let id = EventQueueId::from(id);
|
||||
let queue = queues_mut(token.token())
|
||||
.remove(&id)
|
||||
@@ -59,6 +102,15 @@ impl KernelScheme for EventScheme {
|
||||
_stored_flags: u32,
|
||||
token: &mut CleanLockToken,
|
||||
) -> Result<usize> {
|
||||
+ if is_counter_id(id) {
|
||||
+ let counter = {
|
||||
+ let handles = counters(token.token());
|
||||
+ let handle = handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
+ handle.clone()
|
||||
+ };
|
||||
+ return counter.read(buf, flags & O_NONBLOCK as u32 == 0, token);
|
||||
+ }
|
||||
+
|
||||
let id = EventQueueId::from(id);
|
||||
|
||||
let queue = {
|
||||
@@ -74,10 +126,19 @@ impl KernelScheme for EventScheme {
|
||||
&self,
|
||||
id: usize,
|
||||
buf: UserSliceRo,
|
||||
- _flags: u32,
|
||||
+ flags: u32,
|
||||
_stored_flags: u32,
|
||||
token: &mut CleanLockToken,
|
||||
) -> Result<usize> {
|
||||
+ if is_counter_id(id) {
|
||||
+ let counter = {
|
||||
+ let handles = counters(token.token());
|
||||
+ let handle = handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
+ handle.clone()
|
||||
+ };
|
||||
+ return counter.write(buf, flags & O_NONBLOCK as u32 == 0, token);
|
||||
+ }
|
||||
+
|
||||
let id = EventQueueId::from(id);
|
||||
|
||||
let queue = {
|
||||
@@ -98,8 +159,12 @@ impl KernelScheme for EventScheme {
|
||||
Ok(events_written * size_of::<Event>())
|
||||
}
|
||||
|
||||
- fn kfpath(&self, _id: usize, buf: UserSliceWo, _token: &mut CleanLockToken) -> Result<usize> {
|
||||
- buf.copy_common_bytes_from_slice(b"/scheme/event/")
|
||||
+ fn kfpath(&self, id: usize, buf: UserSliceWo, _token: &mut CleanLockToken) -> Result<usize> {
|
||||
+ if is_counter_id(id) {
|
||||
+ buf.copy_common_bytes_from_slice(b"/scheme/event/eventfd")
|
||||
+ } else {
|
||||
+ buf.copy_common_bytes_from_slice(b"/scheme/event/")
|
||||
+ }
|
||||
}
|
||||
|
||||
fn fevent(
|
||||
@@ -108,6 +173,23 @@ impl KernelScheme for EventScheme {
|
||||
flags: EventFlags,
|
||||
token: &mut CleanLockToken,
|
||||
) -> Result<EventFlags> {
|
||||
+ if is_counter_id(id) {
|
||||
+ let counter = {
|
||||
+ let handles = counters(token.token());
|
||||
+ let handle = handles.get(&id).ok_or(Error::new(EBADF))?;
|
||||
+ handle.clone()
|
||||
+ };
|
||||
+
|
||||
+ let mut ready = EventFlags::empty();
|
||||
+ if flags.contains(EventFlags::EVENT_READ) && counter.is_readable(token) {
|
||||
+ ready |= EventFlags::EVENT_READ;
|
||||
+ }
|
||||
+ if flags.contains(EventFlags::EVENT_WRITE) && counter.is_writable(token) {
|
||||
+ ready |= EventFlags::EVENT_WRITE;
|
||||
+ }
|
||||
+ return Ok(ready);
|
||||
+ }
|
||||
+
|
||||
let id = EventQueueId::from(id);
|
||||
|
||||
let queue = {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff -ruN a/src/header/elf/mod.rs b/src/header/elf/mod.rs
|
||||
--- a/src/header/elf/mod.rs
|
||||
+++ b/src/header/elf/mod.rs
|
||||
@@ -9,8 +9,8 @@
|
||||
pub type Elf32_Word = uint32_t;
|
||||
pub type Elf32_Sword = int32_t;
|
||||
-pub type Elf64_Word = uint64_t;
|
||||
-pub type Elf64_Sword = int64_t;
|
||||
+pub type Elf64_Word = uint32_t;
|
||||
+pub type Elf64_Sword = int32_t;
|
||||
|
||||
pub type Elf32_Xword = uint64_t;
|
||||
pub type Elf32_Sxword = int64_t;
|
||||
@@ -12,12 +12,9 @@ diff -ruN a/src/header/mod.rs b/src/header/mod.rs
|
||||
diff -ruN a/src/header/sys_eventfd/cbindgen.toml b/src/header/sys_eventfd/cbindgen.toml
|
||||
--- a/src/header/sys_eventfd/cbindgen.toml 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/src/header/sys_eventfd/cbindgen.toml 2026-04-15 09:46:42.009280833 +0100
|
||||
@@ -0,0 +1,12 @@
|
||||
@@ -0,0 +1,9 @@
|
||||
+sys_includes = ["stdint.h"]
|
||||
+include_guard = "_SYS_EVENTFD_H"
|
||||
+trailer = """
|
||||
+typedef uint64_t eventfd_t;
|
||||
+"""
|
||||
+language = "C"
|
||||
+style = "Tag"
|
||||
+no_includes = true
|
||||
@@ -28,7 +25,7 @@ diff -ruN a/src/header/sys_eventfd/cbindgen.toml b/src/header/sys_eventfd/cbindg
|
||||
diff -ruN a/src/header/sys_eventfd/mod.rs b/src/header/sys_eventfd/mod.rs
|
||||
--- a/src/header/sys_eventfd/mod.rs 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/src/header/sys_eventfd/mod.rs 2026-04-15 09:46:42.009305629 +0100
|
||||
@@ -0,0 +1,90 @@
|
||||
@@ -0,0 +1,87 @@
|
||||
+//! `sys/eventfd.h` implementation.
|
||||
+//!
|
||||
+//! Non-POSIX, see <https://man7.org/linux/man-pages/man2/eventfd.2.html>.
|
||||
@@ -36,6 +33,7 @@ diff -ruN a/src/header/sys_eventfd/mod.rs b/src/header/sys_eventfd/mod.rs
|
||||
+use core::{mem, slice};
|
||||
+
|
||||
+use crate::{
|
||||
+ c_str::{CStr, CString},
|
||||
+ error::{Errno, ResultExt},
|
||||
+ header::{
|
||||
+ errno::{EFAULT, EINVAL, EIO},
|
||||
@@ -80,18 +78,14 @@ diff -ruN a/src/header/sys_eventfd/mod.rs b/src/header/sys_eventfd/mod.rs
|
||||
+ oflag |= O_NONBLOCK;
|
||||
+ }
|
||||
+
|
||||
+ let fd = Sys::open(c"/scheme/event".into(), oflag, 0)?;
|
||||
+ if initval != 0 {
|
||||
+ let value = u64::from(initval);
|
||||
+ let buf = unsafe {
|
||||
+ slice::from_raw_parts((&raw const value).cast::<u8>(), mem::size_of::<u64>())
|
||||
+ };
|
||||
+ if let Err(err) = write_exact(fd, buf) {
|
||||
+ let _ = Sys::close(fd);
|
||||
+ return Err(err);
|
||||
+ }
|
||||
+ }
|
||||
+ Ok(fd)
|
||||
+ let path = CString::new(format!(
|
||||
+ "/scheme/event/eventfd/{}/{}",
|
||||
+ initval,
|
||||
+ if flags & EFD_SEMAPHORE == EFD_SEMAPHORE { 1 } else { 0 }
|
||||
+ ))
|
||||
+ .map_err(|_| Errno(EINVAL))?;
|
||||
+
|
||||
+ Sys::open(CStr::borrow(&path), oflag, 0)
|
||||
+}
|
||||
+
|
||||
+#[unsafe(no_mangle)]
|
||||
|
||||
@@ -34,7 +34,7 @@ diff --git a/tests/sys_signalfd/signalfd.c b/tests/sys_signalfd/signalfd.c
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tests/sys_signalfd/signalfd.c
|
||||
@@ -0,0 +1,22 @@
|
||||
@@ -0,0 +1,23 @@
|
||||
+#include <assert.h>
|
||||
+#include <signal.h>
|
||||
+#include <stdio.h>
|
||||
@@ -62,7 +62,7 @@ diff --git a/tests/sys_timerfd/timerfd.c b/tests/sys_timerfd/timerfd.c
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tests/sys_timerfd/timerfd.c
|
||||
@@ -0,0 +1,27 @@
|
||||
@@ -0,0 +1,29 @@
|
||||
+#include <assert.h>
|
||||
+#include <stdint.h>
|
||||
+#include <stdio.h>
|
||||
@@ -87,8 +87,44 @@ new file mode 100644
|
||||
+
|
||||
+ memset(&spec, 0, sizeof(spec));
|
||||
+ spec.it_value.tv_sec = 1;
|
||||
+ assert(timerfd_settime(fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &spec, NULL) == 0);
|
||||
+ assert(timerfd_settime(fd, TFD_TIMER_ABSTIME, &spec, NULL) == 0);
|
||||
+ assert(close(fd) == 0);
|
||||
+ puts("timerfd ok");
|
||||
+ return 0;
|
||||
+}
|
||||
diff --git a/tests/Makefile.tests.mk b/tests/Makefile.tests.mk
|
||||
--- a/tests/Makefile.tests.mk
|
||||
+++ b/tests/Makefile.tests.mk
|
||||
@@ -312,6 +312,9 @@ VARIED_NAMES=\
|
||||
grp/getgrgid_r \
|
||||
grp/getgrnam_r \
|
||||
grp/gr_iter \
|
||||
+ sys_eventfd/eventfd \
|
||||
+ sys_signalfd/signalfd \
|
||||
+ sys_timerfd/timerfd \
|
||||
waitpid \
|
||||
waitpid_multiple \
|
||||
$(FAILING_TESTS)
|
||||
diff --git a/tests/Makefile b/tests/Makefile
|
||||
--- a/tests/Makefile
|
||||
+++ b/tests/Makefile
|
||||
@@ -78,14 +78,16 @@ FLAGS=\
|
||||
-Wno-deprecated-declarations \
|
||||
-pedantic \
|
||||
-g \
|
||||
- -I .
|
||||
+ -I . \
|
||||
+ $(CPPFLAGS) $(CFLAGS)
|
||||
|
||||
STATIC_FLAGS=\
|
||||
- -static
|
||||
+ -static $(LDFLAGS)
|
||||
|
||||
DYNAMIC_FLAGS=\
|
||||
-Wl,--enable-new-dtags \
|
||||
- -Wl,-export-dynamic
|
||||
+ -Wl,-export-dynamic \
|
||||
+ $(LDFLAGS)
|
||||
|
||||
SYSROOT?=$(abspath ../sysroot/$(TARGET)/)
|
||||
SYSROOT_TARGET?=$(SYSROOT)
|
||||
|
||||
@@ -38,55 +38,3 @@ new file mode 100644
|
||||
+ puts("shmget ok");
|
||||
+ return 0;
|
||||
+}
|
||||
diff --git a/tests/sys_timerfd/timerfd.c b/tests/sys_timerfd/timerfd.c
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tests/sys_timerfd/timerfd.c
|
||||
@@ -0,0 +1,18 @@
|
||||
+#include <assert.h>
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+#include <sys/timerfd.h>
|
||||
+#include <unistd.h>
|
||||
+
|
||||
+int main(void) {
|
||||
+ int fd = timerfd_create(CLOCK_REALTIME, 0);
|
||||
+ if (fd < 0) {
|
||||
+ puts("timerfd unavailable");
|
||||
+ return 0;
|
||||
+ }
|
||||
+ struct itimerspec spec;
|
||||
+ memset(&spec, 0, sizeof(spec));
|
||||
+ spec.it_value.tv_nsec = 1;
|
||||
+ assert(timerfd_settime(fd, TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET, &spec, NULL) == 0);
|
||||
+ assert(close(fd) == 0);
|
||||
+ puts("timerfd ok");
|
||||
+ return 0;
|
||||
+}
|
||||
diff --git a/tests/sys_signalfd/signalfd.c b/tests/sys_signalfd/signalfd.c
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/tests/sys_signalfd/signalfd.c
|
||||
@@ -0,0 +1,20 @@
|
||||
+#include <assert.h>
|
||||
+#include <signal.h>
|
||||
+#include <stdio.h>
|
||||
+#include <sys/signalfd.h>
|
||||
+#include <unistd.h>
|
||||
+
|
||||
+int main(void) {
|
||||
+ sigset_t mask;
|
||||
+ assert(sigemptyset(&mask) == 0);
|
||||
+ assert(sigaddset(&mask, SIGUSR1) == 0);
|
||||
+ int fd = signalfd(-1, &mask, sizeof(mask));
|
||||
+ if (fd < 0) {
|
||||
+ puts("signalfd unavailable");
|
||||
+ return 0;
|
||||
+ }
|
||||
+ assert(kill(getpid(), SIGUSR1) == 0);
|
||||
+ struct signalfd_siginfo info;
|
||||
+ assert(read(fd, &info, sizeof(info)) == (ssize_t)sizeof(info));
|
||||
+ assert(info.ssi_signo == SIGUSR1);
|
||||
+ puts("signalfd ok");
|
||||
+ return 0;
|
||||
+}
|
||||
|
||||
@@ -1,26 +1,93 @@
|
||||
diff -ruN a/src/header/mod.rs b/src/header/mod.rs
|
||||
--- a/src/header/mod.rs 2026-04-15 09:48:02.257700000 +0100
|
||||
+++ b/src/header/mod.rs 2026-04-19 13:30:00.000000000 +0100
|
||||
@@ -98,6 +98,7 @@
|
||||
pub mod sys_resource;
|
||||
pub mod sys_select;
|
||||
+pub mod sys_signalfd;
|
||||
// TODO: sys/sem.h
|
||||
// TODO: sys/shm.h
|
||||
pub mod sys_socket;
|
||||
diff -ruN a/src/header/sys_signalfd/cbindgen.toml b/src/header/sys_signalfd/cbindgen.toml
|
||||
--- a/src/header/sys_signalfd/cbindgen.toml 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/src/header/sys_signalfd/cbindgen.toml 2026-04-15 09:48:02.257754724 +0100
|
||||
@@ -0,0 +1,12 @@
|
||||
@@ -0,0 +1,41 @@
|
||||
+sys_includes = ["signal.h", "stdint.h"]
|
||||
+include_guard = "_SYS_SIGNALFD_H"
|
||||
+trailer = """
|
||||
+#ifndef SFD_CLOEXEC
|
||||
+#define SFD_CLOEXEC 0x80000
|
||||
+#endif
|
||||
+
|
||||
+#ifndef SFD_NONBLOCK
|
||||
+#define SFD_NONBLOCK 0x800
|
||||
+#endif
|
||||
+
|
||||
+struct signalfd_siginfo {
|
||||
+ uint32_t ssi_signo;
|
||||
+ int32_t ssi_errno;
|
||||
+ int32_t ssi_code;
|
||||
+ uint32_t ssi_pid;
|
||||
+ uint32_t ssi_uid;
|
||||
+ int32_t ssi_fd;
|
||||
+ uint32_t ssi_tid;
|
||||
+ uint32_t ssi_band;
|
||||
+ uint32_t ssi_overrun;
|
||||
+ uint32_t ssi_trapno;
|
||||
+ int32_t ssi_status;
|
||||
+ int32_t ssi_int;
|
||||
+ uint64_t ssi_ptr;
|
||||
+ uint64_t ssi_utime;
|
||||
+ uint64_t ssi_stime;
|
||||
+ uint64_t ssi_addr;
|
||||
+ uint16_t ssi_addr_lsb;
|
||||
+ uint16_t __pad2;
|
||||
+ int32_t ssi_syscall;
|
||||
+ uint64_t ssi_call_addr;
|
||||
+ uint32_t ssi_arch;
|
||||
+ uint8_t __pad[28];
|
||||
+};
|
||||
+"""
|
||||
+language = "C"
|
||||
+style = "Tag"
|
||||
+no_includes = true
|
||||
+cpp_compat = true
|
||||
+
|
||||
|
||||
+[enum]
|
||||
+prefix_with_name = true
|
||||
+
|
||||
|
||||
+[export.rename]
|
||||
+"signalfd_siginfo" = "struct signalfd_siginfo"
|
||||
diff -ruN a/src/header/sys_signalfd/mod.rs b/src/header/sys_signalfd/mod.rs
|
||||
--- a/src/header/sys_signalfd/mod.rs 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/src/header/sys_signalfd/mod.rs 2026-04-15 09:48:02.257778048 +0100
|
||||
@@ -0,0 +1,6 @@
|
||||
@@ -0,0 +1,29 @@
|
||||
+//! `sys/signalfd.h` implementation.
|
||||
+
|
||||
+pub use crate::header::signal::{SFD_CLOEXEC, SFD_NONBLOCK, signalfd, signalfd4, signalfd_siginfo};
|
||||
+use crate::{
|
||||
+ header::signal::{self, sigset_t, signalfd_siginfo},
|
||||
+ platform::types::c_int,
|
||||
+};
|
||||
+
|
||||
+pub const SFD_CLOEXEC: c_int = signal::SFD_CLOEXEC;
|
||||
+pub const SFD_NONBLOCK: c_int = signal::SFD_NONBLOCK;
|
||||
+
|
||||
+#[unsafe(no_mangle)]
|
||||
+pub extern "C" fn _cbindgen_export_sys_signalfd_siginfo(siginfo: signalfd_siginfo) {}
|
||||
+pub unsafe extern "C" fn signalfd(fd: c_int, mask: *const sigset_t, masksize: usize) -> c_int {
|
||||
+ unsafe { signal::signalfd(fd, mask, masksize) }
|
||||
+}
|
||||
+
|
||||
+#[unsafe(no_mangle)]
|
||||
+pub unsafe extern "C" fn signalfd4(
|
||||
+ fd: c_int,
|
||||
+ mask: *const sigset_t,
|
||||
+ masksize: usize,
|
||||
+ flags: c_int,
|
||||
+) -> c_int {
|
||||
+ unsafe { signal::signalfd4(fd, mask, masksize, flags) }
|
||||
+}
|
||||
+
|
||||
+#[unsafe(no_mangle)]
|
||||
+pub extern "C" fn _cbindgen_export_sys_signalfd_siginfo(siginfo: signalfd_siginfo) {
|
||||
+ let _ = siginfo;
|
||||
+}
|
||||
|
||||
@@ -22,9 +22,12 @@ diff -ruN a/src/header/mod.rs b/src/header/mod.rs
|
||||
diff -ruN a/src/header/sys_ipc/cbindgen.toml b/src/header/sys_ipc/cbindgen.toml
|
||||
--- a/src/header/sys_ipc/cbindgen.toml 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/src/header/sys_ipc/cbindgen.toml 2026-04-15 09:57:28.904120977 +0100
|
||||
@@ -0,0 +1,9 @@
|
||||
@@ -0,0 +1,12 @@
|
||||
+sys_includes = ["sys/types.h"]
|
||||
+include_guard = "_SYS_IPC_H"
|
||||
+trailer = """
|
||||
+typedef struct ipc_perm ipc_perm;
|
||||
+"""
|
||||
+language = "C"
|
||||
+style = "Tag"
|
||||
+no_includes = true
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
diff -ruN a/src/header/mod.rs b/src/header/mod.rs
|
||||
--- a/src/header/mod.rs 2026-04-15 09:58:03.811510680 +0100
|
||||
+++ b/src/header/mod.rs 2026-04-15 09:59:40.902089070 +0100
|
||||
@@ -103,6 +103,7 @@
|
||||
pub mod sys_stat;
|
||||
pub mod sys_statvfs;
|
||||
pub mod sys_time;
|
||||
+pub mod sys_timerfd;
|
||||
#[deprecated]
|
||||
pub mod sys_timeb;
|
||||
//pub mod sys_times;
|
||||
diff -ruN a/src/header/sys_timerfd/cbindgen.toml b/src/header/sys_timerfd/cbindgen.toml
|
||||
--- a/src/header/sys_timerfd/cbindgen.toml 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/src/header/sys_timerfd/cbindgen.toml 2026-04-15 09:59:40.902120449 +0100
|
||||
@@ -17,7 +28,7 @@ diff -ruN a/src/header/sys_timerfd/cbindgen.toml b/src/header/sys_timerfd/cbindg
|
||||
diff -ruN a/src/header/sys_timerfd/mod.rs b/src/header/sys_timerfd/mod.rs
|
||||
--- a/src/header/sys_timerfd/mod.rs 1970-01-01 00:00:00.000000000 +0000
|
||||
+++ b/src/header/sys_timerfd/mod.rs 2026-04-15 09:59:40.902160103 +0100
|
||||
@@ -0,0 +1,94 @@
|
||||
@@ -0,0 +1,91 @@
|
||||
+//! `sys/timerfd.h` implementation.
|
||||
+//!
|
||||
+//! Non-POSIX, see <https://man7.org/linux/man-pages/man2/timerfd_create.2.html>.
|
||||
@@ -84,13 +95,10 @@ diff -ruN a/src/header/sys_timerfd/mod.rs b/src/header/sys_timerfd/mod.rs
|
||||
+
|
||||
+#[unsafe(no_mangle)]
|
||||
+pub unsafe extern "C" fn timerfd_settime(fd: c_int, flags: c_int, new: *const itimerspec, old: *mut itimerspec) -> c_int {
|
||||
+ let supported = TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET;
|
||||
+ let supported = TFD_TIMER_ABSTIME;
|
||||
+ if flags & !supported != 0 {
|
||||
+ return Err::<c_int, _>(Errno(EINVAL)).or_minus_one_errno();
|
||||
+ }
|
||||
+ if flags & TFD_TIMER_CANCEL_ON_SET != 0 && flags & TFD_TIMER_ABSTIME == 0 {
|
||||
+ return Err::<c_int, _>(Errno(EINVAL)).or_minus_one_errno();
|
||||
+ }
|
||||
+ if new.is_null() {
|
||||
+ return Err::<c_int, _>(Errno(EFAULT)).or_minus_one_errno();
|
||||
+ }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#ifndef _LINUX_EXPORT_H
|
||||
#define _LINUX_EXPORT_H
|
||||
|
||||
#include <linux/module.h>
|
||||
|
||||
#endif /* _LINUX_EXPORT_H */
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef _LINUX_REFCOUNT_H
|
||||
#define _LINUX_REFCOUNT_H
|
||||
|
||||
#include <linux/atomic.h>
|
||||
#include <linux/mutex.h>
|
||||
#include <linux/spinlock.h>
|
||||
|
||||
typedef struct {
|
||||
atomic_t refs;
|
||||
} refcount_t;
|
||||
|
||||
#define REFCOUNT_INIT(value) { .refs = { .counter = (value) } }
|
||||
|
||||
static inline unsigned int refcount_read(const refcount_t *r)
|
||||
{
|
||||
return (unsigned int)atomic_read(&r->refs);
|
||||
}
|
||||
|
||||
static inline void refcount_set(refcount_t *r, int n)
|
||||
{
|
||||
atomic_set(&r->refs, n);
|
||||
}
|
||||
|
||||
static inline void refcount_inc(refcount_t *r)
|
||||
{
|
||||
atomic_inc(&r->refs);
|
||||
}
|
||||
|
||||
static inline int refcount_inc_not_zero(refcount_t *r)
|
||||
{
|
||||
return atomic_inc_not_zero(&r->refs);
|
||||
}
|
||||
|
||||
static inline int refcount_dec_and_test(refcount_t *r)
|
||||
{
|
||||
return atomic_dec_and_test(&r->refs);
|
||||
}
|
||||
|
||||
static inline int refcount_dec_not_one(refcount_t *r)
|
||||
{
|
||||
int current;
|
||||
|
||||
do {
|
||||
current = atomic_read(&r->refs);
|
||||
if (current == 1) {
|
||||
return 0;
|
||||
}
|
||||
} while (atomic_cmpxchg(&r->refs, current, current - 1) != current);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int refcount_dec_and_mutex_lock(refcount_t *r, struct mutex *lock)
|
||||
{
|
||||
if (!refcount_dec_and_test(r)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
mutex_lock(lock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int refcount_dec_and_lock(refcount_t *r, spinlock_t *lock)
|
||||
{
|
||||
if (!refcount_dec_and_test(r)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
spin_lock(lock);
|
||||
return 1;
|
||||
}
|
||||
|
||||
#endif /* _LINUX_REFCOUNT_H */
|
||||
@@ -119,6 +119,25 @@ pub const PCI_CAP_ID_PCIE: u8 = 0x10;
|
||||
pub const PCI_CAP_ID_POWER: u8 = 0x01;
|
||||
pub const PCI_CAP_ID_VNDR: u8 = 0x09;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum InterruptSupport {
|
||||
None,
|
||||
LegacyOnly,
|
||||
Msi,
|
||||
MsiX,
|
||||
}
|
||||
|
||||
impl InterruptSupport {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::LegacyOnly => "legacy",
|
||||
Self::Msi => "msi",
|
||||
Self::MsiX => "msix",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MsixCapability {
|
||||
pub table_bar: u8,
|
||||
@@ -174,6 +193,41 @@ impl PciDeviceInfo {
|
||||
self.bars.iter().find(|b| b.index == index && b.is_memory())
|
||||
}
|
||||
|
||||
pub fn supports_msi(&self) -> bool {
|
||||
self.find_capability(PCI_CAP_ID_MSI).is_some()
|
||||
}
|
||||
|
||||
pub fn supports_msix(&self) -> bool {
|
||||
self.find_capability(PCI_CAP_ID_MSIX).is_some()
|
||||
}
|
||||
|
||||
pub fn interrupt_support(&self) -> InterruptSupport {
|
||||
let quirks = self.quirks();
|
||||
|
||||
let has_legacy = self.irq.is_some();
|
||||
let has_msi = self.supports_msi() && !quirks.contains(crate::quirks::PciQuirkFlags::NO_MSI);
|
||||
let has_msix =
|
||||
self.supports_msix() && !quirks.contains(crate::quirks::PciQuirkFlags::NO_MSIX);
|
||||
|
||||
if quirks.contains(crate::quirks::PciQuirkFlags::FORCE_LEGACY_IRQ) {
|
||||
return if has_legacy {
|
||||
InterruptSupport::LegacyOnly
|
||||
} else {
|
||||
InterruptSupport::None
|
||||
};
|
||||
}
|
||||
|
||||
if has_msix {
|
||||
InterruptSupport::MsiX
|
||||
} else if has_msi {
|
||||
InterruptSupport::Msi
|
||||
} else if has_legacy {
|
||||
InterruptSupport::LegacyOnly
|
||||
} else {
|
||||
InterruptSupport::None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quirks(&self) -> crate::quirks::PciQuirkFlags {
|
||||
crate::quirks::lookup_pci_quirks(self)
|
||||
}
|
||||
@@ -629,52 +683,11 @@ fn enumerate_pci_filtered(class: Option<u8>) -> Result<Vec<PciDeviceInfo>> {
|
||||
|
||||
let config_path = format!("{}/config", location.scheme_path());
|
||||
if let Ok(data) = std::fs::read(&config_path) {
|
||||
if data.len() < 64 {
|
||||
continue;
|
||||
if let Some(info) = parse_device_info_from_config_space(location, &data)
|
||||
.filter(|info| class.is_none_or(|class| info.class_code == class))
|
||||
{
|
||||
devices.push(info);
|
||||
}
|
||||
let class_code = data[0x0b];
|
||||
if let Some(class) = class {
|
||||
if class_code != class {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let vendor_id = u16::from_le_bytes([data[0x00], data[0x01]]);
|
||||
let device_id = u16::from_le_bytes([data[0x02], data[0x03]]);
|
||||
let subclass = data[0x0a];
|
||||
let prog_if = data[0x09];
|
||||
let revision = data[0x08];
|
||||
let header_type = data[0x0e] & 0x7F;
|
||||
let irq_line = data[0x3c];
|
||||
|
||||
let (subsystem_vendor_id, subsystem_device_id) =
|
||||
if header_type == PCI_HEADER_TYPE_NORMAL && data.len() > 0x2F {
|
||||
(
|
||||
u16::from_le_bytes([data[0x2c], data[0x2d]]),
|
||||
u16::from_le_bytes([data[0x2e], data[0x2f]]),
|
||||
)
|
||||
} else {
|
||||
(0xFFFF, 0xFFFF)
|
||||
};
|
||||
|
||||
devices.push(PciDeviceInfo {
|
||||
location,
|
||||
vendor_id,
|
||||
device_id,
|
||||
subsystem_vendor_id,
|
||||
subsystem_device_id,
|
||||
revision,
|
||||
class_code,
|
||||
subclass,
|
||||
prog_if,
|
||||
header_type,
|
||||
irq: if irq_line != 0 && irq_line != 0xff {
|
||||
Some(irq_line as u32)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
bars: Vec::new(),
|
||||
capabilities: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -688,6 +701,97 @@ fn enumerate_pci_filtered(class: Option<u8>) -> Result<Vec<PciDeviceInfo>> {
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
pub fn parse_device_info_from_config_space(location: PciLocation, data: &[u8]) -> Option<PciDeviceInfo> {
|
||||
if data.len() < 64 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let class_code = data[0x0b];
|
||||
|
||||
let header_type = data[0x0e] & 0x7F;
|
||||
let capabilities = if header_type == PCI_HEADER_TYPE_NORMAL {
|
||||
parse_capabilities_from_config_bytes(data)
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let (subsystem_vendor_id, subsystem_device_id) =
|
||||
if header_type == PCI_HEADER_TYPE_NORMAL && data.len() > 0x2F {
|
||||
(
|
||||
u16::from_le_bytes([data[0x2c], data[0x2d]]),
|
||||
u16::from_le_bytes([data[0x2e], data[0x2f]]),
|
||||
)
|
||||
} else {
|
||||
(0xFFFF, 0xFFFF)
|
||||
};
|
||||
|
||||
let irq_line = data[0x3c];
|
||||
Some(PciDeviceInfo {
|
||||
location,
|
||||
vendor_id: u16::from_le_bytes([data[0x00], data[0x01]]),
|
||||
device_id: u16::from_le_bytes([data[0x02], data[0x03]]),
|
||||
subsystem_vendor_id,
|
||||
subsystem_device_id,
|
||||
revision: data[0x08],
|
||||
class_code,
|
||||
subclass: data[0x0a],
|
||||
prog_if: data[0x09],
|
||||
header_type,
|
||||
irq: if irq_line != 0 && irq_line != 0xff {
|
||||
Some(irq_line as u32)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
bars: Vec::new(),
|
||||
capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_capabilities_from_config_bytes(data: &[u8]) -> Vec<PciCapability> {
|
||||
if data.len() < 64 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let status = u16::from_le_bytes([data[0x06], data[0x07]]);
|
||||
if status & 0x0010 == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut caps = Vec::new();
|
||||
let mut cap_ptr = usize::from(data[0x34]);
|
||||
let mut visited = 0u8;
|
||||
|
||||
while cap_ptr >= 0x40 && cap_ptr + 1 < data.len() && visited < 48 {
|
||||
let cap_id = data[cap_ptr];
|
||||
let next_ptr = usize::from(data[cap_ptr + 1]);
|
||||
|
||||
if cap_id == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let vendor_cap_id = if cap_id == PCI_CAP_ID_VNDR && cap_ptr + 2 < data.len() {
|
||||
Some(data[cap_ptr + 2])
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
caps.push(PciCapability {
|
||||
id: cap_id,
|
||||
offset: cap_ptr as u8,
|
||||
vendor_cap_id,
|
||||
});
|
||||
|
||||
if next_ptr == 0 || next_ptr <= cap_ptr {
|
||||
break;
|
||||
}
|
||||
|
||||
cap_ptr = next_ptr;
|
||||
visited += 1;
|
||||
}
|
||||
|
||||
caps
|
||||
}
|
||||
|
||||
pub fn enumerate_pci_class(class: u8) -> Result<Vec<PciDeviceInfo>> {
|
||||
enumerate_pci_filtered(Some(class))
|
||||
}
|
||||
@@ -792,4 +896,161 @@ mod tests {
|
||||
assert_eq!(parsed.device, 0x1f);
|
||||
assert_eq!(parsed.function, 0x00);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_capabilities_from_config_bytes_reads_standard_and_vendor_caps() {
|
||||
let mut data = vec![0u8; 256];
|
||||
data[0x06] = 0x10;
|
||||
data[0x34] = 0x50;
|
||||
|
||||
data[0x50] = PCI_CAP_ID_MSI;
|
||||
data[0x51] = 0x60;
|
||||
|
||||
data[0x60] = PCI_CAP_ID_VNDR;
|
||||
data[0x61] = 0x00;
|
||||
data[0x62] = 0xAB;
|
||||
|
||||
let caps = parse_capabilities_from_config_bytes(&data);
|
||||
assert_eq!(caps.len(), 2);
|
||||
assert_eq!(caps[0].id, PCI_CAP_ID_MSI);
|
||||
assert_eq!(caps[0].offset, 0x50);
|
||||
assert_eq!(caps[0].vendor_cap_id, None);
|
||||
assert_eq!(caps[1].id, PCI_CAP_ID_VNDR);
|
||||
assert_eq!(caps[1].offset, 0x60);
|
||||
assert_eq!(caps[1].vendor_cap_id, Some(0xAB));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_capabilities_from_config_bytes_stops_on_backwards_pointer() {
|
||||
let mut data = vec![0u8; 256];
|
||||
data[0x06] = 0x10;
|
||||
data[0x34] = 0x50;
|
||||
data[0x50] = PCI_CAP_ID_MSI;
|
||||
data[0x51] = 0x48;
|
||||
|
||||
let caps = parse_capabilities_from_config_bytes(&data);
|
||||
assert_eq!(caps.len(), 1);
|
||||
assert_eq!(caps[0].id, PCI_CAP_ID_MSI);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_device_info_from_config_bytes_includes_capabilities() {
|
||||
let mut data = vec![0u8; 256];
|
||||
data[0x00] = 0x86;
|
||||
data[0x01] = 0x80;
|
||||
data[0x02] = 0x34;
|
||||
data[0x03] = 0x12;
|
||||
data[0x06] = 0x10;
|
||||
data[0x08] = 0x02;
|
||||
data[0x09] = 0x01;
|
||||
data[0x0a] = PCI_CLASS_DISPLAY_VGA;
|
||||
data[0x0b] = PCI_CLASS_DISPLAY;
|
||||
data[0x0e] = PCI_HEADER_TYPE_NORMAL;
|
||||
data[0x2c] = 0x86;
|
||||
data[0x2d] = 0x80;
|
||||
data[0x2e] = 0x78;
|
||||
data[0x2f] = 0x56;
|
||||
data[0x34] = 0x50;
|
||||
data[0x3c] = 11;
|
||||
data[0x50] = PCI_CAP_ID_MSIX;
|
||||
data[0x51] = 0x00;
|
||||
|
||||
let info = parse_device_info_from_config_space(
|
||||
PciLocation {
|
||||
segment: 0,
|
||||
bus: 0,
|
||||
device: 2,
|
||||
function: 0,
|
||||
},
|
||||
&data,
|
||||
)
|
||||
.expect("display device should be parsed");
|
||||
|
||||
assert_eq!(info.vendor_id, PCI_VENDOR_ID_INTEL);
|
||||
assert_eq!(info.device_id, 0x1234);
|
||||
assert_eq!(info.subsystem_device_id, 0x5678);
|
||||
assert_eq!(info.irq, Some(11));
|
||||
assert_eq!(info.capabilities.len(), 1);
|
||||
assert_eq!(info.capabilities[0].id, PCI_CAP_ID_MSIX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_device_info_from_config_space_rejects_short_config() {
|
||||
let location = PciLocation {
|
||||
segment: 0,
|
||||
bus: 0,
|
||||
device: 0,
|
||||
function: 0,
|
||||
};
|
||||
assert!(parse_device_info_from_config_space(location, &[0u8; 32]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_support_prefers_msix_over_msi_and_legacy() {
|
||||
let info = PciDeviceInfo {
|
||||
location: PciLocation {
|
||||
segment: 0,
|
||||
bus: 0,
|
||||
device: 0,
|
||||
function: 0,
|
||||
},
|
||||
vendor_id: 0x1234,
|
||||
device_id: 0x5678,
|
||||
subsystem_vendor_id: 0xffff,
|
||||
subsystem_device_id: 0xffff,
|
||||
revision: 0,
|
||||
class_code: 0,
|
||||
subclass: 0,
|
||||
prog_if: 0,
|
||||
header_type: PCI_HEADER_TYPE_NORMAL,
|
||||
irq: Some(11),
|
||||
bars: Vec::new(),
|
||||
capabilities: vec![
|
||||
PciCapability {
|
||||
id: PCI_CAP_ID_MSI,
|
||||
offset: 0x50,
|
||||
vendor_cap_id: None,
|
||||
},
|
||||
PciCapability {
|
||||
id: PCI_CAP_ID_MSIX,
|
||||
offset: 0x60,
|
||||
vendor_cap_id: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(info.interrupt_support(), InterruptSupport::MsiX);
|
||||
assert_eq!(info.interrupt_support().as_str(), "msix");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interrupt_support_honors_no_msix_quirk() {
|
||||
let info = PciDeviceInfo {
|
||||
location: PciLocation {
|
||||
segment: 0,
|
||||
bus: 0,
|
||||
device: 0,
|
||||
function: 0,
|
||||
},
|
||||
vendor_id: 0x1022,
|
||||
device_id: 0x145C,
|
||||
subsystem_vendor_id: 0xffff,
|
||||
subsystem_device_id: 0xffff,
|
||||
revision: 0,
|
||||
class_code: 0,
|
||||
subclass: 0,
|
||||
prog_if: 0,
|
||||
header_type: PCI_HEADER_TYPE_NORMAL,
|
||||
irq: Some(9),
|
||||
bars: Vec::new(),
|
||||
capabilities: vec![PciCapability {
|
||||
id: PCI_CAP_ID_MSIX,
|
||||
offset: 0x60,
|
||||
vendor_cap_id: None,
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(info.interrupt_support(), InterruptSupport::LegacyOnly);
|
||||
assert_eq!(info.interrupt_support().as_str(), "legacy");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{UsbQuirkEntry, UsbQuirkFlags, PCI_QUIRK_ANY_ID};
|
||||
use super::{UsbQuirkEntry, UsbQuirkFlags};
|
||||
|
||||
const F_00: UsbQuirkFlags = UsbQuirkFlags::from_bits_truncate(
|
||||
UsbQuirkFlags::NEED_RESET.bits() | UsbQuirkFlags::NO_LPM.bits(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# AMD GPU driver port for Redox OS — Phase P2+P5: Display Core + DML2 + TTM
|
||||
# Scope: AMD DC modesetting, DML2 display modeling, TTM memory manager, connector detection.
|
||||
# Full acceleration (compute, video decode) requires Mesa radeonsi backend.
|
||||
# AMD GPU retained display glue path for Redox OS
|
||||
# Scope: bounded Red Bear display glue path for init, connector detection, and modeset.
|
||||
# Imported Linux AMD DC / TTM / amdgpu core trees remain adjacent source under compile triage and
|
||||
# are not part of the default retained build path. Full acceleration still requires broader GPU work
|
||||
# plus Mesa radeonsi backend enablement.
|
||||
|
||||
[source]
|
||||
# Local overlay recipe. The extracted Linux 7.0-rc7 AMDGPU tree lives next to this
|
||||
@@ -20,13 +22,13 @@ DYNAMIC_INIT
|
||||
# Paths
|
||||
AMD_ROOT="${COOKBOOK_SOURCE}/../../amdgpu-source/gpu/drm/amd"
|
||||
AMD_SRC="${AMD_ROOT}"
|
||||
TTM_SRC="${COOKBOOK_SOURCE}/../../amdgpu-source/gpu/drm/ttm"
|
||||
INCLUDES="${COOKBOOK_SOURCE}/../../amdgpu-source/include"
|
||||
LINUX_KPI="${COOKBOOK_ROOT}/local/recipes/drivers/linux-kpi/source/src/c_headers"
|
||||
REDOX_GLUE="${COOKBOOK_SOURCE}"
|
||||
TARGET_CC="${TARGET}-gcc"
|
||||
|
||||
# Compiler flags for AMD driver — DML2 enabled
|
||||
# Compiler flags for the bounded retained AMD path. Legacy AMD DC config defines remain here only
|
||||
# for header compatibility with the adjacent imported Linux source trees.
|
||||
export CFLAGS="-D__redox__ -D__KERNEL__ -DCONFIG_DRM_AMDGPU -DCONFIG_DRM_AMD_DC \
|
||||
-DCONFIG_DRM_AMD_DC_DML2=1 \
|
||||
-DCONFIG_DRM_AMD_DC_FP -DCONFIG_DRM_AMD_ACP \
|
||||
@@ -66,11 +68,20 @@ export CFLAGS="-D__redox__ -D__KERNEL__ -DCONFIG_DRM_AMDGPU -DCONFIG_DRM_AMD_DC
|
||||
"${TARGET_CC}" -c ${CFLAGS} "${REDOX_GLUE}/amdgpu_redox_main.c" -o amdgpu_redox_main.o
|
||||
"${TARGET_CC}" -c ${CFLAGS} "${REDOX_GLUE}/redox_stubs.c" -o redox_stubs.o
|
||||
|
||||
# Stage 2: Compile AMD Display Core (DC) — all display sources including DML/DML2
|
||||
# Each file MUST compile. Any failure is a hard error.
|
||||
# Stage 2: Bounded first-display path
|
||||
#
|
||||
# The current Red Bear AMD display bring-up path does not call into the imported
|
||||
# Linux AMD Display Core tree directly. The live FFI surface comes from the
|
||||
# Red Bear glue layer (`amdgpu_redox_main.c` / `redox_stubs.c`), while the
|
||||
# broad `display/*.c` compile currently drags in optional and unsupported
|
||||
# subtrees such as freesync before the retained path is even proven.
|
||||
#
|
||||
# Keep Stage 2 explicit and intentionally empty until a retained imported
|
||||
# display-source subset is proven necessary by bounded compile triage.
|
||||
DISPLAY_SRCS=""
|
||||
success=0
|
||||
failed=0
|
||||
find "${AMD_SRC}/display/" -name '*.c' | while read -r src; do
|
||||
for src in $DISPLAY_SRCS; do
|
||||
obj=$(basename "${src%.c}.o")
|
||||
if "${TARGET_CC}" -c ${CFLAGS} "$src" -o "$obj" 2>"${obj}.log"; then
|
||||
success=$((success + 1))
|
||||
@@ -81,12 +92,17 @@ find "${AMD_SRC}/display/" -name '*.c' | while read -r src; do
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Stage 2: AMD DC compiled ${success} files, ${failed} failed"
|
||||
echo "Stage 2: bounded AMD display path compiled ${success} imported display files, ${failed} failed"
|
||||
|
||||
# Stage 3: Compile TTM memory manager
|
||||
# Stage 3: Imported TTM path
|
||||
#
|
||||
# The current bounded Red Bear display path uses Rust-side GEM/GTT/ring handling in
|
||||
# `redox-drm`, not the imported Linux TTM stack. Keep this explicit and empty until
|
||||
# the bounded path proves a concrete need for imported TTM code.
|
||||
TTM_SRCS=""
|
||||
success=0
|
||||
failed=0
|
||||
find "${TTM_SRC}/" -name '*.c' | while read -r src; do
|
||||
for src in $TTM_SRCS; do
|
||||
obj=$(basename "${src%.c}.o")
|
||||
if "${TARGET_CC}" -c ${CFLAGS} "$src" -o "$obj" 2>"${obj}.log"; then
|
||||
success=$((success + 1))
|
||||
@@ -97,13 +113,15 @@ find "${TTM_SRC}/" -name '*.c' | while read -r src; do
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "Stage 3: TTM compiled ${success} files, ${failed} failed"
|
||||
echo "Stage 3: bounded imported TTM path compiled ${success} files, ${failed} failed"
|
||||
|
||||
# Stage 4: Compile minimal amdgpu core (enough for display init)
|
||||
CORE_SRCS="amdgpu_device.c amdgpu_drv.c amdgpu_i2c.c amdgpu_atombios.c \
|
||||
amdgpu_atombios_crtc.c amdgpu_bios.c amdgpu_mode.c amdgpu_display.c \
|
||||
amdgpu_fb.c amdgpu_gem.c amdgpu_object.c amdgpu_gmc.c amdgpu_mmhub.c \
|
||||
amdgpu_irq.c amdgpu_ring.c amdgpu_fence.c amdgpu_ttm.c amdgpu_bo_list.c"
|
||||
# Stage 4: Imported amdgpu core path
|
||||
#
|
||||
# The current bounded Red Bear display path uses the custom glue layer for init,
|
||||
# connector enumeration, and modeset, while Rust-side code owns GEM/GTT/ring state.
|
||||
# Keep imported amdgpu core sources out of the retained compile surface until the
|
||||
# bounded path proves a specific dependency on them.
|
||||
CORE_SRCS=""
|
||||
|
||||
success=0
|
||||
failed=0
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
CC = x86_64-unknown-redox-gcc
|
||||
AR = x86_64-unknown-redox-ar
|
||||
|
||||
AMDGPU_SRC ?= ../amdgpu-source/gpu/drm/amd
|
||||
TTM_SRC ?= ../amdgpu-source/gpu/drm/ttm
|
||||
AMDGPU_INCLUDES ?= ../amdgpu-source/include
|
||||
LINUX_KPI ?= ../../drivers/linux-kpi/src/c_headers
|
||||
|
||||
CFLAGS ?= -D__redox__ -D__KERNEL__ -DCONFIG_DRM_AMDGPU -DCONFIG_DRM_AMD_DC \
|
||||
-DCONFIG_DRM_AMD_DC_DML2=1 \
|
||||
-DCONFIG_DRM_AMD_DC_FP -DCONFIG_DRM_AMD_ACP \
|
||||
-I$(LINUX_KPI) \
|
||||
-I. \
|
||||
-I$(AMDGPU_INCLUDES) \
|
||||
-I$(AMDGPU_INCLUDES)/drm \
|
||||
-I$(AMDGPU_SRC)/include \
|
||||
-I$(AMDGPU_SRC)/include/asic_reg \
|
||||
-I$(AMDGPU_SRC)/display \
|
||||
-I$(AMDGPU_SRC)/display/dc \
|
||||
-I$(AMDGPU_SRC)/display/dc/dml \
|
||||
-I$(AMDGPU_SRC)/display/dc/dcn20 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dcn21 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dcn30 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dcn301 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dcn31 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dcn32 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dcn35 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dml2_0 \
|
||||
-I$(AMDGPU_SRC)/display/dc/dml2_0/dml21 \
|
||||
-I$(AMDGPU_SRC)/display/dmub \
|
||||
-I$(AMDGPU_SRC)/display/modules \
|
||||
-I$(AMDGPU_SRC)/display/modules/freesync \
|
||||
-I$(AMDGPU_SRC)/display/modules/color \
|
||||
-I$(AMDGPU_SRC)/display/modules/info_packet \
|
||||
-I$(AMDGPU_SRC)/display/modules/power \
|
||||
-I$(AMDGPU_SRC)/pm/swsmu \
|
||||
-I$(AMDGPU_SRC)/pm/swsmu/inc \
|
||||
-I$(AMDGPU_SRC)/pm/powerplay \
|
||||
-I$(AMDGPU_SRC)/pm/powerplay/inc \
|
||||
-I$(AMDGPU_SRC)/pm/powerplay/hwmgr \
|
||||
-fPIC -O2 -Wall -Wno-unused-function -Wno-unused-variable \
|
||||
-Wno-address-of-packed-member -Wno-initializer-overrides
|
||||
|
||||
LDFLAGS ?= -shared
|
||||
LDLIBS ?= -lredox_driver_sys -llinux_kpi -lm -lpthread
|
||||
|
||||
GLUE_OBJS := redox_stubs.o amdgpu_redox_main.o
|
||||
CORE_SRCS := \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_device.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_drv.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_i2c.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_atombios.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_atombios_crtc.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_bios.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_mode.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_display.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_fb.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_gem.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_object.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_gmc.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_mmhub.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_irq.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_ring.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_fence.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_ttm.c \
|
||||
$(AMDGPU_SRC)/amdgpu/amdgpu_bo_list.c
|
||||
CORE_OBJS := $(patsubst %.c,%.o,$(notdir $(CORE_SRCS)))
|
||||
DISPLAY_SRCS := $(shell find $(AMDGPU_SRC)/display -name '*.c')
|
||||
DISPLAY_OBJS := $(patsubst %.c,%.o,$(notdir $(DISPLAY_SRCS)))
|
||||
TTM_SRCS := $(shell find $(TTM_SRC) -name '*.c')
|
||||
TTM_OBJS := $(patsubst %.c,%.o,$(notdir $(TTM_SRCS)))
|
||||
|
||||
ALL_OBJS := $(GLUE_OBJS) $(DISPLAY_OBJS) $(TTM_OBJS) $(CORE_OBJS)
|
||||
|
||||
.PHONY: all clean check display core ttm
|
||||
|
||||
all: libamdgpu_dc_redox.so
|
||||
|
||||
libamdgpu_dc_redox.so: $(GLUE_OBJS)
|
||||
@set -e; \
|
||||
success=0; failed=0; \
|
||||
for src in $(DISPLAY_SRCS); do \
|
||||
obj=$$(basename "$${src%.c}.o"); \
|
||||
if $(CC) -c $(CFLAGS) "$$src" -o "$$obj"; then \
|
||||
success=$$((success + 1)); \
|
||||
else \
|
||||
failed=$$((failed + 1)); \
|
||||
echo "ERROR: failed to compile $$src"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done; \
|
||||
for src in $(TTM_SRCS); do \
|
||||
obj=$$(basename "$${src%.c}.o"); \
|
||||
if $(CC) -c $(CFLAGS) "$$src" -o "$$obj"; then \
|
||||
success=$$((success + 1)); \
|
||||
else \
|
||||
failed=$$((failed + 1)); \
|
||||
echo "ERROR: failed to compile $$src"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
done; \
|
||||
for src in $(CORE_SRCS); do \
|
||||
if [ -f "$$src" ]; then \
|
||||
obj=$$(basename "$${src%.c}.o"); \
|
||||
if $(CC) -c $(CFLAGS) "$$src" -o "$$obj"; then \
|
||||
success=$$((success + 1)); \
|
||||
else \
|
||||
failed=$$((failed + 1)); \
|
||||
echo "ERROR: failed to compile $$src"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
fi; \
|
||||
done; \
|
||||
echo "AMD DC: compiled $$success files successfully"; \
|
||||
$(CC) $(LDFLAGS) -o $@ $$(find . -maxdepth 1 -name '*.o' -size +0c) $(LDLIBS)
|
||||
|
||||
redox_stubs.o: redox_stubs.c redox_glue.h
|
||||
$(CC) -c $(CFLAGS) $< -o $@
|
||||
|
||||
amdgpu_redox_main.o: amdgpu_redox_main.c redox_glue.h
|
||||
$(CC) -c $(CFLAGS) $< -o $@
|
||||
|
||||
check: $(GLUE_OBJS)
|
||||
$(CC) -fsyntax-only $(CFLAGS) amdgpu_redox_main.c
|
||||
$(CC) -fsyntax-only $(CFLAGS) redox_stubs.c
|
||||
|
||||
clean:
|
||||
rm -f *.o libamdgpu_dc_redox.so
|
||||
@@ -5,6 +5,7 @@ const LIB_NAME: &str = "libamdgpu_dc_redox.so";
|
||||
const ENV_HINTS: &[&str] = &[
|
||||
"AMDGPU_DC_LIB_DIR",
|
||||
"COOKBOOK_STAGE",
|
||||
"COOKBOOK_SYSROOT",
|
||||
"REDOX_SYSROOT",
|
||||
"SYSROOT",
|
||||
"TARGET_SYSROOT",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::collections::{BTreeMap, HashSet, VecDeque};
|
||||
use std::mem::size_of;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -286,6 +286,7 @@ enum NodeKind {
|
||||
struct Handle {
|
||||
node: NodeKind,
|
||||
response: Vec<u8>,
|
||||
event_queue: VecDeque<Vec<u8>>,
|
||||
mapped_gem: Option<GemHandle>,
|
||||
mapped_gem_refs: usize,
|
||||
owned_fbs: Vec<u32>,
|
||||
@@ -548,6 +549,7 @@ impl DrmScheme {
|
||||
Handle {
|
||||
node,
|
||||
response: Vec::new(),
|
||||
event_queue: VecDeque::new(),
|
||||
mapped_gem: None,
|
||||
mapped_gem_refs: 0,
|
||||
owned_fbs: Vec::new(),
|
||||
@@ -636,8 +638,34 @@ impl DrmScheme {
|
||||
|
||||
pub fn handle_driver_event(&mut self, event: DriverEvent) {
|
||||
match event {
|
||||
DriverEvent::Vblank { crtc_id, count } => self.retire_vblank(crtc_id, count),
|
||||
DriverEvent::Hotplug { .. } => {}
|
||||
DriverEvent::Vblank { crtc_id, count } => {
|
||||
self.retire_vblank(crtc_id, count);
|
||||
self.queue_card_event(format!("vblank:{crtc_id}:{count}\n").into_bytes());
|
||||
}
|
||||
DriverEvent::Hotplug { connector_id } => self.queue_hotplug_event(connector_id),
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_card_event(&mut self, payload: Vec<u8>) {
|
||||
for handle in self.handles.values_mut() {
|
||||
if let NodeKind::Card = handle.node {
|
||||
handle.event_queue.push_back(payload.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn queue_hotplug_event(&mut self, connector_id: u32) {
|
||||
let payload = format!("hotplug:{}\n", connector_id).into_bytes();
|
||||
for handle in self.handles.values_mut() {
|
||||
match handle.node {
|
||||
NodeKind::Card => {
|
||||
handle.event_queue.push_back(payload.clone());
|
||||
}
|
||||
NodeKind::Connector(id) if id == connector_id => {
|
||||
handle.event_queue.push_back(payload.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1394,9 +1422,19 @@ impl SchemeBlockMut for DrmScheme {
|
||||
|
||||
fn read(&mut self, id: usize, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get_mut(&id).ok_or_else(|| Error::new(EBADF))?;
|
||||
let len = handle.response.len().min(buf.len());
|
||||
buf[..len].copy_from_slice(&handle.response[..len]);
|
||||
Ok(Some(len))
|
||||
if !handle.response.is_empty() {
|
||||
let len = handle.response.len().min(buf.len());
|
||||
buf[..len].copy_from_slice(&handle.response[..len]);
|
||||
return Ok(Some(len));
|
||||
}
|
||||
|
||||
if let Some(event) = handle.event_queue.pop_front() {
|
||||
let len = event.len().min(buf.len());
|
||||
buf[..len].copy_from_slice(&event[..len]);
|
||||
return Ok(Some(len));
|
||||
}
|
||||
|
||||
Ok(Some(0))
|
||||
}
|
||||
|
||||
fn write(&mut self, id: usize, buf: &[u8]) -> Result<Option<usize>> {
|
||||
@@ -1428,7 +1466,11 @@ impl SchemeBlockMut for DrmScheme {
|
||||
fn fstat(&mut self, id: usize, stat: &mut Stat) -> Result<Option<usize>> {
|
||||
let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
||||
stat.st_mode = MODE_FILE | 0o666;
|
||||
stat.st_size = handle.response.len() as u64;
|
||||
stat.st_size = if !handle.response.is_empty() {
|
||||
handle.response.len() as u64
|
||||
} else {
|
||||
handle.event_queue.front().map(|payload| payload.len()).unwrap_or(0) as u64
|
||||
};
|
||||
stat.st_blksize = 4096;
|
||||
Ok(Some(0))
|
||||
}
|
||||
@@ -1441,9 +1483,14 @@ impl SchemeBlockMut for DrmScheme {
|
||||
Err(Error::new(EOPNOTSUPP))
|
||||
}
|
||||
|
||||
fn fevent(&mut self, id: usize, _flags: EventFlags) -> Result<Option<EventFlags>> {
|
||||
let _ = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
||||
Ok(Some(EventFlags::empty()))
|
||||
fn fevent(&mut self, id: usize, flags: EventFlags) -> Result<Option<EventFlags>> {
|
||||
let handle = self.handles.get(&id).ok_or_else(|| Error::new(EBADF))?;
|
||||
let readiness = if handle.event_queue.is_empty() {
|
||||
EventFlags::empty()
|
||||
} else {
|
||||
flags & EventFlags::EVENT_READ
|
||||
};
|
||||
Ok(Some(readiness))
|
||||
}
|
||||
|
||||
fn close(&mut self, id: usize) -> Result<Option<usize>> {
|
||||
@@ -1785,6 +1832,13 @@ mod tests {
|
||||
scheme.open("card0", 0, 0, 0).unwrap().unwrap()
|
||||
}
|
||||
|
||||
fn open_connector(scheme: &mut DrmScheme, connector_id: u32) -> usize {
|
||||
scheme
|
||||
.open(&format!("card0Connector/{connector_id}"), 0, 0, 0)
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn write_ioctl<T>(scheme: &mut DrmScheme, id: usize, request: usize, payload: &T) -> Result<usize> {
|
||||
let mut buf = request.to_le_bytes().to_vec();
|
||||
buf.extend_from_slice(&bytes_of(payload));
|
||||
@@ -1998,6 +2052,7 @@ mod tests {
|
||||
#[test]
|
||||
fn non_vblank_driver_event_does_not_retire_pending_page_flip() {
|
||||
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
|
||||
let card = open_card(&mut scheme);
|
||||
|
||||
scheme.fb_registry.insert(
|
||||
9,
|
||||
@@ -2015,6 +2070,73 @@ mod tests {
|
||||
|
||||
assert_eq!(scheme.pending_flip_fb.get(&1), Some(&(2, 9)));
|
||||
assert!(scheme.fb_registry.contains_key(&9));
|
||||
assert_eq!(
|
||||
scheme.fevent(card, EventFlags::EVENT_READ).unwrap(),
|
||||
Some(EventFlags::EVENT_READ)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hotplug_event_is_readable_from_card_handle() {
|
||||
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
|
||||
let card = open_card(&mut scheme);
|
||||
|
||||
scheme.handle_driver_event(DriverEvent::Hotplug { connector_id: 7 });
|
||||
|
||||
assert_eq!(
|
||||
scheme.fevent(card, EventFlags::EVENT_READ).unwrap(),
|
||||
Some(EventFlags::EVENT_READ)
|
||||
);
|
||||
|
||||
let mut buf = [0u8; 32];
|
||||
let len = scheme.read(card, &mut buf).unwrap().unwrap();
|
||||
assert_eq!(&buf[..len], b"hotplug:7\n");
|
||||
assert_eq!(
|
||||
scheme.fevent(card, EventFlags::EVENT_READ).unwrap(),
|
||||
Some(EventFlags::empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hotplug_event_targets_matching_connector_handle_only() {
|
||||
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
|
||||
let connector_a = open_connector(&mut scheme, 1);
|
||||
let connector_b = open_connector(&mut scheme, 2);
|
||||
|
||||
scheme.handle_driver_event(DriverEvent::Hotplug { connector_id: 2 });
|
||||
|
||||
assert_eq!(
|
||||
scheme.fevent(connector_a, EventFlags::EVENT_READ).unwrap(),
|
||||
Some(EventFlags::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
scheme.fevent(connector_b, EventFlags::EVENT_READ).unwrap(),
|
||||
Some(EventFlags::EVENT_READ)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vblank_event_is_readable_from_card_handle() {
|
||||
let mut scheme = DrmScheme::new(Arc::new(FakeDriver::new(false)));
|
||||
let card = open_card(&mut scheme);
|
||||
|
||||
scheme.handle_driver_event(DriverEvent::Vblank {
|
||||
crtc_id: 4,
|
||||
count: 12,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
scheme.fevent(card, EventFlags::EVENT_READ).unwrap(),
|
||||
Some(EventFlags::EVENT_READ)
|
||||
);
|
||||
|
||||
let mut buf = [0u8; 32];
|
||||
let len = scheme.read(card, &mut buf).unwrap().unwrap();
|
||||
assert_eq!(&buf[..len], b"vblank:4:12\n");
|
||||
assert_eq!(
|
||||
scheme.fevent(card, EventFlags::EVENT_READ).unwrap(),
|
||||
Some(EventFlags::empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,7 +24,7 @@ include(ECMQtDeclareLoggingCategory)
|
||||
include(ECMDeprecationSettings)
|
||||
include(ECMMarkNonGuiExecutable)
|
||||
include(KDEGitCommitHooks)
|
||||
########include(ECMQmlModule)
|
||||
####################include(ECMQmlModule)
|
||||
|
||||
|
||||
include(CMakeDependentOption)
|
||||
@@ -41,6 +41,18 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
|
||||
# shall we use DBus?
|
||||
# enabled per default on Linux & BSD systems
|
||||
@@ -63,7 +75,7 @@ ecm_setup_version(PROJECT VARIABLE_PREFIX KCMUTILS
|
||||
PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF6KCMUtilsConfigVersion.cmake"
|
||||
SOVERSION 6)
|
||||
|
||||
#######find_package(KF6KIO ${KF_DEP_VERSION} REQUIRED)
|
||||
###################find_package(KF6KIO ${KF_DEP_VERSION} REQUIRED)
|
||||
find_package(KF6ItemViews ${KF_DEP_VERSION} REQUIRED)
|
||||
find_package(KF6ConfigWidgets ${KF_DEP_VERSION} REQUIRED)
|
||||
find_package(KF6CoreAddons ${KF_DEP_VERSION} REQUIRED)
|
||||
|
||||
@@ -14,8 +14,8 @@ ecm_qt_declare_logging_category(kcmutils_logging_STATIC
|
||||
|
||||
|
||||
add_subdirectory(core)
|
||||
#######add_subdirectory(qml)
|
||||
#######add_subdirectory(quick)
|
||||
###################add_subdirectory(qml)
|
||||
###################add_subdirectory(quick)
|
||||
|
||||
########### kcmutils ###############
|
||||
set(kcmutils_LIB_SRCS
|
||||
@@ -118,4 +118,4 @@ ecm_qt_install_logging_categories(
|
||||
DESTINATION "${KDE_INSTALL_LOGGINGCATEGORIESDIR}"
|
||||
)
|
||||
|
||||
#######add_subdirectory(kcmshell)
|
||||
###################add_subdirectory(kcmshell)
|
||||
|
||||
@@ -34,6 +34,22 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
|
||||
set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].")
|
||||
|
||||
|
||||
@@ -42,6 +42,22 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
|
||||
find_package(KF6Codecs ${KF_DEP_VERSION} REQUIRED)
|
||||
find_package(KF6Config ${KF_DEP_VERSION} REQUIRED)
|
||||
|
||||
@@ -29,6 +29,22 @@ find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
|
||||
# shall we use DBus?
|
||||
# enabled per default on Linux & BSD systems
|
||||
|
||||
@@ -49,8 +49,8 @@ QString glRenderer()\
|
||||
return QString();\
|
||||
}' "${KCRASH_SRC}"
|
||||
|
||||
if ! grep -q "sys/wait.h" "${COOKBOOK_SOURCE}/src/kcrash.cpp"; then
|
||||
printf "%s\n" "#include <sys/resource.h>" "#include <sys/wait.h>" > /tmp/wait_h.txt
|
||||
if ! grep -q "<grp.h>" "${COOKBOOK_SOURCE}/src/kcrash.cpp"; then
|
||||
printf "%s\n" "#include <grp.h>" "#include <sys/resource.h>" "#include <sys/wait.h>" > /tmp/wait_h.txt
|
||||
sed -i "\\/<sys.resource.h>/{
|
||||
r /tmp/wait_h.txt
|
||||
d
|
||||
@@ -78,4 +78,4 @@ for lib in "${COOKBOOK_STAGE}/usr/lib/"libKF6*.so.*; do
|
||||
[ -f "${lib}" ] || continue
|
||||
patchelf --remove-rpath "${lib}" 2>/dev/null || true
|
||||
done
|
||||
'''
|
||||
'''
|
||||
|
||||
@@ -33,7 +33,7 @@ if(WITH_X11)
|
||||
find_package(X11 REQUIRED)
|
||||
set(HAVE_X11 TRUE)
|
||||
endif()
|
||||
#######find_package(Qt6Test REQUIRED)
|
||||
#########################find_package(Qt6Test REQUIRED)
|
||||
include(ECMGenerateExportHeader)
|
||||
include(ECMSetupVersion)
|
||||
include(ECMGenerateHeaders)
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
#include <qplatformdefs.h>
|
||||
#ifndef Q_OS_WIN
|
||||
#include <cerrno>
|
||||
#include <grp.h>
|
||||
#include <sys/resource.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/un.h>
|
||||
#else
|
||||
#include <qt_windows.h>
|
||||
|
||||
@@ -32,7 +32,7 @@ find_package(KF6GuiAddons ${KF_DEP_VERSION} REQUIRED)
|
||||
|
||||
|
||||
if(NOT WIN32 AND NOT APPLE AND NOT ANDROID AND NOT REDOX)
|
||||
#### find_package(KF6GlobalAccel ${KF_DEP_VERSION} REQUIRED)
|
||||
################# find_package(KF6GlobalAccel ${KF_DEP_VERSION} REQUIRED)
|
||||
set(HAVE_KGLOBALACCEL TRUE)
|
||||
else()
|
||||
set(HAVE_KGLOBALACCEL FALSE)
|
||||
|
||||
@@ -25,7 +25,7 @@ include(ECMQtDeclareLoggingCategory)
|
||||
include(ECMDeprecationSettings)
|
||||
include(ECMAddQch)
|
||||
include(CMakeDependentOption)
|
||||
##############################include(ECMQmlModule)
|
||||
##############################################include(ECMQmlModule)
|
||||
|
||||
set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].")
|
||||
|
||||
@@ -70,10 +70,26 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6Svg ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
|
||||
|
||||
if (FALSE)
|
||||
########################### find_package(Qt6 ${REQUIRED_QT_VERSION} NO_MODULE REQUIRED Qml Quick)
|
||||
########################################### find_package(Qt6 ${REQUIRED_QT_VERSION} NO_MODULE REQUIRED Qml Quick)
|
||||
endif()
|
||||
|
||||
# shall we use DBus?
|
||||
|
||||
@@ -2,7 +2,7 @@ configure_file(config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h)
|
||||
|
||||
add_subdirectory(tools/kiconfinder)
|
||||
if (KICONTHEMES_USE_QTQUICK)
|
||||
######################### add_subdirectory(qml)
|
||||
######################################### add_subdirectory(qml)
|
||||
endif()
|
||||
if (APPLE)
|
||||
add_subdirectory(tools/ksvg2icns)
|
||||
|
||||
@@ -17,6 +17,30 @@
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
|
||||
#include <QHostInfo>
|
||||
#include "usernotificationhandler_p.h"
|
||||
#include "workerbase.h"
|
||||
|
||||
@@ -30,6 +30,22 @@ find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Widgets)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
|
||||
set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].")
|
||||
|
||||
|
||||
@@ -35,6 +35,18 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
|
||||
if(NOT WIN32 AND NOT APPLE AND NOT ANDROID AND NOT HAIKU)
|
||||
option(WITH_X11 "Build with support for QX11Info::appUserTime()" ON)
|
||||
|
||||
@@ -75,7 +75,7 @@ target_link_libraries(KF6JobWidgets
|
||||
KF6::CoreAddons # KJob
|
||||
PRIVATE
|
||||
KF6::WidgetsAddons # KSqueezedTextLabel
|
||||
##KF6::Notifications
|
||||
##############KF6::Notifications
|
||||
)
|
||||
if (HAVE_QTDBUS)
|
||||
target_link_libraries(KF6JobWidgets PRIVATE Qt6::DBus)
|
||||
@@ -93,7 +93,7 @@ ecm_generate_headers(KJobWidgets_HEADERS
|
||||
KUiServerV2JobTracker
|
||||
KStatusBarJobTracker
|
||||
KWidgetJobTracker
|
||||
##KNotificationJobUiDelegate
|
||||
##############KNotificationJobUiDelegate
|
||||
|
||||
REQUIRED_HEADERS KJobWidgets_HEADERS
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ script = """
|
||||
DYNAMIC_INIT
|
||||
|
||||
HOST_BUILD="${COOKBOOK_ROOT}/build/qt-host-build"
|
||||
QDBUSXML2CPP="${HOST_BUILD}/bin/qdbusxml2cpp"
|
||||
|
||||
for qtdir in plugins mkspecs metatypes modules; do
|
||||
if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ ! -e "${COOKBOOK_SYSROOT}/${qtdir}" ]; then
|
||||
@@ -36,9 +37,19 @@ sed -i 's/^include(ECMQmlModule)/#include(ECMQmlModule)/' \
|
||||
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
|
||||
sed -i 's/^ add_subdirectory(qml)/# add_subdirectory(qml)/' \
|
||||
"${COOKBOOK_SOURCE}/src/CMakeLists.txt" 2>/dev/null || true
|
||||
sed -i 's/^#add_subdirectory(src)/add_subdirectory(src)/' \
|
||||
sed -i 's/^#\\+add_subdirectory(src)/add_subdirectory(src)/' \
|
||||
"${COOKBOOK_SOURCE}/CMakeLists.txt" 2>/dev/null || true
|
||||
|
||||
if [ -x "${QDBUSXML2CPP}" ]; then
|
||||
"${QDBUSXML2CPP}" -m -p "${COOKBOOK_SOURCE}/src/notifications_interface" \
|
||||
"${COOKBOOK_SOURCE}/src/org.freedesktop.Notifications.xml"
|
||||
sed -i '/notifications_interface\\.moc/d' "${COOKBOOK_SOURCE}/src/notifications_interface.cpp"
|
||||
fi
|
||||
sed -i 's/^ qt_add_dbus_interface(knotifications_dbus_SRCS .*$/ set(knotifications_dbus_SRCS notifications_interface.cpp)/' \
|
||||
"${COOKBOOK_SOURCE}/src/CMakeLists.txt" 2>/dev/null || true
|
||||
sed -i 's/^ target_sources(KF6Notifications PRIVATE ${knotifications_dbus_SRCS})$/ target_sources(KF6Notifications PRIVATE ${knotifications_dbus_SRCS})/' \
|
||||
"${COOKBOOK_SOURCE}/src/CMakeLists.txt" 2>/dev/null || true
|
||||
|
||||
rm -f CMakeCache.txt
|
||||
rm -rf CMakeFiles
|
||||
|
||||
|
||||
@@ -78,52 +78,52 @@ endif()
|
||||
|
||||
find_package(KF6Config ${KF_DEP_VERSION} REQUIRED)
|
||||
|
||||
##################if (NOT APPLE AND NOT ANDROID AND NOT WIN32 AND NOT HAIKU OR (WIN32 AND NOT WITH_SNORETOAST))
|
||||
################## find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED DBus)
|
||||
################## find_package(Canberra REQUIRED)
|
||||
################## set_package_properties(Canberra PROPERTIES
|
||||
################## PURPOSE "Needed to build audio notification support")
|
||||
################## if (TARGET Canberra::Canberra)
|
||||
################## add_definitions(-DHAVE_CANBERRA)
|
||||
################## endif()
|
||||
##################endif()
|
||||
#################
|
||||
################## For the Python bindings
|
||||
#################find_package(Python3 3.10 COMPONENTS Interpreter Development)
|
||||
#################find_package(Shiboken6)
|
||||
#################find_package(PySide6)
|
||||
#################
|
||||
################## Python Bindings
|
||||
#################cmake_dependent_option(BUILD_PYTHON_BINDINGS "Generate Python Bindings" ON "TARGET Shiboken6::libshiboken AND TARGET PySide6::pyside6" OFF)
|
||||
#################add_feature_info(PYTHON_BINDINGS ${BUILD_PYTHON_BINDINGS} "Python bindings")
|
||||
#################
|
||||
################## FreeBSD CI is missing required packages
|
||||
#################if (CMAKE_SYSTEM_NAME MATCHES FreeBSD)
|
||||
################# set(BUILD_PYTHON_BINDINGS OFF)
|
||||
#################endif()
|
||||
################
|
||||
################remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY)
|
||||
################
|
||||
#################ecm_install_po_files_as_qm(poqm)
|
||||
################
|
||||
################ecm_set_disabled_deprecation_versions(
|
||||
################ QT 6.8
|
||||
################ KF 6.8
|
||||
################)
|
||||
################
|
||||
################add_subdirectory(src)
|
||||
################if (BUILD_TESTING)
|
||||
################ add_subdirectory(tests)
|
||||
################ add_subdirectory(autotests)
|
||||
################ add_subdirectory(examples)
|
||||
################endif()
|
||||
###############
|
||||
###############if (BUILD_PYTHON_BINDINGS)
|
||||
############### include(ECMGeneratePythonBindings)
|
||||
############### add_subdirectory(python)
|
||||
###############endif()
|
||||
##############
|
||||
## create a Config.cmake and a ConfigVersion.cmake file and install them
|
||||
###################################if (NOT APPLE AND NOT ANDROID AND NOT WIN32 AND NOT HAIKU OR (WIN32 AND NOT WITH_SNORETOAST))
|
||||
################################### find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED DBus)
|
||||
################################### find_package(Canberra REQUIRED)
|
||||
################################### set_package_properties(Canberra PROPERTIES
|
||||
################################### PURPOSE "Needed to build audio notification support")
|
||||
################################### if (TARGET Canberra::Canberra)
|
||||
################################### add_definitions(-DHAVE_CANBERRA)
|
||||
################################### endif()
|
||||
###################################endif()
|
||||
##################################
|
||||
################################### For the Python bindings
|
||||
##################################find_package(Python3 3.10 COMPONENTS Interpreter Development)
|
||||
##################################find_package(Shiboken6)
|
||||
##################################find_package(PySide6)
|
||||
##################################
|
||||
################################### Python Bindings
|
||||
##################################cmake_dependent_option(BUILD_PYTHON_BINDINGS "Generate Python Bindings" ON "TARGET Shiboken6::libshiboken AND TARGET PySide6::pyside6" OFF)
|
||||
##################################add_feature_info(PYTHON_BINDINGS ${BUILD_PYTHON_BINDINGS} "Python bindings")
|
||||
##################################
|
||||
################################### FreeBSD CI is missing required packages
|
||||
##################################if (CMAKE_SYSTEM_NAME MATCHES FreeBSD)
|
||||
################################## set(BUILD_PYTHON_BINDINGS OFF)
|
||||
##################################endif()
|
||||
#################################
|
||||
#################################remove_definitions(-DQT_NO_CAST_FROM_BYTEARRAY)
|
||||
#################################
|
||||
##################################ecm_install_po_files_as_qm(poqm)
|
||||
#################################
|
||||
#################################ecm_set_disabled_deprecation_versions(
|
||||
################################# QT 6.8
|
||||
################################# KF 6.8
|
||||
#################################)
|
||||
#################################
|
||||
add_subdirectory(src)
|
||||
#################################if (BUILD_TESTING)
|
||||
################################# add_subdirectory(tests)
|
||||
################################# add_subdirectory(autotests)
|
||||
################################# add_subdirectory(examples)
|
||||
#################################endif()
|
||||
################################
|
||||
################################if (BUILD_PYTHON_BINDINGS)
|
||||
################################ include(ECMGeneratePythonBindings)
|
||||
################################ add_subdirectory(python)
|
||||
################################endif()
|
||||
###############################
|
||||
################### create a Config.cmake and a ConfigVersion.cmake file and install them
|
||||
set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6Notifications")
|
||||
|
||||
if (BUILD_QCH)
|
||||
|
||||
@@ -62,7 +62,7 @@ endif()
|
||||
|
||||
if (HAVE_DBUS)
|
||||
set(notifications_xml org.freedesktop.Notifications.xml)
|
||||
qt_add_dbus_interface(knotifications_dbus_SRCS ${notifications_xml} notifications_interface)
|
||||
set(knotifications_dbus_SRCS notifications_interface.cpp)
|
||||
target_sources(KF6Notifications PRIVATE ${knotifications_dbus_SRCS})
|
||||
endif()
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* This file was generated by qdbusxml2cpp version 0.8
|
||||
* Command line was: qdbusxml2cpp -m -p /mnt/data/homes/kellito/Builds/rbos/local/recipes/kde/kf6-knotifications/source/src/notifications_interface /mnt/data/homes/kellito/Builds/rbos/local/recipes/kde/kf6-knotifications/source/src/org.freedesktop.Notifications.xml
|
||||
*
|
||||
* qdbusxml2cpp is Copyright (C) 2023 The Qt Company Ltd.
|
||||
*
|
||||
* This is an auto-generated file.
|
||||
* This file may have been hand-edited. Look for HAND-EDIT comments
|
||||
* before re-generating it.
|
||||
*/
|
||||
|
||||
#include "/mnt/data/homes/kellito/Builds/rbos/local/recipes/kde/kf6-knotifications/source/src/notifications_interface.h"
|
||||
|
||||
/*
|
||||
* Implementation of interface class OrgFreedesktopNotificationsInterface
|
||||
*/
|
||||
|
||||
OrgFreedesktopNotificationsInterface::OrgFreedesktopNotificationsInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
|
||||
: QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
|
||||
{
|
||||
}
|
||||
|
||||
OrgFreedesktopNotificationsInterface::~OrgFreedesktopNotificationsInterface()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* This file was generated by qdbusxml2cpp version 0.8
|
||||
* Command line was: qdbusxml2cpp -m -p /mnt/data/homes/kellito/Builds/rbos/local/recipes/kde/kf6-knotifications/source/src/notifications_interface /mnt/data/homes/kellito/Builds/rbos/local/recipes/kde/kf6-knotifications/source/src/org.freedesktop.Notifications.xml
|
||||
*
|
||||
* qdbusxml2cpp is Copyright (C) 2023 The Qt Company Ltd.
|
||||
*
|
||||
* This is an auto-generated file.
|
||||
* Do not edit! All changes made to it will be lost.
|
||||
*/
|
||||
|
||||
#ifndef NOTIFICATIONS_INTERFACE_H
|
||||
#define NOTIFICATIONS_INTERFACE_H
|
||||
|
||||
#include <QtCore/QObject>
|
||||
#include <QtCore/QByteArray>
|
||||
#include <QtCore/QList>
|
||||
#include <QtCore/QMap>
|
||||
#include <QtCore/QString>
|
||||
#include <QtCore/QStringList>
|
||||
#include <QtCore/QVariant>
|
||||
#include <QtDBus/QtDBus>
|
||||
|
||||
/*
|
||||
* Proxy class for interface org.freedesktop.Notifications
|
||||
*/
|
||||
class OrgFreedesktopNotificationsInterface: public QDBusAbstractInterface
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static inline const char *staticInterfaceName()
|
||||
{ return "org.freedesktop.Notifications"; }
|
||||
|
||||
public:
|
||||
OrgFreedesktopNotificationsInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr);
|
||||
|
||||
~OrgFreedesktopNotificationsInterface();
|
||||
|
||||
Q_PROPERTY(bool Inhibited READ inhibited)
|
||||
inline bool inhibited() const
|
||||
{ return qvariant_cast< bool >(property("Inhibited")); }
|
||||
|
||||
public Q_SLOTS: // METHODS
|
||||
inline QDBusPendingReply<> CloseNotification(uint id)
|
||||
{
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << QVariant::fromValue(id);
|
||||
return asyncCallWithArgumentList(QStringLiteral("CloseNotification"), argumentList);
|
||||
}
|
||||
|
||||
inline QDBusPendingReply<QStringList> GetCapabilities()
|
||||
{
|
||||
QList<QVariant> argumentList;
|
||||
return asyncCallWithArgumentList(QStringLiteral("GetCapabilities"), argumentList);
|
||||
}
|
||||
|
||||
inline QDBusPendingReply<QString, QString, QString, QString> GetServerInformation()
|
||||
{
|
||||
QList<QVariant> argumentList;
|
||||
return asyncCallWithArgumentList(QStringLiteral("GetServerInformation"), argumentList);
|
||||
}
|
||||
inline QDBusReply<QString> GetServerInformation(QString &vendor, QString &version, QString &spec_version)
|
||||
{
|
||||
QList<QVariant> argumentList;
|
||||
QDBusMessage reply = callWithArgumentList(QDBus::Block, QStringLiteral("GetServerInformation"), argumentList);
|
||||
if (reply.type() == QDBusMessage::ReplyMessage && reply.arguments().count() == 4) {
|
||||
vendor = qdbus_cast<QString>(reply.arguments().at(1));
|
||||
version = qdbus_cast<QString>(reply.arguments().at(2));
|
||||
spec_version = qdbus_cast<QString>(reply.arguments().at(3));
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
inline QDBusPendingReply<uint> Inhibit(const QString &desktop_entry, const QString &reason, const QVariantMap &hints)
|
||||
{
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << QVariant::fromValue(desktop_entry) << QVariant::fromValue(reason) << QVariant::fromValue(hints);
|
||||
return asyncCallWithArgumentList(QStringLiteral("Inhibit"), argumentList);
|
||||
}
|
||||
|
||||
inline QDBusPendingReply<uint> Notify(const QString &app_name, uint replaces_id, const QString &app_icon, const QString &summary, const QString &body, const QStringList &actions, const QVariantMap &hints, int timeout)
|
||||
{
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << QVariant::fromValue(app_name) << QVariant::fromValue(replaces_id) << QVariant::fromValue(app_icon) << QVariant::fromValue(summary) << QVariant::fromValue(body) << QVariant::fromValue(actions) << QVariant::fromValue(hints) << QVariant::fromValue(timeout);
|
||||
return asyncCallWithArgumentList(QStringLiteral("Notify"), argumentList);
|
||||
}
|
||||
|
||||
inline QDBusPendingReply<> UnInhibit(uint in0)
|
||||
{
|
||||
QList<QVariant> argumentList;
|
||||
argumentList << QVariant::fromValue(in0);
|
||||
return asyncCallWithArgumentList(QStringLiteral("UnInhibit"), argumentList);
|
||||
}
|
||||
|
||||
Q_SIGNALS: // SIGNALS
|
||||
void ActionInvoked(uint id, const QString &action_key);
|
||||
void ActivationToken(uint id, const QString &activation_token);
|
||||
void NotificationClosed(uint id, uint reason);
|
||||
void NotificationReplied(uint id, const QString &text);
|
||||
};
|
||||
|
||||
namespace org {
|
||||
namespace freedesktop {
|
||||
typedef ::OrgFreedesktopNotificationsInterface Notifications;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -44,6 +44,22 @@ find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
find_package(Qt6GuiPrivate ${REQUIRED_QT_VERSION} REQUIRED)
|
||||
|
||||
if (WITH_TEXT_TO_SPEECH)
|
||||
find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED TextToSpeech)
|
||||
|
||||
@@ -52,6 +52,18 @@ find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
find_package(Qt6WaylandClientPrivate REQUIRED)
|
||||
set_package_properties(Wayland PROPERTIES
|
||||
TYPE REQUIRED
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ PUBLIC
|
||||
PRIVATE
|
||||
#QNetworkAccessManager in kaboutapplicationpersonmodel_p
|
||||
#QPrinter in kshortcutseditor
|
||||
### Qt6::CorePrivate #QSystemLocale in initializeLanguages
|
||||
################ Qt6::CorePrivate #QSystemLocale in initializeLanguages
|
||||
KF6::CoreAddons #KAboutData
|
||||
KF6::GuiAddons
|
||||
KF6::WidgetsAddons
|
||||
|
||||
@@ -75,10 +75,10 @@ void initializeLanguages()
|
||||
// Ideally setting the LANGUAGE would change the default QLocale too
|
||||
// but unfortunately this is too late since the QCoreApplication constructor
|
||||
// already created a QLocale at this stage so we need to set the reset it
|
||||
////// // by triggering the creation and destruction of a QSystemLocale
|
||||
//////////////////////////////// // by triggering the creation and destruction of a QSystemLocale
|
||||
// this is highly dependent on Qt internals, so may break, but oh well
|
||||
////// QSystemLocale *dummy = new QSystemLocale();
|
||||
////// delete dummy;
|
||||
//////////////////////////////// QSystemLocale *dummy = new QSystemLocale();
|
||||
//////////////////////////////// delete dummy;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ set_package_properties(PList PROPERTIES
|
||||
|
||||
if (CMAKE_SYSTEM_NAME MATCHES Linux)
|
||||
# Used by the UDisks backend on Linux
|
||||
###find_package(LibMount)
|
||||
################find_package(LibMount)
|
||||
set_package_properties(LibMount PROPERTIES
|
||||
TYPE REQUIRED)
|
||||
endif()
|
||||
|
||||
@@ -45,6 +45,8 @@ function(kwin_add_script name source)
|
||||
file(COPY ${source}/contents ${source}/metadata.json DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/scripts/${name})
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(qpa)
|
||||
|
||||
add_subdirectory(idletime)
|
||||
|
||||
if (KWIN_BUILD_EFFECTS)
|
||||
|
||||
@@ -133,9 +133,9 @@ if [ -f "${COOKBOOK_ROOT}/recipes/core/relibc/target/${TARGET}/stage.tmp/usr/inc
|
||||
"${COOKBOOK_ROOT}/recipes/wip/qt/qtdeclarative/target/${TARGET}/sysroot/include/stdlib.h"
|
||||
cp -f "${COOKBOOK_ROOT}/recipes/core/relibc/target/${TARGET}/stage.tmp/usr/include/stdlib.h" \
|
||||
"${COOKBOOK_ROOT}/recipes/wip/qt/qtdeclarative/target/${TARGET}/sysroot/usr/include/stdlib.h" 2>/dev/null || true
|
||||
sed -i '/^long double strtold(const char \\*nptr, char \\*\\*endptr);$/d' \
|
||||
sed -i '/strtold[[:space:]]*(/d' \
|
||||
"${COOKBOOK_ROOT}/recipes/wip/qt/qtdeclarative/target/${TARGET}/sysroot/include/stdlib.h" 2>/dev/null || true
|
||||
sed -i '/^long double strtold(const char \\*nptr, char \\*\\*endptr);$/d' \
|
||||
sed -i '/strtold[[:space:]]*(/d' \
|
||||
"${COOKBOOK_ROOT}/recipes/wip/qt/qtdeclarative/target/${TARGET}/sysroot/usr/include/stdlib.h" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
@@ -159,7 +159,7 @@ fi
|
||||
|
||||
mkdir -p "${COOKBOOK_SYSROOT}/include/QtGui/private" "${COOKBOOK_SYSROOT}/include/QtCore/private"
|
||||
mkdir -p "${COOKBOOK_SYSROOT}/usr/include/QtGui/private" "${COOKBOOK_SYSROOT}/usr/include/QtCore/private"
|
||||
for hdr in "${COOKBOOK_ROOT}"/recipes/wip/qt/qtbase/source/src/corelib/global/*_p.h; do
|
||||
find "${COOKBOOK_ROOT}/recipes/wip/qt/qtbase/source/src/corelib" -name '*_p.h' | while read -r hdr; do
|
||||
[ -f "$hdr" ] || continue
|
||||
base=$(basename "$hdr")
|
||||
ln -sf "$hdr" "${COOKBOOK_SYSROOT}/include/QtCore/private/$base"
|
||||
@@ -170,7 +170,17 @@ if [ -f "${qt_qconfig_priv}" ]; then
|
||||
ln -sf "${qt_qconfig_priv}" "${COOKBOOK_SYSROOT}/include/QtCore/private/qconfig_p.h"
|
||||
ln -sf "${qt_qconfig_priv}" "${COOKBOOK_SYSROOT}/usr/include/QtCore/private/qconfig_p.h"
|
||||
fi
|
||||
for hdr in "${COOKBOOK_ROOT}"/recipes/wip/qt/qtbase/source/src/gui/kernel/*_p.h "${COOKBOOK_ROOT}"/recipes/wip/qt/qtbase/source/src/gui/platform/unix/*_p.h; do
|
||||
qt_qtcore_config_priv="${COOKBOOK_ROOT}/recipes/wip/qt/qtdeclarative/target/${TARGET}/sysroot/usr/include/QtCore/6.11.0/QtCore/private/qtcore-config_p.h"
|
||||
if [ -f "${qt_qtcore_config_priv}" ]; then
|
||||
ln -sf "${qt_qtcore_config_priv}" "${COOKBOOK_SYSROOT}/include/QtCore/private/qtcore-config_p.h"
|
||||
ln -sf "${qt_qtcore_config_priv}" "${COOKBOOK_SYSROOT}/usr/include/QtCore/private/qtcore-config_p.h"
|
||||
fi
|
||||
qt_qtgui_config_priv="${COOKBOOK_ROOT}/recipes/wip/qt/qtdeclarative/target/${TARGET}/sysroot/usr/include/QtGui/6.11.0/QtGui/private/qtgui-config_p.h"
|
||||
if [ -f "${qt_qtgui_config_priv}" ]; then
|
||||
ln -sf "${qt_qtgui_config_priv}" "${COOKBOOK_SYSROOT}/include/QtGui/private/qtgui-config_p.h"
|
||||
ln -sf "${qt_qtgui_config_priv}" "${COOKBOOK_SYSROOT}/usr/include/QtGui/private/qtgui-config_p.h"
|
||||
fi
|
||||
find "${COOKBOOK_ROOT}/recipes/wip/qt/qtbase/source/src/gui" -name '*_p.h' | while read -r hdr; do
|
||||
[ -f "$hdr" ] || continue
|
||||
base=$(basename "$hdr")
|
||||
ln -sf "$hdr" "${COOKBOOK_SYSROOT}/include/QtGui/private/$base"
|
||||
@@ -394,5 +404,16 @@ PY
|
||||
cmake --build . -j"${COOKBOOK_MAKE_JOBS}"
|
||||
cmake --install . --prefix "${COOKBOOK_STAGE}/usr"
|
||||
|
||||
find "${COOKBOOK_STAGE}" -name '*.so*' -exec patchelf --remove-rpath {} ";" 2>/dev/null || true
|
||||
find "${COOKBOOK_STAGE}/usr/lib" -name '*.so*' -exec patchelf --remove-rpath {} ";" 2>/dev/null || true
|
||||
find "${COOKBOOK_STAGE}/usr/plugins" -name '*.so' -exec patchelf --set-rpath '$ORIGIN/../../lib' {} + 2>/dev/null || true
|
||||
for bin in "${COOKBOOK_STAGE}/usr/bin/kwin_wayland" "${COOKBOOK_STAGE}/usr/bin/kwin_wayland_wrapper"; do
|
||||
[ -f "${bin}" ] || continue
|
||||
patchelf --set-rpath '$ORIGIN/../lib' "${bin}" 2>/dev/null || true
|
||||
done
|
||||
"""
|
||||
|
||||
[package]
|
||||
dependencies = [
|
||||
"fontconfig",
|
||||
"freetype2",
|
||||
]
|
||||
|
||||
@@ -6,7 +6,45 @@
|
||||
#include "syncobjtimeline.h"
|
||||
|
||||
#include <cerrno>
|
||||
#ifdef __redox__
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef EFD_CLOEXEC
|
||||
#define EFD_CLOEXEC O_CLOEXEC
|
||||
#endif
|
||||
|
||||
#ifndef EFD_NONBLOCK
|
||||
#define EFD_NONBLOCK O_NONBLOCK
|
||||
#endif
|
||||
|
||||
#ifndef EFD_SEMAPHORE
|
||||
#define EFD_SEMAPHORE 0x1
|
||||
#endif
|
||||
|
||||
static int eventfd(unsigned int initval, int flags)
|
||||
{
|
||||
const int supported = EFD_CLOEXEC | EFD_NONBLOCK | EFD_SEMAPHORE;
|
||||
int oflag = O_RDWR;
|
||||
char path[64];
|
||||
|
||||
if ((flags & ~supported) != 0) {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
if (flags & EFD_CLOEXEC) {
|
||||
oflag |= O_CLOEXEC;
|
||||
}
|
||||
if (flags & EFD_NONBLOCK) {
|
||||
oflag |= O_NONBLOCK;
|
||||
}
|
||||
|
||||
snprintf(path, sizeof(path), "/scheme/event/eventfd/%u/%d", initval, (flags & EFD_SEMAPHORE) ? 1 : 0);
|
||||
return open(path, oflag);
|
||||
}
|
||||
#else
|
||||
#include <sys/eventfd.h>
|
||||
#endif
|
||||
#include <sys/ioctl.h>
|
||||
#include <xf86drm.h>
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ function(kwin_add_script name source)
|
||||
file(COPY ${source}/contents ${source}/metadata.json DESTINATION ${CMAKE_BINARY_DIR}/bin/kwin/scripts/${name})
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(qpa)
|
||||
|
||||
add_subdirectory(idletime)
|
||||
|
||||
if (KWIN_BUILD_EFFECTS)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
add_library(KWinQpaPlugin OBJECT)
|
||||
target_sources(KWinQpaPlugin PRIVATE
|
||||
add_library(KWinQpaPlugin MODULE
|
||||
backingstore.cpp
|
||||
clipboard.cpp
|
||||
eglhelpers.cpp
|
||||
@@ -13,6 +12,13 @@ target_sources(KWinQpaPlugin PRIVATE
|
||||
window.cpp
|
||||
)
|
||||
|
||||
set_target_properties(KWinQpaPlugin PROPERTIES
|
||||
AUTOMOC ON
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins/platforms"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins/platforms"
|
||||
OUTPUT_NAME "qwayland-org.kde.kwin.qpa"
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(KWinQpaPlugin
|
||||
HEADER logging.h
|
||||
IDENTIFIER KWIN_QPA
|
||||
@@ -20,8 +26,6 @@ ecm_qt_declare_logging_category(KWinQpaPlugin
|
||||
DEFAULT_SEVERITY Critical
|
||||
)
|
||||
|
||||
target_compile_definitions(KWinQpaPlugin PRIVATE QT_STATICPLUGIN)
|
||||
|
||||
target_link_libraries(KWinQpaPlugin PRIVATE
|
||||
Qt::Concurrent
|
||||
Qt::CorePrivate
|
||||
@@ -30,3 +34,5 @@ target_link_libraries(KWinQpaPlugin PRIVATE
|
||||
Fontconfig::Fontconfig
|
||||
kwin
|
||||
)
|
||||
|
||||
install(TARGETS KWinQpaPlugin DESTINATION plugins/platforms)
|
||||
|
||||
@@ -137,7 +137,7 @@ QPlatformWindow *Integration::createPlatformWindow(QWindow *window) const
|
||||
|
||||
QPlatformOffscreenSurface *Integration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const
|
||||
{
|
||||
return new OffscreenSurface(surface);
|
||||
return new KWin::QPA::OffscreenSurface(surface);
|
||||
}
|
||||
|
||||
QPlatformFontDatabase *Integration::fontDatabase() const
|
||||
|
||||
@@ -101,6 +101,23 @@ set(CMAKE_PREFIX_PATH "${COOKBOOK_SYSROOT}")
|
||||
set(CMAKE_LIBRARY_PATH "${COOKBOOK_SYSROOT}/lib")
|
||||
set(CMAKE_INCLUDE_PATH "${COOKBOOK_SYSROOT}/include")
|
||||
|
||||
if(DEFINED ENV{COOKBOOK_SYSROOT} AND EXISTS "$ENV{COOKBOOK_SYSROOT}/lib")
|
||||
set(_redbear_sysroot_link_flags "-L$ENV{COOKBOOK_SYSROOT}/lib -Wl,-rpath-link,$ENV{COOKBOOK_SYSROOT}/lib")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_INIT "${CMAKE_EXE_LINKER_FLAGS_INIT} ${_redbear_sysroot_link_flags}")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_INIT "${CMAKE_SHARED_LINKER_FLAGS_INIT} ${_redbear_sysroot_link_flags}")
|
||||
set(CMAKE_MODULE_LINKER_FLAGS_INIT "${CMAKE_MODULE_LINKER_FLAGS_INIT} ${_redbear_sysroot_link_flags}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${_redbear_sysroot_link_flags}" CACHE STRING "" FORCE)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${_redbear_sysroot_link_flags}" CACHE STRING "" FORCE)
|
||||
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} ${_redbear_sysroot_link_flags}" CACHE STRING "" FORCE)
|
||||
endif()
|
||||
|
||||
if(DEFINED ENV{COOKBOOK_SYSROOT} AND EXISTS "$ENV{COOKBOOK_SYSROOT}/lib/libredbear-qt-strtold-compat.so")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--no-as-needed -L$ENV{COOKBOOK_SYSROOT}/lib -lredbear-qt-strtold-compat" CACHE STRING "" FORCE)
|
||||
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-as-needed -L$ENV{COOKBOOK_SYSROOT}/lib -lredbear-qt-strtold-compat" CACHE STRING "" FORCE)
|
||||
set(CMAKE_C_STANDARD_LIBRARIES_INIT "${CMAKE_C_STANDARD_LIBRARIES_INIT} -Wl,--no-as-needed -L$ENV{COOKBOOK_SYSROOT}/lib -lredbear-qt-strtold-compat")
|
||||
set(CMAKE_CXX_STANDARD_LIBRARIES_INIT "${CMAKE_CXX_STANDARD_LIBRARIES_INIT} -Wl,--no-as-needed -L$ENV{COOKBOOK_SYSROOT}/lib -lredbear-qt-strtold-compat")
|
||||
endif()
|
||||
|
||||
# Install prefix — matches the cookbook convention (see cookbook_cmake in script.rs)
|
||||
set(CMAKE_INSTALL_PREFIX "/usr")
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ struct DiscoveryResult {
|
||||
source: DiscoverySource,
|
||||
kernel_acpi_status: &'static str,
|
||||
ivrs_path: Option<PathBuf>,
|
||||
dmar_present: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "redox"), allow(dead_code))]
|
||||
@@ -141,7 +142,7 @@ fn read_sdt_from_physical(phys_addr: u64) -> Result<Vec<u8>, String> {
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
|
||||
fn find_kernel_acpi_table(signature: &[u8; 4]) -> Result<Option<Vec<u8>>, String> {
|
||||
let rxsdt = match fs::read("/scheme/kernel.acpi/rxsdt") {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => {
|
||||
@@ -150,14 +151,14 @@ fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
|
||||
};
|
||||
|
||||
if rxsdt.len() < ACPI_HEADER_LEN {
|
||||
return Ok(Vec::new());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let signature = &rxsdt[0..4];
|
||||
let entry_size = match signature {
|
||||
let root_signature = &rxsdt[0..4];
|
||||
let entry_size = match root_signature {
|
||||
b"RSDT" => 4,
|
||||
b"XSDT" => 8,
|
||||
_ => return Ok(Vec::new()),
|
||||
_ => return Ok(None),
|
||||
};
|
||||
|
||||
let mut offset = ACPI_HEADER_LEN;
|
||||
@@ -169,24 +170,46 @@ fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
|
||||
};
|
||||
|
||||
let table = read_sdt_from_physical(phys_addr)?;
|
||||
if table.len() >= 4 && &table[0..4] == b"IVRS" {
|
||||
return AmdViUnit::detect(&table).map_err(|err| format!("failed to parse IVRS: {err}"));
|
||||
if table.len() >= 4 && &table[0..4] == signature {
|
||||
return Ok(Some(table));
|
||||
}
|
||||
|
||||
offset += entry_size;
|
||||
}
|
||||
|
||||
Ok(Vec::new())
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn detect_units_from_kernel_acpi() -> Result<Vec<AmdViUnit>, String> {
|
||||
match find_kernel_acpi_table(b"IVRS")? {
|
||||
Some(table) => AmdViUnit::detect(&table).map_err(|err| format!("failed to parse IVRS: {err}")),
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn detect_dmar_from_kernel_acpi() -> Result<bool, String> {
|
||||
Ok(find_kernel_acpi_table(b"DMAR")?.is_some())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn discover_units() -> Result<DiscoveryResult, String> {
|
||||
let dmar_present = match detect_dmar_from_kernel_acpi() {
|
||||
Ok(present) => present,
|
||||
Err(err) => {
|
||||
info!("iommu: kernel ACPI DMAR discovery unavailable: {err}");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
match detect_units_from_kernel_acpi() {
|
||||
Ok(units) if !units.is_empty() => Ok(DiscoveryResult {
|
||||
units,
|
||||
source: DiscoverySource::KernelAcpi,
|
||||
kernel_acpi_status: "ok",
|
||||
ivrs_path: None,
|
||||
dmar_present,
|
||||
}),
|
||||
Ok(_units) => {
|
||||
let (units, ivrs_path) = detect_units_from_discovered_ivrs()?;
|
||||
@@ -199,6 +222,7 @@ fn discover_units() -> Result<DiscoveryResult, String> {
|
||||
units,
|
||||
kernel_acpi_status: "empty",
|
||||
ivrs_path,
|
||||
dmar_present,
|
||||
})
|
||||
}
|
||||
Err(err) => {
|
||||
@@ -213,6 +237,7 @@ fn discover_units() -> Result<DiscoveryResult, String> {
|
||||
units,
|
||||
kernel_acpi_status: "error",
|
||||
ivrs_path,
|
||||
dmar_present,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -230,6 +255,7 @@ fn discover_units() -> Result<DiscoveryResult, String> {
|
||||
units,
|
||||
kernel_acpi_status: "unsupported",
|
||||
ivrs_path,
|
||||
dmar_present: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -254,6 +280,11 @@ fn run() -> Result<(), String> {
|
||||
discovery.source.as_str()
|
||||
);
|
||||
}
|
||||
if discovery.dmar_present {
|
||||
info!(
|
||||
"iommu: detected kernel ACPI DMAR table; Intel VT-d runtime ownership should converge here rather than remain in acpid"
|
||||
);
|
||||
}
|
||||
for (index, unit) in discovery.units.iter().enumerate() {
|
||||
info!(
|
||||
"iommu: discovered unit {} at MMIO {:#x}; initialization is deferred until first use",
|
||||
@@ -308,6 +339,7 @@ fn run_self_test() -> Result<(), String> {
|
||||
|
||||
println!("discovery_source={}", discovery.source.as_str());
|
||||
println!("kernel_acpi_status={}", discovery.kernel_acpi_status);
|
||||
println!("dmar_present={}", if discovery.dmar_present { 1 } else { 0 });
|
||||
println!(
|
||||
"ivrs_path={}",
|
||||
discovery
|
||||
@@ -430,4 +462,10 @@ mod tests {
|
||||
assert_eq!(DiscoverySource::Filesystem.as_str(), "filesystem");
|
||||
assert_eq!(DiscoverySource::None.as_str(), "none");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_discovery_defaults_to_no_dmar() {
|
||||
let discovery = super::discover_units().expect("host discovery should succeed");
|
||||
assert!(!discovery.dmar_present);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
|
||||
[package.files]
|
||||
"/usr/bin/redbear-authd" = "redbear-authd"
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "redbear-authd"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-authd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
# Pure-Rust SHA-256/SHA-512 crypt verifier for /etc/shadow entries.
|
||||
# Free/open-source (`MIT OR Apache-2.0` upstream; acceptable under the project's free-software policy).
|
||||
sha-crypt = "0.6.0-rc.4"
|
||||
@@ -0,0 +1,741 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env,
|
||||
fs,
|
||||
io::{BufRead, BufReader, Write},
|
||||
os::unix::{fs::PermissionsExt, net::{UnixListener, UnixStream}},
|
||||
path::Path,
|
||||
process::{self, Command},
|
||||
sync::{Arc, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha_crypt::{PasswordVerifier, ShaCrypt};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum VerifyError {
|
||||
UnsupportedHashFormat,
|
||||
}
|
||||
|
||||
const AUTH_SOCKET_PATH: &str = "/run/redbear-authd.sock";
|
||||
const SESSIOND_SOCKET_PATH: &str = "/run/redbear-sessiond-control.sock";
|
||||
const FAILURE_WINDOW: Duration = Duration::from_secs(60);
|
||||
const LOCKOUT_DURATION: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Account {
|
||||
username: String,
|
||||
password: String,
|
||||
uid: u32,
|
||||
shell: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct Approval {
|
||||
expires_at: Instant,
|
||||
vt: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct FailureState {
|
||||
attempts: Vec<Instant>,
|
||||
locked_until: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct RuntimeState {
|
||||
approvals: Arc<Mutex<HashMap<String, Approval>>>,
|
||||
failures: Arc<Mutex<HashMap<String, FailureState>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum AuthRequest {
|
||||
Authenticate {
|
||||
request_id: u64,
|
||||
username: String,
|
||||
password: String,
|
||||
vt: u32,
|
||||
},
|
||||
StartSession {
|
||||
request_id: u64,
|
||||
username: String,
|
||||
session: String,
|
||||
vt: u32,
|
||||
},
|
||||
PowerAction {
|
||||
request_id: u64,
|
||||
action: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum AuthResponse {
|
||||
AuthenticateResult {
|
||||
request_id: u64,
|
||||
ok: bool,
|
||||
message: String,
|
||||
},
|
||||
SessionResult {
|
||||
request_id: u64,
|
||||
ok: bool,
|
||||
exit_code: Option<i32>,
|
||||
message: String,
|
||||
},
|
||||
PowerResult {
|
||||
request_id: u64,
|
||||
ok: bool,
|
||||
message: String,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum SessiondUpdate {
|
||||
SetSession {
|
||||
username: String,
|
||||
uid: u32,
|
||||
vt: u32,
|
||||
leader: u32,
|
||||
state: String,
|
||||
},
|
||||
ResetSession {
|
||||
vt: u32,
|
||||
},
|
||||
}
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"Usage: redbear-authd [--help]"
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<(), String> {
|
||||
let mut args = env::args().skip(1);
|
||||
match args.next() {
|
||||
None => Ok(()),
|
||||
Some(arg) if arg == "--help" || arg == "-h" => Err(String::new()),
|
||||
Some(arg) => Err(format!("unrecognized argument '{arg}'")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AccountFormat {
|
||||
Redox,
|
||||
Unix,
|
||||
}
|
||||
|
||||
fn split_account_fields(line: &str) -> (AccountFormat, Vec<String>) {
|
||||
let format = if line.contains(';') {
|
||||
AccountFormat::Redox
|
||||
} else {
|
||||
AccountFormat::Unix
|
||||
};
|
||||
let delimiter = match format {
|
||||
AccountFormat::Redox => ';',
|
||||
AccountFormat::Unix => ':',
|
||||
};
|
||||
(format, line.split(delimiter).map(str::to_string).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
fn load_shadow_passwords() -> Result<HashMap<String, String>, String> {
|
||||
if !Path::new("/etc/shadow").exists() {
|
||||
return Ok(HashMap::new());
|
||||
}
|
||||
|
||||
let mut passwords = HashMap::new();
|
||||
let contents = fs::read_to_string("/etc/shadow")
|
||||
.map_err(|err| format!("failed to read /etc/shadow: {err}"))?;
|
||||
for (index, raw_line) in contents.lines().enumerate() {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (_format, parts) = split_account_fields(line);
|
||||
if parts.len() < 2 {
|
||||
return Err(format!("invalid shadow entry on line {}", index + 1));
|
||||
}
|
||||
passwords.insert(parts[0].clone(), parts[1].clone());
|
||||
}
|
||||
Ok(passwords)
|
||||
}
|
||||
|
||||
fn load_account(username: &str) -> Result<Account, String> {
|
||||
let shadow_passwords = load_shadow_passwords()?;
|
||||
let contents = fs::read_to_string("/etc/passwd")
|
||||
.map_err(|err| format!("failed to read /etc/passwd: {err}"))?;
|
||||
for (index, raw_line) in contents.lines().enumerate() {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (format, parts) = split_account_fields(line);
|
||||
if parts[0] != username {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (uid_index, gid_index, shell_index, passwd_index) = match format {
|
||||
AccountFormat::Redox if parts.len() >= 6 => (1, 2, 5, None),
|
||||
AccountFormat::Unix if parts.len() >= 7 => (2, 3, 6, Some(1)),
|
||||
AccountFormat::Redox => {
|
||||
return Err(format!("invalid Redox passwd entry for user '{username}' on line {}", index + 1))
|
||||
}
|
||||
AccountFormat::Unix => {
|
||||
return Err(format!("invalid passwd entry for user '{username}' on line {}", index + 1))
|
||||
}
|
||||
};
|
||||
|
||||
let uid = parts[uid_index]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| format!("invalid uid for user '{username}'"))?;
|
||||
let _gid = parts[gid_index]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| format!("invalid gid for user '{username}'"))?;
|
||||
let password = shadow_passwords
|
||||
.get(username)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| passwd_index.map(|index| parts[index].clone()).unwrap_or_default());
|
||||
|
||||
return Ok(Account {
|
||||
username: parts[0].clone(),
|
||||
password,
|
||||
uid,
|
||||
shell: parts[shell_index].clone(),
|
||||
});
|
||||
}
|
||||
|
||||
Err(format!("unknown user '{username}'"))
|
||||
}
|
||||
|
||||
fn trim_failures(entries: &mut Vec<Instant>, now: Instant) {
|
||||
entries.retain(|entry| now.saturating_duration_since(*entry) <= FAILURE_WINDOW);
|
||||
}
|
||||
|
||||
fn login_allowed(account: &Account) -> bool {
|
||||
if account.uid != 0 && account.uid < 1000 {
|
||||
return false;
|
||||
}
|
||||
!account.shell.is_empty()
|
||||
}
|
||||
|
||||
fn verify_shadow_password(password: &str, shadow_hash: &str) -> Result<bool, VerifyError> {
|
||||
if shadow_hash.starts_with("$6$") || shadow_hash.starts_with("$5$") {
|
||||
return Ok(ShaCrypt::default()
|
||||
.verify_password(password.as_bytes(), shadow_hash)
|
||||
.is_ok());
|
||||
}
|
||||
Err(VerifyError::UnsupportedHashFormat)
|
||||
}
|
||||
|
||||
fn verify_password(account: &Account, password: &str) -> bool {
|
||||
if account.password.is_empty() || account.password.starts_with('!') || account.password.starts_with('*') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if account.password.starts_with('$') {
|
||||
match verify_shadow_password(password, &account.password) {
|
||||
Ok(ok) => return ok,
|
||||
Err(VerifyError::UnsupportedHashFormat) => {
|
||||
eprintln!(
|
||||
"redbear-authd: password hash for user {} uses an unsupported shadow format",
|
||||
account.username
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
account.password == password
|
||||
}
|
||||
|
||||
fn remember_success(state: &RuntimeState, username: &str, vt: u32) -> Result<(), String> {
|
||||
let mut approvals = state
|
||||
.approvals
|
||||
.lock()
|
||||
.map_err(|_| String::from("approval state is poisoned"))?;
|
||||
approvals.insert(
|
||||
username.to_string(),
|
||||
Approval {
|
||||
expires_at: Instant::now() + Duration::from_secs(15),
|
||||
vt,
|
||||
},
|
||||
);
|
||||
|
||||
let mut failures = state
|
||||
.failures
|
||||
.lock()
|
||||
.map_err(|_| String::from("failure state is poisoned"))?;
|
||||
failures.remove(username);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remember_failure(state: &RuntimeState, username: &str) -> Result<String, String> {
|
||||
let mut failures = state
|
||||
.failures
|
||||
.lock()
|
||||
.map_err(|_| String::from("failure state is poisoned"))?;
|
||||
let now = Instant::now();
|
||||
let entry = failures.entry(username.to_string()).or_default();
|
||||
trim_failures(&mut entry.attempts, now);
|
||||
entry.attempts.push(now);
|
||||
if entry.attempts.len() >= 5 {
|
||||
entry.locked_until = Some(now + LOCKOUT_DURATION);
|
||||
Ok(String::from("Too many failed attempts. Try again shortly."))
|
||||
} else {
|
||||
Ok(String::from("Invalid username or password."))
|
||||
}
|
||||
}
|
||||
|
||||
fn check_lockout(state: &RuntimeState, username: &str) -> Result<Option<String>, String> {
|
||||
let mut failures = state
|
||||
.failures
|
||||
.lock()
|
||||
.map_err(|_| String::from("failure state is poisoned"))?;
|
||||
let now = Instant::now();
|
||||
if let Some(entry) = failures.get_mut(username) {
|
||||
trim_failures(&mut entry.attempts, now);
|
||||
if let Some(locked_until) = entry.locked_until {
|
||||
if locked_until > now {
|
||||
return Ok(Some(String::from("Too many failed attempts. Try again shortly.")));
|
||||
}
|
||||
entry.locked_until = None;
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn take_approval(state: &RuntimeState, username: &str, vt: u32) -> Result<(), String> {
|
||||
let mut approvals = state
|
||||
.approvals
|
||||
.lock()
|
||||
.map_err(|_| String::from("approval state is poisoned"))?;
|
||||
let Some(approval) = approvals.remove(username) else {
|
||||
return Err(String::from("No recent authentication approval exists for this user."));
|
||||
};
|
||||
if approval.expires_at < Instant::now() {
|
||||
return Err(String::from("Authentication approval expired. Please log in again."));
|
||||
}
|
||||
if approval.vt != vt {
|
||||
return Err(String::from("Authentication approval does not match the requested VT."));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_sessiond_update(message: &SessiondUpdate) {
|
||||
let Ok(mut stream) = UnixStream::connect(SESSIOND_SOCKET_PATH) else {
|
||||
return;
|
||||
};
|
||||
let Ok(json) = serde_json::to_string(message) else {
|
||||
return;
|
||||
};
|
||||
let _ = stream.write_all(json.as_bytes());
|
||||
let _ = stream.write_all(b"\n");
|
||||
}
|
||||
|
||||
fn launch_session(account: &Account, session: &str, vt: u32) -> Result<Option<i32>, String> {
|
||||
if session != "kde-wayland" {
|
||||
return Err(format!("unsupported session '{session}'"));
|
||||
}
|
||||
|
||||
let mut child = Command::new("/usr/bin/redbear-session-launch")
|
||||
.arg("--username")
|
||||
.arg(&account.username)
|
||||
.arg("--mode")
|
||||
.arg("session")
|
||||
.arg("--session")
|
||||
.arg(session)
|
||||
.arg("--vt")
|
||||
.arg(vt.to_string())
|
||||
.spawn()
|
||||
.map_err(|err| format!("failed to launch session for {}: {err}", account.username))?;
|
||||
|
||||
send_sessiond_update(&SessiondUpdate::SetSession {
|
||||
username: account.username.clone(),
|
||||
uid: account.uid,
|
||||
vt,
|
||||
leader: child.id(),
|
||||
state: String::from("online"),
|
||||
});
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.map_err(|err| format!("failed while waiting for session process: {err}"))?;
|
||||
|
||||
send_sessiond_update(&SessiondUpdate::ResetSession { vt });
|
||||
Ok(status.code())
|
||||
}
|
||||
|
||||
fn run_power_action(action: &str) -> Result<String, String> {
|
||||
let candidates: &[&[&str]] = match action {
|
||||
"shutdown" => &[&["/usr/bin/shutdown"], &["shutdown"], &["poweroff"]],
|
||||
"reboot" => &[&["/usr/bin/reboot"], &["reboot"]],
|
||||
other => return Err(format!("unsupported power action '{other}'")),
|
||||
};
|
||||
|
||||
for candidate in candidates {
|
||||
let program = candidate[0];
|
||||
let args = &candidate[1..];
|
||||
let Ok(status) = Command::new(program).args(args).status() else {
|
||||
continue;
|
||||
};
|
||||
if status.success() {
|
||||
return Ok(format!("{action} requested"));
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("failed to execute {action} command"))
|
||||
}
|
||||
|
||||
fn handle_request(request: AuthRequest, state: &RuntimeState) -> AuthResponse {
|
||||
match request {
|
||||
AuthRequest::Authenticate {
|
||||
request_id,
|
||||
username,
|
||||
password,
|
||||
vt,
|
||||
} => {
|
||||
match check_lockout(state, &username) {
|
||||
Ok(Some(message)) => {
|
||||
return AuthResponse::AuthenticateResult {
|
||||
request_id,
|
||||
ok: false,
|
||||
message,
|
||||
};
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(message) => return AuthResponse::Error { message },
|
||||
}
|
||||
|
||||
match load_account(&username) {
|
||||
Ok(account) if login_allowed(&account) && verify_password(&account, &password) => {
|
||||
if let Err(message) = remember_success(state, &username, vt) {
|
||||
return AuthResponse::Error { message };
|
||||
}
|
||||
AuthResponse::AuthenticateResult {
|
||||
request_id,
|
||||
ok: true,
|
||||
message: String::from("Authentication successful."),
|
||||
}
|
||||
}
|
||||
Ok(_) | Err(_) => {
|
||||
let message = remember_failure(state, &username)
|
||||
.unwrap_or_else(|_| String::from("Invalid username or password."));
|
||||
AuthResponse::AuthenticateResult {
|
||||
request_id,
|
||||
ok: false,
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AuthRequest::StartSession {
|
||||
request_id,
|
||||
username,
|
||||
session,
|
||||
vt,
|
||||
} => {
|
||||
if let Err(message) = take_approval(state, &username, vt) {
|
||||
return AuthResponse::SessionResult {
|
||||
request_id,
|
||||
ok: false,
|
||||
exit_code: None,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
match load_account(&username).and_then(|account| {
|
||||
let exit_code = launch_session(&account, &session, vt)?;
|
||||
Ok((account, exit_code))
|
||||
}) {
|
||||
Ok((_account, exit_code)) => AuthResponse::SessionResult {
|
||||
request_id,
|
||||
ok: true,
|
||||
exit_code,
|
||||
message: String::from("Session completed."),
|
||||
},
|
||||
Err(message) => AuthResponse::SessionResult {
|
||||
request_id,
|
||||
ok: false,
|
||||
exit_code: None,
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
AuthRequest::PowerAction { request_id, action } => match run_power_action(&action) {
|
||||
Ok(message) => AuthResponse::PowerResult {
|
||||
request_id,
|
||||
ok: true,
|
||||
message,
|
||||
},
|
||||
Err(message) => AuthResponse::PowerResult {
|
||||
request_id,
|
||||
ok: false,
|
||||
message,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_connection(stream: UnixStream, state: RuntimeState) {
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
if reader.read_line(&mut line).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let response = match serde_json::from_str::<AuthRequest>(line.trim()) {
|
||||
Ok(request) => handle_request(request, &state),
|
||||
Err(err) => AuthResponse::Error {
|
||||
message: format!("invalid request: {err}"),
|
||||
},
|
||||
};
|
||||
|
||||
let Ok(payload) = serde_json::to_string(&response) else {
|
||||
return;
|
||||
};
|
||||
let mut stream = reader.into_inner();
|
||||
let _ = stream.write_all(payload.as_bytes());
|
||||
let _ = stream.write_all(b"\n");
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
match parse_args() {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.is_empty() => {
|
||||
println!("{}", usage());
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
if Path::new(AUTH_SOCKET_PATH).exists() {
|
||||
fs::remove_file(AUTH_SOCKET_PATH)
|
||||
.map_err(|err| format!("failed to remove stale auth socket {AUTH_SOCKET_PATH}: {err}"))?;
|
||||
}
|
||||
|
||||
let listener = UnixListener::bind(AUTH_SOCKET_PATH)
|
||||
.map_err(|err| format!("failed to bind auth socket {AUTH_SOCKET_PATH}: {err}"))?;
|
||||
fs::set_permissions(AUTH_SOCKET_PATH, fs::Permissions::from_mode(0o600))
|
||||
.map_err(|err| format!("failed to set permissions on {AUTH_SOCKET_PATH}: {err}"))?;
|
||||
let state = RuntimeState::default();
|
||||
|
||||
eprintln!("redbear-authd: listening on {AUTH_SOCKET_PATH}");
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => handle_connection(stream, state.clone()),
|
||||
Err(err) => eprintln!("redbear-authd: failed to accept connection: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("redbear-authd: {err}");
|
||||
eprintln!("{}", usage());
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
|
||||
fn send_handle_connection_request(request: &str) -> AuthResponse {
|
||||
let state = RuntimeState::default();
|
||||
let (mut client, server) = UnixStream::pair().expect("socket pair should open");
|
||||
client
|
||||
.write_all(request.as_bytes())
|
||||
.and_then(|_| client.write_all(b"\n"))
|
||||
.expect("request should write");
|
||||
handle_connection(server, state);
|
||||
let mut line = String::new();
|
||||
BufReader::new(client)
|
||||
.read_line(&mut line)
|
||||
.expect("response should read");
|
||||
serde_json::from_str(line.trim()).expect("response should parse")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_password_accepts_plain_passwords() {
|
||||
let account = Account {
|
||||
username: String::from("root"),
|
||||
password: String::from("password"),
|
||||
uid: 0,
|
||||
shell: String::from("/usr/bin/ion"),
|
||||
};
|
||||
assert!(verify_password(&account, "password"));
|
||||
assert!(!verify_password(&account, "wrong"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_shadow_password_accepts_sha512_crypt() {
|
||||
let hash = "$6$saltstring$adDbXsJjcDlq2662QPgd.tkSOVmnG9Tt3oXl4HR60SusC3AGjirnDenVZp3DGwLwqy6iYKCzannhaX9DR72nN1";
|
||||
assert_eq!(verify_shadow_password("password", hash), Ok(true));
|
||||
assert_eq!(verify_shadow_password("wrong", hash), Ok(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_shadow_password_accepts_sha256_crypt() {
|
||||
let hash = "$5$saltstring$OH4IDuTlsuTYPdED1gsuiRMyTAwNlRWyA6Xr3I4/dQ5";
|
||||
assert_eq!(verify_shadow_password("password", hash), Ok(true));
|
||||
assert_eq!(verify_shadow_password("wrong", hash), Ok(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_shadow_password_rejects_unknown_hash_prefix() {
|
||||
assert_eq!(verify_shadow_password("password", "$1$legacy$hash"), Err(VerifyError::UnsupportedHashFormat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_password_rejects_locked_accounts() {
|
||||
let account = Account {
|
||||
username: String::from("greeter"),
|
||||
password: String::from("!"),
|
||||
uid: 101,
|
||||
shell: String::from("/usr/bin/ion"),
|
||||
};
|
||||
assert!(!verify_password(&account, "anything"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn login_allowed_rejects_low_uid_non_root_accounts() {
|
||||
let account = Account {
|
||||
username: String::from("greeter"),
|
||||
password: String::from("password"),
|
||||
uid: 101,
|
||||
shell: String::from("/usr/bin/ion"),
|
||||
};
|
||||
assert!(!login_allowed(&account));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remember_failure_locks_after_five_attempts() {
|
||||
let state = RuntimeState::default();
|
||||
for _ in 0..4 {
|
||||
let message = remember_failure(&state, "user").expect("failure tracking should succeed");
|
||||
assert_eq!(message, "Invalid username or password.");
|
||||
}
|
||||
|
||||
let message = remember_failure(&state, "user").expect("lockout tracking should succeed");
|
||||
assert_eq!(message, "Too many failed attempts. Try again shortly.");
|
||||
assert_eq!(
|
||||
check_lockout(&state, "user").expect("lockout lookup should succeed"),
|
||||
Some(String::from("Too many failed attempts. Try again shortly."))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_approval_rejects_vt_mismatch() {
|
||||
let state = RuntimeState::default();
|
||||
remember_success(&state, "user", 3).expect("approval should be recorded");
|
||||
assert_eq!(
|
||||
take_approval(&state, "user", 4),
|
||||
Err(String::from("Authentication approval does not match the requested VT."))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_session_request_rejects_missing_approval() {
|
||||
let state = RuntimeState::default();
|
||||
let response = handle_request(
|
||||
AuthRequest::StartSession {
|
||||
request_id: 7,
|
||||
username: String::from("user"),
|
||||
session: String::from("kde-wayland"),
|
||||
vt: 3,
|
||||
},
|
||||
&state,
|
||||
);
|
||||
|
||||
match response {
|
||||
AuthResponse::SessionResult {
|
||||
request_id,
|
||||
ok,
|
||||
exit_code,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(request_id, 7);
|
||||
assert!(!ok);
|
||||
assert_eq!(exit_code, None);
|
||||
assert_eq!(message, "No recent authentication approval exists for this user.");
|
||||
}
|
||||
_ => panic!("expected session_result response"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authenticate_request_rejects_locked_account_marker() {
|
||||
let account = Account {
|
||||
username: String::from("greeter"),
|
||||
password: String::from("!"),
|
||||
uid: 101,
|
||||
shell: String::from("/usr/bin/ion"),
|
||||
};
|
||||
|
||||
assert!(!login_allowed(&account) || !verify_password(&account, "anything"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power_action_request_rejects_unsupported_action() {
|
||||
let state = RuntimeState::default();
|
||||
let response = handle_request(
|
||||
AuthRequest::PowerAction {
|
||||
request_id: 11,
|
||||
action: String::from("hibernate"),
|
||||
},
|
||||
&state,
|
||||
);
|
||||
|
||||
match response {
|
||||
AuthResponse::PowerResult {
|
||||
request_id,
|
||||
ok,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(request_id, 11);
|
||||
assert!(!ok);
|
||||
assert_eq!(message, "unsupported power action 'hibernate'");
|
||||
}
|
||||
_ => panic!("expected power_result response"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_connection_returns_error_for_invalid_json() {
|
||||
match send_handle_connection_request("not-json") {
|
||||
AuthResponse::Error { message } => {
|
||||
assert!(message.contains("invalid request:"));
|
||||
}
|
||||
_ => panic!("expected error response"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_account_fields_detects_redox_layout() {
|
||||
let (format, parts) = split_account_fields("greeter;101;101;Greeter;/nonexistent;/usr/bin/ion");
|
||||
assert_eq!(format, AccountFormat::Redox);
|
||||
assert_eq!(parts[0], "greeter");
|
||||
assert_eq!(parts[1], "101");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_account_fields_detects_unix_layout() {
|
||||
let (format, parts) = split_account_fields("root:x:0:0:root:/root:/usr/bin/ion");
|
||||
assert_eq!(format, AccountFormat::Unix);
|
||||
assert_eq!(parts[2], "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_account_fields_keeps_empty_redox_shadow_hash() {
|
||||
let (_format, parts) = split_account_fields("greeter;");
|
||||
assert_eq!(parts, vec![String::from("greeter"), String::new()]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "custom"
|
||||
dependencies = ["qtbase", "qtdeclarative", "qtwayland"]
|
||||
script = """
|
||||
set -ex
|
||||
|
||||
DYNAMIC_INIT
|
||||
|
||||
for qtdir in plugins mkspecs metatypes modules; do
|
||||
if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ -d "${COOKBOOK_SYSROOT}/${qtdir}" ] && [ ! -L "${COOKBOOK_SYSROOT}/${qtdir}" ]; then
|
||||
rm -rf "${COOKBOOK_SYSROOT}/${qtdir}"
|
||||
fi
|
||||
if [ -d "${COOKBOOK_SYSROOT}/usr/${qtdir}" ] && [ ! -e "${COOKBOOK_SYSROOT}/${qtdir}" ]; then
|
||||
ln -s "usr/${qtdir}" "${COOKBOOK_SYSROOT}/${qtdir}"
|
||||
fi
|
||||
done
|
||||
|
||||
cookbook_cargo
|
||||
|
||||
rm -f CMakeCache.txt
|
||||
rm -rf CMakeFiles
|
||||
cmake "${COOKBOOK_SOURCE}/ui" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="${COOKBOOK_ROOT}/local/recipes/qt/redox-toolchain.cmake" \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_PREFIX_PATH="${COOKBOOK_SYSROOT}" \
|
||||
-DQT_NO_PRIVATE_MODULE_WARNING=ON \
|
||||
-Wno-dev
|
||||
|
||||
cmake --build . -j${COOKBOOK_MAKE_JOBS}
|
||||
cmake --install . --prefix "${COOKBOOK_STAGE}/usr"
|
||||
|
||||
mkdir -pv "$COOKBOOK_STAGE/usr/bin"
|
||||
mkdir -pv "$COOKBOOK_STAGE/usr/share/redbear/greeter"
|
||||
cp -v "$COOKBOOK_SOURCE/redbear-greeter-compositor" "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
|
||||
chmod 0755 "$COOKBOOK_STAGE/usr/share/redbear/greeter/redbear-greeter-compositor"
|
||||
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS loading background.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/background.png"
|
||||
cp -v "$COOKBOOK_RECIPE/../../../../local/Assets/images/Red Bear OS icon.png" "$COOKBOOK_STAGE/usr/share/redbear/greeter/icon.png"
|
||||
ln -svf ../share/redbear/greeter/redbear-greeter-compositor "$COOKBOOK_STAGE/usr/bin/redbear-greeter-compositor"
|
||||
"""
|
||||
|
||||
[package.files]
|
||||
"/usr/bin/redbear-greeterd" = "redbear-greeterd"
|
||||
"/usr/bin/redbear-greeter-ui" = "redbear-greeter-ui"
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "redbear-greeter"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-greeterd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
export DISPLAY=""
|
||||
export WAYLAND_DISPLAY="${WAYLAND_DISPLAY:-wayland-0}"
|
||||
export XDG_SESSION_TYPE=wayland
|
||||
export LIBSEAT_BACKEND=seatd
|
||||
export SEATD_SOCK=/run/seatd.sock
|
||||
export QT_PLUGIN_PATH="${QT_PLUGIN_PATH:-/usr/plugins}"
|
||||
export QT_QPA_PLATFORM_PLUGIN_PATH="${QT_QPA_PLATFORM_PLUGIN_PATH:-/usr/plugins/platforms}"
|
||||
export QML2_IMPORT_PATH="${QML2_IMPORT_PATH:-/usr/qml}"
|
||||
export XCURSOR_THEME="${XCURSOR_THEME:-Pop}"
|
||||
export XKB_CONFIG_ROOT="${XKB_CONFIG_ROOT:-/usr/share/X11/xkb}"
|
||||
|
||||
if [ -z "${XDG_RUNTIME_DIR:-}" ]; then
|
||||
export XDG_RUNTIME_DIR="/tmp/run/greeter"
|
||||
fi
|
||||
|
||||
mkdir -p "$XDG_RUNTIME_DIR"
|
||||
|
||||
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-launch >/scheme/null 2>&1; then
|
||||
eval "$(dbus-launch --sh-syntax)"
|
||||
fi
|
||||
|
||||
exec kwin_wayland --replace
|
||||
@@ -0,0 +1,699 @@
|
||||
use std::{
|
||||
env,
|
||||
fs,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
os::unix::{fs::PermissionsExt, net::{UnixListener, UnixStream}},
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Child, Command, ExitStatus},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const GREETER_SOCKET_PATH: &str = "/run/redbear-greeterd.sock";
|
||||
const AUTH_SOCKET_PATH: &str = "/run/redbear-authd.sock";
|
||||
const BACKGROUND_PATH: &str = "/usr/share/redbear/greeter/background.png";
|
||||
const ICON_PATH: &str = "/usr/share/redbear/greeter/icon.png";
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum GreeterState {
|
||||
Starting,
|
||||
GreeterReady,
|
||||
Authenticating,
|
||||
LaunchingSession,
|
||||
SessionRunning,
|
||||
ReturningToGreeter,
|
||||
PowerAction,
|
||||
FatalError,
|
||||
}
|
||||
|
||||
impl GreeterState {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
GreeterState::Starting => "starting",
|
||||
GreeterState::GreeterReady => "greeter_ready",
|
||||
GreeterState::Authenticating => "authenticating",
|
||||
GreeterState::LaunchingSession => "launching_session",
|
||||
GreeterState::SessionRunning => "session_running",
|
||||
GreeterState::ReturningToGreeter => "returning_to_greeter",
|
||||
GreeterState::PowerAction => "power_action",
|
||||
GreeterState::FatalError => "fatal_error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct GreeterDaemon {
|
||||
listener: UnixListener,
|
||||
vt: u32,
|
||||
greeter_user: String,
|
||||
runtime_dir: PathBuf,
|
||||
wayland_display: String,
|
||||
state: GreeterState,
|
||||
message: String,
|
||||
compositor: Option<Child>,
|
||||
ui: Option<Child>,
|
||||
restart_attempts: Vec<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum GreeterRequest {
|
||||
Hello { version: u32 },
|
||||
SubmitLogin { username: String, password: String },
|
||||
RequestShutdown,
|
||||
RequestReboot,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum GreeterResponse {
|
||||
HelloOk {
|
||||
background: String,
|
||||
icon: String,
|
||||
session_name: String,
|
||||
state: String,
|
||||
message: String,
|
||||
},
|
||||
LoginResult {
|
||||
ok: bool,
|
||||
state: String,
|
||||
message: String,
|
||||
},
|
||||
ActionResult {
|
||||
ok: bool,
|
||||
message: String,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum AuthRequest<'a> {
|
||||
Authenticate {
|
||||
request_id: u64,
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
vt: u32,
|
||||
},
|
||||
StartSession {
|
||||
request_id: u64,
|
||||
username: &'a str,
|
||||
session: &'a str,
|
||||
vt: u32,
|
||||
},
|
||||
PowerAction {
|
||||
request_id: u64,
|
||||
action: &'a str,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum AuthResponse {
|
||||
AuthenticateResult {
|
||||
ok: bool,
|
||||
message: String,
|
||||
#[allow(dead_code)]
|
||||
request_id: u64,
|
||||
},
|
||||
SessionResult {
|
||||
ok: bool,
|
||||
message: String,
|
||||
#[allow(dead_code)]
|
||||
request_id: u64,
|
||||
#[allow(dead_code)]
|
||||
exit_code: Option<i32>,
|
||||
},
|
||||
PowerResult {
|
||||
ok: bool,
|
||||
message: String,
|
||||
#[allow(dead_code)]
|
||||
request_id: u64,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"Usage: redbear-greeterd [--help]"
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<(), String> {
|
||||
let mut args = env::args().skip(1);
|
||||
match args.next() {
|
||||
None => Ok(()),
|
||||
Some(arg) if arg == "--help" || arg == "-h" => Err(String::new()),
|
||||
Some(arg) => Err(format!("unrecognized argument '{arg}'")),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AccountFormat {
|
||||
Redox,
|
||||
Unix,
|
||||
}
|
||||
|
||||
fn split_account_fields(line: &str) -> (AccountFormat, Vec<&str>) {
|
||||
let format = if line.contains(';') {
|
||||
AccountFormat::Redox
|
||||
} else {
|
||||
AccountFormat::Unix
|
||||
};
|
||||
let delimiter = match format {
|
||||
AccountFormat::Redox => ';',
|
||||
AccountFormat::Unix => ':',
|
||||
};
|
||||
(format, line.split(delimiter).collect())
|
||||
}
|
||||
|
||||
fn parse_uid_gid(parts: &[&str], format: AccountFormat) -> Option<(u32, u32)> {
|
||||
let (uid_index, gid_index) = match format {
|
||||
AccountFormat::Redox if parts.len() >= 3 => (1, 2),
|
||||
AccountFormat::Unix if parts.len() >= 4 => (2, 3),
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let uid = parts[uid_index].parse::<u32>().ok()?;
|
||||
let gid = parts[gid_index].parse::<u32>().ok()?;
|
||||
Some((uid, gid))
|
||||
}
|
||||
|
||||
fn load_uid_gid(username: &str) -> Result<(u32, u32), String> {
|
||||
let passwd = fs::read_to_string("/etc/passwd").map_err(|err| format!("failed to read /etc/passwd: {err}"))?;
|
||||
for line in passwd.lines() {
|
||||
if line.trim().is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let (format, parts) = split_account_fields(line);
|
||||
if parts.len() < 3 || parts[0] != username {
|
||||
continue;
|
||||
}
|
||||
if let Some((uid, gid)) = parse_uid_gid(&parts, format) {
|
||||
return Ok((uid, gid));
|
||||
}
|
||||
return Err(format!("invalid uid/gid for user '{username}'"));
|
||||
}
|
||||
Err(format!("unknown greeter user '{username}'"))
|
||||
}
|
||||
|
||||
fn change_socket_ownership(path: &Path, uid: u32, gid: u32) -> Result<(), String> {
|
||||
let c_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes())
|
||||
.map_err(|_| format!("socket path {} contains interior NUL", path.display()))?;
|
||||
let result = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("failed to chown {}: {}", path.display(), io::Error::last_os_error()))
|
||||
}
|
||||
}
|
||||
|
||||
fn send_auth_request(request: &AuthRequest<'_>) -> Result<AuthResponse, String> {
|
||||
let mut stream = UnixStream::connect(AUTH_SOCKET_PATH)
|
||||
.map_err(|err| format!("failed to connect to {AUTH_SOCKET_PATH}: {err}"))?;
|
||||
let payload = serde_json::to_string(request).map_err(|err| format!("failed to serialize auth request: {err}"))?;
|
||||
stream
|
||||
.write_all(payload.as_bytes())
|
||||
.and_then(|_| stream.write_all(b"\n"))
|
||||
.map_err(|err| format!("failed to write auth request: {err}"))?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
reader
|
||||
.read_line(&mut line)
|
||||
.map_err(|err| format!("failed to read auth response: {err}"))?;
|
||||
serde_json::from_str(line.trim()).map_err(|err| format!("failed to parse auth response: {err}"))
|
||||
}
|
||||
|
||||
impl GreeterDaemon {
|
||||
fn hello_response(&self) -> GreeterResponse {
|
||||
GreeterResponse::HelloOk {
|
||||
background: String::from(BACKGROUND_PATH),
|
||||
icon: String::from(ICON_PATH),
|
||||
session_name: String::from("KDE on Wayland"),
|
||||
state: String::from(self.state.as_str()),
|
||||
message: self.message.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn new() -> Result<Self, String> {
|
||||
let vt = env::var("VT")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u32>().ok())
|
||||
.unwrap_or(3);
|
||||
let greeter_user = env::var("REDBEAR_GREETER_USER").unwrap_or_else(|_| String::from("greeter"));
|
||||
|
||||
if Path::new(GREETER_SOCKET_PATH).exists() {
|
||||
fs::remove_file(GREETER_SOCKET_PATH)
|
||||
.map_err(|err| format!("failed to remove stale greeter socket: {err}"))?;
|
||||
}
|
||||
let listener = UnixListener::bind(GREETER_SOCKET_PATH)
|
||||
.map_err(|err| format!("failed to bind {GREETER_SOCKET_PATH}: {err}"))?;
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.map_err(|err| format!("failed to set nonblocking socket mode: {err}"))?;
|
||||
let (uid, gid) = load_uid_gid(&greeter_user)?;
|
||||
fs::set_permissions(GREETER_SOCKET_PATH, fs::Permissions::from_mode(0o660))
|
||||
.map_err(|err| format!("failed to chmod {GREETER_SOCKET_PATH}: {err}"))?;
|
||||
change_socket_ownership(Path::new(GREETER_SOCKET_PATH), uid, gid)?;
|
||||
|
||||
Ok(Self {
|
||||
listener,
|
||||
vt,
|
||||
greeter_user,
|
||||
runtime_dir: PathBuf::from("/tmp/run/redbear-greeter"),
|
||||
wayland_display: String::from("wayland-0"),
|
||||
state: GreeterState::Starting,
|
||||
message: String::from("Starting greeter"),
|
||||
compositor: None,
|
||||
ui: None,
|
||||
restart_attempts: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn set_state(&mut self, state: GreeterState, message: impl Into<String>) {
|
||||
self.state = state;
|
||||
self.message = message.into();
|
||||
}
|
||||
|
||||
fn configure_command(&self, command: &mut Command) {
|
||||
command.env("QT_PLUGIN_PATH", "/usr/plugins");
|
||||
command.env("QT_QPA_PLATFORM_PLUGIN_PATH", "/usr/plugins/platforms");
|
||||
command.env("QML2_IMPORT_PATH", "/usr/qml");
|
||||
command.env("XCURSOR_THEME", "Pop");
|
||||
command.env("XKB_CONFIG_ROOT", "/usr/share/X11/xkb");
|
||||
command.env("WAYLAND_DISPLAY", &self.wayland_display);
|
||||
}
|
||||
|
||||
fn spawn_as_greeter(&self, program: &str) -> Result<Child, String> {
|
||||
let mut command = Command::new("/usr/bin/redbear-session-launch");
|
||||
command
|
||||
.arg("--username")
|
||||
.arg(&self.greeter_user)
|
||||
.arg("--mode")
|
||||
.arg("command")
|
||||
.arg("--vt")
|
||||
.arg(self.vt.to_string())
|
||||
.arg("--runtime-dir")
|
||||
.arg(&self.runtime_dir)
|
||||
.arg("--wayland-display")
|
||||
.arg(&self.wayland_display)
|
||||
.arg("--command")
|
||||
.arg(program);
|
||||
self.configure_command(&mut command);
|
||||
command
|
||||
.spawn()
|
||||
.map_err(|err| format!("failed to spawn {program} as {}: {err}", self.greeter_user))
|
||||
}
|
||||
|
||||
fn wait_for_wayland_socket(&self) -> Result<(), String> {
|
||||
let socket_path = self.runtime_dir.join(&self.wayland_display);
|
||||
for _ in 0..60 {
|
||||
if socket_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
Err(format!("timed out waiting for compositor socket {}", socket_path.display()))
|
||||
}
|
||||
|
||||
fn start_surface(&mut self) -> Result<(), String> {
|
||||
self.set_state(GreeterState::Starting, "Starting greeter surface");
|
||||
self.compositor = Some(self.spawn_as_greeter("/usr/bin/redbear-greeter-compositor")?);
|
||||
self.wait_for_wayland_socket()?;
|
||||
self.ui = Some(self.spawn_as_greeter("/usr/bin/redbear-greeter-ui")?);
|
||||
self.set_state(GreeterState::GreeterReady, "Ready");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn kill_child(child: &mut Option<Child>) {
|
||||
if let Some(process) = child.as_mut() {
|
||||
let _ = process.kill();
|
||||
let _ = process.wait();
|
||||
}
|
||||
*child = None;
|
||||
}
|
||||
|
||||
fn note_restart(&mut self) -> Result<(), String> {
|
||||
let now = Instant::now();
|
||||
self.restart_attempts
|
||||
.retain(|attempt| now.saturating_duration_since(*attempt) <= Duration::from_secs(60));
|
||||
self.restart_attempts.push(now);
|
||||
if self.restart_attempts.len() > 3 {
|
||||
self.set_state(GreeterState::FatalError, "Greeter restart limit reached");
|
||||
return Err(String::from("greeter restart limit reached; leaving fallback consoles available"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_surface_exit(&mut self, status: ExitStatus) -> Result<(), String> {
|
||||
self.ui = None;
|
||||
if status.success() {
|
||||
self.message = String::from("Greeter UI exited");
|
||||
} else {
|
||||
self.message = format!("Greeter UI exited unexpectedly: {status}");
|
||||
}
|
||||
self.note_restart()?;
|
||||
Self::kill_child(&mut self.compositor);
|
||||
self.start_surface()
|
||||
}
|
||||
|
||||
fn launch_session(&mut self, username: &str) -> Result<(), String> {
|
||||
self.set_state(GreeterState::LaunchingSession, "Starting session");
|
||||
Self::kill_child(&mut self.ui);
|
||||
Self::kill_child(&mut self.compositor);
|
||||
self.set_state(GreeterState::SessionRunning, "Session running");
|
||||
|
||||
let response = send_auth_request(&AuthRequest::StartSession {
|
||||
request_id: 2,
|
||||
username,
|
||||
session: "kde-wayland",
|
||||
vt: self.vt,
|
||||
})?;
|
||||
|
||||
self.set_state(GreeterState::ReturningToGreeter, "Returning to greeter");
|
||||
match response {
|
||||
AuthResponse::SessionResult { ok, message, .. } => {
|
||||
if !ok {
|
||||
self.set_state(GreeterState::GreeterReady, message.clone());
|
||||
}
|
||||
self.message = message;
|
||||
}
|
||||
AuthResponse::Error { message } => self.message = message,
|
||||
_ => self.message = String::from("Unexpected auth response while starting session"),
|
||||
}
|
||||
self.start_surface()
|
||||
}
|
||||
|
||||
fn handle_connection(&mut self, stream: UnixStream) -> Result<(), String> {
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
reader
|
||||
.read_line(&mut line)
|
||||
.map_err(|err| format!("failed to read greeter request: {err}"))?;
|
||||
|
||||
let request = serde_json::from_str::<GreeterRequest>(line.trim())
|
||||
.map_err(|err| format!("invalid greeter request: {err}"))?;
|
||||
let mut launch_username = None;
|
||||
let response = match request {
|
||||
GreeterRequest::Hello { version } => {
|
||||
if version != 1 {
|
||||
GreeterResponse::Error {
|
||||
message: format!("unsupported greeter protocol version {version}"),
|
||||
}
|
||||
} else {
|
||||
self.hello_response()
|
||||
}
|
||||
}
|
||||
GreeterRequest::SubmitLogin { username, password } => {
|
||||
self.set_state(GreeterState::Authenticating, "Authenticating");
|
||||
match send_auth_request(&AuthRequest::Authenticate {
|
||||
request_id: 1,
|
||||
username: &username,
|
||||
password: &password,
|
||||
vt: self.vt,
|
||||
})? {
|
||||
AuthResponse::AuthenticateResult { ok, message, .. } => {
|
||||
if ok {
|
||||
self.set_state(GreeterState::LaunchingSession, "Starting session");
|
||||
launch_username = Some(username);
|
||||
} else {
|
||||
self.set_state(GreeterState::GreeterReady, message.clone());
|
||||
}
|
||||
GreeterResponse::LoginResult {
|
||||
ok,
|
||||
state: String::from(self.state.as_str()),
|
||||
message,
|
||||
}
|
||||
}
|
||||
AuthResponse::Error { message } => {
|
||||
self.set_state(GreeterState::GreeterReady, message.clone());
|
||||
GreeterResponse::Error { message }
|
||||
}
|
||||
_ => GreeterResponse::Error {
|
||||
message: String::from("unexpected auth response"),
|
||||
},
|
||||
}
|
||||
}
|
||||
GreeterRequest::RequestShutdown => {
|
||||
self.set_state(GreeterState::PowerAction, "Requesting shutdown");
|
||||
match send_auth_request(&AuthRequest::PowerAction {
|
||||
request_id: 3,
|
||||
action: "shutdown",
|
||||
})? {
|
||||
AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message },
|
||||
AuthResponse::Error { message } => GreeterResponse::Error { message },
|
||||
_ => GreeterResponse::Error {
|
||||
message: String::from("unexpected power-action response"),
|
||||
},
|
||||
}
|
||||
}
|
||||
GreeterRequest::RequestReboot => {
|
||||
self.set_state(GreeterState::PowerAction, "Requesting reboot");
|
||||
match send_auth_request(&AuthRequest::PowerAction {
|
||||
request_id: 4,
|
||||
action: "reboot",
|
||||
})? {
|
||||
AuthResponse::PowerResult { ok, message, .. } => GreeterResponse::ActionResult { ok, message },
|
||||
AuthResponse::Error { message } => GreeterResponse::Error { message },
|
||||
_ => GreeterResponse::Error {
|
||||
message: String::from("unexpected power-action response"),
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let payload = serde_json::to_string(&response)
|
||||
.map_err(|err| format!("failed to serialize greeter response: {err}"))?;
|
||||
let mut stream = reader.into_inner();
|
||||
stream
|
||||
.write_all(payload.as_bytes())
|
||||
.and_then(|_| stream.write_all(b"\n"))
|
||||
.map_err(|err| format!("failed to write greeter response: {err}"))?;
|
||||
|
||||
if let Some(username) = launch_username {
|
||||
self.launch_session(&username)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_children(&mut self) -> Result<(), String> {
|
||||
if let Some(process) = self.compositor.as_mut() {
|
||||
if let Some(status) = process.try_wait().map_err(|err| format!("failed to poll compositor: {err}"))? {
|
||||
self.compositor = None;
|
||||
self.note_restart()?;
|
||||
self.message = format!("Greeter compositor exited unexpectedly: {status}");
|
||||
Self::kill_child(&mut self.ui);
|
||||
self.start_surface()?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(process) = self.ui.as_mut() {
|
||||
if let Some(status) = process.try_wait().map_err(|err| format!("failed to poll greeter UI: {err}"))? {
|
||||
return self.handle_surface_exit(status);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(&mut self) -> Result<(), String> {
|
||||
self.start_surface()?;
|
||||
loop {
|
||||
self.check_children()?;
|
||||
match self.listener.accept() {
|
||||
Ok((stream, _)) => {
|
||||
if let Err(err) = self.handle_connection(stream) {
|
||||
eprintln!("redbear-greeterd: {err}");
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(err) => return Err(format!("failed to accept greeter connection: {err}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
match parse_args() {
|
||||
Ok(()) => {}
|
||||
Err(err) if err.is_empty() => {
|
||||
println!("{}", usage());
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
|
||||
let mut daemon = GreeterDaemon::new()?;
|
||||
daemon.run()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("redbear-greeterd: {err}");
|
||||
eprintln!("{}", usage());
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static TEST_SOCKET_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
fn test_daemon() -> GreeterDaemon {
|
||||
let unique = TEST_SOCKET_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let socket_path = std::env::temp_dir().join(format!(
|
||||
"redbear-greeterd-test-{}-{}.sock",
|
||||
process::id(),
|
||||
unique
|
||||
));
|
||||
let _ = fs::remove_file(&socket_path);
|
||||
let listener = UnixListener::bind(&socket_path).expect("test listener should bind");
|
||||
listener
|
||||
.set_nonblocking(true)
|
||||
.expect("test listener should become nonblocking");
|
||||
|
||||
GreeterDaemon {
|
||||
listener,
|
||||
vt: 3,
|
||||
greeter_user: String::from("greeter"),
|
||||
runtime_dir: PathBuf::from("/tmp/run/redbear-greeter-test"),
|
||||
wayland_display: String::from("wayland-0"),
|
||||
state: GreeterState::Starting,
|
||||
message: String::from("Starting greeter"),
|
||||
compositor: None,
|
||||
ui: None,
|
||||
restart_attempts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_daemon_request(daemon: &mut GreeterDaemon, request: &str) -> GreeterResponse {
|
||||
let (mut client, server) = UnixStream::pair().expect("socket pair should open");
|
||||
client
|
||||
.write_all(request.as_bytes())
|
||||
.and_then(|_| client.write_all(b"\n"))
|
||||
.expect("request should write");
|
||||
daemon.handle_connection(server).expect("handler should succeed");
|
||||
let mut line = String::new();
|
||||
BufReader::new(client)
|
||||
.read_line(&mut line)
|
||||
.expect("response should read");
|
||||
serde_json::from_str(line.trim()).expect("response should parse")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn greeter_state_strings_match_protocol_contract() {
|
||||
assert_eq!(GreeterState::Starting.as_str(), "starting");
|
||||
assert_eq!(GreeterState::GreeterReady.as_str(), "greeter_ready");
|
||||
assert_eq!(GreeterState::Authenticating.as_str(), "authenticating");
|
||||
assert_eq!(GreeterState::LaunchingSession.as_str(), "launching_session");
|
||||
assert_eq!(GreeterState::SessionRunning.as_str(), "session_running");
|
||||
assert_eq!(GreeterState::ReturningToGreeter.as_str(), "returning_to_greeter");
|
||||
assert_eq!(GreeterState::PowerAction.as_str(), "power_action");
|
||||
assert_eq!(GreeterState::FatalError.as_str(), "fatal_error");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hello_response_uses_installed_asset_paths() {
|
||||
let mut daemon = test_daemon();
|
||||
daemon.set_state(GreeterState::GreeterReady, "Ready");
|
||||
|
||||
match daemon.hello_response() {
|
||||
GreeterResponse::HelloOk {
|
||||
background,
|
||||
icon,
|
||||
session_name,
|
||||
state,
|
||||
message,
|
||||
} => {
|
||||
assert_eq!(background, BACKGROUND_PATH);
|
||||
assert_eq!(icon, ICON_PATH);
|
||||
assert_eq!(session_name, "KDE on Wayland");
|
||||
assert_eq!(state, "greeter_ready");
|
||||
assert_eq!(message, "Ready");
|
||||
}
|
||||
_ => panic!("expected hello_ok response"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_restart_bounds_repeated_failures() {
|
||||
let mut daemon = test_daemon();
|
||||
|
||||
for _ in 0..3 {
|
||||
daemon.note_restart().expect("restart should remain bounded");
|
||||
assert_ne!(daemon.state, GreeterState::FatalError);
|
||||
}
|
||||
|
||||
let error = daemon.note_restart().expect_err("fourth restart should fail");
|
||||
assert!(error.contains("restart limit"));
|
||||
assert_eq!(daemon.state, GreeterState::FatalError);
|
||||
assert_eq!(daemon.message, "Greeter restart limit reached");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_connection_rejects_unsupported_protocol_version() {
|
||||
let mut daemon = test_daemon();
|
||||
|
||||
match send_daemon_request(&mut daemon, r#"{"type":"hello","version":99}"#) {
|
||||
GreeterResponse::Error { message } => {
|
||||
assert_eq!(message, "unsupported greeter protocol version 99");
|
||||
}
|
||||
_ => panic!("expected error response"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_connection_rejects_invalid_json_request() {
|
||||
let mut daemon = test_daemon();
|
||||
let (mut client, server) = UnixStream::pair().expect("socket pair should open");
|
||||
client
|
||||
.write_all(b"not-json\n")
|
||||
.expect("request should write");
|
||||
let error = daemon
|
||||
.handle_connection(server)
|
||||
.expect_err("invalid request should fail");
|
||||
assert!(error.contains("invalid greeter request"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_uid_gid_accepts_redox_style_layout() {
|
||||
assert_eq!(
|
||||
parse_uid_gid(
|
||||
&["greeter", "101", "101", "Greeter", "/nonexistent", "/usr/bin/ion"],
|
||||
AccountFormat::Redox,
|
||||
),
|
||||
Some((101, 101))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_uid_gid_accepts_unix_style_layout() {
|
||||
assert_eq!(
|
||||
parse_uid_gid(
|
||||
&["root", "x", "0", "0", "root", "/root", "/usr/bin/ion"],
|
||||
AccountFormat::Unix,
|
||||
),
|
||||
Some((0, 0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_account_fields_detects_redox_layout() {
|
||||
let (format, parts) = split_account_fields("greeter;101;101;Greeter;/nonexistent;/usr/bin/ion");
|
||||
assert_eq!(format, AccountFormat::Redox);
|
||||
assert_eq!(parts[0], "greeter");
|
||||
assert_eq!(parts[2], "101");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(redbear-greeter-ui LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick QuickControls2)
|
||||
|
||||
qt_add_executable(redbear-greeter-ui
|
||||
main.cpp
|
||||
greeter_backend.cpp
|
||||
greeter_backend.h
|
||||
resources.qrc
|
||||
)
|
||||
|
||||
target_compile_options(redbear-greeter-ui PRIVATE -fcf-protection=none)
|
||||
target_link_options(redbear-greeter-ui PRIVATE -fcf-protection=none)
|
||||
|
||||
target_link_libraries(redbear-greeter-ui PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Qml
|
||||
Qt6::Quick
|
||||
Qt6::QuickControls2
|
||||
)
|
||||
|
||||
install(TARGETS redbear-greeter-ui RUNTIME DESTINATION bin)
|
||||
@@ -0,0 +1,152 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
ApplicationWindow {
|
||||
id: root
|
||||
visible: true
|
||||
visibility: Window.FullScreen
|
||||
color: "#11090a"
|
||||
title: "Red Bear Greeter"
|
||||
|
||||
function submitLogin() {
|
||||
greeterBackend.submitLogin(usernameField.text, passwordField.text)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#11090a"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: greeterBackend.backgroundUrl
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
opacity: 0.88
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "#230a0d"
|
||||
opacity: 0.45
|
||||
}
|
||||
}
|
||||
|
||||
Pane {
|
||||
width: Math.min(parent.width * 0.42, 620)
|
||||
anchors.centerIn: parent
|
||||
padding: 28
|
||||
|
||||
background: Rectangle {
|
||||
radius: 18
|
||||
color: "#cc150c0f"
|
||||
border.color: "#66f7d7d7"
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 18
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 156
|
||||
|
||||
Image {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 2
|
||||
source: greeterBackend.iconUrl
|
||||
width: 108
|
||||
height: 108
|
||||
fillMode: Image.PreserveAspectFit
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Red Bear OS"
|
||||
font.pixelSize: 26
|
||||
font.bold: true
|
||||
color: "#fff4f4"
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: greeterBackend.sessionName
|
||||
font.pixelSize: 15
|
||||
color: "#f1c5c5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: usernameField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Username"
|
||||
enabled: !greeterBackend.busy
|
||||
selectByMouse: true
|
||||
color: "#fff8f8"
|
||||
font.pixelSize: 18
|
||||
onAccepted: passwordField.forceActiveFocus()
|
||||
}
|
||||
|
||||
TextField {
|
||||
id: passwordField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "Password"
|
||||
enabled: !greeterBackend.busy
|
||||
selectByMouse: true
|
||||
echoMode: TextInput.Password
|
||||
color: "#fff8f8"
|
||||
font.pixelSize: 18
|
||||
onAccepted: root.submitLogin()
|
||||
}
|
||||
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
text: greeterBackend.message
|
||||
color: greeterBackend.state === "fatal_error" ? "#ffb4b4" : "#ffe7e7"
|
||||
font.pixelSize: 15
|
||||
}
|
||||
|
||||
BusyIndicator {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
running: greeterBackend.busy
|
||||
visible: running
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
Button {
|
||||
Layout.fillWidth: true
|
||||
text: greeterBackend.busy ? "Working…" : "Log In"
|
||||
enabled: !greeterBackend.busy
|
||||
onClicked: root.submitLogin()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Shutdown"
|
||||
enabled: !greeterBackend.busy
|
||||
onClicked: greeterBackend.requestShutdown()
|
||||
}
|
||||
|
||||
Button {
|
||||
text: "Reboot"
|
||||
enabled: !greeterBackend.busy
|
||||
onClicked: greeterBackend.requestReboot()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: usernameField.forceActiveFocus()
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
#include "greeter_backend.h"
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QTimer>
|
||||
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
constexpr auto kGreeterSocketPath = "/run/redbear-greeterd.sock";
|
||||
constexpr auto kConnectTimeoutMs = 1500;
|
||||
constexpr auto kReadTimeoutMs = 5000;
|
||||
|
||||
bool waitForReadable(int fd, int timeoutMs, QString *error) {
|
||||
pollfd descriptor{};
|
||||
descriptor.fd = fd;
|
||||
descriptor.events = POLLIN;
|
||||
|
||||
const auto pollResult = ::poll(&descriptor, 1, timeoutMs);
|
||||
if (pollResult > 0) {
|
||||
return true;
|
||||
}
|
||||
if (pollResult == 0) {
|
||||
*error = QStringLiteral("timed out waiting for greeter response");
|
||||
return false;
|
||||
}
|
||||
|
||||
*error = QStringLiteral("failed while waiting for greeter response: %1").arg(QString::fromLocal8Bit(std::strerror(errno)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
GreeterBackend::GreeterBackend(QObject *parent) : QObject(parent) {}
|
||||
|
||||
QUrl GreeterBackend::backgroundUrl() const {
|
||||
return m_backgroundUrl;
|
||||
}
|
||||
|
||||
QUrl GreeterBackend::iconUrl() const {
|
||||
return m_iconUrl;
|
||||
}
|
||||
|
||||
QString GreeterBackend::sessionName() const {
|
||||
return m_sessionName;
|
||||
}
|
||||
|
||||
QString GreeterBackend::state() const {
|
||||
return m_state;
|
||||
}
|
||||
|
||||
QString GreeterBackend::message() const {
|
||||
return m_message;
|
||||
}
|
||||
|
||||
bool GreeterBackend::busy() const {
|
||||
return m_busy;
|
||||
}
|
||||
|
||||
void GreeterBackend::initialize() {
|
||||
const auto response = sendRequest(QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("hello")},
|
||||
{QStringLiteral("version"), 1}})
|
||||
.toJson(QJsonDocument::Compact));
|
||||
if (!response.transportOk) {
|
||||
applyError(response.transportError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.type != QStringLiteral("hello_ok")) {
|
||||
applyError(response.message.isEmpty() ? QStringLiteral("unexpected greeter hello response") : response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
setGreeting(response.backgroundPath, response.iconPath, response.sessionName);
|
||||
setStatus(response.state, response.message);
|
||||
}
|
||||
|
||||
void GreeterBackend::submitLogin(const QString &username, const QString &password) {
|
||||
if (m_busy) {
|
||||
return;
|
||||
}
|
||||
if (username.trimmed().isEmpty() || password.isEmpty()) {
|
||||
setStatus(QStringLiteral("greeter_ready"), QStringLiteral("Enter both username and password."));
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setStatus(QStringLiteral("authenticating"), QStringLiteral("Authenticating"));
|
||||
|
||||
const auto response = sendRequest(QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("submit_login")},
|
||||
{QStringLiteral("username"), username},
|
||||
{QStringLiteral("password"), password}})
|
||||
.toJson(QJsonDocument::Compact));
|
||||
setBusy(false);
|
||||
if (!response.transportOk) {
|
||||
applyError(response.transportError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.type == QStringLiteral("login_result")) {
|
||||
setStatus(response.state, response.message);
|
||||
if (response.ok) {
|
||||
QTimer::singleShot(0, qApp, &QCoreApplication::quit);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
applyError(response.message.isEmpty() ? QStringLiteral("unexpected login response") : response.message);
|
||||
}
|
||||
|
||||
void GreeterBackend::requestShutdown() {
|
||||
if (m_busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setStatus(QStringLiteral("power_action"), QStringLiteral("Requesting shutdown"));
|
||||
const auto response = sendRequest(
|
||||
QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("request_shutdown")}})
|
||||
.toJson(QJsonDocument::Compact));
|
||||
setBusy(false);
|
||||
|
||||
if (!response.transportOk) {
|
||||
applyError(response.transportError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.type == QStringLiteral("action_result")) {
|
||||
setStatus(response.ok ? QStringLiteral("power_action") : QStringLiteral("greeter_ready"), response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
applyError(response.message.isEmpty() ? QStringLiteral("unexpected shutdown response") : response.message);
|
||||
}
|
||||
|
||||
void GreeterBackend::requestReboot() {
|
||||
if (m_busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
setBusy(true);
|
||||
setStatus(QStringLiteral("power_action"), QStringLiteral("Requesting reboot"));
|
||||
const auto response = sendRequest(
|
||||
QJsonDocument(QJsonObject{{QStringLiteral("type"), QStringLiteral("request_reboot")}})
|
||||
.toJson(QJsonDocument::Compact));
|
||||
setBusy(false);
|
||||
|
||||
if (!response.transportOk) {
|
||||
applyError(response.transportError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.type == QStringLiteral("action_result")) {
|
||||
setStatus(response.ok ? QStringLiteral("power_action") : QStringLiteral("greeter_ready"), response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
applyError(response.message.isEmpty() ? QStringLiteral("unexpected reboot response") : response.message);
|
||||
}
|
||||
|
||||
GreeterBackend::Response GreeterBackend::sendRequest(const QByteArray &payload) const {
|
||||
Response response;
|
||||
|
||||
const int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (fd < 0) {
|
||||
response.transportError = QStringLiteral("failed to create greeter socket: %1")
|
||||
.arg(QString::fromLocal8Bit(std::strerror(errno)));
|
||||
return response;
|
||||
}
|
||||
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
std::strncpy(address.sun_path, kGreeterSocketPath, sizeof(address.sun_path) - 1);
|
||||
const auto addressSize = static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + std::strlen(address.sun_path) + 1);
|
||||
if (::connect(fd, reinterpret_cast<sockaddr *>(&address), addressSize) != 0) {
|
||||
response.transportError = QStringLiteral("failed to connect to %1: %2")
|
||||
.arg(QString::fromLatin1(kGreeterSocketPath),
|
||||
QString::fromLocal8Bit(std::strerror(errno)));
|
||||
::close(fd);
|
||||
return response;
|
||||
}
|
||||
|
||||
const auto fullPayload = payload + '\n';
|
||||
qsizetype written = 0;
|
||||
while (written < fullPayload.size()) {
|
||||
const auto chunk = ::write(fd, fullPayload.constData() + written, static_cast<size_t>(fullPayload.size() - written));
|
||||
if (chunk < 0) {
|
||||
response.transportError = QStringLiteral("failed to write greeter request: %1")
|
||||
.arg(QString::fromLocal8Bit(std::strerror(errno)));
|
||||
::close(fd);
|
||||
return response;
|
||||
}
|
||||
written += chunk;
|
||||
}
|
||||
|
||||
QString waitError;
|
||||
if (!waitForReadable(fd, kReadTimeoutMs, &waitError)) {
|
||||
response.transportError = waitError;
|
||||
::close(fd);
|
||||
return response;
|
||||
}
|
||||
|
||||
QByteArray reply;
|
||||
char buffer[1024];
|
||||
while (reply.indexOf('\n') < 0) {
|
||||
const auto chunk = ::read(fd, buffer, sizeof(buffer));
|
||||
if (chunk < 0) {
|
||||
response.transportError = QStringLiteral("failed to read greeter response: %1")
|
||||
.arg(QString::fromLocal8Bit(std::strerror(errno)));
|
||||
::close(fd);
|
||||
return response;
|
||||
}
|
||||
if (chunk == 0) {
|
||||
break;
|
||||
}
|
||||
reply.append(buffer, static_cast<int>(chunk));
|
||||
if (reply.indexOf('\n') < 0 && !waitForReadable(fd, kConnectTimeoutMs, &waitError)) {
|
||||
response.transportError = waitError;
|
||||
::close(fd);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
::close(fd);
|
||||
|
||||
const auto newlineIndex = reply.indexOf('\n');
|
||||
if (newlineIndex >= 0) {
|
||||
reply.truncate(newlineIndex);
|
||||
}
|
||||
|
||||
const auto document = QJsonDocument::fromJson(reply);
|
||||
if (!document.isObject()) {
|
||||
response.transportError = QStringLiteral("invalid greeter response payload");
|
||||
return response;
|
||||
}
|
||||
|
||||
const auto object = document.object();
|
||||
response.transportOk = true;
|
||||
response.type = object.value(QStringLiteral("type")).toString();
|
||||
response.ok = object.value(QStringLiteral("ok")).toBool();
|
||||
response.state = object.value(QStringLiteral("state")).toString();
|
||||
response.message = object.value(QStringLiteral("message")).toString();
|
||||
response.sessionName = object.value(QStringLiteral("session_name")).toString();
|
||||
response.backgroundPath = object.value(QStringLiteral("background")).toString();
|
||||
response.iconPath = object.value(QStringLiteral("icon")).toString();
|
||||
if (response.type == QStringLiteral("error") && response.message.isEmpty()) {
|
||||
response.message = QStringLiteral("greeter returned an unspecified error");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
void GreeterBackend::setGreeting(const QString &backgroundPath, const QString &iconPath, const QString &sessionName) {
|
||||
const auto nextBackground = backgroundPath.isEmpty() ? QUrl() : QUrl::fromLocalFile(backgroundPath);
|
||||
const auto nextIcon = iconPath.isEmpty() ? QUrl() : QUrl::fromLocalFile(iconPath);
|
||||
const auto nextSessionName = sessionName.isEmpty() ? QStringLiteral("KDE on Wayland") : sessionName;
|
||||
|
||||
if (m_backgroundUrl == nextBackground && m_iconUrl == nextIcon && m_sessionName == nextSessionName) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_backgroundUrl = nextBackground;
|
||||
m_iconUrl = nextIcon;
|
||||
m_sessionName = nextSessionName;
|
||||
emit greetingChanged();
|
||||
}
|
||||
|
||||
void GreeterBackend::setStatus(const QString &state, const QString &message) {
|
||||
const auto nextState = state.isEmpty() ? QStringLiteral("greeter_ready") : state;
|
||||
if (m_state == nextState && m_message == message) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_state = nextState;
|
||||
m_message = message;
|
||||
emit statusChanged();
|
||||
}
|
||||
|
||||
void GreeterBackend::setBusy(bool busy) {
|
||||
if (m_busy == busy) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_busy = busy;
|
||||
emit busyChanged();
|
||||
}
|
||||
|
||||
void GreeterBackend::applyError(const QString &message) {
|
||||
setStatus(QStringLiteral("fatal_error"), message);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
class GreeterBackend final : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QUrl backgroundUrl READ backgroundUrl NOTIFY greetingChanged)
|
||||
Q_PROPERTY(QUrl iconUrl READ iconUrl NOTIFY greetingChanged)
|
||||
Q_PROPERTY(QString sessionName READ sessionName NOTIFY greetingChanged)
|
||||
Q_PROPERTY(QString state READ state NOTIFY statusChanged)
|
||||
Q_PROPERTY(QString message READ message NOTIFY statusChanged)
|
||||
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
|
||||
|
||||
public:
|
||||
explicit GreeterBackend(QObject *parent = nullptr);
|
||||
|
||||
[[nodiscard]] QUrl backgroundUrl() const;
|
||||
[[nodiscard]] QUrl iconUrl() const;
|
||||
[[nodiscard]] QString sessionName() const;
|
||||
[[nodiscard]] QString state() const;
|
||||
[[nodiscard]] QString message() const;
|
||||
[[nodiscard]] bool busy() const;
|
||||
|
||||
Q_INVOKABLE void initialize();
|
||||
Q_INVOKABLE void submitLogin(const QString &username, const QString &password);
|
||||
Q_INVOKABLE void requestShutdown();
|
||||
Q_INVOKABLE void requestReboot();
|
||||
|
||||
signals:
|
||||
void greetingChanged();
|
||||
void statusChanged();
|
||||
void busyChanged();
|
||||
|
||||
private:
|
||||
struct Response {
|
||||
bool transportOk = false;
|
||||
QString transportError;
|
||||
QString type;
|
||||
bool ok = false;
|
||||
QString state;
|
||||
QString message;
|
||||
QString sessionName;
|
||||
QString backgroundPath;
|
||||
QString iconPath;
|
||||
};
|
||||
|
||||
[[nodiscard]] Response sendRequest(const QByteArray &payload) const;
|
||||
void setGreeting(const QString &backgroundPath, const QString &iconPath, const QString &sessionName);
|
||||
void setStatus(const QString &state, const QString &message);
|
||||
void setBusy(bool busy);
|
||||
void applyError(const QString &message);
|
||||
|
||||
QUrl m_backgroundUrl;
|
||||
QUrl m_iconUrl;
|
||||
QString m_sessionName = QStringLiteral("KDE on Wayland");
|
||||
QString m_state = QStringLiteral("starting");
|
||||
QString m_message = QStringLiteral("Connecting to greeter");
|
||||
bool m_busy = false;
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
#include <QGuiApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QQuickStyle>
|
||||
|
||||
#include "greeter_backend.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
qputenv("QT_QUICK_CONTROLS_STYLE", QByteArrayLiteral("Basic"));
|
||||
|
||||
QGuiApplication app(argc, argv);
|
||||
QQuickStyle::setStyle(QStringLiteral("Basic"));
|
||||
|
||||
GreeterBackend backend;
|
||||
QQmlApplicationEngine engine;
|
||||
engine.rootContext()->setContextProperty(QStringLiteral("greeterBackend"), &backend);
|
||||
engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
|
||||
|
||||
if (engine.rootObjects().isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
backend.initialize();
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>Main.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -10,6 +10,7 @@ template = "cargo"
|
||||
"/usr/bin/redbear-usb-check" = "redbear-usb-check"
|
||||
"/usr/bin/redbear-bluetooth-battery-check" = "redbear-bluetooth-battery-check"
|
||||
"/usr/bin/redbear-drm-display-check" = "redbear-drm-display-check"
|
||||
"/usr/bin/redbear-greeter-check" = "redbear-greeter-check"
|
||||
"/usr/bin/redbear-phase4-wayland-check" = "redbear-phase4-wayland-check"
|
||||
"/usr/bin/redbear-phase5-network-check" = "redbear-phase5-network-check"
|
||||
"/usr/bin/redbear-phase5-wifi-check" = "redbear-phase5-wifi-check"
|
||||
|
||||
@@ -59,6 +59,10 @@ path = "src/bin/redbear-phase5-wifi-link-check.rs"
|
||||
name = "redbear-phase6-kde-check"
|
||||
path = "src/bin/redbear-phase6-kde-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-greeter-check"
|
||||
path = "src/bin/redbear-greeter-check.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-drm-display-check"
|
||||
path = "src/bin/redbear-drm-display-check.rs"
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufRead, BufReader, Write},
|
||||
os::unix::net::UnixStream,
|
||||
path::Path,
|
||||
process,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const PROGRAM: &str = "redbear-greeter-check";
|
||||
const USAGE: &str = "Usage: redbear-greeter-check [--invalid USER PASSWORD | --valid USER PASSWORD]\n\nQuery the installed Red Bear greeter surface inside the guest.";
|
||||
const GREETER_SOCKET: &str = "/run/redbear-greeterd.sock";
|
||||
const GREETERD_BIN: &str = "/usr/bin/redbear-greeterd";
|
||||
const GREETER_UI_BIN: &str = "/usr/bin/redbear-greeter-ui";
|
||||
const AUTHD_BIN: &str = "/usr/bin/redbear-authd";
|
||||
const SESSION_LAUNCH_BIN: &str = "/usr/bin/redbear-session-launch";
|
||||
const GREETER_BACKGROUND: &str = "/usr/share/redbear/greeter/background.png";
|
||||
const GREETER_ICON: &str = "/usr/share/redbear/greeter/icon.png";
|
||||
const AUTHD_SERVICE: &str = "/usr/lib/init.d/19_redbear-authd.service";
|
||||
const DISPLAY_SHIM_SERVICE: &str = "/usr/lib/init.d/20_display.service";
|
||||
const GREETER_SERVICE: &str = "/usr/lib/init.d/20_greeter.service";
|
||||
const ACTIVATE_CONSOLE_SERVICE: &str = "/usr/lib/init.d/29_activate_console.service";
|
||||
const CONSOLE_SERVICE: &str = "/usr/lib/init.d/30_console.service";
|
||||
const DEBUG_CONSOLE_SERVICE: &str = "/usr/lib/init.d/31_debug_console.service";
|
||||
const VALIDATION_REQUEST: &str = "/run/redbear-kde-session.validation-request";
|
||||
const VALIDATION_SUCCESS: &str = "/run/redbear-kde-session.validation-success";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum Request<'a> {
|
||||
Hello { version: u32 },
|
||||
SubmitLogin { username: &'a str, password: &'a str },
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum Response {
|
||||
HelloOk {
|
||||
background: String,
|
||||
icon: String,
|
||||
session_name: String,
|
||||
state: String,
|
||||
message: String,
|
||||
},
|
||||
LoginResult {
|
||||
ok: bool,
|
||||
state: String,
|
||||
message: String,
|
||||
},
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
#[serde(other)]
|
||||
Other,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Status,
|
||||
Invalid { username: String, password: String },
|
||||
Valid { username: String, password: String },
|
||||
}
|
||||
|
||||
fn parse_mode_from_args<I>(args: I) -> Result<Mode, String>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let mut args = args.into_iter();
|
||||
match args.next() {
|
||||
None => Ok(Mode::Status),
|
||||
Some(flag) if flag == "--help" || flag == "-h" => Err(String::new()),
|
||||
Some(flag) if flag == "--invalid" => {
|
||||
let username = args.next().ok_or_else(|| String::from("missing username after --invalid"))?;
|
||||
let password = args.next().ok_or_else(|| String::from("missing password after --invalid"))?;
|
||||
if args.next().is_some() {
|
||||
return Err(String::from("unexpected extra arguments after --invalid USER PASSWORD"));
|
||||
}
|
||||
Ok(Mode::Invalid { username, password })
|
||||
}
|
||||
Some(flag) if flag == "--valid" => {
|
||||
let username = args.next().ok_or_else(|| String::from("missing username after --valid"))?;
|
||||
let password = args.next().ok_or_else(|| String::from("missing password after --valid"))?;
|
||||
if args.next().is_some() {
|
||||
return Err(String::from("unexpected extra arguments after --valid USER PASSWORD"));
|
||||
}
|
||||
Ok(Mode::Valid { username, password })
|
||||
}
|
||||
Some(other) => Err(format!("unsupported argument '{other}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_mode() -> Result<Mode, String> {
|
||||
parse_mode_from_args(std::env::args().skip(1))
|
||||
}
|
||||
|
||||
fn send_request(request: &Request<'_>) -> Result<Response, String> {
|
||||
let mut stream = UnixStream::connect(GREETER_SOCKET)
|
||||
.map_err(|err| format!("failed to connect to {GREETER_SOCKET}: {err}"))?;
|
||||
let payload = serde_json::to_string(request)
|
||||
.map_err(|err| format!("failed to serialize greeter request: {err}"))?;
|
||||
stream
|
||||
.write_all(payload.as_bytes())
|
||||
.and_then(|_| stream.write_all(b"\n"))
|
||||
.map_err(|err| format!("failed to write greeter request: {err}"))?;
|
||||
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
reader
|
||||
.read_line(&mut line)
|
||||
.map_err(|err| format!("failed to read greeter response: {err}"))?;
|
||||
serde_json::from_str(line.trim()).map_err(|err| format!("failed to parse greeter response: {err}"))
|
||||
}
|
||||
|
||||
fn require_path(path: &str) -> Result<(), String> {
|
||||
if Path::new(path).exists() {
|
||||
println!("{path}");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("missing {path}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_for_validation_marker(path: &str, timeout: Duration) -> Result<(), String> {
|
||||
let start = Instant::now();
|
||||
while start.elapsed() <= timeout {
|
||||
if Path::new(path).exists() {
|
||||
return Ok(());
|
||||
}
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
|
||||
Err(format!("timed out waiting for {path}"))
|
||||
}
|
||||
|
||||
fn wait_for_greeter_ready(timeout: Duration) -> Result<(), String> {
|
||||
let start = Instant::now();
|
||||
while start.elapsed() <= timeout {
|
||||
match send_request(&Request::Hello { version: 1 }) {
|
||||
Ok(Response::HelloOk { state, message, .. }) if state == "greeter_ready" => {
|
||||
println!("GREETER_VALID_READY_MESSAGE={message}");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => {}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
|
||||
Err(String::from("timed out waiting for greeter to return to greeter_ready"))
|
||||
}
|
||||
|
||||
fn run_status() -> Result<(), String> {
|
||||
println!("=== Red Bear Greeter Runtime Check ===");
|
||||
require_path(GREETERD_BIN)?;
|
||||
require_path(GREETER_UI_BIN)?;
|
||||
require_path(AUTHD_BIN)?;
|
||||
require_path(SESSION_LAUNCH_BIN)?;
|
||||
require_path(GREETER_BACKGROUND)?;
|
||||
require_path(GREETER_ICON)?;
|
||||
require_path(AUTHD_SERVICE)?;
|
||||
require_path(DISPLAY_SHIM_SERVICE)?;
|
||||
require_path(GREETER_SERVICE)?;
|
||||
require_path(ACTIVATE_CONSOLE_SERVICE)?;
|
||||
require_path(CONSOLE_SERVICE)?;
|
||||
require_path(DEBUG_CONSOLE_SERVICE)?;
|
||||
require_path(GREETER_SOCKET)?;
|
||||
|
||||
match send_request(&Request::Hello { version: 1 })? {
|
||||
Response::HelloOk {
|
||||
background,
|
||||
icon,
|
||||
session_name,
|
||||
state,
|
||||
message,
|
||||
} => {
|
||||
println!("GREETER_BACKGROUND={background}");
|
||||
println!("GREETER_ICON={icon}");
|
||||
println!("GREETER_SESSION={session_name}");
|
||||
println!("GREETER_STATE={state}");
|
||||
println!("GREETER_MESSAGE={message}");
|
||||
println!("GREETER_HELLO=ok");
|
||||
Ok(())
|
||||
}
|
||||
Response::Error { message } => Err(format!("greeter hello failed: {message}")),
|
||||
Response::Other => Err(String::from("unexpected greeter hello response")),
|
||||
Response::LoginResult { .. } => Err(String::from("unexpected login result when greeting greeter")),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_invalid(username: &str, password: &str) -> Result<(), String> {
|
||||
match send_request(&Request::SubmitLogin { username, password })? {
|
||||
Response::LoginResult { ok, state, message } => {
|
||||
println!("GREETER_INVALID_STATE={state}");
|
||||
println!("GREETER_INVALID_MESSAGE={message}");
|
||||
if ok {
|
||||
Err(String::from("invalid login unexpectedly succeeded"))
|
||||
} else {
|
||||
println!("GREETER_INVALID=ok");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Response::Error { message } => Err(format!("invalid-login request failed: {message}")),
|
||||
Response::Other => Err(String::from("unexpected greeter response for invalid login")),
|
||||
Response::HelloOk { .. } => Err(String::from("unexpected hello response for invalid login")),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_valid(username: &str, password: &str) -> Result<(), String> {
|
||||
let _ = fs::remove_file(VALIDATION_REQUEST);
|
||||
let _ = fs::remove_file(VALIDATION_SUCCESS);
|
||||
fs::write(VALIDATION_REQUEST, b"bounded-session\n")
|
||||
.map_err(|err| format!("failed to create validation request: {err}"))?;
|
||||
|
||||
match send_request(&Request::SubmitLogin { username, password })? {
|
||||
Response::LoginResult { ok, state, message } => {
|
||||
println!("GREETER_VALID_STATE={state}");
|
||||
println!("GREETER_VALID_MESSAGE={message}");
|
||||
if !ok {
|
||||
let _ = fs::remove_file(VALIDATION_REQUEST);
|
||||
return Err(String::from("valid login unexpectedly failed"));
|
||||
}
|
||||
}
|
||||
Response::Error { message } => {
|
||||
let _ = fs::remove_file(VALIDATION_REQUEST);
|
||||
return Err(format!("valid-login request failed: {message}"));
|
||||
}
|
||||
Response::Other => {
|
||||
let _ = fs::remove_file(VALIDATION_REQUEST);
|
||||
return Err(String::from("unexpected greeter response for valid login"));
|
||||
}
|
||||
Response::HelloOk { .. } => {
|
||||
let _ = fs::remove_file(VALIDATION_REQUEST);
|
||||
return Err(String::from("unexpected hello response for valid login"));
|
||||
}
|
||||
}
|
||||
|
||||
wait_for_validation_marker(VALIDATION_SUCCESS, Duration::from_secs(30))?;
|
||||
println!("GREETER_VALID_SESSION=started");
|
||||
wait_for_greeter_ready(Duration::from_secs(30))?;
|
||||
|
||||
let _ = fs::remove_file(VALIDATION_REQUEST);
|
||||
let _ = fs::remove_file(VALIDATION_SUCCESS);
|
||||
println!("GREETER_VALID=ok");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mode = match parse_mode() {
|
||||
Ok(mode) => mode,
|
||||
Err(err) if err.is_empty() => {
|
||||
println!("{USAGE}");
|
||||
process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
eprintln!("{USAGE}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let result = match mode {
|
||||
Mode::Status => run_status(),
|
||||
Mode::Invalid { username, password } => run_invalid(&username, &password),
|
||||
Mode::Valid { username, password } => run_valid(&username, &password),
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_mode_defaults_to_status() {
|
||||
assert_eq!(parse_mode_from_args(Vec::<String>::new()).expect("status mode should parse"), Mode::Status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_accepts_invalid_login_arguments() {
|
||||
assert_eq!(
|
||||
parse_mode_from_args(vec![
|
||||
String::from("--invalid"),
|
||||
String::from("alice"),
|
||||
String::from("wrong"),
|
||||
])
|
||||
.expect("invalid-login mode should parse"),
|
||||
Mode::Invalid {
|
||||
username: String::from("alice"),
|
||||
password: String::from("wrong"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_accepts_valid_login_arguments() {
|
||||
assert_eq!(
|
||||
parse_mode_from_args(vec![
|
||||
String::from("--valid"),
|
||||
String::from("alice"),
|
||||
String::from("password"),
|
||||
])
|
||||
.expect("valid-login mode should parse"),
|
||||
Mode::Valid {
|
||||
username: String::from("alice"),
|
||||
password: String::from("password"),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_rejects_extra_valid_arguments() {
|
||||
assert_eq!(
|
||||
parse_mode_from_args(vec![
|
||||
String::from("--valid"),
|
||||
String::from("alice"),
|
||||
String::from("password"),
|
||||
String::from("extra"),
|
||||
]),
|
||||
Err(String::from("unexpected extra arguments after --valid USER PASSWORD"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_rejects_extra_invalid_arguments() {
|
||||
assert_eq!(
|
||||
parse_mode_from_args(vec![
|
||||
String::from("--invalid"),
|
||||
String::from("alice"),
|
||||
String::from("wrong"),
|
||||
String::from("extra"),
|
||||
]),
|
||||
Err(String::from("unexpected extra arguments after --invalid USER PASSWORD"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_rejects_unknown_flags() {
|
||||
assert_eq!(
|
||||
parse_mode_from_args(vec![String::from("--bogus")]),
|
||||
Err(String::from("unsupported argument '--bogus'"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,9 @@ fn run() -> Result<(), String> {
|
||||
if !stdout.contains("discovery_source=") {
|
||||
return Err("iommu self-test did not report discovery source".to_string());
|
||||
}
|
||||
if !stdout.contains("dmar_present=") {
|
||||
return Err("iommu self-test did not report DMAR presence state".to_string());
|
||||
}
|
||||
if !stdout.contains("units_initialized_now=") {
|
||||
return Err("iommu self-test did not report initialized unit count".to_string());
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use std::fs::OpenOptions;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::Path;
|
||||
use std::process::{self, Command};
|
||||
|
||||
use syscall::O_NONBLOCK;
|
||||
|
||||
use redbear_hwutils::parse_args;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase-ps2-check";
|
||||
@@ -8,7 +12,14 @@ const USAGE: &str =
|
||||
"Usage: redbear-phase-ps2-check\n\nRun the bounded PS/2 and serio proof check inside the guest.";
|
||||
|
||||
fn require_path(path: &str) -> Result<(), String> {
|
||||
if Path::new(path).exists() {
|
||||
if Path::new(path).exists()
|
||||
|| OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.custom_flags(O_NONBLOCK as i32)
|
||||
.open(path)
|
||||
.is_ok()
|
||||
{
|
||||
println!("present={path}");
|
||||
Ok(())
|
||||
} else {
|
||||
|
||||
+21
-1
@@ -1,4 +1,4 @@
|
||||
use std::path::Path;
|
||||
use std::{env, path::{Path, PathBuf}};
|
||||
use std::process::{self, Command};
|
||||
|
||||
use redbear_hwutils::parse_args;
|
||||
@@ -51,6 +51,25 @@ fn require_wayland_smoke_marker() -> Result<(), String> {
|
||||
Err("qt6-wayland-smoke did not leave a success marker".to_string())
|
||||
}
|
||||
|
||||
fn require_wayland_socket() -> Result<(), String> {
|
||||
let runtime_dir = env::var("XDG_RUNTIME_DIR")
|
||||
.ok()
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "/tmp/run/user/0".to_string());
|
||||
let display = env::var("WAYLAND_DISPLAY")
|
||||
.ok()
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or_else(|| "wayland-0".to_string());
|
||||
let socket = PathBuf::from(runtime_dir).join(display);
|
||||
|
||||
if socket.exists() {
|
||||
println!("{}", socket.display());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("missing Wayland socket {}", socket.display()))
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
|
||||
if err.is_empty() {
|
||||
@@ -66,6 +85,7 @@ fn run() -> Result<(), String> {
|
||||
require_path("/usr/bin/qt6-plugin-check")?;
|
||||
require_path("/usr/bin/qt6-wayland-smoke")?;
|
||||
require_path("/home/root/.wayland-session.started")?;
|
||||
require_wayland_socket()?;
|
||||
require_wayland_smoke_marker()?;
|
||||
|
||||
let status = Command::new("redbear-info")
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
[source]
|
||||
path = "source"
|
||||
|
||||
[build]
|
||||
template = "cargo"
|
||||
|
||||
[package.files]
|
||||
"/usr/bin/redbear-session-launch" = "redbear-session-launch"
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "redbear-session-launch"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
name = "redbear-session-launch"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
libc = "0.2"
|
||||
@@ -0,0 +1,536 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
env,
|
||||
ffi::CString,
|
||||
fs,
|
||||
io,
|
||||
os::unix::process::CommandExt,
|
||||
path::{Path, PathBuf},
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct Account {
|
||||
username: String,
|
||||
uid: u32,
|
||||
gid: u32,
|
||||
home: String,
|
||||
shell: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct GroupEntry {
|
||||
gid: u32,
|
||||
members: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum LaunchMode {
|
||||
Session,
|
||||
Command { program: String, args: Vec<String> },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct Args {
|
||||
username: String,
|
||||
vt: u32,
|
||||
session: String,
|
||||
runtime_dir: Option<PathBuf>,
|
||||
wayland_display: String,
|
||||
mode: LaunchMode,
|
||||
}
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"Usage: redbear-session-launch --username USER [--mode session|command] [--session kde-wayland] [--vt N] [--runtime-dir PATH] [--wayland-display NAME] [--command PROGRAM [ARGS...]]"
|
||||
}
|
||||
|
||||
fn parse_args_from<I>(args: I) -> Result<Args, String>
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let mut args = args.into_iter();
|
||||
let mut username = None;
|
||||
let mut vt = 3_u32;
|
||||
let mut session = String::from("kde-wayland");
|
||||
let mut runtime_dir = None;
|
||||
let mut wayland_display = String::from("wayland-0");
|
||||
let mut mode = String::from("session");
|
||||
let mut command = None;
|
||||
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"--help" | "-h" => return Err(String::new()),
|
||||
"--username" => username = Some(args.next().ok_or_else(|| String::from("missing value after --username"))?),
|
||||
"--vt" => {
|
||||
let value = args.next().ok_or_else(|| String::from("missing value after --vt"))?;
|
||||
vt = value.parse().map_err(|_| format!("invalid VT '{value}'"))?;
|
||||
}
|
||||
"--session" => session = args.next().ok_or_else(|| String::from("missing value after --session"))?,
|
||||
"--runtime-dir" => {
|
||||
runtime_dir = Some(PathBuf::from(
|
||||
args.next().ok_or_else(|| String::from("missing value after --runtime-dir"))?,
|
||||
));
|
||||
}
|
||||
"--wayland-display" => {
|
||||
wayland_display = args
|
||||
.next()
|
||||
.ok_or_else(|| String::from("missing value after --wayland-display"))?;
|
||||
}
|
||||
"--mode" => mode = args.next().ok_or_else(|| String::from("missing value after --mode"))?,
|
||||
"--command" => {
|
||||
let program = args.next().ok_or_else(|| String::from("missing program after --command"))?;
|
||||
let rest = args.collect::<Vec<_>>();
|
||||
command = Some((program, rest));
|
||||
break;
|
||||
}
|
||||
other => return Err(format!("unrecognized argument '{other}'")),
|
||||
}
|
||||
}
|
||||
|
||||
let username = username.ok_or_else(|| String::from("--username is required"))?;
|
||||
let mode = match mode.as_str() {
|
||||
"session" => LaunchMode::Session,
|
||||
"command" => {
|
||||
let (program, args) = command.ok_or_else(|| String::from("--command is required when --mode=command"))?;
|
||||
LaunchMode::Command { program, args }
|
||||
}
|
||||
other => return Err(format!("unsupported launch mode '{other}'")),
|
||||
};
|
||||
|
||||
Ok(Args {
|
||||
username,
|
||||
vt,
|
||||
session,
|
||||
runtime_dir,
|
||||
wayland_display,
|
||||
mode,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_args() -> Result<Args, String> {
|
||||
parse_args_from(env::args().skip(1))
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum AccountFormat {
|
||||
Redox,
|
||||
Unix,
|
||||
}
|
||||
|
||||
fn split_account_fields(line: &str) -> (AccountFormat, Vec<&str>) {
|
||||
let format = if line.contains(';') {
|
||||
AccountFormat::Redox
|
||||
} else {
|
||||
AccountFormat::Unix
|
||||
};
|
||||
let delimiter = match format {
|
||||
AccountFormat::Redox => ';',
|
||||
AccountFormat::Unix => ':',
|
||||
};
|
||||
(format, line.split(delimiter).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
fn parse_passwd(contents: &str) -> Result<HashMap<String, Account>, String> {
|
||||
let mut accounts = HashMap::new();
|
||||
|
||||
for (index, raw_line) in contents.lines().enumerate() {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (format, parts) = split_account_fields(line);
|
||||
let (uid_index, gid_index, home_index, shell_index) = match format {
|
||||
AccountFormat::Redox if parts.len() >= 6 => (1, 2, 4, 5),
|
||||
AccountFormat::Unix if parts.len() >= 7 => (2, 3, 5, 6),
|
||||
AccountFormat::Redox => return Err(format!("invalid Redox passwd entry on line {}", index + 1)),
|
||||
AccountFormat::Unix => return Err(format!("invalid passwd entry on line {}", index + 1)),
|
||||
};
|
||||
|
||||
let uid = parts[uid_index]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| format!("invalid uid on line {}", index + 1))?;
|
||||
let gid = parts[gid_index]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| format!("invalid gid on line {}", index + 1))?;
|
||||
|
||||
accounts.insert(
|
||||
parts[0].to_string(),
|
||||
Account {
|
||||
username: parts[0].to_string(),
|
||||
uid,
|
||||
gid,
|
||||
home: parts[home_index].to_string(),
|
||||
shell: parts[shell_index].to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(accounts)
|
||||
}
|
||||
|
||||
fn parse_groups(contents: &str) -> Result<Vec<GroupEntry>, String> {
|
||||
let mut groups = Vec::new();
|
||||
|
||||
for (index, raw_line) in contents.lines().enumerate() {
|
||||
let line = raw_line.trim();
|
||||
if line.is_empty() || line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (_format, parts) = split_account_fields(line);
|
||||
if parts.len() < 4 {
|
||||
return Err(format!("invalid group entry on line {}", index + 1));
|
||||
}
|
||||
|
||||
let gid = parts[2]
|
||||
.parse::<u32>()
|
||||
.map_err(|_| format!("invalid group gid on line {}", index + 1))?;
|
||||
let members = if parts[3].is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
parts[3].split(',').map(str::to_string).collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
groups.push(GroupEntry { gid, members });
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
fn load_account(username: &str) -> Result<Account, String> {
|
||||
let passwd = fs::read_to_string("/etc/passwd").map_err(|err| format!("failed to read /etc/passwd: {err}"))?;
|
||||
let accounts = parse_passwd(&passwd)?;
|
||||
accounts
|
||||
.get(username)
|
||||
.cloned()
|
||||
.ok_or_else(|| format!("unknown user '{username}'"))
|
||||
}
|
||||
|
||||
fn load_supplementary_groups(username: &str, primary_gid: u32) -> Result<Vec<u32>, String> {
|
||||
let Ok(group_contents) = fs::read_to_string("/etc/group") else {
|
||||
return Ok(vec![primary_gid]);
|
||||
};
|
||||
|
||||
let mut groups = parse_groups(&group_contents)?
|
||||
.into_iter()
|
||||
.filter(|entry| entry.gid == primary_gid || entry.members.iter().any(|member| member == username))
|
||||
.map(|entry| entry.gid)
|
||||
.collect::<Vec<_>>();
|
||||
groups.sort_unstable();
|
||||
groups.dedup();
|
||||
if groups.is_empty() {
|
||||
groups.push(primary_gid);
|
||||
}
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
fn default_runtime_dir(uid: u32) -> PathBuf {
|
||||
if Path::new("/run/user").exists() {
|
||||
PathBuf::from(format!("/run/user/{uid}"))
|
||||
} else {
|
||||
PathBuf::from(format!("/tmp/run/user/{uid}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_runtime_dir(path: &Path, uid: u32, gid: u32) -> Result<(), String> {
|
||||
fs::create_dir_all(path).map_err(|err| format!("failed to create runtime dir {}: {err}", path.display()))?;
|
||||
let c_path = CString::new(path.as_os_str().as_encoded_bytes())
|
||||
.map_err(|_| format!("runtime dir {} contains interior NUL", path.display()))?;
|
||||
let result = unsafe { libc::chown(c_path.as_ptr(), uid, gid) };
|
||||
if result != 0 {
|
||||
return Err(format!("failed to chown runtime dir {}: {}", path.display(), io::Error::last_os_error()));
|
||||
}
|
||||
fs::set_permissions(path, std::os::unix::fs::PermissionsExt::from_mode(0o700))
|
||||
.map_err(|err| format!("failed to set runtime dir permissions on {}: {err}", path.display()))
|
||||
}
|
||||
|
||||
fn env_value(keys: &[&str]) -> Option<String> {
|
||||
keys.iter().find_map(|key| env::var(key).ok())
|
||||
}
|
||||
|
||||
fn build_environment(account: &Account, args: &Args, runtime_dir: &Path) -> BTreeMap<String, String> {
|
||||
let mut values = BTreeMap::new();
|
||||
values.insert(String::from("HOME"), account.home.clone());
|
||||
values.insert(String::from("USER"), account.username.clone());
|
||||
values.insert(String::from("LOGNAME"), account.username.clone());
|
||||
values.insert(String::from("SHELL"), account.shell.clone());
|
||||
values.insert(String::from("PATH"), String::from("/usr/bin:/bin"));
|
||||
values.insert(String::from("XDG_RUNTIME_DIR"), runtime_dir.display().to_string());
|
||||
values.insert(String::from("WAYLAND_DISPLAY"), args.wayland_display.clone());
|
||||
values.insert(String::from("XDG_SEAT"), String::from("seat0"));
|
||||
values.insert(String::from("XDG_VTNR"), args.vt.to_string());
|
||||
values.insert(String::from("LIBSEAT_BACKEND"), String::from("seatd"));
|
||||
values.insert(String::from("SEATD_SOCK"), String::from("/run/seatd.sock"));
|
||||
values.insert(String::from("DISPLAY"), String::new());
|
||||
values.insert(String::from("XDG_SESSION_TYPE"), String::from("wayland"));
|
||||
|
||||
if let Some(theme) = env_value(&["XCURSOR_THEME"]) {
|
||||
values.insert(String::from("XCURSOR_THEME"), theme);
|
||||
}
|
||||
if let Some(root) = env_value(&["XKB_CONFIG_ROOT"]) {
|
||||
values.insert(String::from("XKB_CONFIG_ROOT"), root);
|
||||
}
|
||||
if let Some(path) = env_value(&["QT_PLUGIN_PATH"]) {
|
||||
values.insert(String::from("QT_PLUGIN_PATH"), path);
|
||||
}
|
||||
if let Some(path) = env_value(&["QT_QPA_PLATFORM_PLUGIN_PATH"]) {
|
||||
values.insert(String::from("QT_QPA_PLATFORM_PLUGIN_PATH"), path);
|
||||
}
|
||||
if let Some(path) = env_value(&["QML2_IMPORT_PATH"]) {
|
||||
values.insert(String::from("QML2_IMPORT_PATH"), path);
|
||||
}
|
||||
|
||||
match args.mode {
|
||||
LaunchMode::Session => {
|
||||
values.insert(String::from("XDG_CURRENT_DESKTOP"), String::from("KDE"));
|
||||
values.insert(String::from("KDE_FULL_SESSION"), String::from("true"));
|
||||
}
|
||||
LaunchMode::Command { .. } => {}
|
||||
}
|
||||
|
||||
values
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn apply_groups(groups: &[u32]) -> io::Result<()> {
|
||||
let raw_groups = groups.iter().map(|gid| *gid as libc::gid_t).collect::<Vec<_>>();
|
||||
let result = unsafe { libc::setgroups(raw_groups.len(), raw_groups.as_ptr()) };
|
||||
if result == 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn apply_groups(_groups: &[u32]) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command_for(args: &Args) -> Result<(String, Vec<String>), String> {
|
||||
match &args.mode {
|
||||
LaunchMode::Session => {
|
||||
if args.session != "kde-wayland" {
|
||||
return Err(format!("unsupported session '{}'", args.session));
|
||||
}
|
||||
|
||||
if Path::new("/usr/bin/dbus-run-session").exists() {
|
||||
Ok((
|
||||
String::from("/usr/bin/dbus-run-session"),
|
||||
vec![String::from("--"), String::from("/usr/bin/redbear-kde-session")],
|
||||
))
|
||||
} else {
|
||||
Ok((String::from("/usr/bin/redbear-kde-session"), Vec::new()))
|
||||
}
|
||||
}
|
||||
LaunchMode::Command { program, args } => Ok((program.clone(), args.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
let args = match parse_args() {
|
||||
Ok(parsed) => parsed,
|
||||
Err(err) if err.is_empty() => {
|
||||
println!("{}", usage());
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
let account = load_account(&args.username)?;
|
||||
let groups = load_supplementary_groups(&account.username, account.gid)?;
|
||||
let runtime_dir = args
|
||||
.runtime_dir
|
||||
.clone()
|
||||
.unwrap_or_else(|| default_runtime_dir(account.uid));
|
||||
ensure_runtime_dir(&runtime_dir, account.uid, account.gid)?;
|
||||
let envs = build_environment(&account, &args, &runtime_dir);
|
||||
let (program, program_args) = command_for(&args)?;
|
||||
|
||||
let group_clone = groups.clone();
|
||||
let mut command = Command::new(&program);
|
||||
command.args(&program_args);
|
||||
command.env_clear();
|
||||
command.envs(&envs);
|
||||
command.uid(account.uid);
|
||||
command.gid(account.gid);
|
||||
unsafe {
|
||||
command.pre_exec(move || apply_groups(&group_clone));
|
||||
}
|
||||
|
||||
let error = command.exec();
|
||||
Err(format!("failed to exec {program}: {error}"))
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("redbear-session-launch: {err}");
|
||||
eprintln!("{}", usage());
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_args_accepts_command_mode() {
|
||||
let parsed = parse_args_from(vec![
|
||||
String::from("--username"),
|
||||
String::from("greeter"),
|
||||
String::from("--mode"),
|
||||
String::from("command"),
|
||||
String::from("--vt"),
|
||||
String::from("7"),
|
||||
String::from("--runtime-dir"),
|
||||
String::from("/tmp/greeter"),
|
||||
String::from("--wayland-display"),
|
||||
String::from("wayland-7"),
|
||||
String::from("--command"),
|
||||
String::from("/usr/bin/redbear-greeter-ui"),
|
||||
String::from("--fullscreen"),
|
||||
])
|
||||
.expect("command mode should parse");
|
||||
|
||||
assert_eq!(parsed.username, "greeter");
|
||||
assert_eq!(parsed.vt, 7);
|
||||
assert_eq!(parsed.runtime_dir, Some(PathBuf::from("/tmp/greeter")));
|
||||
assert_eq!(parsed.wayland_display, "wayland-7");
|
||||
assert_eq!(
|
||||
parsed.mode,
|
||||
LaunchMode::Command {
|
||||
program: String::from("/usr/bin/redbear-greeter-ui"),
|
||||
args: vec![String::from("--fullscreen")],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_requires_command_when_mode_is_command() {
|
||||
assert_eq!(
|
||||
parse_args_from(vec![
|
||||
String::from("--username"),
|
||||
String::from("greeter"),
|
||||
String::from("--mode"),
|
||||
String::from("command"),
|
||||
]),
|
||||
Err(String::from("--command is required when --mode=command"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args_rejects_unknown_mode() {
|
||||
assert_eq!(
|
||||
parse_args_from(vec![
|
||||
String::from("--username"),
|
||||
String::from("user"),
|
||||
String::from("--mode"),
|
||||
String::from("bogus"),
|
||||
]),
|
||||
Err(String::from("unsupported launch mode 'bogus'"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_passwd_accepts_basic_entries() {
|
||||
let accounts = parse_passwd("root:x:0:0:root:/root:/usr/bin/ion\nuser:x:1000:1000:User:/home/user:/usr/bin/ion\n")
|
||||
.expect("passwd should parse");
|
||||
assert_eq!(accounts["root"].uid, 0);
|
||||
assert_eq!(accounts["user"].home, "/home/user");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_passwd_accepts_redox_style_layout() {
|
||||
let accounts = parse_passwd("greeter;101;101;Greeter;/nonexistent;/usr/bin/ion\n")
|
||||
.expect("redox passwd layout should parse");
|
||||
let greeter = accounts.get("greeter").expect("greeter entry should exist");
|
||||
assert_eq!(greeter.uid, 101);
|
||||
assert_eq!(greeter.gid, 101);
|
||||
assert_eq!(greeter.home, "/nonexistent");
|
||||
assert_eq!(greeter.shell, "/usr/bin/ion");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_groups_collects_members() {
|
||||
let groups = parse_groups("sudo:x:1:user,root\nusers:x:1000:user\n").expect("group should parse");
|
||||
assert_eq!(groups[0].gid, 1);
|
||||
assert_eq!(groups[0].members, vec![String::from("user"), String::from("root")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_groups_accepts_redox_style_layout() {
|
||||
let groups = parse_groups("greeter;x;101;greeter\n").expect("redox group should parse");
|
||||
assert_eq!(groups[0].gid, 101);
|
||||
assert_eq!(groups[0].members, vec![String::from("greeter")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_environment_sets_kde_session_values() {
|
||||
let account = Account {
|
||||
username: String::from("user"),
|
||||
uid: 1000,
|
||||
gid: 1000,
|
||||
home: String::from("/home/user"),
|
||||
shell: String::from("/usr/bin/ion"),
|
||||
};
|
||||
let args = Args {
|
||||
username: String::from("user"),
|
||||
vt: 3,
|
||||
session: String::from("kde-wayland"),
|
||||
runtime_dir: None,
|
||||
wayland_display: String::from("wayland-0"),
|
||||
mode: LaunchMode::Session,
|
||||
};
|
||||
|
||||
let envs = build_environment(&account, &args, Path::new("/run/user/1000"));
|
||||
assert_eq!(envs["XDG_CURRENT_DESKTOP"], "KDE");
|
||||
assert_eq!(envs["KDE_FULL_SESSION"], "true");
|
||||
assert_eq!(envs["XDG_VTNR"], "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_environment_omits_kde_session_values_for_command_mode() {
|
||||
let account = Account {
|
||||
username: String::from("greeter"),
|
||||
uid: 101,
|
||||
gid: 101,
|
||||
home: String::from("/nonexistent"),
|
||||
shell: String::from("/usr/bin/ion"),
|
||||
};
|
||||
let args = Args {
|
||||
username: String::from("greeter"),
|
||||
vt: 3,
|
||||
session: String::from("kde-wayland"),
|
||||
runtime_dir: None,
|
||||
wayland_display: String::from("wayland-0"),
|
||||
mode: LaunchMode::Command {
|
||||
program: String::from("/usr/bin/redbear-greeter-ui"),
|
||||
args: Vec::new(),
|
||||
},
|
||||
};
|
||||
|
||||
let envs = build_environment(&account, &args, Path::new("/tmp/run/greeter"));
|
||||
assert!(!envs.contains_key("XDG_CURRENT_DESKTOP"));
|
||||
assert!(!envs.contains_key("KDE_FULL_SESSION"));
|
||||
assert_eq!(envs["XDG_SESSION_TYPE"], "wayland");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_for_rejects_unknown_session_name() {
|
||||
let args = Args {
|
||||
username: String::from("user"),
|
||||
vt: 3,
|
||||
session: String::from("plasma-x11"),
|
||||
runtime_dir: None,
|
||||
wayland_display: String::from("wayland-0"),
|
||||
mode: LaunchMode::Session,
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
command_for(&args),
|
||||
Err(String::from("unsupported session 'plasma-x11'"))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,6 @@ path = "src/main.rs"
|
||||
zbus = { version = "5", default-features = false, features = ["tokio"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
libredox = "0.1"
|
||||
redox-syscall = { package = "redox_syscall", version = "0.7" }
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{BufRead, BufReader},
|
||||
os::unix::{fs::PermissionsExt, net::UnixListener},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::runtime_state::SharedRuntime;
|
||||
|
||||
pub const CONTROL_SOCKET_PATH: &str = "/run/redbear-sessiond-control.sock";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum ControlMessage {
|
||||
SetSession {
|
||||
username: String,
|
||||
uid: u32,
|
||||
vt: u32,
|
||||
leader: u32,
|
||||
state: String,
|
||||
},
|
||||
ResetSession {
|
||||
vt: u32,
|
||||
},
|
||||
}
|
||||
|
||||
fn apply_message(runtime: &SharedRuntime, message: ControlMessage) {
|
||||
let Ok(mut runtime) = runtime.write() else {
|
||||
eprintln!("redbear-sessiond: runtime state is poisoned");
|
||||
return;
|
||||
};
|
||||
|
||||
match message {
|
||||
ControlMessage::SetSession {
|
||||
username,
|
||||
uid,
|
||||
vt,
|
||||
leader,
|
||||
state,
|
||||
} => {
|
||||
runtime.username = username;
|
||||
runtime.uid = uid;
|
||||
runtime.vt = vt;
|
||||
runtime.leader = leader;
|
||||
runtime.state = state;
|
||||
runtime.active = true;
|
||||
}
|
||||
ControlMessage::ResetSession { vt } => {
|
||||
runtime.username = String::from("root");
|
||||
runtime.uid = 0;
|
||||
runtime.vt = vt;
|
||||
runtime.leader = std::process::id();
|
||||
runtime.state = String::from("closing");
|
||||
runtime.active = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_control_socket(runtime: SharedRuntime) {
|
||||
std::thread::spawn(move || {
|
||||
if Path::new(CONTROL_SOCKET_PATH).exists() {
|
||||
if let Err(err) = fs::remove_file(CONTROL_SOCKET_PATH) {
|
||||
eprintln!("redbear-sessiond: failed to remove stale control socket: {err}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let listener = match UnixListener::bind(CONTROL_SOCKET_PATH) {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
eprintln!("redbear-sessiond: failed to bind control socket: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = fs::set_permissions(CONTROL_SOCKET_PATH, fs::Permissions::from_mode(0o600)) {
|
||||
eprintln!("redbear-sessiond: failed to chmod control socket: {err}");
|
||||
}
|
||||
|
||||
for stream in listener.incoming() {
|
||||
let Ok(stream) = stream else {
|
||||
continue;
|
||||
};
|
||||
let mut reader = BufReader::new(stream);
|
||||
let mut line = String::new();
|
||||
if reader.read_line(&mut line).is_err() {
|
||||
continue;
|
||||
}
|
||||
match serde_json::from_str::<ControlMessage>(line.trim()) {
|
||||
Ok(message) => apply_message(&runtime, message),
|
||||
Err(err) => eprintln!("redbear-sessiond: invalid control message: {err}"),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime_state::shared_runtime;
|
||||
|
||||
#[test]
|
||||
fn set_session_message_updates_runtime_state() {
|
||||
let runtime = shared_runtime();
|
||||
|
||||
apply_message(
|
||||
&runtime,
|
||||
ControlMessage::SetSession {
|
||||
username: String::from("user"),
|
||||
uid: 1000,
|
||||
vt: 7,
|
||||
leader: 4242,
|
||||
state: String::from("active"),
|
||||
},
|
||||
);
|
||||
|
||||
let runtime = runtime.read().expect("runtime lock should remain healthy");
|
||||
assert_eq!(runtime.username, "user");
|
||||
assert_eq!(runtime.uid, 1000);
|
||||
assert_eq!(runtime.vt, 7);
|
||||
assert_eq!(runtime.leader, 4242);
|
||||
assert_eq!(runtime.state, "active");
|
||||
assert!(runtime.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_session_message_restores_root_scaffold() {
|
||||
let runtime = shared_runtime();
|
||||
|
||||
apply_message(
|
||||
&runtime,
|
||||
ControlMessage::SetSession {
|
||||
username: String::from("user"),
|
||||
uid: 1000,
|
||||
vt: 7,
|
||||
leader: 4242,
|
||||
state: String::from("active"),
|
||||
},
|
||||
);
|
||||
apply_message(&runtime, ControlMessage::ResetSession { vt: 3 });
|
||||
|
||||
let runtime = runtime.read().expect("runtime lock should remain healthy");
|
||||
assert_eq!(runtime.username, "root");
|
||||
assert_eq!(runtime.uid, 0);
|
||||
assert_eq!(runtime.vt, 3);
|
||||
assert_eq!(runtime.state, "closing");
|
||||
assert!(runtime.active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_message_json_matches_expected_shape() {
|
||||
let message = serde_json::from_str::<ControlMessage>(
|
||||
r#"{"type":"set_session","username":"user","uid":1000,"vt":3,"leader":99,"state":"online"}"#,
|
||||
)
|
||||
.expect("control message json should parse");
|
||||
|
||||
match message {
|
||||
ControlMessage::SetSession {
|
||||
username,
|
||||
uid,
|
||||
vt,
|
||||
leader,
|
||||
state,
|
||||
} => {
|
||||
assert_eq!(username, "user");
|
||||
assert_eq!(uid, 1000);
|
||||
assert_eq!(vt, 3);
|
||||
assert_eq!(leader, 99);
|
||||
assert_eq!(state, "online");
|
||||
}
|
||||
ControlMessage::ResetSession { .. } => panic!("expected set_session message"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,12 @@
|
||||
use std::{collections::HashMap, fs::File, io};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{self, File, OpenOptions},
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DeviceMap {
|
||||
@@ -28,13 +36,11 @@ impl DeviceMap {
|
||||
return Some(path.clone());
|
||||
}
|
||||
|
||||
match (major, minor) {
|
||||
(13, minor) if minor >= 68 => Some(format!("/dev/input/event{}", minor - 64)),
|
||||
_ => None,
|
||||
}
|
||||
self.find_dynamic_path(major, minor)
|
||||
.or_else(|| self.fallback_path(major, minor))
|
||||
}
|
||||
|
||||
pub fn open_device(&self, major: u32, minor: u32) -> io::Result<File> {
|
||||
pub fn open_device(&self, major: u32, minor: u32) -> io::Result<(String, File)> {
|
||||
let Some(path) = self.resolve(major, minor) else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
@@ -42,6 +48,118 @@ impl DeviceMap {
|
||||
));
|
||||
};
|
||||
|
||||
File::open(path)
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(&path)
|
||||
.or_else(|_| OpenOptions::new().read(true).open(&path))
|
||||
.or_else(|_| OpenOptions::new().write(true).open(&path))?;
|
||||
|
||||
Ok((path, file))
|
||||
}
|
||||
|
||||
fn fallback_path(&self, major: u32, minor: u32) -> Option<String> {
|
||||
match (major, minor) {
|
||||
(13, minor) if minor >= 64 => {
|
||||
let path = format!("/dev/input/event{}", minor - 64);
|
||||
Path::new(&path).exists().then_some(path)
|
||||
}
|
||||
(226, minor) => {
|
||||
let path = format!("/scheme/drm/card{minor}");
|
||||
Path::new(&path).exists().then_some(path)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_dynamic_path(&self, major: u32, minor: u32) -> Option<String> {
|
||||
for path in candidate_paths() {
|
||||
if path_matches_device(&path, major, minor) {
|
||||
return Some(path.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn candidate_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
paths.extend(read_dir_paths("/dev/input", |name| name.starts_with("event")));
|
||||
paths.extend(read_dir_paths("/scheme/drm", |name| name.starts_with("card")));
|
||||
|
||||
for direct in ["/dev/fb0", "/scheme/null", "/scheme/zero", "/scheme/rand"] {
|
||||
let path = PathBuf::from(direct);
|
||||
if path.exists() {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
fn read_dir_paths(dir: &str, include: impl Fn(&str) -> bool) -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
let Ok(entries) = fs::read_dir(dir) else {
|
||||
return paths;
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if include(name) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
paths.sort();
|
||||
paths
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn path_matches_device(path: &Path, major: u32, minor: u32) -> bool {
|
||||
let Ok(metadata) = fs::metadata(path) else {
|
||||
return false;
|
||||
};
|
||||
let rdev = metadata.rdev();
|
||||
dev_major(rdev) == major && dev_minor(rdev) == minor
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn path_matches_device(_path: &Path, _major: u32, _minor: u32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn dev_major(device: u64) -> u32 {
|
||||
(((device >> 31 >> 1) & 0xfffff000) | ((device >> 8) & 0x00000fff)) as u32
|
||||
}
|
||||
|
||||
fn dev_minor(device: u64) -> u32 {
|
||||
(((device >> 12) & 0xffffff00) | (device & 0x000000ff)) as u32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{dev_major, dev_minor};
|
||||
|
||||
fn make_dev(major: u64, minor: u64) -> u64 {
|
||||
((major & 0xfffff000) << 32)
|
||||
| ((major & 0x00000fff) << 8)
|
||||
| ((minor & 0xffffff00) << 12)
|
||||
| (minor & 0x000000ff)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splits_compound_dev_numbers() {
|
||||
let device = make_dev(226, 3);
|
||||
assert_eq!(dev_major(device), 226);
|
||||
assert_eq!(dev_minor(device), 3);
|
||||
|
||||
let event = make_dev(13, 67);
|
||||
assert_eq!(dev_major(event), 13);
|
||||
assert_eq!(dev_minor(event), 67);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
mod acpi_watcher;
|
||||
mod control;
|
||||
mod device_map;
|
||||
mod manager;
|
||||
mod runtime_state;
|
||||
mod seat;
|
||||
mod session;
|
||||
|
||||
@@ -15,6 +17,7 @@ use device_map::DeviceMap;
|
||||
use manager::LoginManager;
|
||||
use seat::LoginSeat;
|
||||
use session::LoginSession;
|
||||
use runtime_state::shared_runtime;
|
||||
use tokio::runtime::Builder as RuntimeBuilder;
|
||||
use zbus::{
|
||||
Address,
|
||||
@@ -26,7 +29,7 @@ const BUS_NAME: &str = "org.freedesktop.login1";
|
||||
const MANAGER_PATH: &str = "/org/freedesktop/login1";
|
||||
const SESSION_PATH: &str = "/org/freedesktop/login1/session/c1";
|
||||
const SEAT_PATH: &str = "/org/freedesktop/login1/seat/seat0";
|
||||
const USER_PATH: &str = "/org/freedesktop/login1/user/0";
|
||||
const USER_PATH: &str = "/org/freedesktop/login1/user/current";
|
||||
|
||||
enum Command {
|
||||
Run,
|
||||
@@ -113,10 +116,11 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
|
||||
let session_path = parse_object_path(SESSION_PATH)?;
|
||||
let seat_path = parse_object_path(SEAT_PATH)?;
|
||||
let user_path = parse_object_path(USER_PATH)?;
|
||||
let runtime = shared_runtime();
|
||||
|
||||
let session = LoginSession::new(seat_path.clone(), user_path, DeviceMap::new());
|
||||
let seat = LoginSeat::new(session_path.clone());
|
||||
let manager = LoginManager::new(session_path, seat_path);
|
||||
let session = LoginSession::new(seat_path.clone(), user_path, DeviceMap::new(), runtime.clone());
|
||||
let seat = LoginSeat::new(session_path.clone(), runtime.clone());
|
||||
let manager = LoginManager::new(session_path, seat_path, runtime.clone());
|
||||
|
||||
match system_connection_builder()?
|
||||
.name(BUS_NAME)?
|
||||
@@ -128,6 +132,7 @@ async fn run_daemon() -> Result<(), Box<dyn Error>> {
|
||||
{
|
||||
Ok(connection) => {
|
||||
eprintln!("redbear-sessiond: registered {BUS_NAME} on the system bus");
|
||||
control::start_control_socket(runtime.clone());
|
||||
tokio::spawn(acpi_watcher::watch_and_emit(connection.clone()));
|
||||
wait_for_shutdown().await?;
|
||||
drop(connection);
|
||||
|
||||
@@ -5,20 +5,20 @@ use zbus::{
|
||||
zvariant::OwnedObjectPath,
|
||||
};
|
||||
|
||||
use crate::runtime_state::SharedRuntime;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LoginManager {
|
||||
session_id: String,
|
||||
runtime: SharedRuntime,
|
||||
session_path: OwnedObjectPath,
|
||||
seat_id: String,
|
||||
seat_path: OwnedObjectPath,
|
||||
}
|
||||
|
||||
impl LoginManager {
|
||||
pub fn new(session_path: OwnedObjectPath, seat_path: OwnedObjectPath) -> Self {
|
||||
pub fn new(session_path: OwnedObjectPath, seat_path: OwnedObjectPath, runtime: SharedRuntime) -> Self {
|
||||
Self {
|
||||
session_id: String::from("c1"),
|
||||
runtime,
|
||||
session_path,
|
||||
seat_id: String::from("seat0"),
|
||||
seat_path,
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,11 @@ impl LoginManager {
|
||||
#[interface(name = "org.freedesktop.login1.Manager")]
|
||||
impl LoginManager {
|
||||
fn get_session(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
|
||||
if id == self.session_id {
|
||||
let runtime = self
|
||||
.runtime
|
||||
.read()
|
||||
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
|
||||
if id == runtime.session_id {
|
||||
return Ok(self.session_path.clone());
|
||||
}
|
||||
|
||||
@@ -35,17 +39,25 @@ impl LoginManager {
|
||||
}
|
||||
|
||||
fn list_sessions(&self) -> fdo::Result<Vec<(String, u32, String, String, OwnedObjectPath)>> {
|
||||
let runtime = self
|
||||
.runtime
|
||||
.read()
|
||||
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
|
||||
Ok(vec![(
|
||||
self.session_id.clone(),
|
||||
0,
|
||||
String::from("root"),
|
||||
self.seat_id.clone(),
|
||||
runtime.session_id.clone(),
|
||||
runtime.uid,
|
||||
runtime.username.clone(),
|
||||
runtime.seat_id.clone(),
|
||||
self.session_path.clone(),
|
||||
)])
|
||||
}
|
||||
|
||||
fn get_seat(&self, id: &str) -> fdo::Result<OwnedObjectPath> {
|
||||
if id == self.seat_id {
|
||||
let runtime = self
|
||||
.runtime
|
||||
.read()
|
||||
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))?;
|
||||
if id == runtime.seat_id {
|
||||
return Ok(self.seat_path.clone());
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SessionRuntime {
|
||||
pub session_id: String,
|
||||
pub seat_id: String,
|
||||
pub username: String,
|
||||
pub uid: u32,
|
||||
pub vt: u32,
|
||||
pub leader: u32,
|
||||
pub state: String,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
impl Default for SessionRuntime {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
session_id: String::from("c1"),
|
||||
seat_id: String::from("seat0"),
|
||||
username: String::from("root"),
|
||||
uid: 0,
|
||||
vt: 3,
|
||||
leader: std::process::id(),
|
||||
state: String::from("online"),
|
||||
active: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SharedRuntime = Arc<RwLock<SessionRuntime>>;
|
||||
|
||||
pub fn shared_runtime() -> SharedRuntime {
|
||||
Arc::new(RwLock::new(SessionRuntime::default()))
|
||||
}
|
||||
@@ -2,20 +2,22 @@ use std::sync::Mutex;
|
||||
|
||||
use zbus::{fdo, interface, zvariant::OwnedObjectPath};
|
||||
|
||||
use crate::runtime_state::SharedRuntime;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoginSeat {
|
||||
id: String,
|
||||
session_id: String,
|
||||
session_path: OwnedObjectPath,
|
||||
runtime: SharedRuntime,
|
||||
last_requested_vt: Mutex<u32>,
|
||||
}
|
||||
|
||||
impl LoginSeat {
|
||||
pub fn new(session_path: OwnedObjectPath) -> Self {
|
||||
pub fn new(session_path: OwnedObjectPath, runtime: SharedRuntime) -> Self {
|
||||
Self {
|
||||
id: String::from("seat0"),
|
||||
session_id: String::from("c1"),
|
||||
session_path,
|
||||
runtime,
|
||||
last_requested_vt: Mutex::new(1),
|
||||
}
|
||||
}
|
||||
@@ -46,12 +48,24 @@ impl LoginSeat {
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "ActiveSession")]
|
||||
fn active_session(&self) -> (String, OwnedObjectPath) {
|
||||
(self.session_id.clone(), self.session_path.clone())
|
||||
(
|
||||
self.runtime
|
||||
.read()
|
||||
.map(|runtime| runtime.session_id.clone())
|
||||
.unwrap_or_else(|_| String::from("c1")),
|
||||
self.session_path.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "Sessions")]
|
||||
fn sessions(&self) -> Vec<(String, OwnedObjectPath)> {
|
||||
vec![(self.session_id.clone(), self.session_path.clone())]
|
||||
vec![(
|
||||
self.runtime
|
||||
.read()
|
||||
.map(|runtime| runtime.session_id.clone())
|
||||
.unwrap_or_else(|_| String::from("c1")),
|
||||
self.session_path.clone(),
|
||||
)]
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "CanGraphical")]
|
||||
|
||||
@@ -13,16 +13,14 @@ use zbus::{
|
||||
};
|
||||
|
||||
use crate::device_map::DeviceMap;
|
||||
use crate::runtime_state::SharedRuntime;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoginSession {
|
||||
id: String,
|
||||
seat_id: String,
|
||||
seat_path: OwnedObjectPath,
|
||||
user_uid: u32,
|
||||
user_path: OwnedObjectPath,
|
||||
leader: u32,
|
||||
device_map: DeviceMap,
|
||||
runtime: SharedRuntime,
|
||||
controlled: Mutex<bool>,
|
||||
taken_devices: Mutex<HashSet<(u32, u32)>>,
|
||||
}
|
||||
@@ -32,15 +30,13 @@ impl LoginSession {
|
||||
seat_path: OwnedObjectPath,
|
||||
user_path: OwnedObjectPath,
|
||||
device_map: DeviceMap,
|
||||
runtime: SharedRuntime,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: String::from("c1"),
|
||||
seat_id: String::from("seat0"),
|
||||
seat_path,
|
||||
user_uid: 0,
|
||||
user_path,
|
||||
leader: process::id(),
|
||||
device_map,
|
||||
runtime,
|
||||
controlled: Mutex::new(false),
|
||||
taken_devices: Mutex::new(HashSet::new()),
|
||||
}
|
||||
@@ -57,21 +53,35 @@ impl LoginSession {
|
||||
.lock()
|
||||
.map_err(|_| fdo::Error::Failed(String::from("login1 device state is poisoned")))
|
||||
}
|
||||
|
||||
fn runtime(&self) -> fdo::Result<crate::runtime_state::SessionRuntime> {
|
||||
self.runtime
|
||||
.read()
|
||||
.map(|runtime| runtime.clone())
|
||||
.map_err(|_| fdo::Error::Failed(String::from("login1 runtime state is poisoned")))
|
||||
}
|
||||
}
|
||||
|
||||
#[interface(name = "org.freedesktop.login1.Session")]
|
||||
impl LoginSession {
|
||||
fn activate(&self) -> fdo::Result<()> {
|
||||
eprintln!("redbear-sessiond: Activate requested for session {}", self.id);
|
||||
eprintln!("redbear-sessiond: Activate requested for session {}", self.runtime()?.session_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_control(&self, force: bool) -> fdo::Result<()> {
|
||||
let mut controlled = self.control_state()?;
|
||||
let runtime = self.runtime()?;
|
||||
if *controlled && !force {
|
||||
return Err(fdo::Error::Failed(format!(
|
||||
"session {} is already under control",
|
||||
runtime.session_id
|
||||
)));
|
||||
}
|
||||
*controlled = true;
|
||||
eprintln!(
|
||||
"redbear-sessiond: TakeControl requested for session {} (force={force})",
|
||||
self.id
|
||||
runtime.session_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -79,34 +89,56 @@ impl LoginSession {
|
||||
fn release_control(&self) -> fdo::Result<()> {
|
||||
let mut controlled = self.control_state()?;
|
||||
*controlled = false;
|
||||
eprintln!("redbear-sessiond: ReleaseControl requested for session {}", self.id);
|
||||
self.taken_devices()?.clear();
|
||||
eprintln!("redbear-sessiond: ReleaseControl requested for session {}", self.runtime()?.session_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn take_device(&self, major: u32, minor: u32) -> fdo::Result<OwnedFd> {
|
||||
let file = self
|
||||
let runtime = self.runtime()?;
|
||||
if !*self.control_state()? {
|
||||
return Err(fdo::Error::AccessDenied(format!(
|
||||
"session {} must TakeControl before TakeDevice",
|
||||
runtime.session_id
|
||||
)));
|
||||
}
|
||||
|
||||
let mut taken_devices = self.taken_devices()?;
|
||||
if taken_devices.contains(&(major, minor)) {
|
||||
return Err(fdo::Error::Failed(format!(
|
||||
"device ({major}, {minor}) is already taken for session {}",
|
||||
runtime.session_id
|
||||
)));
|
||||
}
|
||||
|
||||
let (path, file) = self
|
||||
.device_map
|
||||
.open_device(major, minor)
|
||||
.map_err(|err| fdo::Error::Failed(format!("TakeDevice({major}, {minor}) failed: {err}")))?;
|
||||
|
||||
let mut taken_devices = self.taken_devices()?;
|
||||
taken_devices.insert((major, minor));
|
||||
|
||||
let owned_fd: StdOwnedFd = file.into();
|
||||
eprintln!(
|
||||
"redbear-sessiond: TakeDevice granted for session {} -> ({major}, {minor})",
|
||||
self.id
|
||||
"redbear-sessiond: TakeDevice granted for session {} -> ({major}, {minor}) at {}",
|
||||
runtime.session_id, path
|
||||
);
|
||||
|
||||
Ok(OwnedFd::from(owned_fd))
|
||||
}
|
||||
|
||||
fn release_device(&self, major: u32, minor: u32) -> fdo::Result<()> {
|
||||
let runtime = self.runtime()?;
|
||||
let mut taken_devices = self.taken_devices()?;
|
||||
taken_devices.remove(&(major, minor));
|
||||
if !taken_devices.remove(&(major, minor)) {
|
||||
return Err(fdo::Error::Failed(format!(
|
||||
"device ({major}, {minor}) was not taken for session {}",
|
||||
runtime.session_id
|
||||
)));
|
||||
}
|
||||
eprintln!(
|
||||
"redbear-sessiond: ReleaseDevice requested for session {} -> ({major}, {minor})",
|
||||
self.id
|
||||
runtime.session_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -114,14 +146,14 @@ impl LoginSession {
|
||||
fn pause_device_complete(&self, major: u32, minor: u32) -> fdo::Result<()> {
|
||||
eprintln!(
|
||||
"redbear-sessiond: PauseDeviceComplete received for session {} -> ({major}, {minor})",
|
||||
self.id
|
||||
self.runtime()?.session_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "Active")]
|
||||
fn active(&self) -> bool {
|
||||
true
|
||||
self.runtime().map(|runtime| runtime.active).unwrap_or(true)
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "Remote")]
|
||||
@@ -156,32 +188,40 @@ impl LoginSession {
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "Id")]
|
||||
fn id(&self) -> String {
|
||||
self.id.clone()
|
||||
self.runtime().map(|runtime| runtime.session_id).unwrap_or_else(|_| String::from("c1"))
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "State")]
|
||||
fn state(&self) -> String {
|
||||
String::from("online")
|
||||
self.runtime().map(|runtime| runtime.state).unwrap_or_else(|_| String::from("online"))
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "Seat")]
|
||||
fn seat(&self) -> (String, OwnedObjectPath) {
|
||||
(self.seat_id.clone(), self.seat_path.clone())
|
||||
(
|
||||
self.runtime()
|
||||
.map(|runtime| runtime.seat_id)
|
||||
.unwrap_or_else(|_| String::from("seat0")),
|
||||
self.seat_path.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "User")]
|
||||
fn user(&self) -> (u32, OwnedObjectPath) {
|
||||
(self.user_uid, self.user_path.clone())
|
||||
(
|
||||
self.runtime().map(|runtime| runtime.uid).unwrap_or(0),
|
||||
self.user_path.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "VTNr")]
|
||||
fn vt_nr(&self) -> u32 {
|
||||
1
|
||||
self.runtime().map(|runtime| runtime.vt).unwrap_or(3)
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "Leader")]
|
||||
fn leader(&self) -> u32 {
|
||||
self.leader
|
||||
self.runtime().map(|runtime| runtime.leader).unwrap_or(process::id())
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "Audit")]
|
||||
@@ -191,7 +231,7 @@ impl LoginSession {
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "TTY")]
|
||||
fn tty(&self) -> String {
|
||||
String::new()
|
||||
format!("tty{}", self.runtime().map(|runtime| runtime.vt).unwrap_or(3))
|
||||
}
|
||||
|
||||
#[zbus(property(emits_changed_signal = "const"), name = "RemoteUser")]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user