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:
2026-04-19 17:59:58 +01:00
parent e778af2103
commit e3d776aa9a
137 changed files with 14176 additions and 2016 deletions
@@ -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(),
+35 -17
View File
@@ -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",
+132 -10
View File
@@ -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
+3 -3
View File
@@ -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)
+26 -5
View File
@@ -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
+17
View File
@@ -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")
+46 -8
View File
@@ -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 {
@@ -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")]
@@ -13,6 +13,7 @@ libredox = { version = "0.1", features = ["call", "std"] }
log = { version = "0.4", features = ["std"] }
redox-scheme = "0.11"
syscall = { package = "redox_syscall", version = "0.7", features = ["std"] }
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source" }
[target.'cfg(target_os = "redox")'.dependencies]
redox-driver-sys = { path = "../../../drivers/redox-driver-sys/source", features = ["redox"] }
@@ -10,10 +10,9 @@ use std::process::Command;
pub(crate) static TEST_ENV_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
use redox_driver_sys::pci::{parse_device_info_from_config_space, PciLocation};
#[cfg(target_os = "redox")]
use redox_driver_sys::pci::PciDevice;
#[cfg(target_os = "redox")]
use redox_driver_sys::pci::PciLocation;
#[derive(Clone, Debug)]
struct ParsedPciLocation {
@@ -862,7 +861,7 @@ fn detect_intel_wifi_interfaces(
device_id,
subsystem_id,
firmware_family,
transport_status: transport_status_from_config(&config),
transport_status: transport_status_from_config(&location, &config),
ucode_candidates,
selected_ucode,
pnvm_candidate,
@@ -945,7 +944,7 @@ fn intel_firmware_candidates(
)
}
fn transport_status_from_config(config: &[u8]) -> String {
fn transport_status_from_config(location: &ParsedPciLocation, config: &[u8]) -> String {
let command = u16::from_le_bytes([config[0x04], config[0x05]]);
let bar0 = u32::from_le_bytes([config[0x10], config[0x11], config[0x12], config[0x13]]);
let irq_pin = config[0x3D];
@@ -954,13 +953,38 @@ fn transport_status_from_config(config: &[u8]) -> String {
let bus_master = (command & 0x4) != 0;
let bar_present = bar0 != 0;
let irq_present = irq_pin != 0;
let interrupt_support = parse_device_info_from_config_space(
PciLocation {
segment: location.segment,
bus: location.bus,
device: location.device,
function: location.function,
},
config,
)
.map(|info| {
let support = info.interrupt_support();
if support.as_str() == "none" && irq_present {
"legacy".to_string()
} else {
support.as_str().to_string()
}
})
.unwrap_or_else(|| {
if irq_present {
"legacy".to_string()
} else {
"none".to_string()
}
});
format!(
"transport=pci memory_enabled={} bus_master={} bar0_present={} irq_pin_present={}",
"transport=pci memory_enabled={} bus_master={} bar0_present={} irq_pin_present={} interrupt_support={}",
if memory_enabled { "yes" } else { "no" },
if bus_master { "yes" } else { "no" },
if bar_present { "yes" } else { "no" },
if irq_present { "yes" } else { "no" }
if irq_present { "yes" } else { "no" },
interrupt_support
)
}
@@ -1174,7 +1198,8 @@ fn read_transport_status(config_path: &PathBuf) -> Result<String, String> {
config_path.display()
));
}
Ok(transport_status_from_config(&config))
let location = parse_location_from_config_path(config_path)?;
Ok(transport_status_from_config(&location, &config))
}
fn program_transport_bits(config_path: &PathBuf) -> Result<(), String> {
@@ -1286,6 +1311,38 @@ mod tests {
.transport_status("wlan0")
.contains("memory_enabled=yes"));
assert!(backend.transport_status("wlan0").contains("bus_master=yes"));
assert!(backend
.transport_status("wlan0")
.contains("interrupt_support=legacy"));
}
#[test]
fn transport_status_reports_interrupt_support_from_shared_pci_parser() {
let location = ParsedPciLocation {
segment: 0,
bus: 0,
device: 0x14,
function: 3,
};
let mut cfg = vec![0u8; 256];
cfg[0x00] = 0x86;
cfg[0x01] = 0x80;
cfg[0x02] = 0x25;
cfg[0x03] = 0x27;
cfg[0x04] = 0x06;
cfg[0x06] = 0x10;
cfg[0x0A] = 0x80;
cfg[0x0B] = 0x02;
cfg[0x0E] = 0x00;
cfg[0x34] = 0x50;
cfg[0x3C] = 11;
cfg[0x50] = 0x05;
cfg[0x51] = 0x00;
let status = transport_status_from_config(&location, &cfg);
assert!(status.contains("memory_enabled=yes"));
assert!(status.contains("bus_master=yes"));
assert!(status.contains("interrupt_support=msi"));
}
#[test]
@@ -1,5 +1,6 @@
#include <QCoreApplication>
#include <QDebug>
#include <QFile>
#include <QPluginLoader>
#include <cstdio>
@@ -26,6 +27,20 @@ int main(int argc, char **argv) {
? QString::fromLocal8Bit(argv[1])
: QStringLiteral("/usr/plugins/platforms/libqminimal.so");
QFile rawFile(plugin);
if (rawFile.open(QIODevice::ReadOnly)) {
const QByteArray header = rawFile.read(64);
qInfo() << "qt6-plugin-check raw-header" << header.toHex(' ');
if (header.size() >= 56) {
const quint8 low = static_cast<quint8>(header[54]);
const quint8 high = static_cast<quint8>(header[55]);
const quint16 phentsize = quint16(low) | (quint16(high) << 8);
qInfo() << "qt6-plugin-check raw-e_phentsize" << phentsize;
}
} else {
qWarning() << "qt6-plugin-check failed to open raw file" << rawFile.errorString();
}
QPluginLoader loader(plugin);
mark("before-metadata");
std::fprintf(stderr, "qt6-plugin-check before metadata\n");