diff --git a/Makefile b/Makefile index cd4191820..6d7294685 100644 --- a/Makefile +++ b/Makefile @@ -202,6 +202,7 @@ include tool/hash/hash.mk include tool/net/net.mk include tool/viz/viz.mk include tool/tool.mk +include net/turfwar/turfwar.mk include test/libc/tinymath/test.mk include test/libc/intrin/test.mk include test/libc/mem/test.mk diff --git a/libc/calls/chdir.c b/libc/calls/chdir.c index 78c43a239..925a503ee 100644 --- a/libc/calls/chdir.c +++ b/libc/calls/chdir.c @@ -16,11 +16,12 @@ │ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ -#include "libc/intrin/strace.internal.h" #include "libc/calls/syscall-nt.internal.h" #include "libc/calls/syscall-sysv.internal.h" #include "libc/dce.h" #include "libc/intrin/asan.internal.h" +#include "libc/intrin/strace.internal.h" +#include "libc/runtime/runtime.h" #include "libc/sysv/errfuns.h" /** @@ -34,6 +35,7 @@ */ int chdir(const char *path) { int rc; + GetProgramExecutableName(); // XXX: ugly workaround if (!path || (IsAsan() && !__asan_is_valid(path, 1))) { rc = efault(); } else if (!IsWindows()) { diff --git a/libc/calls/struct/timespec.h b/libc/calls/struct/timespec.h index 3bc0cb042..75be831a2 100644 --- a/libc/calls/struct/timespec.h +++ b/libc/calls/struct/timespec.h @@ -19,6 +19,7 @@ int timespec_getres(struct timespec *, int); int _timespec_cmp(struct timespec, struct timespec) pureconst; bool _timespec_eq(struct timespec, struct timespec) pureconst; +bool _timespec_gt(struct timespec, struct timespec) pureconst; bool _timespec_gte(struct timespec, struct timespec) pureconst; int64_t _timespec_tomicros(struct timespec) pureconst; int64_t _timespec_tomillis(struct timespec) pureconst; diff --git a/libc/integral/normalize.inc b/libc/integral/normalize.inc index 30be7c9b9..a8ccdf124 100644 --- a/libc/integral/normalize.inc +++ b/libc/integral/normalize.inc @@ -68,7 +68,7 @@ /* TODO(jart): Remove this in favor of GetStackSize() */ #if defined(COSMO) && (defined(MODE_DBG) || defined(__SANITIZE_ADDRESS__)) -#define STACKSIZE 262144 /* 256kb stack */ +#define STACKSIZE 524288 /* 512kb stack */ #elif defined(COSMO) #define STACKSIZE 65536 /* 64kb stack */ #else diff --git a/libc/zipos/get.c b/libc/zipos/get.c index a6d618c9c..c08c3d18f 100644 --- a/libc/zipos/get.c +++ b/libc/zipos/get.c @@ -17,15 +17,15 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/calls/calls.h" -#include "libc/intrin/strace.internal.h" #include "libc/intrin/cmpxchg.h" #include "libc/intrin/promises.internal.h" -#include "libc/thread/thread.h" +#include "libc/intrin/strace.internal.h" #include "libc/macros.internal.h" #include "libc/runtime/runtime.h" #include "libc/sysv/consts/map.h" #include "libc/sysv/consts/o.h" #include "libc/sysv/consts/prot.h" +#include "libc/thread/thread.h" #include "libc/zip.h" #include "libc/zipos/zipos.internal.h" diff --git a/net/net.mk b/net/net.mk index cd1e17196..58294aed9 100644 --- a/net/net.mk +++ b/net/net.mk @@ -4,4 +4,5 @@ .PHONY: o/$(MODE)/net o/$(MODE)/net: o/$(MODE)/net/finger \ o/$(MODE)/net/http \ - o/$(MODE)/net/https + o/$(MODE)/net/https \ + o/$(MODE)/net/turfwar diff --git a/net/turfwar/turfwar.c b/net/turfwar/turfwar.c new file mode 100644 index 000000000..68496157c --- /dev/null +++ b/net/turfwar/turfwar.c @@ -0,0 +1,932 @@ +/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│ +│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8 :vi│ +╞══════════════════════════════════════════════════════════════════════════════╡ +│ Copyright 2022 Justine Alexandra Roberts Tunney │ +│ │ +│ Permission to use, copy, modify, and/or distribute this software for │ +│ any purpose with or without fee is hereby granted, provided that the │ +│ above copyright notice and this permission notice appear in all copies. │ +│ │ +│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL │ +│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED │ +│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE │ +│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL │ +│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR │ +│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER │ +│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR │ +│ PERFORMANCE OF THIS SOFTWARE. │ +╚─────────────────────────────────────────────────────────────────────────────*/ +#include "libc/calls/calls.h" +#include "libc/calls/pledge.h" +#include "libc/calls/struct/iovec.h" +#include "libc/calls/struct/sigaction.h" +#include "libc/calls/struct/stat.h" +#include "libc/calls/struct/timespec.h" +#include "libc/calls/struct/timeval.h" +#include "libc/errno.h" +#include "libc/fmt/conv.h" +#include "libc/fmt/itoa.h" +#include "libc/intrin/bits.h" +#include "libc/intrin/kprintf.h" +#include "libc/intrin/strace.internal.h" +#include "libc/log/check.h" +#include "libc/log/log.h" +#include "libc/macros.internal.h" +#include "libc/mem/gc.h" +#include "libc/mem/mem.h" +#include "libc/nexgen32e/crc32.h" +#include "libc/runtime/internal.h" +#include "libc/runtime/runtime.h" +#include "libc/sock/sock.h" +#include "libc/sock/struct/pollfd.h" +#include "libc/sock/struct/sockaddr.h" +#include "libc/stdio/append.h" +#include "libc/stdio/stdio.h" +#include "libc/str/slice.h" +#include "libc/str/str.h" +#include "libc/sysv/consts/af.h" +#include "libc/sysv/consts/clock.h" +#include "libc/sysv/consts/o.h" +#include "libc/sysv/consts/poll.h" +#include "libc/sysv/consts/sig.h" +#include "libc/sysv/consts/so.h" +#include "libc/sysv/consts/sock.h" +#include "libc/sysv/consts/sol.h" +#include "libc/sysv/consts/tcp.h" +#include "libc/thread/thread.h" +#include "libc/time/struct/tm.h" +#include "libc/x/x.h" +#include "libc/x/xasprintf.h" +#include "libc/zip.h" +#include "net/http/escape.h" +#include "net/http/http.h" +#include "net/http/url.h" +#include "third_party/getopt/getopt.h" +#include "third_party/nsync/cv.h" +#include "third_party/nsync/mu.h" +#include "third_party/nsync/note.h" +#include "third_party/sqlite3/sqlite3.h" +#include "third_party/zlib/zconf.h" +#include "third_party/zlib/zlib.h" +#include "tool/net/lfuncs.h" + +/** + * @fileoverview production webserver for turfwar online game + */ + +#define PORT 8080 +#define WORKERS 2000 +#define HEARTBEAT 2000 +#define KEEPALIVE_MS 2000 +#define DATE_UPDATE_MS 500 +#define POLL_ASSETS_MS 250 +#define BOARD_GENERATE_MS 10000 +#define CLAIM_DEADLINE_MS 1000 +#define CLAIM_MAX 200 +#define NICK_MAX 40 +#define MSG_MAX 10 + +#define GETOPTS "dvp:w:k:" +#define USAGE \ + "\ +Usage: turfwar.com [-dv] ARGS...\n\ + -d daemonize\n\ + -v verbosity\n\ + -p INT port\n\ + -w INT workers\n\ + -k INT keepalive\n\ +" + +#define STANDARD_RESPONSE_HEADERS \ + "Server: turfwar\r\n" \ + "Referrer-Policy: origin\r\n" \ + "Access-Control-Allow-Origin: *\r\n" + +#define HasHeader(H) (!!msg->headers[H].a) +#define HeaderData(H) (inbuf + msg->headers[H].a) +#define HeaderLength(H) (msg->headers[H].b - msg->headers[H].a) +#define HeaderEqual(H, S) \ + SlicesEqual(S, strlen(S), HeaderData(H), HeaderLength(H)) +#define HeaderEqualCase(H, S) \ + SlicesEqualCase(S, strlen(S), HeaderData(H), HeaderLength(H)) +#define UrlEqual(S) \ + SlicesEqual(inbuf + msg->uri.a, msg->uri.b - msg->uri.a, S, strlen(S)) + +#if 1 +#define LOG(...) kprintf(__VA_ARGS__) +#else +#define LOG(...) (void)0 +#endif + +#if 0 +#define DEBUG(...) kprintf(__VA_ARGS__) +#else +#define DEBUG(...) (void)0 +#endif + +#define CHECK_SYS(x) \ + do { \ + if (!CheckSys(__FILE__, __LINE__, x)) { \ + goto OnError; \ + } \ + } while (0) +#define CHECK_SQL(x) \ + do { \ + int e = errno; \ + if (!CheckSql(__FILE__, __LINE__, x)) { \ + goto OnError; \ + } \ + errno = e; \ + } while (0) +#define CHECK_DB(x) \ + do { \ + int e = errno; \ + if (!CheckDb(__FILE__, __LINE__, x, db)) { \ + goto OnError; \ + } \ + errno = e; \ + } while (0) + +static const uint8_t kGzipHeader[] = { + 0x1F, // MAGNUM + 0x8B, // MAGNUM + 0x08, // CM: DEFLATE + 0x00, // FLG: NONE + 0x00, // MTIME: NONE + 0x00, // + 0x00, // + 0x00, // + 0x00, // XFL + kZipOsUnix, // OS +}; + +struct Data { + char *p; + size_t n; +}; + +struct Asset { + char *path; + nsync_mu lock; + const char *type; + struct Data data; + struct Data gzip; + struct timespec mtim; + char lastmod[32]; +}; + +bool g_daemonize; +int g_port = PORT; +int g_workers = WORKERS; +int g_keepalive = KEEPALIVE_MS; + +struct tm g_nowish; +nsync_note g_shutdown; +nsync_mu g_nowish_lock; + +struct Board { + nsync_mu mu; + nsync_cv cv; +} g_board; + +struct Assets { + struct Asset index; + struct Asset about; + struct Asset user; + struct Asset board; +} g_asset; + +struct Claims { + int pos; + int count; + nsync_mu mu; + nsync_cv non_full; + nsync_cv non_empty; + struct Claim { + uint32_t ip; + char name[NICK_MAX + 1]; + } data[CLAIM_MAX]; +} g_claims; + +bool CheckSys(const char *file, int line, long rc) { + if (rc != -1) return true; + kprintf("%s:%d: %s\n", file, line, strerror(errno)); + return false; +} + +bool CheckSql(const char *file, int line, int rc) { + if (rc == SQLITE_OK) return true; + kprintf("%s:%d: %s\n", file, line, sqlite3_errstr(rc)); + return false; +} + +bool CheckDb(const char *file, int line, int rc, sqlite3 *db) { + if (rc == SQLITE_OK) return true; + kprintf("%s:%d: %s: %s\n", file, line, sqlite3_errstr(rc), + sqlite3_errmsg(db)); + return false; +} + +bool IsValidNick(const char *s, size_t n) { + size_t i; + if (n > NICK_MAX) return false; + for (i = 0; i < n; ++i) { + if (!(isalnum(s[i]) || // + s[i] == '@' || // + s[i] == '/' || // + s[i] == ':' || // + s[i] == '.' || // + s[i] == '+' || // + s[i] == '_' || // + s[i] == '*')) { + return false; + } + } + return true; +} + +char *FormatUnixHttpDateTime(char *s, int64_t t) { + struct tm tm; + gmtime_r(&t, &tm); + FormatHttpDateTime(s, &tm); + return s; +} + +void UpdateNow(void) { + int64_t secs; + struct tm tm; + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + secs = ts.tv_sec; + gmtime_r(&secs, &tm); + //!//!//!//!//!//!//!//!//!//!//!//!//!/ + nsync_mu_lock(&g_nowish_lock); + g_nowish = tm; + nsync_mu_unlock(&g_nowish_lock); + //!//!//!//!//!//!//!//!//!//!//!//!//!/ +} + +char *FormatDate(char *p) { + //////////////////////////////////////// + nsync_mu_rlock(&g_nowish_lock); + p = FormatHttpDateTime(p, &g_nowish); + nsync_mu_runlock(&g_nowish_lock); + //////////////////////////////////////// + return p; +} + +bool AddClaim(struct Claims *q, const struct Claim *v, nsync_time dead) { + bool wake = false; + bool added = false; + nsync_mu_lock(&q->mu); + while (q->count == ARRAYLEN(q->data)) { + if (nsync_cv_wait_with_deadline(&q->non_full, &q->mu, dead, g_shutdown)) { + break; // must be ETIMEDOUT or ECANCELED + } + } + if (q->count != ARRAYLEN(q->data)) { + int i = q->pos + q->count; + if (ARRAYLEN(q->data) <= i) i -= ARRAYLEN(q->data); + memcpy(q->data + i, v, sizeof(*v)); + if (!q->count) wake = true; + q->count++; + added = true; + } + nsync_mu_unlock(&q->mu); + if (wake) { + nsync_cv_broadcast(&q->non_empty); + } + return added; +} + +int GetClaims(struct Claims *q, struct Claim *out, int len, nsync_time dead) { + int got = 0; + nsync_mu_lock(&q->mu); + while (!q->count) { + if (nsync_cv_wait_with_deadline(&q->non_empty, &q->mu, dead, g_shutdown)) { + break; // must be ETIMEDOUT or ECANCELED + } + } + while (got < len && q->count) { + memcpy(out + got, q->data + q->pos, sizeof(*out)); + if (q->count == ARRAYLEN(q->data)) { + nsync_cv_broadcast(&q->non_full); + } + ++got; + q->pos++; + q->count--; + if (q->pos == ARRAYLEN(q->data)) { + q->pos = 0; + } + } + nsync_mu_unlock(&q->mu); + return got; +} + +static bool GetNick(char *inbuf, struct HttpMessage *msg, struct Claim *v) { + size_t i, n; + struct Url url; + void *f[2] = {0}; + bool found = false; + f[0] = ParseUrl(inbuf + msg->uri.a, msg->uri.b - msg->uri.a, &url, + kUrlPlus | kUrlLatin1); + f[1] = url.params.p; + for (i = 0; i < url.params.n; ++i) { + if (SlicesEqual("name", 4, url.params.p[i].key.p, url.params.p[i].key.n) && + url.params.p[i].val.p && + IsValidNick(url.params.p[i].val.p, url.params.p[i].val.n)) { + memcpy(v->name, url.params.p[i].val.p, url.params.p[i].val.n); + found = true; + break; + } + } + free(f[1]); + free(f[0]); + return found; +} + +// thousands of threads for handling client connections +void *HttpWorker(void *arg) { + int server; + int yes = 1; + int id = (intptr_t)arg; + struct HttpMessage *msg; + STRACE("HttpWorker #%d started", id); + + // load balance incoming connections for port 8080 across all threads + // hangup on any browser clients that lag for more than a few seconds + struct timeval timeo = {g_keepalive / 1000, g_keepalive % 1000}; + struct sockaddr_in addr = {.sin_family = AF_INET, .sin_port = htons(g_port)}; + + CHECK_NE(-1, (server = socket(AF_INET, SOCK_STREAM, 0))); + setsockopt(server, SOL_SOCKET, SO_RCVTIMEO, &timeo, sizeof(timeo)); + setsockopt(server, SOL_SOCKET, SO_SNDTIMEO, &timeo, sizeof(timeo)); + setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); + setsockopt(server, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)); + setsockopt(server, SOL_TCP, TCP_FASTOPEN, &yes, sizeof(yes)); + setsockopt(server, SOL_TCP, TCP_QUICKACK, &yes, sizeof(yes)); + errno = 0; + + CHECK_NE(-1, bind(server, &addr, sizeof(addr))); + CHECK_NE(-1, listen(server, 1)); + msg = _gc(xmalloc(sizeof(struct HttpMessage))); + + // connection loop + while (!nsync_note_is_notified(g_shutdown)) { + int msgcount; + struct Data d; + struct Url url; + bool comp, ipv6; + struct Asset *a; + ssize_t got, sent; + struct iovec iov[2]; + uint32_t ip, clientip; + uint32_t clientaddrsize; + struct sockaddr_in clientaddr; + int client, inmsglen, outmsglen; + char inbuf[1500], outbuf[512], ipbuf[32], *p, *q; + + // this slows the server down a lot but is needed on non-Linux to + // react to keyboard ctrl-c + if (!IsLinux() && + poll(&(struct pollfd){server, POLLIN}, 1, HEARTBEAT) < 1) { + continue; + } + + // wait for client connection + clientaddrsize = sizeof(clientaddr); + client = accept(server, (struct sockaddr *)&clientaddr, &clientaddrsize); + if (client == -1) continue; + ip = clientip = ntohl(clientaddr.sin_addr.s_addr); + + // strict message loop w/o pipelining + msgcount = 0; + do { + InitHttpMessage(msg, kHttpRequest); + if ((got = read(client, inbuf, sizeof(inbuf))) <= 0) break; + if ((inmsglen = ParseHttpMessage(msg, inbuf, got)) <= 0) break; + if (msg->version != 11) break; // cloudflare won't send 0.9 or 1.0 + + // get the ip address again + // we assume a firewall only lets the frontend talk to this server + ipv6 = false; + if (HasHeader(kHttpXForwardedFor) && + ParseForwarded(HeaderData(kHttpXForwardedFor), + HeaderLength(kHttpXForwardedFor), &ip, 0) == -1) { + ipv6 = true; + } + ksnprintf(ipbuf, sizeof(ipbuf), "%hhu.%hhu.%hhu.%hhu", ip >> 24, ip >> 16, + ip >> 8, ip); + + if (UrlEqual("/") || UrlEqual("/index.html")) { + a = &g_asset.index; + } else if (UrlEqual("/about.html")) { + a = &g_asset.about; + } else if (UrlEqual("/user.html")) { + a = &g_asset.user; + } else if (UrlEqual("/board")) { + a = &g_asset.board; + } else { + a = 0; + } + + if (a) { + comp = HeaderHas(msg, inbuf, kHttpAcceptEncoding, "gzip", 4); + p = stpcpy(outbuf, "HTTP/1.1 200 OK\r\n" STANDARD_RESPONSE_HEADERS + "Cache-Control: max-age=60, must-revalidate\r\n" + "Vary: Accept-Encoding\r\n" + "Date: "); + p = FormatDate(p); + //////////////////////////////////////// + nsync_mu_rlock(&a->lock); + p = stpcpy(p, "\r\nLast-Modified: "); + p = stpcpy(p, a->lastmod); + p = stpcpy(p, "\r\nContent-Type: "); + p = stpcpy(p, a->type); + if (comp) p = stpcpy(p, "\r\nContent-Encoding: gzip"); + p = stpcpy(p, "\r\nContent-Length: "); + d = comp ? a->gzip : a->data; + p = FormatInt32(p, d.n); + p = stpcpy(p, "\r\n\r\n"); + iov[0].iov_base = outbuf; + iov[0].iov_len = p - outbuf; + iov[1].iov_base = d.p; + iov[1].iov_len = d.n; + sent = writev(client, iov, 2); + outmsglen = iov[0].iov_len + iov[1].iov_len; + nsync_mu_runlock(&a->lock); + //////////////////////////////////////// + + } else if (UrlEqual("/ip")) { + if (!ipv6) { + p = stpcpy(outbuf, "HTTP/1.1 200 OK\r\n" STANDARD_RESPONSE_HEADERS + "Content-Type: text/plain\r\n" + "Cache-Control: private\r\n" + "Date: "); + p = FormatDate(p); + p = stpcpy(p, "\r\nContent-Length: "); + p = FormatInt32(p, strlen(ipbuf)); + p = stpcpy(p, "\r\n\r\n"); + p = stpcpy(p, ipbuf); + outmsglen = p - outbuf; + sent = write(client, outbuf, outmsglen); + } else { + Ipv6Warning: + DEBUG("%.*s via %s: 400 Need IPv4\n", + HeaderLength(kHttpXForwardedFor), + HeaderData(kHttpXForwardedFor), ipbuf); + q = "IPv4 Games only supports IPv4 right now"; + p = stpcpy(outbuf, + "HTTP/1.1 400 Need IPv4\r\n" STANDARD_RESPONSE_HEADERS + "Content-Type: text/plain\r\n" + "Cache-Control: private\r\n" + "Connection: close\r\n" + "Date: "); + p = FormatDate(p); + p = stpcpy(p, "\r\nContent-Length: "); + p = FormatInt32(p, strlen(q)); + p = stpcpy(p, "\r\n\r\n"); + p = stpcpy(p, q); + outmsglen = p - outbuf; + sent = write(client, outbuf, p - outbuf); + break; + } + + } else if (msg->uri.b - msg->uri.a > 6 && + !memcmp(inbuf + msg->uri.a, "/claim", 6)) { + if (ipv6) goto Ipv6Warning; + struct Claim v = {.ip = ip}; + if (GetNick(inbuf, msg, &v)) { + if (AddClaim( + &g_claims, &v, + _timespec_add(_timespec_real(), + _timespec_frommillis(CLAIM_DEADLINE_MS)))) { + LOG("%s claimed by %s\n", ipbuf, v.name); + q = xasprintf("\n" + "The land at %s was claimed for %s.\n" + "\n" + "The land at %s was claimed for %s.\n" + "

