From 5c54bf18c21e60d82ef063b62337f6b545d914d3 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Mon, 27 Sep 2021 11:29:13 +0200 Subject: [PATCH] feat(sessions): mirrored sessions (#740) * feat(sessions): mirrored sessions * fix(tests): input units * style(fmt): make rustfmt happy * fix(tests): make mirrored sessions e2e test more robust * refactor(sessions): remove force attach * style(fmt): rustfmtify * docs(changelog): update change * fix(e2e): retry on all errors --- CHANGELOG.md | 1 + Cargo.lock | 1 + Makefile.toml | 2 +- src/main.rs | 15 +- src/tests/e2e/cases.rs | 96 +++++++- src/tests/e2e/remote_runner.rs | 178 ++++++++++---- ...lly_when_active_terminal_is_too_small.snap | 4 +- ..._tests__e2e__cases__mirrored_sessions.snap | 29 +++ zellij-client/Cargo.toml | 1 + zellij-client/src/input_handler.rs | 30 ++- zellij-client/src/lib.rs | 50 +++- zellij-client/src/unit/input_handler_tests.rs | 79 +++++-- zellij-server/src/lib.rs | 221 +++++++++++++----- zellij-server/src/os_input_output.rs | 101 +++----- zellij-server/src/route.rs | 81 +++++-- zellij-server/src/screen.rs | 13 +- zellij-server/src/tab.rs | 6 +- zellij-server/src/unit/screen_tests.rs | 47 ++-- zellij-server/src/unit/tab_tests.rs | 52 ++--- zellij-utils/src/cli.rs | 5 - zellij-utils/src/errors.rs | 33 ++- zellij-utils/src/ipc.rs | 9 +- 22 files changed, 730 insertions(+), 324 deletions(-) create mode 100644 src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap diff --git a/CHANGELOG.md b/CHANGELOG.md index 29e311d8..4481475c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * Feature: Add ability to solely specify the tab name in the `tabs` section (https://github.com/zellij-org/zellij/pull/722) * Feature: Plugins can be configured and the groundwork for "Headless" plugins has been laid (https://github.com/zellij-org/zellij/pull/660) * Automatically update `example/default.yaml` on release (https://github.com/zellij-org/zellij/pull/736) +* Feature: allow mirroring sessions in multiple terminal windows (https://github.com/zellij-org/zellij/pull/740) ## [0.17.0] - 2021-09-15 * New panes/tabs now open in CWD of focused pane (https://github.com/zellij-org/zellij/pull/691) diff --git a/Cargo.lock b/Cargo.lock index bf4ae0fd..27d84465 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2736,6 +2736,7 @@ dependencies = [ "log", "mio", "termbg", + "zellij-tile", "zellij-utils", ] diff --git a/Makefile.toml b/Makefile.toml index a5bb174e..79392a64 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -142,7 +142,7 @@ args = ["build", "--verbose", "--release", "--target", "${CARGO_MAKE_TASK_ARGS}" workspace = false dependencies = ["build-plugins", "build-dev-data-dir"] command = "cargo" -args = ["build", "--verbose", "--target", "x86_64-unknown-linux-musl"] +args = ["build", "--verbose", "--release", "--target", "x86_64-unknown-linux-musl"] # Run e2e tests - we mark the e2e tests as "ignored" so they will not be run with the normal ones [tasks.e2e-test] diff --git a/src/main.rs b/src/main.rs index 0a91ca65..90eeb96b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,7 +56,6 @@ pub fn main() { }; if let Some(Command::Sessions(Sessions::Attach { session_name, - force, create, options, })) = opts.command.clone() @@ -73,22 +72,14 @@ pub fn main() { (ClientInfo::New(session_name.unwrap()), layout) } else { ( - ClientInfo::Attach( - session_name.unwrap(), - force, - config_options.clone(), - ), + ClientInfo::Attach(session_name.unwrap(), config_options.clone()), None, ) } } else { assert_session(session); ( - ClientInfo::Attach( - session_name.unwrap(), - force, - config_options.clone(), - ), + ClientInfo::Attach(session_name.unwrap(), config_options.clone()), None, ) } @@ -106,7 +97,7 @@ pub fn main() { } } ActiveSession::One(session_name) => ( - ClientInfo::Attach(session_name, force, config_options.clone()), + ClientInfo::Attach(session_name, config_options.clone()), None, ), ActiveSession::Many => { diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 307986c1..77892463 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -153,20 +153,12 @@ pub fn cannot_split_terminals_vertically_when_active_terminal_is_too_small() { }, }) .add_step(Step { - name: "Send text to terminal", - instruction: |mut remote_terminal: RemoteTerminal| -> bool { - // this is just normal input that should be sent into the one terminal so that we can make - // sure we silently failed to split in the previous step - remote_terminal.send_key("Hi!".as_bytes()); - true - }, - }) - .add_step(Step { - name: "Wait for text to appear", + name: "Make sure only one pane appears", instruction: |remote_terminal: RemoteTerminal| -> bool { let mut step_is_complete = false; - if remote_terminal.cursor_position_is(6, 2) && remote_terminal.snapshot_contains("Hi!") + if remote_terminal.cursor_position_is(3, 2) && remote_terminal.snapshot_contains("...") { + // ... is the truncated tip line step_is_complete = true; } step_is_complete @@ -917,3 +909,85 @@ pub fn start_without_pane_frames() { .run_all_steps(); assert_snapshot!(last_snapshot); } + +#[test] +#[ignore] +pub fn mirrored_sessions() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + let mut test_attempts = 10; + let session_name = "mirrored_sessions"; + let mut last_snapshot = None; + loop { + // we run this test in a loop because there are some edge cases (especially in the CI) + // where the second runner times out and then we also need to restart the first runner + // if no test timed out, we break the loop and assert the snapshot + let mut first_runner = + RemoteRunner::new_with_session_name("mirrored_sessions", fake_win_size, session_name) + .add_step(Step { + name: "Split pane to the right", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() + && remote_terminal.cursor_position_is(3, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&SPLIT_RIGHT_IN_PANE_MODE); + // back to normal mode after split + remote_terminal.send_key(&ENTER); + step_is_complete = true; + } + step_is_complete + }, + }) + .add_step(Step { + name: "Wait for new pane to open", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) + && remote_terminal.tip_appears() + { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }); + first_runner.run_all_steps(); + + let mut second_runner = + RemoteRunner::new_existing_session("mirrored_sessions", fake_win_size, session_name) + .add_step(Step { + name: "Make sure session appears correctly", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(63, 2) + && remote_terminal.tip_appears() + { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }); + let last_test_snapshot = second_runner.run_all_steps(); + + if (first_runner.test_timed_out || second_runner.test_timed_out) && test_attempts >= 0 { + test_attempts -= 1; + continue; + } else { + last_snapshot = Some(last_test_snapshot); + break; + } + } + match last_snapshot { + Some(last_snapshot) => { + assert_snapshot!(last_snapshot); + } + None => { + panic!("test timed out before completing"); + } + } +} diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 7c4587f3..924babf6 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -10,7 +10,7 @@ use std::net::TcpStream; use std::path::Path; -const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/debug/zellij"; +const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij"; const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; const CONNECTION_USERNAME: &str = "test"; @@ -24,7 +24,7 @@ fn ssh_connect() -> ssh2::Session { sess.handshake().unwrap(); sess.userauth_password(CONNECTION_USERNAME, CONNECTION_PASSWORD) .unwrap(); - sess.set_timeout(20000); + sess.set_timeout(3000); sess } @@ -58,6 +58,27 @@ fn start_zellij(channel: &mut ssh2::Channel) { channel.flush().unwrap(); } +fn start_zellij_in_session(channel: &mut ssh2::Channel, session_name: &str) { + stop_zellij(channel); + channel + .write_all( + format!( + "{} --session {}\n", + ZELLIJ_EXECUTABLE_LOCATION, session_name + ) + .as_bytes(), + ) + .unwrap(); + channel.flush().unwrap(); +} + +fn attach_to_existing_session(channel: &mut ssh2::Channel, session_name: &str) { + channel + .write_all(format!("{} attach {}\n", ZELLIJ_EXECUTABLE_LOCATION, session_name).as_bytes()) + .unwrap(); + channel.flush().unwrap(); +} + fn start_zellij_without_frames(channel: &mut ssh2::Channel) { stop_zellij(channel); channel @@ -188,6 +209,9 @@ pub struct RemoteRunner { win_size: Size, layout_file_name: Option<&'static str>, without_frames: bool, + session_name: Option, + attach_to_existing: bool, + pub test_timed_out: bool, } impl RemoteRunner { @@ -216,10 +240,89 @@ impl RemoteRunner { test_name, currently_running_step: None, current_step_index: 0, - retries_left: 3, + retries_left: 10, win_size, layout_file_name: None, without_frames: false, + session_name: None, + attach_to_existing: false, + test_timed_out: false, + } + } + pub fn new_with_session_name( + test_name: &'static str, + win_size: Size, + session_name: &str, + ) -> Self { + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let vte_parser = vte::Parser::new(); + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let pane_geom = PaneGeom { + x: 0, + y: 0, + rows, + cols, + }; + let terminal_output = TerminalPane::new(0, pane_geom, Palette::default(), 0); // 0 is the pane index + setup_remote_environment(&mut channel, win_size); + start_zellij_in_session(&mut channel, &session_name); + RemoteRunner { + steps: vec![], + channel, + terminal_output, + vte_parser, + test_name, + currently_running_step: None, + current_step_index: 0, + retries_left: 10, + win_size, + layout_file_name: None, + without_frames: false, + session_name: Some(String::from(session_name)), + attach_to_existing: false, + test_timed_out: false, + } + } + pub fn new_existing_session( + test_name: &'static str, + win_size: Size, + session_name: &str, + ) -> Self { + let sess = ssh_connect(); + let mut channel = sess.channel_session().unwrap(); + let vte_parser = vte::Parser::new(); + let mut rows = Dimension::fixed(win_size.rows); + let mut cols = Dimension::fixed(win_size.cols); + rows.set_inner(win_size.rows); + cols.set_inner(win_size.cols); + let pane_geom = PaneGeom { + x: 0, + y: 0, + rows, + cols, + }; + let terminal_output = TerminalPane::new(0, pane_geom, Palette::default(), 0); // 0 is the pane index + setup_remote_environment(&mut channel, win_size); + attach_to_existing_session(&mut channel, &session_name); + RemoteRunner { + steps: vec![], + channel, + terminal_output, + vte_parser, + test_name, + currently_running_step: None, + current_step_index: 0, + retries_left: 10, + win_size, + layout_file_name: None, + without_frames: false, + session_name: Some(String::from(session_name)), + attach_to_existing: true, + test_timed_out: false, } } pub fn new_without_frames(test_name: &'static str, win_size: Size) -> Self { @@ -247,10 +350,13 @@ impl RemoteRunner { test_name, currently_running_step: None, current_step_index: 0, - retries_left: 3, + retries_left: 10, win_size, layout_file_name: None, without_frames: true, + session_name: None, + attach_to_existing: false, + test_timed_out: false, } } pub fn new_with_layout( @@ -283,10 +389,13 @@ impl RemoteRunner { test_name, currently_running_step: None, current_step_index: 0, - retries_left: 3, + retries_left: 10, win_size, layout_file_name: Some(layout_file_name), without_frames: false, + session_name: None, + attach_to_existing: false, + test_timed_out: false, } } pub fn add_step(mut self, step: Step) -> Self { @@ -296,16 +405,6 @@ impl RemoteRunner { pub fn replace_steps(&mut self, steps: Vec) { self.steps = steps; } - fn current_remote_terminal_state(&mut self) -> RemoteTerminal { - let current_snapshot = self.get_current_snapshot(); - let (cursor_x, cursor_y) = self.terminal_output.cursor_coordinates().unwrap_or((0, 0)); - RemoteTerminal { - cursor_x, - cursor_y, - current_snapshot, - channel: &mut self.channel, - } - } pub fn run_next_step(&mut self) { if let Some(next_step) = self.steps.get(self.current_step_index) { let current_snapshot = take_snapshot(&mut self.terminal_output); @@ -341,6 +440,24 @@ impl RemoteRunner { new_runner.replace_steps(self.steps.clone()); drop(std::mem::replace(self, new_runner)); self.run_all_steps() + } else if self.session_name.is_some() { + let mut new_runner = if self.attach_to_existing { + RemoteRunner::new_existing_session( + self.test_name, + self.win_size, + &self.session_name.as_ref().unwrap(), + ) + } else { + RemoteRunner::new_with_session_name( + self.test_name, + self.win_size, + &self.session_name.as_ref().unwrap(), + ) + }; + new_runner.retries_left = self.retries_left - 1; + new_runner.replace_steps(self.steps.clone()); + drop(std::mem::replace(self, new_runner)); + self.run_all_steps() } else { let mut new_runner = RemoteRunner::new(self.test_name, self.win_size); new_runner.retries_left = self.retries_left - 1; @@ -349,22 +466,6 @@ impl RemoteRunner { self.run_all_steps() } } - fn display_informative_error(&mut self) { - let test_name = self.test_name; - let current_step_name = self.currently_running_step.as_ref().cloned(); - match current_step_name { - Some(current_step) => { - let remote_terminal = self.current_remote_terminal_state(); - eprintln!("Timed out waiting for data on the SSH channel for test {}. Was waiting for step: {}", test_name, current_step); - eprintln!("{:?}", remote_terminal); - } - None => { - let remote_terminal = self.current_remote_terminal_state(); - eprintln!("Timed out waiting for data on the SSH channel for test {}. Haven't begun running steps yet.", test_name); - eprintln!("{:?}", remote_terminal); - } - } - } pub fn run_all_steps(&mut self) -> String { // returns the last snapshot loop { @@ -381,23 +482,16 @@ impl RemoteRunner { break; } } - Err(e) => { - if e.kind() == std::io::ErrorKind::TimedOut { - if self.retries_left > 0 { - return self.restart_test(); - } - self.display_informative_error(); - panic!("Timed out waiting for test"); + Err(_e) => { + if self.retries_left > 0 { + return self.restart_test(); } - panic!("Error while reading remote session: {}", e); + self.test_timed_out = true; } } } take_snapshot(&mut self.terminal_output) } - pub fn get_current_snapshot(&mut self) -> String { - take_snapshot(&mut self.terminal_output) - } } impl Drop for RemoteRunner { diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap index d37f23e3..796a596b 100644 --- a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__cannot_split_terminals_vertically_when_active_terminal_is_too_small.snap @@ -5,7 +5,7 @@ expression: last_snapshot --- Zellij ┌──────┐ -│$ Hi!█│ +│$ █ │ │ │ │ │ │ │ @@ -22,4 +22,4 @@ expression: last_snapshot │ │ └──────┘ Ctrl + - + ... diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap new file mode 100644 index 00000000..3223680c --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__mirrored_sessions.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: last_snapshot + +--- + Zellij (mirrored_sessions)  Tab #1  +┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +│$ ││$ █ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +│ ││ │ +└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  SCROLL  SESSION  QUIT  + Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes. diff --git a/zellij-client/Cargo.toml b/zellij-client/Cargo.toml index b7b61425..4c580f13 100644 --- a/zellij-client/Cargo.toml +++ b/zellij-client/Cargo.toml @@ -12,6 +12,7 @@ license = "MIT" mio = "0.7.11" termbg = "0.2.3" zellij-utils = { path = "../zellij-utils/", version = "0.18.0" } +zellij-tile = { path = "../zellij-tile/", version = "0.18.0" } log = "0.4.14" [dev-dependencies] diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 217b10f9..e78f59c1 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -8,15 +8,16 @@ use zellij_utils::{ termion, zellij_tile, }; -use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting}; +use crate::{ + os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting, InputInstruction, +}; use zellij_utils::{ - channels::{SenderWithContext, OPENCALLS}, - errors::ContextType, + channels::{Receiver, SenderWithContext, OPENCALLS}, + errors::{ContextType, ErrorContext}, input::{actions::Action, cast_termion_key, config::Config, keybinds::Keybinds}, ipc::{ClientToServerMsg, ExitReason}, }; -use termion::input::TermReadEventsAndRaw; use zellij_tile::data::{InputMode, Key}; /// Handles the dispatching of [`Action`]s according to the current @@ -31,6 +32,7 @@ struct InputHandler { send_client_instructions: SenderWithContext, should_exit: bool, pasting: bool, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, } impl InputHandler { @@ -42,6 +44,7 @@ impl InputHandler { options: Options, send_client_instructions: SenderWithContext, mode: InputMode, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, ) -> Self { InputHandler { mode, @@ -52,6 +55,7 @@ impl InputHandler { send_client_instructions, should_exit: false, pasting: false, + receive_input_instructions, } } @@ -71,10 +75,9 @@ impl InputHandler { if self.should_exit { break; } - let stdin_buffer = self.os_input.read_from_stdin(); - for key_result in stdin_buffer.events_and_raw() { - match key_result { - Ok((event, raw_bytes)) => match event { + match self.receive_input_instructions.recv() { + Ok((InputInstruction::KeyEvent(event, raw_bytes), _error_context)) => { + match event { termion::event::Event::Key(key) => { let key = cast_termion_key(key); self.handle_key(&key, raw_bytes); @@ -101,9 +104,12 @@ impl InputHandler { self.handle_unknown_key(raw_bytes); } } - }, - Err(err) => panic!("Encountered read error: {:?}", err), + } } + Ok((InputInstruction::SwitchToMode(input_mode), _error_context)) => { + self.mode = input_mode; + } + Err(err) => panic!("Encountered read error: {:?}", err), } } } @@ -179,6 +185,8 @@ impl InputHandler { should_break = true; } Action::SwitchToMode(mode) => { + // this is an optimistic update, we should get a SwitchMode instruction from the + // server later that atomically changes the mode as well self.mode = mode; self.os_input .send_to_server(ClientToServerMsg::Action(action)); @@ -224,6 +232,7 @@ pub(crate) fn input_loop( command_is_executing: CommandIsExecuting, send_client_instructions: SenderWithContext, default_mode: InputMode, + receive_input_instructions: Receiver<(InputInstruction, ErrorContext)>, ) { let _handler = InputHandler::new( os_input, @@ -232,6 +241,7 @@ pub(crate) fn input_loop( options, send_client_instructions, default_mode, + receive_input_instructions, ) .handle_input(); } diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index b4d63d27..92b87e70 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -14,12 +14,15 @@ use crate::{ command_is_executing::CommandIsExecuting, input_handler::input_loop, os_input_output::ClientOsApi, }; +use termion::input::TermReadEventsAndRaw; +use zellij_tile::data::InputMode; use zellij_utils::{ channels::{self, ChannelWithContext, SenderWithContext}, consts::{SESSION_NAME, ZELLIJ_IPC_PIPE}, errors::{ClientContext, ContextType, ErrorInstruction}, input::{actions::Action, config::Config, options::Options}, ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, + termion, }; use zellij_utils::{cli::CliArgs, input::layout::LayoutFromYaml}; @@ -30,6 +33,7 @@ pub(crate) enum ClientInstruction { Render(String), UnblockInputThread, Exit(ExitReason), + SwitchToMode(InputMode), } impl From for ClientInstruction { @@ -38,6 +42,9 @@ impl From for ClientInstruction { ServerToClientMsg::Exit(e) => ClientInstruction::Exit(e), ServerToClientMsg::Render(buffer) => ClientInstruction::Render(buffer), ServerToClientMsg::UnblockInputThread => ClientInstruction::UnblockInputThread, + ServerToClientMsg::SwitchToMode(input_mode) => { + ClientInstruction::SwitchToMode(input_mode) + } } } } @@ -49,6 +56,7 @@ impl From<&ClientInstruction> for ClientContext { ClientInstruction::Error(_) => ClientContext::Error, ClientInstruction::Render(_) => ClientContext::Render, ClientInstruction::UnblockInputThread => ClientContext::UnblockInputThread, + ClientInstruction::SwitchToMode(_) => ClientContext::SwitchToMode, } } } @@ -78,10 +86,16 @@ fn spawn_server(socket_path: &Path) -> io::Result<()> { #[derive(Debug, Clone)] pub enum ClientInfo { - Attach(String, bool, Options), + Attach(String, Options), New(String), } +#[derive(Debug, Clone)] +pub enum InputInstruction { + KeyEvent(termion::event::Event, Vec), + SwitchToMode(InputMode), +} + pub fn start_client( mut os_input: Box, opts: CliArgs, @@ -121,11 +135,11 @@ pub fn start_client( }; let first_msg = match info { - ClientInfo::Attach(name, force, config_options) => { + ClientInfo::Attach(name, config_options) => { SESSION_NAME.set(name).unwrap(); std::env::set_var(&"ZELLIJ_SESSION_NAME", SESSION_NAME.get().unwrap()); - ClientToServerMsg::AttachClient(client_attributes, force, config_options) + ClientToServerMsg::AttachClient(client_attributes, config_options) } ClientInfo::New(name) => { SESSION_NAME.set(name).unwrap(); @@ -159,6 +173,11 @@ pub fn start_client( > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + std::panic::set_hook({ use zellij_utils::errors::handle_panic; let send_client_instructions = send_client_instructions.clone(); @@ -171,6 +190,22 @@ pub fn start_client( let _stdin_thread = thread::Builder::new() .name("stdin_handler".to_string()) + .spawn({ + let os_input = os_input.clone(); + let send_input_instructions = send_input_instructions.clone(); + move || loop { + let stdin_buffer = os_input.read_from_stdin(); + for key_result in stdin_buffer.events_and_raw() { + let (key_event, raw_bytes) = key_result.unwrap(); + send_input_instructions + .send(InputInstruction::KeyEvent(key_event, raw_bytes)) + .unwrap(); + } + } + }); + + let _input_thread = thread::Builder::new() + .name("input_handler".to_string()) .spawn({ let send_client_instructions = send_client_instructions.clone(); let command_is_executing = command_is_executing.clone(); @@ -184,6 +219,7 @@ pub fn start_client( command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ) } }); @@ -239,12 +275,13 @@ pub fn start_client( os_input.disable_mouse(); let error = format!( "{}\n{}{}", - goto_start_of_last_line, restore_snapshot, backtrace + restore_snapshot, goto_start_of_last_line, backtrace ); let _ = os_input .get_stdout_writer() .write(error.as_bytes()) .unwrap(); + let _ = os_input.get_stdout_writer().flush().unwrap(); std::process::exit(1); }; @@ -280,6 +317,11 @@ pub fn start_client( ClientInstruction::UnblockInputThread => { command_is_executing.unblock_input_thread(); } + ClientInstruction::SwitchToMode(input_mode) => { + send_input_instructions + .send(InputInstruction::SwitchToMode(input_mode)) + .unwrap(); + } } } diff --git a/zellij-client/src/unit/input_handler_tests.rs b/zellij-client/src/unit/input_handler_tests.rs index 17bb1b0f..c2cf784b 100644 --- a/zellij-client/src/unit/input_handler_tests.rs +++ b/zellij-client/src/unit/input_handler_tests.rs @@ -3,8 +3,11 @@ use zellij_utils::input::actions::{Action, Direction}; use zellij_utils::input::config::Config; use zellij_utils::input::options::Options; use zellij_utils::pane_size::Size; +use zellij_utils::termion::event::Event; +use zellij_utils::termion::event::Key; use zellij_utils::zellij_tile::data::Palette; +use crate::InputInstruction; use crate::{os_input_output::ClientOsApi, ClientInstruction, CommandIsExecuting}; use std::path::Path; @@ -67,14 +70,12 @@ pub mod commands { } struct FakeClientOsApi { - stdin_events: Arc>>>, events_sent_to_server: Arc>>, command_is_executing: Arc>, } impl FakeClientOsApi { pub fn new( - mut stdin_events: Vec>, events_sent_to_server: Arc>>, command_is_executing: CommandIsExecuting, ) -> Self { @@ -82,10 +83,7 @@ impl FakeClientOsApi { // Arc here because we need interior mutability, otherwise we'll have to change the // ClientOsApi trait, and that will cause a lot of havoc let command_is_executing = Arc::new(Mutex::new(command_is_executing)); - stdin_events.push(commands::QUIT.to_vec()); - let stdin_events = Arc::new(Mutex::new(stdin_events)); // this is also done for interior mutability FakeClientOsApi { - stdin_events, events_sent_to_server, command_is_executing, } @@ -106,11 +104,7 @@ impl ClientOsApi for FakeClientOsApi { unimplemented!() } fn read_from_stdin(&self) -> Vec { - let mut stdin_events = self.stdin_events.lock().unwrap(); - if stdin_events.is_empty() { - panic!("ran out of stdin events!"); - } - stdin_events.remove(0) + unimplemented!() } fn box_clone(&self) -> Box { unimplemented!() @@ -156,11 +150,10 @@ fn extract_actions_sent_to_server( #[test] pub fn quit_breaks_input_loop() { - let stdin_events = vec![]; + let stdin_events = vec![(commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q')))]; let events_sent_to_server = Arc::new(Mutex::new(vec![])); let command_is_executing = CommandIsExecuting::new(); let client_os_api = Box::new(FakeClientOsApi::new( - stdin_events, events_sent_to_server.clone(), command_is_executing.clone(), )); @@ -172,6 +165,16 @@ pub fn quit_breaks_input_loop() { > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + for event in stdin_events { + send_input_instructions + .send(InputInstruction::KeyEvent(event.1, event.0)) + .unwrap(); + } + let default_mode = InputMode::Normal; input_loop( client_os_api, @@ -180,6 +183,7 @@ pub fn quit_breaks_input_loop() { command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ); let expected_actions_sent_to_server = vec![Action::Quit]; let received_actions = extract_actions_sent_to_server(events_sent_to_server); @@ -190,12 +194,18 @@ pub fn quit_breaks_input_loop() { } #[test] -pub fn move_focus_left_in_pane_mode() { - let stdin_events = vec![commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec()]; +pub fn move_focus_left_in_normal_mode() { + let stdin_events = vec![ + ( + commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(), + Event::Key(Key::Alt('h')), + ), + (commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q'))), + ]; + let events_sent_to_server = Arc::new(Mutex::new(vec![])); let command_is_executing = CommandIsExecuting::new(); let client_os_api = Box::new(FakeClientOsApi::new( - stdin_events, events_sent_to_server.clone(), command_is_executing.clone(), )); @@ -207,6 +217,16 @@ pub fn move_focus_left_in_pane_mode() { > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + for event in stdin_events { + send_input_instructions + .send(InputInstruction::KeyEvent(event.1, event.0)) + .unwrap(); + } + let default_mode = InputMode::Normal; input_loop( client_os_api, @@ -215,6 +235,7 @@ pub fn move_focus_left_in_pane_mode() { command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ); let expected_actions_sent_to_server = vec![Action::MoveFocusOrTab(Direction::Left), Action::Quit]; @@ -228,14 +249,23 @@ pub fn move_focus_left_in_pane_mode() { #[test] pub fn bracketed_paste() { let stdin_events = vec![ - commands::BRACKETED_PASTE_START.to_vec(), - commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(), - commands::BRACKETED_PASTE_END.to_vec(), + ( + commands::BRACKETED_PASTE_START.to_vec(), + Event::Unsupported(commands::BRACKETED_PASTE_START.to_vec()), + ), + ( + commands::MOVE_FOCUS_LEFT_IN_NORMAL_MODE.to_vec(), + Event::Key(Key::Alt('h')), + ), + ( + commands::BRACKETED_PASTE_END.to_vec(), + Event::Unsupported(commands::BRACKETED_PASTE_END.to_vec()), + ), + (commands::QUIT.to_vec(), Event::Key(Key::Ctrl('q'))), ]; let events_sent_to_server = Arc::new(Mutex::new(vec![])); let command_is_executing = CommandIsExecuting::new(); let client_os_api = Box::new(FakeClientOsApi::new( - stdin_events, events_sent_to_server.clone(), command_is_executing.clone(), )); @@ -247,6 +277,16 @@ pub fn bracketed_paste() { > = channels::bounded(50); let send_client_instructions = SenderWithContext::new(send_client_instructions); + let (send_input_instructions, receive_input_instructions): ChannelWithContext< + InputInstruction, + > = channels::bounded(50); + let send_input_instructions = SenderWithContext::new(send_input_instructions); + for event in stdin_events { + send_input_instructions + .send(InputInstruction::KeyEvent(event.1, event.0)) + .unwrap(); + } + let default_mode = InputMode::Normal; input_loop( client_os_api, @@ -255,6 +295,7 @@ pub fn bracketed_paste() { command_is_executing, send_client_instructions, default_mode, + receive_input_instructions, ); let expected_actions_sent_to_server = vec![ Action::Write(commands::BRACKETED_PASTE_START.to_vec()), diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 36569522..34b39fd8 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -11,11 +11,13 @@ mod ui; mod wasm_vm; use log::info; +use std::collections::HashMap; use std::{ path::PathBuf, sync::{Arc, Mutex, RwLock}, thread, }; +use zellij_utils::pane_size::Size; use zellij_utils::zellij_tile; use wasmer::Store; @@ -40,10 +42,12 @@ use zellij_utils::{ options::Options, plugins::PluginsConfig, }, - ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, + ipc::{ClientAttributes, ExitReason, ServerToClientMsg}, setup::get_default_data_dir, }; +pub(crate) type ClientId = u16; + /// Instructions related to server-side application #[derive(Debug, Clone)] pub(crate) enum ServerInstruction { @@ -52,28 +56,16 @@ pub(crate) enum ServerInstruction { Box, Box, LayoutFromYaml, + ClientId, Option, ), Render(Option), UnblockInputThread, - ClientExit, + ClientExit(ClientId), + RemoveClient(ClientId), Error(String), - DetachSession, - AttachClient(ClientAttributes, bool, Options), -} - -impl From for ServerInstruction { - fn from(instruction: ClientToServerMsg) -> Self { - match instruction { - ClientToServerMsg::NewClient(attrs, opts, options, layout, plugins) => { - ServerInstruction::NewClient(attrs, opts, options, layout, plugins) - } - ClientToServerMsg::AttachClient(attrs, force, options) => { - ServerInstruction::AttachClient(attrs, force, options) - } - _ => unreachable!(), - } - } + DetachSession(ClientId), + AttachClient(ClientAttributes, Options, ClientId), } impl From<&ServerInstruction> for ServerContext { @@ -82,9 +74,10 @@ impl From<&ServerInstruction> for ServerContext { ServerInstruction::NewClient(..) => ServerContext::NewClient, ServerInstruction::Render(_) => ServerContext::Render, ServerInstruction::UnblockInputThread => ServerContext::UnblockInputThread, - ServerInstruction::ClientExit => ServerContext::ClientExit, + ServerInstruction::ClientExit(..) => ServerContext::ClientExit, + ServerInstruction::RemoveClient(..) => ServerContext::RemoveClient, ServerInstruction::Error(_) => ServerContext::Error, - ServerInstruction::DetachSession => ServerContext::DetachSession, + ServerInstruction::DetachSession(..) => ServerContext::DetachSession, ServerInstruction::AttachClient(..) => ServerContext::AttachClient, } } @@ -117,14 +110,67 @@ impl Drop for SessionMetaData { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(crate) enum SessionState { - Attached, - Detached, - Uninitialized, +macro_rules! remove_client { + ($client_id:expr, $os_input:expr, $session_state:expr) => { + $os_input.remove_client($client_id); + $session_state.write().unwrap().remove_client($client_id); + }; } -pub fn start_server(os_input: Box, socket_path: PathBuf) { +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct SessionState { + clients: HashMap>, +} + +impl SessionState { + pub fn new() -> Self { + SessionState { + clients: HashMap::new(), + } + } + pub fn new_client(&mut self) -> ClientId { + let mut clients: Vec = self.clients.keys().copied().collect(); + clients.sort_unstable(); + let next_client_id = clients.last().unwrap_or(&0) + 1; + self.clients.insert(next_client_id, None); + next_client_id + } + pub fn remove_client(&mut self, client_id: ClientId) { + self.clients.remove(&client_id); + } + pub fn set_client_size(&mut self, client_id: ClientId, size: Size) { + self.clients.insert(client_id, Some(size)); + } + pub fn min_client_terminal_size(&self) -> Option { + // None if there are no client sizes + let mut rows: Vec = self + .clients + .values() + .filter_map(|size| size.map(|size| size.rows)) + .collect(); + rows.sort_unstable(); + let mut cols: Vec = self + .clients + .values() + .filter_map(|size| size.map(|size| size.cols)) + .collect(); + cols.sort_unstable(); + let min_rows = rows.first(); + let min_cols = cols.first(); + match (min_rows, min_cols) { + (Some(min_rows), Some(min_cols)) => Some(Size { + rows: *min_rows, + cols: *min_cols, + }), + _ => None, + } + } + pub fn client_ids(&self) -> Vec { + self.clients.keys().copied().collect() + } +} + +pub fn start_server(mut os_input: Box, socket_path: PathBuf) { info!("Starting Zellij server!"); daemonize::Daemonize::new() .working_directory(std::env::current_dir().unwrap()) @@ -137,7 +183,7 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { 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)); + let session_state = Arc::new(RwLock::new(SessionState::new())); std::panic::set_hook({ use zellij_utils::errors::handle_panic; @@ -170,7 +216,8 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { match stream { Ok(stream) => { let mut os_input = os_input.clone(); - os_input.update_receiver(stream); + let client_id = session_state.write().unwrap().new_client(); + let receiver = os_input.new_client(client_id, stream); let session_data = session_data.clone(); let session_state = session_state.clone(); let to_server = to_server.clone(); @@ -183,6 +230,8 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { session_state, os_input, to_server, + receiver, + client_id, ) }) .unwrap(), @@ -205,6 +254,7 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { opts, config_options, layout, + client_id, plugins, ) => { let session = init_session( @@ -220,7 +270,10 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { }, ); *session_data.write().unwrap() = Some(session); - *session_state.write().unwrap() = SessionState::Attached; + session_state + .write() + .unwrap() + .set_client_size(client_id, client_attributes.size); let default_shell = config_options.default_shell.map(|shell| { TerminalAction::RunCommand(RunCommand { @@ -248,17 +301,26 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { spawn_tabs(None); } } - ServerInstruction::AttachClient(attrs, _, options) => { - *session_state.write().unwrap() = SessionState::Attached; + ServerInstruction::AttachClient(attrs, options, client_id) => { let rlock = session_data.read().unwrap(); let session_data = rlock.as_ref().unwrap(); + session_state + .write() + .unwrap() + .set_client_size(client_id, attrs.size); + let min_size = session_state + .read() + .unwrap() + .min_client_terminal_size() + .unwrap(); session_data .senders - .send_to_screen(ScreenInstruction::TerminalResize(attrs.size)) + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) .unwrap(); let default_mode = options.default_mode.unwrap_or_default(); let mode_info = get_mode_info(default_mode, attrs.palette, session_data.capabilities); + let mode = mode_info.mode; session_data .senders .send_to_screen(ScreenInstruction::ChangeMode(mode_info.clone())) @@ -270,37 +332,86 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { Event::ModeUpdate(mode_info), )) .unwrap(); - } - ServerInstruction::UnblockInputThread => { - if *session_state.read().unwrap() == SessionState::Attached { - os_input.send_to_client(ServerToClientMsg::UnblockInputThread); + for client_id in session_state.read().unwrap().clients.keys() { + os_input.send_to_client(*client_id, ServerToClientMsg::SwitchToMode(mode)); } } - ServerInstruction::ClientExit => { - os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); - break; + ServerInstruction::UnblockInputThread => { + for client_id in session_state.read().unwrap().clients.keys() { + os_input.send_to_client(*client_id, ServerToClientMsg::UnblockInputThread); + } } - ServerInstruction::DetachSession => { - *session_state.write().unwrap() = SessionState::Detached; - os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal)); - os_input.remove_client_sender(); + ServerInstruction::ClientExit(client_id) => { + os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); + if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .unwrap(); + } + if session_state.read().unwrap().clients.is_empty() { + *session_data.write().unwrap() = None; + break; + } } - 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::RemoveClient(client_id) => { + remove_client!(client_id, os_input, session_state); + if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .unwrap(); + } + } + ServerInstruction::DetachSession(client_id) => { + os_input.send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); + if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() { + session_data + .write() + .unwrap() + .as_ref() + .unwrap() + .senders + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) + .unwrap(); + } + } + ServerInstruction::Render(mut output) => { + let client_ids = session_state.read().unwrap().client_ids(); + // Here the output is of the type Option sent by screen thread. + // If `Some(_)`- unwrap it and forward it to the clients to render. + // If `None`- Send an exit instruction. This is the case when a user closes the last Tab/Pane. + if let Some(op) = output.as_mut() { + for client_id in client_ids { + os_input.send_to_client(client_id, ServerToClientMsg::Render(op.clone())); } + } else { + for client_id in client_ids { + os_input + .send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal)); + remove_client!(client_id, os_input, session_state); + } + break; } } ServerInstruction::Error(backtrace) => { - if *session_state.read().unwrap() == SessionState::Attached { - os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Error(backtrace))); + let client_ids = session_state.read().unwrap().client_ids(); + for client_id in client_ids { + os_input.send_to_client( + client_id, + ServerToClientMsg::Exit(ExitReason::Error(backtrace.clone())), + ); + remove_client!(client_id, os_input, session_state); } break; } diff --git a/zellij-server/src/os_input_output.rs b/zellij-server/src/os_input_output.rs index e24b24b4..09c5e9d7 100644 --- a/zellij-server/src/os_input_output.rs +++ b/zellij-server/src/os_input_output.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + #[cfg(target_os = "macos")] use darwin_libproc; @@ -22,12 +24,8 @@ use nix::unistd::{self, ForkResult}; use signal_hook::consts::*; use zellij_tile::data::Palette; use zellij_utils::{ - errors::ErrorContext, input::command::{RunCommand, TerminalAction}, - ipc::{ - ClientToServerMsg, ExitReason, IpcReceiverWithContext, IpcSenderWithContext, - ServerToClientMsg, - }, + ipc::{ClientToServerMsg, IpcReceiverWithContext, IpcSenderWithContext, ServerToClientMsg}, shared::default_palette, }; @@ -37,6 +35,8 @@ use byteorder::{BigEndian, ByteOrder}; pub use nix::unistd::Pid; +use crate::ClientId; + pub(crate) fn set_terminal_size_using_fd(fd: RawFd, columns: u16, rows: u16) { // TODO: do this with the nix ioctl use libc::ioctl; @@ -230,8 +230,7 @@ pub fn spawn_terminal( #[derive(Clone)] pub struct ServerOsInputOutput { orig_termios: Arc>, - receive_instructions_from_client: Option>>>, - send_instructions_to_client: Arc>>>, + client_senders: Arc>>>, } // async fn in traits is not supported by rust, so dtolnay's excellent async_trait macro is being @@ -285,22 +284,13 @@ pub trait ServerOsApi: Send + Sync { fn force_kill(&self, pid: Pid) -> Result<(), nix::Error>; /// Returns a [`Box`] pointer to this [`ServerOsApi`] struct. fn box_clone(&self) -> Box; - /// Receives a message on server-side IPC channel - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext); - /// Sends a message to client - fn send_to_client(&self, msg: ServerToClientMsg); - /// Adds a sender to client - fn add_client_sender(&self); - /// Send to the temporary client - // A temporary client is the one that hasn't been registered as a client yet. - // Only the corresponding router thread has access to send messages to it. - // This can be the case when the client cannot attach to the session, - // so it tries to connect and then exits, hence temporary. - fn send_to_temp_client(&self, msg: ServerToClientMsg); - /// Removes the sender to client - fn remove_client_sender(&self); - /// Update the receiver socket for the client - fn update_receiver(&mut self, stream: LocalSocketStream); + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg); + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext; + fn remove_client(&mut self, client_id: ClientId); fn load_palette(&self) -> Palette; /// Returns the current working directory for a given pid fn get_cwd(&self, pid: Pid) -> Option; @@ -340,55 +330,29 @@ impl ServerOsApi for ServerOsInputOutput { let _ = kill(pid, Some(Signal::SIGKILL)); Ok(()) } - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) { - self.receive_instructions_from_client - .as_ref() - .unwrap() - .lock() - .unwrap() - .recv() - } - fn send_to_client(&self, msg: ServerToClientMsg) { - self.send_instructions_to_client - .lock() - .unwrap() - .as_mut() - .unwrap() - .send(msg); - } - fn add_client_sender(&self) { - let sender = self - .receive_instructions_from_client - .as_ref() - .unwrap() - .lock() - .unwrap() - .get_sender(); - let old_sender = self - .send_instructions_to_client - .lock() - .unwrap() - .replace(sender); - if let Some(mut sender) = old_sender { - sender.send(ServerToClientMsg::Exit(ExitReason::ForceDetached)); + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) { + if let Some(sender) = self.client_senders.lock().unwrap().get_mut(&client_id) { + sender.send(msg); } } - fn send_to_temp_client(&self, msg: ServerToClientMsg) { - self.receive_instructions_from_client - .as_ref() - .unwrap() + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext { + let receiver = IpcReceiverWithContext::new(stream); + let sender = receiver.get_sender(); + self.client_senders .lock() .unwrap() - .get_sender() - .send(msg); + .insert(client_id, sender); + receiver } - fn remove_client_sender(&self) { - assert!(self.send_instructions_to_client.lock().unwrap().is_some()); - *self.send_instructions_to_client.lock().unwrap() = None; - } - fn update_receiver(&mut self, stream: LocalSocketStream) { - self.receive_instructions_from_client = - Some(Arc::new(Mutex::new(IpcReceiverWithContext::new(stream)))); + fn remove_client(&mut self, client_id: ClientId) { + let mut client_senders = self.client_senders.lock().unwrap(); + if client_senders.contains_key(&client_id) { + client_senders.remove(&client_id); + } } fn load_palette(&self) -> Palette { default_palette() @@ -418,8 +382,7 @@ pub fn get_server_os_input() -> Result { let orig_termios = Arc::new(Mutex::new(current_termios)); Ok(ServerOsInputOutput { orig_termios, - receive_instructions_from_client: None, - send_instructions_to_client: Arc::new(Mutex::new(None)), + client_senders: Arc::new(Mutex::new(HashMap::new())), }) } diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index a88d04b0..3d1a5960 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -13,14 +13,17 @@ use zellij_utils::{ command::TerminalAction, get_mode_info, }, - ipc::{ClientToServerMsg, ExitReason, ServerToClientMsg}, + ipc::{ClientToServerMsg, IpcReceiverWithContext, ServerToClientMsg}, }; +use crate::ClientId; + fn route_action( action: Action, session: &SessionMetaData, _os_input: &dyn ServerOsApi, to_server: &SenderWithContext, + client_id: ClientId, ) -> bool { let mut should_break = false; session @@ -241,11 +244,15 @@ fn route_action( .unwrap(); } Action::Quit => { - to_server.send(ServerInstruction::ClientExit).unwrap(); + to_server + .send(ServerInstruction::ClientExit(client_id)) + .unwrap(); should_break = true; } Action::Detach => { - to_server.send(ServerInstruction::DetachSession).unwrap(); + to_server + .send(ServerInstruction::DetachSession(client_id)) + .unwrap(); should_break = true; } Action::LeftClick(point) => { @@ -282,47 +289,75 @@ pub(crate) fn route_thread_main( session_state: Arc>, os_input: Box, to_server: SenderWithContext, + mut receiver: IpcReceiverWithContext, + client_id: ClientId, ) { loop { - let (instruction, err_ctx) = os_input.recv_from_client(); + let (instruction, err_ctx) = receiver.recv(); err_ctx.update_thread_ctx(); let rlocked_sessions = session_data.read().unwrap(); match instruction { ClientToServerMsg::Action(action) => { if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { - if route_action(action, rlocked_sessions, &*os_input, &to_server) { + if let Action::SwitchToMode(input_mode) = action { + for client_id in session_state.read().unwrap().clients.keys() { + os_input.send_to_client( + *client_id, + ServerToClientMsg::SwitchToMode(input_mode), + ); + } + } + if route_action(action, rlocked_sessions, &*os_input, &to_server, client_id) { break; } } } ClientToServerMsg::TerminalResize(new_size) => { + session_state + .write() + .unwrap() + .set_client_size(client_id, new_size); + let min_size = session_state + .read() + .unwrap() + .min_client_terminal_size() + .unwrap(); rlocked_sessions .as_ref() .unwrap() .senders - .send_to_screen(ScreenInstruction::TerminalResize(new_size)) + .send_to_screen(ScreenInstruction::TerminalResize(min_size)) .unwrap(); } - ClientToServerMsg::NewClient(..) => { - if *session_state.read().unwrap() != SessionState::Uninitialized { - os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::Error( - "Cannot add new client".into(), - ))); - } else { - os_input.add_client_sender(); - to_server.send(instruction.into()).unwrap(); - } + ClientToServerMsg::NewClient( + client_attributes, + cli_args, + opts, + layout, + plugin_config, + ) => { + let new_client_instruction = ServerInstruction::NewClient( + client_attributes, + cli_args, + opts, + layout, + client_id, + plugin_config, + ); + to_server.send(new_client_instruction).unwrap(); } - ClientToServerMsg::AttachClient(_, force, _) => { - if *session_state.read().unwrap() == SessionState::Attached && !force { - os_input.send_to_temp_client(ServerToClientMsg::Exit(ExitReason::CannotAttach)); - } else { - os_input.add_client_sender(); - to_server.send(instruction.into()).unwrap(); - } + ClientToServerMsg::AttachClient(client_attributes, opts) => { + let attach_client_instruction = + ServerInstruction::AttachClient(client_attributes, opts, client_id); + to_server.send(attach_client_instruction).unwrap(); + } + ClientToServerMsg::ClientExited => { + // we don't unwrap this because we don't really care if there's an error here (eg. + // if the main server thread exited before this router thread did) + let _ = to_server.send(ServerInstruction::RemoveClient(client_id)); + break; } - ClientToServerMsg::ClientExited => break, } } } diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 5e1addb5..01bdc990 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -252,12 +252,10 @@ impl Screen { .unwrap(); if self.tabs.is_empty() { self.active_tab_index = None; - if *self.session_state.read().unwrap() == SessionState::Attached { - self.bus - .senders - .send_to_server(ServerInstruction::Render(None)) - .unwrap(); - } + self.bus + .senders + .send_to_server(ServerInstruction::Render(None)) + .unwrap(); } else { if let Some(tab) = self.get_active_tab() { tab.visible(false); @@ -288,9 +286,6 @@ impl Screen { /// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. pub fn render(&mut self) { - if *self.session_state.read().unwrap() != SessionState::Attached { - return; - } if let Some(active_tab) = self.get_active_tab_mut() { if active_tab.get_active_pane().is_some() { active_tab.render(); diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index fa62cfce..92085955 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -725,12 +725,10 @@ impl Tab { } } pub fn render(&mut self) { - if self.active_terminal.is_none() - || *self.session_state.read().unwrap() != SessionState::Attached - { + if self.active_terminal.is_none() || self.session_state.read().unwrap().clients.is_empty() { // we might not have an active terminal if we closed the last pane // in that case, we should not render as the app is exiting - // or if this session is not attached to a client, we do not have to render + // or if there are no attached clients to this session return; } self.senders diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index e1df2c64..186bdf7f 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -3,13 +3,14 @@ use crate::zellij_tile::data::{ModeInfo, Palette}; use crate::{ os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, thread_bus::Bus, - SessionState, + ClientId, SessionState, }; use std::convert::TryInto; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::command::TerminalAction; use zellij_utils::input::layout::LayoutTemplate; +use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::Size; use std::os::unix::io::RawFd; @@ -18,7 +19,6 @@ use zellij_utils::ipc::ClientAttributes; use zellij_utils::nix; use zellij_utils::{ - errors::ErrorContext, interprocess::local_socket::LocalSocketStream, ipc::{ClientToServerMsg, ServerToClientMsg}, }; @@ -27,49 +27,44 @@ use zellij_utils::{ struct FakeInputOutput {} impl ServerOsApi for FakeInputOutput { - fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { + fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16) { // noop } fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { unimplemented!() } - fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { + fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result { unimplemented!() } - fn async_file_reader(&self, _fd: RawFd) -> Box { + fn async_file_reader(&self, fd: RawFd) -> Box { unimplemented!() } - fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result { + fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result { unimplemented!() } - fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> { + fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error> { + unimplemented!() + } + fn kill(&self, pid: Pid) -> Result<(), nix::Error> { + unimplemented!() + } + fn force_kill(&self, pid: Pid) -> Result<(), nix::Error> { unimplemented!() } fn box_clone(&self) -> Box { Box::new((*self).clone()) } - fn force_kill(&self, _pid: Pid) -> Result<(), nix::Error> { + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) { unimplemented!() } - fn kill(&self, _pid: Pid) -> Result<(), nix::Error> { + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext { unimplemented!() } - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) { - unimplemented!() - } - fn send_to_client(&self, _msg: ServerToClientMsg) { - unimplemented!() - } - fn add_client_sender(&self) { - unimplemented!() - } - fn send_to_temp_client(&self, _msg: ServerToClientMsg) { - unimplemented!() - } - fn remove_client_sender(&self) { - unimplemented!() - } - fn update_receiver(&mut self, _stream: LocalSocketStream) { + fn remove_client(&mut self, client_id: ClientId) { unimplemented!() } fn load_palette(&self) -> Palette { @@ -90,7 +85,7 @@ fn create_new_screen(size: Size) -> Screen { }; let max_panes = None; let mode_info = ModeInfo::default(); - let session_state = Arc::new(RwLock::new(SessionState::Attached)); + let session_state = Arc::new(RwLock::new(SessionState::new())); Screen::new( bus, &client_attributes, diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index 7f0fa71f..17bd98be 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -4,12 +4,13 @@ use crate::{ os_input_output::{AsyncReader, ChildId, Pid, ServerOsApi}, panes::PaneId, thread_bus::ThreadSenders, - SessionState, + ClientId, SessionState, }; use std::convert::TryInto; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::layout::LayoutTemplate; +use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::Size; use std::os::unix::io::RawFd; @@ -17,58 +18,53 @@ use std::os::unix::io::RawFd; use zellij_utils::nix; use zellij_utils::{ - errors::ErrorContext, input::command::TerminalAction, interprocess::local_socket::LocalSocketStream, ipc::{ClientToServerMsg, ServerToClientMsg}, }; +#[derive(Clone)] struct FakeInputOutput {} impl ServerOsApi for FakeInputOutput { - fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { + fn set_terminal_size_using_fd(&self, fd: RawFd, cols: u16, rows: u16) { // noop } fn spawn_terminal(&self, _file_to_open: TerminalAction) -> (RawFd, ChildId) { unimplemented!() } - fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { + fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result { unimplemented!() } - fn async_file_reader(&self, _fd: RawFd) -> Box { + fn async_file_reader(&self, fd: RawFd) -> Box { unimplemented!() } - fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result { + fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result { unimplemented!() } - fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> { + fn tcdrain(&self, fd: RawFd) -> Result<(), nix::Error> { + unimplemented!() + } + fn kill(&self, pid: Pid) -> Result<(), nix::Error> { + unimplemented!() + } + fn force_kill(&self, pid: Pid) -> Result<(), nix::Error> { unimplemented!() } fn box_clone(&self) -> Box { + Box::new((*self).clone()) + } + fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) { unimplemented!() } - fn force_kill(&self, _pid: Pid) -> Result<(), nix::Error> { + fn new_client( + &mut self, + client_id: ClientId, + stream: LocalSocketStream, + ) -> IpcReceiverWithContext { unimplemented!() } - fn kill(&self, _pid: Pid) -> Result<(), nix::Error> { - unimplemented!() - } - fn recv_from_client(&self) -> (ClientToServerMsg, ErrorContext) { - unimplemented!() - } - fn send_to_client(&self, _msg: ServerToClientMsg) { - unimplemented!() - } - fn add_client_sender(&self) { - unimplemented!() - } - fn send_to_temp_client(&self, _msg: ServerToClientMsg) { - unimplemented!() - } - fn remove_client_sender(&self) { - unimplemented!() - } - fn update_receiver(&mut self, _stream: LocalSocketStream) { + fn remove_client(&mut self, client_id: ClientId) { unimplemented!() } fn load_palette(&self) -> Palette { @@ -88,7 +84,7 @@ fn create_new_tab(size: Size) -> Tab { let max_panes = None; let mode_info = ModeInfo::default(); let colors = Palette::default(); - let session_state = Arc::new(RwLock::new(SessionState::Attached)); + let session_state = Arc::new(RwLock::new(SessionState::new())); let mut tab = Tab::new( index, position, diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index eb52589a..452033b8 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -81,11 +81,6 @@ pub enum Sessions { /// Name of the session to attach to. session_name: Option, - /// Force attach- session will detach from the other - /// zellij client (if any) and attach to this. - #[structopt(long, short)] - force: bool, - /// Create a session if one does not exist. #[structopt(short, long)] create: bool, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 87c3bb4d..dc9a9789 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -60,8 +60,37 @@ where ), }; + let one_line_backtrace = match (info.location(), msg) { + (Some(location), Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}': {}:{}\n\u{1b}[0;0m", + err_ctx, + thread, + msg, + location.file(), + location.line(), + ), + (Some(location), None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked: {}:{}\n\u{1b}[0;0m", + err_ctx, + thread, + location.file(), + location.line(), + ), + (None, Some(msg)) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked at '{}'\n\u{1b}[0;0m", + err_ctx, thread, msg + ), + (None, None) => format!( + "{}\n\u{1b}[0;0mError: \u{1b}[0;31mthread '{}' panicked\n\u{1b}[0;0m", + err_ctx, thread + ), + }; + if thread == "main" { - println!("{}", backtrace); + // here we only show the first line because the backtrace is not readable otherwise + // a better solution would be to escape raw mode before we do this, but it's not trivial + // to get os_input here + println!("\u{1b}[2J{}", one_line_backtrace); process::exit(1); } else { let _ = sender.send(T::error(backtrace)); @@ -262,6 +291,7 @@ pub enum ClientContext { UnblockInputThread, Render, ServerError, + SwitchToMode, } /// Stack call representations corresponding to the different types of [`ServerInstruction`]s. @@ -271,6 +301,7 @@ pub enum ServerContext { Render, UnblockInputThread, ClientExit, + RemoveClient, Error, DetachSession, AttachClient, diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index 1b883d2a..5c327022 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -16,7 +16,7 @@ use std::{ os::unix::io::{AsRawFd, FromRawFd}, }; -use zellij_tile::data::Palette; +use zellij_tile::data::{InputMode, Palette}; type SessionId = u64; @@ -65,7 +65,7 @@ pub enum ClientToServerMsg { LayoutFromYaml, Option, ), - AttachClient(ClientAttributes, bool, Options), + AttachClient(ClientAttributes, Options), Action(Action), ClientExited, } @@ -80,6 +80,7 @@ pub enum ServerToClientMsg { Render(String), UnblockInputThread, Exit(ExitReason), + SwitchToMode(InputMode), } #[derive(Serialize, Deserialize, Debug, Clone)] @@ -126,7 +127,9 @@ impl IpcSenderWithContext { pub fn send(&mut self, msg: T) { let err_ctx = get_current_ctx(); bincode::serialize_into(&mut self.sender, &(msg, err_ctx)).unwrap(); - self.sender.flush().unwrap(); + // TODO: unwrapping here can cause issues when the server disconnects which we don't mind + // do we need to handle errors here in other cases? + let _ = self.sender.flush(); } /// Returns an [`IpcReceiverWithContext`] with the same socket as this sender.