Skip to content
Merged
28 changes: 26 additions & 2 deletions src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ pub fn cmd_wrap(capture: &str) -> Result<()> {
if slave_raw > libc::STDERR_FILENO {
let _ = close(slave_raw);
}
let shell_cstr = CString::new(shell.as_str()).unwrap();
let shell_cstr = match CString::new(shell.as_str()) {
Ok(c) => c,
Err(_) => {
eprintln!("error: SHELL contains invalid characters");
unsafe { libc::_exit(1) };
}
};
let _ = execvp(&shell_cstr, std::slice::from_ref(&shell_cstr));
unsafe {
libc::_exit(127);
Expand Down Expand Up @@ -403,7 +409,12 @@ pub async fn cmd_info(config: &Config, target: String, json: bool) -> Result<()>
}
}

pub async fn cmd_attach(config: &Config, target: String, read_only: bool) -> Result<()> {
pub async fn cmd_attach(
config: &Config,
target: String,
read_only: bool,
force: bool,
) -> Result<()> {
use crossterm::event::{Event, EventStream, KeyCode, KeyModifiers};
use crossterm::terminal;
use futures_lite::StreamExt;
Expand All @@ -415,6 +426,7 @@ pub async fn cmd_attach(config: &Config, target: String, read_only: bool) -> Res
.request(&Request::SessionAttach {
target: target.clone(),
read_only,
force,
})
.await?;

Expand Down Expand Up @@ -471,6 +483,18 @@ pub async fn cmd_attach(config: &Config, target: String, read_only: bool) -> Res
let _ = std::io::Write::write_all(&mut tty_out, &payload);
let _ = std::io::Write::flush(&mut tty_out);
} else if msg_type == MSG_SESSION_EVENT {
let msg = if let Ok(resp) = decode_response(msg_type, &payload) {
match resp {
Response::SessionEvent { event, .. } if event == "stolen" => {
"\r\n\x1b[33m[Session stolen by another client]\x1b[0m\r\n"
}
_ => "\r\n\x1b[33m[Session killed by snag]\x1b[0m\r\n",
}
} else {
"\r\n\x1b[33m[Session ended]\x1b[0m\r\n"
};
let _ = std::io::Write::write_all(&mut tty_out, msg.as_bytes());
let _ = std::io::Write::flush(&mut tty_out);
break Ok(());
}
// MSG_OK/MSG_ERROR from control messages — ignore
Expand Down
3 changes: 3 additions & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ pub enum Command {
/// Read-only mode
#[arg(long)]
read_only: bool,
/// Force-steal from another attached client
#[arg(long)]
force: bool,
},
/// Send a command to a session
Send {
Expand Down
5 changes: 4 additions & 1 deletion src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ fn start_daemon(config: &Config) -> Result<()> {
let _ = nix::unistd::dup2(devnull.as_raw_fd(), nix::libc::STDIN_FILENO);
let _ = nix::unistd::dup2(devnull.as_raw_fd(), nix::libc::STDOUT_FILENO);
// Keep stderr for daemon logging
let log_path = socket_path.parent().unwrap().join("snagd.log");
let log_path = socket_path
.parent()
.unwrap_or(std::path::Path::new("/tmp"))
.join("snagd.log");
if let Ok(log) = std::fs::File::create(&log_path) {
let _ = nix::unistd::dup2(log.as_raw_fd(), nix::libc::STDERR_FILENO);
}
Expand Down
20 changes: 18 additions & 2 deletions src/daemon/adopt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,24 @@ fn get_pts_number_for_fd(pid: u32, fd: i32) -> Option<u32> {
}
}

// Fallback: try to figure out from /proc/<pid>/fd/<fd> -> /dev/pts/<N> mapping
// by checking all processes for matching tty
// Fallback: try to resolve via /proc/<pid>/fd/<fd> -> /dev/pts/<N> symlink.
// Some PTY masters (e.g., opened via /dev/pts/ptmx) have a sibling slave fd
// pointing to /dev/pts/<N>. Scan adjacent fds for one linked to /dev/pts/<N>.
let fd_dir = format!("/proc/{pid}/fd");
if let Ok(entries) = std::fs::read_dir(&fd_dir) {
for entry in entries.flatten() {
if let Ok(target) = std::fs::read_link(entry.path()) {
let s = target.to_string_lossy();
if let Some(rest) = s.strip_prefix("/dev/pts/") {
if rest != "ptmx" {
if let Ok(n) = rest.parse::<u32>() {
return Some(n);
}
}
}
}
}
}
None
}

Expand Down
5 changes: 5 additions & 0 deletions src/daemon/pty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ pub fn spawn_shell(shell: &str, cwd: &Path) -> Result<SpawnResult> {
let _ = std::env::set_current_dir("/");
}

// Ensure terminal environment is set for proper color and feature support.
// The daemon may have been started from a non-terminal context.
std::env::set_var("TERM", "xterm-256color");
std::env::set_var("COLORTERM", "truecolor");

// Exec shell
let shell_cstr =
CString::new(shell).unwrap_or_else(|_| CString::new("/bin/sh").unwrap());
Expand Down
9 changes: 9 additions & 0 deletions src/daemon/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ impl SessionRegistry {
pub fn session_ids(&self) -> Vec<SessionId> {
self.sessions.keys().cloned().collect()
}

/// Find a session whose pts_path matches the given path.
pub fn find_by_pts(&self, pts_path: &std::path::Path) -> Option<&str> {
self.sessions
.iter()
.find(|(_, s)| s.pts_path == pts_path)
.map(|(id, _)| id.as_str())
}
}

#[cfg(test)]
Expand Down Expand Up @@ -168,6 +176,7 @@ mod tests {
registered: false,
capture_path: None,
capture_abort: None,
in_alternate_screen: false,
}
}

Expand Down
Loading
Loading