Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
5 changes: 5 additions & 0 deletions doc/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ pub struct NLockArgs {
pub subcommand: Option<NLockSubcommands>,

/// Log verbosity
#[arg(short, long, default_value = "info")]
pub log_level: LogLevel,
#[arg(short, long)]
pub log_level: Option<LogLevel>,
/// Configuration file path
#[arg(short, long)]
pub config_file: Option<String>,
/// Enable debug mode
#[arg(short, long, action)]
pub debug: bool,

/// Sets the background color
#[arg(long)]
Expand Down
15 changes: 15 additions & 0 deletions src/comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self>
Expand Down Expand Up @@ -169,3 +175,12 @@ impl FromBytes for AuthState {
}
}
}

impl FromBytes for () {
fn from_bytes(_: &[u8]) -> Option<Self>
where
Self: Sized,
{
Some(())
}
}
16 changes: 15 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -24,6 +24,7 @@ pub enum EventType {
Wayland = 0,
KeyboardRepeat = 1,
AuthStateChanged = 2,
Debug = 3,
}

impl NLockState {
Expand Down Expand Up @@ -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 } => {
Expand Down
28 changes: 22 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -104,15 +116,19 @@ 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();
debug!("nlock started at {}", now.to_rfc3339());

match NLockConfig::load(&args) {
Ok(cfg) => {
if let Err(e) = start(cfg) {
if let Err(e) = start(cfg, args.debug) {
error!("{:?}", e);
}
}
Expand Down
46 changes: 46 additions & 0 deletions src/signal.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<PipeCommChannel<()>>> = 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<PipeCommChannel<()>>) -> 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(())
}
41 changes: 33 additions & 8 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -42,6 +44,13 @@ use crate::{
surface::NLockSurface,
};

pub struct NLockStateArgs {
pub config: NLockConfig,
pub display: wl_display::WlDisplay,
pub auth_comm: Arc<AuthChannel>,
pub debug: bool,
}

pub struct NLockState {
pub config: NLockConfig,
pub running: Arc<AtomicBool>,
Expand All @@ -64,21 +73,24 @@ pub struct NLockState {
pub auth_state: Arc<AtomicAuthState>,
pub background_image: Option<cairo::ImageSurface>,
pub event_loop: NLockEventLoop,
pub debug_comm: Option<Arc<PipeCommChannel<()>>>,
}

impl NLockState {
pub fn new(
config: NLockConfig,
display: wl_display::WlDisplay,
auth_comm: Arc<AuthChannel>,
) -> Result<Self> {
pub fn new(args: NLockStateArgs) -> Result<Self> {
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,
Expand All @@ -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() {
Expand All @@ -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)
}

Expand Down
Loading