use std::fmt;
use std::fs::File;
use std::io::{self, Read, Write};
use std::os::windows::io::OwnedHandle;
use std::sync::Arc;
use super::builder::PtyBuilder;
#[cfg(test)]
use crate::backend::BackendKind;
use crate::core::is_disconnect_error;
use crate::core::pseudocon::ConsoleShared;
use crate::core::session::Session as SessionCore;
#[cfg(test)]
use crate::error::Result;
#[cfg(test)]
use crate::size::Size;
use crate::PtyController;
pub(crate) struct Pty {
pub(super) reader: ConoutReader,
pub(super) writer: ConinWriter,
pub(super) inner: Arc<SessionCore>,
}
impl fmt::Debug for Pty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pty")
.field("size", &self.inner.size())
.field("backend_kind", self.inner.backend_kind())
.finish_non_exhaustive()
}
}
impl Pty {
#[must_use]
pub(crate) fn builder() -> PtyBuilder {
PtyBuilder::default()
}
#[cfg(test)]
pub(crate) fn resize(&self, size: Size) -> Result<()> {
self.inner.resize(size)
}
#[must_use]
#[cfg(test)]
pub(crate) fn size(&self) -> Size {
self.inner.size()
}
#[cfg(test)]
pub(crate) fn clear(&self) -> Result<()> {
self.inner.clear()
}
#[must_use]
#[cfg(test)]
pub(crate) fn supports_clear(&self) -> bool {
self.inner.supports_clear()
}
#[must_use]
#[cfg(test)]
pub(crate) fn supports_release(&self) -> bool {
self.inner.supports_release()
}
#[must_use]
#[cfg(test)]
pub(crate) fn backend_kind(&self) -> &BackendKind {
self.inner.backend_kind()
}
#[must_use]
pub(crate) fn controller(&self) -> PtyController {
PtyController::new(Arc::clone(&self.inner))
}
#[must_use]
#[cfg(test)]
pub(crate) fn split(&mut self) -> (ReadHalf<'_>, WriteHalf<'_>) {
let Self { reader, writer, .. } = self;
(ReadHalf { reader }, WriteHalf { writer })
}
#[must_use]
pub(crate) fn into_split(self) -> (OwnedReadHalf, OwnedWriteHalf) {
let Self {
reader,
writer,
inner,
} = self;
let read_session = Arc::clone(&inner);
(
OwnedReadHalf {
reader,
_session: read_session,
},
OwnedWriteHalf {
writer,
_session: inner,
},
)
}
}
impl Read for Pty {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf)
}
}
impl Write for Pty {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[cfg(test)]
pub(crate) struct ReadHalf<'a> {
reader: &'a mut ConoutReader,
}
#[cfg(test)]
impl fmt::Debug for ReadHalf<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ReadHalf").finish_non_exhaustive()
}
}
#[cfg(test)]
impl Read for ReadHalf<'_> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf)
}
}
#[cfg(test)]
pub(crate) struct WriteHalf<'a> {
writer: &'a mut ConinWriter,
}
#[cfg(test)]
impl fmt::Debug for WriteHalf<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WriteHalf").finish_non_exhaustive()
}
}
#[cfg(test)]
impl Write for WriteHalf<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
pub struct OwnedReadHalf {
reader: ConoutReader,
_session: Arc<SessionCore>,
}
impl fmt::Debug for OwnedReadHalf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OwnedReadHalf").finish_non_exhaustive()
}
}
impl Read for OwnedReadHalf {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.reader.read(buf)
}
}
pub struct OwnedWriteHalf {
writer: ConinWriter,
_session: Arc<SessionCore>,
}
impl fmt::Debug for OwnedWriteHalf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OwnedWriteHalf").finish_non_exhaustive()
}
}
impl Write for OwnedWriteHalf {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub(super) struct ConoutReader {
file: Option<File>,
shared: Arc<ConsoleShared>,
saw_eof: bool,
}
fn notify_eof_once(saw_eof: &mut bool, notify: impl FnOnce()) {
if !*saw_eof {
*saw_eof = true;
notify();
}
}
fn conout_error_as_eof(err: io::Error) -> io::Result<()> {
if is_disconnect_error(&err) {
Ok(())
} else {
Err(err)
}
}
impl ConoutReader {
pub(super) fn new(handle: OwnedHandle, shared: Arc<ConsoleShared>) -> Self {
Self {
file: Some(File::from(handle)),
shared,
saw_eof: false,
}
}
fn on_eof(&mut self) {
let shared = &self.shared;
notify_eof_once(&mut self.saw_eof, || shared.notify_reader_eof());
}
}
impl Read for ConoutReader {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
let Some(file) = self.file.as_mut() else {
return Ok(0);
};
match file.read(buf) {
Ok(0) => {
self.on_eof();
Ok(0)
},
Ok(read) => Ok(read),
Err(err) => match conout_error_as_eof(err) {
Ok(()) => {
self.on_eof();
Ok(0)
},
Err(err) => Err(err),
},
}
}
}
impl Drop for ConoutReader {
fn drop(&mut self) {
drop(self.file.take());
self.shared.notify_reader_closed();
}
}
#[derive(Debug)]
pub(super) struct ConinWriter {
file: Option<File>,
session: Arc<SessionCore>,
}
impl ConinWriter {
pub(super) fn new(handle: OwnedHandle, session: Arc<SessionCore>) -> Self {
Self {
file: Some(File::from(handle)),
session,
}
}
}
impl Write for ConinWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file
.as_mut()
.ok_or_else(|| io::Error::new(io::ErrorKind::BrokenPipe, "input is closed"))?
.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Drop for ConinWriter {
fn drop(&mut self) {
drop(self.file.take());
self.session.request_close_after_input();
}
}
#[cfg(test)]
mod behavior_tests {
use std::cell::Cell;
use std::io;
use std::sync::Arc;
use super::{conout_error_as_eof, notify_eof_once, Pty};
use crate::backend::ConPtyBackend;
use crate::blocking::Command;
#[test]
fn writer_drop_requests_close_while_the_controller_keeps_the_session_alive() {
let backend = ConPtyBackend::system()
.expect("ConPTY must be available")
.without_release();
let pty = Pty::builder()
.backend(backend)
.eof_on_root_exit(false)
.build()
.expect("building a forced-legacy pty must succeed");
let controller = pty.controller();
let shared = Arc::clone(&pty.reader.shared);
let child = Command::new("cmd.exe")
.args(["/c", "pause"])
.kill_on_drop(true)
.spawn_in(&pty)
.expect("spawning must succeed");
let (reader, writer) = pty.into_split();
drop(reader);
assert!(
!shared.is_closed(),
"reader retirement alone must not request pseudoconsole close"
);
drop(writer);
assert!(
shared.is_closed(),
"writer drop must claim pseudoconsole close while the controller keeps it alive"
);
drop(child);
drop(controller);
}
#[test]
fn eof_notification_runs_exactly_once() {
let mut saw_eof = false;
let notifications = Cell::new(0);
notify_eof_once(&mut saw_eof, || notifications.set(notifications.get() + 1));
notify_eof_once(&mut saw_eof, || notifications.set(notifications.get() + 1));
assert!(saw_eof);
assert_eq!(notifications.get(), 1);
}
#[test]
fn only_disconnect_errors_become_eof() {
assert!(conout_error_as_eof(io::Error::new(io::ErrorKind::BrokenPipe, "closed")).is_ok());
let err = conout_error_as_eof(io::Error::new(io::ErrorKind::PermissionDenied, "denied"))
.expect_err("an unrelated I/O failure must not become EOF");
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
}
}