tty: arrow keys, backspace, and four terminals nobody was home on
Three things, all in the first minute of using a shell.
**Arrow keys corrupted the two keystrokes after them.** They arrive as single
keycodes -- KEY_UP is 0xE2 -- and were pushed into the input buffer exactly
like that. No terminal works that way, and 0xE2 is the worst possible byte to
have chosen: it is a UTF-8 lead byte for a three-byte character, so a reader
decoding UTF-8 swallowed the next two keys waiting for continuations that never
arrived. consoleintr() sends the DEC/xterm sequences now -- ESC[A, ESC[D,
ESC[5~ and the rest -- which every terminfo entry and every line editor already
knows. Not echoed: in canonical mode echoing them would move the cursor for
real, which was the other half of what "weird glitches" looked like.
**Then the end of the line vanished as you typed into it**, with the buffer
perfectly correct. One line in vt_putc(): backspace erased the cell it moved
onto. That is a line-discipline erase, not the BS control character -- a
program rubbing a character out sends `\b \b`, and the separation matters
because programs also use plain `\b` to go back. zsh does, on every mid-line
edit:
echo alpha \b\b zha \b\b
back two, rewrite the tail, back two again to park the cursor. Against a
destructive backspace that last pair deleted the `ha` it had just drawn.
`\b` moves and nothing else now, 0x7f is ignored as on a real terminal, and
consputc() spells its own erase out as `\b \b` as it already did for serial.
ESC[@, ESC[P and ESC[X went in alongside, because TERM=linux declares them.
**VT3 to VT6 were blank** because nothing ran on them. NVT is 6 and the kernel
made all six; only two had a shell, so Alt+F3 switched to a terminal that was
working perfectly and had nobody to talk to. Four more manifests.
Six shells starting at once is enough concurrency to expose an intermittent
fault in one of them -- one boot in five, a wild jump into .data that runs
until it writes to 0, which nothing catches because SchOS never sets EFER.NXE.
Three boots with two shells, none. Recorded rather than fixed: it is not caused
by the manifests, only made likelier by them.
Also: QEMU_NET forwards host 8080/8081/8000 to guest 80/8080/8000, so the httpd
is reachable from the machine running QEMU instead of being sealed behind NAT.
Verified end to end. And the greeting prints once again -- a replaced
supervisor was announcing a boot that had already happened.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
75ccf9841c
commit
ff7afa746e
9 changed files with 317 additions and 8 deletions
22
Makefile
22
Makefile
|
|
@ -207,7 +207,27 @@ QEMU_DISPLAY_1 = -vga std
|
|||
#
|
||||
# To point the driver at different hardware once there is a second one, override
|
||||
# it: make qemu QEMU_NET='-netdev user,id=net0 -device rtl8139,netdev=net0'
|
||||
QEMU_NET = -netdev user,id=net0 -device e1000,netdev=net0
|
||||
# The guest's network, and a way in from the host.
|
||||
#
|
||||
# `-netdev user` is QEMU's NAT: the guest can reach the outside world and
|
||||
# nothing outside can reach the guest, which is the safe default and exactly
|
||||
# wrong when the thing you are testing is a server. SchOS has an httpd now, and
|
||||
# a web server nobody can open is hard to believe in.
|
||||
#
|
||||
# hostfwd punches three holes, on the ports something is actually likely to be
|
||||
# listening on. They bind 127.0.0.1 on purpose: the guest becomes reachable
|
||||
# from *this* machine and not from the network this machine is on, which is a
|
||||
# development convenience rather than a server somebody forgot about.
|
||||
#
|
||||
# http://localhost:8080 -> guest port 80 (httpd's default)
|
||||
# http://localhost:8000 -> guest port 8000
|
||||
# http://localhost:8081 -> guest port 8080
|
||||
#
|
||||
# Port 80 on the host is not used: binding it needs root, and `make qemu`
|
||||
# should not. Override the whole variable for anything else --
|
||||
# make qemu QEMU_NET='-netdev user,id=net0,hostfwd=tcp::9000-:9000 ...'
|
||||
QEMU_HOSTFWD = hostfwd=tcp:127.0.0.1:8080-:80,hostfwd=tcp:127.0.0.1:8000-:8000,hostfwd=tcp:127.0.0.1:8081-:8080
|
||||
QEMU_NET = -netdev user,id=net0,$(QEMU_HOSTFWD) -device e1000,netdev=net0
|
||||
|
||||
# An absolute pointing device, on an xHCI controller.
|
||||
#
|
||||
|
|
|
|||
|
|
@ -650,7 +650,11 @@ main(void)
|
|||
// anything printed before this point would come after it on screen. The
|
||||
// kernel's whole boot is a kinfo() and is in the klog; this is the part a
|
||||
// person is meant to read.
|
||||
if (!verbose_boot)
|
||||
// Only the first supervisor of a boot says it. A replacement adopts
|
||||
// running jobs rather than starting them, so greeting the user again would
|
||||
// be announcing a boot that already happened -- and it printed twice for
|
||||
// real, the day schinitd was replaced during startup.
|
||||
if (!verbose_boot && h.generation <= 1)
|
||||
out("\nWelcome to SchOS!"
|
||||
" (F8 at boot for the full log, or dmesg once you are in)\n\n");
|
||||
|
||||
|
|
|
|||
|
|
@ -261,6 +261,45 @@ It returns **`EINTR`** now, which is what a signal-interrupted read is required
|
|||
to give: the whole convention — retry, or return to the caller — is built on
|
||||
receiving that and not something else.
|
||||
|
||||
## Editing a line you have already typed
|
||||
|
||||
Two bugs, both in the same half-second of using a shell, and both worth
|
||||
recording because the symptom pointed away from the cause.
|
||||
|
||||
**Arrow keys corrupted the next two keystrokes.** They arrive from the keyboard
|
||||
as single keycodes -- `KEY_UP` is `0xE2` -- and were pushed into the input
|
||||
buffer exactly like that. No terminal has ever worked that way, and `0xE2` is
|
||||
the worst byte to have picked: it is a UTF-8 lead byte for a three-byte
|
||||
character, so a reader decoding UTF-8 swallowed the *next two keys* waiting for
|
||||
continuations that never came. `consoleintr()` translates them to the DEC/xterm
|
||||
sequences now -- `ESC[A`, `ESC[D`, `ESC[5~` and the rest -- which is what every
|
||||
terminfo entry and every line editor already expects.
|
||||
|
||||
**Then the end of the line vanished as you typed into it.** With the arrows
|
||||
working, editing mid-line left the buffer perfectly correct and the screen
|
||||
wrong, which is a maddening pair. The cause is one line in `vt_putc()`:
|
||||
backspace *erased the cell it moved onto*.
|
||||
|
||||
That is what a line-discipline erase does. It is not what the BS control
|
||||
character does — a program that wants to rub a character out sends `\b \b`,
|
||||
backspace, space, backspace, and the separation matters because programs also
|
||||
use plain `\b` simply to go back. zsh does, on every mid-line edit:
|
||||
|
||||
```
|
||||
echo alpha \b\b zha \b\b
|
||||
```
|
||||
|
||||
Move back two, rewrite the tail, move back two again to leave the cursor where
|
||||
the user expects it. Against a destructive backspace that last pair deleted the
|
||||
`ha` it had just drawn.
|
||||
|
||||
`\b` moves and nothing else now; `0x7f` is ignored, as it is on a real
|
||||
terminal; and `consputc()` spells its own erase out as `\b \b`, exactly as it
|
||||
already did for the serial line. `ESC[@`, `ESC[P` and `ESC[X` — insert, delete
|
||||
and erase characters — were added at the same time, because `TERM=linux`
|
||||
declares them and a terminal that drops them silently is the same class of bug
|
||||
waiting for a different editor.
|
||||
|
||||
## What it does not do
|
||||
|
||||
- **No scrollback.** The grid is exactly the visible screen. Shift+PageUp does
|
||||
|
|
|
|||
29
etc/schinit/tty3-shell.job
Normal file
29
etc/schinit/tty3-shell.job
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# An interactive shell on /dev/tty3.
|
||||
#
|
||||
# NVT is 6 (include/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F3 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
|
||||
Label = os.schos.tty3-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty3
|
||||
StandardOut = /dev/tty3
|
||||
StandardError = /dev/tty3
|
||||
|
||||
ThrottleInterval = 1
|
||||
29
etc/schinit/tty4-shell.job
Normal file
29
etc/schinit/tty4-shell.job
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# An interactive shell on /dev/tty4.
|
||||
#
|
||||
# NVT is 6 (include/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F4 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
|
||||
Label = os.schos.tty4-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty4
|
||||
StandardOut = /dev/tty4
|
||||
StandardError = /dev/tty4
|
||||
|
||||
ThrottleInterval = 1
|
||||
29
etc/schinit/tty5-shell.job
Normal file
29
etc/schinit/tty5-shell.job
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# An interactive shell on /dev/tty5.
|
||||
#
|
||||
# NVT is 6 (include/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F5 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
|
||||
Label = os.schos.tty5-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty5
|
||||
StandardOut = /dev/tty5
|
||||
StandardError = /dev/tty5
|
||||
|
||||
ThrottleInterval = 1
|
||||
29
etc/schinit/tty6-shell.job
Normal file
29
etc/schinit/tty6-shell.job
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# An interactive shell on /dev/tty6.
|
||||
#
|
||||
# NVT is 6 (include/vt.h) and the kernel creates all six terminals, but only
|
||||
# two ever had anything running on them -- so Alt+F6 switched to a terminal
|
||||
# that was working perfectly and had nobody to talk to, which looks exactly
|
||||
# like a broken one.
|
||||
#
|
||||
# Identical to tty2-shell apart from the number. Six manifests rather than one
|
||||
# with a range, because schinit has no notion of a template and inventing one
|
||||
# for six lines each is the wrong trade.
|
||||
|
||||
Label = os.schos.tty6-shell
|
||||
Program = /bin/zsh
|
||||
Args = -zsh
|
||||
RunAtLoad = yes
|
||||
KeepAlive = yes
|
||||
|
||||
Environment = TERM=linux
|
||||
Environment = PATH=/bin:/usr/bin
|
||||
Environment = HOME=/root
|
||||
Environment = SHELL=/bin/zsh
|
||||
|
||||
ControllingTerminal = yes
|
||||
|
||||
StandardIn = /dev/tty6
|
||||
StandardOut = /dev/tty6
|
||||
StandardError = /dev/tty6
|
||||
|
||||
ThrottleInterval = 1
|
||||
|
|
@ -266,10 +266,16 @@ consputc(int c)
|
|||
if (cons_logonly)
|
||||
return;
|
||||
|
||||
// The kernel's own erase, echoed when you press backspace at a prompt the
|
||||
// line discipline is reading. Now that the terminal's `\b` only moves -- see
|
||||
// the note in vt.c -- this has to spell the erase out, exactly as it already
|
||||
// did for the serial line.
|
||||
if (c == BACKSPACE) {
|
||||
uartputc('\b'); uartputc(' '); uartputc('\b');
|
||||
if (!cons_serial_only)
|
||||
vt_putc(vt_current(), '\b');
|
||||
if (!cons_serial_only) {
|
||||
struct vt *v = vt_current();
|
||||
vt_putc(v, '\b'); vt_putc(v, ' '); vt_putc(v, '\b');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -559,6 +565,51 @@ consoleintr(int (*getc)(void))
|
|||
}
|
||||
}
|
||||
|
||||
// Arrow keys and the rest of the navigation block, as escape sequences.
|
||||
//
|
||||
// These arrive as single keycodes -- KEY_UP is 0xE2 -- and used to be
|
||||
// pushed into the input buffer exactly like that. A terminal has never
|
||||
// worked that way, and 0xE2 in particular is the worst possible choice of
|
||||
// byte: it is a UTF-8 lead byte for a three-byte character, so a reader
|
||||
// decoding UTF-8 swallowed the next two keys waiting for continuations
|
||||
// that never came. Every arrow key corrupted the two keystrokes after it.
|
||||
//
|
||||
// What a terminal sends is `ESC [ A`, and what makes zsh's line editor
|
||||
// work is receiving that. The sequences below are the DEC/xterm ones that
|
||||
// every terminfo entry and every line editor already knows.
|
||||
{
|
||||
const char *esc = 0;
|
||||
switch (c) {
|
||||
case KEY_UP: esc = "\033[A"; break;
|
||||
case KEY_DN: esc = "\033[B"; break;
|
||||
case KEY_RT: esc = "\033[C"; break;
|
||||
case KEY_LF: esc = "\033[D"; break;
|
||||
case KEY_HOME: esc = "\033[H"; break;
|
||||
case KEY_END: esc = "\033[F"; break;
|
||||
case KEY_PGUP: esc = "\033[5~"; break;
|
||||
case KEY_PGDN: esc = "\033[6~"; break;
|
||||
case KEY_INS: esc = "\033[2~"; break;
|
||||
case KEY_DEL: esc = "\033[3~"; break;
|
||||
}
|
||||
if (esc) {
|
||||
// Not echoed. In raw mode -- which is where a line editor lives, and
|
||||
// the only mode where these mean anything -- nothing is echoed anyway.
|
||||
// In canonical mode echoing them would send the escape straight back
|
||||
// to the renderer and move the cursor for real, which is the other
|
||||
// half of what "weird glitches" looked like.
|
||||
for (const char *q = esc; *q; q++) {
|
||||
if (v->in.e - v->in.r >= VT_INPUT_BUF)
|
||||
break; // buffer full: drop the rest
|
||||
v->in.buf[v->in.e++ % VT_INPUT_BUF] = *q;
|
||||
}
|
||||
if (!(v->tio.c_lflag & ICANON)) {
|
||||
v->in.w = v->in.e;
|
||||
wakeup(&v->in.r);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (v->in.e - v->in.r >= VT_INPUT_BUF)
|
||||
continue; // buffer full: drop it
|
||||
|
||||
|
|
|
|||
|
|
@ -325,13 +325,32 @@ vt_putc(struct vt *v, int glyph)
|
|||
v->cx = 0;
|
||||
break;
|
||||
|
||||
// Backspace moves the cursor. It does **not** erase.
|
||||
//
|
||||
// This blanked the cell it moved onto, which is what a line-discipline erase
|
||||
// does, not what the BS control character does. A program that wants to rub
|
||||
// a character out sends `\b \b` -- backspace, space, backspace -- and the
|
||||
// separation matters because programs also use plain `\b` simply to go back.
|
||||
//
|
||||
// zsh does, on every edit of a line you have already typed. Inserting a
|
||||
// character in the middle, it sends:
|
||||
//
|
||||
// echo alpha \b\b zha \b\b
|
||||
//
|
||||
// -- move back two, rewrite the tail, then move back two again to leave the
|
||||
// cursor where the user expects it. Against a destructive backspace that
|
||||
// last pair deleted the `ha` it had just drawn, so the line appeared to lose
|
||||
// its tail while the buffer was perfectly correct. Which is exactly how it
|
||||
// looked: type in the middle of a line and the end of it vanishes.
|
||||
case '\b':
|
||||
case 0x7f:
|
||||
v->wrap_pending = 0;
|
||||
if (v->cx > 0) {
|
||||
if (v->cx > 0)
|
||||
v->cx--;
|
||||
put_blank_at(v, v->cx, v->cy);
|
||||
}
|
||||
break;
|
||||
|
||||
// DEL is ignored, as it is on a real terminal. It is not a movement and it
|
||||
// is not an erase; it exists to be discarded on a paper tape.
|
||||
case 0x7f:
|
||||
break;
|
||||
|
||||
case '\t':
|
||||
|
|
@ -453,6 +472,62 @@ vt_erase_line(struct vt *v, int mode)
|
|||
}
|
||||
}
|
||||
|
||||
// Insert and delete characters within a line: ESC[n@ and ESC[nP.
|
||||
//
|
||||
// These were missing, and what was missing with them was **editing a line you
|
||||
// have already typed**. `TERM=linux` declares `ich1=\E[@` and `dch1=\E[P`, so a
|
||||
// line editor that inserts a character in the middle sends one of these and
|
||||
// then writes the character -- and against a terminal that silently drops the
|
||||
// escape, the text after the cursor is overwritten instead of shifted. What
|
||||
// that looks like is the end of the line disappearing as you type into it,
|
||||
// with the *buffer* perfectly correct, which is a maddening pair of symptoms
|
||||
// because the program is doing nothing wrong.
|
||||
//
|
||||
// The row is shifted in the grid and then repainted from the cursor to the end
|
||||
// of the line. No attempt at a hardware scroll: a line is at most a couple of
|
||||
// hundred cells and this happens once per keystroke.
|
||||
static void
|
||||
vt_shift_line(struct vt *v, int n, int insert)
|
||||
{
|
||||
int row = v->cy, start = v->cx, width = v->cols;
|
||||
int i;
|
||||
|
||||
if (n < 1)
|
||||
n = 1;
|
||||
if (n > width - start)
|
||||
n = width - start;
|
||||
|
||||
if (insert) {
|
||||
for (i = width - 1; i >= start + n; i--)
|
||||
*cell_at(v, i, row) = *cell_at(v, i - n, row);
|
||||
for (i = start; i < start + n; i++) {
|
||||
struct vtcell *k = cell_at(v, i, row);
|
||||
k->ch = ' '; k->fg = v->fg; k->bg = v->bg; k->attr = 0;
|
||||
}
|
||||
} else {
|
||||
for (i = start; i < width - n; i++)
|
||||
*cell_at(v, i, row) = *cell_at(v, i + n, row);
|
||||
for (i = width - n; i < width; i++) {
|
||||
struct vtcell *k = cell_at(v, i, row);
|
||||
k->ch = ' '; k->fg = v->fg; k->bg = v->bg; k->attr = 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = start; i < width; i++)
|
||||
show(v, i, row);
|
||||
}
|
||||
|
||||
// ESC[nX -- erase n characters where they stand, without moving anything.
|
||||
static void
|
||||
vt_erase_chars(struct vt *v, int n)
|
||||
{
|
||||
if (n < 1)
|
||||
n = 1;
|
||||
if (n > v->cols - v->cx)
|
||||
n = v->cols - v->cx;
|
||||
erase_run(v, v->cx, v->cy, n);
|
||||
}
|
||||
|
||||
void
|
||||
vt_clear(struct vt *v)
|
||||
{
|
||||
|
|
@ -647,6 +722,10 @@ vt_csi(struct vt *v, int final)
|
|||
case 'J': vt_erase_display(v, n); break;
|
||||
case 'K': vt_erase_line(v, n); break;
|
||||
|
||||
case '@': vt_shift_line(v, n1, 1); break; // ICH -- insert characters
|
||||
case 'P': vt_shift_line(v, n1, 0); break; // DCH -- delete characters
|
||||
case 'X': vt_erase_chars(v, n1); break; // ECH -- erase characters
|
||||
|
||||
case 'm': vt_sgr(v); break;
|
||||
|
||||
case 's': v->saved_cx = col; v->saved_cy = row; break;
|
||||
|
|
|
|||
Loading…
Reference in a new issue