From 1b34f889f90cd879367f233c90d98b18dd47d338 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 14 Jan 2004 18:08:39 +0000 Subject: initial commit git-svn-id: file:///home/lennart/svn/public/bidilink/trunk@3 9cde1c1d-e4d0-0310-8a68-bf217395ea82 --- src/Makefile | 11 +++ src/bidilink.c | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/client-tcp.c | 66 ++++++++++++++++ src/client-tcp.h | 8 ++ src/client-tty.c | 55 ++++++++++++++ src/client-tty.h | 8 ++ src/client-unix.c | 46 +++++++++++ src/client-unix.h | 8 ++ src/exec.c | 67 ++++++++++++++++ src/exec.h | 8 ++ src/server-tcp.c | 84 +++++++++++++++++++++ src/server-tcp.h | 8 ++ src/server-tty.c | 69 +++++++++++++++++ src/server-tty.h | 8 ++ src/server-unix.c | 65 ++++++++++++++++ src/server-unix.h | 8 ++ src/std.c | 18 +++++ src/std.h | 8 ++ src/stream.c | 52 +++++++++++++ src/stream.h | 17 +++++ 20 files changed, 836 insertions(+) create mode 100644 src/Makefile create mode 100644 src/bidilink.c create mode 100644 src/client-tcp.c create mode 100644 src/client-tcp.h create mode 100644 src/client-tty.c create mode 100644 src/client-tty.h create mode 100644 src/client-unix.c create mode 100644 src/client-unix.h create mode 100644 src/exec.c create mode 100644 src/exec.h create mode 100644 src/server-tcp.c create mode 100644 src/server-tcp.h create mode 100644 src/server-tty.c create mode 100644 src/server-tty.h create mode 100644 src/server-unix.c create mode 100644 src/server-unix.h create mode 100644 src/std.c create mode 100644 src/std.h create mode 100644 src/stream.c create mode 100644 src/stream.h diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..d4ec88d --- /dev/null +++ b/src/Makefile @@ -0,0 +1,11 @@ +CFLAGS=-g -Wall -pipe + +all: bidilink + +bidilink: bidilink.o std.o exec.o stream.o client-tty.o server-tty.o client-tcp.o server-tcp.o client-unix.o server-unix.o + $(CC) $^ -o $@ + +clean: + rm -f *.o bidilink + +.PHONY: all clean diff --git a/src/bidilink.c b/src/bidilink.c new file mode 100644 index 0000000..3f421bf --- /dev/null +++ b/src/bidilink.c @@ -0,0 +1,222 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "stream.h" + +#define BUFSIZE (64*1024) + +volatile int quit = 0; + +static void signal_handler(int s) { + quit = 1; +} + +static void usage(FILE *f, const char *argv0) { + fprintf(f, "%s [STREAM1] [STREAM2]\n", argv0); +} + +static int read_fd(struct stream *s, void *buf, size_t *buf_fill, size_t *buf_index) { + ssize_t r; + assert(s && buf && buf_fill && buf_index); + + if ((r = read(s->input_fd, buf, BUFSIZE)) <= 0) { + + if (r == 0) + return 0; + + fprintf(stderr, "Failure to read(): %s\n", strerror(errno)); + return -1; + } + + *buf_fill = r; + *buf_index = 0; + return 1; +} + + +static int write_fd(struct stream *s, void *buf, size_t *buf_fill, size_t *buf_index) { + ssize_t r; + assert(s && buf && buf_fill && buf_index); + + if ((r = write(s->output_fd, buf+*buf_index, *buf_fill)) < 0) { + + if (r == EPIPE) + return 0; + + fprintf(stderr, "Failure to write(): %s\n", strerror(errno)); + return -1; + } + + *buf_fill -= r; + *buf_index += r; + return 1; +} + +const char *byte_str(uint64_t l) { + static char c[32]; + + if (l <= 1024) + snprintf(c, sizeof(c), "%llu B", l); + else if (l <= 1024*1024) + snprintf(c, sizeof(c), "%llu KB", l/1024); + else if (l <= 1024*1024*1024) + snprintf(c, sizeof(c), "%llu MB", l/1024/1024); + else + snprintf(c, sizeof(c), "%llu GB", l/1024/1024/1024); + + return c; +}; + +int main(int argc, char *argv[]) { + struct stream *a = NULL, *b = NULL; + + void *ab_buf = NULL, *ba_buf = NULL; + size_t ab_buf_fill = 0, ab_buf_index = 0, + ba_buf_fill = 0, ba_buf_index = 0; + + uint64_t ab_count = 0, ba_count = 0; + + int a_readable = 0, + a_writable = 0, + b_readable = 0, + b_writable = 0, + verbose = 0; + + int ret = -1; + int ai; + + if (argc < 2) { + usage(stderr, argv[0]); + ret = 0; + goto finish; + } + + signal(SIGINT, signal_handler); + siginterrupt(SIGINT, 1); + signal(SIGTERM, signal_handler); + siginterrupt(SIGTERM, 1); + signal(SIGPIPE, SIG_IGN); + + ai = 1; + + if (!strcmp(argv[ai], "-v")) { + verbose = 1; + ai++; + } + + if (!(a = stream_open(argv[ai++]))) + goto finish; + + if (!(b = stream_open(argc > ai ? argv[ai++] : NULL))) + goto finish; + + ab_buf = malloc(BUFSIZE); + assert(ab_buf); + ba_buf = malloc(BUFSIZE); + assert(ba_buf); + + for (;;) { + fd_set ifds, ofds; + + + FD_ZERO(&ifds); + + if (!a_readable) + FD_SET(a->input_fd, &ifds); + if (!b_readable) + FD_SET(b->input_fd, &ifds); + + FD_ZERO(&ofds); + + if (!a_writable) + FD_SET(a->output_fd, &ofds); + if (!b_writable) + FD_SET(b->output_fd, &ofds); + + if (select(FD_SETSIZE, &ifds, &ofds, NULL, 0) < 0) { + if (errno == EINTR) + continue; + + fprintf(stderr, "select() failed: %s\n", strerror(errno)); + ret = -1; + break; + } + + if (FD_ISSET(a->input_fd, &ifds)) + a_readable = 1; + if (FD_ISSET(b->input_fd, &ifds)) + b_readable = 1; + + if (FD_ISSET(a->output_fd, &ofds)) + a_writable = 1; + if (FD_ISSET(b->output_fd, &ofds)) + b_writable = 1; + + + if (!ab_buf_fill && a_readable && b_writable) { + int r; + if ((r = read_fd(a, ab_buf, &ab_buf_fill, &ab_buf_index)) <= 0) { + ret = r < 0 ? 1 : 0; + break; + } + a_readable = 0; + + ab_count += ab_buf_fill; + } + + if (ab_buf_fill && b_writable) { + int r; + if ((r = write_fd(b, ab_buf, &ab_buf_fill, &ab_buf_index)) <= 0) { + ret = r < 0 ? 1 : 0; + break; + } + b_writable = 0; + } + + if (!ba_buf_fill && b_readable && a_writable) { + int r; + if ((r = read_fd(b, ba_buf, &ba_buf_fill, &ba_buf_index)) <= 0) { + ret = r < 0 ? 1 : 0; + break; + } + b_readable = 0; + ba_count += ba_buf_fill; + } + + if (ba_buf_fill && a_writable) { + int r; + if ((r = write_fd(a, ba_buf, &ba_buf_fill, &ba_buf_index)) <= 0) { + ret = r < 0 ? 1 : 0; + break; + } + a_writable = 0; + } + + if (verbose) { + fprintf(stderr, "A: %s; ", byte_str(ab_count)); + fprintf(stderr, "B: %s \r", byte_str(ba_count)); + } + } + +finish: + + free(ab_buf); + free(ba_buf); + + if (a) + stream_close(a); + if (b) + stream_close(b); + + return ret; + +} diff --git a/src/client-tcp.c b/src/client-tcp.c new file mode 100644 index 0000000..78d8fc4 --- /dev/null +++ b/src/client-tcp.c @@ -0,0 +1,66 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "client-tcp.h" + +struct stream* stream_client_tcp(const char *args) { + struct stream *s = NULL; + int fd = -1; + struct sockaddr_in sa; + struct hostent *h; + size_t l; + uint16_t port = 23; + + char hn[256]; + l = strcspn(args, ":"); + if (l > sizeof(hn)-1) + l = sizeof(hn)-1; + strncpy(hn, args, l); + hn[l] = 0; + + if (args[l] == ':') + port = atoi(args+l+1); + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + if ((fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { + fprintf(stderr, "socket(): %s\n", strerror(errno)); + goto fail; + } + + memset(&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + if (!(h = gethostbyname(hn))) { + fprintf(stderr, "Unknown host '%s'.\n", hn); + goto fail; + } + sa.sin_addr = *(struct in_addr *) h->h_addr; + + if (connect(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) { + fprintf(stderr, "connect(): %s\n", strerror(errno)); + goto fail; + } + + s->input_fd = s->output_fd = fd; + return s; + +fail: + free(s); + if (fd >= 0) + close(fd); + + return NULL; +} diff --git a/src/client-tcp.h b/src/client-tcp.h new file mode 100644 index 0000000..53f06c1 --- /dev/null +++ b/src/client-tcp.h @@ -0,0 +1,8 @@ +#ifndef fooclienttcphfoo +#define fooclienttcphfoo + +#include "stream.h" + +struct stream* stream_client_tcp(const char *args); + +#endif diff --git a/src/client-tty.c b/src/client-tty.c new file mode 100644 index 0000000..e94d560 --- /dev/null +++ b/src/client-tty.c @@ -0,0 +1,55 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "client-tty.h" + +struct stream* stream_client_tty(const char *args) { + struct stream *s = NULL; + struct termios ts; + int fd = -1; + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + if ((fd = open(args, O_RDWR|O_NOCTTY)) < 0) { + fprintf(stderr, "open('%s', O_RDWR|O_NOCTTY): %s\n", args, strerror(errno)); + goto fail; + } + + if (tcgetattr(fd, &ts) < 0) { + fprintf(stderr, "tcgetattr(): %s\n", strerror(errno)); + goto fail; + } + + cfmakeraw(&ts); + + ts.c_cflag |= HUPCL | CS8 | CLOCAL; + ts.c_iflag |= IGNPAR | IGNBRK; + + ts.c_cc[VMIN] = 1; + ts.c_cc[VTIME] = 0; + + tcflush(fd, TCIOFLUSH); + + if (tcsetattr(fd, TCSANOW, &ts) < 0) { + fprintf(stderr, "tcsetattr(): %s\n", strerror(errno)); + goto fail; + } + + s->input_fd = s->output_fd = fd; + return s; + +fail: + free(s); + if (fd >= 0) + close(fd); + + return NULL; +} diff --git a/src/client-tty.h b/src/client-tty.h new file mode 100644 index 0000000..9befa7b --- /dev/null +++ b/src/client-tty.h @@ -0,0 +1,8 @@ +#ifndef fooclientttyhfoo +#define fooclientttyhfoo + +#include "stream.h" + +struct stream* stream_client_tty(const char *args); + +#endif diff --git a/src/client-unix.c b/src/client-unix.c new file mode 100644 index 0000000..04dd496 --- /dev/null +++ b/src/client-unix.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "client-unix.h" + +struct stream* stream_client_unix(const char *args) { + struct stream *s = NULL; + int fd = -1; + struct sockaddr_un sa; + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { + fprintf(stderr, "socket(): %s\n", strerror(errno)); + goto fail; + } + + memset(&sa, 0, sizeof(sa)); + sa.sun_family = AF_UNIX; + strncpy (sa.sun_path, args, sizeof(sa.sun_path)-1); + + if (connect(fd, (struct sockaddr *) &sa, SUN_LEN(&sa)) < 0) { + fprintf(stderr, "connect(): %s\n", strerror(errno)); + goto fail; + } + + s->input_fd = s->output_fd = fd; + return s; + +fail: + free(s); + if (fd >= 0) + close(fd); + + return NULL; +} diff --git a/src/client-unix.h b/src/client-unix.h new file mode 100644 index 0000000..bb17b74 --- /dev/null +++ b/src/client-unix.h @@ -0,0 +1,8 @@ +#ifndef fooclientunixhfoo +#define fooclientunixhfoo + +#include "stream.h" + +struct stream* stream_client_unix(const char *args); + +#endif diff --git a/src/exec.c b/src/exec.c new file mode 100644 index 0000000..db4345e --- /dev/null +++ b/src/exec.c @@ -0,0 +1,67 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "exec.h" + +static void close_pipe(int p[2]) { + if (p[0] >= 0) + close(p[0]); + + if (p[1] >= 0) + close(p[1]); +} + +struct stream* stream_exec(const char *args) { + struct stream *s = NULL; + int stdout_pipe[2]; + int stdin_pipe[2]; + pid_t pid; + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + if (pipe(stdin_pipe) < 0 || pipe(stdout_pipe) < 0) { + fprintf(stderr, "pipe(): %s\n", strerror(errno)); + goto fail; + } + + if ((pid = fork()) < 0) { + fprintf(stderr, "fork(): %s\n", strerror(errno)); + goto fail; + } else if (pid == 0) { + close(stdin_pipe[1]); + close(stdout_pipe[0]); + + if (dup2(stdin_pipe[0], 0) < 0 || dup2(stdout_pipe[1], 1) < 0) { + fprintf(stderr, "dup2(): %s\n", strerror(errno)); + exit(1); + } + + execl("/bin/sh", "/bin/sh", "-c", args, NULL); + + fprintf(stderr, "exec(): %s\n", strerror(errno)); + exit(1); + } + + s->output_fd = stdin_pipe[1]; + close(stdin_pipe[0]); + s->input_fd = stdout_pipe[0]; + close(stdout_pipe[1]); + + return s; + +fail: + + free(s); + + close_pipe(stdout_pipe); + close_pipe(stdin_pipe); + + return NULL; +} diff --git a/src/exec.h b/src/exec.h new file mode 100644 index 0000000..a657614 --- /dev/null +++ b/src/exec.h @@ -0,0 +1,8 @@ +#ifndef fooexechfoo +#define fooexechfoo + +#include "stream.h" + +struct stream* stream_exec(const char *args); + +#endif diff --git a/src/server-tcp.c b/src/server-tcp.c new file mode 100644 index 0000000..2a2e125 --- /dev/null +++ b/src/server-tcp.c @@ -0,0 +1,84 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "server-tcp.h" + +struct stream* stream_server_tcp(const char *args) { + struct stream *s = NULL; + int fd = -1, cfd; + struct sockaddr_in sa; + struct hostent *h; + size_t l; + uint16_t port = 23; + char hn[256] = ""; + + l = strcspn(args, ":"); + if (l > sizeof(hn)-1) + l = sizeof(hn)-1; + + if (args[l] == ':') { + strncpy(hn, args, l); + hn[l] = 0; + + port = atoi(args+l+1); + } else + port = atoi(args); + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + if ((fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) { + fprintf(stderr, "socket(): %s\n", strerror(errno)); + goto fail; + } + + memset(&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons(port); + if (hn[0]) { + if (!(h = gethostbyname(hn))) { + fprintf(stderr, "Unknown host '%s'.\n", hn); + goto fail; + } + sa.sin_addr = *(struct in_addr *) h->h_addr; + } else + sa.sin_addr.s_addr = htonl(INADDR_ANY); + + if (bind(fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) { + fprintf(stderr, "bind(): %s\n", strerror(errno)); + goto fail; + } + + if (listen(fd, 1) < 0) { + fprintf(stderr, "listen(): %s\n", strerror(errno)); + goto fail; + } + + if ((cfd = accept(fd, NULL, 0)) < 0) { + fprintf(stderr, "accept(): %s\n", strerror(errno)); + goto fail; + } + + close(fd); + s->input_fd = s->output_fd = cfd; + + return s; + +fail: + free(s); + if (fd >= 0) + close(fd); + + return NULL; +} diff --git a/src/server-tcp.h b/src/server-tcp.h new file mode 100644 index 0000000..f8b2849 --- /dev/null +++ b/src/server-tcp.h @@ -0,0 +1,8 @@ +#ifndef fooservertcphfoo +#define fooservertcphfoo + +#include "stream.h" + +struct stream* stream_server_tcp(const char *args); + +#endif diff --git a/src/server-tty.c b/src/server-tty.c new file mode 100644 index 0000000..d3f36b1 --- /dev/null +++ b/src/server-tty.c @@ -0,0 +1,69 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include + +#include "server-tty.h" + +static void destruct(struct stream *s) { + assert(s && s->user); + if (unlink((char*) s->user) < 0) + fprintf(stderr, "WARNING: unlink(): %s\n", strerror(errno)); + free(s->user); +} + +struct stream* stream_server_tty(const char *args) { + struct stream *s = NULL; + int fd = -1; + char *n; + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + if ((fd = open("/dev/ptmx", O_RDWR|O_NOCTTY)) < 0) { + fprintf(stderr, "open('/dev/ptmx', O_RDWR|O_NOCTTY): %s\n", strerror(errno)); + goto fail; + } + + if (grantpt(fd) < 0) { + fprintf(stderr, "grantpt(): %s\n", strerror(errno)); + goto fail; + } + + if (unlockpt(fd) < 0) { + fprintf(stderr, "grantpt(): %s\n", strerror(errno)); + goto fail; + } + + if (!(n = ptsname(fd)) < 0) { + fprintf(stderr, "ptsname(): %s\n", strerror(errno)); + goto fail; + } + + if (args) { + if (symlink(n, args) < 0) { + fprintf(stderr, "symlink('%s', '%s'): %s\n", n, args, strerror(errno)); + goto fail; + } + } else + fprintf(stderr, "Allocated pseudo tty '%s'.\n", n); + + s->user = strdup(args); + s->destruct = destruct; + s->input_fd = s->output_fd = fd; + return s; + +fail: + free(s); + if (fd >= 0) + close(fd); + + return NULL; + +} diff --git a/src/server-tty.h b/src/server-tty.h new file mode 100644 index 0000000..626a55f --- /dev/null +++ b/src/server-tty.h @@ -0,0 +1,8 @@ +#ifndef fooserverttyhfoo +#define fooserverttyhfoo + +#include "stream.h" + +struct stream* stream_server_tty(const char *args); + +#endif diff --git a/src/server-unix.c b/src/server-unix.c new file mode 100644 index 0000000..36f1a4c --- /dev/null +++ b/src/server-unix.c @@ -0,0 +1,65 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "server-unix.h" + +struct stream* stream_server_unix(const char *args) { + struct stream *s = NULL; + int fd = -1, cfd; + struct sockaddr_un sa; + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) { + fprintf(stderr, "socket(): %s\n", strerror(errno)); + goto fail; + } + + memset(&sa, 0, sizeof(sa)); + sa.sun_family = AF_UNIX; + strncpy (sa.sun_path, args, sizeof(sa.sun_path)-1); + + if (bind(fd, (struct sockaddr *) &sa, SUN_LEN(&sa)) < 0) { + fprintf(stderr, "bind(): %s\n", strerror(errno)); + goto fail; + } + + if (listen(fd, 1) < 0) { + fprintf(stderr, "listen(): %s\n", strerror(errno)); + goto fail; + } + + if ((cfd = accept(fd, NULL, 0)) < 0) { + fprintf(stderr, "accept(): %s\n", strerror(errno)); + goto fail; + } + + close(fd); + + if (unlink(args) < 0) + fprintf(stderr, "WARNING: unlink('%s'): %s\n", args, strerror(errno)); + + s->input_fd = s->output_fd = cfd; + + return s; + +fail: + free(s); + + if (fd >= 0) + close(fd); + + unlink(args); + + return NULL; +} diff --git a/src/server-unix.h b/src/server-unix.h new file mode 100644 index 0000000..aa8693f --- /dev/null +++ b/src/server-unix.h @@ -0,0 +1,8 @@ +#ifndef fooserverunixhfoo +#define fooserverunixhfoo + +#include "stream.h" + +struct stream* stream_server_unix(const char *args); + +#endif diff --git a/src/std.c b/src/std.c new file mode 100644 index 0000000..b0cc8a9 --- /dev/null +++ b/src/std.c @@ -0,0 +1,18 @@ +#include +#include +#include + +#include "std.h" + +struct stream* stream_std(const char *args) { + struct stream *s; + + s = malloc(sizeof(struct stream)); + assert(s); + memset(s, 0, sizeof(struct stream)); + + s->input_fd = 0; + s->output_fd = 1; + + return s; +} diff --git a/src/std.h b/src/std.h new file mode 100644 index 0000000..1c2af1c --- /dev/null +++ b/src/std.h @@ -0,0 +1,8 @@ +#ifndef foostdhfoo +#define foostdhfoo + +#include "stream.h" + +struct stream* stream_std(const char *args); + +#endif diff --git a/src/stream.c b/src/stream.c new file mode 100644 index 0000000..80ad2c2 --- /dev/null +++ b/src/stream.c @@ -0,0 +1,52 @@ +#include +#include +#include +#include +#include + +#include "stream.h" +#include "std.h" +#include "exec.h" +#include "client-tty.h" +#include "server-tty.h" +#include "client-tcp.h" +#include "server-tcp.h" +#include "client-unix.h" +#include "server-unix.h" + +struct stream* stream_open(const char *spec) { + if (!spec || !strncmp(spec, "std:",4)) + return stream_std(spec+4); + else if (!strncmp(spec, "exec:", 5)) + return stream_exec(spec+5); + else if (!strncmp(spec, "tty:", 4)) + return stream_client_tty(spec+4); + else if (!strncmp(spec, "pty:", 4)) + return stream_server_tty(spec+4); + else if (!strncmp(spec, "tcp-client:", 11)) + return stream_client_tcp(spec+11); + else if (!strncmp(spec, "tcp-server:", 11)) + return stream_server_tcp(spec+11); + else if (!strncmp(spec, "unix-client:", 12)) + return stream_client_unix(spec+12); + else if (!strncmp(spec, "unix-server:", 12)) + return stream_server_unix(spec+12); + + fprintf(stderr, "Found no stream implementation for '%s'.\n", spec); + return NULL; +} + + +void stream_close(struct stream *s) { + assert(s); + + if (s->destruct) + s->destruct(s); + + if (s->input_fd != -1) + close(s->input_fd); + if (s->output_fd != -1 && s->input_fd != s->output_fd) + close(s->output_fd); + + free(s); +} diff --git a/src/stream.h b/src/stream.h new file mode 100644 index 0000000..29bf46e --- /dev/null +++ b/src/stream.h @@ -0,0 +1,17 @@ +#ifndef foostreamhfoo +#define foostreamhfoo + + +struct stream { + int input_fd, output_fd; + + void (*destruct) (struct stream *s); + + void* user; +}; + +struct stream* stream_open(const char *sspec); +void stream_close(struct stream *s); + + +#endif -- cgit