mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-02 10:45:57 +00:00
Add resize feature to Rust server
This commit is contained in:
parent
720c81c704
commit
978205da76
5 changed files with 377 additions and 20 deletions
|
|
@ -79,6 +79,12 @@ struct InputRequest {
|
|||
text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ResizeRequest {
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct MkdirRequest {
|
||||
path: String,
|
||||
|
|
@ -349,6 +355,11 @@ pub fn start_server(
|
|||
{
|
||||
handle_session_input(&control_path, path, &req)
|
||||
}
|
||||
(&Method::POST, path)
|
||||
if path.starts_with("/api/sessions/") && path.ends_with("/resize") =>
|
||||
{
|
||||
handle_session_resize(&control_path, path, &req)
|
||||
}
|
||||
(&Method::DELETE, path)
|
||||
if path.starts_with("/api/sessions/") && path.ends_with("/cleanup") =>
|
||||
{
|
||||
|
|
@ -588,11 +599,13 @@ fn handle_create_session(
|
|||
let session_info_path = session_path.join("session.json");
|
||||
let stream_out_path = session_path.join("stream-out");
|
||||
let stdin_path = session_path.join("stdin");
|
||||
let control_path = session_path.join("control");
|
||||
let notification_stream_path = session_path.join("notification-stream");
|
||||
|
||||
if let Err(e) = tty_spawn
|
||||
.stdout_path(&stream_out_path, true)
|
||||
.and_then(|spawn| spawn.stdin_path(&stdin_path))
|
||||
.and_then(|spawn| spawn.control_path(&control_path))
|
||||
{
|
||||
eprintln!("Failed to set up TTY paths for session {session_id_clone}: {e}");
|
||||
return;
|
||||
|
|
@ -971,6 +984,103 @@ fn handle_session_input(
|
|||
}
|
||||
}
|
||||
|
||||
fn handle_session_resize(
|
||||
control_path: &Path,
|
||||
path: &str,
|
||||
req: &crate::http_server::HttpRequest,
|
||||
) -> Response<String> {
|
||||
if let Some(session_id) = extract_session_id(path) {
|
||||
let body_bytes = req.body();
|
||||
let body = String::from_utf8_lossy(body_bytes);
|
||||
|
||||
if let Ok(resize_req) = serde_json::from_str::<ResizeRequest>(&body) {
|
||||
// Validate dimensions
|
||||
if resize_req.cols == 0 || resize_req.rows == 0 {
|
||||
let error = ApiResponse {
|
||||
success: None,
|
||||
message: None,
|
||||
error: Some("Invalid dimensions: cols and rows must be greater than 0".to_string()),
|
||||
session_id: None,
|
||||
};
|
||||
return json_response(StatusCode::BAD_REQUEST, &error);
|
||||
}
|
||||
|
||||
// First validate session exists and is running
|
||||
match sessions::list_sessions(control_path) {
|
||||
Ok(sessions) => {
|
||||
if let Some(session_entry) = sessions.get(&session_id) {
|
||||
// Check if session is running
|
||||
if session_entry.session_info.status != "running" {
|
||||
let error = ApiResponse {
|
||||
success: None,
|
||||
message: None,
|
||||
error: Some("Session is not running".to_string()),
|
||||
session_id: None,
|
||||
};
|
||||
return json_response(StatusCode::BAD_REQUEST, &error);
|
||||
}
|
||||
|
||||
// Perform the resize
|
||||
match sessions::resize_session(control_path, &session_id, resize_req.cols, resize_req.rows) {
|
||||
Ok(()) => {
|
||||
let response = ApiResponse {
|
||||
success: Some(true),
|
||||
message: Some(format!("Session resized to {}x{}", resize_req.cols, resize_req.rows)),
|
||||
error: None,
|
||||
session_id: None,
|
||||
};
|
||||
json_response(StatusCode::OK, &response)
|
||||
}
|
||||
Err(e) => {
|
||||
let error = ApiResponse {
|
||||
success: None,
|
||||
message: None,
|
||||
error: Some(format!("Failed to resize session: {e}")),
|
||||
session_id: None,
|
||||
};
|
||||
json_response(StatusCode::INTERNAL_SERVER_ERROR, &error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = ApiResponse {
|
||||
success: None,
|
||||
message: None,
|
||||
error: Some("Session not found".to_string()),
|
||||
session_id: None,
|
||||
};
|
||||
json_response(StatusCode::NOT_FOUND, &error)
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error = ApiResponse {
|
||||
success: None,
|
||||
message: None,
|
||||
error: Some(format!("Failed to list sessions: {e}")),
|
||||
session_id: None,
|
||||
};
|
||||
json_response(StatusCode::INTERNAL_SERVER_ERROR, &error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let error = ApiResponse {
|
||||
success: None,
|
||||
message: None,
|
||||
error: Some("Invalid request body. Expected JSON with 'cols' and 'rows' fields".to_string()),
|
||||
session_id: None,
|
||||
};
|
||||
json_response(StatusCode::BAD_REQUEST, &error)
|
||||
}
|
||||
} else {
|
||||
let error = ApiResponse {
|
||||
success: None,
|
||||
message: None,
|
||||
error: Some("Invalid session ID".to_string()),
|
||||
session_id: None,
|
||||
};
|
||||
json_response(StatusCode::BAD_REQUEST, &error)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_session_kill(control_path: &Path, path: &str) -> Response<String> {
|
||||
let session_id = if let Some(id) = extract_session_id(path) {
|
||||
id
|
||||
|
|
@ -1976,6 +2086,20 @@ mod tests {
|
|||
assert_eq!(request.text, "arrow_up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resize_request_deserialization() {
|
||||
let json = r#"{"cols":120,"rows":40}"#;
|
||||
let request: ResizeRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(request.cols, 120);
|
||||
assert_eq!(request.rows, 40);
|
||||
|
||||
// Test with zero values (should be rejected by handler)
|
||||
let json = r#"{"cols":0,"rows":0}"#;
|
||||
let request: ResizeRequest = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(request.cols, 0);
|
||||
assert_eq!(request.rows, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mkdir_request_deserialization() {
|
||||
let json = r#"{"path":"/tmp/test"}"#;
|
||||
|
|
@ -2100,6 +2224,8 @@ mod tests {
|
|||
started_at: Some(jiff::Timestamp::now()),
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
fs::write(
|
||||
|
|
@ -2140,6 +2266,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
fs::write(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
let mut session_id = std::env::var("TTY_SESSION_ID").ok();
|
||||
let mut send_key = None::<String>;
|
||||
let mut send_text = None::<String>;
|
||||
let mut resize = None::<String>;
|
||||
let mut signal = None::<i32>;
|
||||
let mut stop = false;
|
||||
let mut kill = false;
|
||||
|
|
@ -64,6 +65,9 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
p if p.is_long("send-text") => {
|
||||
send_text = Some(parser.value()?);
|
||||
}
|
||||
p if p.is_long("resize") => {
|
||||
resize = Some(parser.value()?);
|
||||
}
|
||||
p if p.is_long("signal") => {
|
||||
let signal_str: String = parser.value()?;
|
||||
signal = Some(
|
||||
|
|
@ -112,6 +116,7 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
println!(" --send-key <key> Send key input to session");
|
||||
println!(" Keys: arrow_up, arrow_down, arrow_left, arrow_right, escape, enter, ctrl_enter, shift_enter");
|
||||
println!(" --send-text <text> Send text input to session");
|
||||
println!(" --resize <cols>x<rows> Resize terminal (e.g., --resize 120x40)");
|
||||
println!(" --signal <number> Send signal number to session PID");
|
||||
println!(
|
||||
" --stop Send SIGTERM to session (equivalent to --signal 15)"
|
||||
|
|
@ -162,6 +167,28 @@ fn main() -> Result<(), anyhow::Error> {
|
|||
return Err(anyhow!("--send-text requires --session <session_id>"));
|
||||
}
|
||||
|
||||
// Handle resize command
|
||||
if let Some(resize_spec) = resize {
|
||||
if let Some(sid) = &session_id {
|
||||
// Parse resize spec like "120x40"
|
||||
let parts: Vec<&str> = resize_spec.split('x').collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(anyhow!("Invalid resize format. Use <cols>x<rows> (e.g., 120x40)"));
|
||||
}
|
||||
let cols: u16 = parts[0].parse()
|
||||
.map_err(|_| anyhow!("Invalid column value: {}", parts[0]))?;
|
||||
let rows: u16 = parts[1].parse()
|
||||
.map_err(|_| anyhow!("Invalid row value: {}", parts[1]))?;
|
||||
|
||||
if cols == 0 || rows == 0 {
|
||||
return Err(anyhow!("Column and row values must be greater than 0"));
|
||||
}
|
||||
|
||||
return sessions::resize_session(&control_path, sid, cols, rows);
|
||||
}
|
||||
return Err(anyhow!("--resize requires --session <session_id>"));
|
||||
}
|
||||
|
||||
// Handle signal command
|
||||
if let Some(sig) = signal {
|
||||
if let Some(sid) = &session_id {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@ pub struct SessionInfo {
|
|||
pub term: String,
|
||||
#[serde(default = "get_default_spawn_type")]
|
||||
pub spawn_type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub cols: Option<u16>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub rows: Option<u16>,
|
||||
}
|
||||
|
||||
fn get_default_term() -> String {
|
||||
|
|
@ -760,6 +764,8 @@ mod tests {
|
|||
started_at: Some(Timestamp::now()),
|
||||
term: "xterm-256color".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&session).unwrap();
|
||||
|
|
@ -1119,6 +1125,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
},
|
||||
stream_out: "/tmp/stream.out".to_string(),
|
||||
stdin: "/tmp/stdin".to_string(),
|
||||
|
|
|
|||
|
|
@ -289,6 +289,71 @@ pub fn reap_zombies() {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn resize_session(
|
||||
control_path: &Path,
|
||||
session_id: &str,
|
||||
cols: u16,
|
||||
rows: u16,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let session_path = control_path.join(session_id);
|
||||
let session_json_path = session_path.join("session.json");
|
||||
let control_fifo_path = session_path.join("control");
|
||||
|
||||
if !session_json_path.exists() {
|
||||
return Err(anyhow!("Session {} not found", session_id));
|
||||
}
|
||||
|
||||
// Read session info
|
||||
let content = fs::read_to_string(&session_json_path)?;
|
||||
let mut session_info: serde_json::Value = serde_json::from_str(&content)?;
|
||||
|
||||
// Update dimensions in session.json
|
||||
session_info["cols"] = serde_json::json!(cols);
|
||||
session_info["rows"] = serde_json::json!(rows);
|
||||
|
||||
// Write updated session info
|
||||
let updated_content = serde_json::to_string_pretty(&session_info)?;
|
||||
fs::write(&session_json_path, updated_content)?;
|
||||
|
||||
// Create control message
|
||||
let control_msg = serde_json::json!({
|
||||
"cmd": "resize",
|
||||
"cols": cols,
|
||||
"rows": rows
|
||||
});
|
||||
let control_msg_str = serde_json::to_string(&control_msg)?;
|
||||
|
||||
// Try to send resize command via control FIFO if it exists
|
||||
if control_fifo_path.exists() {
|
||||
// Write to control FIFO with timeout
|
||||
write_to_pipe_with_timeout(
|
||||
&control_fifo_path,
|
||||
format!("{}\n", control_msg_str).as_bytes(),
|
||||
Duration::from_secs(2),
|
||||
)?;
|
||||
} else {
|
||||
// If no control FIFO, try sending SIGWINCH to the process
|
||||
if let Some(pid) = session_info.get("pid").and_then(|p| p.as_u64()) {
|
||||
if is_pid_alive(pid as u32) {
|
||||
let result = unsafe { libc::kill(pid as i32, libc::SIGWINCH) };
|
||||
if result != 0 {
|
||||
return Err(anyhow!("Failed to send SIGWINCH to PID {}", pid));
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"Session {} process (PID: {}) is not running",
|
||||
session_id,
|
||||
pid
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow!("Session {} has no PID recorded", session_id));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_signal_to_session(
|
||||
control_path: &Path,
|
||||
session_id: &str,
|
||||
|
|
@ -485,6 +550,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
let session2_info = SessionInfo {
|
||||
|
|
@ -497,6 +564,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm-256color".to_string(),
|
||||
spawn_type: "socket".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
create_test_session(control_path, "session1", &session1_info).unwrap();
|
||||
|
|
@ -538,6 +607,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
create_test_session(control_path, "valid-session", &session_info).unwrap();
|
||||
|
||||
|
|
@ -599,6 +670,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
create_test_session(control_path, "current-session", &session_info).unwrap();
|
||||
|
||||
|
|
@ -713,6 +786,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
create_test_session(control_path, "test-session", &session_info).unwrap();
|
||||
|
||||
|
|
@ -753,6 +828,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
create_test_session(control_path, "test-session", &session_info).unwrap();
|
||||
|
||||
|
|
@ -788,6 +865,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
create_test_session(control_path, "running-session", &session_info).unwrap();
|
||||
|
||||
|
|
@ -816,6 +895,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
let running_session = SessionInfo {
|
||||
|
|
@ -828,6 +909,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
let no_pid_session = SessionInfo {
|
||||
|
|
@ -840,6 +923,8 @@ mod tests {
|
|||
started_at: None,
|
||||
term: "xterm".to_string(),
|
||||
spawn_type: "pty".to_string(),
|
||||
cols: None,
|
||||
rows: None,
|
||||
};
|
||||
|
||||
create_test_session(control_path, "dead-session", &dead_session).unwrap();
|
||||
|
|
@ -902,4 +987,36 @@ mod tests {
|
|||
// Just ensure the function doesn't panic
|
||||
reap_zombies();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resize_session() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let control_path = temp_dir.path();
|
||||
|
||||
// Create a test session with cols/rows
|
||||
let mut session_info = SessionInfo::default();
|
||||
session_info.status = "running".to_string();
|
||||
session_info.pid = Some(std::process::id());
|
||||
session_info.cols = Some(80);
|
||||
session_info.rows = Some(24);
|
||||
|
||||
create_test_session(control_path, "test-session", &session_info).unwrap();
|
||||
|
||||
// Create control FIFO
|
||||
let control_fifo_path = control_path.join("test-session").join("control");
|
||||
unsafe {
|
||||
let path_cstr = std::ffi::CString::new(control_fifo_path.to_str().unwrap()).unwrap();
|
||||
libc::mkfifo(path_cstr.as_ptr(), 0o666);
|
||||
}
|
||||
|
||||
// Note: Actually testing resize would require a real PTY and process
|
||||
// This test just verifies the session.json update logic
|
||||
|
||||
// Read back session.json to verify initial dimensions
|
||||
let session_json_path = control_path.join("test-session").join("session.json");
|
||||
let content = std::fs::read_to_string(&session_json_path).unwrap();
|
||||
let session_data: serde_json::Value = serde_json::from_str(&content).unwrap();
|
||||
assert_eq!(session_data.get("cols").and_then(|v| v.as_u64()), Some(80));
|
||||
assert_eq!(session_data.get("rows").and_then(|v| v.as_u64()), Some(24));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ impl TtySpawn {
|
|||
command,
|
||||
stdin_file: None,
|
||||
stdout_file: None,
|
||||
control_file: None,
|
||||
notification_writer: None,
|
||||
session_json_path: None,
|
||||
session_name: None,
|
||||
|
|
@ -181,6 +182,19 @@ impl TtySpawn {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
/// Sets a path as control file for resize and other control commands.
|
||||
pub fn control_path<P: AsRef<Path>>(&mut self, path: P) -> Result<&mut Self, Error> {
|
||||
let path = path.as_ref();
|
||||
mkfifo_atomic(path)?;
|
||||
let file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.custom_flags(O_NONBLOCK)
|
||||
.open(path)?;
|
||||
self.options_mut().control_file = Some(file);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Sets a path as output file for stdout.
|
||||
///
|
||||
/// If the `truncate` flag is set to `true` the file will be truncated
|
||||
|
|
@ -251,6 +265,7 @@ struct SpawnOptions {
|
|||
command: Vec<OsString>,
|
||||
stdin_file: Option<File>,
|
||||
stdout_file: Option<File>,
|
||||
control_file: Option<File>,
|
||||
notification_writer: Option<NotificationWriter>,
|
||||
session_json_path: Option<PathBuf>,
|
||||
session_name: Option<String>,
|
||||
|
|
@ -265,6 +280,8 @@ pub fn create_session_info(
|
|||
name: String,
|
||||
cwd: String,
|
||||
term: String,
|
||||
cols: Option<u16>,
|
||||
rows: Option<u16>,
|
||||
) -> Result<(), Error> {
|
||||
let session_info = SessionInfo {
|
||||
cmdline,
|
||||
|
|
@ -276,6 +293,8 @@ pub fn create_session_info(
|
|||
started_at: Some(Timestamp::now()),
|
||||
term,
|
||||
spawn_type: "socket".to_string(),
|
||||
cols,
|
||||
rows,
|
||||
};
|
||||
|
||||
let session_info_str = serde_json::to_string(&session_info)?;
|
||||
|
|
@ -325,6 +344,27 @@ fn update_session_status(
|
|||
/// optional `out` log file. Additionally it can retrieve instructions from
|
||||
/// the given control socket.
|
||||
fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
|
||||
// if we can't retrieve the terminal atts we're not directly connected
|
||||
// to a pty in which case we won't do any of the terminal related
|
||||
// operations. In detached mode, we don't connect to the current terminal.
|
||||
let term_attrs = if opts.detached {
|
||||
None
|
||||
} else {
|
||||
tcgetattr(io::stdin()).ok()
|
||||
};
|
||||
let winsize = if opts.detached {
|
||||
Some(Winsize {
|
||||
ws_row: 24,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
})
|
||||
} else {
|
||||
term_attrs
|
||||
.as_ref()
|
||||
.and_then(|_| get_winsize(io::stdin().as_fd()))
|
||||
};
|
||||
|
||||
// Create session info at the beginning if we have a session JSON path
|
||||
if let Some(ref session_json_path) = opts.session_json_path {
|
||||
// Get executable name for session name
|
||||
|
|
@ -355,6 +395,8 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
|
|||
session_name.clone(),
|
||||
current_dir.clone(),
|
||||
opts.term.clone(),
|
||||
winsize.as_ref().map(|w| w.ws_col),
|
||||
winsize.as_ref().map(|w| w.ws_row),
|
||||
)?;
|
||||
|
||||
// Send session started notification
|
||||
|
|
@ -371,26 +413,6 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
|
|||
let _ = notification_writer.write_notification(notification);
|
||||
}
|
||||
}
|
||||
// if we can't retrieve the terminal atts we're not directly connected
|
||||
// to a pty in which case we won't do any of the terminal related
|
||||
// operations. In detached mode, we don't connect to the current terminal.
|
||||
let term_attrs = if opts.detached {
|
||||
None
|
||||
} else {
|
||||
tcgetattr(io::stdin()).ok()
|
||||
};
|
||||
let winsize = if opts.detached {
|
||||
Some(Winsize {
|
||||
ws_row: 24,
|
||||
ws_col: 80,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
})
|
||||
} else {
|
||||
term_attrs
|
||||
.as_ref()
|
||||
.and_then(|_| get_winsize(io::stdin().as_fd()))
|
||||
};
|
||||
|
||||
// Create the outer pty for stdout
|
||||
let pty = openpty(&winsize, &term_attrs)?;
|
||||
|
|
@ -434,6 +456,7 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
|
|||
let session_json_path = opts.session_json_path.clone();
|
||||
let notification_writer = opts.notification_writer;
|
||||
let stdin_file = opts.stdin_file;
|
||||
let control_file = opts.control_file;
|
||||
|
||||
// Create StreamWriter for detached session if we have an output file
|
||||
let stream_writer = if let Some(stdout_file) = opts.stdout_file.take() {
|
||||
|
|
@ -464,6 +487,7 @@ fn spawn(mut opts: SpawnOptions) -> Result<i32, Error> {
|
|||
notification_writer,
|
||||
stream_writer,
|
||||
stdin_file,
|
||||
control_file,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -845,6 +869,7 @@ fn monitor_detached_session(
|
|||
mut notification_writer: Option<NotificationWriter>,
|
||||
mut stream_writer: Option<StreamWriter>,
|
||||
stdin_file: Option<File>,
|
||||
control_file: Option<File>,
|
||||
) -> Result<(), Error> {
|
||||
let mut buf = [0; 4096];
|
||||
let mut done = false;
|
||||
|
|
@ -858,6 +883,10 @@ fn monitor_detached_session(
|
|||
read_fds.insert(f.as_fd());
|
||||
}
|
||||
|
||||
if let Some(ref f) = control_file {
|
||||
read_fds.insert(f.as_fd());
|
||||
}
|
||||
|
||||
match select(None, Some(&mut read_fds), None, None, Some(&mut timeout)) {
|
||||
Ok(0) => {
|
||||
// Timeout occurred - just continue
|
||||
|
|
@ -868,6 +897,54 @@ fn monitor_detached_session(
|
|||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
|
||||
if let Some(ref f) = control_file {
|
||||
if read_fds.contains(f.as_fd()) {
|
||||
match read(f, &mut buf) {
|
||||
Ok(0) | Err(Errno::EAGAIN | Errno::EINTR) => {}
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(n) => {
|
||||
// Parse control command
|
||||
if let Ok(cmd_str) = std::str::from_utf8(&buf[..n]) {
|
||||
for line in cmd_str.lines() {
|
||||
if let Ok(cmd) = serde_json::from_str::<serde_json::Value>(line) {
|
||||
if let Some(cmd_type) = cmd.get("cmd").and_then(|v| v.as_str()) {
|
||||
if cmd_type == "resize" {
|
||||
if let (Some(cols), Some(rows)) = (
|
||||
cmd.get("cols").and_then(|v| v.as_u64()),
|
||||
cmd.get("rows").and_then(|v| v.as_u64()),
|
||||
) {
|
||||
let winsize = Winsize {
|
||||
ws_row: rows as u16,
|
||||
ws_col: cols as u16,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
if let Err(e) = set_winsize(master.as_fd(), winsize) {
|
||||
eprintln!("Failed to resize terminal: {}", e);
|
||||
} else {
|
||||
// Log resize event
|
||||
if let Some(writer) = &mut stream_writer {
|
||||
let time = writer.elapsed_time();
|
||||
let data = format!("{}x{}", cols, rows);
|
||||
let event = AsciinemaEvent {
|
||||
time,
|
||||
event_type: AsciinemaEventType::Resize,
|
||||
data,
|
||||
};
|
||||
let _ = writer.write_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref f) = stdin_file {
|
||||
if read_fds.contains(f.as_fd()) {
|
||||
match read(f, &mut buf) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue