Skip to content

Commit

Permalink
Add missing ICANON features
Browse files Browse the repository at this point in the history
  • Loading branch information
jart committed Sep 5, 2024
1 parent dd8544c commit 03875be
Show file tree
Hide file tree
Showing 22 changed files with 526 additions and 251 deletions.
139 changes: 101 additions & 38 deletions examples/ctrlc.c
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,43 @@
│ • http://creativecommons.org/publicdomain/zero/1.0/ │
╚─────────────────────────────────────────────────────────────────*/
#endif
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/sigaction.h"
#include "libc/errno.h"
#include "libc/limits.h"
#include "libc/runtime/runtime.h"
#include "libc/sock/struct/pollfd.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/f.h"
#include "libc/sysv/consts/limits.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/poll.h"
#include "libc/sysv/consts/sig.h"
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>

// this program is used by jart for manually testing teletype interrupts
// and canonical mode line editing. this file documents the hidden depth
// of 1960's era computer usage, that's entrenched in primitive i/o apis
//
// manual testing checklist:
//
// - "hello" enter echos "got: hello^J"
//
// - "hello" ctrl-d echos "got: hello"
//
// - "hello" ctrl-r echos "^R\nhello"
//
// - "hello" ctrl-u enter echos "got: ^J"
//
// - ctrl-d during i/o task prints "got eof" and exits
//
// - ctrl-d during cpu task gets delayed until read() is called
//
// - ctrl-c during cpu task echos ^C, then calls SignalHandler()
// asynchronously, and program exits
//
// - ctrl-c during i/o task echos ^C, then calls SignalHandler()
// asynchronously, read() raises EINTR, and program exits
//
// - ctrl-v ctrl-c should echo "^\b" then echo "^C" and insert "\3"
//
// - ctrl-v ctrl-d should echo "^\b" then echo "^D" and insert "\4"
//

volatile bool gotsig;

Expand All @@ -34,23 +57,41 @@ void SignalHandler(int sig) {
gotsig = true;
}

// this is the easiest way to write a string literal to standard output,
// without formatting. printf() has an enormous binary footprint so it's
// nice to avoid linking that when it is not needed.
#define WRITE(sliteral) write(1, sliteral, sizeof(sliteral) - 1)

