1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
//! The bidirectional terminal loop, shared by interactive `exec` and `run`.
//!
//! Both hand a hijacked socket to the caller's terminal and pump bytes until the
//! command ends. They differ only in which libpod object gets resized — an exec
//! session or a container — so that is the one parameter, and the loop that must
//! not get raw mode wrong lives once rather than twice.
//!
//! The loop itself is platform-independent: raw mode, the size query and the
//! resize events differ per platform, and all three live behind the one
//! signature `query::terminal` exports.
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::error::{ComposeError, Result};
use crate::libpod::client::Hijacked;
use crate::libpod::API_PREFIX;
use super::query::terminal::{window_size, RawMode, ResizeWatcher};
use super::Engine;
impl Engine {
/// Pump the caller's terminal against a hijacked stream until it ends.
///
/// `resize_base` is the libpod path segment identifying what to resize —
/// `exec/{id}` or `containers/{name}` — since the endpoint differs and
/// nothing else does.
///
/// The terminal is restored by `RawMode`'s `Drop`, so every exit path leaves
/// the caller's shell usable. That is the one thing this must not get wrong:
/// a terminal left raw has no echo and no line discipline, and the user
/// cannot see what they type to fix it.
pub(crate) async fn pump_terminal(&self, hijacked: Hijacked, resize_base: &str) -> Result<()> {
// Raw mode only *after* the stream is known to be live. Enabling it
// first would leave a terminal unusable behind a failed start.
let _raw = RawMode::enable();
// Size the pty now, not before: there is nothing to resize until the
// session exists, and libpod rejects the call — silently, since a failed
// resize is only cosmetic. Sizing here means a full-screen program draws
// correctly from its first frame rather than at libpod's 80x24 default.
if let Some((rows, cols)) = window_size() {
self.resize_pty(resize_base, rows, cols).await;
}
let (mut server_read, mut server_write) = tokio::io::split(hijacked.stream);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
// Follow the window. libpod has no way to learn the caller's terminal
// size on its own, so every change has to be forwarded or a resized
// window leaves the remote program drawing at the old geometry.
let mut resize = ResizeWatcher::new().map_err(ComposeError::Io)?;
let mut to_server = [0u8; 8 * 1024];
let mut from_server = [0u8; 32 * 1024];
loop {
tokio::select! {
// Output first: a biased select would starve it behind a caller
// holding the keyboard down, and seeing the command's output is
// the point of being attached.
read = server_read.read(&mut from_server) => {
match read {
Ok(0) => break,
Ok(n) => {
stdout.write_all(&from_server[..n]).await.map_err(ComposeError::Io)?;
stdout.flush().await.map_err(ComposeError::Io)?;
}
// The command exiting closes this side; that is the
// ordinary end of an interactive session, not a failure.
Err(_) => break,
}
}
read = stdin.read(&mut to_server) => {
match read {
// Ctrl-D / a closed stdin: stop writing but keep reading,
// so the command can finish and flush. Shutting the write
// half is what tells it EOF.
Ok(0) => {
let _ = server_write.shutdown().await;
}
Ok(n) => {
if server_write.write_all(&to_server[..n]).await.is_err() {
break;
}
if server_write.flush().await.is_err() {
break;
}
}
Err(_) => break,
}
}
size = resize.next() => {
let (rows, cols) = size;
self.resize_pty(resize_base, rows, cols).await;
}
}
}
Ok(())
}
/// Tell libpod the pty's new size. Best-effort: a failed resize makes the
/// remote program draw at the wrong geometry, which is cosmetic, and turning
/// it into a hard error would kill a working session over a window drag.
async fn resize_pty(&self, resize_base: &str, rows: u16, cols: u16) {
let path = format!("{API_PREFIX}/{resize_base}/resize?h={rows}&w={cols}");
if let Err(e) = self.client.post_empty_ok(&path).await {
tracing::debug!("resize to {rows}x{cols} failed: {e}");
}
}
}