Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Tmux control mode #1090

Closed
wants to merge 22 commits into from
Closed

[WIP] Tmux control mode #1090

wants to merge 22 commits into from

Conversation

Dixeran
Copy link
Contributor

@Dixeran Dixeran commented Aug 30, 2021

A proof of concept mainly inspired by previous tmux-cc related PR and @skyline75489 's tmux_cc branch.

The idea is to place the tmux parser directly into escape::Parser, so we can

  1. bypass VTParse from here
  2. recover from that state if we encounter any error parsing tmux events(now we can press 'q' and nicely quit tmux mode)

Thus, DCS handler will handle the parsed TmuxEvents, things after that will remain trivial.

@skyline75489
Copy link
Contributor

Man, I'm excited about this PR. Hope this gets merged eventually 👍

@Dixeran
Copy link
Contributor Author

Dixeran commented Sep 17, 2021

I think this can be reviewed now since it's been at a basic functional status. All comments are appreciated!

/*how tmux things interact with other modules*/

                        ┌──────────────────┐
                        │  Tmux Server     │
                        └────┬─▲───────────┘
                             │ │
                             │ │
                             │ │                       ┌───────────────────────────────────┐
                             │ │                       │ GUI Window For Tmux               │
                        ┌────▼─┴───────────┐           │                                   │
         ┌──────────────┤Original LocalPane│           │                                   │
         │              └──────▲───────────┘           │  ┌─────────────────────────────┐  │
         │                     │                       │  │TermTab - LocalPane          │  │
         │                     │                       │  │             │               │  │
┌────────▼───────┐             │                       │  │             │               │  │
│  Tmux Parser   │             │                       │  │       ┌─────▼────────┐      │  │
└────────┬───────┘             │                       │  │       │              │      │  │
         │                     │                       │  │       │  Tmux Pty    │      │  │
         │              ┌──────┴───────────┐           │  │       │              │      │  │
         │              │                  ├───────────┼──┼──────►└─────┬────────┘      │  │
         └──────────────►  Tmux Domain     │           │  │             │               │  │
          Tmux events   │                  ◄───────────┼──┼─────────────┘               │  │
                        └──────────────────┘           │  │                             │  │
                                                       │  │                             │  │
                                                       │  │                             │  │
                                                       │  │                             │  │
                                                       │  │                             │  │
                                                       │  └─────────────────────────────┘  │
                                                       │                                   │
                                                       │                                   │
                                                       └───────────────────────────────────┘

@Dixeran Dixeran marked this pull request as ready for review September 17, 2021 07:05
write!(&mut s, "0x{:X}\r", byte).expect("unable to write key");
}
format!("send-keys -t {} {}", self.pane, s)
// FIXME: An unexpected duplicated command will prompt next line, why?
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some search, I found this really hard to solve. Currently Tmux use ESC k to set the window name, which we don't support yet. It's really annoying to live with that window title echoed after every command..

Relate issue in mosh: mobile-shell/mosh#992

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That ESC k TITLE ST sequence is pretty awkward because it doesn't match the typical OSC style escapes that use ST (string terminator). That makes it difficult to fit into the parser at the right layer :-(

Do you know what is emitting that sequence?
http://manpages.ubuntu.com/manpages/precise/en/man1/tmux.1.html#names%20and%20titles
suggests that automatic-rename may be a factor: can we tell tmux to disable that option when we set up control mode?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that it was my zsh. Set DISABLE_AUTO_TITLE="true" disable this sequence.

@skyline75489
Copy link
Contributor

Build is failing on Windows for some reason.

Although tmux itself isn't available natively on Windows, one can still use tmux in WSL and over SSH.

mux/src/tmux_commands.rs Outdated Show resolved Hide resolved
tmux-cc/src/lib.rs Outdated Show resolved Hide resolved
@@ -32,6 +32,7 @@ serde = {version="1.0", features = ["rc", "derive"], optional=true}
sha2 = "0.9"
terminfo = "0.7"
thiserror = "1.0"
tmux-cc = {version = "0.1", path = "../tmux-cc"}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dependency management thinking: termwiz is its own crate on crates.io.
Adding a dep here means that we'll also either:

  • need to publish tmux-cc to crates.io (and I'm not sure that that makes sense as it doesn't seem obviously reusable)
  • move tmux-cc to be a module inside termwiz, and potentially make it an optional feature

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woa, didn't expect that. Move into termwiz seems more reasonable, maybe better to move it after all other issues fixed?

