Spec couche 0 — Famille terminal (termios/tty)
Spécification technique — Version 1.0
Vue d’ensemble de la famille
La famille terminal porte l’interface POSIX termios — la discipline de
ligne des terminaux (POSIX.1-1988, racines System V), terminal-agnostique et
stable. Elle est introduite par le descellement additif de la couche 0
(ADR-060) pour
débloquer la libc (tcgetattr/tcsetattr/PTY pour OpenSSH, isatty pour
stdio) et l’objet ergonomique de couche 1
air-terminal.
Découpage en tranches.
- Tranche 1 — le TYPE (ce document). [
Termios] et ses types associés vivent dansair-sys-types/src/terminal.rs. C’est un objet ABI pur, entièrement testable sur l’hôte (aucun syscall) : structure#[repr(C)], accesseurs typés, encodage/décodage des drapeaux et de la vitesse. Couverture 100 % lignes + branches. - Tranche 2 — les WRAPPERS « Attributs ». [
tcgetattr]/[tcsetattr] (cœur termios) arrivent dansair-sys-syscall/src/terminal.rset consomment le type décrit ici. Documentées ci-dessous (section « Wrappers — Attributs (tranche 2) »). Couverture 100 % lignes + branches. - Tranche 3 — les WRAPPERS « Contrôle de ligne » et « Taille fenêtre ».
Les
tc*POSIX restants ([tcdrain]/[tcflush]/[tcflow]/[tcsendbreak]) et les ioctls de dimension de fenêtre ([get_winsize]/[set_winsize], avec le type [Winsize]) arrivent dansair-sys-syscall/src/terminal.rs. Documentés ci-dessous (sections « Wrappers — Contrôle de ligne (tranche 3) » et « Wrappers — Taille fenêtre (tranche 3) »). Couverture 100 % lignes + branches. - Tranche 4 — les WRAPPERS « PTY ». L’allocation d’une paire
maître/esclave de pseudo-terminal ([
open_pty_master], [pty_number] avec le type [PtyNumber], [pty_unlock], [pty_peer]) arrive dansair-sys-syscall/src/terminal.rs. Documentée ci-dessous (section « Wrappers — PTY (tranche 4) »). Couverture 100 % lignes + branches. - Tranche 5 — les WRAPPERS « Session / job-control ». Le terminal de
contrôle et les groupes de premier plan
(
tcgetpgrp/tcsetpgrp/session_id/set_ctty/clear_ctty). Documentés ci-dessous (section « Wrappers — Session / job-control (tranche 5) »). Couverture 100 % hors exceptions CHILD-EXIT documentées et justifiées (ADR-035 : cycle nominal validé dans un enfant forkésetsid).
Caractéristiques transverses.
- Image ABI exacte du kernel. [
Termios] reproduitstruct termiosde<asm-generic/termbits.h>, identique sur x86_64 et aarch64 (tous deuxasm-generic). Aucune divergence de disposition. - Doctrine « kernel = bible ». Les champs bruts (
c_iflag,c_oflag,c_cflag,c_lflag) sont conservés tels quels ; les bits inconnus (d’un kernel plus récent) sont retenus, jamais silencieusement effacés. - Zéro allocation. Le type est
Copy, sur la pile, sans indirection ni heap — compatibleno_stdfutur. - Accès typé en lecture, champs bruts en écriture directe possible. Les
accesseurs retournent des drapeaux/enum typés ; les champs restent
pubpour l’échange direct avec le kernel via les ioctls (couche syscall).
Sous-section 1 : la structure Termios
#![allow(unused)]
fn main() {
pub const NCCS: usize = 19;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct Termios {
pub c_iflag: u32, // drapeaux d'entrée (brut)
pub c_oflag: u32, // drapeaux de sortie (brut)
pub c_cflag: u32, // drapeaux de contrôle (brut, inclut la vitesse CBAUD)
pub c_lflag: u32, // drapeaux locaux (brut)
pub c_line: u8, // discipline de ligne
pub c_cc: [u8; NCCS], // caractères de contrôle
}
}
Disposition. #[repr(C)] reproduit struct termios du kernel ; la même
disposition vaut sur x86_64 et aarch64 (asm-generic). NCCS vaut 19 : le
tableau c_cc a 19 slots, dont seuls les indices 0..=16 sont nommés (cf.
[ControlChar]) — les slots 17 et 18 sont réservés (padding kernel, sans nom
POSIX) et conservés à zéro.
Constructeur.
#![allow(unused)]
fn main() {
pub const fn zeroed() -> Termios;
}
Retourne l’image toute-à-zéro, à passer à un TCGETS qui la remplira. Sur cette
image : drapeaux vides, taille de caractère Bits5 (motif CSIZE = 0), vitesse
B0.
Accesseurs de drapeaux (lecture typée).
#![allow(unused)]
fn main() {
pub const fn input_flags(&self) -> InputFlags;
pub const fn output_flags(&self) -> OutputFlags;
pub const fn control_flags(&self) -> ControlFlags;
pub const fn local_flags(&self) -> LocalFlags;
}
Chacun décode le champ brut correspondant via from_bits_retain : les bits
inconnus sont conservés (doctrine kernel = bible). control_flags() expose le
c_cflag complet, y compris les bits CSIZE/CBAUD — la taille et la vitesse
se lisent via leurs accesseurs dédiés (ci-dessous), pas via ControlFlags.
Mutateurs de drapeaux (écriture typée).
#![allow(unused)]
fn main() {
pub const fn set_input_flags(&mut self, flags: InputFlags);
pub const fn set_output_flags(&mut self, flags: OutputFlags);
pub const fn set_control_flags(&mut self, flags: ControlFlags);
pub const fn set_local_flags(&mut self, flags: LocalFlags);
}
set_input_flags/set_output_flags/set_local_flags remplacent
intégralement le champ brut. set_control_flags est spécial : il préserve les
bits CSIZE et CBAUD de c_cflag (taille de caractère et vitesse), de sorte
qu’écrire les drapeaux de contrôle ne détruise ni la taille ni la vitesse déjà
posées. ControlFlags ne recouvre ni CSIZE ni CBAUD : pas de chevauchement.
Taille de caractère (CSIZE).
#![allow(unused)]
fn main() {
pub const fn char_size(&self) -> CharSize;
pub const fn set_char_size(&mut self, size: CharSize);
}
Lecture/écriture des 2 bits CSIZE de c_cflag, indépendamment de la vitesse et
des autres drapeaux de contrôle.
Vitesse (CBAUD/CBAUDEX).
#![allow(unused)]
fn main() {
pub const fn output_speed(&self) -> BaudRate;
pub const fn input_speed(&self) -> BaudRate; // = output_speed (termios classique Linux)
pub const fn set_speed(&mut self, baud: BaudRate);
}
En termios classique Linux, entrée et sortie partagent le champ CBAUD de
c_cflag : input_speed() et output_speed() renvoient donc la même valeur.
set_speed ne touche que les bits CBAUD|CBAUDEX, indépendamment de la taille.
Caractères de contrôle (c_cc).
#![allow(unused)]
fn main() {
pub const fn control_char(&self, which: ControlChar) -> u8;
pub const fn set_control_char(&mut self, which: ControlChar, value: u8);
}
Indexation typée du tableau c_cc par [ControlChar] — jamais d’entier brut en
indice.
Sous-section 2 : les types associés
ControlChar — indices des caractères de contrôle
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum ControlChar {
Intr = 0, Quit = 1, Erase = 2, Kill = 3, Eof = 4, Time = 5, Min = 6,
Swtc = 7, Start = 8, Stop = 9, Susp = 10, Eol = 11, Reprint = 12,
Discard = 13, Werase = 14, Lnext = 15, Eol2 = 16,
}
}
Chaque variante est l’indice POSIX (V* de <asm-generic/termbits.h>) dans
c_cc. 17 caractères nommés (0..=16) pour 19 slots (NCCS) : les slots 17/18
sont réservés.
Drapeaux — quatre bitflags typés
Quatre types bitflags exposent les bits des quatre champs de drapeaux. Chacun
dérive from_bits_retain (conserve les bits inconnus) et bits() (round-trip
vers l’entier brut).
- [
InputFlags] (c_iflag) :IGNBRK,BRKINT,IGNPAR,PARMRK,INPCK,ISTRIP,INLCR,IGNCR,ICRNL,IUCLC,IXON,IXANY,IXOFF,IMAXBEL,IUTF8. - [
OutputFlags] (c_oflag) :OPOST,OLCUC,ONLCR,OCRNL,ONOCR,ONLRET,OFILL,OFDEL. - [
ControlFlags] (c_cflag, hors taille/vitesse) :CSTOPB,CREAD,PARENB,PARODD,HUPCL,CLOCAL,CRTSCTS. - [
LocalFlags] (c_lflag) :ISIG,ICANON,ECHO,ECHOE,ECHOK,ECHONL,NOFLSH,TOSTOP,ECHOCTL,ECHOPRT,ECHOKE,FLUSHO,PENDIN,IEXTEN.
CharSize — nombre de bits de données (CSIZE)
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum CharSize { Bits5 = 0x00, Bits6 = 0x10, Bits7 = 0x20, Bits8 = 0x30 }
}
Le masque CSIZE (0x30) n’a que 4 motifs : le décodage est total
(jamais None) ; le motif 0x30 est Bits8.
BaudRate — vitesse de transmission (CBAUD/CBAUDEX)
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BaudRate(u32);
pub const fn from_code(code: u32) -> BaudRate; // masque par CBAUD (0x100f)
pub const fn code(self) -> u32; // réinjection dans c_cflag
}
Newtype sur le code de vitesse encodé (CBAUD|CBAUDEX). Constantes POSIX
standard exposées : B0, B50, …, B38400 (0x0..0xf), puis les vitesses
étendues B57600, B115200, B230400, B460800, B921600 (0x1001..).
from_code/code forment un round-trip qui préserve l’octet de vitesse ;
une valeur kernel inconnue est rendue telle quelle (bits hors CBAUD_MASK
ignorés, le reste conservé) — pas de perte d’information sur la vitesse.
SetAction, FlushQueue, FlowAction — arguments des wrappers à venir
Ces trois enums typent les arguments des ioctls des tranches suivantes
(air-sys-syscall). Ils remplacent les constantes magiques du kernel par des
variantes explicites (ADR-021 conv. 3).
#![allow(unused)]
fn main() {
pub enum SetAction { Now, Drain, Flush } // tcsetattr : quand appliquer (TCSANOW/TCSADRAIN/TCSAFLUSH)
pub enum FlushQueue { Input, Output, Both } // tcflush : quelle file purger (TCIFLUSH/TCOFLUSH/TCIOFLUSH)
pub enum FlowAction { SuspendOutput, ResumeOutput, // tcflow : contrôle de flux (TCOOFF/TCOON/
SuspendInput, ResumeInput } // TCIOFF/TCION)
}
SetAction type l’argument de [tcgetattr]/[tcsetattr] (tranche 2) ;
FlushQueue et FlowAction typent respectivement [tcflush] et [tcflow]
(tranche 3, ci-dessous).
Winsize — dimensions de la fenêtre du terminal
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct Winsize {
pub ws_row: u16, // nombre de lignes (rangées)
pub ws_col: u16, // nombre de colonnes
pub ws_xpixel: u16, // largeur en pixels (souvent 0)
pub ws_ypixel: u16, // hauteur en pixels (souvent 0)
}
pub const fn zeroed() -> Winsize;
pub const fn rows(&self) -> u16; // = ws_row
pub const fn cols(&self) -> u16; // = ws_col
pub const fn x_pixels(&self) -> u16; // = ws_xpixel
pub const fn y_pixels(&self) -> u16; // = ws_ypixel
}
Image ABI de struct winsize (<asm-generic/termios.h>), #[repr(C)],
identique x86_64/aarch64. Champs pub bruts (échange direct avec le kernel,
comme [Termios]) plus des accesseurs const nommés pour la lecture. zeroed()
donne l’image nulle à passer à un TIOCGWINSZ. Les champs pixels sont souvent à
0 (nombre de terminaux ne les renseignent pas). Le kernel signale un changement
de taille par SIGWINCH. Échangée par [get_winsize]/[set_winsize] (tranche 3).
Wrappers — Attributs (tranche 2)
Le module air-sys-syscall/src/terminal.rs livre les deux ioctls typés du
cœur termios (ADR-060,
groupe « Attributs »). Ce sont des fonctions dédiées typées (ADR-021 conv. 3 :
jamais d’ioctl(fd, REQ, …) générique) ; zéro allocation (le [Termios] vit sur
la pile) ; EINTR est remonté tel quel à l’appelant, sans retry automatique
(ADR-021 conv. 2).
#![allow(unused)]
fn main() {
pub fn tcgetattr(fd: BorrowedFd<'_>) -> Result<Termios, Errno>;
pub fn tcsetattr(fd: BorrowedFd<'_>, action: SetAction, attributes: &Termios)
-> Result<(), Errno>;
}
tcgetattr (TCGETS). Alloue un [Termios::zeroed] sur la pile, le fait
remplir par le kernel via l’ioctl TCGETS (0x5401), puis le renvoie. Le retour
est normalisé (comme musl, cf. audit §7) : ioctl < 0 → Err(errno) ; sinon
Ok(termios) — jamais une valeur ioctl arbitraire propagée.
tcsetattr (TCSETS/TCSETSW/TCSETSF). Applique attributes au terminal
à l’instant indiqué par action. Le mapping action → requête se fait par un
match exhaustif, jamais par l’arithmétique TCSETS + action (fragilité
pointée par l’audit musl §8, dépendante de la contiguïté des constantes) :
[SetAction] | Requête ioctl | Valeur | Sémantique POSIX |
|---|---|---|---|
Now | TCSETS | 0x5402 | TCSANOW — applique immédiatement |
Drain | TCSETSW | 0x5403 | TCSADRAIN — après vidage de la sortie |
Flush | TCSETSF | 0x5404 | TCSAFLUSH — après vidage sortie + purge entrée |
Les constantes TC* proviennent de <asm-generic/ioctls.h> et sont identiques
sur x86_64 et aarch64. Le module porte son propre helper de syscall ioctl
bas niveau (inline asm syscall/svc 0, SYS_ioctl = 16/29) et son propre
helper de décodage d’errno, sur le modèle de device.rs.
Erreurs. [Errno::ENOTTY] (fd n’est pas un terminal), [Errno::EBADF]
(descripteur invalide), [Errno::EINTR] (interrompu, remonté tel quel). Une
erreur EFAULT est exclue par construction : le Termios échangé est
toujours une variable de pile vivante et alignée.
Terminal de test. Le happy path exige un vrai terminal : les tests ouvrent le
maître du PTY via /dev/ptmx (O_RDWR), qui répond aux ioctls TCGETS/
TCSETS* sur Linux sans privilège (l’allocation PTY complète —
TIOCGPTN/TIOCSPTLCK — relève d’une tranche ultérieure). Le round-trip bascule
un drapeau (ECHO) et vérifie qu’il est bien appliqué ; les trois variantes de
[SetAction] sont chacune exercées. Les chemins d’erreur (Err) s’exercent sur
un fd non-tty (/dev/null → ENOTTY). Couverture 100 % lignes + branches,
sans exception ADR-035.
Wrappers — Contrôle de ligne (tranche 3)
Le module air-sys-syscall/src/terminal.rs livre les quatre tc* POSIX restants
(ADR-060, groupe
« Contrôle de ligne »). Mêmes conventions que la tranche 2 : fonctions dédiées
typées (ADR-021 conv. 3), zéro allocation, EINTR remonté tel quel sans retry.
#![allow(unused)]
fn main() {
pub fn tcdrain(fd: BorrowedFd<'_>) -> Result<(), Errno>;
pub fn tcflush(fd: BorrowedFd<'_>, queue: FlushQueue) -> Result<(), Errno>;
pub fn tcflow(fd: BorrowedFd<'_>, action: FlowAction) -> Result<(), Errno>;
pub fn tcsendbreak(fd: BorrowedFd<'_>) -> Result<(), Errno>;
}
tcdrain (TCSBRK, argument 1). Bloque jusqu’à la transmission de tous les
octets déjà écrits vers fd. Sur Linux, ioctl(fd, TCSBRK, arg) avec un arg
non nul draine la sortie (sémantique tcdrain, cf. musl) ; l’envoi de break
correspond à arg == 0 ([tcsendbreak]). C’est le point d’annulation de la
famille (audit musl §3) : elle peut rendre [Errno::EINTR], remonté tel quel,
aucun retry automatique (ADR-021 conv. 2 — l’appelant qui veut retenter écrit la
boucle).
tcflush (TCFLSH). Jette les octets non traités de la (des) file(s) visée(s).
Le mapping [FlushQueue] → argument entier est un match exhaustif :
[FlushQueue] | Argument | Valeur | Sémantique POSIX |
|---|---|---|---|
Input | TCIFLUSH | 0 | purge les données reçues non lues |
Output | TCOFLUSH | 1 | purge les données écrites non transmises |
Both | TCIOFLUSH | 2 | purge les deux files |
tcflow (TCXONC). Suspend/reprend le flux d’entrée ou de sortie. Le mapping
[FlowAction] → argument entier est un match exhaustif :
[FlowAction] | Argument | Valeur | Sémantique POSIX |
|---|---|---|---|
SuspendOutput | TCOOFF | 0 | suspend la sortie |
ResumeOutput | TCOON | 1 | reprend la sortie |
SuspendInput | TCIOFF | 2 | émet un STOP (suspend l’entrée distante) |
ResumeInput | TCION | 3 | émet un START (reprend l’entrée distante) |
tcsendbreak (TCSBRK, argument 0). Transmet une condition BREAK. Durée
non paramétrée (honnêteté de contrat). POSIX prévoit un paramètre de durée, mais
il est implementation-defined et musl l’ignore silencieusement (audit musl
§6/§8). Plutôt qu’exposer un paramètre avalé sans effet, Air n’en expose aucun
— le break dure la durée par défaut du kernel, et le contrat dit exactement ce
qu’il fait (doctrine « la libc fait ce qu’elle dit »). Le partage du code d’ioctl
TCSBRK avec [tcdrain] est explicite : deux constantes d’argument nommées
(0 = break, 1 = drain) distinguent les sémantiques.
Wrappers — Taille fenêtre (tranche 3)
Deux ioctls typés échangent la [Winsize] du terminal
(ADR-060, groupe
« Taille fenêtre »).
#![allow(unused)]
fn main() {
pub fn get_winsize(fd: BorrowedFd<'_>) -> Result<Winsize, Errno>;
pub fn set_winsize(fd: BorrowedFd<'_>, size: &Winsize) -> Result<(), Errno>;
}
get_winsize (TIOCGWINSZ, 0x5413). Alloue un [Winsize::zeroed] sur la
pile, le fait remplir par le kernel, puis le renvoie (retour normalisé comme
[tcgetattr]). set_winsize (TIOCSWINSZ, 0x5414). Impose size à la
fenêtre ; le kernel émet alors SIGWINCH vers le groupe de premier plan. Les deux
passent un pointeur vers la struct winsize (comme les ioctls termios).
Erreurs (contrôle de ligne + taille fenêtre). [Errno::ENOTTY] (fd n’est pas
un terminal), [Errno::EBADF] (descripteur invalide), et [Errno::EINTR] pour la
seule [tcdrain] (opération bloquante). EFAULT est exclue par construction
pour get_winsize/set_winsize : la Winsize échangée est toujours une variable
de pile vivante et alignée.
Terminal de test. Comme en tranche 2, le happy path s’exerce sur le maître du
PTY via /dev/ptmx (sans privilège) : tcdrain/tcsendbreak réussissent
immédiatement, les trois [FlushQueue] et les quatre [FlowAction] exercent tous
les bras des match, et un round-trip set_winsize(40×120) → get_winsize
vérifie les valeurs relues. Les chemins d’erreur (Err) de chaque fonction
s’exercent sur un fd non-tty (/dev/null → ENOTTY). Couverture 100 % lignes +
branches, sans exception
ADR-035.
Wrappers — PTY (tranche 4)
Quatre wrappers allouent une paire maître/esclave de pseudo-terminal (ADR-060, groupe « PTY ») et un newtype d’identifiant nomme l’esclave.
#![allow(unused)]
fn main() {
pub struct PtyNumber(u32); // N de /dev/pts/N (rendu par TIOCGPTN)
impl PtyNumber {
pub const fn new(value: u32) -> Self;
pub const fn value(self) -> u32;
}
pub fn open_pty_master() -> Result<OwnedFd, Errno>;
pub fn pty_number(master: BorrowedFd<'_>) -> Result<PtyNumber, Errno>;
pub fn pty_unlock(master: BorrowedFd<'_>) -> Result<(), Errno>;
pub fn pty_peer(master: BorrowedFd<'_>) -> Result<OwnedFd, Errno>;
}
PtyNumber. Newtype d’identifiant (ADR-021 : jamais un u32 brut pour
désigner un esclave, comme [Pid]/[Tid]/[PidFd]). Il porte l’N du nœud
/dev/pts/N.
open_pty_master. Ouvre un nouveau maître via /dev/ptmx en
O_RDWR | O_NOCTTY (chaque ouverture alloue une paire distincte). Dogfoode
openat — qui ajoute O_CLOEXEC — plutôt qu’un
open brut. Le maître est rendu en [OwnedFd] (RAII : sa fermeture libère la
paire). O_NOCTTY empêche le maître de devenir le terminal contrôlant.
pty_number (TIOCGPTN, 0x8004_5430). Le kernel écrit un unsigned int (u32) sur la pile ; renvoyé en [PtyNumber].
pty_unlock (TIOCSPTLCK, 0x4004_5431). Le kernel lit un int sur la
pile ; on lui présente un 0 qui déverrouille l’esclave (prérequis avant son
ouverture, équivalent d’unlockpt). Le verrouillage (1) n’est pas exposé —
seul le déverrouillage est utile.
pty_peer (TIOCGPTPEER, 0x5441). Ouvre directement l’esclave du
maître et renvoie son fd (en [OwnedFd]). L’argument ioctl est une valeur
entière de drapeaux d’ouverture (O_RDWR | O_NOCTTY | O_CLOEXEC,
O_CLOEXEC demandé explicitement car cet ioctl ne l’ajoute pas) ; la valeur de
retour de l’ioctl est le nouveau descripteur. Cette voie moderne (Linux 4.13+)
est préférée au chemin /dev/pts/N : elle évite le TOCTOU (l’esclave ne
peut être substitué entre la résolution du nom et l’ouverture) et ne dépend pas du
montage de devpts.
Encodage des requêtes. Contrairement aux TC* numérotées en clair, ces trois
ioctls suivent le schéma d’encodage _IOC arch-générique de
<asm-generic/ioctls.h> (_IOR/_IOW/_IO), identique x86_64/aarch64. La
valeur de retour >= 0 de TIOCGPTPEER est toujours un fd dans l’intervalle
i32 (un descripteur est < 2^31) : la conversion as i32 est exacte et locale
(commentée), sans try_from dont le bras Err serait inatteignable.
Flux d’allocation. open_pty_master → pty_number (identifier l’esclave, si
besoin) → pty_unlock (déverrouiller) → pty_peer (ouvrir l’esclave).
Erreurs. [Errno::ENOTTY] (le fd n’est pas un maître de PTY, ex. /dev/null),
[Errno::EBADF] (descripteur invalide), [Errno::EINVAL] (kernel < 4.13 pour
pty_peer), [Errno::ENOSPC] (plus de PTY libre pour open_pty_master). Aucun
EFAULT par construction : TIOCGPTN/TIOCSPTLCK échangent une variable de pile
vivante et alignée ; TIOCGPTPEER ne prend qu’un entier.
Terminal de test. Le happy path exerce le cycle complet sans privilège :
open_pty_master → pty_number (valeur lisible) → pty_unlock → pty_peer,
puis tcgetattr(slave) prouve que l’esclave est un vrai terminal, et
master != slave prouve la paire. Les chemins d’erreur (Err) de pty_number,
pty_unlock et pty_peer s’exercent sur un fd non-tty (/dev/null → ENOTTY).
Couverture 100 % lignes + branches, sans exception
ADR-035.
Wrappers — Session / job-control (tranche 5)
Cinq wrappers pilotent le terminal de contrôle et les groupes de premier plan (ADR-060, groupe « Session / job-control »).
#![allow(unused)]
fn main() {
pub fn tcgetpgrp(fd: BorrowedFd<'_>) -> Result<Option<Pid>, Errno>;
pub fn tcsetpgrp(fd: BorrowedFd<'_>, process_group: Pid) -> Result<(), Errno>;
pub fn session_id(fd: BorrowedFd<'_>) -> Result<Pid, Errno>;
pub fn set_ctty(fd: BorrowedFd<'_>) -> Result<(), Errno>;
pub fn clear_ctty(fd: BorrowedFd<'_>) -> Result<(), Errno>;
}
tcgetpgrp (TIOCGPGRP, 0x540F). Le kernel écrit un pid_t (i32) sur
la pile : le groupe de premier plan du terminal. Un pid_t 0 signifie
« aucun groupe de premier plan » → converti en [None] (sentinelle → Option,
ADR-021 conv. 1). La conversion passe par [Pid::try_from_raw], qui replie tout
pid_t non strictement positif (donc 0 comme un négatif aberrant que ce
ioctl ne peut pas produire) sur None — défense en profondeur (Principe 5).
tcsetpgrp (TIOCSPGRP, 0x5410). Le kernel lit un pid_t (i32) sur la
pile : le groupe de premier plan à installer. process_group est un [Pid] typé
(non nul), donc aucune sentinelle 0 n’est jamais présentée au kernel.
session_id (TIOCGSID, 0x5429). Le kernel écrit le pid_t du leader
de session du terminal, toujours strictement positif ; la conversion via
[Pid::try_from_raw] est dénotée par .expect (motif de getsid) — un pid_t ≤ 0
serait une violation du contrat kernel, inatteignable par construction.
set_ctty (TIOCSCTTY, 0x540E, argument 0). Fait de fd le terminal de
contrôle du leader de session appelant. L’argument 0 demande une
acquisition sans vol : le vol (arg = 1) exige CAP_SYS_ADMIN et n’est
pas exposé (hors périmètre — listé en UNSUPPORTED.md à la tranche 6).
clear_ctty (TIOCNOTTY, 0x5422). Détache le terminal de contrôle du
processus appelant. Appelé par le leader de session, le kernel envoie
SIGHUP+SIGCONT au groupe de premier plan (à prendre en compte par l’appelant).
Encodage des requêtes. Ces cinq ioctls sont numérotés en clair dans
<asm-generic/ioctls.h> (comme les TC*), identiques x86_64/aarch64.
Erreurs. [Errno::ENOTTY] (le fd n’est pas le terminal de contrôle de
l’appelant, ou pas un terminal du tout : /dev/null, ou pty non-contrôlant),
[Errno::EPERM] (set_ctty par un non-leader, ou tcsetpgrp d’un groupe hors
session), [Errno::EBADF] (descripteur invalide). Aucun EFAULT par
construction : TIOCGPGRP/TIOCGSID écrivent une variable de pile vivante et
alignée, TIOCSPGRP en lit une, TIOCSCTTY/TIOCNOTTY ne prennent qu’un entier.
Terminal de test — validation on-target (enfant forké setsid). Le chemin
nominal de ces ioctls exige un terminal de contrôle, que le kernel
n’accorde qu’à un leader de session. Les chemins d’erreur (Err) sont donc
couverts dans le parent sur un fd non-tty (/dev/null → ENOTTY), et le
cycle nominal complet est validé dans un enfant forké setsid (helper
fork_run, motif de process.rs) : setsid → set_ctty(slave) →
session_id(slave) == SID → tcsetpgrp(slave, SID) → tcgetpgrp(slave) == Some(SID) → clear_ctty(slave) (après avoir bloqué SIGHUP). L’enfant encode
chaque étape dans son code de sortie (0 = tout le cycle a réussi). Le bras
Ok(None) de tcgetpgrp est, lui, couvert dans le parent (un maître
/dev/ptmx neuf n’a aucun groupe de premier plan → TIOCGPGRP rend 0). Les
lignes du bras Ok de tcsetpgrp/session_id/set_ctty/clear_ctty, exécutées
uniquement dans l’enfant, sont des exceptions CHILD-EXIT
(ADR-035 ; profil LLVM
de l’enfant non flushé, async-signal-safe — cf. docs/COVERAGE-EXCEPTIONS.md),
prouvées par le code de sortie.
Périmètre hors tranches 1 à 5
Les cinq groupes d’ioctl de la famille sont désormais livrés en air-sys-syscall
(« Attributs » — tcgetattr/tcsetattr — en tranche 2 ; « Contrôle de ligne » et
« Taille fenêtre » en tranche 3 ; PTY —
open_pty_master/pty_number/pty_unlock/pty_peer — en tranche 4 ; Session
/ job-control — tcgetpgrp/tcsetpgrp/session_id/set_ctty/clear_ctty —
en tranche 5, documentés ci-dessus ; cf.
ADR-060). Restent
hors périmètre : le vol de terminal de contrôle (TIOCSCTTY arg = 1,
exige CAP_SYS_ADMIN) et les ioctls tty legacy/dangereux (TIOCSTI,
TCGETA/TCSETA pré-POSIX) — à consigner dans UNSUPPORTED.md. isatty et
ttyname restent en couche 1 (dérivés), de même que l’objet ergonomique
air-terminal (ADR-061).