\nBack to homepage\n", + ipbuf, v.name, ipbuf, v.name, v.name); + p = stpcpy(outbuf, "HTTP/1.1 200 OK\r\n" STANDARD_RESPONSE_HEADERS + "Content-Type: text/html\r\n" + "Cache-Control: private\r\n" + "Date: "); + p = FormatDate(p); + p = stpcpy(p, "\r\nContent-Length: "); + p = FormatInt32(p, strlen(q)); + p = stpcpy(p, "\r\n\r\n"); + p = stpcpy(p, q); + outmsglen = p - outbuf; + sent = write(client, outbuf, p - outbuf); + free(q); + } else { + LOG("%s: 502 Claims Queue Full\n", ipbuf); + q = "Claims Queue Full"; + p = stpcpy( + outbuf, + "HTTP/1.1 502 Claims Queue Full\r\n" STANDARD_RESPONSE_HEADERS + "Content-Type: text/plain\r\n" + "Cache-Control: private\r\n" + "Connection: close\r\n" + "Date: "); + p = FormatDate(p); + p = stpcpy(p, "\r\nContent-Length: "); + p = FormatInt32(p, strlen(q)); + p = stpcpy(p, "\r\n\r\n"); + p = stpcpy(p, q); + outmsglen = p - outbuf; + sent = write(client, outbuf, p - outbuf); + break; + } + } else { + LOG("%s: 400 invalid name\n", ipbuf); + q = "invalid name"; + p = stpcpy(outbuf, + "HTTP/1.1 400 Invalid Name\r\n" STANDARD_RESPONSE_HEADERS + "Content-Type: text/plain\r\n" + "Cache-Control: private\r\n" + "Connection: close\r\n" + "Date: "); + p = FormatDate(p); + p = stpcpy(p, "\r\nContent-Length: "); + p = FormatInt32(p, strlen(q)); + p = stpcpy(p, "\r\n\r\n"); + p = stpcpy(p, q); + outmsglen = p - outbuf; + sent = write(client, outbuf, p - outbuf); + break; + } + + } else { + LOG("%s: 400 not found\n", ipbuf); + q = "\r\n" + "404 not found\r\n" + "

