From 547fad83dcb8f629b160aa4394d087fa89ffa0cf Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Mon, 13 Jul 2026 13:21:43 +0100 Subject: [PATCH 1/2] accept a sigusr1 to exit when debug flag present --- Cargo.toml | 2 +- src/args.rs | 7 +++++-- src/comm.rs | 15 +++++++++++++++ src/event.rs | 16 +++++++++++++++- src/main.rs | 28 ++++++++++++++++++++++------ src/signal.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/state.rs | 41 +++++++++++++++++++++++++++++++++-------- 7 files changed, 137 insertions(+), 18 deletions(-) create mode 100644 src/signal.rs diff --git a/Cargo.toml b/Cargo.toml index c13710d..3ab47fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ clap_complete = "4.5.64" config = { version = "0.15.18", features = ["toml"], default-features = false } dirs = "6.0.0" gdk-pixbuf = { version = "0.21.5", optional = true } -nix = { version = "0.31.3", features = ["event", "fs", "mman", "process", "time", "user"] } +nix = { version = "0.31.3", features = ["event", "fs", "mman", "process", "signal", "time", "user"] } num_enum = "0.7.6" pango = { version = "0.21.5", optional = true } pangocairo = { version = "0.21.5", optional = true } diff --git a/src/args.rs b/src/args.rs index 96a5c5f..4669f1b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -24,11 +24,14 @@ pub struct NLockArgs { pub subcommand: Option, /// Log verbosity - #[arg(short, long, default_value = "info")] - pub log_level: LogLevel, + #[arg(short, long)] + pub log_level: Option, /// Configuration file path #[arg(short, long)] pub config_file: Option, + /// Enable debug mode + #[arg(short, long, action)] + pub debug: bool, /// Sets the background color #[arg(long)] diff --git a/src/comm.rs b/src/comm.rs index 3db60a1..8be8cad 100644 --- a/src/comm.rs +++ b/src/comm.rs @@ -121,6 +121,12 @@ impl AsBytes for AuthState { } } +impl AsBytes for () { + fn as_bytes(&self) -> &[u8] { + &[0u8] + } +} + pub trait FromBytes { /// Convert from a bytes-like representation of the object fn from_bytes(bytes: &[u8]) -> Option @@ -169,3 +175,12 @@ impl FromBytes for AuthState { } } } + +impl FromBytes for () { + fn from_bytes(_: &[u8]) -> Option + where + Self: Sized, + { + Some(()) + } +} diff --git a/src/event.rs b/src/event.rs index ed54799..65c8344 100644 --- a/src/event.rs +++ b/src/event.rs @@ -9,7 +9,7 @@ use std::{ use anyhow::{Result, anyhow}; use nix::errno::Errno; use num_enum::{IntoPrimitive, TryFromPrimitive}; -use tracing::warn; +use tracing::{debug, warn}; use wayland_client::{EventQueue, QueueHandle, backend::ReadEventsGuard}; use crate::{ @@ -24,6 +24,7 @@ pub enum EventType { Wayland = 0, KeyboardRepeat = 1, AuthStateChanged = 2, + Debug = 3, } impl NLockState { @@ -76,6 +77,19 @@ impl NLockState { warn!("Failed to receive auth response: {e}"); } }, + EventType::Debug => { + if let Some(debug_comm) = &mut self.debug_comm { + let _ = debug_comm.read(); + + debug!("Received a debug event, exiting..."); + + // fake a "success" and exit + self.running.store(false, Ordering::Relaxed); + self.state_changed.store(true, Ordering::Relaxed); + } else { + warn!("Received debug event, but not in debug mode, ignoring"); + } + } _ => {} }, Event::Timeout { tag } => { diff --git a/src/main.rs b/src/main.rs index 5ceeac9..e89ab6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ pub mod event_loop; pub mod font; pub mod render; pub mod seat; +pub mod signal; pub mod state; pub mod surface; pub mod util; @@ -25,30 +26,41 @@ use anyhow::{Result, bail}; #[cfg(target_os = "linux")] use nix::sys::prctl; -use tracing::{debug, error, warn}; +use tracing::{debug, error, info, warn}; use wayland_client::Connection; use crate::{ args::run_cli, auth::{AuthChannel, AuthConfig, setup_auth}, config::NLockConfig, - state::NLockState, + state::{NLockState, NLockStateArgs}, + util::LogLevel, }; -fn start(config: NLockConfig) -> Result<()> { +fn start(config: NLockConfig, debug: bool) -> Result<()> { // Prevent ptrace from attaching to nlock // Only do this in release config #[cfg(not(debug_assertions))] #[cfg(target_os = "linux")] prctl::set_dumpable(false)?; + if debug { + info!("Running in DEBUG mode"); + } + let conn = Connection::connect_to_env()?; let display = conn.display(); let auth_comm = Arc::new(AuthChannel::new()?); let auth_config: AuthConfig = (&config).into(); - let mut state = NLockState::new(config, display, auth_comm.clone())?; + let state_args = NLockStateArgs { + config, + display, + auth_comm: auth_comm.clone(), + debug, + }; + let mut state = NLockState::new(state_args)?; let mut event_queue = conn.new_event_queue(); let qh = event_queue.handle(); @@ -104,7 +116,11 @@ fn main() { tracing_subscriber::fmt() .with_timer(tracing_subscriber::fmt::time::uptime()) - .with_max_level(args.log_level) + .with_max_level(args.log_level.unwrap_or(if args.debug { + LogLevel::Debug + } else { + LogLevel::Info + })) .init(); let now = chrono::Local::now(); @@ -112,7 +128,7 @@ fn main() { match NLockConfig::load(&args) { Ok(cfg) => { - if let Err(e) = start(cfg) { + if let Err(e) = start(cfg, args.debug) { error!("{:?}", e); } } diff --git a/src/signal.rs b/src/signal.rs new file mode 100644 index 0000000..36580c4 --- /dev/null +++ b/src/signal.rs @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (c) 2026, Nathan Gill + +use std::{ + os::raw::c_int, + sync::{Arc, OnceLock}, +}; + +use anyhow::{Result, anyhow}; +use nix::sys::signal::{SigHandler, Signal}; +use tracing::debug; + +use crate::comm::PipeCommChannel; + +/// PipeCommChannel is async-signal-safe, only using read/write syscalls +/// Unit type only, since only a notification is needed, not actual data +static DEBUG_COMM: OnceLock>> = OnceLock::new(); + +/// Handle a debug signal, probably SIGUSR1 +/// This function is async-signal-safe, but not reentrant, thus should +/// only be registered for one signal at a time, for which POSIX mandates +/// it won't be interrupted by itself. +extern "C" fn handle_debug(_: c_int) { + if let Some(comm) = DEBUG_COMM.get() { + let _ = comm.write(()); + } +} + +/// Register a pipe comm channel to use as a signal callback, typically SIGUSR1 +pub fn install_debug_handler(comm: Arc>) -> Result<()> { + if DEBUG_COMM.set(comm).is_err() { + return Err(anyhow!( + "Failed to set debug comm channel, already initialised" + )); + } + + let handler = SigHandler::Handler(handle_debug); + unsafe { nix::sys::signal::signal(Signal::SIGUSR1, handler) }?; + + debug!( + "Debug handler set up for SIGUSR1 ({})", + Signal::SIGUSR1 as c_int + ); + + Ok(()) +} diff --git a/src/state.rs b/src/state.rs index 7adcceb..cc735fe 100644 --- a/src/state.rs +++ b/src/state.rs @@ -30,7 +30,9 @@ use {crate::cairo_ext::ImageSurfaceExt, gdk_pixbuf::Pixbuf}; use crate::{ auth::AuthChannel, cairo_ext::SubpixelOrderExt, + comm::PipeCommChannel, event_loop::{EventSource, NLockEventLoop}, + signal::install_debug_handler, }; use crate::{ auth::{AtomicAuthState, AuthState}, @@ -42,6 +44,13 @@ use crate::{ surface::NLockSurface, }; +pub struct NLockStateArgs { + pub config: NLockConfig, + pub display: wl_display::WlDisplay, + pub auth_comm: Arc, + pub debug: bool, +} + pub struct NLockState { pub config: NLockConfig, pub running: Arc, @@ -64,21 +73,24 @@ pub struct NLockState { pub auth_state: Arc, pub background_image: Option, pub event_loop: NLockEventLoop, + pub debug_comm: Option>>, } impl NLockState { - pub fn new( - config: NLockConfig, - display: wl_display::WlDisplay, - auth_comm: Arc, - ) -> Result { + pub fn new(args: NLockStateArgs) -> Result { + let debug_comm = if args.debug { + Some(Arc::new(PipeCommChannel::new()?)) + } else { + None + }; + let mut s = Self { - config, + config: args.config, running: Arc::new(AtomicBool::new(true)), locked: false, unlocked: false, state_changed: Arc::new(AtomicBool::new(false)), - display, + display: args.display, registry: None, compositor: None, subcompositor: None, @@ -90,10 +102,11 @@ impl NLockState { seat: NLockSeat::default(), xkb: NLockXkb::default(), password: Zeroizing::new("".to_string()), - auth_comm, + auth_comm: args.auth_comm, auth_state: Arc::new(AtomicAuthState::new(AuthState::Idle)), background_image: None, event_loop: NLockEventLoop::default(), + debug_comm, }; if let Err(e) = s.try_load_background_image() { @@ -109,6 +122,18 @@ impl NLockState { EventType::AuthStateChanged.into(), )?; + if args.debug + && let Some(debug_comm) = &s.debug_comm + { + s.event_loop.add( + EventSource::Fd(debug_comm.rx().as_raw_fd()), + EventType::Debug.into(), + )?; + + // install a handler on SIGUSR1 + install_debug_handler(debug_comm.clone())?; + } + Ok(s) } From c9907634002da7569d2e3c44fe26a429af2bb1d2 Mon Sep 17 00:00:00 2001 From: Nathan Gill Date: Mon, 13 Jul 2026 13:28:39 +0100 Subject: [PATCH 2/2] doc/args: add note on debug flag --- doc/cli.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/cli.md b/doc/cli.md index 654d3c7..4aeea4b 100644 --- a/doc/cli.md +++ b/doc/cli.md @@ -11,6 +11,11 @@ The following options are only available as command line arguments: here is the **only** one loaded, any other configuration files on disk will be ignored. Options specified in here can still be overriden by command line options. +- `-d`/`--debug`, enable debug mode. Debug mode enables the `debug` log level by + default. Sending nlock a SIGUSR1 while running in debug mode will result + in immediate exit, whether authentication was successful or not. This flag + is specifically designed for developing or testing nlock, where its + behaviours are desirable and save considerable time. The following correspond directly to configuration options. See [configuration file documentation](config.md) for more information about these.