use std::io::{Read, Write};
use std::net::TcpStream;
use std::path::Path;
use std::process::ExitStatus;
#[cfg(windows)]
use std::io::{BufRead, BufReader};
#[cfg(unix)]
use std::os::unix::net::UnixStream;
#[cfg(unix)]
use std::path::PathBuf;
#[cfg(windows)]
use std::process::{Child, ChildStdin, ChildStdout};
#[cfg(windows)]
use std::sync::{Arc, Mutex};
use super::super::config::ENV_DEVSHELL_VM_SOCKET;
#[cfg(windows)]
use super::super::podman_machine;
use super::super::VmError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum SocketSpec {
#[cfg(unix)]
Unix(PathBuf),
Tcp(String),
#[cfg(windows)]
Stdio,
}
pub(super) enum IpcStream {
#[cfg(unix)]
Unix(UnixStream),
Tcp(TcpStream),
#[cfg(windows)]
StdioPipe(StdioPipe),
}
#[cfg(windows)]
pub(super) struct StdioPipe {
inner: Arc<Mutex<StdioPipeInner>>,
}
#[cfg(windows)]
struct StdioPipeInner {
_child: Child,
stdin: ChildStdin,
reader: BufReader<ChildStdout>,
}
#[cfg(windows)]
impl StdioPipe {
pub(super) fn new(child: Child, stdin: ChildStdin, stdout: ChildStdout) -> Self {
let reader = BufReader::new(stdout);
Self {
inner: Arc::new(Mutex::new(StdioPipeInner {
_child: child,
stdin,
reader,
})),
}
}
pub(super) fn read_json_line(&self) -> Result<serde_json::Value, VmError> {
let mut out = String::new();
let n = {
let mut g = self.inner.lock().map_err(|e| VmError::Ipc(e.to_string()))?;
g.reader
.read_line(&mut out)
.map_err(|e| VmError::Ipc(e.to_string()))?
};
if n == 0 || out.trim().is_empty() {
return Err(VmError::Ipc(
"beta sidecar (stdio) sent no JSON line (connection closed or empty response). \
Check podman machine start, devshell-vm Linux binary, or set DEVSHELL_VM_BACKEND=host."
.into(),
));
}
serde_json::from_str(out.trim()).map_err(|e| {
VmError::Ipc(format!(
"beta sidecar response is not JSON ({e}); first line prefix: {:?}",
out.chars().take(80).collect::<String>()
))
})
}
}
#[cfg(windows)]
impl Write for StdioPipe {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let mut g = self
.inner
.lock()
.map_err(|e| std::io::Error::other(e.to_string()))?;
g.stdin.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
let mut g = self
.inner
.lock()
.map_err(|e| std::io::Error::other(e.to_string()))?;
g.stdin.flush()
}
}
impl Read for IpcStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
match self {
#[cfg(unix)]
Self::Unix(u) => u.read(buf),
Self::Tcp(t) => t.read(buf),
#[cfg(windows)]
Self::StdioPipe(s) => {
let mut g = s
.inner
.lock()
.map_err(|e| std::io::Error::other(e.to_string()))?;
g.reader.read(buf)
}
}
}
}
impl Write for IpcStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
match self {
#[cfg(unix)]
Self::Unix(u) => u.write(buf),
Self::Tcp(t) => t.write(buf),
#[cfg(windows)]
Self::StdioPipe(s) => Write::write(s, buf),
}
}
fn flush(&mut self) -> std::io::Result<()> {
match self {
#[cfg(unix)]
Self::Unix(u) => u.flush(),
Self::Tcp(t) => t.flush(),
#[cfg(windows)]
Self::StdioPipe(s) => Write::flush(s),
}
}
}
pub(super) fn parse_devshell_vm_socket(raw: &str) -> Result<SocketSpec, VmError> {
let t = raw.trim();
if t.is_empty() {
return Err(VmError::Ipc(format!("{ENV_DEVSHELL_VM_SOCKET} is empty")));
}
#[cfg(windows)]
if t.eq_ignore_ascii_case("stdio") {
return Ok(SocketSpec::Stdio);
}
if let Some(rest) = t.strip_prefix("tcp://") {
let addr = rest.trim();
if addr.is_empty() {
return Err(VmError::Ipc("tcp:// address is empty".into()));
}
return Ok(SocketSpec::Tcp(addr.to_string()));
}
if let Some(rest) = t.strip_prefix("tcp:") {
let addr = rest.trim();
if !addr.is_empty() && !addr.contains('\\') && !addr.starts_with('/') {
return Ok(SocketSpec::Tcp(addr.to_string()));
}
}
#[cfg(unix)]
{
Ok(SocketSpec::Unix(PathBuf::from(t)))
}
#[cfg(not(unix))]
{
Err(VmError::Ipc(
"DEVSHELL_VM_SOCKET on Windows must be stdio or tcp:HOST:PORT (e.g. tcp:127.0.0.1:9847); see docs/devshell-vm-windows.md".into(),
))
}
}
pub(super) fn connect_ipc(spec: &SocketSpec, workspace_root: &Path) -> Result<IpcStream, VmError> {
#[cfg(not(windows))]
let _ = workspace_root;
match spec {
#[cfg(unix)]
SocketSpec::Unix(p) => UnixStream::connect(p).map(IpcStream::Unix).map_err(|e| {
VmError::Ipc(format!(
"connect {}: {e}; start: devshell-vm --serve-socket {}",
p.display(),
p.display()
))
}),
SocketSpec::Tcp(addr) => TcpStream::connect(addr).map(IpcStream::Tcp).map_err(|e| {
let suffix = if cfg!(windows) {
"\nIf nothing is listening: start devshell-vm --serve-tcp, or set DEVSHELL_VM_BACKEND=host"
} else {
""
};
VmError::Ipc(format!(
"connect tcp {addr}: {e}; start: devshell-vm --serve-tcp {addr}{suffix}"
))
}),
#[cfg(windows)]
SocketSpec::Stdio => {
let mut child = podman_machine::spawn_devshell_vm_stdio(workspace_root)?;
let stdin = child.stdin.take().ok_or_else(|| {
VmError::Ipc("podman machine ssh: missing stdin pipe".to_string())
})?;
let stdout = child.stdout.take().ok_or_else(|| {
VmError::Ipc("podman machine ssh: missing stdout pipe".to_string())
})?;
Ok(IpcStream::StdioPipe(StdioPipe::new(child, stdin, stdout)))
}
}
}
pub(super) fn exit_status_from_code(code: i32) -> ExitStatus {
let code = code.clamp(0, 255);
#[cfg(windows)]
{
std::os::windows::process::ExitStatusExt::from_raw(code as u32)
}
#[cfg(not(windows))]
{
std::os::unix::process::ExitStatusExt::from_raw(code << 8)
}
}