mirror of
https://github.com/asterinas/asterinas.git
synced 2025-06-09 05:16:47 +00:00
Refactor tty&pty code
This commit is contained in:
parent
001326110e
commit
9d8a2b420d
@ -21,22 +21,8 @@ PtyTest.SwitchNoncanonToCanonNoNewlineBig
|
|||||||
PtyTest.NoncanonBigWrite
|
PtyTest.NoncanonBigWrite
|
||||||
PtyTest.SwitchNoncanonToCanonMultiline
|
PtyTest.SwitchNoncanonToCanonMultiline
|
||||||
PtyTest.SwitchTwiceMultiline
|
PtyTest.SwitchTwiceMultiline
|
||||||
JobControlTest.SetTTYMaster
|
|
||||||
JobControlTest.SetTTY
|
|
||||||
JobControlTest.SetTTYNonLeader
|
|
||||||
JobControlTest.SetTTYBadArg
|
JobControlTest.SetTTYBadArg
|
||||||
JobControlTest.SetTTYDifferentSession
|
JobControlTest.SetTTYDifferentSession
|
||||||
JobControlTest.ReleaseTTY
|
|
||||||
JobControlTest.ReleaseUnsetTTY
|
|
||||||
JobControlTest.ReleaseWrongTTY
|
|
||||||
JobControlTest.ReleaseTTYNonLeader
|
|
||||||
JobControlTest.ReleaseTTYDifferentSession
|
|
||||||
JobControlTest.ReleaseTTYSignals
|
JobControlTest.ReleaseTTYSignals
|
||||||
JobControlTest.GetForegroundProcessGroup
|
|
||||||
JobControlTest.GetForegroundProcessGroupNonControlling
|
|
||||||
JobControlTest.SetForegroundProcessGroup
|
JobControlTest.SetForegroundProcessGroup
|
||||||
JobControlTest.SetForegroundProcessGroupWrongTTY
|
|
||||||
JobControlTest.SetForegroundProcessGroupNegPgid
|
|
||||||
JobControlTest.SetForegroundProcessGroupEmptyProcessGroup
|
JobControlTest.SetForegroundProcessGroupEmptyProcessGroup
|
||||||
JobControlTest.SetForegroundProcessGroupDifferentSession
|
|
||||||
JobControlTest.OrphanRegression
|
|
@ -33,7 +33,7 @@ pub fn init() -> Result<()> {
|
|||||||
|
|
||||||
pub fn new_pty_pair(index: u32, ptmx: Arc<dyn Inode>) -> Result<(Arc<PtyMaster>, Arc<PtySlave>)> {
|
pub fn new_pty_pair(index: u32, ptmx: Arc<dyn Inode>) -> Result<(Arc<PtyMaster>, Arc<PtySlave>)> {
|
||||||
debug!("pty index = {}", index);
|
debug!("pty index = {}", index);
|
||||||
let master = Arc::new(PtyMaster::new(ptmx, index));
|
let master = PtyMaster::new(ptmx, index);
|
||||||
let slave = Arc::new(PtySlave::new(master.clone()));
|
let slave = PtySlave::new(&master);
|
||||||
Ok((master, slave))
|
Ok((master, slave))
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,16 @@ use ringbuf::{ring_buffer::RbBase, HeapRb, Rb};
|
|||||||
use crate::device::tty::line_discipline::LineDiscipline;
|
use crate::device::tty::line_discipline::LineDiscipline;
|
||||||
use crate::events::IoEvents;
|
use crate::events::IoEvents;
|
||||||
use crate::fs::device::{Device, DeviceId, DeviceType};
|
use crate::fs::device::{Device, DeviceId, DeviceType};
|
||||||
use crate::fs::file_handle::FileLike;
|
use crate::fs::devpts::DevPts;
|
||||||
use crate::fs::fs_resolver::FsPath;
|
use crate::fs::fs_resolver::FsPath;
|
||||||
|
use crate::fs::inode_handle::FileIo;
|
||||||
use crate::fs::utils::{AccessMode, Inode, InodeMode, IoctlCmd};
|
use crate::fs::utils::{AccessMode, Inode, InodeMode, IoctlCmd};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::process::signal::signals::kernel::KernelSignal;
|
||||||
use crate::process::signal::{Pollee, Poller};
|
use crate::process::signal::{Pollee, Poller};
|
||||||
|
use crate::process::{JobControl, Terminal};
|
||||||
use crate::util::{read_val_from_user, write_val_to_user};
|
use crate::util::{read_val_from_user, write_val_to_user};
|
||||||
|
|
||||||
const PTS_DIR: &str = "/dev/pts";
|
|
||||||
const BUFFER_CAPACITY: usize = 4096;
|
const BUFFER_CAPACITY: usize = 4096;
|
||||||
|
|
||||||
/// Pesudo terminal master.
|
/// Pesudo terminal master.
|
||||||
@ -23,19 +25,23 @@ pub struct PtyMaster {
|
|||||||
index: u32,
|
index: u32,
|
||||||
output: Arc<LineDiscipline>,
|
output: Arc<LineDiscipline>,
|
||||||
input: SpinLock<HeapRb<u8>>,
|
input: SpinLock<HeapRb<u8>>,
|
||||||
|
job_control: JobControl,
|
||||||
/// The state of input buffer
|
/// The state of input buffer
|
||||||
pollee: Pollee,
|
pollee: Pollee,
|
||||||
|
weak_self: Weak<Self>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PtyMaster {
|
impl PtyMaster {
|
||||||
pub fn new(ptmx: Arc<dyn Inode>, index: u32) -> Self {
|
pub fn new(ptmx: Arc<dyn Inode>, index: u32) -> Arc<Self> {
|
||||||
Self {
|
Arc::new_cyclic(|weak_ref| PtyMaster {
|
||||||
ptmx,
|
ptmx,
|
||||||
index,
|
index,
|
||||||
output: LineDiscipline::new(),
|
output: LineDiscipline::new(),
|
||||||
input: SpinLock::new(HeapRb::new(BUFFER_CAPACITY)),
|
input: SpinLock::new(HeapRb::new(BUFFER_CAPACITY)),
|
||||||
|
job_control: JobControl::new(),
|
||||||
pollee: Pollee::new(IoEvents::OUT),
|
pollee: Pollee::new(IoEvents::OUT),
|
||||||
}
|
weak_self: weak_ref.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn index(&self) -> u32 {
|
pub fn index(&self) -> u32 {
|
||||||
@ -46,16 +52,12 @@ impl PtyMaster {
|
|||||||
&self.ptmx
|
&self.ptmx
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn slave_push_byte(&self, byte: u8) {
|
pub(super) fn slave_push_char(&self, ch: u8) {
|
||||||
let mut input = self.input.lock_irq_disabled();
|
let mut input = self.input.lock_irq_disabled();
|
||||||
input.push_overwrite(byte);
|
input.push_overwrite(ch);
|
||||||
self.update_state(&input);
|
self.update_state(&input);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn slave_read(&self, buf: &mut [u8]) -> Result<usize> {
|
|
||||||
self.output.read(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn slave_poll(&self, mask: IoEvents, poller: Option<&Poller>) -> IoEvents {
|
pub(super) fn slave_poll(&self, mask: IoEvents, poller: Option<&Poller>) -> IoEvents {
|
||||||
let mut poll_status = IoEvents::empty();
|
let mut poll_status = IoEvents::empty();
|
||||||
|
|
||||||
@ -87,7 +89,7 @@ impl PtyMaster {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileLike for PtyMaster {
|
impl FileIo for PtyMaster {
|
||||||
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
||||||
// TODO: deal with nonblocking read
|
// TODO: deal with nonblocking read
|
||||||
if buf.is_empty() {
|
if buf.is_empty() {
|
||||||
@ -121,10 +123,22 @@ impl FileLike for PtyMaster {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write(&self, buf: &[u8]) -> Result<usize> {
|
fn write(&self, buf: &[u8]) -> Result<usize> {
|
||||||
let mut input = self.input.lock();
|
let may_send_signal = || {
|
||||||
|
let Some(foreground) = self.foreground() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let send_signal = move |signal: KernelSignal| {
|
||||||
|
foreground.kernel_signal(signal);
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Arc::new(send_signal) as Arc<dyn Fn(KernelSignal) + Send + Sync>)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut input = self.input.lock();
|
||||||
for character in buf {
|
for character in buf {
|
||||||
self.output.push_char(*character, |content| {
|
self.output
|
||||||
|
.push_char(*character, may_send_signal, |content| {
|
||||||
for byte in content.as_bytes() {
|
for byte in content.as_bytes() {
|
||||||
input.push_overwrite(*byte);
|
input.push_overwrite(*byte);
|
||||||
}
|
}
|
||||||
@ -193,29 +207,35 @@ impl FileLike for PtyMaster {
|
|||||||
self.output.set_window_size(winsize);
|
self.output.set_window_size(winsize);
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
IoctlCmd::TIOCSCTTY => {
|
|
||||||
// TODO: reimplement when adding session.
|
|
||||||
let foreground = {
|
|
||||||
let current = current!();
|
|
||||||
let process_group = current.process_group().unwrap();
|
|
||||||
Arc::downgrade(&process_group)
|
|
||||||
};
|
|
||||||
self.output.set_fg(foreground);
|
|
||||||
Ok(0)
|
|
||||||
}
|
|
||||||
IoctlCmd::TIOCGPGRP => {
|
IoctlCmd::TIOCGPGRP => {
|
||||||
let Some(fg_pgid) = self.output.fg_pgid() else {
|
let Some(foreground) = self.foreground() else {
|
||||||
return_errno_with_message!(
|
return_errno_with_message!(
|
||||||
Errno::ESRCH,
|
Errno::ESRCH,
|
||||||
"the foreground process group does not exist"
|
"the foreground process group does not exist"
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
let fg_pgid = foreground.pgid();
|
||||||
write_val_to_user(arg, &fg_pgid)?;
|
write_val_to_user(arg, &fg_pgid)?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
IoctlCmd::TIOCSPGRP => {
|
||||||
|
let pgid = {
|
||||||
|
let pgid: i32 = read_val_from_user(arg)?;
|
||||||
|
if pgid < 0 {
|
||||||
|
return_errno_with_message!(Errno::EINVAL, "negative pgid");
|
||||||
|
}
|
||||||
|
pgid as u32
|
||||||
|
};
|
||||||
|
|
||||||
|
self.set_foreground(&pgid)?;
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
IoctlCmd::TIOCSCTTY => {
|
||||||
|
self.set_current_session()?;
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
IoctlCmd::TIOCNOTTY => {
|
IoctlCmd::TIOCNOTTY => {
|
||||||
// TODO: reimplement when adding session.
|
self.release_current_session()?;
|
||||||
self.output.set_fg(Weak::new());
|
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
IoctlCmd::FIONREAD => {
|
IoctlCmd::FIONREAD => {
|
||||||
@ -246,15 +266,47 @@ impl FileLike for PtyMaster {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PtySlave(Arc<PtyMaster>);
|
impl Terminal for PtyMaster {
|
||||||
|
fn arc_self(&self) -> Arc<dyn Terminal> {
|
||||||
|
self.weak_self.upgrade().unwrap() as _
|
||||||
|
}
|
||||||
|
|
||||||
|
fn job_control(&self) -> &JobControl {
|
||||||
|
&self.job_control
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PtyMaster {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let fs = self.ptmx.fs();
|
||||||
|
let devpts = fs.downcast_ref::<DevPts>().unwrap();
|
||||||
|
|
||||||
|
let index = self.index;
|
||||||
|
devpts.remove_slave(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PtySlave {
|
||||||
|
master: Weak<PtyMaster>,
|
||||||
|
job_control: JobControl,
|
||||||
|
weak_self: Weak<Self>,
|
||||||
|
}
|
||||||
|
|
||||||
impl PtySlave {
|
impl PtySlave {
|
||||||
pub fn new(master: Arc<PtyMaster>) -> Self {
|
pub fn new(master: &Arc<PtyMaster>) -> Arc<Self> {
|
||||||
PtySlave(master)
|
Arc::new_cyclic(|weak_ref| PtySlave {
|
||||||
|
master: Arc::downgrade(master),
|
||||||
|
job_control: JobControl::new(),
|
||||||
|
weak_self: weak_ref.clone(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn index(&self) -> u32 {
|
pub fn index(&self) -> u32 {
|
||||||
self.0.index()
|
self.master().index()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn master(&self) -> Arc<PtyMaster> {
|
||||||
|
self.master.upgrade().unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,50 +318,92 @@ impl Device for PtySlave {
|
|||||||
fn id(&self) -> crate::fs::device::DeviceId {
|
fn id(&self) -> crate::fs::device::DeviceId {
|
||||||
DeviceId::new(88, self.index())
|
DeviceId::new(88, self.index())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal for PtySlave {
|
||||||
|
fn arc_self(&self) -> Arc<dyn Terminal> {
|
||||||
|
self.weak_self.upgrade().unwrap() as _
|
||||||
|
}
|
||||||
|
|
||||||
|
fn job_control(&self) -> &JobControl {
|
||||||
|
&self.job_control
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileIo for PtySlave {
|
||||||
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
||||||
self.0.slave_read(buf)
|
self.master()
|
||||||
|
.output
|
||||||
|
.read(buf, || self.job_control.current_belongs_to_foreground())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&self, buf: &[u8]) -> Result<usize> {
|
fn write(&self, buf: &[u8]) -> Result<usize> {
|
||||||
|
let master = self.master();
|
||||||
for ch in buf {
|
for ch in buf {
|
||||||
// do we need to add '\r' here?
|
// do we need to add '\r' here?
|
||||||
if *ch == b'\n' {
|
if *ch == b'\n' {
|
||||||
self.0.slave_push_byte(b'\r');
|
master.slave_push_char(b'\r');
|
||||||
self.0.slave_push_byte(b'\n');
|
master.slave_push_char(b'\n');
|
||||||
} else {
|
} else {
|
||||||
self.0.slave_push_byte(*ch);
|
master.slave_push_char(*ch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(buf.len())
|
Ok(buf.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn poll(&self, mask: IoEvents, poller: Option<&Poller>) -> IoEvents {
|
||||||
|
self.master().slave_poll(mask, poller)
|
||||||
|
}
|
||||||
|
|
||||||
fn ioctl(&self, cmd: IoctlCmd, arg: usize) -> Result<i32> {
|
fn ioctl(&self, cmd: IoctlCmd, arg: usize) -> Result<i32> {
|
||||||
match cmd {
|
match cmd {
|
||||||
IoctlCmd::TCGETS
|
IoctlCmd::TCGETS
|
||||||
| IoctlCmd::TCSETS
|
| IoctlCmd::TCSETS
|
||||||
| IoctlCmd::TIOCGPGRP
|
|
||||||
| IoctlCmd::TIOCGPTN
|
| IoctlCmd::TIOCGPTN
|
||||||
| IoctlCmd::TIOCGWINSZ
|
| IoctlCmd::TIOCGWINSZ
|
||||||
| IoctlCmd::TIOCSWINSZ => self.0.ioctl(cmd, arg),
|
| IoctlCmd::TIOCSWINSZ => self.master().ioctl(cmd, arg),
|
||||||
|
IoctlCmd::TIOCGPGRP => {
|
||||||
|
if !self.is_controlling_terminal() {
|
||||||
|
return_errno_with_message!(Errno::ENOTTY, "slave is not controlling terminal");
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(foreground) = self.foreground() else {
|
||||||
|
return_errno_with_message!(
|
||||||
|
Errno::ESRCH,
|
||||||
|
"the foreground process group does not exist"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let fg_pgid = foreground.pgid();
|
||||||
|
write_val_to_user(arg, &fg_pgid)?;
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
|
IoctlCmd::TIOCSPGRP => {
|
||||||
|
let pgid = {
|
||||||
|
let pgid: i32 = read_val_from_user(arg)?;
|
||||||
|
if pgid < 0 {
|
||||||
|
return_errno_with_message!(Errno::EINVAL, "negative pgid");
|
||||||
|
}
|
||||||
|
pgid as u32
|
||||||
|
};
|
||||||
|
|
||||||
|
self.set_foreground(&pgid)?;
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
IoctlCmd::TIOCSCTTY => {
|
IoctlCmd::TIOCSCTTY => {
|
||||||
// TODO:
|
self.set_current_session()?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
IoctlCmd::TIOCNOTTY => {
|
IoctlCmd::TIOCNOTTY => {
|
||||||
// TODO:
|
self.release_current_session()?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
IoctlCmd::FIONREAD => {
|
IoctlCmd::FIONREAD => {
|
||||||
let buffer_len = self.0.slave_buf_len() as i32;
|
let buffer_len = self.master().slave_buf_len() as i32;
|
||||||
write_val_to_user(arg, &buffer_len)?;
|
write_val_to_user(arg, &buffer_len)?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
_ => Ok(0),
|
_ => Ok(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn poll(&self, mask: IoEvents, poller: Option<&Poller>) -> IoEvents {
|
|
||||||
self.0.slave_poll(mask, poller)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
47
services/libs/jinux-std/src/device/tty/device.rs
Normal file
47
services/libs/jinux-std/src/device/tty/device.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use crate::events::IoEvents;
|
||||||
|
use crate::fs::device::{Device, DeviceId, DeviceType};
|
||||||
|
use crate::fs::inode_handle::FileIo;
|
||||||
|
use crate::prelude::*;
|
||||||
|
use crate::process::signal::Poller;
|
||||||
|
|
||||||
|
/// Corresponds to `/dev/tty` in the file system. This device represents the controlling terminal
|
||||||
|
/// of the session of current process.
|
||||||
|
pub struct TtyDevice;
|
||||||
|
|
||||||
|
impl Device for TtyDevice {
|
||||||
|
fn open(&self) -> Result<Option<Arc<dyn FileIo>>> {
|
||||||
|
let current = current!();
|
||||||
|
let session = current.session().unwrap();
|
||||||
|
|
||||||
|
let Some(terminal) = session.terminal() else {
|
||||||
|
return_errno_with_message!(
|
||||||
|
Errno::ENOTTY,
|
||||||
|
"the session does not have controlling terminal"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Some(terminal as Arc<dyn FileIo>))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn type_(&self) -> DeviceType {
|
||||||
|
DeviceType::CharDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> DeviceId {
|
||||||
|
DeviceId::new(5, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileIo for TtyDevice {
|
||||||
|
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&self, buf: &[u8]) -> Result<usize> {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll(&self, mask: IoEvents, poller: Option<&Poller>) -> IoEvents {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,10 @@
|
|||||||
use crate::events::IoEvents;
|
use crate::events::IoEvents;
|
||||||
|
use crate::prelude::*;
|
||||||
use crate::process::signal::constants::{SIGINT, SIGQUIT};
|
use crate::process::signal::constants::{SIGINT, SIGQUIT};
|
||||||
|
use crate::process::signal::signals::kernel::KernelSignal;
|
||||||
use crate::process::signal::{Pollee, Poller};
|
use crate::process::signal::{Pollee, Poller};
|
||||||
use crate::process::ProcessGroup;
|
|
||||||
use crate::thread::work_queue::work_item::WorkItem;
|
use crate::thread::work_queue::work_item::WorkItem;
|
||||||
use crate::thread::work_queue::{submit_work_item, WorkPriority};
|
use crate::thread::work_queue::{submit_work_item, WorkPriority};
|
||||||
use crate::{
|
|
||||||
prelude::*,
|
|
||||||
process::{signal::signals::kernel::KernelSignal, Pgid},
|
|
||||||
};
|
|
||||||
use alloc::format;
|
use alloc::format;
|
||||||
use jinux_frame::trap::disable_local;
|
use jinux_frame::trap::disable_local;
|
||||||
use ringbuf::{ring_buffer::RbBase, Rb, StaticRb};
|
use ringbuf::{ring_buffer::RbBase, Rb, StaticRb};
|
||||||
@ -24,8 +21,6 @@ pub struct LineDiscipline {
|
|||||||
current_line: SpinLock<CurrentLine>,
|
current_line: SpinLock<CurrentLine>,
|
||||||
/// The read buffer
|
/// The read buffer
|
||||||
read_buffer: SpinLock<StaticRb<u8, BUFFER_CAPACITY>>,
|
read_buffer: SpinLock<StaticRb<u8, BUFFER_CAPACITY>>,
|
||||||
/// The foreground process group
|
|
||||||
foreground: SpinLock<Weak<ProcessGroup>>,
|
|
||||||
/// termios
|
/// termios
|
||||||
termios: SpinLock<KernelTermios>,
|
termios: SpinLock<KernelTermios>,
|
||||||
/// Windows size,
|
/// Windows size,
|
||||||
@ -87,7 +82,6 @@ impl LineDiscipline {
|
|||||||
Self {
|
Self {
|
||||||
current_line: SpinLock::new(CurrentLine::new()),
|
current_line: SpinLock::new(CurrentLine::new()),
|
||||||
read_buffer: SpinLock::new(StaticRb::default()),
|
read_buffer: SpinLock::new(StaticRb::default()),
|
||||||
foreground: SpinLock::new(Weak::new()),
|
|
||||||
termios: SpinLock::new(KernelTermios::default()),
|
termios: SpinLock::new(KernelTermios::default()),
|
||||||
winsize: SpinLock::new(WinSize::default()),
|
winsize: SpinLock::new(WinSize::default()),
|
||||||
pollee: Pollee::new(IoEvents::empty()),
|
pollee: Pollee::new(IoEvents::empty()),
|
||||||
@ -98,7 +92,15 @@ impl LineDiscipline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Push char to line discipline.
|
/// Push char to line discipline.
|
||||||
pub fn push_char<F: FnMut(&str)>(&self, ch: u8, echo_callback: F) {
|
pub fn push_char<
|
||||||
|
F1: Fn() -> Option<Arc<dyn Fn(KernelSignal) + Send + Sync>>,
|
||||||
|
F2: FnMut(&str),
|
||||||
|
>(
|
||||||
|
&self,
|
||||||
|
ch: u8,
|
||||||
|
may_send_signal: F1,
|
||||||
|
echo_callback: F2,
|
||||||
|
) {
|
||||||
let termios = self.termios.lock_irq_disabled();
|
let termios = self.termios.lock_irq_disabled();
|
||||||
|
|
||||||
let ch = if termios.contains_icrnl() && ch == b'\r' {
|
let ch = if termios.contains_icrnl() && ch == b'\r' {
|
||||||
@ -107,7 +109,7 @@ impl LineDiscipline {
|
|||||||
ch
|
ch
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.may_send_signal_to_foreground(&termios, ch) {
|
if self.may_send_signal(&termios, ch, may_send_signal) {
|
||||||
// The char is already dealt with, so just return
|
// The char is already dealt with, so just return
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -158,31 +160,32 @@ impl LineDiscipline {
|
|||||||
self.update_readable_state_deferred();
|
self.update_readable_state_deferred();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn may_send_signal_to_foreground(&self, termios: &KernelTermios, ch: u8) -> bool {
|
fn may_send_signal<F: Fn() -> Option<Arc<dyn Fn(KernelSignal) + Send + Sync>>>(
|
||||||
if !termios.contains_isig() {
|
&self,
|
||||||
|
termios: &KernelTermios,
|
||||||
|
ch: u8,
|
||||||
|
may_send_signal: F,
|
||||||
|
) -> bool {
|
||||||
|
if !termios.is_canonical_mode() || !termios.contains_isig() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(foreground) = self.foreground.lock().upgrade() else {
|
let Some(send_signal) = may_send_signal() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let signal = match ch {
|
let signal = match ch {
|
||||||
item if item == *termios.get_special_char(CC_C_CHAR::VINTR) => {
|
ch if ch == *termios.get_special_char(CC_C_CHAR::VINTR) => KernelSignal::new(SIGINT),
|
||||||
KernelSignal::new(SIGINT)
|
ch if ch == *termios.get_special_char(CC_C_CHAR::VQUIT) => KernelSignal::new(SIGQUIT),
|
||||||
}
|
|
||||||
item if item == *termios.get_special_char(CC_C_CHAR::VQUIT) => {
|
|
||||||
KernelSignal::new(SIGQUIT)
|
|
||||||
}
|
|
||||||
_ => return false,
|
_ => return false,
|
||||||
};
|
};
|
||||||
// `kernel_signal()` may cause sleep, so only construct parameters here.
|
// `kernel_signal()` may cause sleep, so only construct parameters here.
|
||||||
self.work_item_para.lock_irq_disabled().kernel_signal = Some(signal);
|
self.work_item_para.lock_irq_disabled().kernel_signal = Some((send_signal, signal));
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_readable_state(&self) {
|
pub fn update_readable_state(&self) {
|
||||||
let buffer = self.read_buffer.lock_irq_disabled();
|
let buffer = self.read_buffer.lock_irq_disabled();
|
||||||
if !buffer.is_empty() {
|
if !buffer.is_empty() {
|
||||||
self.pollee.add_events(IoEvents::IN);
|
self.pollee.add_events(IoEvents::IN);
|
||||||
@ -193,7 +196,6 @@ impl LineDiscipline {
|
|||||||
|
|
||||||
fn update_readable_state_deferred(&self) {
|
fn update_readable_state_deferred(&self) {
|
||||||
let buffer = self.read_buffer.lock_irq_disabled();
|
let buffer = self.read_buffer.lock_irq_disabled();
|
||||||
let pollee = self.pollee.clone();
|
|
||||||
// add/del events may sleep, so only construct parameters here.
|
// add/del events may sleep, so only construct parameters here.
|
||||||
if !buffer.is_empty() {
|
if !buffer.is_empty() {
|
||||||
self.work_item_para.lock_irq_disabled().pollee_type = Some(PolleeType::Add);
|
self.work_item_para.lock_irq_disabled().pollee_type = Some(PolleeType::Add);
|
||||||
@ -205,12 +207,10 @@ impl LineDiscipline {
|
|||||||
|
|
||||||
/// include all operations that may cause sleep, and processes by a work queue.
|
/// include all operations that may cause sleep, and processes by a work queue.
|
||||||
fn update_readable_state_after(&self) {
|
fn update_readable_state_after(&self) {
|
||||||
if let Some(signal) = self.work_item_para.lock_irq_disabled().kernel_signal.take() {
|
if let Some((send_signal, signal)) =
|
||||||
self.foreground
|
self.work_item_para.lock_irq_disabled().kernel_signal.take()
|
||||||
.lock()
|
{
|
||||||
.upgrade()
|
send_signal(signal);
|
||||||
.unwrap()
|
|
||||||
.kernel_signal(signal)
|
|
||||||
};
|
};
|
||||||
if let Some(pollee_type) = self.work_item_para.lock_irq_disabled().pollee_type.take() {
|
if let Some(pollee_type) = self.work_item_para.lock_irq_disabled().pollee_type.take() {
|
||||||
match pollee_type {
|
match pollee_type {
|
||||||
@ -243,42 +243,33 @@ impl LineDiscipline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// read all bytes buffered to dst, return the actual read length.
|
pub fn read<F: Fn() -> bool>(&self, buf: &mut [u8], belongs_to_foreground: F) -> Result<usize> {
|
||||||
pub fn read(&self, dst: &mut [u8]) -> Result<usize> {
|
|
||||||
let mut poller = None;
|
let mut poller = None;
|
||||||
loop {
|
loop {
|
||||||
let res = self.try_read(dst);
|
if !belongs_to_foreground() {
|
||||||
|
init_poller(&mut poller);
|
||||||
|
if self.poll(IoEvents::IN, poller.as_ref()).is_empty() {
|
||||||
|
poller.as_ref().unwrap().wait()?
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = self.try_read(buf);
|
||||||
match res {
|
match res {
|
||||||
Ok(read_len) => {
|
Ok(len) => return Ok(len),
|
||||||
return Ok(read_len);
|
Err(e) if e.error() != Errno::EAGAIN => return Err(e),
|
||||||
|
Err(_) => {
|
||||||
|
init_poller(&mut poller);
|
||||||
|
if self.poll(IoEvents::IN, poller.as_ref()).is_empty() {
|
||||||
|
poller.as_ref().unwrap().wait()?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
if e.error() != Errno::EAGAIN {
|
|
||||||
return Err(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for read event
|
/// read all bytes buffered to dst, return the actual read length.
|
||||||
let need_poller = if poller.is_none() {
|
fn try_read(&self, dst: &mut [u8]) -> Result<usize> {
|
||||||
poller = Some(Poller::new());
|
|
||||||
poller.as_ref()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let revents = self.pollee.poll(IoEvents::IN, need_poller);
|
|
||||||
if revents.is_empty() {
|
|
||||||
// FIXME: deal with ldisc read timeout
|
|
||||||
poller.as_ref().unwrap().wait()?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn try_read(&self, dst: &mut [u8]) -> Result<usize> {
|
|
||||||
if !self.current_can_read() {
|
|
||||||
return_errno!(Errno::EAGAIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
let (vmin, vtime) = {
|
let (vmin, vtime) = {
|
||||||
let termios = self.termios.lock_irq_disabled();
|
let termios = self.termios.lock_irq_disabled();
|
||||||
let vmin = *termios.get_special_char(CC_C_CHAR::VMIN);
|
let vmin = *termios.get_special_char(CC_C_CHAR::VMIN);
|
||||||
@ -326,7 +317,8 @@ impl LineDiscipline {
|
|||||||
if termios.is_canonical_mode() {
|
if termios.is_canonical_mode() {
|
||||||
// canonical mode, read until meet new line
|
// canonical mode, read until meet new line
|
||||||
if is_line_terminator(next_char, &termios) {
|
if is_line_terminator(next_char, &termios) {
|
||||||
if !should_not_be_read(next_char, &termios) {
|
// The eof should not be read
|
||||||
|
if !is_eof(next_char, &termios) {
|
||||||
*dst_i = next_char;
|
*dst_i = next_char;
|
||||||
read_len += 1;
|
read_len += 1;
|
||||||
}
|
}
|
||||||
@ -368,31 +360,6 @@ impl LineDiscipline {
|
|||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine whether current process can read the line discipline. If current belongs to the foreground process group.
|
|
||||||
/// or the foreground process group is None, returns true.
|
|
||||||
fn current_can_read(&self) -> bool {
|
|
||||||
let current = current!();
|
|
||||||
let Some(foreground) = self.foreground.lock_irq_disabled().upgrade() else {
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
foreground.contains_process(current.pid())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// set foreground process group
|
|
||||||
pub fn set_fg(&self, foreground: Weak<ProcessGroup>) {
|
|
||||||
*self.foreground.lock_irq_disabled() = foreground;
|
|
||||||
// Some background processes may be waiting on the wait queue, when set_fg, the background processes may be able to read.
|
|
||||||
self.update_readable_state();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// get foreground process group id
|
|
||||||
pub fn fg_pgid(&self) -> Option<Pgid> {
|
|
||||||
self.foreground
|
|
||||||
.lock_irq_disabled()
|
|
||||||
.upgrade()
|
|
||||||
.map(|foreground| foreground.pgid())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// whether there is buffered data
|
/// whether there is buffered data
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.read_buffer.lock_irq_disabled().len() == 0
|
self.read_buffer.lock_irq_disabled().len() == 0
|
||||||
@ -439,8 +406,7 @@ fn is_line_terminator(item: u8, termios: &KernelTermios) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The special char should not be read by reading process
|
fn is_eof(ch: u8, termios: &KernelTermios) -> bool {
|
||||||
fn should_not_be_read(ch: u8, termios: &KernelTermios) -> bool {
|
|
||||||
ch == *termios.get_special_char(CC_C_CHAR::VEOF)
|
ch == *termios.get_special_char(CC_C_CHAR::VEOF)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -461,13 +427,22 @@ fn get_printable_char(ctrl_char: u8) -> u8 {
|
|||||||
ctrl_char + b'A' - 1
|
ctrl_char + b'A' - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_poller(poller: &mut Option<Poller>) {
|
||||||
|
if poller.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
*poller = Some(Poller::new());
|
||||||
|
}
|
||||||
|
|
||||||
enum PolleeType {
|
enum PolleeType {
|
||||||
Add,
|
Add,
|
||||||
Del,
|
Del,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LineDisciplineWorkPara {
|
struct LineDisciplineWorkPara {
|
||||||
kernel_signal: Option<KernelSignal>,
|
#[allow(clippy::type_complexity)]
|
||||||
|
kernel_signal: Option<(Arc<dyn Fn(KernelSignal) + Send + Sync>, KernelSignal)>,
|
||||||
pollee_type: Option<PolleeType>,
|
pollee_type: Option<PolleeType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,23 +2,28 @@ use spin::Once;
|
|||||||
|
|
||||||
use self::driver::TtyDriver;
|
use self::driver::TtyDriver;
|
||||||
use self::line_discipline::LineDiscipline;
|
use self::line_discipline::LineDiscipline;
|
||||||
use super::*;
|
|
||||||
use crate::events::IoEvents;
|
use crate::events::IoEvents;
|
||||||
|
use crate::fs::device::{Device, DeviceId, DeviceType};
|
||||||
|
use crate::fs::inode_handle::FileIo;
|
||||||
use crate::fs::utils::IoctlCmd;
|
use crate::fs::utils::IoctlCmd;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
use crate::process::signal::signals::kernel::KernelSignal;
|
||||||
use crate::process::signal::Poller;
|
use crate::process::signal::Poller;
|
||||||
use crate::process::{process_table, ProcessGroup};
|
use crate::process::{JobControl, Process, Terminal};
|
||||||
use crate::util::{read_val_from_user, write_val_to_user};
|
use crate::util::{read_val_from_user, write_val_to_user};
|
||||||
|
|
||||||
|
mod device;
|
||||||
pub mod driver;
|
pub mod driver;
|
||||||
pub mod line_discipline;
|
pub mod line_discipline;
|
||||||
pub mod termio;
|
pub mod termio;
|
||||||
|
|
||||||
|
pub use device::TtyDevice;
|
||||||
|
|
||||||
static N_TTY: Once<Arc<Tty>> = Once::new();
|
static N_TTY: Once<Arc<Tty>> = Once::new();
|
||||||
|
|
||||||
pub(super) fn init() {
|
pub(super) fn init() {
|
||||||
let name = CString::new("console").unwrap();
|
let name = CString::new("console").unwrap();
|
||||||
let tty = Arc::new(Tty::new(name));
|
let tty = Tty::new(name);
|
||||||
N_TTY.call_once(|| tty);
|
N_TTY.call_once(|| tty);
|
||||||
driver::init();
|
driver::init();
|
||||||
}
|
}
|
||||||
@ -28,45 +33,49 @@ pub struct Tty {
|
|||||||
name: CString,
|
name: CString,
|
||||||
/// line discipline
|
/// line discipline
|
||||||
ldisc: Arc<LineDiscipline>,
|
ldisc: Arc<LineDiscipline>,
|
||||||
|
job_control: JobControl,
|
||||||
/// driver
|
/// driver
|
||||||
driver: SpinLock<Weak<TtyDriver>>,
|
driver: SpinLock<Weak<TtyDriver>>,
|
||||||
|
weak_self: Weak<Self>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tty {
|
impl Tty {
|
||||||
pub fn new(name: CString) -> Self {
|
pub fn new(name: CString) -> Arc<Self> {
|
||||||
Tty {
|
Arc::new_cyclic(|weak_ref| Tty {
|
||||||
name,
|
name,
|
||||||
ldisc: LineDiscipline::new(),
|
ldisc: LineDiscipline::new(),
|
||||||
|
job_control: JobControl::new(),
|
||||||
driver: SpinLock::new(Weak::new()),
|
driver: SpinLock::new(Weak::new()),
|
||||||
}
|
weak_self: weak_ref.clone(),
|
||||||
}
|
})
|
||||||
|
|
||||||
/// Set foreground process group
|
|
||||||
pub fn set_fg(&self, process_group: Weak<ProcessGroup>) {
|
|
||||||
self.ldisc.set_fg(process_group);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_driver(&self, driver: Weak<TtyDriver>) {
|
pub fn set_driver(&self, driver: Weak<TtyDriver>) {
|
||||||
*self.driver.lock_irq_disabled() = driver;
|
*self.driver.lock_irq_disabled() = driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive_char(&self, item: u8) {
|
pub fn receive_char(&self, ch: u8) {
|
||||||
self.ldisc.push_char(item, |content| print!("{}", content));
|
let may_send_signal = || {
|
||||||
|
let Some(foreground) = self.foreground() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let send_signal = move |signal: KernelSignal| {
|
||||||
|
foreground.kernel_signal(signal);
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Arc::new(send_signal) as Arc<dyn Fn(KernelSignal) + Send + Sync>)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.ldisc
|
||||||
|
.push_char(ch, may_send_signal, |content| print!("{}", content));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Device for Tty {
|
impl FileIo for Tty {
|
||||||
fn type_(&self) -> DeviceType {
|
|
||||||
DeviceType::CharDevice
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id(&self) -> DeviceId {
|
|
||||||
// Same value with Linux
|
|
||||||
DeviceId::new(5, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
fn read(&self, buf: &mut [u8]) -> Result<usize> {
|
||||||
self.ldisc.read(buf)
|
self.ldisc
|
||||||
|
.read(buf, || self.job_control.current_belongs_to_foreground())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write(&self, buf: &[u8]) -> Result<usize> {
|
fn write(&self, buf: &[u8]) -> Result<usize> {
|
||||||
@ -92,23 +101,28 @@ impl Device for Tty {
|
|||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
IoctlCmd::TIOCGPGRP => {
|
IoctlCmd::TIOCGPGRP => {
|
||||||
let Some(fg_pgid) = self.ldisc.fg_pgid() else {
|
let Some(foreground) = self.foreground() else {
|
||||||
return_errno_with_message!(Errno::ENOENT, "No fg process group")
|
return_errno_with_message!(Errno::ESRCH, "No fg process group")
|
||||||
};
|
};
|
||||||
|
let fg_pgid = foreground.pgid();
|
||||||
debug!("fg_pgid = {}", fg_pgid);
|
debug!("fg_pgid = {}", fg_pgid);
|
||||||
write_val_to_user(arg, &fg_pgid)?;
|
write_val_to_user(arg, &fg_pgid)?;
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
IoctlCmd::TIOCSPGRP => {
|
IoctlCmd::TIOCSPGRP => {
|
||||||
// Set the process group id of fg progress group
|
// Set the process group id of fg progress group
|
||||||
let pgid = read_val_from_user::<i32>(arg)?;
|
let pgid = {
|
||||||
|
let pgid: i32 = read_val_from_user(arg)?;
|
||||||
if pgid < 0 {
|
if pgid < 0 {
|
||||||
return_errno_with_message!(Errno::EINVAL, "invalid pgid");
|
return_errno_with_message!(Errno::EINVAL, "negative pgid");
|
||||||
}
|
|
||||||
match process_table::pgid_to_process_group(pgid as u32) {
|
|
||||||
None => self.ldisc.set_fg(Weak::new()),
|
|
||||||
Some(process_group) => self.ldisc.set_fg(Arc::downgrade(&process_group)),
|
|
||||||
}
|
}
|
||||||
|
pgid as u32
|
||||||
|
};
|
||||||
|
|
||||||
|
self.set_foreground(&pgid)?;
|
||||||
|
// Some background processes may be waiting on the wait queue,
|
||||||
|
// when set_fg, the background processes may be able to read.
|
||||||
|
self.ldisc.update_readable_state();
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
IoctlCmd::TCSETS => {
|
IoctlCmd::TCSETS => {
|
||||||
@ -143,12 +157,49 @@ impl Device for Tty {
|
|||||||
self.ldisc.set_window_size(winsize);
|
self.ldisc.set_window_size(winsize);
|
||||||
Ok(0)
|
Ok(0)
|
||||||
}
|
}
|
||||||
|
IoctlCmd::TIOCSCTTY => {
|
||||||
|
self.set_current_session()?;
|
||||||
|
Ok(0)
|
||||||
|
}
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FIXME: should we maintain a static console?
|
impl Terminal for Tty {
|
||||||
|
fn arc_self(&self) -> Arc<dyn Terminal> {
|
||||||
|
self.weak_self.upgrade().unwrap() as _
|
||||||
|
}
|
||||||
|
|
||||||
|
fn job_control(&self) -> &JobControl {
|
||||||
|
&self.job_control
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device for Tty {
|
||||||
|
fn type_(&self) -> DeviceType {
|
||||||
|
DeviceType::CharDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id(&self) -> DeviceId {
|
||||||
|
// The same value as /dev/console in linux.
|
||||||
|
DeviceId::new(88, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_n_tty() -> &'static Arc<Tty> {
|
pub fn get_n_tty() -> &'static Arc<Tty> {
|
||||||
N_TTY.get().unwrap()
|
N_TTY.get().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open `N_TTY` as the controlling terminal for the process. This method should
|
||||||
|
/// only be called when creating the init process.
|
||||||
|
pub fn open_ntty_as_controlling_terminal(process: &Process) -> Result<()> {
|
||||||
|
let tty = get_n_tty();
|
||||||
|
|
||||||
|
let session = &process.session().unwrap();
|
||||||
|
let process_group = process.process_group().unwrap();
|
||||||
|
tty.job_control.set_session(session);
|
||||||
|
tty.job_control.set_foreground(Some(&process_group))?;
|
||||||
|
|
||||||
|
session.set_terminal(|| Ok(tty.clone()))
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user