mux/src/tmux_commands.rs Outdated Show resolved Hide resolved
mux/src/tmux_pty.rs Outdated Show resolved Hide resolved
fn read(&mut self, mut buf: &mut [u8]) -> std::io::Result<usize> {
match self.rx.recv() {
Ok(str) => {
return buf.write(str.as_bytes());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This buffer reader has a potential problem: if we recv a String that is larger than buf it won't fit and we'll discard the excess.

I think we can make this safer by using a socketpair from our filedescriptor crate. Take a look at read_from_pane_pty to see how it uses a socketpair as a channel for the bytes.

The tmuxreader could potentially just one of the FileDescriptors from the socket pair and simplify some of the code in here.

Copy link
Contributor Author

@Dixeran Dixeran Sep 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't get the point of socketpair here since there is already a channel between tmux's output and TmuxPty reader. But I tried to add a buffer to avoid the "overrange" problem.

mux/src/tmux_pty.rs Outdated Show resolved Hide resolved
@skyline75489
Copy link
Contributor

Would it possible that it was filtered by ConHost

Yes, some of the sequences will be filtered due to how ConPTY works. I think this is on the roadmap of the ConPTY project.

Comment on lines +23 to +55
if !self.head_buffer.is_empty() {
let mut buffer_cleared = false;
let bytes = if self.head_cursor + buf.len() >= self.head_buffer.len() {
buffer_cleared = true;
&self.head_buffer[self.head_cursor..]
} else {
&self.head_buffer[self.head_cursor..(self.head_cursor + buf.len())]
};
return buf.write(bytes.as_bytes()).map(|res| {
// update buffer if write success
if buffer_cleared {
self.head_buffer.clear();
self.head_cursor = 0;
} else {
self.head_cursor = self.head_cursor + buf.len();
}
res
});
} else {
match self.rx.recv() {
Ok(str) => {
if str.len() > buf.len() {
self.head_buffer = str;
self.head_cursor = 0;
return self.read(buf);
} else {
return buf.write(str.as_bytes());
}
}
Err(_) => {
return Ok(0);
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a buffer here to prevent write out of range. Will this solve the problem?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked deeply at this change yet, but my knee-jerk reaction is that that looks very complex. I think replacing the channel based reader/writer with a socketpair will make most of the complexity disappear!

@wez
Copy link
Owner

wez commented Oct 3, 2021

It's taking me a while to get through and review this: I've been a bit preoccupied with preparation for a long distance house move in real life, so wanted to let you know that that's why I'm going slow

@Dixeran
Copy link
Contributor Author

Dixeran commented Oct 3, 2021

I fully understand that this PR will take some time, I'm thinking of writing a more detailed design description within the next few days.

@Dixeran
Copy link
Contributor Author

Dixeran commented Oct 13, 2021

I fully understand that this PR will take some time, I'm thinking of writing a more detailed design description within the next few days.

What happened with tmux mode

To make tmux working:

  1. Redirect tmux content received from the original pane to a legit consumer.
  2. Present the tmux content.
  3. Send user input back to the tmux-server.
  4. Quit tmux mode (stop redirect) if received a END sequence or some invalid sequence.

1

Currently, a parse_buffered_data function is running in a background thread and call Parser.parse() to get corresponding Actions, dispatch to pane. If we encounter the tmux specified sequence \033P1000p, a Action::DeviceControl(DeviceControlMode::Enter) would be dispatched.

This action will cause the current LocalPane switch into tmux mode, handle DCS actions as tmux event. A TmuxDomain will be created and is responsible for dealing with the tmux events (go into details later).

At the meanwhile, the performer will wire up a tmux_cc::Parser, indicate that we should redirect all the bytes to that parser next time, however all the remained bytes this time will still dispatch byte-by-byte using Action::DeviceControl(DeviceControlMode::Data).

check tmux leading sequence

parse single byte

For the next time, we will parse tmux content by batch, and dispatch tmux actions using new type DeviceControlMode::TmuxEvents.

parse batch bytes

local pane handle tmux events

2/3

pub(crate) struct TmuxDomainState {
    pub pane_id: PaneId,     // ID of the original pane
    pub domain_id: DomainId, // ID of TmuxDomain
    state: RefCell<State>,
    pub cmd_queue: Arc<Mutex<TmuxCmdQueue>>,
    pub gui_window: RefCell<Option<MuxWindowBuilder>>,
    pub gui_tabs: RefCell<Vec<TmuxTab>>,
    pub remote_panes: RefCell<HashMap<TmuxPaneId, RefTmuxRemotePane>>,
    pub tmux_session: RefCell<Option<TmuxSessionId>>,
}

For TmuxDomain, it handle tmux events and send tmux commands. Commands are queued in the cmd_queue, we flush that queue once a new command generated.

All tmux commands can be serialized into string and write into the original pty.

send command

There are two special events: Guarded and Output. We expect a Guarded event for each command we sent. Initially there will be a ListAllPanes command inside event queue, we use it to sync pane status with remote. All tmux related panes are created with a TmuxPty.

create tmux related pane

For Output event, the TmuxDomain will pipe its content to the destinate pane by looking into remote_panes using the TmuxPaneId. Once we get the ref to that TmuxRemotePane struct, we can use the channel sender placed within to send the output content. This channel is connected to the TmuxPty of that pane. Inside this pane, there is another parse_buffered_data thread polling bytes from TmuxPty to another buffer, so we just consume the content from channel and feed to the buffer. (If we replace that channel with a socketpair, how will it prevent the 'overrange' problem? For my understanding, a socketpair is like a system-wise channel, or is there some special features I missed? )

read from Tmux

4

If tmux control mode is nicely quitted, there should be a Exit tmux event followed by a DeviceControl Exit sequence.

Exit tmux event will make TmuxDomain destroy all related structs.

TmuxDomain perform Exit

When an invalid tmux sequence arrived (especially a DeviceControll Exit sequence), tmux_cc::Parser will rise an error containing all bytes cannot be parsed, so the escape::Parser will realized it's time to back to normal. Unparsed bytes will feed as the normal way.

Parser exit tmux mode

For the normal Parser, a DeviceControll Exit sequence will generate a DeviceControlMode::Exit event, lead to destroy of TmuxDomain within LocalPane.

Noted that this procedure isn't perfect because:

  • Unrecognized tmux event will break the parser, thus break the tmux mode
  • Without the DeviceControll Exit sequence (which will not present if error with the tmux server or the connection occurred), we can't get out of DeviceControllMode since there will be no DeviceControlMode::Exit shows up.

@wez
Copy link
Owner

wez commented Oct 27, 2021

I haven't forgotten this, I'm just in the middle of moving house!
Just wanted to drop a note here to ask if you could take a look at #1276 to see if those decimal literals are really a problem!

@joexue
Copy link

joexue commented Dec 8, 2021

can not wait it.

@Felixoid
Copy link

Felixoid commented Jan 4, 2022

Kudos to this PR, I'd really like to have it working!

@hurricanehrndz
Copy link

This work looks amazing!

wez added a commit that referenced this pull request Jan 17, 2022
wez added a commit that referenced this pull request Jan 17, 2022
wez added a commit that referenced this pull request Jan 17, 2022
Needs the % prefix to find the pane by id

refs: #1090
wez added a commit that referenced this pull request Jan 17, 2022
@wez
Copy link
Owner

wez commented Jan 17, 2022

Thanks for this!

I rebased it and fixed up the build for recent changes in main, addressed the socketpair/buffering issue we discussed and fixed up send-keys and capturep so that they correctly reference panes, and pushed that to main.

The current state is:

  • Attaching now shows the pane content
  • The cursor position is wrong
  • Typing sends input to the pane

There's still a bunch of things to fully reconcile with the items in #336

One of the interesting recent developments in main is that we now have a workspace concept which is analogous to the tmux session concept, which should make some of the integration a bit easier to model.

@wez
Copy link
Owner

wez commented Jan 17, 2022

I'm closing this as this batch of changes is now in main, and we can make additional PRs to follow up.
Thanks again for your work here!

@wez wez closed this Jan 17, 2022
@Dixeran
Copy link
Contributor Author

Dixeran commented Jan 18, 2022

Really glad to hear that! Can't wait to move forward.

@skyline75489
Copy link
Contributor

Thanks @Dixeran and @wez for the amazing work here! Can't wait to see this land :)

@OmegaRogue
Copy link

whats the current state of this in main? It doesnt seem to do anything after attaching to a tmux session yet

@wez
Copy link
Owner

wez commented Feb 24, 2022

It is incomplete as indicated in #1090 (comment)

@Dixeran Dixeran deleted the tmux_control branch February 28, 2022 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants