use std::path::{Path, PathBuf};
use std::sync::Mutex as StdMutex;
use std::sync::{Arc, MutexGuard};
use tokio::process::Command;
use tracing::warn;
use crate::error::{Result, ServerError};
use crate::server::shell_access::{configure_live_shell_context, resolve_process_cwd};
#[derive(Clone, Debug, Default)]
pub(crate) struct ConnectionShellState {
shell_pid: Arc<StdMutex<Option<u32>>>,
}
impl ConnectionShellState {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn shell_pid(&self) -> Option<u32> {
*self.lock_shell_pid()
}
pub(crate) fn set_shell_pid(&self, pid: Option<u32>) {
let mut guard = self.lock_shell_pid();
tracing::info!("Connection state update: Shell PID registered as {:?}", pid);
*guard = pid;
}
pub(crate) fn clear_shell_pid_if_matches(&self, pid: Option<u32>) {
let mut guard = self.lock_shell_pid();
if *guard == pid {
tracing::info!("Connection state update: Clearing shell PID {:?}", pid);
*guard = None;
} else {
tracing::debug!(
"Not clearing shell PID; current={:?}, requested_clear={:?}",
*guard,
pid
);
}
}
fn lock_shell_pid(&self) -> MutexGuard<'_, Option<u32>> {
match self.shell_pid.lock() {
Ok(guard) => guard,
Err(poisoned) => {
warn!("shell pid state mutex poisoned; recovering inner state");
poisoned.into_inner()
}
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum ShellContext {
Live { pid: u32 },
Stateless,
}
impl ShellContext {
pub(super) fn from_state(shell_state: &ConnectionShellState) -> Self {
let pid = shell_state.shell_pid();
match pid {
Some(pid) => {
tracing::info!("Transfer context: Live (shell PID {})", pid);
Self::Live { pid }
}
None => {
tracing::info!("Transfer context: Stateless (no active shell PID)");
Self::Stateless
}
}
}
pub(super) fn configure(self, command: &mut Command) {
if let Self::Live { pid } = self {
configure_live_shell_context(command, pid);
}
}
pub(super) async fn cwd(self) -> Result<PathBuf> {
match self {
Self::Live { pid } => {
let path = resolve_process_cwd(pid).await?;
tracing::debug!(
"Resolved live shell CWD for PID {}: {}",
pid,
path.display()
);
Ok(path)
}
Self::Stateless => {
let home = server_home_dir().ok_or_else(|| ServerError::ShellError {
details: "could not determine server home directory".to_string(),
})?;
tracing::debug!("Resolved stateless CWD (home): {}", home.display());
Ok(home)
}
}
}
pub(super) async fn path_exists(self, path: &str) -> Result<bool> {
#[cfg(target_os = "linux")]
if let Self::Live { .. } = self {
let mut command = Command::new("test");
command.arg("-e").arg(path);
self.configure(&mut command);
let status = command
.status()
.await
.map_err(|e| ServerError::ShellError {
details: format!("failed to probe remote path existence: {e}"),
})?;
return Ok(status.success());
}
Ok(tokio::fs::try_exists(path).await?)
}
pub(super) async fn is_dir(self, path: &str) -> Result<bool> {
#[cfg(target_os = "linux")]
if let Self::Live { .. } = self {
let mut command = Command::new("test");
command.arg("-d").arg(path);
self.configure(&mut command);
let status = command
.status()
.await
.map_err(|e| ServerError::ShellError {
details: format!("failed to probe remote path directory: {e}"),
})?;
return Ok(status.success());
}
if let Ok(metadata) = tokio::fs::metadata(path).await {
Ok(metadata.is_dir())
} else {
Ok(false)
}
}
pub(super) async fn path_missing(self, path: &str) -> Result<bool> {
#[cfg(target_os = "linux")]
if let Self::Live { .. } = self {
let mut command = Command::new("test");
command.arg("!").arg("-e").arg(path);
self.configure(&mut command);
let status = command
.status()
.await
.map_err(|e| ServerError::ShellError {
details: format!("failed to probe remote path absence: {e}"),
})?;
return Ok(status.success());
}
Ok(!tokio::fs::try_exists(path).await?)
}
pub(super) async fn create_dir_all(self, path: &Path) -> Result<bool> {
#[cfg(target_os = "linux")]
if let Self::Live { .. } = self {
let mut command = Command::new("mkdir");
command.arg("-p").arg("--").arg(path);
self.configure(&mut command);
let status = command
.status()
.await
.map_err(|e| ServerError::ShellError {
details: format!("failed to spawn mkdir: {e}"),
})?;
return Ok(status.success());
}
tokio::fs::create_dir_all(path).await?;
Ok(true)
}
pub(super) async fn remove_file_if_present(self, path: &str) {
#[cfg(target_os = "linux")]
if let Self::Live { .. } = self {
let mut command = Command::new("rm");
command.arg("-f").arg("--").arg(path);
self.configure(&mut command);
let _ = command.status().await;
return;
}
let _ = tokio::fs::remove_file(path).await;
}
pub(super) async fn rename(self, from: &str, to: &str) -> Result<bool> {
#[cfg(target_os = "linux")]
if let Self::Live { .. } = self {
let mut command = Command::new("mv");
command.arg("-f").arg("--").arg(from).arg(to);
self.configure(&mut command);
let status = command
.status()
.await
.map_err(|e| ServerError::ShellError {
details: format!("failed to spawn mv for atomic rename: {e}"),
})?;
return Ok(status.success());
}
tokio::fs::rename(from, to).await?;
Ok(true)
}
pub(super) async fn chmod(self, path: &str, mode: u32) {
#[cfg(target_os = "linux")]
if let Self::Live { .. } = self {
let mut command = Command::new("chmod");
command.arg(format!("{:o}", mode)).arg("--").arg(path);
self.configure(&mut command);
let _ = command.status().await;
return;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).await;
}
#[cfg(not(unix))]
{
let _ = (path, mode);
}
}
}
impl ShellContext {
pub(crate) async fn resolve_path(self, raw: &str) -> Result<PathBuf> {
if raw.trim().is_empty() {
return Err(ServerError::TransferFailed {
details: "transfer path is empty".to_string(),
}
.into());
}
let path = Path::new(raw);
if path.is_absolute() {
return Ok(path.to_path_buf());
}
if raw == "~" {
return server_home_dir().ok_or_else(|| {
ServerError::ShellError {
details: "could not determine server home directory for ~ expansion"
.to_string(),
}
.into()
});
}
if let Some(home_relative) = raw.strip_prefix("~/") {
let home = server_home_dir().ok_or_else(|| ServerError::ShellError {
details: "could not determine server home directory for ~/ expansion".to_string(),
})?;
return Ok(home.join(home_relative));
}
let base = self.cwd().await?;
let full = base.join(path);
tracing::info!("Resolved remote path: '{}' -> '{}'", raw, full.display());
Ok(full)
}
}
fn server_home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
}