zellij/zellij-server/src/lib.rs
Brooks J Rady a9ce13c1d2 feat(ui): added feature for an experimental resize
The `parametric_resize_beta` feature has been added that enables the new
resize system. This change also introduces some background tweaks that
start laying the groundwork for future resize work without breaking
functionality.
2021-06-03 13:05:52 +01:00

412 lines
15 KiB
Rust

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<CliArgs>, Box<Options>),
Render(Option<String>),
UnblockInputThread,
ClientExit,
Error(String),
DetachSession,
AttachClient(ClientAttributes, bool, Options),
}
impl From<ClientToServerMsg> 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<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>,
wasm_thread: Option<thread::JoinHandle<()>>,
}
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<dyn ServerOsApi>, 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<ServerInstruction> = channels::bounded(50);
let to_server = SenderWithContext::new(to_server);
let session_data: Arc<RwLock<Option<SessionMetaData>>> = 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<String> 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<dyn ServerOsApi>,
opts: Box<CliArgs>,
config_options: Box<Options>,
to_server: SenderWithContext<ServerInstruction>,
client_attributes: ClientAttributes,
session_state: Arc<RwLock<SessionState>>,
) -> SessionMetaData {
let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded();
let to_screen = SenderWithContext::new(to_screen);
let (to_screen_bounded, bounded_screen_receiver): ChannelWithContext<ScreenInstruction> =
channels::bounded(50);
let to_screen_bounded = SenderWithContext::new(to_screen_bounded);
let (to_plugin, plugin_receiver): ChannelWithContext<PluginInstruction> = channels::unbounded();
let to_plugin = SenderWithContext::new(to_plugin);
let (to_pty, pty_receiver): ChannelWithContext<PtyInstruction> = 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),
}
}