/* Simulate low bitrate serial connection, like a 1980s modem. Author: B. Watson. License: WTFPL. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* portability stuff only tested on Linux and OSX. Hope it's OK. */ #if defined(__FreeBSD__) || defined(HAVE_LIBUTIL_H) #include #elif defined (__OpenBSD__) || defined (__NetBSD__) || defined (__APPLE__) || defined(HAVE_UTIL_H) #include #else #include #endif #define DEFAULT_BPS 2400 #define MIN_BPS 1 #define MAX_BPS 500000 const char *self; unsigned int bps; unsigned long interval, starttime, outbytes = 0; int debug = 0; int timer; struct timeval tv; struct itimerval itv, itv_disarm; sigset_t sigmask; #define FRACTIONAL_USEC #ifdef FRACTIONAL_USEC #define FRAC_US_DENOM 100 int frac_us_num; #endif #define NOW_USEC() ( (gettimeofday(&tv, NULL)), tv.tv_sec * 1000000L + tv.tv_usec ) void die(const char *msg) { if(errno) { fprintf(stderr, "%s: %s: %s\n", self, strerror(errno), msg); } else { fprintf(stderr, "%s: %s\n", self, msg); } exit(1); } void setup_timer(void) { itv_disarm.it_interval.tv_sec = itv_disarm.it_interval.tv_usec = itv_disarm.it_value.tv_sec = itv_disarm.it_value.tv_usec = 0; itv = itv_disarm; itv.it_interval.tv_usec = interval; itv.it_value.tv_usec = interval; sigemptyset(&sigmask); sigaddset(&sigmask, SIGALRM); sigprocmask(SIG_BLOCK, &sigmask, NULL); } /* on both Linux and OSX, using setitimer() and sigwait() gives better timing accuracy than usleep() or nanosleep(). */ void slow_write(int outfd, char c) { int j; unsigned long now, target, overslept; setitimer(ITIMER_REAL, &itv, NULL); target = NOW_USEC() + interval; #ifdef FRACTIONAL_USEC if((random() % FRAC_US_DENOM) < frac_us_num) target++; #endif write(outfd, &c, 1); outbytes++; sigwait(&sigmask, &j); setitimer(ITIMER_REAL, &itv_disarm, NULL); now = NOW_USEC(); if(now > target) { overslept = now - target; if(overslept < itv.it_interval.tv_usec) { // fprintf(stderr, "overslept %luus\n", overslept); itv.it_interval.tv_usec -= overslept; itv.it_value.tv_usec = itv.it_interval.tv_usec; } } else { while(NOW_USEC() < target) /* NOP */ ; } } void debug_stats(void) { if(outbytes) { unsigned long elapsed_us = NOW_USEC() - starttime; double elapsed_sec = ((double)elapsed_us/1000000.0L); double finterval = (1000000.0L / ((double)bps / 10.0L)); double actual = ((double)outbytes * 10.0L) / elapsed_sec; double offby = 100.0L * (((double)bps / actual) - 1.0L); fprintf(stderr, "outbytes %lu, elapsed_us %lu, tv_usec %lu, requested bps %d (%.2fms), " "actual %.2f, accuracy %.2f%%\n", outbytes, elapsed_us, itv.it_value.tv_usec, bps, finterval, actual, 100.0 - offby); } } void slowcat(int infd) { int c; outbytes = 0; starttime = NOW_USEC(); while(read(infd, &c, 1) == 1) { slow_write(1, c); } if(debug) debug_stats(); } void slowtty(char **args) { char *dflt_args[] = { NULL, NULL }; pid_t kidpid; int master, status; struct termios term, oldterm; struct winsize winsz; char c; if(!*args) { args = dflt_args; args[0] = getenv("SHELL"); if(!*args) args[0] = "/bin/sh"; } if(debug) { char **a = args; fprintf(stderr, "executing cmd '%s', argv[]: ", a[0]); while(*a) { fprintf(stderr, "%s ", *a++); } fprintf(stderr, "\n"); } if( (tcgetattr(0, &term)) ) die("tcgetattr"); if( (ioctl(0, TIOCGWINSZ, &winsz)) ) die("ioctl(TIOCGWINSZ)"); kidpid = forkpty(&master, NULL, &term, &winsz); if(kidpid < 0) die("forkpty"); if(kidpid == 0) { /* we are the child, do childish things */ execvp(args[0], args); perror("execvp"); /* don't use die() */ exit(127); } /* we are the parent, watch the child */ oldterm = term; /* so we can restore it later */ cfmakeraw(&term); tcsetattr(0, TCSANOW, &term); while(1) { ssize_t nbytes; fd_set readfds; FD_ZERO(&readfds); FD_SET(0, &readfds); FD_SET(master, &readfds); if(select(master + 1, &readfds, NULL, NULL, NULL) < 0) { if(errno == EINTR) continue; break; } if(FD_ISSET(0, &readfds)) { nbytes = read(0, &c, 1); if(nbytes < 1) break; write(master, &c, 1); } if(FD_ISSET(master, &readfds)) { nbytes = read(master, &c, 1); if(nbytes < 1) break; slow_write(1, c); } } close(master); while (waitpid(kidpid, &status, 0) < 0 && errno == EINTR) /* NOP */ ; tcsetattr(STDIN_FILENO, TCSANOW, &oldterm); tcsetattr(STDOUT_FILENO, TCSANOW, &oldterm); tcsetattr(STDERR_FILENO, TCSANOW, &oldterm); exit(WIFEXITED(status) ? WEXITSTATUS(status) : 127); } void echo_args(char **args) { starttime = NOW_USEC(); while(*args) { char *a = *args; while(*a) slow_write(1, *a++); args++; if(*args) slow_write(1, ' '); } slow_write(1, '\n'); if(debug) debug_stats(); exit(0); } void benchmark(char **args) { int bytes = 0, devnull; if(*args) bytes = atoi(*args); if(bytes < 1) bytes = 4096; fprintf(stderr, "benchmarking with %d bytes\n", bytes); devnull = open("/dev/null", O_RDONLY); if(devnull < 1) die("/dev/null"); starttime = NOW_USEC(); while(bytes--) slow_write(devnull, 0); close(devnull); debug_stats(); exit(0); } void usage(int exitstat) { printf("Usage: %s [] [ [ ...]]\n", self); printf(" With no filenames, reads stdin.\n"); printf("or: %s [] -c [ [ ...]]\n", self); printf(" With no command, spawns a shell.\n"); printf("or: %s [] -e [ ...]\n", self); printf("or: %s [] -b []\n", self); printf("With no , default rate is %d\n", DEFAULT_BPS); exit(exitstat); } int main(int argc, char **argv) { int infd; const char *p, *env_bps; if(getenv("SLOWBAUD_DEBUG")) debug = 1; /* for usage message, we want "slowbaud", not "/path/to/slowbaud" */ self = argv[0]; for(p = argv[0]; *p; p++) { if(*p == '/') self = p + 1; } argv++, argc--; /* set bps. command line option overrides env, env overrides builtin */ bps = DEFAULT_BPS; if( (env_bps = getenv("SLOWBAUD_BPS")) ) bps = atoi(env_bps); if(*argv && (**argv > '0' && **argv <= '9')) { char *e; long b = strtol(*argv, &e, 0); if(!*e) { bps = (int)b; argv++, argc--; } } if(bps < MIN_BPS || bps > MAX_BPS) { fprintf(stderr, "%s: invalid bps, range %d - %d\n", self, MIN_BPS, MAX_BPS); exit(1); } /* if we used only integer math here, we couldn't support bps not a multiple of 10 (e.g. 75 would be taken as 70). We still have a rounding problem: 115200bps is 86.81ms/char, but it gets rounded down to 86 here. Which would be around 116279bps, or almost 1% too fast. We never reach 100% speed anyway, so the loss from overhead actually offsets the extra bit of speed. */ interval = (unsigned long)(1000000.0L / ((double)bps / 10.0L)); #ifdef FRACTIONAL_USEC frac_us_num = (double)FRAC_US_DENOM * ((1000000.0L / ((double)bps / 10.0L) - interval)); if(debug) fprintf(stderr, "interval %ld + %d/%d us\n", interval, frac_us_num, FRAC_US_DENOM); // srandom(NOW_USEC()); // don't think we should do this #else if(debug) fprintf(stderr, "interval %ld us\n", interval); #endif if(*argv && argv[0][0] == '-' && argv[0][1] == '-' && !argv[0][2]) { if(debug) fprintf(stderr, "skipping -- argument\n"); argv++, argc--; } setup_timer(); if(!argc) { if(debug) fprintf(stderr, "no args, reading stdin\n"); slowcat(0); } else if(**argv == '-') { switch(argv[0][1]) { case 'c': if(debug) fprintf(stderr, "-c given, creating a pty\n"); slowtty(++argv); break; case 'e': if(argc < 2) die("-e requires at least one argument"); if(debug) fprintf(stderr, "-e given, echoing args\n"); echo_args(++argv); break; case 'b': benchmark(++argv); break; case 'h': case '?': usage(0); break; default: die("invalid option, try -h for help"); break; } } else { while(*argv) { if(debug) fprintf(stderr, "opening file %s for reading\n", *argv); if( (infd = open(*argv, O_RDONLY)) < 0) { die(*argv); } slowcat(infd); close(infd); argv++; } } exit(0); }