Honor bread.command.lock.lock; pin theme to v0.7.2
Pin bread-theme/bread-utils to bread-ecosystem tag v0.7.2. Subscribe from the locker and from `breadlock listen` so the command works while unlocked; start breadlock the same way hypridle does. Emit bread.lock.lock.done / .failed. Document loginctl lock-session as the Super+L equivalent. Optional GPG detach-sign of the .pkg.tar.zst when GPG_PRIVATE_KEY is set; ISO [breadway] stays SigLevel = Never until a signed db exists.
This commit is contained in:
parent
76544398d3
commit
80caedc7fd
7 changed files with 401 additions and 28 deletions
|
|
@ -7,10 +7,10 @@ on:
|
|||
jobs:
|
||||
package:
|
||||
runs-on: [self-hosted, hestia]
|
||||
# Forgejo's Arch package registry does not GPG-sign packages for pacman.
|
||||
# BOS therefore uses SigLevel=Never on [Breadway.os.git.breadway.dev]
|
||||
# until a signed repo exists. Keep publishing here — do not flip this
|
||||
# job to a different registry just to get signatures.
|
||||
# Forgejo's Arch package registry does not GPG-sign a pacman db.
|
||||
# BOS ISO [breadway] stays SigLevel = Never until a signed db exists.
|
||||
# Do not flip that here — flipping without a signed db breaks pacman.
|
||||
# Keep publishing the unsigned .pkg.tar.zst to the registry below.
|
||||
container:
|
||||
image: archlinux:latest
|
||||
steps:
|
||||
|
|
@ -37,8 +37,50 @@ jobs:
|
|||
# --nocheck: packaging builds the artifact; tests belong in a CI job.
|
||||
su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck"
|
||||
PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1)
|
||||
mkdir -p /tmp/breadlock-pkg
|
||||
cp "$PKG" /tmp/breadlock-pkg/
|
||||
echo "${VERSION}" > /tmp/breadlock-pkg/VERSION
|
||||
curl -fsS -X PUT \
|
||||
-H "Authorization: token ${PUBLISH_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${PKG}" \
|
||||
"https://git.breadway.dev/api/packages/Breadway/arch/os"
|
||||
|
||||
# Optional detach-sign. secrets.GPG_PRIVATE_KEY is the same BOS
|
||||
# release-signing key (releases@breadway.dev). If the secret is
|
||||
# missing, skip — the registry PUT above already published the
|
||||
# unsigned package. A lone .sig is not a signed repo: ISO
|
||||
# [breadway] stays SigLevel = Never until a signed db exists.
|
||||
# The .sig is uploaded as a generic-package artifact next to that
|
||||
# PUT, not injected into the Arch repo (which would not make
|
||||
# pacman verify anything without a signed db).
|
||||
- name: Detach-sign package (optional)
|
||||
env:
|
||||
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${GPG_PRIVATE_KEY:-}" ]; then
|
||||
echo "GPG_PRIVATE_KEY unset; skipping detach-sign."
|
||||
echo "ISO [breadway] stays SigLevel = Never until a signed db exists."
|
||||
exit 0
|
||||
fi
|
||||
PKG=$(find /tmp/breadlock-pkg -name '*.pkg.tar.zst' | head -1)
|
||||
if [ -z "$PKG" ]; then
|
||||
echo "no package in /tmp/breadlock-pkg; cannot sign" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION=$(cat /tmp/breadlock-pkg/VERSION)
|
||||
pacman -S --noconfirm --needed gnupg
|
||||
export GNUPGHOME=/tmp/gnupg-breadlock
|
||||
mkdir -m 700 -p "$GNUPGHOME"
|
||||
echo "$GPG_PRIVATE_KEY" | gpg --batch --import
|
||||
gpg --batch --yes --detach-sign -o "${PKG}.sig" "$PKG"
|
||||
echo "Signed $(basename "$PKG") -> $(basename "$PKG").sig"
|
||||
# Generic package: workflow artifact alongside the Arch PUT.
|
||||
# Does not change [breadway] / pacman SigLevel.
|
||||
curl -fsS -X PUT \
|
||||
-H "Authorization: token ${PUBLISH_TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${PKG}.sig" \
|
||||
"https://git.breadway.dev/api/packages/Breadway/generic/breadlock/${VERSION}/$(basename "$PKG").sig"
|
||||
|
|
|
|||
8
Cargo.lock
generated
8
Cargo.lock
generated
|
|
@ -77,8 +77,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bread-theme"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
|
||||
version = "0.7.2"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"gtk4",
|
||||
|
|
@ -88,8 +88,8 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bread-utils"
|
||||
version = "0.3.1"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886"
|
||||
version = "0.7.2"
|
||||
source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.2#30517f161724132cdeb658c04cf5e490be07ee73"
|
||||
dependencies = [
|
||||
"bread-shared",
|
||||
"dirs",
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ members = ["breadlock-ui", "breadlock", "breadgreet"]
|
|||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1" }
|
||||
bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
|
||||
bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.2" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
|
|
|
|||
62
EVENTS.md
62
EVENTS.md
|
|
@ -9,9 +9,13 @@ follows.
|
|||
|
||||
App id: **`lock`**. Transport: `bread-utils`'s `bread_client` module
|
||||
(feature `bread-client`) — `breadlock` links it directly. Each `emit` is
|
||||
its own short-lived connection (`BreadClient::emit` is fire-and-forget);
|
||||
there is no long-running subscription half because breadlock has no
|
||||
command verbs (see below).
|
||||
its own short-lived connection (`BreadClient::emit` is fire-and-forget).
|
||||
Commands are received on a `BreadClient::subscribe` background thread
|
||||
(reconnect/backoff) from two places:
|
||||
|
||||
- the locker process itself, while the session is locked
|
||||
- `breadlock listen`, a tiny long-running subscriber so the command
|
||||
works while unlocked
|
||||
|
||||
`breadgreet` is not wired to the bus. It runs under greetd (typically as
|
||||
the dedicated greeter user, before a user session exists), so breadd is
|
||||
|
|
@ -24,21 +28,55 @@ from session lock/unlock.
|
|||
|-------|------|------|
|
||||
| `bread.lock.locked` | `{}` | The compositor accepted the `ext-session-lock-v1` request (`SessionLockHandler::locked`). Not emitted merely because breadlock started or asked to lock. |
|
||||
| `bread.lock.unlocked` | `{}` | PAM authenticated successfully and breadlock sent `unlock` to the compositor. Not emitted on a compositor-ended lock (`finished`), a dispatch-error exit (fail-secure: the session stays locked), or a failed/typo password. |
|
||||
| `bread.lock.lock.done` | `{}` | `bread.command.lock.lock` was honored: the locker was already running, or a locker process was started (same no-args invocation as hypridle's `lock_cmd = breadlock`). This is the command confirmation, not compositor proof — wait on `bread.lock.locked` if you need the session-lock protocol to have completed. |
|
||||
| `bread.lock.lock.failed` | `{ "error": "<message>" }` | `bread.command.lock.lock` was received but the locker could not be started (e.g. this binary is missing from disk). |
|
||||
|
||||
## Commands honored (`bread.command.lock.*`)
|
||||
|
||||
None. breadlock is started by hypridle / `loginctl lock-session` (or
|
||||
directly) and unlocks only via PAM on this process. There is no
|
||||
`lock`/`unlock`/`pin`/`blur` verb, and none is stubbed as a no-op.
|
||||
| Verb | Effect |
|
||||
|------|--------|
|
||||
| `lock` | If a locker is already running, emit `bread.lock.lock.done` and do nothing else. Otherwise start `breadlock` the same way hypridle does (`lock_cmd = breadlock`: this binary, no args) and emit `done` or `failed`. |
|
||||
|
||||
`background.blur` in `breadlock.toml` remains a documented locker no-op
|
||||
(accepted, warned, surface drawn unblurred). That is appearance config,
|
||||
not a bus command — do not invent `bread.command.lock.blur` for it.
|
||||
A Lua workflow that wants the session locked should `bread.wait` /
|
||||
`bread.wait_any` on `bread.lock.lock.done` (or `.failed`) with a timeout.
|
||||
To know the compositor actually locked, wait on `bread.lock.locked`.
|
||||
|
||||
### Who is listening
|
||||
|
||||
`bread.command.lock.lock` is a silent no-op if nobody is subscribed
|
||||
(bread's usual "no listener, no-op" rule). Two subscribers exist:
|
||||
|
||||
1. **`breadlock listen`** — run this for the unlocked path (Hyprland
|
||||
`exec-once = breadlock listen`, a bread module, or equivalent).
|
||||
Without it, a command sent while the session is unlocked has no
|
||||
process to receive it.
|
||||
2. **The locker process** — always subscribes once the lock screen is
|
||||
up, so a command received during an active lock is an idempotent
|
||||
`done`.
|
||||
|
||||
### Session-level equivalent
|
||||
|
||||
Super+L on BOS is `loginctl lock-session`. hypridle picks that up and
|
||||
runs `lock_cmd = breadlock`. That path does **not** go through the
|
||||
bread command bus. It is the session-level equivalent of
|
||||
`bread.command.lock.lock` + `breadlock listen`: same locker binary,
|
||||
same `ext-session-lock-v1` request. Prefer `loginctl lock-session`
|
||||
from a keybind; prefer the bus command from a Lua workflow.
|
||||
|
||||
### Not implemented: `unlock` / `pin` / `blur`
|
||||
|
||||
Unlock is PAM on this process only — there is no `bread.command.lock.unlock`
|
||||
and none is stubbed. `background.blur` in `breadlock.toml` remains a
|
||||
documented locker no-op (accepted, warned, surface drawn unblurred).
|
||||
That is appearance config, not a bus command — do not invent
|
||||
`bread.command.lock.blur` for it.
|
||||
|
||||
## Fail-safe behavior
|
||||
|
||||
- If breadd isn't installed or isn't running, `emit` is a silent no-op
|
||||
(`BreadClient::emit` never blocks or errors the caller) — breadlock's
|
||||
(`BreadClient::emit` never blocks or errors the caller) and the
|
||||
command subscription simply never receives anything — breadlock's
|
||||
actual lock/unlock path is entirely unaffected either way.
|
||||
- There is no command subscription, so a breadd restart while the lock
|
||||
screen is up changes nothing on this side.
|
||||
- If breadd restarts, the command subscription reconnects automatically
|
||||
(`BreadClient::subscribe`'s background thread has its own backoff loop);
|
||||
no restart of the locker or of `breadlock listen` is needed.
|
||||
|
|
|
|||
16
README.md
16
README.md
|
|
@ -12,9 +12,14 @@ Both use [`bread-theme`](https://git.breadway.dev/Breadway/bread-ecosystem) for
|
|||
## bread event integration
|
||||
|
||||
`breadlock` works the same with or without `breadd`. When `breadd` is
|
||||
running, it publishes `bread.lock.locked` / `bread.lock.unlocked`. See
|
||||
[EVENTS.md](EVENTS.md) for the bus contract. `breadgreet` is not on the
|
||||
bus. There is no `bakery.toml` (PAM / pacman exception).
|
||||
running, it publishes `bread.lock.locked` / `bread.lock.unlocked` and
|
||||
honors `bread.command.lock.lock` (emits `bread.lock.lock.done` /
|
||||
`.failed`). Run `breadlock listen` so the command works while unlocked;
|
||||
the locker also subscribes while the session is locked. Super+L remains
|
||||
`loginctl lock-session` (hypridle then runs `breadlock`) — that is the
|
||||
session-level equivalent, not a bus command. See [EVENTS.md](EVENTS.md).
|
||||
`breadgreet` is not on the bus. There is no `bakery.toml` (PAM / pacman
|
||||
exception).
|
||||
|
||||
## Architecture
|
||||
|
||||
|
|
@ -76,6 +81,11 @@ command = "cage -s -- breadgreet"
|
|||
lock_cmd = breadlock
|
||||
```
|
||||
|
||||
`breadlock listen` is the unlocked-path subscriber for
|
||||
`bread.command.lock.lock`. It is not started by hypridle; add it to
|
||||
session startup (`exec-once = breadlock listen`) if a Lua workflow
|
||||
should be able to lock the session while it is unlocked.
|
||||
|
||||
## Verification (why this is safe to test without a lockout risk)
|
||||
|
||||
1. **PAM logic in isolation first**: `cargo run --bin breadlock-auth-check` exercises the exact PAM flow `breadlock` uses, against a typed password, with **no Wayland surface at all**. A bad `/etc/pam.d/breadlock` just prints an error here — it can never lock a session.
|
||||
|
|
|
|||
|
|
@ -1,16 +1,32 @@
|
|||
//! `bread.lock.*` event integration — optional, non-blocking. See
|
||||
//! `EVENTS.md` at the repo root for the full contract. breadlock works
|
||||
//! identically with or without breadd running; every call here is
|
||||
//! identically with or without breadd running; every `emit` here is
|
||||
//! fire-and-forget (`BreadClient::emit` never blocks or errors this
|
||||
//! process) so a missing or restarting breadd never affects locking
|
||||
//! itself.
|
||||
//!
|
||||
//! `bread.command.lock.lock` is the one verb this process honors. The
|
||||
//! locker subscribes while the session is locked (already-locked is
|
||||
//! `bread.lock.lock.done`). `breadlock listen` is the unlocked-path
|
||||
//! subscriber: it starts this same binary the way hypridle's
|
||||
//! `lock_cmd = breadlock` does. Session-level equivalent of Super+L is
|
||||
//! `loginctl lock-session`.
|
||||
|
||||
use bread_utils::bread_client::BreadClient;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::thread;
|
||||
|
||||
use bread_utils::bread_client::{BreadClient, BreadEvent, Subscription};
|
||||
use bread_utils::singleton::{try_acquire, Acquire};
|
||||
|
||||
/// This app's id in bread's sibling-app namespace registry
|
||||
/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.lock.*`.
|
||||
/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.lock.*`,
|
||||
/// commands arrive on `bread.command.lock.*`.
|
||||
pub const APP_ID: &str = "lock";
|
||||
|
||||
/// Distinct singleton for `breadlock listen` so a listen process and a
|
||||
/// locker process can coexist. The locker itself uses [`APP_ID`].
|
||||
pub const LISTEN_APP: &str = "lock-listen";
|
||||
|
||||
pub fn emit_locked() {
|
||||
BreadClient::connect(APP_ID).emit("bread.lock.locked", serde_json::json!({}));
|
||||
}
|
||||
|
|
@ -18,3 +34,138 @@ pub fn emit_locked() {
|
|||
pub fn emit_unlocked() {
|
||||
BreadClient::connect(APP_ID).emit("bread.lock.unlocked", serde_json::json!({}));
|
||||
}
|
||||
|
||||
pub fn emit_lock_done() {
|
||||
BreadClient::connect(APP_ID).emit("bread.lock.lock.done", serde_json::json!({}));
|
||||
}
|
||||
|
||||
pub fn emit_lock_failed(error: &str) {
|
||||
BreadClient::connect(APP_ID).emit(
|
||||
"bread.lock.lock.failed",
|
||||
serde_json::json!({ "error": error }),
|
||||
);
|
||||
}
|
||||
|
||||
/// True when another process holds the locker singleton — i.e. breadlock
|
||||
/// is already locking this session. A `try_acquire` that succeeds is
|
||||
/// released immediately; this is a check, not a claim.
|
||||
pub fn locker_is_running() -> bool {
|
||||
singleton_held(APP_ID)
|
||||
}
|
||||
|
||||
fn singleton_held(app: &str) -> bool {
|
||||
match try_acquire(app) {
|
||||
Ok(Acquire::HeldByOther(_)) => true,
|
||||
Ok(Acquire::Acquired(_guard)) => false,
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a locker the same way hypridle's `lock_cmd = breadlock` does:
|
||||
/// this binary, no args. The child is reaped on a background thread so
|
||||
/// a later unlock cannot leave a zombie under `breadlock listen`.
|
||||
pub fn start_locker() -> Result<(), String> {
|
||||
let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from("breadlock"));
|
||||
let mut child = Command::new(exe)
|
||||
.stdin(Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|e| format!("failed to start breadlock: {e}"))?;
|
||||
thread::spawn(move || {
|
||||
let _ = child.wait();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Honor `bread.command.lock.lock`: already locked is success; otherwise
|
||||
/// start the locker. `done` means the command was acted on, not that
|
||||
/// `ext-session-lock-v1` has been accepted — wait on `bread.lock.locked`
|
||||
/// for the compositor confirmation.
|
||||
pub fn honor_lock_command() {
|
||||
honor_lock_command_with(start_locker);
|
||||
}
|
||||
|
||||
fn honor_lock_command_with(start: impl FnOnce() -> Result<(), String>) {
|
||||
if locker_is_running() {
|
||||
tracing::info!("bread.command.lock.lock: already locked");
|
||||
emit_lock_done();
|
||||
return;
|
||||
}
|
||||
match start() {
|
||||
Ok(()) => {
|
||||
tracing::info!("bread.command.lock.lock: started breadlock");
|
||||
emit_lock_done();
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%error, "bread.command.lock.lock: failed to start breadlock");
|
||||
emit_lock_failed(&error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reacts to `bread.command.lock.*`. Unknown verbs are ignored, not stubbed.
|
||||
pub fn handle_command(event: &BreadEvent) {
|
||||
let Some(verb) = event.event.strip_prefix("bread.command.lock.") else {
|
||||
return;
|
||||
};
|
||||
match verb {
|
||||
"lock" => honor_lock_command(),
|
||||
other => tracing::info!(verb = other, "ignoring unknown bread.command.lock verb"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to commands addressed to this app. Keep the handle alive
|
||||
/// for as long as this process should honor them.
|
||||
pub fn subscribe_commands() -> Subscription {
|
||||
BreadClient::connect(APP_ID).subscribe("bread.command.lock.**", |event| {
|
||||
handle_command(&event);
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn event(name: &str) -> BreadEvent {
|
||||
BreadEvent {
|
||||
event: name.to_string(),
|
||||
timestamp: 0,
|
||||
data: serde_json::json!({}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handle_command_ignores_unrecognized_verb() {
|
||||
handle_command(&event("bread.command.lock.unlock"));
|
||||
handle_command(&event("bread.command.lock.pin"));
|
||||
handle_command(&event("bread.command.clip.clear"));
|
||||
handle_command(&event("bread.lock.locked"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singleton_held_is_false_when_nothing_holds_the_name() {
|
||||
let app = format!("breadlock-test-held-false-{}", std::process::id());
|
||||
assert!(!singleton_held(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singleton_held_is_true_while_this_process_holds_the_name() {
|
||||
let app = format!("breadlock-test-held-true-{}", std::process::id());
|
||||
let guard = match try_acquire(&app).unwrap() {
|
||||
Acquire::Acquired(g) => g,
|
||||
Acquire::HeldByOther(_) => panic!("expected to be the first instance"),
|
||||
};
|
||||
assert!(singleton_held(&app));
|
||||
drop(guard);
|
||||
assert!(!singleton_held(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn honor_lock_command_with_failed_start_does_not_panic() {
|
||||
honor_lock_command_with(|| Err("boom".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn honor_lock_command_with_successful_start_does_not_panic() {
|
||||
honor_lock_command_with(|| Ok(()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,116 @@ use wayland_client::globals::registry_queue_init;
|
|||
use wayland_client::{protocol::wl_buffer, Connection, QueueHandle};
|
||||
|
||||
use background::Background;
|
||||
use bread_utils::singleton::{try_acquire, Acquire};
|
||||
use state::{AppState, AuthState, LockSurface};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Lock,
|
||||
Listen,
|
||||
Help,
|
||||
}
|
||||
|
||||
fn parse_mode<I, S>(args: I) -> Result<Mode, String>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<str>,
|
||||
{
|
||||
let mut args = args.into_iter();
|
||||
match args.next().as_ref().map(|s| s.as_ref()) {
|
||||
None => Ok(Mode::Lock),
|
||||
Some("listen") if args.next().is_none() => Ok(Mode::Listen),
|
||||
Some("-h" | "--help" | "help") => Ok(Mode::Help),
|
||||
Some("listen") => Err("listen takes no arguments".into()),
|
||||
Some(other) => Err(format!("unknown argument '{other}'")),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!(
|
||||
"Usage: breadlock [listen]\n\
|
||||
\n\
|
||||
(no args) lock this session — hypridle lock_cmd / Super+L via loginctl lock-session\n\
|
||||
listen subscribe to bread.command.lock.lock so the command works while unlocked\n\
|
||||
\n\
|
||||
Session-level equivalent of Super+L: loginctl lock-session (hypridle then runs breadlock).\n\
|
||||
See EVENTS.md for the bus contract."
|
||||
);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
match parse_mode(std::env::args().skip(1)) {
|
||||
Ok(Mode::Lock) => run_lock(),
|
||||
Ok(Mode::Listen) => run_listen(),
|
||||
Ok(Mode::Help) => print_usage(),
|
||||
Err(err) => {
|
||||
eprintln!("breadlock: {err}");
|
||||
print_usage();
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Long-running subscriber so `bread.command.lock.lock` works while the
|
||||
/// session is unlocked. The locker process also subscribes; this path is
|
||||
/// what actually starts breadlock (the same no-args invocation hypridle
|
||||
/// uses). One listen process per session.
|
||||
fn run_listen() {
|
||||
let _guard = match try_acquire(bread_events::LISTEN_APP) {
|
||||
Ok(Acquire::Acquired(g)) => g,
|
||||
Ok(Acquire::HeldByOther(pid)) => {
|
||||
tracing::info!(?pid, "breadlock listen already running");
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(%err, "failed to acquire listen singleton");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Common when started early in the session (exec-once). The locker we
|
||||
// spawn needs WAYLAND_DISPLAY; we stay up either way so a later command
|
||||
// still has a subscriber.
|
||||
for _ in 0..20 {
|
||||
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
if std::env::var_os("WAYLAND_DISPLAY").is_none() {
|
||||
tracing::warn!("WAYLAND_DISPLAY not set; spawned breadlock will fail until it is");
|
||||
}
|
||||
|
||||
let _commands = bread_events::subscribe_commands();
|
||||
tracing::info!("listening for bread.command.lock.lock");
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_secs(3600));
|
||||
}
|
||||
}
|
||||
|
||||
fn run_lock() {
|
||||
let _locker_guard = match try_acquire(bread_events::APP_ID) {
|
||||
Ok(Acquire::Acquired(g)) => Some(g),
|
||||
Ok(Acquire::HeldByOther(pid)) => {
|
||||
tracing::info!(?pid, "session already locked by another breadlock; exiting");
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
// Refusing to lock because flock failed would be worse than
|
||||
// running without the singleton — hypridle still needs a locker.
|
||||
tracing::warn!(%err, "could not acquire lock singleton; continuing");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Honor bread.command.lock.lock while this locker is up (already-locked
|
||||
// is bread.lock.lock.done). Unlocked commands need `breadlock listen`.
|
||||
let _commands = bread_events::subscribe_commands();
|
||||
|
||||
let username = std::env::var("USER")
|
||||
.or_else(|_| std::env::var("LOGNAME"))
|
||||
.unwrap_or_else(|_| {
|
||||
|
|
@ -204,3 +307,32 @@ smithay_client_toolkit::delegate_seat!(AppState);
|
|||
smithay_client_toolkit::delegate_keyboard!(AppState);
|
||||
smithay_client_toolkit::delegate_registry!(AppState);
|
||||
wayland_client::delegate_noop!(AppState: ignore wl_buffer::WlBuffer);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_mode, Mode};
|
||||
|
||||
#[test]
|
||||
fn parse_mode_no_args_is_lock() {
|
||||
let args: [&str; 0] = [];
|
||||
assert_eq!(parse_mode(args), Ok(Mode::Lock));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_listen() {
|
||||
assert_eq!(parse_mode(["listen"]), Ok(Mode::Listen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_help() {
|
||||
assert_eq!(parse_mode(["--help"]), Ok(Mode::Help));
|
||||
assert_eq!(parse_mode(["-h"]), Ok(Mode::Help));
|
||||
assert_eq!(parse_mode(["help"]), Ok(Mode::Help));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mode_rejects_unknown_and_extra_listen_args() {
|
||||
assert!(parse_mode(["unlock"]).is_err());
|
||||
assert!(parse_mode(["listen", "--foreground"]).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue