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
This commit is contained in:
Aram Drevekenin 2021-09-27 11:29:13 +02:00 committed by GitHub
parent c93a4f1f67
commit 5c54bf18c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 730 additions and 324 deletions

View file

@ -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)

1
Cargo.lock generated
View file

@ -2736,6 +2736,7 @@ dependencies = [
"log",
"mio",
"termbg",
"zellij-tile",
"zellij-utils",
]

View file

@ -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]

View file

@ -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 => {

View file

@ -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");
}
}
}

View file

@ -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<String>,
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<Step>) {
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 {
Err(_e) => {
if self.retries_left > 0 {
return self.restart_test();
}
self.display_informative_error();
panic!("Timed out waiting for 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 {

View file

@ -5,7 +5,7 @@ expression: last_snapshot
---
Zellij
┌──────┐
│$ Hi!█│
│$ █
│ │
│ │
│ │
@ -22,4 +22,4 @@ expression: last_snapshot
│ │
└──────┘
Ctrl +
...

View file

@ -0,0 +1,29 @@
---
source: src/tests/e2e/cases.rs
expression: last_snapshot
---
Zellij (mirrored_sessions)  Tab #1 
┌ Pane #1 ─────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐
│$ ││$ █ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
│ ││ │
└──────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘
Ctrl + <g> LOCK  <p> PANE  <t> TAB  <n> RESIZE  <s> SCROLL  <o> SESSION  <q> QUIT 
Tip: Alt + n => open new pane. Alt + [] or hjkl => navigate between panes.

View file

@ -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]

View file

@ -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<ClientInstruction>,
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<ClientInstruction>,
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,10 +104,13 @@ 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),
}
}
}
fn handle_unknown_key(&mut self, raw_bytes: Vec<u8>) {
@ -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<ClientInstruction>,
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();
}

View file

@ -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<ServerToClientMsg> for ClientInstruction {
@ -38,6 +42,9 @@ impl From<ServerToClientMsg> 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<u8>),
SwitchToMode(InputMode),
}
pub fn start_client(
mut os_input: Box<dyn ClientOsApi>,
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();
}
}
}

View file

@ -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<Mutex<Vec<Vec<u8>>>>,
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: Arc<Mutex<CommandIsExecuting>>,
}
impl FakeClientOsApi {
pub fn new(
mut stdin_events: Vec<Vec<u8>>,
events_sent_to_server: Arc<Mutex<Vec<ClientToServerMsg>>>,
command_is_executing: CommandIsExecuting,
) -> Self {
@ -82,10 +83,7 @@ impl FakeClientOsApi {
// Arc<Mutex> 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<u8> {
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<dyn ClientOsApi> {
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(),
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()),

View file

@ -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<CliArgs>,
Box<Options>,
LayoutFromYaml,
ClientId,
Option<PluginsConfig>,
),
Render(Option<String>),
UnblockInputThread,
ClientExit,
ClientExit(ClientId),
RemoveClient(ClientId),
Error(String),
DetachSession,
AttachClient(ClientAttributes, bool, Options),
}
impl From<ClientToServerMsg> 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<dyn ServerOsApi>, socket_path: PathBuf) {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct SessionState {
clients: HashMap<ClientId, Option<Size>>,
}
impl SessionState {
pub fn new() -> Self {
SessionState {
clients: HashMap::new(),
}
}
pub fn new_client(&mut self) -> ClientId {
let mut clients: Vec<ClientId> = 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<Size> {
// None if there are no client sizes
let mut rows: Vec<usize> = self
.clients
.values()
.filter_map(|size| size.map(|size| size.rows))
.collect();
rows.sort_unstable();
let mut cols: Vec<usize> = 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<ClientId> {
self.clients.keys().copied().collect()
}
}
pub fn start_server(mut os_input: Box<dyn ServerOsApi>, 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<dyn ServerOsApi>, socket_path: PathBuf) {
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));
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<dyn ServerOsApi>, 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<dyn ServerOsApi>, socket_path: PathBuf) {
session_state,
os_input,
to_server,
receiver,
client_id,
)
})
.unwrap(),
@ -205,6 +254,7 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, 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<dyn ServerOsApi>, 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<dyn ServerOsApi>, 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<dyn ServerOsApi>, socket_path: PathBuf) {
Event::ModeUpdate(mode_info),
))
.unwrap();
for client_id in session_state.read().unwrap().clients.keys() {
os_input.send_to_client(*client_id, ServerToClientMsg::SwitchToMode(mode));
}
}
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::UnblockInputThread);
}
}
ServerInstruction::ClientExit => {
os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
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::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));
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<String> 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 {
os_input.send_to_client(ServerToClientMsg::Exit(ExitReason::Normal));
break;
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;
}

