1use std::io::{BufRead, BufReader, Write};
18use std::os::unix::net::{UnixListener, UnixStream};
19use std::path::{Path, PathBuf};
20
21use crate::cmd::Command;
22
23const SOCKET_NAME: &str = "tree-space.sock";
25
26pub fn socket_path() -> PathBuf {
31 let dir = std::env::var_os("XDG_RUNTIME_DIR")
32 .map(PathBuf::from)
33 .filter(|p| !p.as_os_str().is_empty())
34 .unwrap_or_else(|| PathBuf::from("/tmp"));
35 dir.join(SOCKET_NAME)
36}
37
38pub fn deliver(cmd: &Command) -> bool {
41 deliver_at(&socket_path(), cmd)
42}
43
44pub fn deliver_at(path: &Path, cmd: &Command) -> bool {
46 let Ok(mut stream) = UnixStream::connect(path) else {
47 return false;
48 };
49 let line = format!("{}\n", cmd.encode());
50 if stream.write_all(line.as_bytes()).is_err() || stream.flush().is_err() {
51 return false;
52 }
53 true
54}
55
56pub fn bind() -> std::io::Result<UnixListener> {
63 bind_at(&socket_path())
64}
65
66pub fn bind_at(path: &Path) -> std::io::Result<UnixListener> {
68 if let Some(parent) = path.parent() {
69 let _ = std::fs::create_dir_all(parent);
70 }
71 match UnixListener::bind(path) {
72 Ok(listener) => Ok(listener),
73 Err(err) if err.kind() == std::io::ErrorKind::AddrInUse => {
74 if UnixStream::connect(path).is_ok() {
75 return Err(err);
77 }
78 let _ = std::fs::remove_file(path);
80 UnixListener::bind(path)
81 }
82 Err(err) => Err(err),
83 }
84}
85
86pub fn spawn_listener(listener: UnixListener, deliver: impl Fn(Command) + Send + 'static) {
90 std::thread::spawn(move || {
91 for connection in listener.incoming() {
92 let Ok(stream) = connection else { continue };
93 let mut reader = BufReader::new(stream);
94 let mut line = String::new();
95 if reader.read_line(&mut line).is_err() {
96 continue;
97 }
98 if let Some(cmd) = Command::decode(line.trim_end()) {
99 deliver(cmd);
100 }
101 }
102 });
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use std::sync::mpsc;
109 use std::time::Duration;
110
111 #[test]
112 fn deliver_and_listener_round_trip() {
113 let dir = tempfile::tempdir().unwrap();
114 let path = dir.path().join(SOCKET_NAME);
115 let listener = bind_at(&path).unwrap();
116
117 let (tx, rx) = mpsc::channel();
118 spawn_listener(listener, move |cmd| tx.send(cmd).unwrap());
119
120 let expected = Command {
121 side: Some(crate::config::PanelSide::Right),
122 roots: vec![PathBuf::from("/home/eolu/x")],
123 ..Command::default()
124 };
125 assert!(deliver_at(&path, &expected));
126 assert_eq!(rx.recv_timeout(Duration::from_secs(2)), Ok(expected));
127 }
128
129 #[test]
130 fn deliver_fails_without_a_server() {
131 let dir = tempfile::tempdir().unwrap();
132 let path = dir.path().join(SOCKET_NAME);
133 assert!(!deliver_at(&path, &Command::default()));
134 }
135
136 #[test]
137 fn deliver_toggle_round_trip() {
138 let dir = tempfile::tempdir().unwrap();
139 let path = dir.path().join(SOCKET_NAME);
140 let listener = bind_at(&path).unwrap();
141 let (tx, rx) = mpsc::channel();
142 spawn_listener(listener, move |cmd| tx.send(cmd).unwrap());
143 assert!(deliver_at(&path, &Command::default()));
144 assert_eq!(rx.recv_timeout(Duration::from_secs(2)), Ok(Command::default()));
145 }
146
147 #[test]
148 fn bind_replaces_a_stale_socket() {
149 let dir = tempfile::tempdir().unwrap();
150 let path = dir.path().join(SOCKET_NAME);
151 std::fs::write(&path, b"stale").unwrap();
153 let listener = bind_at(&path).unwrap();
154 assert!(listener.local_addr().is_ok());
155 }
156
157 #[test]
158 fn bind_refuses_when_a_live_server_holds_the_socket() {
159 let dir = tempfile::tempdir().unwrap();
160 let path = dir.path().join(SOCKET_NAME);
161 let _server = bind_at(&path).unwrap();
162 assert!(bind_at(&path).is_err());
165 }
166}