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 370d27f44d
commit 9880e0a5b2
137 changed files with 14176 additions and 2016 deletions
@@ -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>