diff --git a/Makefile b/Makefile index f8125dd..89d4060 100644 --- a/Makefile +++ b/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. # diff --git a/cmd/schinitd/main.c b/cmd/schinitd/main.c index f19401e..b4c0c4a 100644 --- a/cmd/schinitd/main.c +++ b/cmd/schinitd/main.c @@ -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"); diff --git a/docs/architecture/terminals.md b/docs/architecture/terminals.md index c3f48f6..90a6f1d 100644 --- a/docs/architecture/terminals.md +++ b/docs/architecture/terminals.md @@ -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 diff --git a/etc/schinit/tty3-shell.job b/etc/schinit/tty3-shell.job new file mode 100644 index 0000000..ade94e1 --- /dev/null +++ b/etc/schinit/tty3-shell.job @@ -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 diff --git a/etc/schinit/tty4-shell.job b/etc/schinit/tty4-shell.job new file mode 100644 index 0000000..022d7d8 --- /dev/null +++ b/etc/schinit/tty4-shell.job @@ -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 diff --git a/etc/schinit/tty5-shell.job b/etc/schinit/tty5-shell.job new file mode 100644 index 0000000..1ed3d7d --- /dev/null +++ b/etc/schinit/tty5-shell.job @@ -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 diff --git a/etc/schinit/tty6-shell.job b/etc/schinit/tty6-shell.job new file mode 100644 index 0000000..6b3faa2 --- /dev/null +++ b/etc/schinit/tty6-shell.job @@ -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 diff --git a/kernel/core/console.c b/kernel/core/console.c index 62e6ac5..c2174c7 100644 --- a/kernel/core/console.c +++ b/kernel/core/console.c @@ -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 diff --git a/kernel/dev/vt.c b/kernel/dev/vt.c index ba30635..26540a2 100644 --- a/kernel/dev/vt.c +++ b/kernel/dev/vt.c @@ -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;