1#![allow(unsafe_code)]
10
11#[cfg(unix)]
12use std::io::{self, Write};
13use std::path::{Path, PathBuf};
14#[cfg(unix)]
15use std::time::Duration;
16
17use serde::{Deserialize, Serialize};
18
19use crate::error::ComputerError;
20
21std::thread_local! {
22 static SOCKET_PATH: std::cell::RefCell<Option<PathBuf>> = const { std::cell::RefCell::new(None) };
23}
24
25pub struct SocketPathGuard {
27 previous: Option<PathBuf>,
28}
29
30impl Drop for SocketPathGuard {
31 fn drop(&mut self) {
32 let previous = self.previous.take();
33 SOCKET_PATH.with(|slot| *slot.borrow_mut() = previous);
34 }
35}
36
37pub fn push_socket_path(path: impl Into<PathBuf>) -> SocketPathGuard {
39 let previous = SOCKET_PATH.with(|slot| slot.borrow_mut().replace(path.into()));
40 SocketPathGuard { previous }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct OverlayPoint {
46 pub x: i32,
48 pub y: i32,
50 pub session: String,
52}
53
54pub fn encode_point(x: i32, y: i32, session: &str) -> String {
56 let point = OverlayPoint {
57 x,
58 y,
59 session: session.to_string(),
60 };
61 let mut line = serde_json::to_string(&point).unwrap_or_else(|_| "{}".to_string());
62 line.push('\n');
63 line
64}
65
66pub fn parse_overlay_line(bytes: &[u8]) -> Result<OverlayPoint, &'static str> {
68 let text = std::str::from_utf8(bytes).map_err(|_| "overlay message is not utf-8 json")?;
69 let text = text.trim();
70 if text.is_empty() {
71 return Err("overlay message is empty");
72 }
73 if !text.starts_with('{') {
74 return Err("overlay message is not a json object");
75 }
76 let point: OverlayPoint =
77 serde_json::from_str(text).map_err(|_| "overlay message is missing x, y, or session")?;
78 Ok(point)
79}
80
81pub fn session_hue(session: &str) -> u32 {
83 let mut hash: u32 = 2_166_136_261;
84 for byte in session.bytes() {
85 hash ^= u32::from(byte);
86 hash = hash.wrapping_mul(16_777_619);
87 }
88 hash % 360
89}
90
91pub fn note_point(x: i32, y: i32, session: &str) -> Option<&'static str> {
96 let path = SOCKET_PATH
97 .with(|slot| slot.borrow().clone())
98 .or_else(production_socket)?;
99 if deliver(&path, x, y, session).is_err() {
100 Some("overlay helper not running")
101 } else {
102 None
103 }
104}
105
106#[cfg(all(not(test), target_os = "macos"))]
107fn production_socket() -> Option<PathBuf> {
108 Some(crate::cache::cache_dir().join("overlay.sock"))
109}
110
111#[cfg(not(all(not(test), target_os = "macos")))]
112fn production_socket() -> Option<PathBuf> {
113 None
114}
115
116pub fn serve() -> Result<(), ComputerError> {
118 crate::backend::serve_overlay()
119}
120
121fn deliver(path: &Path, x: i32, y: i32, session: &str) -> Result<(), ()> {
122 #[cfg(unix)]
123 {
124 unix_send(path, &encode_point(x, y, session))
125 }
126 #[cfg(not(unix))]
127 {
128 let _ = (path, x, y, session);
129 Err(())
130 }
131}
132
133#[cfg(unix)]
134fn unix_send(path: &Path, line: &str) -> Result<(), ()> {
135 let mut stream = connect_timeout(path, Duration::from_millis(200))?;
136 stream
137 .set_write_timeout(Some(Duration::from_millis(200)))
138 .map_err(|_| ())?;
139 stream.write_all(line.as_bytes()).map_err(|_| ())?;
140 Ok(())
141}
142
143#[cfg(unix)]
144fn connect_timeout(path: &Path, timeout: Duration) -> Result<std::os::unix::net::UnixStream, ()> {
145 use std::os::fd::FromRawFd;
146 use std::os::unix::ffi::OsStrExt;
147 let bytes = path.as_os_str().as_bytes();
148 unsafe {
152 let fd = libc::socket(libc::AF_UNIX, libc::SOCK_STREAM, 0);
153 if fd < 0 {
154 return Err(());
155 }
156 let flags = libc::fcntl(fd, libc::F_GETFL, 0);
157 if flags < 0 || libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
158 libc::close(fd);
159 return Err(());
160 }
161 let mut addr: libc::sockaddr_un = std::mem::zeroed();
162 addr.sun_family = libc::AF_UNIX as libc::sa_family_t;
163 if bytes.contains(&0) || bytes.len() >= addr.sun_path.len() {
164 libc::close(fd);
165 return Err(());
166 }
167 std::ptr::copy_nonoverlapping(
168 bytes.as_ptr(),
169 addr.sun_path.as_mut_ptr().cast::<u8>(),
170 bytes.len(),
171 );
172 let mut len = std::mem::offset_of!(libc::sockaddr_un, sun_path) + bytes.len();
173 if bytes.first().is_some_and(|byte| *byte != 0) {
174 len += 1;
175 }
176 let rc = libc::connect(fd, (&raw const addr).cast(), len as libc::socklen_t);
177 if rc < 0 {
178 let err = io::Error::last_os_error();
179 let in_progress = err.raw_os_error() == Some(libc::EINPROGRESS)
180 || err.raw_os_error() == Some(libc::EAGAIN)
181 || err.kind() == io::ErrorKind::WouldBlock;
182 if !in_progress {
183 libc::close(fd);
184 return Err(());
185 }
186 let mut pollfd = libc::pollfd {
187 fd,
188 events: libc::POLLOUT,
189 revents: 0,
190 };
191 let ms = timeout.as_millis().min(i32::MAX as u128) as libc::c_int;
192 if libc::poll(&mut pollfd, 1, ms) <= 0 {
193 libc::close(fd);
194 return Err(());
195 }
196 let mut so_error: libc::c_int = 0;
197 let mut so_len = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
198 let got = libc::getsockopt(
199 fd,
200 libc::SOL_SOCKET,
201 libc::SO_ERROR,
202 (&raw mut so_error).cast(),
203 &mut so_len,
204 );
205 if got < 0 || so_error != 0 {
206 libc::close(fd);
207 return Err(());
208 }
209 }
210 if flags >= 0 {
211 let _ = libc::fcntl(fd, libc::F_SETFL, flags);
212 }
213 Ok(std::os::unix::net::UnixStream::from_raw_fd(fd))
214 }
215}
216
217#[cfg(unix)]
219#[cfg_attr(not(any(test, target_os = "macos")), allow(dead_code))]
220pub(crate) fn bind_socket(path: &Path) -> io::Result<std::os::unix::net::UnixListener> {
221 use std::os::unix::fs::PermissionsExt;
222 use std::os::unix::net::UnixListener;
223 if let Some(parent) = path.parent() {
224 fs_create(parent)?;
225 fs_set_mode(parent, 0o700);
226 }
227 if path.exists() {
228 std::fs::remove_file(path)?;
229 }
230 let listener = UnixListener::bind(path)?;
231 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
232 Ok(listener)
233}
234
235#[cfg(unix)]
236#[cfg_attr(not(any(test, target_os = "macos")), allow(dead_code))]
237fn fs_create(path: &Path) -> io::Result<()> {
238 std::fs::create_dir_all(path)
239}
240
241#[cfg(unix)]
242#[cfg_attr(not(any(test, target_os = "macos")), allow(dead_code))]
243fn fs_set_mode(path: &Path, mode: u32) {
244 use std::os::unix::fs::PermissionsExt;
245 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode));
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 #[test]
253 fn parser_accepts_a_json_line_and_rejects_a_binary_blob() {
254 let line = encode_point(10, 20, "session-a");
255 let point = parse_overlay_line(line.as_bytes()).unwrap();
256 assert_eq!(
257 point,
258 OverlayPoint {
259 x: 10,
260 y: 20,
261 session: "session-a".into(),
262 }
263 );
264 assert!(parse_overlay_line(&[0xff, 0xfe, 0x00]).is_err());
265 assert!(parse_overlay_line(b"not-json\n").is_err());
266 assert!(session_hue("notes") < 360);
267 assert_eq!(session_hue("notes"), session_hue("notes"));
268 }
269
270 #[test]
271 fn missing_helper_warns_and_a_live_socket_receives_the_line() {
272 #[cfg(unix)]
274 let dir = std::path::PathBuf::from(format!("/tmp/cuo{}", std::process::id()));
275 #[cfg(not(unix))]
276 let dir = std::env::temp_dir().join(format!("cuo{}", std::process::id()));
277 let _ = std::fs::remove_dir_all(&dir);
278 std::fs::create_dir_all(&dir).unwrap();
279 let missing = dir.join("missing.sock");
280 let _guard = push_socket_path(&missing);
281 assert_eq!(note_point(3, 4, "s"), Some("overlay helper not running"));
282 drop(_guard);
283
284 #[cfg(unix)]
285 {
286 use std::io::BufRead;
287 let path = dir.join("overlay.sock");
288 let listener = bind_socket(&path).unwrap();
289 let _guard = push_socket_path(&path);
290 let server = std::thread::spawn(move || {
291 let (stream, _) = listener.accept().unwrap();
292 let mut line = String::new();
293 std::io::BufReader::new(stream)
294 .read_line(&mut line)
295 .unwrap();
296 line
297 });
298 assert_eq!(note_point(8, 9, "agent"), None);
299 let line = server.join().unwrap();
300 let point = parse_overlay_line(line.as_bytes()).unwrap();
301 assert_eq!(point.x, 8);
302 assert_eq!(point.y, 9);
303 assert_eq!(point.session, "agent");
304 }
305 let _ = std::fs::remove_dir_all(&dir);
306 }
307}