summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorLuke Shumaker <lukeshu@sbcglobal.net>2016-04-08 17:00:05 -0400
committerLuke Shumaker <lukeshu@sbcglobal.net>2016-04-08 17:00:05 -0400
commit41bfc4bb5e1042ec9eca48ed645393b12978d703 (patch)
tree094cf68df41f7f5c98a1c844e51bc32fb0e7155d /src
parent608f4f254ccf95a01b5480e2dbe7a1b6e54c8609 (diff)
Actual install process
Diffstat (limited to 'src')
-rw-r--r--src/.gitignore5
-rw-r--r--src/freenect-server.c289
-rw-r--r--src/multipart-replace-http-server.c367
-rw-r--r--src/multipart-replace.c168
-rw-r--r--src/multipart-replace.h26
-rw-r--r--src/util.c72
-rw-r--r--src/util.h21
-rw-r--r--src/wg.c45
-rw-r--r--src/wg.h19
9 files changed, 1012 insertions, 0 deletions
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000..bf84729
--- /dev/null
+++ b/src/.gitignore
@@ -0,0 +1,5 @@
+/freenect-server
+/multipart-replace-http-server
+
+*.o
+.*.mk
diff --git a/src/freenect-server.c b/src/freenect-server.c
new file mode 100644
index 0000000..c5ed99a
--- /dev/null
+++ b/src/freenect-server.c
@@ -0,0 +1,289 @@
+/*
+ * This file is part of the OpenKinect Project. http://www.openkinect.org
+ *
+ * Copyright (c) 2010 Brandyn White (bwhite@dappervision.com)
+ * Copyright (c) 2016 Luke Shumaker (lukeshu@sbcglobal.net)
+ *
+ * This code is licensed to you under the terms of the Apache License, version
+ * 2.0, or, at your option, the terms of the GNU General Public License,
+ * version 2.0. See the APACHE20 and GPL2 files for the text of the licenses,
+ * or the following URLs:
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.gnu.org/licenses/gpl-2.0.txt
+ *
+ * If you redistribute this file in source form, modified or unmodified, you
+ * may:
+ * 1) Leave this header intact and distribute it under the same terms,
+ * accompanying it with the APACHE20 and GPL20 files, or
+ * 2) Delete the Apache 2.0 clause and accompany it with the GPL2 file, or
+ * 3) Delete the GPL v2 clause and accompany it with the APACHE20 file
+ * In all cases you must keep the copyright notice intact and include a copy
+ * of the CONTRIB file.
+ *
+ * Binary distributions must follow the binary distribution requirements of
+ * either License.
+ */
+#include <errno.h>
+#include <error.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h> /* atexit */
+#include <unistd.h> /* dup2 */
+
+#include <libfreenect/libfreenect.h>
+#include <libusb-1.0/libusb.h>
+
+#include <jpeglib.h>
+
+#include "util.h"
+
+FILE *depth_stream = NULL;
+FILE *video_stream = NULL;
+FILE *accel_stream = NULL;
+freenect_context *ctx;
+
+struct jpeg_error_mgr jpeg_encoder_err;
+struct jpeg_compress_struct jpeg_encoder;
+unsigned char *jpeg_encoder_buf;
+unsigned long jpeg_encoder_len;
+
+void jpeg_init() {
+ jpeg_encoder.err = jpeg_std_error(&jpeg_encoder_err);
+ jpeg_create_compress(&jpeg_encoder);
+ jpeg_encoder.image_width = 640;
+ jpeg_encoder.image_height = 480;
+ jpeg_encoder.input_components = 3;
+ jpeg_encoder.in_color_space = JCS_RGB;
+ jpeg_set_defaults(&jpeg_encoder);
+ jpeg_mem_dest(&jpeg_encoder, &jpeg_encoder_buf, &jpeg_encoder_len);
+}
+
+void write_imageframe(FILE *stream, JSAMPLE *data) {
+ JSAMPLE *row_pointers[480];
+ for (size_t i = 0; i < 480; i++)
+ row_pointers[i] = &data[i*640*3];
+
+ jpeg_start_compress(&jpeg_encoder, TRUE);
+ while (jpeg_encoder.next_scanline < jpeg_encoder.image_height)
+ jpeg_write_scanlines(&jpeg_encoder, &row_pointers[jpeg_encoder.next_scanline], 480-jpeg_encoder.next_scanline);
+ jpeg_finish_compress(&jpeg_encoder);
+
+ if (fprintf(stream,
+ "--ffserver\r\n"
+ "Content-type: image/jpeg\r\n"
+ "Content-length: %lu\r\n"
+ "\r\n",
+ jpeg_encoder_len) < 1)
+ error(EXIT_FAILURE, ferror(stream), "writing header");
+ if (fwrite(jpeg_encoder_buf, jpeg_encoder_len, 1, stream) < 1)
+ error(EXIT_FAILURE, ferror(stream), "writing body");
+ if (fwrite("\r\n", 2, 1, stream) < 1)
+ error(EXIT_FAILURE, ferror(stream), "writing footer");
+ fflush(stream);
+}
+
+void handle_accel(freenect_device *dev UNUSED, freenect_raw_tilt_state* data) {
+ double x, y, z, angle;
+ freenect_get_mks_accel(data, &x, &y, &z);
+ angle = freenect_get_tilt_degs(data);
+
+ const char *motor;
+ switch (data->tilt_status) {
+ case TILT_STATUS_STOPPED:
+ motor = "stopped";
+ break;
+ case TILT_STATUS_LIMIT:
+ motor = "limit";
+ break;
+ case TILT_STATUS_MOVING:
+ motor = "moving";
+ break;
+ default:
+ motor = "unknown";
+ }
+
+ char *json = NULL;
+ int len = asprintf(&json,
+ "{ \"x\": %f,\n"
+ " \"y\": %f,\n"
+ " \"z\": %f,\n"
+ " \"angle\": %f,\n"
+ " \"motor\": \"%s\" }\n",
+ x, y, z, angle, motor);
+ if (len < 0)
+ error(EXIT_FAILURE, errno, "asprintf");
+ fprintf(accel_stream,
+ "--ffserver\r\n"
+ "Content-type: application/json\r\n"
+ "Content-length: %d\r\n"
+ "\r\n"
+ "%s\r\n",
+ len, json);
+ fflush(accel_stream);
+}
+
+uint8_t depth_rgb[640*480*3];
+void handle_depth(freenect_device *dev, void *depth, uint32_t timestamp UNUSED) {
+ uint32_t size = freenect_get_current_depth_mode(dev).bytes;
+ if (size != 640*480*2)
+ error(EXIT_FAILURE, 0, "handle_depth: expected 640*480*2 byte frame, but got %"PRId32, size);
+ /* scale the 11-bit values into 8-bit values */
+ uint16_t *depth_grey = depth;
+ for (size_t i = 0; i < 640*480; i++)
+ depth_rgb[i*3+0] = depth_rgb[i*3+1] = depth_rgb[i*3+2] = (uint8_t)(depth_grey[i]*8.0/11.0);
+ /* write the image */
+ write_imageframe(depth_stream, depth_rgb);
+}
+
+void handle_video(freenect_device *dev, void *rgb, uint32_t timestamp UNUSED) {
+ uint32_t size = freenect_get_current_video_mode(dev).bytes;
+ if (size != 640*480*3)
+ error(EXIT_FAILURE, 0, "handle_video: expected 640*480*3 byte frame, but got %"PRId32, size);
+ /* write the image */
+ write_imageframe(video_stream, rgb);
+}
+
+void print_mode(const char *name, freenect_frame_mode mode) {
+ /* This is just a courtesy function to let the user know the mode
+ if it becomes a bother for maintainability just comment out the
+ code in its body. It will only break if struct entries go missing.
+ */
+ printf("%s Mode: {%d, %d, {%d}, %d, %d, %d, %d, %d, %d, %d}\n", name,
+ mode.reserved, (int)mode.resolution, (int)mode.video_format, mode.bytes, mode.width,
+ mode.height, mode.data_bits_per_pixel, mode.padding_bits_per_pixel,
+ mode.framerate, mode.is_valid);
+}
+
+void cleanup() {
+ log("STOPPING=1");
+ if (ctx)
+ freenect_shutdown(ctx);
+ if (video_stream)
+ fclose(video_stream);
+ if (depth_stream)
+ fclose(depth_stream);
+ if (accel_stream)
+ fclose(accel_stream);
+ fflush(stderr);
+ sleep(5); /* work around systemd bug dropping log messages */
+}
+
+FILE *xfopen(const char *path, const char *mode) {
+ FILE *stream = fopen(path, mode);
+ if (stream == NULL)
+ error(EXIT_FAILURE, errno, "fopen: %s", path);
+ return stream;
+}
+
+FILE *xfdopen(const char *path, const char *mode) {
+ int fd = get_fd(path);
+ if (fd < 0)
+ error(EXIT_FAILURE, -fd, "get_fd: %s", path);
+ FILE *stream = fdopen(fd, mode);
+ if (stream == NULL)
+ error(EXIT_FAILURE, errno, "fdopen: %s (%d)", path, fd);
+ return stream;
+}
+
+void usage() {
+ printf("Usage: %s [-h] [-v video.mjpg|-V video_fd] [-d depth.mjpg|-D depth_fd] [-a accel.mjson|-A accel_fd]\n", program_invocation_name);
+}
+
+int main(int argc, char *argv[]) {
+ int res = 0;
+ freenect_device *dev;
+
+ atexit(cleanup);
+
+ for (int i = 1; i < argc; i++) {
+ if (argv[i][0] == '-' && argv[i][1] != '\0' && argv[i][2] == '\0') {
+ switch (argv[i][1]) {
+ case 'h':
+ usage();
+ return EXIT_SUCCESS;
+ case 'v':
+ i++;
+ video_stream = xfopen(argv[i], "w");
+ break;
+ case 'd':
+ i++;
+ depth_stream = xfopen(argv[i], "w");
+ break;
+ case 'a':
+ i++;
+ accel_stream = xfopen(argv[i], "w");
+ break;
+ case 'V':
+ i++;
+ video_stream = xfdopen(argv[i], "w");
+ break;
+ case 'D':
+ i++;
+ depth_stream = xfdopen(argv[i], "w");
+ break;
+ case 'A':
+ i++;
+ accel_stream = xfdopen(argv[i], "w");
+ break;
+ default:
+ dup2(2, 1);
+ usage();
+ return EXIT_FAILURE;
+ }
+ } else {
+ dup2(2, 1);
+ usage();
+ return EXIT_FAILURE;
+ }
+ }
+ if (!(video_stream || depth_stream || accel_stream)) {
+ dup2(2, 1);
+ usage();
+ return EXIT_FAILURE;
+ }
+
+ jpeg_init();
+
+ res = freenect_init(&ctx, 0);
+ if (res) {
+ error(EXIT_FAILURE, 0, "freenect_init: %s", libusb_strerror(res));
+ }
+
+ freenect_device_flags devices = 0;
+ if (video_stream || depth_stream)
+ devices |= FREENECT_DEVICE_CAMERA;
+ if (accel_stream)
+ devices |= FREENECT_DEVICE_MOTOR;
+
+ freenect_select_subdevices(ctx, devices);
+ if ((res = freenect_open_device(ctx, &dev, 0))) {
+ error(EXIT_FAILURE, 0, "freenect_open_device: %s", libusb_strerror(res));
+ }
+
+ if (video_stream) {
+ print_mode("Video", freenect_find_video_mode(FREENECT_RESOLUTION_MEDIUM, FREENECT_VIDEO_RGB));
+ freenect_set_video_mode(dev, freenect_find_video_mode(FREENECT_RESOLUTION_MEDIUM, FREENECT_VIDEO_RGB));
+ freenect_start_video(dev);
+ freenect_set_video_callback(dev, handle_video);
+ }
+
+ if (depth_stream) {
+ print_mode("Depth", freenect_find_depth_mode(FREENECT_RESOLUTION_MEDIUM, FREENECT_DEPTH_11BIT));
+ freenect_set_depth_mode(dev, freenect_find_depth_mode(FREENECT_RESOLUTION_MEDIUM, FREENECT_DEPTH_11BIT));
+ freenect_start_depth(dev);
+ freenect_set_depth_callback(dev, handle_depth);
+ }
+
+ while (freenect_process_events(ctx) >= 0) {
+ if (accel_stream) {
+ freenect_raw_tilt_state* state;
+ freenect_update_tilt_state(dev);
+ state = freenect_get_tilt_state(dev);
+ handle_accel(dev, state);
+ }
+ }
+
+ freenect_stop_depth(dev);
+ freenect_stop_video(dev);
+ freenect_close_device(dev);
+}
diff --git a/src/multipart-replace-http-server.c b/src/multipart-replace-http-server.c
new file mode 100644
index 0000000..726714f
--- /dev/null
+++ b/src/multipart-replace-http-server.c
@@ -0,0 +1,367 @@
+/* Copyright 2016 Luke Shumaker */
+
+#include <errno.h>
+#include <error.h>
+#include <fcntl.h> /* for open */
+#include <netdb.h> /* for {get,free}addrinfo() */
+#include <stdio.h>
+#include <stdlib.h> /* atexit */
+#include <stdlib.h> /* for EXIT_FAILURE */
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h> /* for open */
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "util.h"
+#include "wg.h"
+#include "multipart-replace.h"
+
+struct httpfile {
+ char name[256];
+ struct multipart_replace_stream *stream;
+};
+
+size_t filec = 0;
+struct httpfile *filev = NULL;
+
+struct reader_thread_args {
+ struct multipart_replace_stream *stream;
+ int fd;
+ const char *filename;
+ const char *boundary;
+};
+
+
+bool running = true;
+int sock;
+struct wg wg;
+
+void stop(int sig) {
+ log("Caught %d", sig);
+ log("STOPPING=1");
+ running = false;
+ close(sock);
+}
+
+#define threaderror(stat, errnum, ...) do { error(0, errnum, __VA_ARGS__); stop(0); } while(0)
+
+void *reader_thread(void *args_anon) {
+ struct reader_thread_args *args = args_anon;
+
+ char *name = alloca(strlen("read ")+strlen(args->filename)+1);
+ strcpy(name, "read ");
+ strcpy(&name[strlen("read ")], args->filename);
+ pthread_setname_np(pthread_self(), name);
+ debug("starting thread: %s", name);
+
+ multipart_replace_reader(args->stream, args->fd, args->boundary);
+ sleep(5);
+ threaderror(EXIT_FAILURE, 0, "multipart_replace stream ended: %s", args->filename);
+ wg_sub(&wg, 1);
+ return NULL;
+}
+
+void start_multipart_replace_reader(struct multipart_replace_stream *s, int fd, const char *filename, const char *boundary) {
+ struct reader_thread_args *args = xrealloc(NULL, sizeof(struct reader_thread_args));
+ args->stream = s;
+ args->fd = fd;
+ args->filename = strdup(filename);
+ args->boundary = boundary;
+ pthread_t thread;
+ wg_add(&wg, 1);
+ pthread_create(&thread, NULL, reader_thread, args);
+}
+
+void file_add(const char *filename) {
+ struct multipart_replace_stream *stream = xrealloc(NULL, sizeof(struct multipart_replace_stream));
+ init_multipart_replace_stream(stream);
+ int fd = open(filename, O_RDONLY);
+ if (fd < 0) {
+ threaderror(EXIT_FAILURE, errno, "opening file: %s", filename);
+ return;
+ }
+ filev = xrealloc(filev, (++filec)*sizeof(*filev));
+ char *shortname = strrchr(filename, '/');
+ if (shortname == NULL) {
+ shortname = alloca(strlen(filename)+2);
+ shortname[0] = '/';
+ strcpy(&shortname[1], filename);
+ }
+ strncpy(filev[filec-1].name, shortname, sizeof(filev[filec-1].name));
+ log("added file #%zd: %s", filec-1, filev[filec-1].name);
+ filev[filec-1].stream = stream;
+ start_multipart_replace_reader(stream, fd, filev[filec-1].name, "ffserver" /* FIXME */);
+}
+
+struct multipart_replace_stream *file_get(const char *filename) {
+ for (size_t i = 0; i < filec; i++) {
+ if (strncmp(filename, filev[i].name, sizeof(filev[i].name)) == 0) {
+ return filev[i].stream;
+ }
+ }
+ return NULL;
+}
+
+void connection_handler(int fd) {
+ FILE *netstream = fdopen(fd, "r");
+ char *line_buf = NULL;
+ size_t line_cap = 0;
+ ssize_t line_len = 0;
+
+ line_len = getline(&line_buf, &line_cap, netstream);
+ /* expect line to be "GET /path.mjpg HTTP/1.1" */
+ if (strncmp(line_buf, "GET ", 4) != 0) {
+ dprintf(fd,
+ "HTTP/1.1 405 Method Not Allowed\r\n"
+ "Allow: GET\r\n"
+ "\r\n");
+ close(fd);
+ return;
+ }
+ char *version = &line_buf[4];
+ char *path = strsep(&version, " ");
+ if (strcmp(version, "HTTP/1.1\r\n") != 0 && strcmp(version, "HTTP/1.0\r\n")) {
+ close(fd);
+ return;
+ }
+ path = strdupa(path);
+
+ /* run through the rest of the headers */
+ while (strcmp(line_buf, "\r\n") != 0 && line_len >= 0)
+ line_len = getline(&line_buf, &line_cap, netstream);
+
+ if (strcmp(path, "/") == 0) {
+ log("200 %s", path);
+ dprintf(fd,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: text/html; charset=utf-8\r\n"
+ "\r\n"
+ "<!DOCTYPE html>\n"
+ "<html>\n"
+ "<head>\n"
+ " <title>multipart/x-mixed-replace HTTP server</title>\n"
+ "</head>\n"
+ "<body>\n"
+ " <ul>\n"
+ );
+ for (size_t i = 0; i < filec; i++)
+ dprintf(fd, " <li><a href=\"%s\">%s</a></li>\n", filev[i].name, filev[i].name);
+ dprintf(fd,
+ " </ul>\n"
+ "</body>\n"
+ "</html>\n");
+ close(fd);
+ return;
+ }
+ struct multipart_replace_stream *vidstream = file_get(path);
+ if (vidstream == NULL) {
+ log("404 %s", path);
+ dprintf(fd,
+ "HTTP/1.1 404 Not Found\r\n"
+ "\r\n");
+ return;
+ }
+
+ const char *boundary = "boundary" /* FIXME */;
+ log("200 %s", path);
+ dprintf(fd,
+ "HTTP/1.1 200 OK\r\n"
+ "Content-Type: multipart/x-mixed-replace;boundary=%s\r\n"
+ "\r\n",
+ boundary);
+ multipart_replace_writer(vidstream, fd, boundary);
+ close(fd);
+}
+
+void *connection_thread(void *arg_anon) {
+ pthread_setname_np(pthread_self(), "connection");
+ int fd = (int)(intptr_t)arg_anon;
+ log("Connection %d opened", fd);
+ connection_handler(fd);
+ log("Connection %d closed", fd);
+ wg_sub(&wg, 1);
+ return NULL;
+}
+
+
+/* same error codes values as -getaddrinfo(); */
+int sockstream_listen(const char *type, const char *addr) {
+ int sock;
+ if (strcmp(type, "fd") == 0) {
+ sock = get_fd(addr);
+ if (sock < 0) {
+ errno = -sock;
+ return -EAI_SYSTEM;
+ }
+ /* make sure it's a socket */
+ struct stat st;
+ if (fstat(sock, &st) < 0)
+ return -EAI_SYSTEM;
+ if (!S_ISSOCK(st.st_mode)) {
+ errno = ENOTSOCK;
+ return -EAI_SYSTEM;
+ }
+ /* make sure the socket is a stream */
+ int fd_socktype = 0;
+ socklen_t l = sizeof(fd_socktype);
+ if (getsockopt(sock, SOL_SOCKET, SO_TYPE, &fd_socktype, &l) < 0)
+ return -EAI_SYSTEM;
+ if (l != sizeof(fd_socktype)) {
+ errno = EINVAL;
+ return -EAI_SYSTEM;
+ }
+ if (fd_socktype != SOCK_STREAM) {
+ errno = ENOSTR;
+ return -EAI_SYSTEM;
+ }
+ /* make sure the socket is listening */
+ int fd_listening = 0;
+ l = sizeof(fd_listening);
+ if (getsockopt(sock, SOL_SOCKET, SO_ACCEPTCONN, &fd_listening, &l) < 0)
+ return -EAI_SYSTEM;
+ if (l != sizeof(fd_listening)) {
+ errno = EINVAL;
+ return -EAI_SYSTEM;
+ }
+ if (!fd_listening)
+ listen(sock, SOMAXCONN);
+ /* return the socket */
+ return sock;
+ } else if (strcmp(type, "unix") == 0) {
+ struct sockaddr_un un_addr = { 0 };
+ if (strlen(addr)+1 > sizeof(un_addr.sun_path)) {
+ errno = ENAMETOOLONG;
+ return -EAI_SYSTEM;
+ }
+ un_addr.sun_family = AF_UNIX;
+ strcpy(un_addr.sun_path, addr);
+ int sock = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sock < 0)
+ return -EAI_SYSTEM;
+ unlink(addr);
+ if (bind(sock, (struct sockaddr *)&un_addr, sizeof(un_addr)) < 0)
+ return -EAI_SYSTEM;
+ if (listen(sock, SOMAXCONN) < 0)
+ return -EAI_SYSTEM;
+ return sock;
+ } else if (strcmp(type, "tcp4") == 0 || strcmp(type, "tcp6") == 0) {
+ struct addrinfo *ai_addr = NULL;
+ struct addrinfo ai_hints = { 0 };
+
+ char *host = strdupa(addr);
+ char *col = strrchr(host, ':');
+ if (col == NULL) {
+ errno = EINVAL;
+ return -EAI_SYSTEM;
+ }
+ char *port = &col[1];
+ *col = '\0';
+ if (host[0] == '\0')
+ host = NULL;
+
+ ai_hints.ai_family = (strcmp(type, "tcp6") == 0) ? AF_INET6 : AF_INET;
+ ai_hints.ai_socktype = SOCK_STREAM;
+ ai_hints.ai_flags = AI_PASSIVE;
+
+ int r = getaddrinfo(host, port, &ai_hints, &ai_addr);
+ if (r < 0)
+ return r;
+ if (r > 0)
+ return -r;
+
+ int sock = socket(ai_addr->ai_family, ai_addr->ai_socktype, ai_addr->ai_protocol);
+ if (sock < 0)
+ return -EAI_SYSTEM;
+
+ int yes = 1;
+ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
+
+ if (bind(sock, ai_addr->ai_addr, ai_addr->ai_addrlen) < 0)
+ return -EAI_SYSTEM;
+
+ if (listen(sock, SOMAXCONN) < 0)
+ return -EAI_SYSTEM;
+
+ freeaddrinfo(ai_addr);
+ return sock;
+ } else {
+ errno = EINVAL;
+ return -EAI_SYSTEM;
+ }
+}
+
+void usage() {
+ printf("Usage: %s [-h] ADDRTYPE ADDR [FILENAME...]\n", program_invocation_name);
+ printf("Multiplex several multipart/x-mixed-replace streams over HTTP.\n"
+ "\n"
+ "ADDRTYPE is \"tcp4\", \"tcp6\", \"fd\", or \"unix\". For tcp4/6, ADDR\n"
+ "is a socket address in standard notation. For fd, ADDR is \"stdin\",\n"
+ "\"stdout\", \"stder\", \"systemd\", or an unsigned integer. For unix,\n"
+ "ADDR is a file path.\n"
+ "\n"
+ "The leading directories of the FILENAMEs are stripped; they are served\n"
+ "at just the base filename. As common sense should eventually suggest,\n"
+ "the FILENAMEs should be pipes or sockets.\n");
+}
+
+void cleanup() {
+ fflush(stderr);
+ sleep(5); /* work around systemd bug dropping log messages */
+}
+
+pthread_t main_thread;
+
+void progname() {
+ char threadname[16];
+ if (pthread_self() != main_thread && pthread_getname_np(pthread_self(), threadname, sizeof(threadname)) == 0)
+ fprintf(stderr, "%s:%s: ", program_invocation_name, threadname);
+ else
+ fprintf(stderr, "%s: ", program_invocation_name);
+}
+
+int main(int argc, char *argv[]) {
+ if (argc >=2 && strcmp(argv[1], "-h") == 0) {
+ usage();
+ return EXIT_SUCCESS;
+ }
+ if (argc < 3) {
+ dup2(2, 1);
+ usage();
+ return EXIT_FAILURE;
+ }
+
+ main_thread = pthread_self();
+ error_print_progname = progname;
+ atexit(cleanup);
+
+ sock = sockstream_listen(argv[1], argv[2]);
+ if (sock < 0) {
+ if (sock == -EAI_SYSTEM)
+ error(EXIT_FAILURE, errno, "Opening socket %s %s", argv[1], argv[2]);
+ else
+ error(EXIT_FAILURE, 0, "Opening socket %s %s: %s", argv[1], argv[2], gai_strerror(-sock));
+ }
+
+ wg_init(&wg);
+ for (int i = 3; i < argc; i++)
+ file_add(argv[i]);
+
+ signal(SIGPIPE, SIG_IGN);
+ signal(SIGTERM, stop);
+ signal(SIGQUIT, stop);
+ signal(SIGINT, stop);
+ while (running) {
+ int conn = accept(sock, NULL, NULL);
+ if (conn < 0)
+ continue;
+ pthread_t thread;
+ wg_add(&wg, 1);
+ pthread_create(&thread, NULL, connection_thread, (void*)(intptr_t)conn);
+ }
+ wg_wait(&wg);
+ for (size_t i = 0; i < filec; i++)
+ destroy_multipart_replace_stream(filev[i].stream);
+ free(filev);
+ return 0;
+}
diff --git a/src/multipart-replace.c b/src/multipart-replace.c
new file mode 100644
index 0000000..9a5e268
--- /dev/null
+++ b/src/multipart-replace.c
@@ -0,0 +1,168 @@
+/* Copyright 2016 Luke Shumaker */
+
+#include <errno.h>
+#include <error.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "multipart-replace.h"
+#include "util.h"
+
+#define error(stat, errnum, ...) do { error(0, errnum, __VA_ARGS__); goto end; } while(0)
+
+static
+char *boundary_line(const char *old) {
+ size_t len = strlen(old);
+ char *new = xrealloc(NULL, len+5);
+ new[0] = '-';
+ new[1] = '-';
+ strcpy(&new[2], old);
+ new[2+len+0] = '\r';
+ new[2+len+1] = '\n';
+ new[2+len+2] = '\0';
+ return new;
+}
+
+static
+ssize_t safe_atoi(char *str) {
+ while (*str == ' ')
+ str++;
+ size_t len = 0;
+ while ('0' <= str[len] && str[len] <= '9')
+ len++;
+ size_t i = len;
+ while (str[i] == ' ')
+ i++;
+ if (len < 1 || strcmp(&str[i], "\r\n") != 0)
+ return -1;
+ return atoi(str);
+}
+
+void multipart_replace_reader(struct multipart_replace_stream *s, int fd, const char *_boundary) {
+ FILE *stream = fdopen(fd, "r");
+ char *boundary = boundary_line(_boundary);
+
+ char *line_buf = NULL;
+ size_t line_cap = 0;
+ ssize_t line_len = 0;
+ ssize_t content_length = -1;
+
+ bool first = true;
+ while (running) {
+ content_length = -1;
+ /* scan for the first non-empty line */
+ do {
+ line_len = getline(&line_buf, &line_cap, stream);
+ if (line_len < 0)
+ error(EXIT_FAILURE, ferror(stream), "source hung up");
+ } while (strcmp(line_buf, "\r\n") == 0);
+ /* make sure it matches the boundary separator */
+ if (first) {
+ while (strcmp(line_buf, boundary) != 0) {
+ line_len = getline(&line_buf, &line_cap, stream);
+ if (line_len < 0)
+ error(EXIT_FAILURE, ferror(stream), "source hung up");
+ }
+ first = false;
+ } else {
+ if (strcmp(line_buf, boundary) != 0)
+ error(EXIT_FAILURE, 0, "line does not match boundary: \"%s\"", line_buf);
+ }
+ /* read the frame header (MIME headers) */
+ s->back->len = 0;
+ do {
+ line_len = getline(&line_buf, &line_cap, stream);
+ if (line_len < 0)
+ error(EXIT_FAILURE, ferror(stream), "source hung up");
+ /* append the line to the frame contents */
+ if ((ssize_t)s->back->cap < s->back->len + line_len)
+ s->back->buf = xrealloc(s->back->buf, s->back->cap = s->back->len + line_len);
+ memcpy(&s->back->buf[s->back->len], line_buf, line_len);
+ s->back->len += line_len;
+ /* parse the Content-length (if applicable) */
+ if (strncasecmp(line_buf, "Content-length:", strlen("Content-length:")) == 0) {
+ content_length = safe_atoi(&line_buf[strlen("Content-length:")]);
+ }
+ } while (strcmp(line_buf, "\r\n") != 0);
+ /* make sure that it included a Content-length header */
+ if (content_length < 0)
+ error(EXIT_FAILURE, 0, "did not get frame length");
+ /* read the frame contents */
+ if ((ssize_t)s->back->cap < s->back->len + content_length)
+ s->back->buf = xrealloc(s->back->buf, s->back->cap = s->back->len + content_length);
+ if (fread(&s->back->buf[s->back->len], content_length, 1, stream) != 1)
+ error(EXIT_FALURE, ferror(stream), "fread(%zd)", s->back->len);
+ s->back->len += content_length;
+
+ /* swap the frames */
+ pthread_rwlock_wrlock(&s->frontlock);
+ struct frame *tmp = s->front;
+ s->front = s->back;
+ s->back = tmp;
+ s->framecount++;
+ pthread_rwlock_unlock(&s->frontlock);
+ }
+ end:
+ free(boundary);
+ fclose(stream);
+}
+
+void multipart_replace_writer(struct multipart_replace_stream *s, int fd, const char *_boundary) {
+ FILE *stream = fdopen(fd, "w");
+ struct frame myframe = { 0 };
+ long lastframe = 0;
+ char *boundary = boundary_line(_boundary);
+ size_t boundary_len = strlen(boundary);
+
+ pthread_rwlock_rdlock(&s->frontlock);
+ while (running) {
+ /* get the most recent frame (copy `s->front` to `myframe`) */
+ if (myframe.cap < (size_t)s->front->len)
+ myframe.buf = xrealloc(myframe.buf, myframe.cap = s->front->len);
+ memcpy(myframe.buf, s->front->buf, myframe.len = s->front->len);
+ lastframe = s->framecount;
+
+ pthread_rwlock_unlock(&s->frontlock);
+
+ /* send the frame to the client */
+ if (fwrite(boundary, boundary_len, 1, stream) < 1)
+ error(EXIT_FAILURE, ferror(stream), "fwrite(boundary)");
+ if (fwrite(myframe.buf, myframe.len, 1, stream) < 1)
+ error(EXIT_FAILURE, ferror(stream), "fwrite(frame.buf)");
+ /* send a blank line for pleasantness */
+ if (fwrite("\r\n", 2, 1, stream) < 1)
+ error(EXIT_FAILURE, ferror(stream), "fwrite(\"\\r\\n\")");
+ fflush(stream);
+
+ /* poll until there's a new frame */
+ pthread_rwlock_rdlock(&s->frontlock);
+ while (s->framecount == lastframe && running) {
+ pthread_rwlock_unlock(&s->frontlock);
+ usleep(30000); /* a bit over 30 FPS */
+ pthread_rwlock_rdlock(&s->frontlock);
+ }
+ }
+ pthread_rwlock_unlock(&s->frontlock);
+ end:
+ free(boundary);
+ free(myframe.buf);
+ fclose(stream);
+}
+
+void init_multipart_replace_stream(struct multipart_replace_stream *s) {
+ ZERO(s->a);
+ ZERO(s->b);
+ s->front = &s->a;
+ s->back = &s->b;
+ pthread_rwlock_init(&s->frontlock, NULL);
+ s->framecount = 0;
+}
+
+void destroy_multipart_replace_stream(struct multipart_replace_stream *s) {
+ free(s->a.buf);
+ free(s->b.buf);
+ pthread_rwlock_destroy(&s->frontlock);
+}
diff --git a/src/multipart-replace.h b/src/multipart-replace.h
new file mode 100644
index 0000000..2aa5e61
--- /dev/null
+++ b/src/multipart-replace.h
@@ -0,0 +1,26 @@
+/* Copyright 2016 Luke Shumaker */
+
+#pragma once
+
+#include <pthread.h>
+#include <stdbool.h>
+
+struct frame {
+ ssize_t len;
+ size_t cap;
+ char *buf;
+};
+
+struct multipart_replace_stream {
+ struct frame a, b;
+ struct frame *front, *back;
+ pthread_rwlock_t frontlock;
+ long framecount;
+};
+
+void multipart_replace_reader(struct multipart_replace_stream *s, int fd, const char *boundary);
+void multipart_replace_writer(struct multipart_replace_stream *s, int fd, const char *boundary);
+void init_multipart_replace_stream(struct multipart_replace_stream *s);
+void destroy_multipart_replace_stream(struct multipart_replace_stream *s);
+
+extern bool running;
diff --git a/src/util.c b/src/util.c
new file mode 100644
index 0000000..772dbf8
--- /dev/null
+++ b/src/util.c
@@ -0,0 +1,72 @@
+/* Copyright 2016 Luke Shumaker */
+
+#include <ctype.h> /* for isdigit */
+#include <stdlib.h>
+#include <error.h>
+#include <errno.h>
+
+#include "util.h"
+
+void *xrealloc(void *ptr, size_t size)
+{
+ void *ret = realloc(ptr, size);
+ if (ret == NULL) {
+ if (ptr==NULL)
+ error(1, errno, "Could not allocate memory");
+ else
+ error(1, errno, "Could not re-allocate memory");
+ }
+ return ret;
+}
+
+bool is_numeric(const char *str) {
+ for (size_t i = 0; str[i] != '\0'; i++)
+ if (!isdigit(str[i]))
+ return false;
+ if (str[0] == '\0')
+ return false;
+ return true;
+}
+
+int get_fd(const char *addr) {
+ int sock;
+ if (strcmp(addr, "stdin") == 0) {
+ sock = 0;
+ } else if (strcmp(addr, "stdout") == 0) {
+ sock = 1;
+ } else if (strcmp(addr, "stderr") == 0) {
+ sock = 2;
+ } else if (strncmp(addr, "systemd", strlen("systemd")) == 0) {
+ sock = 3; /* <systemd/sd-daemon.h>:SD_LISTEN_FDS_START */
+ addr = &addr[strlen("systemd")];
+ switch (addr[0]) {
+ case '\0':
+ /* do nothing */
+ break;
+ case ':':
+ addr = &addr[1];
+ if (is_numeric(addr)) {
+ sock += atoi(addr);
+ } else {
+ const char *e = getenv("LISTEN_FDNAMES");
+ if (e == NULL)
+ return -ENOTCONN;
+ char *names = strdupa(e);
+ char *name = NULL;
+ int i = -1;
+ do {
+ name = strsep(&names, ":");
+ i++;
+ } while (name != NULL && strcmp(name, addr) != 0);
+ if (name == NULL)
+ return -ENOENT;
+ sock += i;
+ }
+ }
+ } else {
+ if (!is_numeric(addr))
+ return -EINVAL;
+ sock = atoi(addr);
+ }
+ return sock;
+}
diff --git a/src/util.h b/src/util.h
new file mode 100644
index 0000000..0432a57
--- /dev/null
+++ b/src/util.h
@@ -0,0 +1,21 @@
+/* Copyright 2016 Luke Shumaker */
+
+#pragma once
+
+#include <signal.h> /* for sig_atomic_t */
+#include <string.h> /* for memset(3) */
+#include <stdbool.h>
+
+#define UNUSED __attribute__((__unused__))
+#define ZERO(x) memset(&(x), 0, sizeof(x))
+
+#ifndef _
+#define _(str) str
+#endif
+
+#define log(...) error(0, 0, __VA_ARGS__)
+#define debug(...) error(0, 0, __VA_ARGS__)
+
+void *xrealloc(void *ptr, size_t size);
+bool is_numeric(const char *str);
+int get_fd(const char *addr);
diff --git a/src/wg.c b/src/wg.c
new file mode 100644
index 0000000..4f60765
--- /dev/null
+++ b/src/wg.c
@@ -0,0 +1,45 @@
+#include <errno.h>
+#include <error.h>
+#include <stdlib.h> /* for EXIT_FAILURE */
+#include <unistd.h>
+
+#include "wg.h"
+
+/* Thread management tools modeled on https://golang.org/pkg/sync/#WaitGroup */
+
+/* pthread_cond_t is overly complicated. Just use a self-pipe. */
+
+void wg_init(struct wg *wg) {
+ wg->count = 0;
+ pthread_mutex_init(&wg->lock, NULL);
+ int fds[2];
+ if (pipe(fds) != 0)
+ error(EXIT_FAILURE, errno, "pipe");
+ wg->fd_wait = fds[0];
+ wg->fd_signal = fds[1];
+}
+
+void wg_add(struct wg *wg, unsigned int n) {
+ pthread_mutex_lock(&wg->lock);
+ wg->count += n;
+ pthread_mutex_unlock(&wg->lock);
+}
+
+void wg_sub(struct wg *wg, unsigned int n) {
+ pthread_mutex_lock(&wg->lock);
+ wg->count -= n;
+ if (wg->count == 0)
+ if (write(wg->fd_signal, " ", 1) < 1)
+ error(EXIT_FAILURE, errno, "write");
+ pthread_mutex_unlock(&wg->lock);
+}
+
+void wg_wait(struct wg *wg) {
+ char b;
+ retry:
+ if (read(wg->fd_wait, &b, 1) == -1) {
+ if (errno == EINTR)
+ goto retry;
+ error(EXIT_FAILURE, errno, "wg_wait");
+ }
+}
diff --git a/src/wg.h b/src/wg.h
new file mode 100644
index 0000000..777e8a3
--- /dev/null
+++ b/src/wg.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <pthread.h>
+
+/* Thread management tools modeled on https://golang.org/pkg/sync/#WaitGroup */
+
+/* pthread_cond_t is overly complicated. Just use a self-pipe. */
+
+struct wg {
+ int count;
+ pthread_mutex_t lock;
+ int fd_wait;
+ int fd_signal;
+};
+
+void wg_init(struct wg *);
+void wg_add(struct wg *, unsigned int);
+void wg_sub(struct wg *, unsigned int);
+void wg_wait(struct wg*);