diff options
author | Ralph Amissah <ralph.amissah@gmail.com> | 2022-02-25 19:59:47 -0500 |
---|---|---|
committer | Ralph Amissah <ralph.amissah@gmail.com> | 2022-02-25 20:54:19 -0500 |
commit | 78a231014be3a76e9e546b31a5e6fa2a9a7b720e (patch) | |
tree | 89c64cc66898e20b7f2f81e837df68f265c62757 /sundry/spine_search_cgi/src/ext_depends_cgi/arsd | |
parent | verbosity level, "vox_gt[lv]" (voice greater than) (diff) |
external dependency update, housekeeping, routine
Diffstat (limited to 'sundry/spine_search_cgi/src/ext_depends_cgi/arsd')
-rw-r--r-- | sundry/spine_search_cgi/src/ext_depends_cgi/arsd/cgi.d | 1049 |
1 files changed, 857 insertions, 192 deletions
diff --git a/sundry/spine_search_cgi/src/ext_depends_cgi/arsd/cgi.d b/sundry/spine_search_cgi/src/ext_depends_cgi/arsd/cgi.d index 9ac46b9..9189052 100644 --- a/sundry/spine_search_cgi/src/ext_depends_cgi/arsd/cgi.d +++ b/sundry/spine_search_cgi/src/ext_depends_cgi/arsd/cgi.d @@ -85,6 +85,8 @@ void main() { # now you can go to http://localhost:8080/?name=whatever ) + Please note: the default port for http is 8085 and for cgi is 4000. I recommend you set your own by the command line argument in a startup script instead of relying on any hard coded defaults. It is possible though to hard code your own with [RequestServer]. + Compile_versions: @@ -370,6 +372,15 @@ version(Posix) { } } +version(Windows) { + version(minimal) { + + } else { + // not too concerned about gdc here since the mingw version is fairly new as well + version=with_breaking_cgi_features; + } +} + void cloexec(int fd) { version(Posix) { import core.sys.posix.fcntl; @@ -386,7 +397,7 @@ void cloexec(Socket s) { version(embedded_httpd_hybrid) { version=embedded_httpd_threads; - version(cgi_no_fork) {} else + version(cgi_no_fork) {} else version(Posix) version=cgi_use_fork; version=cgi_use_fiber; } @@ -564,14 +575,6 @@ private struct stdin { import core.sys.windows.windows; static: - static this() { - // Set stdin to binary mode - version(Win64) - _setmode(std.stdio.stdin.fileno(), 0x8000); - else - setmode(std.stdio.stdin.fileno(), 0x8000); - } - T[] rawRead(T)(T[] buf) { uint bytesRead; auto result = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf.ptr, cast(int) (buf.length * T.sizeof), &bytesRead, null); @@ -808,7 +811,7 @@ class Cgi { } } else { // it is an argument of some sort - if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT) { + if(requestMethod == Cgi.RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { auto parts = breakUp(arg); _post[parts[0]] ~= parts[1]; allPostNamesInOrder ~= parts[0]; @@ -1017,7 +1020,7 @@ class Cgi { // FIXME: DOCUMENT_ROOT? // FIXME: what about PUT? - if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT) { + if(requestMethod == RequestMethod.POST || requestMethod == Cgi.RequestMethod.PATCH || requestMethod == Cgi.RequestMethod.PUT || requestMethod == Cgi.RequestMethod.CommandLine) { version(preserveData) // a hack to make forwarding simpler immutable(ubyte)[] data; size_t amountReceived = 0; @@ -2004,10 +2007,13 @@ class Cgi { uri ~= "s"; uri ~= "://"; uri ~= host; + /+ // the host has the port so p sure this never needed, cgi on apache and embedded http all do the right hting now + version(none) if(!(!port || port == defaultPort)) { uri ~= ":"; uri ~= to!string(port); } + +/ uri ~= requestUri; return uri; } @@ -3294,6 +3300,8 @@ mixin template DispatcherMain(Presenter, DispatcherArgs...) { /++ Request handler that creates the presenter then forwards to the [dispatcher] function. Renders 404 if the dispatcher did not handle the request. + + Will automatically serve the presenter.style and presenter.script as "style.css" and "script.js" +/ void handler(Cgi cgi) { auto presenter = new Presenter; @@ -3303,11 +3311,29 @@ mixin template DispatcherMain(Presenter, DispatcherArgs...) { if(cgi.dispatcher!DispatcherArgs(presenter)) return; - presenter.renderBasicError(cgi, 404); + switch(cgi.pathInfo) { + case "/style.css": + cgi.setCache(true); + cgi.setResponseContentType("text/css"); + cgi.write(presenter.style(), true); + break; + case "/script.js": + cgi.setCache(true); + cgi.setResponseContentType("application/javascript"); + cgi.write(presenter.script(), true); + break; + default: + presenter.renderBasicError(cgi, 404); + } } mixin GenericMain!handler; } +mixin template DispatcherMain(DispatcherArgs...) if(!is(DispatcherArgs[0] : WebPresenter!T, T)) { + class GenericPresenter : WebPresenter!GenericPresenter {} + mixin DispatcherMain!(GenericPresenter, DispatcherArgs); +} + private string simpleHtmlEncode(string s) { return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\n", "<br />\n"); } @@ -3463,7 +3489,11 @@ struct RequestServer { listeningPort = defaultPort; } - /// Reads the args into the other values. + /++ + Reads the command line arguments into the values here. + + Possible arguments are `--listening-host`, `--listening-port` (or `--port`), `--uid`, and `--gid`. + +/ void configureFromCommandLine(string[] args) { bool foundPort = false; bool foundHost = false; @@ -3567,10 +3597,27 @@ struct RequestServer { } } - void serveEmbeddedHttp(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { - auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, fun)); + /++ + Runs the embedded HTTP thread server specifically, regardless of which build configuration you have. + + If you want the forking worker process server, you do need to compile with the embedded_httpd_processes config though. + +/ + void serveEmbeddedHttp(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)(ThisFor!fun _this) { + static if(__traits(isStaticFunction, fun)) + alias funToUse = fun; + else + void funToUse(CustomCgi cgi) { + static if(__VERSION__ > 2097) + __traits(child, _this, fun)(cgi); + else static assert(0, "Not implemented in your compiler version!"); + } + auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadHttpConnection!(CustomCgi, funToUse)); manager.listen(); } + + /++ + Runs the embedded SCGI server specifically, regardless of which build configuration you have. + +/ void serveScgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { auto manager = new ListeningConnectionManager(listeningHost, listeningPort, &doThreadScgiConnection!(CustomCgi, fun, maxContentLength)); manager.listen(); @@ -3588,11 +3635,50 @@ struct RequestServer { doThreadHttpConnectionGuts!(CustomCgi, fun, true)(new FakeSocketForStdin()); } - void stop() { - // FIXME + /++ + Stops serving after the current requests. + + Bugs: + Not implemented on version=embedded_httpd_processes, version=fastcgi, or on any operating system aside from Linux at this time. + Try SIGINT there perhaps. + + A Windows implementation is planned but not sure about the others. Maybe a posix pipe can be used on other OSes. I do not intend + to implement this for the processes config. + +/ + version(embedded_httpd_processes) {} else + static void stop() { + globalStopFlag = true; + + version(Posix) + if(cancelfd > 0) { + ulong a = 1; + core.sys.posix.unistd.write(cancelfd, &a, a.sizeof); + } + version(Windows) + if(iocp) { + foreach(i; 0 .. 16) // FIXME + PostQueuedCompletionStatus(iocp, 0, cast(ULONG_PTR) null, null); + } } } +private alias AliasSeq(T...) = T; + +version(with_breaking_cgi_features) +mixin(q{ + template ThisFor(alias t) { + static if(__traits(isStaticFunction, t)) { + alias ThisFor = AliasSeq!(); + } else { + alias ThisFor = __traits(parent, t); + } + } +}); +else + alias ThisFor(alias t) = AliasSeq!(); + +private __gshared bool globalStopFlag = false; + private int privDropUserId; private int privDropGroupId; @@ -3726,7 +3812,7 @@ void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer param Cgi cgi; try { cgi = new CustomCgi(ir, &closeConnection); - cgi._outputFileHandle = s; + cgi._outputFileHandle = cast(CgiConnectionHandle) s; // if we have a single process and the browser tries to leave the connection open while concurrently requesting another, it will block everything an deadlock since there's no other server to accept it. By closing after each request in this situation, it tells the browser to serialize for us. if(processPoolSize <= 1) closeConnection = true; @@ -3927,13 +4013,24 @@ void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMax } auto lp = params.listeningPort; + auto host = params.listeningHost; FCGX_Request request; - if(lp) { + if(lp || !host.empty) { // if a listening port was specified on the command line, we want to spawn ourself // (needed for nginx without spawn-fcgi, e.g. on Windows) FCGX_Init(); - auto sock = FCGX_OpenSocket(toStringz(params.listeningHost ~ ":" ~ to!string(lp)), 12); + + int sock; + + if(host.startsWith("unix:")) { + sock = FCGX_OpenSocket(toStringz(params.listeningHost["unix:".length .. $]), 12); + } else if(host.startsWith("abstract:")) { + sock = FCGX_OpenSocket(toStringz("\0" ~ params.listeningHost["abstract:".length .. $]), 12); + } else { + sock = FCGX_OpenSocket(toStringz(params.listeningHost ~ ":" ~ to!string(lp)), 12); + } + if(sock < 0) throw new Exception("Couldn't listen on the port"); FCGX_InitRequest(&request, sock, 0); @@ -4015,13 +4112,25 @@ void cgiMainImpl(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxC //version(plain_cgi) void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)() { // standard CGI is the default version + + + // Set stdin to binary mode if necessary to avoid mangled newlines + // the fact that stdin is global means this could be trouble but standard cgi request + // handling is one per process anyway so it shouldn't actually be threaded here or anything. + version(Windows) { + version(Win64) + _setmode(std.stdio.stdin.fileno(), 0x8000); + else + setmode(std.stdio.stdin.fileno(), 0x8000); + } + Cgi cgi; try { cgi = new CustomCgi(maxContentLength); version(Posix) - cgi._outputFileHandle = 1; // stdout + cgi._outputFileHandle = cast(CgiConnectionHandle) 1; // stdout else version(Windows) - cgi._outputFileHandle = GetStdHandle(STD_OUTPUT_HANDLE); + cgi._outputFileHandle = cast(CgiConnectionHandle) GetStdHandle(STD_OUTPUT_HANDLE); else static assert(0); } catch(Throwable t) { version(CRuntime_Musl) { @@ -4058,6 +4167,8 @@ void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaul } } +private __gshared int cancelfd = -1; + /+ The event loop for embedded_httpd_threads will prolly fiber dispatch cgi constructors too, so slow posts will not monopolize a worker thread. @@ -4140,22 +4251,259 @@ class CgiFiber : Fiber { } void proceed() { - call(); - auto py = postYield; - postYield = null; - if(py !is null) - py(); + try { + call(); + auto py = postYield; + postYield = null; + if(py !is null) + py(); + } catch(Exception e) { + if(connection) + connection.close(); + goto terminate; + } + if(state == State.TERM) { + terminate: import core.memory; GC.removeRoot(cast(void*) this); } } } +version(cgi_use_fiber) +version(Windows) { + +extern(Windows) private { + + import core.sys.windows.mswsock; + + alias GROUP=uint; + alias LPWSAPROTOCOL_INFOW = void*; + SOCKET WSASocketW(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags); + int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); + int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine); + + struct WSABUF { + ULONG len; + CHAR *buf; + } + alias LPWSABUF = WSABUF*; + + alias WSAOVERLAPPED = OVERLAPPED; + alias LPWSAOVERLAPPED = LPOVERLAPPED; + /+ + + alias LPFN_ACCEPTEX = + BOOL + function( + SOCKET sListenSocket, + SOCKET sAcceptSocket, + //_Out_writes_bytes_(dwReceiveDataLength+dwLocalAddressLength+dwRemoteAddressLength) PVOID lpOutputBuffer, + void* lpOutputBuffer, + WORD dwReceiveDataLength, + WORD dwLocalAddressLength, + WORD dwRemoteAddressLength, + LPDWORD lpdwBytesReceived, + LPOVERLAPPED lpOverlapped + ); + + enum WSAID_ACCEPTEX = GUID([0xb5367df1,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]]); + +/ + + enum WSAID_GETACCEPTEXSOCKADDRS = GUID(0xb5367df2,0xcbac,0x11cf,[0x95,0xca,0x00,0x80,0x5f,0x48,0xa1,0x92]); +} + +private class PseudoblockingOverlappedSocket : Socket { + SOCKET handle; + + CgiFiber fiber; + + this(AddressFamily af, SocketType st) { + auto handle = WSASocketW(af, st, 0, null, 0, 1 /*WSA_FLAG_OVERLAPPED*/); + if(!handle) + throw new Exception("WSASocketW"); + this.handle = handle; + + iocp = CreateIoCompletionPort(cast(HANDLE) handle, iocp, cast(ULONG_PTR) cast(void*) this, 0); + + if(iocp is null) { + writeln(GetLastError()); + throw new Exception("CreateIoCompletionPort"); + } + + super(cast(socket_t) handle, af); + } + this() pure nothrow @trusted { assert(0); } + + override void blocking(bool) {} // meaningless to us, just ignore it. + + protected override Socket accepting() pure nothrow { + assert(0); + } + + bool addressesParsed; + Address la; + Address ra; + + private void populateAddresses() { + if(addressesParsed) + return; + addressesParsed = true; + + int lalen, ralen; + + sockaddr_in* la; + sockaddr_in* ra; + + lpfnGetAcceptExSockaddrs( + scratchBuffer.ptr, + 0, // same as in the AcceptEx call! + sockaddr_in.sizeof + 16, + sockaddr_in.sizeof + 16, + cast(sockaddr**) &la, + &lalen, + cast(sockaddr**) &ra, + &ralen + ); + + if(la) + this.la = new InternetAddress(*la); + if(ra) + this.ra = new InternetAddress(*ra); + + } + + override @property @trusted Address localAddress() { + populateAddresses(); + return la; + } + override @property @trusted Address remoteAddress() { + populateAddresses(); + return ra; + } + + PseudoblockingOverlappedSocket accepted; + + __gshared static LPFN_ACCEPTEX lpfnAcceptEx; + __gshared static typeof(&GetAcceptExSockaddrs) lpfnGetAcceptExSockaddrs; + + override Socket accept() @trusted { + __gshared static LPFN_ACCEPTEX lpfnAcceptEx; + + if(lpfnAcceptEx is null) { + DWORD dwBytes; + GUID GuidAcceptEx = WSAID_ACCEPTEX; + + auto iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, + &GuidAcceptEx, GuidAcceptEx.sizeof, + &lpfnAcceptEx, lpfnAcceptEx.sizeof, + &dwBytes, null, null); + + GuidAcceptEx = WSAID_GETACCEPTEXSOCKADDRS; + iResult = WSAIoctl(handle, 0xc8000006 /*SIO_GET_EXTENSION_FUNCTION_POINTER*/, + &GuidAcceptEx, GuidAcceptEx.sizeof, + &lpfnGetAcceptExSockaddrs, lpfnGetAcceptExSockaddrs.sizeof, + &dwBytes, null, null); + + } + + auto pfa = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); + accepted = pfa; + + SOCKET pendingForAccept = pfa.handle; + DWORD ignored; + + auto ret = lpfnAcceptEx(handle, + pendingForAccept, + // buffer to receive up front + pfa.scratchBuffer.ptr, + 0, + // size of local and remote addresses. normally + 16. + sockaddr_in.sizeof + 16, + sockaddr_in.sizeof + 16, + &ignored, // bytes would be given through the iocp instead but im not even requesting the thing + &overlapped + ); + + return pfa; + } + + override void connect(Address to) { assert(0); } + + DWORD lastAnswer; + ubyte[1024] scratchBuffer; + static assert(scratchBuffer.length > sockaddr_in.sizeof * 2 + 32); + + WSABUF[1] buffer; + OVERLAPPED overlapped; + override ptrdiff_t send(const(void)[] buf, SocketFlags flags) @trusted { + overlapped = overlapped.init; + buffer[0].len = cast(DWORD) buf.length; + buffer[0].buf = cast(CHAR*) buf.ptr; + fiber.setPostYield( () { + if(!WSASend(handle, buffer.ptr, cast(DWORD) buffer.length, null, 0, &overlapped, null)) { + if(GetLastError() != 997) { + //throw new Exception("WSASend fail"); + } + } + }); + + Fiber.yield(); + return lastAnswer; + } + override ptrdiff_t receive(void[] buf, SocketFlags flags) @trusted { + overlapped = overlapped.init; + buffer[0].len = cast(DWORD) buf.length; + buffer[0].buf = cast(CHAR*) buf.ptr; + + DWORD flags2 = 0; + + fiber.setPostYield(() { + if(!WSARecv(handle, buffer.ptr, cast(DWORD) buffer.length, null, &flags2 /* flags */, &overlapped, null)) { + if(GetLastError() != 997) { + //writeln("WSARecv ", WSAGetLastError()); + //throw new Exception("WSARecv fail"); + } + } + }); + + Fiber.yield(); + return lastAnswer; + } + + // I might go back and implement these for udp things. + override ptrdiff_t receiveFrom(void[] buf, SocketFlags flags, ref Address from) @trusted { + assert(0); + } + override ptrdiff_t receiveFrom(void[] buf, SocketFlags flags) @trusted { + assert(0); + } + override ptrdiff_t sendTo(const(void)[] buf, SocketFlags flags, Address to) @trusted { + assert(0); + } + override ptrdiff_t sendTo(const(void)[] buf, SocketFlags flags) @trusted { + assert(0); + } + + // lol overload sets + alias send = typeof(super).send; + alias receive = typeof(super).receive; + alias sendTo = typeof(super).sendTo; + alias receiveFrom = typeof(super).receiveFrom; + +} +} + void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) { assert(connection !is null); version(cgi_use_fiber) { auto fiber = new CgiFiber(&doThreadHttpConnectionGuts!(CustomCgi, fun)); + + version(Windows) { + (cast(PseudoblockingOverlappedSocket) connection).fiber = fiber; + } + import core.memory; GC.addRoot(cast(void*) fiber); fiber.connection = connection; @@ -4168,8 +4516,10 @@ void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) { void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) { scope(failure) { // catch all for other errors - sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); - connection.close(); + try { + sendAll(connection, plainHttpError(false, "500 Internal Server Error", null)); + connection.close(); + } catch(Exception e) {} // swallow it, we're aborting anyway. } bool closeConnection = alwaysCloseConnection; @@ -4200,7 +4550,11 @@ void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection Cgi cgi; try { cgi = new CustomCgi(ir, &closeConnection); - cgi._outputFileHandle = connection.handle; + // There's a bunch of these casts around because the type matches up with + // the -version=.... specifiers, just you can also create a RequestServer + // and instantiate the things where the types don't match up. It isn't exactly + // correct but I also don't care rn. Might FIXME and either remove it later or something. + cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; } catch(ConnectionClosedException ce) { closeConnection = true; break; @@ -4244,6 +4598,9 @@ void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection closeConnection = true; } + if(globalStopFlag) + closeConnection = true; + if(closeConnection || alwaysCloseConnection) { connection.shutdown(SocketShutdown.BOTH); connection.close(); @@ -4351,7 +4708,7 @@ void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket Cgi cgi; try { cgi = new CustomCgi(maxContentLength, headers, &getScgiChunk, &writeScgi, &flushScgi); - cgi._outputFileHandle = connection.handle; + cgi._outputFileHandle = cast(CgiConnectionHandle) connection.handle; } catch(Throwable t) { sendAll(connection, plainHttpError(true, "400 Bad Request", t)); connection.close(); @@ -4509,16 +4866,30 @@ import std.socket; version(cgi_use_fiber) { import core.thread; - import core.sys.linux.epoll; - __gshared int epfd; + version(linux) { + import core.sys.linux.epoll; + + int epfd = -1; // thread local because EPOLLEXCLUSIVE works much better this way... weirdly. + } else version(Windows) { + // declaring the iocp thing below... + } else static assert(0, "The hybrid fiber server is not implemented on your OS."); } +version(Windows) + __gshared HANDLE iocp; -version(cgi_use_fiber) -private enum WakeupEvent { - Read = EPOLLIN, - Write = EPOLLOUT +version(cgi_use_fiber) { + version(linux) + private enum WakeupEvent { + Read = EPOLLIN, + Write = EPOLLOUT + } + else version(Windows) + private enum WakeupEvent { + Read, Write + } + else static assert(0); } version(cgi_use_fiber) @@ -4527,35 +4898,45 @@ private void registerEventWakeup(bool* registered, Socket source, WakeupEvent e) // static cast since I know what i have in here and don't want to pay for dynamic cast auto f = cast(CgiFiber) cast(void*) Fiber.getThis(); - f.setPostYield = () { - if(*registered) { - // rearm - epoll_event evt; - evt.events = e | EPOLLONESHOT; - evt.data.ptr = cast(void*) f; - if(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1) - throw new Exception("epoll_ctl"); - } else { - // initial registration - *registered = true ; - int fd = source.handle; - epoll_event evt; - evt.events = e | EPOLLONESHOT; - evt.data.ptr = cast(void*) f; - if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1) - throw new Exception("epoll_ctl"); - } - }; + version(linux) { + f.setPostYield = () { + if(*registered) { + // rearm + epoll_event evt; + evt.events = e | EPOLLONESHOT; + evt.data.ptr = cast(void*) f; + if(epoll_ctl(epfd, EPOLL_CTL_MOD, source.handle, &evt) == -1) + throw new Exception("epoll_ctl"); + } else { + // initial registration + *registered = true ; + int fd = source.handle; + epoll_event evt; + evt.events = e | EPOLLONESHOT; + evt.data.ptr = cast(void*) f; + if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &evt) == -1) + throw new Exception("epoll_ctl"); + } + }; - Fiber.yield(); + Fiber.yield(); - f.setPostYield(null); + f.setPostYield(null); + } else version(Windows) { + Fiber.yield(); + } + else static assert(0); } version(cgi_use_fiber) void unregisterSource(Socket s) { - epoll_event evt; - epoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt); + version(linux) { + epoll_event evt; + epoll_ctl(epfd, EPOLL_CTL_DEL, s.handle(), &evt); + } else version(Windows) { + // intentionally blank + } + else static assert(0); } // it is a class primarily for reference semantics @@ -4788,8 +5169,36 @@ class ListeningConnectionManager { ubyte nextIndexBack; shared(int) queueLength; + Socket acceptCancelable() { + version(Posix) { + import core.sys.posix.sys.select; + fd_set read_fds; + FD_ZERO(&read_fds); + FD_SET(listener.handle, &read_fds); + FD_SET(cancelfd, &read_fds); + auto max = listener.handle > cancelfd ? listener.handle : cancelfd; + auto ret = select(max + 1, &read_fds, null, null, null); + if(ret == -1) { + import core.stdc.errno; + if(errno == EINTR) + return null; + else + throw new Exception("wtf select"); + } + + if(FD_ISSET(cancelfd, &read_fds)) { + return null; + } + + if(FD_ISSET(listener.handle, &read_fds)) + return listener.accept(); + + return null; + } else + return listener.accept(); // FIXME: check the cancel flag! + } + void listen() { - running = true; shared(int) loopBroken; version(Posix) { @@ -4797,14 +5206,20 @@ class ListeningConnectionManager { signal(SIGPIPE, SIG_IGN); } + version(linux) { + if(cancelfd == -1) + cancelfd = eventfd(0, 0); + } + version(cgi_no_threads) { // NEVER USE THIS // it exists only for debugging and other special occasions // the thread mode is faster and less likely to stall the whole // thing when a request is slow - while(!loopBroken && running) { - auto sn = listener.accept(); + while(!loopBroken && !globalStopFlag) { + auto sn = acceptCancelable(); + if(sn is null) continue; cloexec(sn); try { handler(sn); @@ -4822,20 +5237,10 @@ class ListeningConnectionManager { } version(cgi_use_fiber) { - import core.sys.linux.epoll; - epfd = epoll_create1(EPOLL_CLOEXEC); - if(epfd == -1) - throw new Exception("epoll_create1 " ~ to!string(errno)); - scope(exit) { - import core.sys.posix.unistd; - close(epfd); - } - epoll_event ev; - ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. - ev.data.fd = listener.handle; - if(epoll_ctl(epfd, EPOLL_CTL_ADD, listener.handle, &ev) == -1) - throw new Exception("epoll_ctl " ~ to!string(errno)); + version(Windows) { + listener.accept(); + } WorkerThread[] threads = new WorkerThread[](totalCPUs * 1 + 1); foreach(i, ref thread; threads) { @@ -4855,7 +5260,7 @@ class ListeningConnectionManager { } - while(running) { + while(!globalStopFlag) { Thread.sleep(1.seconds); if(fiber_crash_check()) break; @@ -4870,58 +5275,66 @@ class ListeningConnectionManager { thread = new ConnectionThread(this, handler, cast(int) i); thread.start(); } - } - while(!loopBroken && running) { - Socket sn; + while(!loopBroken && !globalStopFlag) { + Socket sn; - bool crash_check() { - bool hasAnyRunning; - foreach(thread; threads) { - if(!thread.isRunning) { - thread.join(); - } else hasAnyRunning = true; - } + bool crash_check() { + bool hasAnyRunning; + foreach(thread; threads) { + if(!thread.isRunning) { + thread.join(); + } else hasAnyRunning = true; + } - return (!hasAnyRunning); - } + return (!hasAnyRunning); + } - void accept_new_connection() { - sn = listener.accept(); - cloexec(sn); - if(tcp) { - // disable Nagle's algorithm to avoid a 40ms delay when we send/recv - // on the socket because we do some buffering internally. I think this helps, - // certainly does for small requests, and I think it does for larger ones too - sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); + void accept_new_connection() { + sn = acceptCancelable(); + if(sn is null) return; + cloexec(sn); + if(tcp) { + // disable Nagle's algorithm to avoid a 40ms delay when we send/recv + // on the socket because we do some buffering internally. I think this helps, + // certainly does for small requests, and I think it does for larger ones too + sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); - sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); + sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); + } } - } - void existing_connection_new_data() { - // wait until a slot opens up - //int waited = 0; - while(queueLength >= queue.length) { - Thread.sleep(1.msecs); - //waited ++; - } - //if(waited) {import std.stdio; writeln(waited);} - synchronized(this) { - queue[nextIndexBack] = sn; - nextIndexBack++; - atomicOp!"+="(queueLength, 1); + void existing_connection_new_data() { + // wait until a slot opens up + //int waited = 0; + while(queueLength >= queue.length) { + Thread.sleep(1.msecs); + //waited ++; + } + //if(waited) {import std.stdio; writeln(waited);} + synchronized(this) { + queue[nextIndexBack] = sn; + nextIndexBack++; + atomicOp!"+="(queueLength, 1); + } + semaphore.notify(); } - semaphore.notify(); - } - accept_new_connection(); - existing_connection_new_data(); + accept_new_connection(); + if(sn !is null) + existing_connection_new_data(); + else if(sn is null && globalStopFlag) { + foreach(thread; threads) { + semaphore.notify(); + } + Thread.sleep(50.msecs); + } - if(crash_check()) - break; + if(crash_check()) + break; + } } // FIXME: i typically stop this with ctrl+c which never @@ -4960,11 +5373,6 @@ class ListeningConnectionManager { Socket listener; void delegate(Socket) handler; - - bool running; - void quit() { - running = false; - } } Socket startListening(string host, ushort port, ref bool tcp, ref void delegate() cleanup, int backQueue) { @@ -4996,7 +5404,14 @@ Socket startListening(string host, ushort port, ref bool tcp, ref void delegate( throw new Exception("abstract unix sockets not supported on this system"); } } else { - listener = new TcpSocket(); + version(cgi_use_fiber) { + version(Windows) + listener = new PseudoblockingOverlappedSocket(AddressFamily.INET, SocketType.STREAM); + else + listener = new TcpSocket(); + } else { + listener = new TcpSocket(); + } cloexec(listener); listener.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); listener.bind(host.length ? parseAddress(host, port) : new InternetAddress(port)); @@ -5103,6 +5518,8 @@ class ConnectionThread : Thread { // so if there's a bunch of idle keep-alive connections, it can // consume all the worker threads... just sitting there. lcm.semaphore.wait(); + if(globalStopFlag) + return; Socket socket; synchronized(lcm) { auto idx = lcm.nextIndexFront; @@ -5164,8 +5581,69 @@ class WorkerThread : Thread { super(&run); } + version(Windows) void run() { - while(lcm.running) { + auto timeout = INFINITE; + PseudoblockingOverlappedSocket key; + OVERLAPPED* overlapped; + DWORD bytes; + while(!globalStopFlag && GetQueuedCompletionStatus(iocp, &bytes, cast(PULONG_PTR) &key, &overlapped, timeout)) { + if(key is null) + continue; + key.lastAnswer = bytes; + if(key.fiber) { + key.fiber.proceed(); + } else { + // we have a new connection, issue the first receive on it and issue the next accept + + auto sn = key.accepted; + + key.accept(); + + cloexec(sn); + if(lcm.tcp) { + // disable Nagle's algorithm to avoid a 40ms delay when we send/recv + // on the socket because we do some buffering internally. I think this helps, + // certainly does for small requests, and I think it does for larger ones too + sn.setOption(SocketOptionLevel.TCP, SocketOption.TCP_NODELAY, 1); + + sn.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(10)); + } + + dg(sn); + } + } + //SleepEx(INFINITE, TRUE); + } + + version(linux) + void run() { + + import core.sys.linux.epoll; + epfd = epoll_create1(EPOLL_CLOEXEC); + if(epfd == -1) + throw new Exception("epoll_create1 " ~ to!string(errno)); + scope(exit) { + import core.sys.posix.unistd; + close(epfd); + } + + { + epoll_event ev; + ev.events = EPOLLIN; + ev.data.fd = cancelfd; + epoll_ctl(epfd, EPOLL_CTL_ADD, cancelfd, &ev); + } + + epoll_event ev; + ev.events = EPOLLIN | EPOLLEXCLUSIVE; // EPOLLEXCLUSIVE is only available on kernels since like 2017 but that's prolly good enough. + ev.data.fd = lcm.listener.handle; + if(epoll_ctl(epfd, EPOLL_CTL_ADD, lcm.listener.handle, &ev) == -1) + throw new Exception("epoll_ctl " ~ to!string(errno)); + + + + while(!globalStopFlag) { Socket sn; epoll_event[64] events; @@ -5179,18 +5657,19 @@ class WorkerThread : Thread { foreach(idx; 0 .. nfds) { auto flags = events[idx].events; - if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) { + if(cast(size_t) events[idx].data.ptr == cast(size_t) cancelfd) { + globalStopFlag = true; + //import std.stdio; writeln("exit heard"); + break; + } else if(cast(size_t) events[idx].data.ptr == cast(size_t) lcm.listener.handle) { + //import std.stdio; writeln(myThreadNumber, " woken up ", flags); // this try/catch is because it is set to non-blocking mode // and Phobos' stupid api throws an exception instead of returning // if it would block. Why would it block? because a forked process // might have beat us to it, but the wakeup event thundered our herds. - version(cgi_use_fork) { try - sn = lcm.listener.accept(); + sn = lcm.listener.accept(); // don't need to do the acceptCancelable here since the epoll checks it better catch(SocketAcceptException e) { continue; } - } else { - sn = lcm.listener.accept(); - } cloexec(sn); if(lcm.tcp) { @@ -5204,6 +5683,9 @@ class WorkerThread : Thread { dg(sn); } else { + if(cast(size_t) events[idx].data.ptr < 1024) { + throw new Exception("this doesn't look like a fiber pointer..."); + } auto fiber = cast(CgiFiber) events[idx].data.ptr; fiber.proceed(); } @@ -6366,11 +6848,14 @@ void freeIoOp(ref IoOp* ptr) { version(Posix) version(with_addon_servers_connections) void nonBlockingWrite(EventIoServer eis, int connection, const void[] data) { + + //import std.stdio : writeln; writeln(cast(string) data); + import core.sys.posix.unistd; auto ret = write(connection, data.ptr, data.length); if(ret != data.length) { - if(ret == 0 || errno == EPIPE) { + if(ret == 0 || (ret == -1 && (errno == EPIPE || errno == ETIMEDOUT))) { // the file is closed, remove it eis.fileClosed(connection); } else @@ -7396,7 +7881,9 @@ final class EventSourceServerImplementation : EventSourceServer, EventIoServer { } return false; } - void handleLocalConnectionClose(IoOp* op) {} + void handleLocalConnectionClose(IoOp* op) { + fileClosed(op.fd); + } void handleLocalConnectionComplete(IoOp* op) {} void wait_timeout() { @@ -7404,9 +7891,9 @@ final class EventSourceServerImplementation : EventSourceServer, EventIoServer { foreach(url, connections; eventConnectionsByUrl) foreach(connection; connections) if(connection.needsChunking) - nonBlockingWrite(this, connection.fd, "2\r\n:\n\r\n"); + nonBlockingWrite(this, connection.fd, "1b\r\nevent: keepalive\ndata: ok\n\n\r\n"); else - nonBlockingWrite(this, connection.fd, ":\n\r\n"); + nonBlockingWrite(this, connection.fd, "event: keepalive\ndata: ok\n\n\r\n"); } void fileClosed(int fd) { @@ -7672,11 +8159,20 @@ void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoS ioops[sock] = acceptOp; } + import core.time : MonoTime, seconds; + + MonoTime timeout = MonoTime.currTime + 15.seconds; + while(true) { // FIXME: it should actually do a timerfd that runs on any thing that hasn't been run recently - int timeout_milliseconds = 15000; // -1; // infinite + int timeout_milliseconds = 0; // -1; // infinite + + timeout_milliseconds = cast(int) (timeout - MonoTime.currTime).total!"msecs"; + if(timeout_milliseconds < 0) + timeout_milliseconds = 0; + //writeln("waiting for ", name); version(linux) { @@ -7693,6 +8189,7 @@ void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoS if(nfds == 0) { eis.wait_timeout(); + timeout += 15.seconds; } foreach(idx; 0 .. nfds) { @@ -7715,7 +8212,11 @@ void runAddonServer(EIS)(string localListenerName, EIS eis) if(is(EIS : EventIoS void newConnection() { // on edge triggering, it is important that we get it all while(true) { - auto size = cast(uint) addr.sizeof; + version(Android) { + auto size = cast(int) addr.sizeof; + } else { + auto size = cast(uint) addr.sizeof; + } auto ns = accept(sock, cast(sockaddr*) &addr, &size); if(ns == -1) { if(errno == EAGAIN || errno == EWOULDBLOCK) { @@ -7849,6 +8350,7 @@ ssize_t write_fd(int fd, void *ptr, size_t nbytes, int sendfd) { version(OSX) { //msg.msg_accrights = cast(cattr_t) &sendfd; //msg.msg_accrightslen = int.sizeof; + } else version(Android) { } else { union ControlUnion { cmsghdr cm; @@ -7890,6 +8392,7 @@ ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { version(OSX) { //msg.msg_accrights = cast(cattr_t) recvfd; //msg.msg_accrightslen = int.sizeof; + } else version(Android) { } else { union ControlUnion { cmsghdr cm; @@ -7916,6 +8419,7 @@ ssize_t read_fd(int fd, void *ptr, size_t nbytes, int *recvfd) { version(OSX) { //if(msg.msg_accrightslen != int.sizeof) //*recvfd = -1; + } else version(Android) { } else { if ( (cmptr = CMSG_FIRSTHDR(&msg)) != null && cmptr.cmsg_len == CMSG_LEN(int.sizeof)) { @@ -8053,6 +8557,25 @@ class MissingArgumentException : Exception { } /++ + You can throw this from an api handler to indicate a 404 response. This is done by the presentExceptionAsHtml function in the presenter. + + History: + Added December 15, 2021 (dub v10.5) ++/ +class ResourceNotFoundException : Exception { + string resourceType; + string resourceId; + + this(string resourceType, string resourceId, string file = __FILE__, size_t line = __LINE__, Throwable next = null) { + this.resourceType = resourceType; + this.resourceId = resourceId; + + super("Resource not found: " ~ resourceType ~ " " ~ resourceId, file, line, next); + } + +} + +/++ This can be attached to any constructor or function called from the cgi system. If it is present, the function argument can NOT be set from web params, but instead @@ -8365,6 +8888,74 @@ private bool hasIfCalledFromWeb(attrs...)() { +/ template AutomaticForm(alias customizer) { } +/++ + This is meant to be returned by a function that takes a form POST submission. You + want to set the url of the new resource it created, which is set as the http + Location header for a "201 Created" result, and you can also set a separate + destination for browser users, which it sets via a "Refresh" header. + + The `resourceRepresentation` should generally be the thing you just created, and + it will be the body of the http response when formatted through the presenter. + The exact thing is up to you - it could just return an id, or the whole object, or + perhaps a partial object. + + Examples: + --- + class Test : WebObject { + @(Cgi.RequestMethod.POST) + CreatedResource!int makeThing(string value) { + return CreatedResource!int(value.to!int, "/resources/id"); + } + } + --- + + History: + Added December 18, 2021 ++/ +struct CreatedResource(T) { + static if(!is(T == void)) + T resourceRepresentation; + string resourceUrl; + string refreshUrl; +} + +/+ +/++ + This can be attached as a UDA to a handler to add a http Refresh header on a + successful run. (It will not be attached if the function throws an exception.) + This will refresh the browser the given number of seconds after the page loads, + to the url returned by `urlFunc`, which can be either a static function or a + member method of the current handler object. + + You might use this for a POST handler that is normally used from ajax, but you + want it to degrade gracefully to a temporarily flashed message before reloading + the main page. + + History: + Added December 18, 2021 ++/ +struct Refresh(alias urlFunc) { + int waitInSeconds; + + string url() { + static if(__traits(isStaticFunction, urlFunc)) + return urlFunc(); + else static if(is(urlFunc : string)) + return urlFunc; + } +} ++/ + +/+ +/++ + Sets a filter to be run before + + A before function can do validations of params and log and stop the function from running. ++/ +template Before(alias b) {} +template After(alias b) {} ++/ + /+ Argument conversions: for the most part, it is to!Thing(string). @@ -8466,8 +9057,8 @@ class WebPresenter(CRTP) { :root { --mild-border: #ccc; --middle-border: #999; - --accent-color: #e8e8e8; - --sidebar-color: #f2f2f2; + --accent-color: #f2f2f2; + --sidebar-color: #fefefe; } ` ~ genericFormStyling() ~ genericSiteStyling(); } @@ -8615,6 +9206,17 @@ html", true, true); cgi.setResponseLocation(ret.to, true, getHttpCodeText(ret.code)); } + /// [CreatedResource]s send code 201 and will set the given urls, then present the given representation. + void presentSuccessfulReturn(T : CreatedResource!R, Meta, R)(Cgi cgi, T ret, Meta meta, string format) { + cgi.setResponseStatus(getHttpCodeText(201)); + if(ret.resourceUrl.length) + cgi.header("Location: " ~ ret.resourceUrl); + if(ret.refreshUrl.length) + cgi.header("Refresh: 0;" ~ ret.refreshUrl); + static if(!is(R == void)) + presentSuccessfulReturn(cgi, ret.resourceRepresentation, meta, format); + } + /// Multiple responses deconstruct the algebraic type and forward to the appropriate handler at runtime void presentSuccessfulReturn(T : MultipleResponses!Types, Meta, Types...)(Cgi cgi, T ret, Meta meta, string format) { bool outputted = false; @@ -8653,11 +9255,27 @@ html", true, true); useful forms or richer error messages for the user. +/ void presentExceptionAsHtml(alias func, T)(Cgi cgi, Throwable t, T dg) { - presentExceptionAsHtmlImpl(cgi, t, createAutomaticFormForFunction!(func)(dg)); + Form af; + foreach(attr; __traits(getAttributes, func)) { + static if(__traits(isSame, attr, AutomaticForm)) { + af = createAutomaticFormForFunction!(func)(dg); + } + } + presentExceptionAsHtmlImpl(cgi, t, af); } void presentExceptionAsHtmlImpl(Cgi cgi, Throwable t, Form automaticForm) { - if(auto mae = cast(MissingArgumentException) t) { + if(auto e = cast(ResourceNotFoundException) t) { + auto container = this.htmlContainer(); + + container.addChild("p", e.msg); + + if(!cgi.outputtedResponseData) + cgi.setResponseStatus("404 Not Found"); + cgi.write(container.parentDocument.toString(), true); + } else if(auto mae = cast(MissingArgumentException) t) { + if(automaticForm is null) + goto generic; auto container = this.htmlContainer(); if(cgi.requestMethod == Cgi.RequestMethod.POST) container.appendChild(Element.make("p", "Argument `" ~ mae.argumentName ~ "` of type `" ~ mae.argumentType ~ "` is missing")); @@ -8665,6 +9283,7 @@ html", true, true); cgi.write(container.parentDocument.toString(), true); } else { + generic: auto container = this.htmlContainer(); // import std.stdio; writeln(t.toString()); @@ -8726,7 +9345,7 @@ html", true, true); /++ Returns an element for a particular type +/ - Element elementFor(T)(string displayName, string name) { + Element elementFor(T)(string displayName, string name, Element function() udaSuggestion) { import std.traits; auto div = Element.make("div"); @@ -8752,7 +9371,7 @@ html", true, true); fieldset.addChild("input", name); foreach(idx, memberName; __traits(allMembers, T)) static if(__traits(compiles, __traits(getMember, T, memberName).offsetof)) { - fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName)); + fieldset.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(beautify(memberName), name ~ "." ~ memberName, null /* FIXME: pull off the UDA */)); } } else static if(isSomeString!T || isIntegral!T || isFloatingPoint!T) { Element lbl; @@ -8763,13 +9382,22 @@ html", true, true); } else { lbl = div; } - auto i = lbl.addChild("input", name); + Element i; + if(udaSuggestion) { + i = udaSuggestion(); + lbl.appendChild(i); + } else { + i = lbl.addChild("input", name); + } i.attrs.name = name; static if(isSomeString!T) i.attrs.type = "text"; else i.attrs.type = "number"; - i.attrs.value = to!string(T.init); + if(i.tagName == "textarea") + i.textContent = to!string(T.init); + else + i.attrs.value = to!string(T.init); } else static if(is(T == bool)) { Element lbl; if(displayName !is null) { @@ -8797,7 +9425,7 @@ html", true, true); i.attrs.type = "file"; } else static if(is(T == K[], K)) { auto templ = div.addChild("template"); - templ.appendChild(elementFor!(K)(null, name)); + templ.appendChild(elementFor!(K)(null, name, null /* uda??*/)); if(displayName !is null) div.addChild("span", displayName, "label-text"); auto btn = div.addChild("button"); @@ -8846,10 +9474,15 @@ html", true, true); static if(!mustNotBeSetFromWebParams!(param[0], __traits(getAttributes, param))) { string displayName = beautify(__traits(identifier, param)); - foreach(attr; __traits(getAttributes, param)) + Element function() element; + foreach(attr; __traits(getAttributes, param)) { static if(is(typeof(attr) == DisplayName)) displayName = attr.name; - auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param))); + else static if(is(typeof(attr) : typeof(element))) { + element = attr; + } + } + auto i = form.appendChild(elementFor!(param)(displayName, __traits(identifier, param), element)); if(i.querySelector("input[type=file]") !is null) form.setAttribute("enctype", "multipart/form-data"); } @@ -8877,10 +9510,13 @@ html", true, true); foreach(idx, memberName; __traits(derivedMembers, T)) {{ static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { string displayName = beautify(memberName); + Element function() element; foreach(attr; __traits(getAttributes, __traits(getMember, T, memberName))) static if(is(typeof(attr) == DisplayName)) displayName = attr.name; - form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName)); + else static if(is(typeof(attr) : typeof(element))) + element = attr; + form.appendChild(elementFor!(typeof(__traits(getMember, T, memberName)))(displayName, memberName, element)); form.setValue(memberName, to!string(__traits(getMember, obj, memberName))); }}} @@ -8988,6 +9624,11 @@ html", true, true); ol.addChild("li", formatReturnValueAsHtml(e)); return ol; } + } else static if(is(T : Object)) { + static if(is(typeof(t.toHtml()))) // FIXME: maybe i will make this an interface + return Element.make("div", t.toHtml()); + else + return Element.make("div", t.toString()); } else static assert(0, "bad return value for cgi call " ~ T.stringof); assert(0); @@ -9081,6 +9722,7 @@ struct MultipleResponses(T...) { +/ } +// FIXME: implement this somewhere maybe struct RawResponse { int code; string[] headers; @@ -9096,7 +9738,7 @@ struct RawResponse { +/ struct Redirection { string to; /// The URL to redirect to. - int code = 303; /// The HTTP code to retrn. + int code = 303; /// The HTTP code to return. } /++ @@ -9580,6 +10222,8 @@ template urlNamesForMethod(alias method, string default_) { /++ The base of all REST objects, to be used with [serveRestObject] and [serveRestCollectionOf]. + + WARNING: this is not stable. +/ class RestObject(CRTP) : WebObject { @@ -9594,21 +10238,24 @@ class RestObject(CRTP) : WebObject { show(); } - ValidationResult delegate(typeof(this)) validateFromReflection; - Element delegate(typeof(this)) toHtmlFromReflection; - var delegate(typeof(this)) toJsonFromReflection; - /// Override this to provide access control to this object. AccessCheck accessCheck(string urlId, Operation operation) { return AccessCheck.allowed; } ValidationResult validate() { - if(validateFromReflection !is null) - return validateFromReflection(this); + // FIXME return ValidationResult.valid; } + string getUrlSlug() { + import std.conv; + static if(is(typeof(CRTP.id))) + return to!string((cast(CRTP) this).id); + else + return null; + } + // The functions with more arguments are the low-level ones, // they forward to the ones with fewer arguments by default. @@ -9618,7 +10265,9 @@ class RestObject(CRTP) : WebObject { of the new object. +/ string create(scope void delegate() applyChanges) { - return null; + applyChanges(); + save(); + return getUrlSlug(); } void replace() { @@ -9649,18 +10298,31 @@ class RestObject(CRTP) : WebObject { abstract void load(string urlId); abstract void save(); - Element toHtml() { - if(toHtmlFromReflection) - return toHtmlFromReflection(this); - else - assert(0); + Element toHtml(Presenter)(Presenter presenter) { + import arsd.dom; + import std.conv; + auto obj = cast(CRTP) this; + auto div = Element.make("div"); + div.addClass("Dclass_" ~ CRTP.stringof); + div.dataset.url = getUrlSlug(); + bool first = true; + foreach(idx, memberName; __traits(derivedMembers, CRTP)) + static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { + if(!first) div.addChild("br"); else first = false; + div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName))); + } + return div; } var toJson() { - if(toJsonFromReflection) - return toJsonFromReflection(this); - else - assert(0); + import arsd.jsvar; + var v = var.emptyObject(); + auto obj = cast(CRTP) this; + foreach(idx, memberName; __traits(derivedMembers, CRTP)) + static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { + v[memberName] = __traits(getMember, obj, memberName); + } + return v; } /+ @@ -9889,32 +10551,6 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u // FIXME: support precondition failed, if-modified-since, expectation failed, etc. auto obj = new T(); - obj.toHtmlFromReflection = delegate(t) { - import arsd.dom; - auto div = Element.make("div"); - div.addClass("Dclass_" ~ T.stringof); - div.dataset.url = urlId; - bool first = true; - foreach(idx, memberName; __traits(derivedMembers, T)) - static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { - if(!first) div.addChild("br"); else first = false; - div.appendChild(presenter.formatReturnValueAsHtml(__traits(getMember, obj, memberName))); - } - return div; - }; - obj.toJsonFromReflection = delegate(t) { - import arsd.jsvar; - var v = var.emptyObject(); - foreach(idx, memberName; __traits(derivedMembers, T)) - static if(__traits(compiles, __traits(getMember, obj, memberName).offsetof)) { - v[memberName] = __traits(getMember, obj, memberName); - } - return v; - }; - obj.validateFromReflection = delegate(t) { - // FIXME - return ValidationResult.valid; - }; obj.initialize(cgi); // FIXME: populate reflection info delegates @@ -9965,13 +10601,14 @@ bool restObjectServeHandler(T, Presenter)(Cgi cgi, Presenter presenter, string u `); else container.appendHtml(` + <a href="..">Back</a> <form> <button type="submit" name="_method" value="PATCH">Edit</button> <button type="submit" name="_method" value="DELETE">Delete</button> </form> `); } - container.appendChild(obj.toHtml()); + container.appendChild(obj.toHtml(presenter)); cgi.write(container.parentDocument.toString, true); } } @@ -10162,6 +10799,32 @@ auto serveStaticFile(string urlPrefix, string filename = null, string contentTyp return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(filename, contentType)); } +/++ + Serves static data. To be used with [dispatcher]. + + History: + Added October 31, 2021 ++/ +auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentType = null) { + assert(urlPrefix[0] == '/'); + if(contentType is null) { + contentType = contentTypeFromFileExtension(urlPrefix); + } + + static struct DispatcherDetails { + immutable(void)[] data; + string contentType; + } + + static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) { + cgi.setCache(true); + cgi.setResponseContentType(details.contentType); + cgi.write(details.data, true); + return true; + } + return DispatcherDefinition!(internalHandler, DispatcherDetails)(urlPrefix, true, DispatcherDetails(data, contentType)); +} + string contentTypeFromFileExtension(string filename) { if(filename.endsWith(".png")) return "image/png"; @@ -10504,6 +11167,8 @@ bool apiDispatcher()(Cgi cgi) { import arsd.dom; } +/ +version(linux) +private extern(C) int eventfd (uint initval, int flags) nothrow @trusted @nogc; /* Copyright: Adam D. Ruppe, 2008 - 2021 License: [http://www.boost.org/LICENSE_1_0.txt|Boost License 1.0]. |