#![cfg(unix)]
use std::io::Write;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender, TrySendError};
use std::sync::{Arc, Mutex, MutexGuard};
use std::thread::JoinHandle;
use std::time::Duration;
use crate::mxc::contrast::{is_dark, Contrast};
use crate::mxc::wire::{ByeEvent, ByeReason, Colors, Message, Origin, ThemeEvent};
use crate::mxc::{now_ms, PROTOCOL_VERSION};
use crate::theme::Theme;
const PEER_QUEUE: usize = 64;
const WRITE_TIMEOUT: Duration = Duration::from_secs(2);
enum Outbound {
Theme(Arc<ThemeEvent>),
Bye(ByeReason),
}
struct Peer {
id: u64,
tx: SyncSender<Outbound>,
handle: Option<JoinHandle<()>>,
}
struct Inner {
peers: Vec<Peer>,
last: Option<Arc<ThemeEvent>>,
next_id: u64,
}
struct Shared {
inner: Mutex<Inner>,
closed: AtomicBool,
path: PathBuf,
}
impl Shared {
fn lock(&self) -> MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(|e| e.into_inner())
}
fn reap(&self, id: u64) {
self.lock().peers.retain(|p| p.id != id);
}
}
pub struct Publisher {
shared: Arc<Shared>,
}
impl Publisher {
pub fn bind(path: &Path) -> std::io::Result<Publisher> {
ignore_sigpipe();
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir)?;
}
let _ = std::fs::remove_file(path);
let listener = UnixListener::bind(path)?;
let shared = Arc::new(Shared {
inner: Mutex::new(Inner {
peers: Vec::new(),
last: None,
next_id: 0,
}),
closed: AtomicBool::new(false),
path: path.to_path_buf(),
});
let accept_shared = Arc::clone(&shared);
std::thread::Builder::new()
.name("mxc-accept".into())
.spawn(move || accept_loop(listener, accept_shared))?;
Ok(Publisher { shared })
}
pub fn publish(&self, origin: Origin, theme: &Theme, fade_ms: u32) {
if self.shared.closed.load(Ordering::Acquire) {
return;
}
let colors = Colors::from(theme);
let mut inner = self.shared.lock();
if inner.last.as_ref().is_some_and(|l| l.colors == colors) {
return;
}
let event = Arc::new(ThemeEvent {
v: PROTOCOL_VERSION,
seq: 0,
ts: now_ms(),
origin,
fade_ms,
is_dark: is_dark(theme.background),
colors,
contrast: Contrast::compute(&colors),
});
inner.last = Some(Arc::clone(&event));
inner.peers.retain(|p| {
match p.tx.try_send(Outbound::Theme(Arc::clone(&event))) {
Ok(()) => true,
Err(TrySendError::Full(_) | TrySendError::Disconnected(_)) => false,
}
});
}
pub fn subscriber_count(&self) -> usize {
self.shared.lock().peers.len()
}
pub fn shutdown(&self, reason: ByeReason) {
if self.shared.closed.swap(true, Ordering::AcqRel) {
return;
}
let _ = UnixStream::connect(&self.shared.path);
let _ = std::fs::remove_file(&self.shared.path);
let peers = std::mem::take(&mut self.shared.lock().peers);
for peer in &peers {
let _ = peer.tx.try_send(Outbound::Bye(reason));
}
for mut peer in peers {
drop(peer.tx);
if let Some(h) = peer.handle.take() {
let _ = h.join();
}
}
}
}
impl Drop for Publisher {
fn drop(&mut self) {
self.shutdown(ByeReason::Shutdown);
}
}
fn accept_loop(listener: UnixListener, shared: Arc<Shared>) {
for stream in listener.incoming() {
if shared.closed.load(Ordering::Acquire) {
return;
}
let Ok(stream) = stream else {
continue;
};
if stream.set_write_timeout(Some(WRITE_TIMEOUT)).is_err() {
continue;
}
let (tx, rx) = sync_channel::<Outbound>(PEER_QUEUE);
let mut inner = shared.lock();
let id = inner.next_id;
inner.next_id += 1;
if let Some(last) = inner.last.as_ref() {
let _ = tx.try_send(Outbound::Theme(Arc::clone(last)));
}
let peer_shared = Arc::clone(&shared);
let handle = std::thread::Builder::new()
.name(format!("mxc-peer-{id}"))
.spawn(move || {
peer_loop(stream, rx);
peer_shared.reap(id);
});
match handle {
Ok(handle) => inner.peers.push(Peer {
id,
tx,
handle: Some(handle),
}),
Err(_) => continue,
}
}
}
fn peer_loop(mut stream: UnixStream, rx: Receiver<Outbound>) {
let mut seq: u64 = 0;
while let Ok(item) = rx.recv() {
let msg = match item {
Outbound::Theme(event) => Message::Theme(ThemeEvent {
seq,
..(*event).clone()
}),
Outbound::Bye(reason) => Message::Bye(ByeEvent {
v: PROTOCOL_VERSION,
seq,
ts: now_ms(),
reason,
}),
};
let Ok(line) = msg.to_ndjson() else {
continue;
};
if stream.write_all(line.as_bytes()).is_err() || stream.flush().is_err() {
return;
}
seq += 1;
}
}
fn ignore_sigpipe() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
const SIGPIPE: i32 = 13;
const SIG_IGN: usize = 1;
extern "C" {
fn signal(signum: i32, handler: usize) -> usize;
}
unsafe {
signal(SIGPIPE, SIG_IGN);
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gradient::Rgb;
use crate::mxc::wire::OriginKind;
use crate::theme::TOKYONIGHT;
use std::io::{BufRead, BufReader};
use std::sync::atomic::AtomicU32;
use std::time::Instant;
const PATIENCE: Duration = Duration::from_secs(5);
fn sock() -> PathBuf {
static N: AtomicU32 = AtomicU32::new(0);
let n = N.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
std::env::temp_dir().join(format!("mxc{pid}-{n}.s"))
}
fn tweak(i: u8) -> Theme {
let mut t = TOKYONIGHT;
t.background = Rgb::new(i, 0x10, 0x18);
t
}
fn origin() -> Origin {
Origin::named(OriginKind::Builtin, "test")
}
struct Client(BufReader<UnixStream>);
impl Client {
fn connect(path: &Path) -> Client {
let s = UnixStream::connect(path).expect("connect");
s.set_read_timeout(Some(PATIENCE)).unwrap();
Client(BufReader::new(s))
}
fn next(&mut self) -> Option<Message> {
let mut line = String::new();
match self.0.read_line(&mut line) {
Ok(0) => None,
Ok(_) => Some(serde_json::from_str(&line).expect("valid MXC json")),
Err(e) => panic!("read failed: {e}"),
}
}
fn theme(&mut self) -> ThemeEvent {
match self.next().expect("expected a message, got EOF") {
Message::Theme(t) => t,
other => panic!("expected theme, got {other:?}"),
}
}
}
#[test]
fn snapshot_is_the_first_line_and_starts_at_seq_zero() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
pubr.publish(origin(), &tweak(1), 600);
let mut c = Client::connect(&path);
let ev = c.theme();
assert_eq!(ev.seq, 0, "the snapshot is always seq 0");
assert_eq!(ev.v, PROTOCOL_VERSION);
assert_eq!(ev.colors, Colors::from(&tweak(1)));
assert_eq!(ev.fade_ms, 600);
assert_eq!(ev.contrast, Contrast::compute(&ev.colors));
assert!(ev.is_dark);
}
#[test]
fn connecting_before_any_publish_yields_silence_not_a_fake_palette() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
let mut c = Client::connect(&path);
wait_for(|| pubr.subscriber_count() == 1);
pubr.publish(origin(), &tweak(7), 0);
let ev = c.theme();
assert_eq!(ev.seq, 0, "first message on a connection is seq 0");
assert_eq!(ev.colors, Colors::from(&tweak(7)));
}
#[test]
fn two_subscribers_both_receive_the_same_broadcast() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
pubr.publish(origin(), &tweak(1), 0);
let mut a = Client::connect(&path);
assert_eq!(a.theme().colors, Colors::from(&tweak(1)));
let mut b = Client::connect(&path);
assert_eq!(b.theme().colors, Colors::from(&tweak(1)));
pubr.publish(origin(), &tweak(2), 0);
let (ea, eb) = (a.theme(), b.theme());
assert_eq!(ea.colors, Colors::from(&tweak(2)));
assert_eq!(eb.colors, Colors::from(&tweak(2)));
assert_eq!(ea.ts, eb.ts, "one event, fanned out — not two builds");
}
#[test]
fn seq_is_per_connection_not_global() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
pubr.publish(origin(), &tweak(1), 0);
let mut a = Client::connect(&path);
assert_eq!(a.theme().seq, 0);
pubr.publish(origin(), &tweak(2), 0);
assert_eq!(a.theme().seq, 1);
let mut b = Client::connect(&path);
assert_eq!(b.theme().seq, 0, "late subscriber restarts at 0");
pubr.publish(origin(), &tweak(3), 0);
let (ea, eb) = (a.theme(), b.theme());
assert_eq!(ea.seq, 2);
assert_eq!(eb.seq, 1);
assert_eq!(ea.colors, eb.colors);
}
#[test]
fn identical_palettes_are_published_once() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
pubr.publish(origin(), &tweak(1), 0);
let mut c = Client::connect(&path);
assert_eq!(c.theme().colors, Colors::from(&tweak(1)));
pubr.publish(
Origin::named(OriginKind::AlbumArt, "different name"),
&tweak(1),
250,
);
pubr.publish(origin(), &tweak(2), 0);
let ev = c.theme();
assert_eq!(
ev.colors,
Colors::from(&tweak(2)),
"the duplicate must not appear on the wire at all"
);
assert_eq!(ev.seq, 1, "a deduped publish does not consume a seq");
}
#[test]
fn publishing_with_no_subscribers_is_free_and_safe() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
let start = Instant::now();
for i in 0..1_000u16 {
pubr.publish(origin(), &tweak(i as u8), 0);
}
assert_eq!(pubr.subscriber_count(), 0);
assert!(
start.elapsed() < PATIENCE,
"zero-consumer publish must be trivial, took {:?}",
start.elapsed()
);
}
#[test]
fn shutdown_sends_a_parseable_bye_as_the_last_line() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
pubr.publish(origin(), &tweak(1), 0);
let mut c = Client::connect(&path);
assert_eq!(c.theme().seq, 0);
pubr.shutdown(ByeReason::Reload);
pubr.shutdown(ByeReason::Shutdown);
let mut last = None;
while let Some(msg) = c.next() {
last = Some(msg);
}
match last.expect("stream ended without a bye") {
Message::Bye(b) => {
assert_eq!(b.reason, ByeReason::Reload);
assert_eq!(b.v, PROTOCOL_VERSION);
assert_eq!(b.seq, 1, "bye continues this connection's sequence");
}
other => panic!("last line must be bye, got {other:?}"),
}
assert!(!path.exists(), "shutdown must unlink the socket");
}
#[test]
fn a_wedged_subscriber_cannot_block_publish() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
let hung_path = path.clone();
let hung = std::thread::spawn(move || {
let s = UnixStream::connect(&hung_path).expect("connect");
ready_tx.send(()).unwrap();
std::thread::sleep(Duration::from_millis(600));
drop(s);
});
ready_rx.recv_timeout(PATIENCE).expect("client connected");
wait_for(|| pubr.subscriber_count() == 1);
let start = Instant::now();
for i in 0..(PEER_QUEUE as u16 * 8) {
pubr.publish(origin(), &tweak(i as u8), 0);
}
let elapsed = start.elapsed();
assert!(
elapsed < PATIENCE,
"publish blocked on a wedged peer: {elapsed:?} for {} sends",
PEER_QUEUE * 8
);
assert!(
elapsed < Duration::from_millis(600),
"publish appears coupled to consumer progress: {elapsed:?}"
);
hung.join().unwrap();
}
#[test]
fn a_disconnected_peer_is_reaped() {
let path = sock();
let pubr = Publisher::bind(&path).unwrap();
let c = Client::connect(&path);
wait_for(|| pubr.subscriber_count() == 1);
drop(c);
let start = Instant::now();
let mut i = 0u16;
while pubr.subscriber_count() > 0 && start.elapsed() < PATIENCE {
pubr.publish(origin(), &tweak(i as u8), 0);
i = i.wrapping_add(1);
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(pubr.subscriber_count(), 0, "dead peer was leaked");
}
#[test]
fn binding_over_a_stale_socket_file_succeeds() {
let path = sock();
{
let _dead = Publisher::bind(&path).unwrap();
std::mem::forget(_dead); }
assert!(path.exists(), "precondition: stale file is present");
let pubr = Publisher::bind(&path).unwrap();
pubr.publish(origin(), &tweak(9), 0);
let mut c = Client::connect(&path);
assert_eq!(c.theme().colors, Colors::from(&tweak(9)));
}
fn wait_for(mut cond: impl FnMut() -> bool) {
let start = Instant::now();
while start.elapsed() < PATIENCE {
if cond() {
return;
}
std::thread::sleep(Duration::from_millis(5));
}
panic!("condition never became true within {PATIENCE:?}");
}
}