pub mod os_input_output; pub mod panes; pub mod tab; mod pty; mod route; mod screen; mod thread_bus; mod ui; mod wasm_vm; use zellij_utils::zellij_tile; use std::path::PathBuf; use std::sync::{Arc, Mutex, RwLock}; use std::thread; use wasmer::Store; use zellij_tile::data::{Event, Palette, PluginCapabilities}; use crate::{ os_input_output::ServerOsApi, pty::{pty_thread_main, Pty, PtyInstruction}, screen::{screen_thread_main, ScreenInstruction}, thread_bus::{Bus, ThreadSenders}, ui::layout::Layout, wasm_vm::{wasm_thread_main, PluginInstruction}, }; use route::route_thread_main; use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, cli::CliArgs, errors::{ContextType, ErrorInstruction, ServerContext}, input::{get_mode_info, options::Options}, ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, setup::get_default_data_dir, }; /// Instructions related to server-side application #[derive(Debug, Clone)] pub(crate) enum ServerInstruction { NewClient(ClientAttributes, Box, Box), Render(Option), UnblockInputThread, ClientExit, Error(String), DetachSession, AttachClient(ClientAttributes, bool, Options), } impl From for ServerInstruction { fn from(instruction: ClientToServerMsg) -> Self { match instruction { ClientToServerMsg::NewClient(attrs, opts, options) => { ServerInstruction::NewClient(attrs, opts, options) } ClientToServerMsg::AttachClient(attrs, force, options) => { ServerInstruction::AttachClient(attrs, force, options) } _ => unreachable!(), } } } impl From<&ServerInstruction> for ServerContext { fn from(server_instruction: &ServerInstruction) -> Self { match *server_instruction { ServerInstruction::NewClient(..) => ServerContext::NewClient, ServerInstruction::Render(_) => ServerContext::Render, ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread, ServerInstruction::ClientExit => ServerContext::ClientExit, ServerInstruction::Error(_) => ServerContext::Error, ServerInstruction::DetachSession => ServerContext::DetachSession, ServerInstruction::AttachClient(..) => ServerContext::AttachClient, } } } impl ErrorInstruction for ServerInstruction { fn error(err: String) -> Self { ServerInstruction::Error(err) } } pub(crate) struct SessionMetaData { pub senders: ThreadSenders, pub capabilities: PluginCapabilities, pub palette: Palette, screen_thread: Option>, pty_thread: Option>, wasm_thread: Option>, } impl Drop for SessionMetaData { fn drop(&mut self) { let _ = self.senders.send_to_pty(PtyInstruction::Exit); let _ = self.senders.send_to_screen(ScreenInstruction::Exit); let _ = self.senders.send_to_plugin(PluginInstruction::Exit); let _ = self.screen_thread.take().unwrap().join(); let _ = self.pty_thread.take().unwrap().join(); let _ = self.wasm_thread.take().unwrap().join(); } } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) enum SessionState { Attached, Detached, Uninitialized, } pub fn start_server(os_input: Box, socket_path: PathBuf) { #[cfg(not(any(feature = "test", test)))] daemonize::Daemonize::new() .working_directory(std::env::current_dir().unwrap()) .umask(0o077) // FIXME: My cherished `dbg!` was broken, so this is a hack to bring it back //.stderr(std::fs::File::create("dbg.log").unwrap()) .start() .expect("could not daemonize the server process"); std::env::set_var(&"ZELLIJ", "0"); let (to_server, server_receiver): ChannelWithContext = channels::bounded(50); let to_server = SenderWithContext::new(to_server); let session_data: Arc>> = Arc::new(RwLock::new(None)); let session_state = Arc::new(RwLock::new(SessionState::Uninitialized)); #[cfg(not(any(feature = "test", test)))] std::panic::set_hook({ use zellij_utils::errors::handle_panic; let to_server = to_server.clone(); Box::new(move |info| { handle_panic(info, &to_server); }) }); let thread_handles = Arc::new(Mutex::new(Vec::new())); #[cfg(any(feature = "test", test))] thread_handles.lock().unwrap().push( thread::Builder::new() .name("server_router".to_string()) .spawn({ let session_data = session_data.clone(); let os_input = os_input.clone(); let to_server = to_server.clone(); let session_state = session_state.clone(); move || route_thread_main(session_data, session_state, os_input, to_server) }) .unwrap(), ); #[cfg(not(any(feature = "test", test)))] let _ = thread::Builder::new() .name("server_listener".to_string()) .spawn({ use zellij_utils::{ interprocess::local_socket::LocalSocketListener, shared::set_permissions, }; let os_input = os_input.clone(); let session_data = session_data.clone(); let session_state = session_state.clone(); let to_server = to_server.clone(); let socket_path = socket_path.clone(); let thread_handles = thread_handles.clone(); move || { drop(std::fs::remove_file(&socket_path)); let listener = LocalSocketListener::bind(&*socket_path).unwrap(); set_permissions(&socket_path).unwrap(); for stream in listener.incoming() { match stream { Ok(stream) => { let mut os_input = os_input.clone(); os_input.update_receiver(stream); let session_data = session_data.clone(); let session_state = session_state.clone(); let to_server = to_server.clone(); thread_handles.lock().unwrap().push( thread::Builder::new() .name("server_router".to_string()) .spawn({ let session_data = session_data.clone(); let os_input = os_input.clone(); let to_server = to_server.clone(); move || { route_thread_main( session_data, session_state, os_input, to_server, ) } }) .unwrap(), ); } Err(err) => { panic!("err {:?}", err); } } } } }); loop { let (instruction, mut err_ctx) = server_receiver.recv().unwrap(); err_ctx.add_call(ContextType::IPCServer((&instruction).into())); match instruction { ServerInstruction::NewClient(client_attributes, opts, config_options) => { let session = init_session( os_input.clone(), opts, config_options, to_server.clone(), client_attributes, session_state.clone(), ); *session_data.write().unwrap() = Some(session); *session_state.write().unwrap() = SessionState::Attached; session_data .read() .unwrap() .as_ref() .unwrap() .senders .send_to_pty(PtyInstruction::NewTab) .unwrap(); } ServerInstruction::AttachClient(attrs, _, options) => { *session_state.write().unwrap() = SessionState::Attached; let rlock = session_data.read().unwrap(); let session_data = rlock.as_ref().unwrap(); session_data .senders .send_to_screen(ScreenInstruction::TerminalResize(attrs.position_and_size)) .unwrap(); let default_mode = options.default_mode.unwrap_or_default(); let mode_info = get_mode_info(default_mode, attrs.palette, session_data.capabilities); session_data .senders .send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone())) .unwrap(); session_data .senders .send_to_plugin(PluginInstruction::Update( None, Event::ModeUpdate(mode_info), )) .unwrap(); } ServerInstruction::UnblockInputThread => { if *session_state.read().unwrap() == SessionState::Attached { os_input.send_to_client(ServerToClientMsg::UnblockInputThread); } } ServerInstruction::ClientExit => { *session_data.write().unwrap() = None; os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); break; } ServerInstruction::DetachSession => { *session_state.write().unwrap() = SessionState::Detached; os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); os_input.remove_client_sender(); } ServerInstruction::Render(output) => { if *session_state.read().unwrap() == SessionState::Attached { // Here output is of the type Option sent by screen thread. // If `Some(_)`- unwrap it and forward it to the client to render. // If `None`- Send an exit instruction. This is the case when the user closes last Tab/Pane. if let Some(op) = output { os_input.send_to_client(ServerToClientMsg::Render(op)); } else { os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); break; } } } ServerInstruction::Error(backtrace) => { if *session_state.read().unwrap() == SessionState::Attached { os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Error(backtrace))); } break; } } } thread_handles .lock() .unwrap() .drain(..) .for_each(|h| drop(h.join())); #[cfg(not(any(feature = "test", test)))] drop(std::fs::remove_file(&socket_path)); } fn init_session( os_input: Box, opts: Box, config_options: Box, to_server: SenderWithContext, client_attributes: ClientAttributes, session_state: Arc>, ) -> SessionMetaData { let (to_screen, screen_receiver): ChannelWithContext = channels::unbounded(); let to_screen = SenderWithContext::new(to_screen); let (to_screen_bounded, bounded_screen_receiver): ChannelWithContext = channels::bounded(50); let to_screen_bounded = SenderWithContext::new(to_screen_bounded); let (to_plugin, plugin_receiver): ChannelWithContext = channels::unbounded(); let to_plugin = SenderWithContext::new(to_plugin); let (to_pty, pty_receiver): ChannelWithContext = channels::unbounded(); let to_pty = SenderWithContext::new(to_pty); // Determine and initialize the data directory let data_dir = opts.data_dir.unwrap_or_else(get_default_data_dir); let capabilities = PluginCapabilities { arrow_fonts: config_options.simplified_ui, }; // Don't use default layouts in tests, but do everywhere else #[cfg(not(any(feature = "test", test)))] let default_layout = Some(PathBuf::from("default")); #[cfg(any(feature = "test", test))] let default_layout = None; let layout_path = opts.layout_path; let maybe_layout = opts .layout .as_ref() .map(|p| Layout::from_dir(&p, &data_dir)) .or_else(|| layout_path.map(|p| Layout::new(&p))) .or_else(|| default_layout.map(|p| Layout::from_dir(&p, &data_dir))); let pty_thread = thread::Builder::new() .name("pty".to_string()) .spawn({ let pty = Pty::new( Bus::new( vec![pty_receiver], Some(&to_screen_bounded), None, Some(&to_plugin), Some(&to_server), Some(os_input.clone()), ), opts.debug, ); move || pty_thread_main(pty, maybe_layout) }) .unwrap(); let screen_thread = thread::Builder::new() .name("screen".to_string()) .spawn({ let screen_bus = Bus::new( vec![screen_receiver, bounded_screen_receiver], None, Some(&to_pty), Some(&to_plugin), Some(&to_server), Some(os_input.clone()), ); let max_panes = opts.max_panes; move || { screen_thread_main( screen_bus, max_panes, client_attributes, config_options, session_state, ); } }) .unwrap(); let wasm_thread = thread::Builder::new() .name("wasm".to_string()) .spawn({ let plugin_bus = Bus::new( vec![plugin_receiver], Some(&to_screen), Some(&to_pty), None, None, None, ); let store = Store::default(); move || wasm_thread_main(plugin_bus, store, data_dir) }) .unwrap(); SessionMetaData { senders: ThreadSenders { to_screen: Some(to_screen), to_pty: Some(to_pty), to_plugin: Some(to_plugin), to_server: None, }, capabilities, palette: client_attributes.palette, screen_thread: Some(screen_thread), pty_thread: Some(pty_thread), wasm_thread: Some(wasm_thread), } }