int main(int argc, char *argv[]) {

printf("echoing stdin until ctrl+c is pressed\n");
WRITE("echoing stdin until ctrl+c is pressed\n");

// you need to set your signal handler using sigaction() rather than
// signal(), since the latter uses .sa_flags=SA_RESTART, which means
// read will restart itself after signals, rather than raising EINTR
// when you type ctrl-c, by default it'll kill the process, unless you
// define a SIGINT handler. there's multiple ways to do it. the common
// way is to say signal(SIGINT, func) which is normally defined to put
// the signal handler in Berkeley-style SA_RESTART mode. that means if
// a signal handler is called while inside a function like read() then
// the read operation will keep going afterwards like nothing happened
// which can make it difficult to break your event loop. to avoid this
// we can use sigaction() without specifying SA_RESTART in sa_flag and
// that'll put the signal in system v mode. this means that whenever a
// signal handler function in your program is called during an i/o op,
// that i/o op will return an EINTR error, so you can churn your loop.
// don't take that error too seriously though since SIGINT can also be
// delivered asynchronously, during the times you're crunching numbers
// rather than performing i/o which means you get no EINTR to warn you
sigaction(SIGINT, &(struct sigaction){.sa_handler = SignalHandler}, 0);

for (;;) {

// some programs are blocked on cpu rather than i/o
// such programs shall rely on asynchronous signals
printf("doing cpu task...\n");
// asynchronous signals are needed to interrupt math, which we shall
// simulate here. signals can happen any time any place. that's only
// not the case when you use sigprocmask() to block signals which is
// useful for kicking the can down the road.
WRITE("doing cpu task...\n");
for (volatile int i = 0; i < INT_MAX / 5; ++i) {
if (gotsig) {
printf("\rgot ctrl+c asynchronously\n");
WRITE("\rgot ctrl+c asynchronously\n");
exit(0);
}
}
Expand All @@ -71,14 +112,18 @@ int main(int argc, char *argv[]) {

// read data from standard input
//
// since this is a blocking operation and we're not performing a
// cpu-bound operation it is almost with absolute certainty that
// when the ctrl-c signal gets delivered, it'll happen in read()
//
// it's possible to be more precise if we were building library
// code. for example, you can block signals using sigprocmask()
// and then use pselect() to do the waiting.
printf("doing read i/o task...\n");
// assuming you started this program in your terminal standard input
// will be plugged into your termios driver, which cosmpolitan codes
// in libc/calls/read-nt.c on windows. your read() function includes
// a primitive version of readline/linenoise called "canonical mode"
// which lets you edit the data that'll be returned by read() before
// it's actually returned. for example, if you type hello and enter,
// then "hello\n" will be returned. if you type hello and then ^D or
// ctrl-d, then "hello" will be returned. the ctrl-d keystroke is in
// fact an ascii control code whose special behavior can be bypassed
// if you type ctrl-v ctrl-d and then enter, in which case "\3\n" is
// returned, also known as ^D^J.
WRITE("doing read i/o task...\n");
int got = read(0, buf, sizeof(buf));

// check if the read operation failed
Expand All @@ -94,10 +139,10 @@ int main(int argc, char *argv[]) {
// the \r character is needed so when the line is printed
// it'll overwrite the ^C that got echo'd with the ctrl-c
if (gotsig) {
printf("\rgot ctrl+c via i/o eintr\n");
WRITE("\rgot ctrl+c via i/o eintr\n");
exit(0);
} else {
printf("\rgot spurious eintr\n");
WRITE("\rgot spurious eintr\n");
continue;
}
} else {
Expand All @@ -109,16 +154,34 @@ int main(int argc, char *argv[]) {

// check if the user typed ctrl-d which closes the input handle
if (!got) {
printf("got eof\n");
WRITE("got eof\n");
exit(0);
}

// relay read data to standard output
// visualize line data returned by canonical mode to standard output
//
// it's usually safe to ignore the return code of write; your system
// will send SIGPIPE if there's any problem, which kills by default.
//
// it's usually safe to ignore the return code of write. the
// operating system will send SIGPIPE if there's any problem
// which kills the process by default
// it's possible to use keyboard shortcuts to embed control codes in
// the line. so we visualize them using the classic tty notation. it
// is also possible to type the ascii representation, so we use bold
// to visually distinguish ascii codes. see also o//examples/ttyinfo
write(1, "got: ", 5);
write(1, buf, got);
for (int i = 0; i < got; ++i) {
if (isascii(buf[i])) {
if (iscntrl(buf[i])) {
char ctl[2];
ctl[0] = '^';
ctl[1] = buf[i] ^ 0100;
WRITE("\033[1m");
write(1, ctl, 2);
WRITE("\033[0m");
} else {
write(1, &buf[i], 1);
}
}
}
WRITE("\n");
}
}
4 changes: 3 additions & 1 deletion libc/calls/internal.h
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#ifndef COSMOPOLITAN_LIBC_CALLS_INTERNAL_H_
#define COSMOPOLITAN_LIBC_CALLS_INTERNAL_H_
#include "libc/atomic.h"
#include "libc/intrin/fds.h"
#include "libc/calls/struct/sigset.h"
#include "libc/calls/struct/sigval.h"
#include "libc/dce.h"
#include "libc/intrin/fds.h"
#include "libc/macros.h"
#include "libc/stdbool.h"

Expand All @@ -25,6 +26,7 @@ uint32_t sys_getuid_nt(void);
int __ensurefds_unlocked(int);
void __printfds(struct Fd *, size_t);
int CountConsoleInputBytes(void);
int CountConsoleInputBytesBlocking(uint32_t, sigset_t);
int FlushConsoleInputBytes(void);
int64_t GetConsoleInputHandle(void);
int64_t GetConsoleOutputHandle(void);
Expand Down
3 changes: 2 additions & 1 deletion libc/calls/linkat.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
*
* @param flags can have AT_EMPTY_PATH or AT_SYMLINK_NOFOLLOW
* @return 0 on success, or -1 w/ errno
* @raise EROFS if either path is under /zip/...
* @asyncsignalsafe
*/
int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath,
Expand All @@ -42,7 +43,7 @@ int linkat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath,
if (_weaken(__zipos_notat) &&
((rc = __zipos_notat(olddirfd, oldpath)) == -1 ||
(rc = __zipos_notat(newdirfd, newpath)) == -1)) {
STRACE("zipos fchownat not supported yet");
rc = erofs();
} else if (!IsWindows()) {
rc = sys_linkat(olddirfd, oldpath, newdirfd, newpath, flags);
} else {
Expand Down
2 changes: 1 addition & 1 deletion libc/calls/mkdirat.c
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
int mkdirat(int dirfd, const char *path, unsigned mode) {
int rc;
if (_weaken(__zipos_notat) && (rc = __zipos_notat(dirfd, path)) == -1) {
STRACE("zipos mkdirat not supported yet");
rc = erofs();
} else if (!IsWindows()) {
rc = sys_mkdirat(dirfd, path, mode);
} else {
Expand Down
9 changes: 5 additions & 4 deletions libc/calls/park.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@ static textwindows int _park_thread(uint32_t msdelay, sigset_t waitmask,
int sig, handler_was_called;
if (_check_cancel() == -1)
return -1;
if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask))) {
if (_weaken(__sig_get) && (sig = _weaken(__sig_get)(waitmask)))
goto HandleSignal;
}
int expect = 0;
atomic_int futex = 0;
struct PosixThread *pt = _pthread_self();
Expand All @@ -49,9 +48,11 @@ static textwindows int _park_thread(uint32_t msdelay, sigset_t waitmask,
handler_was_called = _weaken(__sig_relay)(sig, SI_KERNEL, waitmask);
if (_check_cancel() == -1)
return -1;
if (!restartable || (handler_was_called & SIG_HANDLED_NO_RESTART)) {
if (handler_was_called & SIG_HANDLED_NO_RESTART)
return eintr();
}
if (handler_was_called & SIG_HANDLED_SA_RESTART)
if (!restartable)
return eintr();
}
return 0;
}
Expand Down
55 changes: 39 additions & 16 deletions libc/calls/poll-nt.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,15 @@ static textwindows int sys_poll_nt_impl(struct pollfd *fds, uint64_t nfds,
bool ok;
uint64_t millis;
uint32_t cm, avail, waitfor;
struct sys_pollfd_nt pipefds[8];
struct sys_pollfd_nt pipefds[64];
struct sys_pollfd_nt sockfds[64];
int pipeindices[ARRAYLEN(pipefds)];
int sockindices[ARRAYLEN(sockfds)];
struct timespec started, deadline, remain, now;
struct timespec deadline, remain, now;
int i, rc, sn, pn, gotinvals, gotpipes, gotsocks;

started = timespec_real();
deadline = timespec_add(started, timespec_frommillis(ms ? *ms : -1u));
waitfor = ms ? *ms : -1u;
deadline = timespec_add(timespec_mono(), timespec_frommillis(waitfor));

// do the planning
// we need to read static variables
Expand Down Expand Up @@ -168,16 +168,39 @@ static textwindows int sys_poll_nt_impl(struct pollfd *fds, uint64_t nfds,
pipefds[i].revents |= POLLERR_;
}
} else if (GetConsoleMode(pipefds[i].handle, &cm)) {
switch (CountConsoleInputBytes()) {
case 0:
break;
case -1:
pipefds[i].revents &= ~POLLWRNORM_;
pipefds[i].revents |= POLLHUP_;
break;
default:
pipefds[i].revents |= POLLRDNORM_;
break;
// some programs like bash like to poll([stdin], 1, -1) so let's
// avoid busy looping in such cases. we could generalize this to
// always avoid busy loops, but we'd need poll to launch threads
if (pn == 1 && sn == 0 && (pipefds[i].events & POLLRDNORM_)) {
int err = errno;
switch (CountConsoleInputBytesBlocking(waitfor, sigmask)) {
case -1:
if (errno == EINTR || errno == ECANCELED)
return -1;
errno = err;
pipefds[i].revents &= ~POLLWRNORM_;
pipefds[i].revents |= POLLERR_;
break;
case 0:
pipefds[i].revents &= ~POLLWRNORM_;
pipefds[i].revents |= POLLHUP_;
break;
default:
pipefds[i].revents |= POLLRDNORM_;
break;
}
} else {
switch (CountConsoleInputBytes()) {
case 0:
break;
case -1:
pipefds[i].revents &= ~POLLWRNORM_;
pipefds[i].revents |= POLLHUP_;
break;
default:
pipefds[i].revents |= POLLRDNORM_;
break;
}
}
} else {
// we have no way of polling if a non-socket is readable yet
Expand All @@ -202,7 +225,7 @@ static textwindows int sys_poll_nt_impl(struct pollfd *fds, uint64_t nfds,
// check for pending signals, thread cancelation, etc.
waitfor = 0;
if (!gotinvals && !gotsocks && !gotpipes) {
now = timespec_real();
now = timespec_mono();
if (timespec_cmp(now, deadline) < 0) {
remain = timespec_sub(deadline, now);
millis = timespec_tomillis(remain);
Expand All @@ -211,7 +234,7 @@ static textwindows int sys_poll_nt_impl(struct pollfd *fds, uint64_t nfds,
if (waitfor) {
POLLTRACE("poll() sleeping for %'d out of %'lu ms", waitfor,
timespec_tomillis(remain));
if ((rc = _park_norestart(waitfor, sigmask)) == -1)
if (_park_norestart(waitfor, sigmask) == -1)
return -1; // eintr, ecanceled, etc.
}
}
Expand Down
Loading

0 comments on commit 03875be

Please sign in to comment.