404 not found

\r\n"; + p = stpcpy(outbuf, + "HTTP/1.1 404 Not Found\r\n" STANDARD_RESPONSE_HEADERS + "Content-Type: text/html; charset=utf-8\r\n" + "Date: "); + p = FormatDate(p); + p = stpcpy(p, "\r\nContent-Length: "); + p = FormatInt32(p, strlen(q)); + p = stpcpy(p, "\r\n\r\n"); + p = stpcpy(p, q); + outmsglen = p - outbuf; + sent = write(client, outbuf, p - outbuf); + } + + // if the client isn't pipelining and write() wrote the full + // amount, then since we sent the content length and checked + // that the client didn't attach a payload, we are so synced + // thus we can safely process more messages + } while (got == inmsglen && // + sent == outmsglen && // + ++msgcount < MSG_MAX && // + !msg->headers[kHttpContentLength].a && + !msg->headers[kHttpTransferEncoding].a && + (msg->method == kHttpGet || msg->method == kHttpHead) && + !nsync_note_is_notified(g_shutdown)); + DestroyHttpMessage(msg); + close(client); + } + + STRACE("HttpWorker #%d exiting", id); + close(server); + return 0; +} + +struct Data Gzip(struct Data data) { + char *p; + void *tmp; + uint32_t crc; + char footer[8]; + struct Data res; + z_stream zs = {0}; + crc = crc32_z(0, data.p, data.n); + WRITE32LE(footer + 0, crc); + WRITE32LE(footer + 4, data.n); + CHECK_EQ(Z_OK, deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, + -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY)); + zs.next_in = (const Bytef *)data.p; + zs.avail_in = data.n; + zs.avail_out = compressBound(data.n); + zs.next_out = tmp = xmalloc(zs.avail_out); + CHECK_EQ(Z_STREAM_END, deflate(&zs, Z_FINISH)); + CHECK_EQ(Z_OK, deflateEnd(&zs)); + res.n = sizeof(kGzipHeader) + zs.total_out + sizeof(footer); + p = res.p = xmalloc(res.n); + p = mempcpy(p, kGzipHeader, sizeof(kGzipHeader)); + p = mempcpy(p, tmp, zs.total_out); + p = mempcpy(p, footer, sizeof(footer)); + free(tmp); + return res; +} + +struct Asset LoadAsset(const char *path, const char *type) { + struct stat st; + struct Asset a = {0}; + CHECK_EQ(0, stat(path, &st)); + CHECK_NOTNULL((a.data.p = xslurp(path, &a.data.n))); + a.type = type; + a.path = xstrdup(path); + a.mtim = st.st_mtim; + a.gzip = Gzip(a.data); + FormatUnixHttpDateTime(a.lastmod, a.mtim.tv_sec); + return a; +} + +bool ReloadAsset(struct Asset *a) { + int fd; + void *f[2]; + ssize_t rc; + struct stat st; + char lastmod[32]; + struct Data data = {0}; + struct Data gzip = {0}; + CHECK_SYS((fd = open(a->path, O_RDONLY))); + CHECK_SYS(fstat(fd, &st)); + if (_timespec_gt(st.st_mtim, a->mtim) && (data.p = malloc(st.st_size))) { + FormatUnixHttpDateTime(lastmod, st.st_mtim.tv_sec); + CHECK_SYS((rc = read(fd, data.p, st.st_size))); + if (rc != st.st_size) goto OnError; + close(fd); + gzip = Gzip(data); + //!//!//!//!//!//!//!//!//!//!//!//!//!/ + nsync_mu_lock(&a->lock); + f[0] = a->data.p; + f[1] = a->gzip.p; + a->data = data; + a->gzip = gzip; + a->mtim = st.st_mtim; + memcpy(a->lastmod, lastmod, 32); + nsync_mu_unlock(&a->lock); + //!//!//!//!//!//!//!//!//!//!//!//!//!/ + free(f[0]); + free(f[1]); + } + return true; +OnError: + free(data.p); + close(fd); + return false; +} + +void FreeAsset(struct Asset *a) { + free(a->path); + free(a->data.p); + free(a->gzip.p); +} + +static void GetOpts(int argc, char *argv[]) { + int opt; + while ((opt = getopt(argc, argv, GETOPTS)) != -1) { + switch (opt) { + case 'd': + g_daemonize = true; + break; + case 'p': + g_port = atoi(optarg); + break; + case 'w': + g_workers = atoi(optarg); + break; + case 'k': + g_keepalive = atoi(optarg); + break; + case 'v': + ++__log_level; + break; + case '?': + write(1, USAGE, sizeof(USAGE) - 1); + exit(0); + default: + write(2, USAGE, sizeof(USAGE) - 1); + exit(64); + } + } +} + +void OnCtrlC(int sig) { + nsync_note_notify(g_shutdown); +} + +bool GenerateBoard(struct Asset *out) { + char *sb = 0; + sqlite3 *db = 0; + size_t sblen = 0; + struct Asset a = {0}; + sqlite3_stmt *stmt = 0; + DEBUG("GenerateBoard\n"); + a.type = "application/json"; + CHECK_SYS(clock_gettime(CLOCK_REALTIME, &a.mtim)); + FormatUnixHttpDateTime(a.lastmod, a.mtim.tv_sec); + CHECK_SYS(appends(&a.data.p, "{\"leaders\":[\n")); + CHECK_SQL(sqlite3_open("db.sqlite3", &db)); + CHECK_SQL(sqlite3_exec(db, "PRAGMA journal_mode=WAL", 0, 0, 0)); + CHECK_SQL(sqlite3_exec(db, "PRAGMA synchronous=NORMAL", 0, 0, 0)); + CHECK_DB(sqlite3_prepare_v2(db, + "SELECT nick AS name, COUNT(ip) AS count\n" + "FROM land\n" + "WHERE ip >= ?1 AND ip <= ?2\n" + "GROUP BY nick\n" + "ORDER BY count DESC\n" + "LIMIT 1", + -1, &stmt, 0)); + CHECK_SQL(sqlite3_exec(db, "BEGIN TRANSACTION", 0, 0, 0)); + for (long i = 0; i < 256; ++i) { + if (i) CHECK_SYS(appends(&a.data.p, ",\n")); + CHECK_DB(sqlite3_bind_int64(stmt, 1, i * 0x1000000)); + CHECK_DB(sqlite3_bind_int64(stmt, 2, i * 0x1000000 + 0xFFFFFF)); + switch (sqlite3_step(stmt)) { + case SQLITE_ROW: + CHECK_SYS(appendf( + &a.data.p, "{\"name\":\"%s\",\"count\":%ld}", + EscapeJsStringLiteral(&sb, &sblen, + (void *)sqlite3_column_text(stmt, 0), -1, 0), + sqlite3_column_int64(stmt, 1))); + break; + case SQLITE_DONE: + CHECK_SYS(appends(&a.data.p, "false")); + break; + default: + kprintf("%s\n", sqlite3_errmsg(db)); + abort(); + } + CHECK_DB(sqlite3_reset(stmt)); + } + CHECK_SQL(sqlite3_exec(db, "END TRANSACTION", 0, 0, 0)); + CHECK_SYS(appends(&a.data.p, "\n]}\n")); + CHECK_DB(sqlite3_finalize(stmt)); + CHECK_SQL(sqlite3_close(db)); + a.data.n = appendz(a.data.p).i; + a.gzip = Gzip(a.data); + *out = a; + return true; +OnError: + sqlite3_finalize(stmt); + sqlite3_close(db); + free(a.data.p); + return false; +} + +// single thread for regenerating the leaderboard json +void *BoardWorker(void *arg) { + int rc; + void *f[2]; + struct Asset a; + LOG("BoardWorker started\n"); + for (;;) { + nsync_mu_lock(&g_board.mu); + rc = nsync_cv_wait_with_deadline(&g_board.cv, &g_board.mu, + nsync_time_no_deadline, g_shutdown); + nsync_mu_unlock(&g_board.mu); + if (rc == ECANCELED) break; + if (GenerateBoard(&a)) { + //!//!//!//!//!//!//!//!//!//!//!//!//!/ + nsync_mu_lock(&g_asset.board.lock); + f[0] = g_asset.board.data.p; + f[1] = g_asset.board.gzip.p; + g_asset.board.data = a.data; + g_asset.board.gzip = a.gzip; + g_asset.board.mtim = a.mtim; + memcpy(g_asset.board.lastmod, a.lastmod, 32); + nsync_mu_unlock(&g_asset.board.lock); + //!//!//!//!//!//!//!//!//!//!//!//!//!/ + free(f[0]); + free(f[1]); + } + } + LOG("BoardWorker exiting\n"); + return 0; +} + +// single thread for inserting batched claims into the database +void *ClaimWorker(void *arg) { + int i, n, rc; + sqlite3 *db = 0; + struct Claim v[32]; + sqlite3_stmt *stmt = 0; +StartOver: + CHECK_SQL(sqlite3_open("db.sqlite3", &db)); + CHECK_SQL(sqlite3_exec(db, "PRAGMA journal_mode=WAL", 0, 0, 0)); + CHECK_SQL(sqlite3_exec(db, "PRAGMA synchronous=NORMAL", 0, 0, 0)); + CHECK_DB(sqlite3_prepare_v2(db, + "INSERT INTO land (ip, nick) VALUES (?1, ?2)\n" + "ON CONFLICT (ip) DO UPDATE SET (nick) = (?2)\n" + "WHERE nick != ?2", + -1, &stmt, 0)); + LOG("ClaimWorker started\n"); + while ((n = GetClaims(&g_claims, v, ARRAYLEN(v), nsync_time_no_deadline))) { + CHECK_SQL(sqlite3_exec(db, "BEGIN TRANSACTION", 0, 0, 0)); + for (i = 0; i < n; ++i) { + CHECK_DB(sqlite3_bind_int64(stmt, 1, v[i].ip)); + CHECK_DB(sqlite3_bind_text(stmt, 2, v[i].name, -1, SQLITE_TRANSIENT)); + CHECK_DB((rc = sqlite3_step(stmt)) == SQLITE_DONE ? SQLITE_OK : rc); + CHECK_DB(sqlite3_reset(stmt)); + } + CHECK_SQL(sqlite3_exec(db, "COMMIT TRANSACTION", 0, 0, 0)); + DEBUG("Committed %d claims\n", n); + nsync_mu_lock(&g_board.mu); + nsync_cv_signal(&g_board.cv); + nsync_mu_unlock(&g_board.mu); + } + CHECK_DB(sqlite3_finalize(stmt)); + CHECK_SQL(sqlite3_close(db)); + return 0; +OnError: + sqlite3_finalize(stmt); + sqlite3_close(db); + stmt = 0; + db = 0; + usleep(1000 * 1000); + goto StartOver; +} + +// single thread for computing HTTP Date header +void *NowWorker(void *arg) { + nsync_time deadline; + for (deadline = _timespec_real();;) { + deadline = _timespec_add(deadline, _timespec_frommillis(DATE_UPDATE_MS)); + if (!nsync_note_wait(g_shutdown, deadline)) { + UpdateNow(); + } else { + break; + } + } + return 0; +} + +// single thread for monitoring assets on disk +void *AssetWorker(void *arg) { + nsync_time deadline; + for (deadline = _timespec_real();;) { + deadline = _timespec_add(deadline, _timespec_frommillis(POLL_ASSETS_MS)); + if (!nsync_note_wait(g_shutdown, deadline)) { + ReloadAsset(&g_asset.index); + ReloadAsset(&g_asset.about); + ReloadAsset(&g_asset.user); + } else { + break; + } + } + return 0; +} + +int main(int argc, char *argv[]) { + ShowCrashReports(); + GetOpts(argc, argv); + + __enable_threads(); + sqlite3_initialize(); + g_shutdown = nsync_note_new(0, nsync_time_no_deadline); + + CHECK_EQ(0, chdir("/opt/turfwar")); + putenv("TMPDIR=/opt/turfwar/tmp"); + g_asset.index = LoadAsset("index.html", "text/html; charset=utf-8"); + g_asset.about = LoadAsset("about.html", "text/html; charset=utf-8"); + g_asset.user = LoadAsset("user.html", "text/html; charset=utf-8"); + + CHECK_EQ(0, unveil("/opt/turfwar", "rwc")); + CHECK_EQ(0, unveil(0, 0)); + __pledge_mode = PLEDGE_PENALTY_RETURN_EPERM; + CHECK_EQ(0, pledge("stdio flock rpath wpath cpath inet", 0)); + + // create threads + pthread_t boarder; + CHECK_EQ(1, GenerateBoard(&g_asset.board)); + CHECK_EQ(0, pthread_create(&boarder, 0, BoardWorker, 0)); + pthread_t claimer; + CHECK_EQ(0, pthread_create(&claimer, 0, ClaimWorker, 0)); + pthread_t nower; + UpdateNow(); + CHECK_EQ(0, pthread_create(&nower, 0, NowWorker, 0)); + pthread_t *httper = _gc(xcalloc(g_workers, sizeof(pthread_t))); + for (intptr_t i = 0; i < g_workers; ++i) { + CHECK_EQ(0, pthread_create(httper + i, 0, HttpWorker, (void *)i)); + } + + // main thread activity + struct sigaction sa = {.sa_handler = OnCtrlC}; + sigaction(SIGHUP, &sa, 0); + sigaction(SIGINT, &sa, 0); + sigaction(SIGTERM, &sa, 0); + AssetWorker(0); + + // wait for threads to finish + for (int i = 0; i < g_workers; ++i) { + CHECK_EQ(0, pthread_join(httper[i], 0)); + } + CHECK_EQ(0, pthread_join(claimer, 0)); + CHECK_EQ(0, pthread_join(boarder, 0)); + CHECK_EQ(0, pthread_join(nower, 0)); + + // free memory + FreeAsset(&g_asset.user); + FreeAsset(&g_asset.about); + FreeAsset(&g_asset.index); + nsync_note_free(g_shutdown); + // CheckForMemoryLeaks(); +} diff --git a/net/turfwar/turfwar.mk b/net/turfwar/turfwar.mk new file mode 100644 index 000000000..30aa0130b --- /dev/null +++ b/net/turfwar/turfwar.mk @@ -0,0 +1,61 @@ +#-*-mode:makefile-gmake;indent-tabs-mode:t;tab-width:8;coding:utf-8-*-┐ +#───vi: set et ft=make ts=8 tw=8 fenc=utf-8 :vi───────────────────────┘ + +PKGS += NET_TURFWAR + +NET_TURFWAR_SRCS := $(wildcard net/turfwar/*.c) + +NET_TURFWAR_OBJS = \ + $(NET_TURFWAR_SRCS:%.c=o/$(MODE)/%.o) + +NET_TURFWAR_COMS = \ + $(NET_TURFWAR_SRCS:%.c=o/$(MODE)/%.com) + +NET_TURFWAR_BINS = \ + $(NET_TURFWAR_COMS) \ + $(NET_TURFWAR_COMS:%=%.dbg) + +NET_TURFWAR_DIRECTDEPS = \ + LIBC_CALLS \ + LIBC_FMT \ + LIBC_INTRIN \ + LIBC_LOG \ + LIBC_MEM \ + LIBC_NEXGEN32E \ + LIBC_RUNTIME \ + LIBC_SOCK \ + LIBC_STDIO \ + LIBC_STR \ + LIBC_STUBS \ + LIBC_SYSV \ + LIBC_THREAD \ + LIBC_TIME \ + LIBC_X \ + NET_HTTP \ + THIRD_PARTY_GETOPT \ + THIRD_PARTY_NSYNC \ + THIRD_PARTY_NSYNC_MEM \ + THIRD_PARTY_SQLITE3 \ + THIRD_PARTY_ZLIB + +NET_TURFWAR_DEPS := \ + $(call uniq,$(foreach x,$(NET_TURFWAR_DIRECTDEPS),$($(x)))) + +o/$(MODE)/net/turfwar/turfwar.pkg: \ + $(NET_TURFWAR_OBJS) \ + $(foreach x,$(NET_TURFWAR_DIRECTDEPS),$($(x)_A).pkg) + +o/$(MODE)/net/turfwar/%.com.dbg: \ + $(NET_TURFWAR_DEPS) \ + o/$(MODE)/net/turfwar/%.o \ + o/$(MODE)/net/turfwar/turfwar.pkg \ + $(CRT) \ + $(APE_NO_MODIFY_SELF) + @$(APELINK) + +$(NET_TURFWAR_OBJS): \ + $(BUILD_FILES) \ + net/turfwar/turfwar.mk + +.PHONY: o/$(MODE)/net/turfwar +o/$(MODE)/net/turfwar: $(NET_TURFWAR_BINS) $(NET_TURFWAR_CHECKS) diff --git a/test/libc/intrin/pthread_once_test.c b/test/libc/intrin/pthread_once_test.c index dc831aa22..07b075bc9 100644 --- a/test/libc/intrin/pthread_once_test.c +++ b/test/libc/intrin/pthread_once_test.c @@ -17,9 +17,10 @@ │ PERFORMANCE OF THIS SOFTWARE. │ ╚─────────────────────────────────────────────────────────────────────────────*/ #include "libc/atomic.h" +#include "libc/dce.h" #include "libc/intrin/atomic.h" -#include "libc/mem/mem.h" #include "libc/mem/gc.internal.h" +#include "libc/mem/mem.h" #include "libc/testlib/testlib.h" #include "libc/thread/spawn.h" #include "libc/thread/thread.h" @@ -54,3 +55,11 @@ TEST(pthread_once, test) { ASSERT_EQ(1, atomic_load(&y)); ASSERT_EQ(0, pthread_barrier_destroy(&b)); } + +__attribute__((__constructor__)) static void init(void) { + // try to test both the nsync and non-nsync versions with regular builds + if (!IsTiny()) { + pthread_cond_t c = {0}; + pthread_cond_broadcast(&c); + } +} diff --git a/third_party/nsync/mem/nsync_cv.c b/third_party/nsync/mem/nsync_cv.c index 5b859f939..7e2cf1428 100644 --- a/third_party/nsync/mem/nsync_cv.c +++ b/third_party/nsync/mem/nsync_cv.c @@ -24,6 +24,10 @@ #include "third_party/nsync/wait_s.internal.h" #include "third_party/nsync/waiter.h" +// once we're paying the cost of nsync we might as well get the benefit +// of a better pthread_once(), since no other component pulls it in now +STATIC_YOINK("nsync_run_once"); + asm(".ident\t\"\\n\\n\ *NSYNC (Apache 2.0)\\n\ Copyright 2016 Google, Inc.\\n\ diff --git a/third_party/sqlite3/os_unix.c b/third_party/sqlite3/os_unix.c index 4c4be88c7..752a4526e 100644 --- a/third_party/sqlite3/os_unix.c +++ b/third_party/sqlite3/os_unix.c @@ -5766,6 +5766,7 @@ static const char *unixTempFileDir(void){ static int unixGetTempname(int nBuf, char *zBuf){ const char *zDir; int iLimit = 0; + int e = errno; // [jart] don't pollute strace logs /* It's odd to simulate an io-error here, but really this is just ** using the io-error infrastructure to test that SQLite handles this @@ -5785,6 +5786,7 @@ static int unixGetTempname(int nBuf, char *zBuf){ zDir, r, 0); if( zBuf[nBuf-2]!=0 || (iLimit++)>10 ) return SQLITE_ERROR; }while( osAccess(zBuf,0)==0 ); + errno = e; // [jart] don't pollute strace logs return SQLITE_OK; } @@ -6317,8 +6319,10 @@ static int unixAccess( if( flags==SQLITE_ACCESS_EXISTS ){ struct stat buf; + int e = errno; // [jart] don't clobber errno *pResOut = 0==osStat(zPath, &buf) && (!S_ISREG(buf.st_mode) || buf.st_size>0); + errno = e; // [jart] don't clobber errno }else{ *pResOut = osAccess(zPath, W_OK|R_OK)==0; }