סביבת הרצה אינטראקטיבית ומוגבלת בזמן¶
נניח שכלי שורת פקודה עובר למצב raw כדי לקרוא מקש בודד, ואז מחזיר שגיאה לפני שהחזיר את המסוף למצב הרגיל. המשתמש נשאר עם מסוף שאינו מציג הקלדה או אינו מפרש Enter כפי שהוא מצפה. זאת בעיית חיים של משאב, בדומה למתאר או הקצאה, אבל כאן המשאב הוא מצב שמשותף גם לתוכניות אחרות שמשתמשות במסוף.
ה־TTY אינו רק קובץ בייטים. שכבת line discipline יכולה לאגור שורה, להציג echo, לתרגם תווים ולייצר אותות עבור קבוצת התהליכים שבחזית. במצב canonical הקריאה יכולה להמתין לשורה שלמה, ובמצב raw אנחנו מקבלים שליטה רחבה יותר אבל גם אחריות לפירוש הקלט ולשחזור המצב.
העטיפה הבאה שומרת את מצב המסוף ומשחזרת אותו בהשמדה רגילה. היא דורשת libc ומיועדת להרצה מתוך מסוף אמיתי בסביבת לינוקס.
use std::{fs::OpenOptions, io::{self, Read}, mem::MaybeUninit};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
struct RawMode<'a> { fd: BorrowedFd<'a>, original: libc::termios }
impl<'a> RawMode<'a> {
fn enter(fd: BorrowedFd<'a>) -> io::Result<Self> {
let mut storage = MaybeUninit::<libc::termios>::uninit();
if unsafe { libc::tcgetattr(fd.as_raw_fd(), storage.as_mut_ptr()) } == -1 {
return Err(io::Error::last_os_error());
}
let original = unsafe { storage.assume_init() };
let mut raw = original;
unsafe { libc::cfmakeraw(&mut raw); }
if unsafe { libc::tcsetattr(fd.as_raw_fd(), libc::TCSANOW, &raw) } == -1 {
return Err(io::Error::last_os_error());
}
Ok(Self { fd, original })
}
}
impl Drop for RawMode<'_> {
fn drop(&mut self) {
unsafe { libc::tcsetattr(self.fd.as_raw_fd(), libc::TCSANOW, &self.original); }
}
}
fn main() -> io::Result<()> {
let terminal = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
println!("press one key");
let mode = RawMode::enter(terminal.as_fd())?;
let mut byte = [0];
(&terminal).read_exact(&mut byte)?;
drop(mode);
println!("byte={}", byte[0]);
Ok(())
}
ה־guard מחזיק השאלה למתאר, ולכן הבעלים של המסוף צריך להישאר חי עד השחזור. פעולת ההשמדה אינה מחזירה שגיאה, ולכן הדוגמה אינה מבטיחה שהשחזור הצליח בכל מצב. במוצר נרצה גם מתודת restore מפורשת שמחזירה תוצאה, ונשתמש ב־Drop כרשת ניקוי נוספת במסלולים רגילים.
ההשמדה גם אינה מופעלת אחרי SIGKILL או קריסה שמסיימת את התהליך בלי unwind. לכן עטיפת RAII מצמצמת מסלולי כשל אבל אינה יכולה להבטיח שחזור מול כל דרך סיום. בניסוי כדאי להכיר את הפקודה stty sane, שאפשר להריץ מה־shell כדי להחזיר מצב מסוף שימושי כאשר נשאר מצב לא צפוי.
פסאודו־מסוף PTY מאפשר לתוכנית אחרת להחזיק את צד ה־master ולהציג לתהליך ילד צד slave שמתנהג כמסוף. זהו הבסיס להרבה אמולטורי מסוף וכלי אוטומציה. עצם חיבור stdin לצינור אינו יוצר TTY, ולכן תוכניות עשויות לבחור buffering והתנהגות אינטראקטיבית אחרת. כדי לבנות מפקח אינטראקטיבי נצטרך לנהל גם session, controlling terminal וקבוצת foreground.
כעת נוסיף את שאלת הזמן. שעון realtime יכול להשתנות עקב כיוון שעון, ולכן timeout של פעולה צריך בדרך כלל להסתמך על שעון מונוטוני. בראסט נשתמש ב־Instant למדידת משך, ונחשב deadline אחד במקום להתחיל timeout מלא מחדש בכל יקיצה.
הקוד הבא הוא תוכנית עצמאית שמדגימה חישוב זמן שנותר. ההמתנה כאן היא רק הדגמה, ובהמשך נחבר את אותו חישוב למנגנוני קלט ופלט.
use std::time::{Duration, Instant};
fn main() {
let deadline = Instant::now() + Duration::from_millis(250);
loop {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() { break; }
std::thread::sleep(remaining.min(Duration::from_millis(30)));
}
println!("deadline reached");
}
אם בכל יקיצה היינו ממתינים מחדש רבע שנייה, הפרעות חוזרות היו יכולות לדחות את הסיום בלי גבול. ה־deadline שומר את התקציב המקורי. בטיימרים מחזוריים צריך להחליט אם המחזור הבא נגזר מהתכנון המקורי או מזמן הסיום בפועל, משום שהבחירה השנייה צוברת סטייה.
המנגנון timerfd מאפשר לייצג פקיעת טיימר כמתאר קריא, כולל מידע על מספר פקיעות שלא נקראו עדיין. הוא מתאים ללולאת אירועים, אבל אינו מבטיח שהמשימה תתוזמן בדיוק ברגע הפקיעה. הפרידו בין זמן הפקיעה של הטיימר לבין זמן הטיפול של התהליך, במיוחד כאשר מודדים latency.
הפרטים של מצב המסוף מתועדים בעמוד termios, והטיימר המתואר בעמוד timerfd_create. שני המנגנונים מחייבים חוזה סיום מפורש: מי משחזר את המצב ומי מחליט שהזמן נגמר.