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:
@@ -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>
|
||||
Reference in New Issue
Block a user