use crate::ipc::{ ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ResizeCause, ServerToClientMsg, }; use crate::pane_size::Size; use interprocess::local_socket::{prelude::*, ListenerOptions}; use std::sync::atomic::{AtomicU32, Ordering}; static IPC_COUNTER: AtomicU32 = AtomicU32::new(0); // --- Cross-platform IPC helpers --- // On Unix: use filesystem-based sockets (TempDir + PathBuf + GenericFilePath) // On Windows: use named pipes (no guard needed, String name + GenericNamespaced) #[cfg(unix)] type IpcGuard = tempfile::TempDir; #[cfg(unix)] type IpcName = std::path::PathBuf; #[cfg(windows)] type IpcGuard = (); #[cfg(windows)] type IpcName = String; #[cfg(unix)] fn new_ipc() -> (IpcGuard, IpcName) { let dir = tempfile::TempDir::new().expect("failed to create temp dir"); let id = IPC_COUNTER.fetch_add(1, Ordering::SeqCst); let path = dir .path() .join(format!("test-{}-{}.sock", std::process::id(), id)); (dir, path) } #[cfg(windows)] fn new_ipc() -> (IpcGuard, IpcName) { let id = IPC_COUNTER.fetch_add(1, Ordering::SeqCst); let name = format!( "@zellij-test-{}-{}-{}", std::process::id(), id, std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_nanos() ); ((), name) } #[cfg(unix)] fn bind_listener(name: &IpcName) -> interprocess::local_socket::Listener { use interprocess::local_socket::GenericFilePath; ListenerOptions::new() .name(name.as_path().to_fs_name::().unwrap()) .create_sync() .expect("bind failed") } #[cfg(windows)] fn bind_listener(name: &IpcName) -> interprocess::local_socket::Listener { use interprocess::local_socket::GenericNamespaced; ListenerOptions::new() .name(name.as_str().to_ns_name::().unwrap()) .create_sync() .expect("bind failed") } #[cfg(unix)] fn connect_stream(name: &IpcName) -> interprocess::local_socket::Stream { use interprocess::local_socket::{GenericFilePath, Stream as LocalSocketStream}; LocalSocketStream::connect(name.as_path().to_fs_name::().unwrap()) .expect("connect failed") } #[cfg(windows)] fn connect_stream(name: &IpcName) -> interprocess::local_socket::Stream { use interprocess::local_socket::{GenericNamespaced, Stream as LocalSocketStream}; LocalSocketStream::connect(name.as_str().to_ns_name::().unwrap()) .expect("connect failed") } #[cfg(unix)] fn try_connect(name: &IpcName) -> std::io::Result { use interprocess::local_socket::{GenericFilePath, Stream as LocalSocketStream}; LocalSocketStream::connect(name.as_path().to_fs_name::().unwrap()) } #[cfg(windows)] fn try_connect(name: &IpcName) -> std::io::Result { use interprocess::local_socket::{GenericNamespaced, Stream as LocalSocketStream}; LocalSocketStream::connect(name.as_str().to_ns_name::().unwrap()) } #[test] fn client_to_server_message_over_socket() { let (_guard, name) = new_ipc(); let listener = bind_listener(&name); let client = std::thread::spawn({ let name = name.clone(); move || { let stream = connect_stream(&name); let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); sender .send_client_msg(ClientToServerMsg::ConnStatus) .expect("send failed"); } }); let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(stream); let msg = receiver.recv_client_msg(); assert!(msg.is_some(), "should receive a message"); let (msg, _ctx) = msg.unwrap(); assert!( matches!(msg, ClientToServerMsg::ConnStatus), "should be ConnStatus, got: {:?}", msg ); client.join().expect("client thread panicked"); } #[test] fn server_to_client_message_over_socket() { let (_guard, name) = new_ipc(); let listener = bind_listener(&name); let server = std::thread::spawn(move || { let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); sender .send_server_msg(ServerToClientMsg::Connected) .expect("send failed"); }); let stream = connect_stream(&name); let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(stream); let msg = receiver.recv_server_msg(); assert!(msg.is_some(), "should receive a message"); let (msg, _ctx) = msg.unwrap(); assert!( matches!(msg, ServerToClientMsg::Connected), "should be Connected, got: {:?}", msg ); server.join().expect("server thread panicked"); } #[test] fn bidirectional_communication_via_fd_duplication() { let (_guard, name) = new_ipc(); let listener = bind_listener(&name); let server = std::thread::spawn(move || { let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); let mut receiver: IpcReceiverWithContext = sender.get_receiver(); sender .send_server_msg(ServerToClientMsg::Connected) .expect("send failed"); let msg = receiver.recv_client_msg(); assert!(msg.is_some(), "server should receive client message"); let (msg, _) = msg.unwrap(); assert!( matches!(msg, ClientToServerMsg::ConnStatus), "should be ConnStatus" ); }); let stream = connect_stream(&name); let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); let mut receiver: IpcReceiverWithContext = sender.get_receiver(); let msg = receiver.recv_server_msg(); assert!(msg.is_some(), "client should receive server message"); let (msg, _) = msg.unwrap(); assert!( matches!(msg, ServerToClientMsg::Connected), "should be Connected" ); sender .send_client_msg(ClientToServerMsg::ConnStatus) .expect("send failed"); server.join().expect("server thread panicked"); } #[test] fn multiple_messages_in_sequence() { let (_guard, name) = new_ipc(); let listener = bind_listener(&name); let client = std::thread::spawn({ let name = name.clone(); move || { let stream = connect_stream(&name); let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); sender .send_client_msg(ClientToServerMsg::ConnStatus) .expect("send 1 failed"); sender .send_client_msg(ClientToServerMsg::TerminalResize { new_size: Size { rows: 50, cols: 120, }, cause: ResizeCause::Viewport, }) .expect("send 2 failed"); sender .send_client_msg(ClientToServerMsg::KillSession) .expect("send 3 failed"); } }); let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(stream); let (msg1, _) = receiver.recv_client_msg().expect("missing message 1"); assert!(matches!(msg1, ClientToServerMsg::ConnStatus)); let (msg2, _) = receiver.recv_client_msg().expect("missing message 2"); match msg2 { ClientToServerMsg::TerminalResize { new_size, cause } => { assert_eq!(new_size.rows, 50); assert_eq!(new_size.cols, 120); assert_eq!(cause, ResizeCause::Viewport); }, other => panic!("expected TerminalResize, got: {:?}", other), } let (msg3, _) = receiver.recv_client_msg().expect("missing message 3"); assert!(matches!(msg3, ClientToServerMsg::KillSession)); client.join().expect("client thread panicked"); } #[test] fn receiver_returns_none_on_closed_connection() { let (_guard, name) = new_ipc(); let listener = bind_listener(&name); let client = std::thread::spawn({ let name = name.clone(); move || { let stream = connect_stream(&name); let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); sender .send_client_msg(ClientToServerMsg::ConnStatus) .expect("send failed"); } }); let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(stream); client.join().expect("client thread panicked"); let msg = receiver.recv_client_msg(); assert!(msg.is_some(), "should receive the sent message"); let msg = receiver.recv_client_msg(); assert!(msg.is_none(), "should return None after connection closed"); } #[cfg(unix)] #[test] fn is_socket_identifies_bound_unix_socket() { use std::os::unix::fs::FileTypeExt; let (_guard, name) = new_ipc(); let _listener = bind_listener(&name); let metadata = std::fs::metadata(&name).expect("metadata failed"); assert!( metadata.file_type().is_socket(), "a bound LocalSocketListener path should be identified as a socket" ); } #[cfg(unix)] #[test] fn is_socket_rejects_regular_file() { use std::os::unix::fs::FileTypeExt; let dir = tempfile::TempDir::new().expect("failed to create temp dir"); let file_path = dir.path().join("not_a_socket"); std::fs::write(&file_path, b"regular file").expect("write failed"); let metadata = std::fs::metadata(&file_path).expect("metadata failed"); assert!( !metadata.file_type().is_socket(), "a regular file should NOT be identified as a socket" ); } #[test] fn session_probe_accepts_responding_socket() { let (_guard, name) = new_ipc(); let listener = bind_listener(&name); let server = std::thread::spawn(move || { let stream = listener.incoming().next().unwrap().expect("accept failed"); let mut receiver: IpcReceiverWithContext = IpcReceiverWithContext::new(stream); let mut sender: IpcSenderWithContext = receiver.get_sender(); let msg = receiver.recv_client_msg(); assert!(matches!(msg, Some((ClientToServerMsg::ConnStatus, _)))); sender .send_server_msg(ServerToClientMsg::Connected) .expect("send failed"); }); let stream = connect_stream(&name); let mut sender: IpcSenderWithContext = IpcSenderWithContext::new(stream); sender .send_client_msg(ClientToServerMsg::ConnStatus) .expect("send failed"); let mut receiver: IpcReceiverWithContext = sender.get_receiver(); let result = receiver.recv_server_msg(); assert!( matches!(result, Some((ServerToClientMsg::Connected, _))), "probing a live session socket should return Connected" ); server.join().expect("server thread panicked"); } #[test] fn session_probe_rejects_dead_socket() { let (_guard, name) = new_ipc(); { let _listener = bind_listener(&name); } let result = try_connect(&name); assert!( result.is_err(), "connecting to a dead socket should fail (no listener)" ); } #[cfg(unix)] #[test] fn socket_directory_enumeration_finds_sockets() { use interprocess::local_socket::GenericFilePath; use std::os::unix::fs::FileTypeExt; let dir = tempfile::TempDir::new().expect("failed to create temp dir"); let sock_path = dir.path().join("test-session"); let _listener = ListenerOptions::new() .name(sock_path.as_path().to_fs_name::().unwrap()) .create_sync() .expect("bind failed"); let file_path = dir.path().join("not-a-session"); std::fs::write(&file_path, b"data").expect("write failed"); let entries: Vec = std::fs::read_dir(dir.path()) .expect("read_dir failed") .filter_map(|entry| { let entry = entry.ok()?; if entry.file_type().ok()?.is_socket() { entry.file_name().into_string().ok() } else { None } }) .collect(); assert_eq!(entries.len(), 1, "should find exactly one socket"); assert_eq!(entries[0], "test-session"); }