use std::collections::HashMap;
use std::io::{Read, Write};
use std::os::unix::io::{AsRawFd, RawFd};
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use crate::ported::commands::main::{finish_args, Args};
#[allow(non_snake_case)]
pub fn USE_FILESYSTEM() -> bool {
!cfg!(target_os = "linux")
}
pub const EOF: &[u8] = b"EOF\0\0";
#[allow(non_snake_case)]
pub fn HOME() -> String {
std::env::var("HOME").unwrap_or_default()
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NonInteractiveArgParser;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct PowerlineKey {
pub ext: String,
pub renderer_module: Option<String>,
pub config_override: Option<Vec<String>>,
pub theme_override: Option<Vec<String>>,
pub config_path: Option<Vec<String>>,
pub env_theme_overrides: String,
pub env_config_overrides: String,
pub env_config_paths: String,
}
impl PowerlineKey {
pub fn from(args: &Args, environ: &HashMap<String, String>) -> Self {
let ext = args.ext.first().cloned().unwrap_or_default();
Self {
ext,
renderer_module: args.renderer_module.clone(),
config_override: args.config_override.clone(),
theme_override: args.theme_override.clone(),
config_path: args.config_path.clone(),
env_theme_overrides: environ
.get("POWERLINE_THEME_OVERRIDES")
.cloned()
.unwrap_or_default(),
env_config_overrides: environ
.get("POWERLINE_CONFIG_OVERRIDES")
.cloned()
.unwrap_or_default(),
env_config_paths: environ
.get("POWERLINE_CONFIG_PATHS")
.cloned()
.unwrap_or_default(),
}
}
}
#[derive(Debug)]
pub struct WmHandle {
pub thread: Option<JoinHandle<()>>,
pub shutdown_event: Arc<AtomicBool>,
}
#[derive(Debug, Default)]
pub struct State {
pub powerlines: HashMap<PowerlineKey, ()>,
pub logger: Option<()>,
pub config_loader: Option<()>,
pub started_wm_threads: HashMap<String, WmHandle>,
pub ts_shutdown_event: Arc<AtomicBool>,
}
impl State {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NonDaemonShellPowerline;
impl NonDaemonShellPowerline {
pub fn get_log_handler(&self) {
}
}
pub fn eintr_retry_call<F, T>(mut func: F) -> std::io::Result<T>
where
F: FnMut() -> std::io::Result<T>,
{
loop {
match func() {
Ok(v) => return Ok(v),
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(e),
}
}
}
pub fn safe_bytes(s: &str) -> Vec<u8> {
s.as_bytes().to_vec()
}
#[allow(clippy::type_complexity)]
pub fn parse_args(req: &[u8]) -> Option<(Vec<String>, HashMap<String, String>, String)> {
let parts: Vec<String> = req
.split(|&b| b == 0)
.filter(|s| !s.is_empty())
.map(|s| String::from_utf8_lossy(s).into_owned())
.collect();
if parts.is_empty() {
return None;
}
let numargs = usize::from_str_radix(&parts[0], 16).ok()?;
if parts.len() < 1 + numargs + 1 {
return None;
}
let shell_args: Vec<String> = parts[1..1 + numargs].to_vec();
let mut cwd = parts.get(1 + numargs).cloned().unwrap_or_default();
let mut environ: HashMap<String, String> = HashMap::new();
for kv in parts.iter().skip(numargs + 2) {
if let Some(eq) = kv.find('=') {
environ.insert(kv[..eq].to_string(), kv[eq + 1..].to_string());
}
}
if cwd.is_empty() {
cwd = environ
.get("PWD")
.cloned()
.unwrap_or_else(|| "/".to_string());
}
Some((shell_args, environ, cwd))
}
pub fn do_read(conn: &mut UnixStream, timeout: Duration) -> Option<Vec<u8>> {
let mut read: Vec<u8> = Vec::new();
let end_time = Instant::now() + timeout;
while !read.ends_with(b"\0\0") {
let now = Instant::now();
if now >= end_time {
return None;
}
let remaining = end_time - now;
let fd = conn.as_raw_fd();
let ready = match poll_readable(fd, remaining) {
Ok(b) => b,
Err(_) => return None, };
if !ready {
continue;
}
let mut buf = [0u8; 4096];
match eintr_retry_call(|| conn.read(&mut buf)) {
Ok(n) if n > 0 => read.extend_from_slice(&buf[..n]),
Ok(_) => break,
Err(_) => return None,
}
}
Some(read)
}
fn poll_readable(fd: RawFd, timeout: Duration) -> std::io::Result<bool> {
let mut pfd = libc::pollfd {
fd,
events: libc::POLLIN,
revents: 0,
};
let ms = timeout
.as_millis()
.min(i32::MAX as u128)
.try_into()
.unwrap_or(i32::MAX);
let rc = unsafe { libc::poll(&mut pfd, 1, ms) };
if rc < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
return Ok(false);
}
return Err(err);
}
if pfd.revents & (libc::POLLERR | libc::POLLNVAL) != 0 {
return Err(std::io::Error::other("poll: fd error"));
}
Ok(rc > 0 && (pfd.revents & (libc::POLLIN | libc::POLLHUP)) != 0)
}
pub fn do_write(conn: &mut UnixStream, result: &[u8]) {
let _ = eintr_retry_call(|| conn.write_all(result));
}
pub type RenderFn = dyn Fn(&Args, &HashMap<String, String>, &str, bool) -> Vec<u8> + Send + Sync;
pub type SpawnWmFn =
dyn Fn(&str, Arc<AtomicBool>, Arc<AtomicBool>) -> Option<WmHandle> + Send + Sync;
pub fn start_wm(args: &Args, state: &mut State, spawn_wm_fn: &Arc<SpawnWmFn>) -> Vec<u8> {
let ext = args.ext.first().cloned().unwrap_or_default();
let wm_name = ext.strip_prefix("wm.").unwrap_or(&ext).to_string();
if state.started_wm_threads.contains_key(&wm_name) {
return Vec::new();
}
let thread_shutdown_event = Arc::new(AtomicBool::new(false));
match spawn_wm_fn(
&wm_name,
thread_shutdown_event.clone(),
state.ts_shutdown_event.clone(),
) {
Some(handle) => {
state.started_wm_threads.insert(wm_name, handle);
}
None => {
return safe_bytes(&format!("Unknown WM: {}", wm_name));
}
}
Vec::new()
}
pub fn render(
args: &Args,
environ: &HashMap<String, String>,
cwd: &str,
is_daemon: bool,
state: &mut State,
render_fn: &Arc<RenderFn>,
) -> Vec<u8> {
let key = PowerlineKey::from(args, environ);
state.powerlines.entry(key).or_insert(());
let _ = cwd;
render_fn(args, environ, cwd, is_daemon)
}
pub fn get_answer(
req: &[u8],
is_daemon: bool,
state: &mut State,
render_fn: &Arc<RenderFn>,
spawn_wm_fn: &Arc<SpawnWmFn>,
) -> Vec<u8> {
let (raw_args, environ, cwd) = match parse_args(req) {
Some(t) => t,
None => return safe_bytes("malformed request"),
};
let mut args = parse_client_argv(&raw_args);
if let Err(e) = finish_args(&environ, &mut args, true) {
return safe_bytes(&e);
}
if args
.ext
.first()
.map(|e| e.starts_with("wm."))
.unwrap_or(false)
{
return start_wm(&args, state, spawn_wm_fn);
}
render(&args, &environ, &cwd, is_daemon, state, render_fn)
}
pub fn parse_client_argv(argv: &[String]) -> Args {
let mut a = Args::default();
let mut positional: Vec<String> = Vec::new();
let mut i = 0;
while i < argv.len() {
let cur = &argv[i];
match cur.as_str() {
"-w" | "--width" => {
if let Some(v) = argv.get(i + 1) {
a.width = v.parse().ok();
i += 2;
continue;
}
}
"-r" | "--renderer-module" => {
if let Some(v) = argv.get(i + 1) {
a.renderer_module = Some(v.clone());
i += 2;
continue;
}
}
"-c" | "--config-override" => {
if let Some(v) = argv.get(i + 1) {
a.config_override
.get_or_insert_with(Vec::new)
.push(v.clone());
i += 2;
continue;
}
}
"-t" | "--theme-override" => {
if let Some(v) = argv.get(i + 1) {
a.theme_override
.get_or_insert_with(Vec::new)
.push(v.clone());
i += 2;
continue;
}
}
"-R" | "--renderer-arg" => {
if let Some(v) = argv.get(i + 1) {
a.renderer_arg.get_or_insert_with(Vec::new).push(v.clone());
i += 2;
continue;
}
}
"-p" | "--config-path" => {
if let Some(v) = argv.get(i + 1) {
a.config_path.get_or_insert_with(Vec::new).push(v.clone());
i += 2;
continue;
}
}
"--socket" => {
if let Some(v) = argv.get(i + 1) {
a.socket = Some(v.clone());
i += 2;
continue;
}
}
"-m" | "--mode" => {
if let Some(v) = argv.get(i + 1) {
a.renderer_arg
.get_or_insert_with(Vec::new)
.push(format!("mode={}", v));
i += 2;
continue;
}
}
"--last-exit-code" => {
if let Some(v) = argv.get(i + 1) {
if let Ok(parsed) = crate::ported::commands::main::int_or_sig(v) {
a.last_exit_code = Some(parsed);
}
i += 2;
continue;
}
}
"--last-pipe-status" => {
if let Some(v) = argv.get(i + 1) {
a.last_pipe_status = v
.split_whitespace()
.filter_map(|tok| crate::ported::commands::main::int_or_sig(tok).ok())
.collect();
i += 2;
continue;
}
}
"--jobnum" => {
if let Some(v) = argv.get(i + 1) {
a.jobnum = v.parse().ok();
i += 2;
continue;
}
}
s if s.starts_with('-') => {
i += 1;
continue;
}
_ => {
positional.push(cur.clone());
i += 1;
continue;
}
}
i += 1;
}
if let Some(ext) = positional.first().cloned() {
a.ext.push(ext);
}
if let Some(side) = positional.get(1).cloned() {
a.side = Some(side);
}
a
}
pub struct Conn {
stream: UnixStream,
pending_response: Option<Vec<u8>>,
}
pub fn do_one(
listener: &UnixListener,
conns: &mut HashMap<RawFd, Conn>,
is_daemon: bool,
state: &mut State,
render_fn: &Arc<RenderFn>,
spawn_wm_fn: &Arc<SpawnWmFn>,
) -> Option<i32> {
let listener_fd = listener.as_raw_fd();
let mut pfds: Vec<libc::pollfd> = Vec::with_capacity(1 + conns.len());
pfds.push(libc::pollfd {
fd: listener_fd,
events: libc::POLLIN,
revents: 0,
});
let conn_fds: Vec<RawFd> = conns.keys().copied().collect();
for fd in &conn_fds {
let events = if conns
.get(fd)
.and_then(|c| c.pending_response.as_ref())
.is_some()
{
libc::POLLOUT
} else {
libc::POLLIN
};
pfds.push(libc::pollfd {
fd: *fd,
events,
revents: 0,
});
}
let rc = unsafe { libc::poll(pfds.as_mut_ptr(), pfds.len() as libc::nfds_t, 60_000) };
if rc < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
return None;
}
return Some(1);
}
if rc == 0 {
return None;
}
if pfds[0].revents & (libc::POLLERR | libc::POLLNVAL) != 0 {
return Some(1);
}
for (i, fd) in conn_fds.iter().enumerate() {
let revents = pfds[i + 1].revents;
if revents & (libc::POLLERR | libc::POLLNVAL) != 0 {
conns.remove(fd);
}
}
if pfds[0].revents & libc::POLLIN != 0 {
match eintr_retry_call(|| listener.accept()) {
Ok((stream, _)) => {
let fd = stream.as_raw_fd();
conns.insert(
fd,
Conn {
stream,
pending_response: None,
},
);
}
Err(_) => {
}
}
}
for fd in &conn_fds {
let idx = match conn_fds.iter().position(|x| x == fd) {
Some(i) => i + 1,
None => continue,
};
let revents = pfds[idx].revents;
let mut conn = match conns.remove(fd) {
Some(c) => c,
None => continue,
};
if conn.pending_response.is_some() {
conns.insert(*fd, conn);
continue;
}
if revents & (libc::POLLIN | libc::POLLHUP) == 0 {
conns.insert(*fd, conn);
continue;
}
let req = do_read(&mut conn.stream, Duration::from_secs_f64(2.0));
match req {
Some(ref r) if r == EOF => return Some(0),
Some(r) if !r.is_empty() => {
let ans = get_answer(&r, is_daemon, state, render_fn, spawn_wm_fn);
conn.pending_response = Some(ans);
conns.insert(*fd, conn);
}
_ => {
drop(conn);
}
}
}
let writable_fds: Vec<RawFd> = conns
.iter()
.filter(|(_, c)| c.pending_response.is_some())
.map(|(fd, _)| *fd)
.collect();
for fd in writable_fds {
let idx = conn_fds.iter().position(|x| *x == fd).map(|i| i + 1);
let writable = match idx {
Some(i) => pfds[i].revents & libc::POLLOUT != 0,
None => false,
};
if !writable {
continue;
}
if let Some(mut conn) = conns.remove(&fd) {
if let Some(result) = conn.pending_response.take() {
do_write(&mut conn.stream, &result);
}
}
}
None
}
pub fn shutdown(conns: &mut HashMap<RawFd, Conn>, state: &mut State) {
let total = Duration::from_secs(2);
let start = Instant::now();
conns.clear();
state.ts_shutdown_event.store(true, Ordering::SeqCst);
for handle in state.started_wm_threads.values() {
handle.shutdown_event.store(true, Ordering::SeqCst);
}
let names: Vec<String> = state.started_wm_threads.keys().cloned().collect();
for name in names {
let elapsed = start.elapsed();
if elapsed >= total {
break;
}
if let Some(handle) = state.started_wm_threads.get_mut(&name) {
if let Some(t) = handle.thread.take() {
let remaining = total - elapsed;
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let _ = t.join();
let _ = tx.send(());
});
let _ = rx.recv_timeout(remaining);
}
}
}
let elapsed = start.elapsed();
if elapsed < total {
std::thread::sleep(total - elapsed);
}
}
pub fn main_loop(
listener: UnixListener,
is_daemon: bool,
render_fn: Arc<RenderFn>,
spawn_wm_fn: Arc<SpawnWmFn>,
) -> i32 {
listener.set_nonblocking(true).ok();
let mut conns: HashMap<RawFd, Conn> = HashMap::new();
let mut state = State::new();
let sigint = Arc::new(AtomicBool::new(false));
install_sigint_flag(sigint.clone());
let exit_code = loop {
if sigint.load(Ordering::SeqCst) {
break 0;
}
if let Some(code) = do_one(
&listener,
&mut conns,
is_daemon,
&mut state,
&render_fn,
&spawn_wm_fn,
) {
break code;
}
};
shutdown(&mut conns, &mut state);
exit_code
}
fn install_sigint_flag(flag: Arc<AtomicBool>) {
static SIGINT_FLAG: Mutex<Option<Arc<AtomicBool>>> = Mutex::new(None);
extern "C" fn handler(_sig: libc::c_int) {
if let Ok(g) = SIGINT_FLAG.lock() {
if let Some(f) = g.as_ref() {
f.store(true, Ordering::SeqCst);
}
}
}
if let Ok(mut g) = SIGINT_FLAG.lock() {
*g = Some(flag);
}
unsafe {
libc::signal(libc::SIGINT, handler as *const () as libc::sighandler_t);
}
}
pub fn daemonize() -> bool {
let pid = unsafe { libc::fork() };
if pid < 0 {
eprintln!(
"fork #1 failed: {} ({})",
std::io::Error::last_os_error().raw_os_error().unwrap_or(-1),
std::io::Error::last_os_error()
);
std::process::exit(1);
}
if pid > 0 {
std::process::exit(0);
}
let _ = std::env::set_current_dir("/");
unsafe {
libc::setsid();
}
unsafe {
libc::umask(0);
}
let pid = unsafe { libc::fork() };
if pid < 0 {
eprintln!(
"fork #2 failed: {} ({})",
std::io::Error::last_os_error().raw_os_error().unwrap_or(-1),
std::io::Error::last_os_error()
);
std::process::exit(1);
}
if pid > 0 {
std::process::exit(0);
}
redirect_std_to_devnull();
true
}
fn redirect_std_to_devnull() {
use std::fs::OpenOptions;
if let Ok(stdin) = OpenOptions::new().read(true).open("/dev/null") {
unsafe {
libc::dup2(stdin.as_raw_fd(), libc::STDIN_FILENO);
}
}
if let Ok(stdout) = OpenOptions::new()
.create(true)
.truncate(false)
.append(true)
.open("/dev/null")
{
unsafe {
libc::dup2(stdout.as_raw_fd(), libc::STDOUT_FILENO);
}
}
if let Ok(stderr) = OpenOptions::new()
.create(true)
.truncate(false)
.append(true)
.open("/dev/null")
{
unsafe {
libc::dup2(stderr.as_raw_fd(), libc::STDERR_FILENO);
}
}
}
pub fn check_existing(address: &str) -> std::io::Result<Option<UnixListener>> {
if !address.starts_with('\0') {
let _ = std::fs::remove_file(address);
}
match UnixListener::bind(address) {
Ok(l) => Ok(Some(l)),
Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => Ok(None),
Err(e) => Err(e),
}
}
pub fn kill_daemon(address: &str) -> bool {
match UnixStream::connect(address) {
Ok(mut sock) => {
let _ = eintr_retry_call(|| sock.write_all(EOF));
true
}
Err(_) => false,
}
}
pub fn cleanup_lockfile(pidfile: &str, from_signal_handler: bool) -> std::io::Result<()> {
std::fs::remove_file(pidfile).ok();
if from_signal_handler {
std::process::exit(1);
}
Ok(())
}
static SIGTERM_PIDFILE: Mutex<Option<std::ffi::CString>> = Mutex::new(None);
extern "C" fn sigterm_handler(_sig: libc::c_int) {
unsafe {
if let Ok(g) = SIGTERM_PIDFILE.lock() {
if let Some(path) = g.as_ref() {
libc::unlink(path.as_ptr());
}
}
libc::_exit(1);
}
}
pub struct PidLock {
pub pidfile: String,
pub fd: RawFd,
}
impl Drop for PidLock {
fn drop(&mut self) {
let _ = cleanup_lockfile(&self.pidfile, false);
unsafe {
libc::close(self.fd);
}
}
}
pub fn lockpidfile(pidfile: &str) -> Option<PidLock> {
use std::ffi::CString;
let cpath = CString::new(pidfile).ok()?;
let fd = unsafe {
libc::open(
cpath.as_ptr(),
libc::O_WRONLY | libc::O_CREAT,
(libc::S_IRUSR | libc::S_IWUSR | libc::S_IRGRP | libc::S_IROTH) as libc::c_uint,
)
};
if fd < 0 {
return None;
}
let flock = libc::flock {
l_type: libc::F_WRLCK as libc::c_short,
l_whence: libc::SEEK_SET as libc::c_short,
l_start: 0,
l_len: 0,
l_pid: 0,
#[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))]
l_sysid: 0,
};
let rc = unsafe { libc::fcntl(fd, libc::F_SETLK, &flock) };
if rc < 0 {
unsafe {
libc::close(fd);
}
return None;
}
unsafe {
libc::lseek(fd, 0, libc::SEEK_SET);
libc::ftruncate(fd, 0);
let pid_str = format!("{}", std::process::id());
libc::write(
fd,
pid_str.as_ptr() as *const libc::c_void,
pid_str.len() as libc::size_t,
);
libc::fsync(fd);
}
{
if let Ok(mut g) = SIGTERM_PIDFILE.lock() {
*g = CString::new(pidfile).ok();
}
unsafe {
libc::signal(
libc::SIGTERM,
sigterm_handler as *const () as libc::sighandler_t,
);
}
}
Some(PidLock {
pidfile: pidfile.to_string(),
fd,
})
}
#[derive(Debug, Default)]
pub struct DaemonArgs {
pub quiet: bool,
pub socket: Option<String>,
pub kill: bool,
pub foreground: bool,
pub replace: bool,
}
pub fn parse_daemon_argv(argv: &[String]) -> DaemonArgs {
let mut a = DaemonArgs::default();
let mut i = 0;
while i < argv.len() {
match argv[i].as_str() {
"-q" | "--quiet" => a.quiet = true,
"-s" | "--socket" => {
if let Some(v) = argv.get(i + 1) {
a.socket = Some(v.clone());
i += 1;
}
}
"-k" | "--kill" => a.kill = true,
"-f" | "--foreground" => a.foreground = true,
"-r" | "--replace" => a.replace = true,
_ => {}
}
i += 1;
}
a
}
pub fn main(argv: &[String], render_fn: Arc<RenderFn>, spawn_wm_fn: Arc<SpawnWmFn>) -> i32 {
let args = parse_daemon_argv(argv);
let mut is_daemon = false;
let mut address = if let Some(sock) = args.socket.as_ref() {
sock.clone()
} else {
let uid = unsafe { libc::getuid() };
if USE_FILESYSTEM() {
format!("/tmp/powerline-ipc-{}", uid)
} else {
format!("\0powerline-ipc-{}", uid)
}
};
let pidfile = if USE_FILESYSTEM() {
Some(format!("{}.pid", address))
} else {
None
};
if args.kill {
if args.foreground || args.replace {
eprintln!(
"powerline-daemon: --kill and --foreground/--replace cannot be used together"
);
return 2;
}
if kill_daemon(&address) {
if !args.quiet {
println!(
"Kill command sent to daemon, if it does not die in a couple of seconds use kill to kill it"
);
}
return 0;
} else {
if !args.quiet {
println!("No running daemon found");
}
return 1;
}
}
if args.replace {
while kill_daemon(&address) {
if !args.quiet {
println!(
"Kill command sent to daemon, waiting for daemon to exit, press Ctrl-C to terminate wait and exit"
);
}
std::thread::sleep(Duration::from_secs(2));
}
}
if USE_FILESYSTEM() && !args.foreground {
is_daemon = daemonize();
}
let _pid_guard = if let Some(pf) = pidfile.as_ref() {
match lockpidfile(pf) {
Some(g) => Some(g),
None => {
if !args.quiet {
eprintln!("The daemon is already running. Use powerline-daemon -k to kill it.");
}
return 1;
}
}
} else {
None
};
let listener = match check_existing(&address) {
Ok(Some(l)) => l,
Ok(None) => {
if !args.quiet {
eprintln!("The daemon is already running. Use powerline-daemon -k to kill it.");
}
return 1;
}
Err(e) => {
eprintln!("powerline-daemon: bind error: {}", e);
return 1;
}
};
if !USE_FILESYSTEM() && !args.foreground {
is_daemon = daemonize();
}
if address.starts_with('\0') {
address = address.replacen('\0', "@", 1);
}
let _ = address;
main_loop(listener, is_daemon, render_fn, spawn_wm_fn)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn use_filesystem_matches_platform() {
assert_eq!(USE_FILESYSTEM(), !cfg!(target_os = "linux"));
}
#[test]
fn eof_sentinel_value() {
assert_eq!(EOF, b"EOF\0\0");
}
#[test]
fn home_reads_from_env() {
let h = HOME();
if !h.is_empty() {
assert!(h.starts_with('/'));
}
}
#[test]
fn state_new_has_empty_collections() {
let s = State::new();
assert!(s.powerlines.is_empty());
assert!(s.started_wm_threads.is_empty());
assert!(s.logger.is_none());
assert!(s.config_loader.is_none());
assert!(!s.ts_shutdown_event.load(Ordering::SeqCst));
}
#[test]
fn non_interactive_arg_parser_default_constructible() {
let _p = NonInteractiveArgParser;
}
#[test]
fn non_daemon_shell_powerline_get_log_handler_no_op() {
let p = NonDaemonShellPowerline;
p.get_log_handler();
}
#[test]
fn eintr_retry_call_returns_ok() {
let r: std::io::Result<i32> = eintr_retry_call(|| Ok(42));
assert_eq!(r.unwrap(), 42);
}
#[test]
fn eintr_retry_call_retries_on_interrupt_then_succeeds() {
use std::cell::Cell;
let calls = Cell::new(0u32);
let r: std::io::Result<i32> = eintr_retry_call(|| {
calls.set(calls.get() + 1);
if calls.get() < 3 {
Err(std::io::Error::new(
std::io::ErrorKind::Interrupted,
"EINTR",
))
} else {
Ok(99)
}
});
assert_eq!(r.unwrap(), 99);
assert_eq!(calls.get(), 3);
}
#[test]
fn eintr_retry_call_propagates_non_interrupt_error() {
let r: std::io::Result<i32> = eintr_retry_call(|| Err(std::io::Error::other("other")));
assert!(r.is_err());
}
#[test]
fn safe_bytes_returns_utf8_bytes() {
assert_eq!(safe_bytes("hello"), b"hello".to_vec());
assert_eq!(safe_bytes("héllo"), "héllo".as_bytes().to_vec());
}
#[test]
fn parse_args_decodes_wire_format() {
let req: &[u8] = b"01\0shell\0/home\0HOME=/h\0";
let (args, environ, cwd) = parse_args(req).unwrap();
assert_eq!(args, vec!["shell".to_string()]);
assert_eq!(cwd, "/home");
assert_eq!(environ.get("HOME"), Some(&"/h".to_string()));
}
#[test]
fn parse_args_falls_back_to_pwd_when_cwd_missing() {
let req: &[u8] = b"00\0/explicit/cwd\0PWD=/tmp\0";
let (args, environ, cwd) = parse_args(req).unwrap();
assert!(args.is_empty());
assert_eq!(cwd, "/explicit/cwd");
assert_eq!(environ.get("PWD"), Some(&"/tmp".to_string()));
}
#[test]
fn parse_args_returns_none_on_empty() {
assert!(parse_args(&[]).is_none());
}
#[test]
fn parse_client_argv_handles_tmux_right() {
let argv = vec![
"tmux".to_string(),
"right".to_string(),
"-R".to_string(),
"pane_id=%0".to_string(),
];
let a = parse_client_argv(&argv);
assert_eq!(a.ext, vec!["tmux".to_string()]);
assert_eq!(a.side.as_deref(), Some("right"));
assert_eq!(
a.renderer_arg.as_ref().unwrap(),
&vec!["pane_id=%0".to_string()]
);
}
#[test]
fn parse_client_argv_handles_shell_with_width() {
let argv = vec![
"shell".to_string(),
"left".to_string(),
"-w".to_string(),
"120".to_string(),
"-r".to_string(),
".zsh".to_string(),
];
let a = parse_client_argv(&argv);
assert_eq!(a.ext, vec!["shell".to_string()]);
assert_eq!(a.side.as_deref(), Some("left"));
assert_eq!(a.width, Some(120));
assert_eq!(a.renderer_module.as_deref(), Some(".zsh"));
}
#[test]
fn powerline_key_collapses_identical_requests() {
let env = HashMap::new();
let args = Args {
ext: vec!["tmux".to_string()],
side: Some("right".to_string()),
renderer_module: Some(".tmux".to_string()),
..Default::default()
};
let k1 = PowerlineKey::from(&args, &env);
let k2 = PowerlineKey::from(&args, &env);
assert_eq!(k1, k2);
}
#[test]
fn powerline_key_distinguishes_distinct_ext() {
let env = HashMap::new();
let a1 = Args {
ext: vec!["tmux".to_string()],
..Default::default()
};
let a2 = Args {
ext: vec!["shell".to_string()],
..Default::default()
};
let k1 = PowerlineKey::from(&a1, &env);
let k2 = PowerlineKey::from(&a2, &env);
assert_ne!(k1, k2);
}
#[test]
fn render_invokes_closure_and_caches_key() {
let mut state = State::new();
let calls = Arc::new(std::sync::atomic::AtomicU32::new(0));
let calls_clone = calls.clone();
let render_fn: Arc<RenderFn> = Arc::new(move |_, _, _, _| {
calls_clone.fetch_add(1, Ordering::SeqCst);
b"RENDERED".to_vec()
});
let args = Args {
ext: vec!["tmux".to_string()],
side: Some("right".to_string()),
..Default::default()
};
let environ = HashMap::new();
let r = render(&args, &environ, "/", false, &mut state, &render_fn);
assert_eq!(r, b"RENDERED".to_vec());
assert_eq!(calls.load(Ordering::SeqCst), 1);
let k = PowerlineKey::from(&args, &environ);
assert!(state.powerlines.contains_key(&k));
let r2 = render(&args, &environ, "/", false, &mut state, &render_fn);
assert_eq!(r2, b"RENDERED".to_vec());
assert_eq!(state.powerlines.len(), 1);
}
#[test]
fn start_wm_is_idempotent() {
let mut state = State::new();
let spawn_count = Arc::new(std::sync::atomic::AtomicU32::new(0));
let spawn_count_c = spawn_count.clone();
let spawn_wm_fn: Arc<SpawnWmFn> = Arc::new(move |_name, t_evt, _pl_evt| {
spawn_count_c.fetch_add(1, Ordering::SeqCst);
let t = std::thread::spawn(move || {
while !t_evt.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(5));
}
});
Some(WmHandle {
thread: Some(t),
shutdown_event: Arc::new(AtomicBool::new(false)),
})
});
let args = Args {
ext: vec!["wm.i3".to_string()],
..Default::default()
};
let r1 = start_wm(&args, &mut state, &spawn_wm_fn);
let r2 = start_wm(&args, &mut state, &spawn_wm_fn);
assert!(r1.is_empty());
assert!(r2.is_empty());
assert_eq!(spawn_count.load(Ordering::SeqCst), 1);
if let Some(h) = state.started_wm_threads.get_mut("i3") {
h.shutdown_event.store(true, Ordering::SeqCst);
}
}
#[test]
fn start_wm_reports_unknown_wm_name() {
let mut state = State::new();
let spawn_wm_fn: Arc<SpawnWmFn> = Arc::new(|_, _, _| None);
let args = Args {
ext: vec!["wm.bogus".to_string()],
..Default::default()
};
let r = start_wm(&args, &mut state, &spawn_wm_fn);
assert_eq!(r, safe_bytes("Unknown WM: bogus"));
assert!(state.started_wm_threads.is_empty());
}
#[test]
fn get_answer_routes_shell_request_to_render() {
let mut state = State::new();
let render_fn: Arc<RenderFn> = Arc::new(|_, _, _, _| b"OK".to_vec());
let spawn_wm_fn: Arc<SpawnWmFn> = Arc::new(|_, _, _| None);
let req: &[u8] = b"02\0shell\0left\0/cwd\0HOME=/h\0";
let r = get_answer(req, true, &mut state, &render_fn, &spawn_wm_fn);
assert_eq!(r, b"OK".to_vec());
}
#[test]
fn get_answer_routes_wm_request_to_start_wm() {
let mut state = State::new();
let render_fn: Arc<RenderFn> = Arc::new(|_, _, _, _| b"WRONG".to_vec());
let spawn_wm_fn: Arc<SpawnWmFn> = Arc::new(|_, _, _| None);
let req: &[u8] = b"01\0wm.bogus\0/cwd\0";
let r = get_answer(req, true, &mut state, &render_fn, &spawn_wm_fn);
assert_eq!(r, safe_bytes("Unknown WM: bogus"));
}
#[test]
fn kill_daemon_returns_false_when_no_daemon_running() {
let r = kill_daemon("/tmp/powerline-test-nonexistent-socket-abc");
assert!(!r);
}
#[test]
fn check_existing_returns_listener_for_fresh_address() {
let p = std::env::temp_dir().join(format!(
"powerliners-daemon-test-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let r = check_existing(&p.to_string_lossy()).unwrap();
assert!(r.is_some());
drop(r);
let _ = std::fs::remove_file(&p);
}
#[test]
fn check_existing_returns_none_when_address_in_use() {
let p = std::env::temp_dir().join(format!(
"powerliners-daemon-busy-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let l1 = UnixListener::bind(&p).unwrap();
let path = p.to_string_lossy().to_string();
match UnixListener::bind(&path) {
Ok(_) => panic!("expected EADDRINUSE"),
Err(e) => assert_eq!(e.kind(), std::io::ErrorKind::AddrInUse),
}
drop(l1);
let _ = std::fs::remove_file(&p);
}
#[test]
fn cleanup_lockfile_unlinks_pidfile() {
let p = std::env::temp_dir().join(format!(
"powerliners-daemon-pid-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::write(&p, "12345").unwrap();
assert!(p.exists());
let _ = cleanup_lockfile(&p.to_string_lossy(), false);
assert!(!p.exists());
}
#[test]
fn lockpidfile_writes_pid_and_unlinks_on_drop() {
let p = std::env::temp_dir().join(format!(
"powerliners-lockpid-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let path = p.to_string_lossy().to_string();
let guard = lockpidfile(&path).expect("first lock");
assert!(p.exists());
let content = std::fs::read_to_string(&p).unwrap();
assert_eq!(content, format!("{}", std::process::id()));
drop(guard);
assert!(!p.exists());
}
#[test]
fn parse_daemon_argv_picks_flags() {
let a = parse_daemon_argv(&[
"-q".to_string(),
"--socket".to_string(),
"/tmp/s".to_string(),
"-r".to_string(),
]);
assert!(a.quiet);
assert_eq!(a.socket.as_deref(), Some("/tmp/s"));
assert!(a.replace);
assert!(!a.foreground);
assert!(!a.kill);
}
#[test]
fn do_read_returns_after_double_null_terminator() {
let p = std::env::temp_dir().join(format!(
"powerliners-doread-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let path = p.to_string_lossy().to_string();
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let writer_path = path.clone();
let t = std::thread::spawn(move || {
let mut s = UnixStream::connect(&writer_path).unwrap();
s.write_all(b"02\0shell\0left\0/cwd\0\0").unwrap();
});
let (mut conn, _) = listener.accept().unwrap();
let r = do_read(&mut conn, Duration::from_secs(2)).expect("got bytes");
assert!(r.ends_with(b"\0\0"));
t.join().unwrap();
let _ = std::fs::remove_file(&path);
}
#[test]
fn do_read_times_out_when_no_data() {
let p = std::env::temp_dir().join(format!(
"powerliners-doread-timeout-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let path = p.to_string_lossy().to_string();
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let writer_path = path.clone();
let t = std::thread::spawn(move || {
let _s = UnixStream::connect(&writer_path).unwrap();
std::thread::sleep(Duration::from_millis(300));
});
let (mut conn, _) = listener.accept().unwrap();
let start = Instant::now();
let r = do_read(&mut conn, Duration::from_millis(150));
let elapsed = start.elapsed();
assert!(r.is_none());
assert!(elapsed >= Duration::from_millis(100));
t.join().unwrap();
let _ = std::fs::remove_file(&path);
}
#[test]
fn do_write_is_best_effort_on_closed_peer() {
let p = std::env::temp_dir().join(format!(
"powerliners-dowrite-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let path = p.to_string_lossy().to_string();
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let writer_path = path.clone();
let t = std::thread::spawn(move || {
let s = UnixStream::connect(&writer_path).unwrap();
drop(s); });
let (mut conn, _) = listener.accept().unwrap();
t.join().unwrap();
do_write(&mut conn, b"hello");
let _ = std::fs::remove_file(&path);
}
#[test]
fn main_loop_exits_on_eof_sentinel() {
let p = std::env::temp_dir().join(format!(
"powerliners-mainloop-eof-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let path = p.to_string_lossy().to_string();
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let writer_path = path.clone();
let t = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
let mut s = UnixStream::connect(&writer_path).unwrap();
s.write_all(EOF).unwrap();
});
let render_fn: Arc<RenderFn> = Arc::new(|_, _, _, _| b"x".to_vec());
let spawn_wm_fn: Arc<SpawnWmFn> = Arc::new(|_, _, _| None);
let code = main_loop(listener, true, render_fn, spawn_wm_fn);
assert_eq!(code, 0);
t.join().unwrap();
let _ = std::fs::remove_file(&path);
}
#[test]
fn main_loop_renders_request_then_eofs() {
let p = std::env::temp_dir().join(format!(
"powerliners-mainloop-render-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let path = p.to_string_lossy().to_string();
let _ = std::fs::remove_file(&path);
let listener = UnixListener::bind(&path).unwrap();
let writer_path = path.clone();
let t = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(50));
let mut c1 = UnixStream::connect(&writer_path).unwrap();
c1.write_all(b"02\0tmux\0right\0/cwd\0HOME=/h\0\0").unwrap();
let mut buf = Vec::new();
c1.read_to_end(&mut buf).ok();
assert_eq!(buf, b"RENDERED".to_vec());
let mut c2 = UnixStream::connect(&writer_path).unwrap();
c2.write_all(EOF).unwrap();
});
let render_fn: Arc<RenderFn> = Arc::new(|_, _, _, _| b"RENDERED".to_vec());
let spawn_wm_fn: Arc<SpawnWmFn> = Arc::new(|_, _, _| None);
let code = main_loop(listener, true, render_fn, spawn_wm_fn);
assert_eq!(code, 0);
t.join().unwrap();
let _ = std::fs::remove_file(&path);
}
}