One seat: what pupitre-seat does¶
/usr/lib/pupitre/pupitre-seat <N> runs one seat, that is one Dell Wyse 1010 client, as one
X session. It is the ExecStart of pupitre-seat@N.service, which the manager starts when the
client mapped to seat N broadcasts. It is scoped by seat number so that a dozen
copies coexist on one host, and it never kills a process by name: only PIDs it started itself.
Per seat paths¶
| What | Where |
|---|---|
| Environment from the manager | /run/pupitre/seat-N.env |
| X display | :(100+N), seat 3 is :103 |
| Runtime dir | /run/pupitre/seat-N/ |
| Named pipes | /run/pupitre/seat-N/{video,input,audio} |
| Xvfb framebuffer | /run/pupitre/seat-N/fb/Xvfb_screen0 |
XDG_RUNTIME_DIR |
/run/pupitre/seat-N/xdg |
| User | pupitre-seat-N, created on first use; the desktop, its applications, PulseAudio and the key's mount run as this user |
| X cookie | /run/pupitre/seat-N/xauthority, owned by the seat's user, passed to Xvfb with -auth |
HOME |
/var/lib/pupitre/home/seat-N (created on demand, owned by the seat's user) |
| Logs | /var/log/pupitre/seat-N/{session,xvfb,desktop,input,encoder}.log, plus pulseaudio.log, curseur.log, cle.log, cle-fuse.log and lightdm.log depending on the features in use |
| fast_tcp2 log | /var/log/pupitre/seat-N/fast_tcp2_<date>_<client IP>.log, 0600 (it carries input reports), the 10 most recent kept, path printed in session.log |
Step by step¶
- Environment. Reads
/run/pupitre/seat-N.envunless systemd already loaded it (EnvironmentFile=).PUPITRE_MAC,PUPITRE_SERVER_IPandPUPITRE_IFACEare required; the launcher exits 1 with a clear message otherwise. - Directories and pipes. Creates the runtime dir, the log dir and the home, wipes the
framebuffer dir, recreates the
videoandinputpipes. WithPUPITRE_AUDIO=1it also creates theaudiopipe and exportsWYSE_AUDIO, then starts the seat's PulseAudio (null sinkwyse) and theparecbridge into the pipe (on by default, see the install guide). - Xvfb on the seat display,
1280x1024x24,-fbdirso the encoder can mmap the framebuffer,-nolisten tcp. Waits for the X socket instead of a fixed sleep. - Appearance.
pupitre-apparence <display> <config home>writes the four xfconf channels the session is about to read: the PrimTux 9 look, and above all a flat backdrop, Xfce's gradient wallpaper costing 6.4 times its RLE on every full screen. Optional and degrading on its own,PUPITRE_APPARENCE=0skips it. Seedocs/apparence.md. - Desktop.
dbus-run-session -- xfce4-sessionwithDISPLAY,HOME,XDG_RUNTIME_DIRand the XDG config, cache and data dirs pointing into the seat's home. - Input injector.
input_inject.py --tube <input pipe> --display :D. It injects key codes and pointer events through XTEST with python3-xlib, and blocks on the pipe until fast_tcp2 opens it for writing. - Protocol environment. Exports the
WYSE_*variables that fast_tcp2 reads (it inherits them through the session process). Each group has a comment in the script saying why; the two that matter most areWYSE_EXTRA_TUNNELS=1(the keyboard and mouse tunnels) andWYSE_POLL_ALL=1(without it not one input report comes back). - Session process with retry. Runs
python3 -m pupitre.main --ip <server ip> --iface <iface> --mac <mac>(UDP discovery for that MAC only, then fast_tcp2 and the video handshake). One handshake in four fails on the device side, so up to five attempts, each judged ready by two conditions read from the fast_tcp2 log withawk(the log contains NUL bytes,grepwould treat it as binary): theflux vivantline, meaning the client reached its frame loop, and awin=value of at least 2048 and below the default window, meaning the device armed a useful 4321 window. A failed attempt gets fast_tcp2 SIGTERMed first, then the session, then a cooldown: under about 20 s the device does not answer the next attempt at all. The cooldown starts at 25 s and DOUBLES after each degraded attempt, up to 300 s, and is not slept after the last one. - Encoder. Once ready,
encoder.py --sans-xvfb --tube <video pipe> --fbdir <fb> --fps 8 --jobs 1 --double 3 --duree 0(0means no time limit) turns dirty stripes of the framebuffer into RLE stripes on the video pipe.--doublematchesWYSE_HEAD_FLIP=1: double buffering with the flip in each stripe head, which is what removes tearing. Mode 3, the default (PUPITRE_DOUBLE), sends each buffer only what it lacks; mode 1 sends every changed zone to both, twice the bytes. - Watch. Every 2 s checks that the session process, the encoder and Xvfb are alive, and
exits 1 as soon as one is gone. systemd restarts the instance (
Restart=on-failure, the delay growing geometrically fromRestartSec=10toRestartMaxDelaySec=900), and the manager restarts it on the client's next broadcast anyway.
Shutdown order¶
On exit, SIGTERM or SIGINT the launcher tears its own tree down in a fixed order:
- SIGTERM to the fast_tcp2 child of the session process, alone, then 2 s. Its signal handler sends a FIN on every connection. Without this the device keeps its connections open and refuses the next session until it is power cycled.
- SIGTERM to the session process, the encoder, the injector, the desktop tree and Xvfb.
- 2 s later, SIGKILL to what is left of that tree, then the pipes are removed.
The unit uses KillMode=mixed and TimeoutStopSec=15: systemd signals the launcher only and
lets it do the above, then kills the whole cgroup if it has not finished.
Running one seat by hand¶
With the package installed, stop the manager's instance first so that two launchers do not fight for the same device, then run the launcher in a terminal:
sudo systemctl stop pupitre-seat@3
sudo env PUPITRE_SEAT=3 PUPITRE_MAC=00:80:64:aa:bb:cc PUPITRE_CLIENT_IP=192.0.2.42 \
PUPITRE_SERVER_IP=192.0.2.6 PUPITRE_IFACE=enp1s0 \
/usr/lib/pupitre/pupitre-seat 3
Or write /run/pupitre/seat-3.env by hand and pass only the seat number: the launcher reads
the file when PUPITRE_MAC is not in its environment. Ctrl-C runs the ordered shutdown.
When the pupil switches the device off¶
A client that DIES is seen at once: the launcher watches four vital processes and rebuilds the
link. A client that SURVIVES while talking into the void is the ordinary case: the pupil switches the device off and on
again, or unplugs the network. The driver stays alive, the status file still says ready, and
the screen stays blank for the rest of the lesson.
The witness is the client's own log: every packet RECEIVED from the device leaves a
1234< t=..., 4321< t=... or 1235< t=... line there, unconditionally. The launcher samples
the last one every two seconds and calls the link dead once it has not moved for
PUPITRE_SILENCE seconds.
In normal operation the largest gap between two inbound packets is about 2 s, so fifteen seconds
leaves a wide margin. It does not fire on a device stall either: during a stall the device keeps
acknowledging at win=24 every few milliseconds, and stall tolerance stays where it belongs, in
the client (twelve stalled frames in a row).
The desktop is kept. A dead link does not restart the seat: only the link is rebuilt, and Xvfb, the desktop, PulseAudio and the injector stay up, so the pupil finds their windows where they left them. The line is not "what produces pixels" but what survives the pipe closing:
| Side | Processes | Why |
|---|---|---|
| session | Xvfb, xfce4-session, PulseAudio, the parec bridge, the injector |
the injector reopens its pipe instead of exiting, and the bridge is already restarted in a loop |
| link | pupitre.main and its fast_tcp2, the encoder, the hardware cursor, the key watcher |
they open their pipe ONCE and have nobody left once the client is gone |
Two consequences worth knowing. The encoder is killed explicitly on every reconnection even
when it looks dead already: it only takes its BrokenPipeError on the first write after the
reader leaves, and a still desktop writes nothing, so on a fixed screen it survives the client
with its descriptor still open. Left alive, it and its successor would both write into the same
pipe, interleaved. The key watcher is cycled too: its socket is created by fast_tcp2, and a
FUSE left mounted on a dead socket would never give the key back.
A desktop that dies is a different matter: nothing here can repair it, the launcher exits 1 and systemd restarts the whole seat.
PUPITRE_GRACE (600 s, 0 to never give up) bounds the wait. Past it the seat releases its
memory rather than holding a desktop nobody is watching, and it exits 0, NOT 1: a failed unit
raises the manager's restart backoff, while a clean exit leaves the unit inactive and the
device's next announce is served at once. The grace is judged at the top of the loop, and only a
session of at least PUPITRE_UTILE seconds (60) resets its clock: a link that opens and hangs
up at once would otherwise keep going through the success branch and the seat would run forever
on a failing device. The status file says attente while the desktop is up
and the device is away; the console files that under "starting", which is fair, and while the
device stays off it sees the seat "offline" from its announces anyway.
Knobs, all environment variables: PUPITRE_AUDIO=0 (no sound; by default the seat has its own
PulseAudio, a null sink wyse and a parec bridge into the audio pipe), PUPITRE_FPS (8), PUPITRE_DEBIT_MAX (0, the video throughput cap in KiB/s, 0 for none), PUPITRE_ATTEMPTS
(5), PUPITRE_READY_TIMEOUT (45 s per attempt), PUPITRE_RETRY_COOLDOWN (25 s),
PUPITRE_SILENCE (15 s, 0 to disable: how long the device may stay silent before the link is
called dead, see below), PUPITRE_GRACE (600 s, 0 to never give up: how long a seat keeps its
desktop up without a link), PUPITRE_UTILE (60 s, below which a session counts as a flap),
PUPITRE_QUIET_KEYS=0 to log key contents while debugging (never in production, a session
may contain a password), PUPITRE_PYTHON to use another interpreter, PUPITRE_CLE=0 to
disable the USB key. With the key enabled (the default), fast_tcp2 serves the key plugged into
the client on /run/pupitre/seat-N/cle.sock, readable by the seat user only, and nothing is
mounted by default. pupitre-cle N surveiller, started by the seat, keeps
wyse_cle_fuse running in /run/pupitre/seat-N/cle-img (the key as a plain file disque.img,
0 bytes while no key is open) and puts a "Clé USB" launcher on the desktop while a key is
plugged in. A double click (pupitre-cle N monter) mounts the key's FAT with fusefatfs in
~/Clé USB and opens it; the launcher becomes "Éjecter la clé USB" (pupitre-cle N ejecter). A key
pulled out while mounted is unmounted and its launcher removed. The kernel never mounts the key.
tools/cle_fuse_essai.sh tests the file layer without a client; mdir -i <dir>/disque.img@@<offset>
:: reads the image directly, the offset being the partition's first sector times 512.
From a source checkout, without the package, scripts/pupitre-seat finds the tools in
tools/ and puts the tree on PYTHONPATH when /usr/lib/pupitre/fast_tcp2 does not exist;
src/fast_tcp2/fast_tcp2 must be built and given its capabilities (scripts/setcap.sh), and
the nft rule installed (scripts/nft-setup.sh). PUPITRE_LIBDIR overrides the tools
directory.
To watch a seat: journalctl -fu pupitre-seat@3 for the launcher, tail -f
/var/log/pupitre/seat-3/session.log for the session driver, and the fast_tcp2 log it names.
Pupils' logins (PUPITRE_CONNEXION=eleves)¶
By default (PUPITRE_CONNEXION=fixe) a seat opens one fixed desktop. In eleves mode the seat
shows a LightDM login screen on its own Xvfb instead, and each pupil gets a session under their
own account. The Xvfb and the fast_tcp2 link are never restarted: a logout brings the login
screen back in about a second.
Enabling it, per seat. A systemd drop-in, then a restart of that seat:
# /etc/systemd/system/pupitre-seat@N.service.d/eleves.conf
[Service]
Environment=PUPITRE_CONNEXION=eleves
systemctl daemon-reload && systemctl restart pupitre-seat@N
Restart LightDM once, after the package is first installed. LightDM reads its configuration
(/usr/share/lightdm/lightdm.conf.d/50-pupitre.conf, not a conffile) only at start, and
restarting it closes every open session, which is why the package does not do it. Until then
pupils cannot log in (LightDM authenticates through its own PAM service, no home is created).
The seat logs LightDM started before pupitre's configuration when it sees this: it compares
LightDM's start with /var/lib/pupitre/lightdm-config.stamp, which the postinst renews only
when the snippet's content changes (first install, new snippet). An upgrade that leaves the
snippet as it was does not warn. It only warns.
PUPITRE_FERMETURE (600 s, 0 to keep it until PUPITRE_GRACE stops the seat): how long a pupil's session survives the
device being switched off, after which it is closed. The device coming back cancels the timer.
PUPITRE_GRACE bounds it: after PUPITRE_GRACE seconds without link the seat stops, and
stopping closes the session. The effective delay is therefore min(PUPITRE_FERMETURE,
PUPITRE_GRACE) when PUPITRE_GRACE is not 0, and PUPITRE_FERMETURE=0 means "until
PUPITRE_GRACE". The seat logs a warning at start in mode eleves when the value set cannot hold.
Homes are /home/eleves/<user>, mode 0700, created at first login from /etc/skel. Never
under /var/lib/pupitre, which two root services own.
The root hook, /usr/lib/pupitre/pupitre-connexion debut|fin|fermer, is LightDM's session
setup and cleanup script. debut refuses names outside letters, digits, ., _, -, allows
the pupil alone on the seat's X display (xhost +SI:localuser:) and on its sound socket (an ACL),
closes the same account's session on another seat, and writes the seat's session file. fin undoes
all of it, stops the key watcher (so the key is unmounted) before closing the seat's session. A
session is closed by stopping its own systemd scope. XDG_SEAT is emptied only on seat displays
(:1??), so the server's console keeps its seat.
Beyond the seat's own session, the hooks act on pupil accounts only: uid at least UID_MIN
(/etc/login.defs, 1000 by default), not root, in neither sudo nor adm. For a pupil, debut
also closes their sessions on the other seat displays (:101 to :199), and fin stops their
whole user-UID.slice; user@UID and the slice are stopped only when logind shows no other
session of that uid (ssh, console, another seat). An adult who tries a seat with their own
account loses nothing but that seat's session.
Known limits.
- Restarting LightDM closes every session; the seat adds its LightDM seat again (about 1.7 s).
- logind sees the session as seatless but active.
- A personal PulseAudio still starts for each pupil, unused (the sound goes to the seat's).
- The greeter's accessibility bus leaks into the session: a harmless dbind-WARNING.
Behaviour in the cases that matter.
- Reinstalling the package during a session: the session survives, and so does the device link, since the postinst reloads the nft rule in place instead of restarting its unit.
- LightDM restarted with a pupil in session: the session is closed and the login screen is back in 1.7 s.
- Sound: the session reaches the seat's PulseAudio (a personal one still runs, unused). Another pupil cannot use the seat's sound: the connection is refused.
- USB key (unmounted before the session ends): the key is offered, mounted, read and ejected in the pupil's session. After logout nothing of the first pupil is left (no process, no FUSE mount), and the next pupil can neither read the first one's home nor see the key mounted.
- Device switched off (
PUPITRE_FERMETURE): a link drop shorter than the delay keeps the pupil's session; a longer absence closes it once the delay has elapsed since the link fell, and the seat returns to the login screen when the device comes back. - Same pupil on two devices: the other seat's session is closed.