View file

@ -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<Mutex<termios::Termios>>,
receive_instructions_from_client: Option<Arc<Mutex<IpcReceiverWithContext<ClientToServerMsg>>>>,
send_instructions_to_client: Arc<Mutex<Option<IpcSenderWithContext<ServerToClientMsg>>>>,
client_senders: Arc<Mutex<HashMap<ClientId, IpcSenderWithContext<ServerToClientMsg>>>>,
}
// 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<dyn ServerOsApi>;
/// 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<ClientToServerMsg>;
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<PathBuf>;
@ -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()
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 new_client(
&mut self,
client_id: ClientId,
stream: LocalSocketStream,
) -> IpcReceiverWithContext<ClientToServerMsg> {
let receiver = IpcReceiverWithContext::new(stream);
let sender = receiver.get_sender();
self.client_senders
.lock()
.unwrap()
.recv()
.insert(client_id, sender);
receiver
}
fn send_to_client(&self, msg: ServerToClientMsg) {
self.send_instructions_to_client
.lock()
.unwrap()
.as_mut()
.unwrap()
.send(msg);
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 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_temp_client(&self, msg: ServerToClientMsg) {
self.receive_instructions_from_client
.as_ref()
.unwrap()
.lock()
.unwrap()
.get_sender()
.send(msg);
}
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 load_palette(&self) -> Palette {
default_palette()
@ -418,8 +382,7 @@ pub fn get_server_os_input() -> Result<ServerOsInputOutput, nix::Error> {
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())),
})
}

View file

@ -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<ServerInstruction>,
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<RwLock<SessionState>>,
os_input: Box<dyn ServerOsApi>,
to_server: SenderWithContext<ServerInstruction>,
mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
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(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::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::ClientExited => break,
}
}
}

View file

@ -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();
}
} 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();

View file

@ -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

View file

@ -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<usize, nix::Error> {
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
unimplemented!()
}
fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> {
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> {
unimplemented!()
}
fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result<usize, nix::Error> {
fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result<usize, nix::Error> {
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<dyn ServerOsApi> {
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<ClientToServerMsg> {
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,

View file

@ -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<usize, nix::Error> {
fn read_from_tty_stdout(&self, fd: RawFd, buf: &mut [u8]) -> Result<usize, nix::Error> {
unimplemented!()
}
fn async_file_reader(&self, _fd: RawFd) -> Box<dyn AsyncReader> {
fn async_file_reader(&self, fd: RawFd) -> Box<dyn AsyncReader> {
unimplemented!()
}
fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result<usize, nix::Error> {
fn write_to_tty_stdin(&self, fd: RawFd, buf: &[u8]) -> Result<usize, nix::Error> {
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<dyn ServerOsApi> {
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<ClientToServerMsg> {
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,

View file

@ -81,11 +81,6 @@ pub enum Sessions {
/// Name of the session to attach to.
session_name: Option<String>,
/// 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,

View file

@ -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,

View file

@ -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<PluginsConfig>,
),
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<T: Serialize> IpcSenderWithContext<T> {
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.