summaryrefslogtreecommitdiffhomepage
path: root/src/ext_depends
diff options
context:
space:
mode:
authorRalph Amissah <ralph.amissah@gmail.com>2025-02-16 17:27:25 -0500
committerRalph Amissah <ralph.amissah@gmail.com>2025-02-16 17:27:34 -0500
commita7244f480a2a4750252bf6a1bcc0b213f7a4c76d (patch)
tree310dfd22d6a26ad5331306a7a04675ed5937b93b /src/ext_depends
parentorg, reorganize version info (diff)
ext_depends/arsd/{core,cgi}.d downloaded/updatedHEADmain
Diffstat (limited to 'src/ext_depends')
-rw-r--r--src/ext_depends/arsd/cgi.d634
-rw-r--r--src/ext_depends/arsd/core.d3429
2 files changed, 3793 insertions, 270 deletions
diff --git a/src/ext_depends/arsd/cgi.d b/src/ext_depends/arsd/cgi.d
index 0497eb2..0af9f25 100644
--- a/src/ext_depends/arsd/cgi.d
+++ b/src/ext_depends/arsd/cgi.d
@@ -134,7 +134,7 @@ void main() {
* `embedded_httpd` for the embedded httpd version (built-in web server). This is the default for dub builds. You can run the program then connect directly to it from your browser. Note: prior to version 11, this would be embedded_httpd_processes on Linux and embedded_httpd_threads everywhere else. It now means embedded_httpd_hybrid everywhere supported and embedded_httpd_threads everywhere else.
* `cgi` for traditional cgi binaries. These are run by an outside web server as-needed to handle requests.
* `fastcgi` for FastCGI builds. FastCGI is managed from an outside helper, there's one built into Microsoft IIS, Apache httpd, and Lighttpd, and a generic program you can use with nginx called `spawn-fcgi`. If you don't already know how to use it, I suggest you use one of the other modes.
- * `scgi` for SCGI builds. SCGI is a simplified form of FastCGI, where you run the server as an application service which is proxied by your outside webserver.
+ * `scgi` for SCGI builds. SCGI is a simplified form of FastCGI, where you run the server as an application service which is proxied by your outside webserver. Please note: on nginx make sure you add `scgi_param PATH_INFO $document_uri;` to the config!
* `stdio_http` for speaking raw http over stdin and stdout. This is made for systemd services. See [RequestServer.serveSingleHttpConnectionOnStdio] for more information.
)
@@ -188,6 +188,12 @@ void main() {
For an embedded HTTP server, run `dmd yourfile.d cgi.d -version=embedded_httpd` and run the generated program. It listens on port 8085 by default. You can change this on the command line with the --port option when running your program.
+ Command_line_interface:
+
+ If using [GenericMain] or [DispatcherMain], an application using arsd.cgi will offer a command line interface out of the box.
+
+ See [RequestServer.listenSpec] for more information.
+
Simulating_requests:
If you are using one of the [GenericMain] or [DispatcherMain] mixins, or main with your own call to [RequestServer.trySimulatedRequest], you can simulate requests from your command-ine shell. Call the program like this:
@@ -486,10 +492,6 @@ void main() {
+/
module arsd.cgi;
-static import arsd.core;
-version(Posix)
-import arsd.core : makeNonBlocking;
-
// FIXME: Nullable!T can be a checkbox that enables/disables the T on the automatic form
// and a SumType!(T, R) can be a radio box to pick between T and R to disclose the extra boxes on the automatic form
@@ -572,8 +574,44 @@ unittest {
}
}
+/++
+ The session system works via a built-in spawnable server.
+
+ Bugs:
+ Requires addon servers, which are not implemented yet on Windows.
++/
+version(Posix)
+version(Demo)
+unittest {
+ import arsd.cgi;
+
+ struct SessionData {
+ string userId;
+ }
+
+ void handler(Cgi cgi) {
+ auto session = cgi.getSessionObject!SessionData;
+
+ if(cgi.pathInfo == "/login") {
+ session.userId = cgi.queryString;
+ cgi.setResponseLocation("view");
+ } else {
+ cgi.write(session.userId);
+ }
+ }
+
+ mixin GenericMain!handler;
+}
+
static import std.file;
+static import arsd.core;
+version(Posix)
+import arsd.core : makeNonBlocking;
+
+import arsd.core : encodeUriComponent, decodeUriComponent;
+
+
// for a single thread, linear request thing, use:
// -version=embedded_httpd_threads -version=cgi_no_threads
@@ -584,6 +622,8 @@ version(Posix) {
} else {
version(FreeBSD) {
+ // not implemented on bsds
+ } else version(OpenBSD) {
// I never implemented the fancy stuff there either
} else {
version=with_breaking_cgi_features;
@@ -622,7 +662,10 @@ version(with_addon_servers)
version=with_addon_servers_connections;
version(embedded_httpd) {
- version=embedded_httpd_hybrid;
+ version(OSX)
+ version = embedded_httpd_threads;
+ else
+ version=embedded_httpd_hybrid;
/*
version(with_openssl) {
pragma(lib, "crypto");
@@ -689,7 +732,6 @@ enum long defaultMaxContentLength = 5_000_000;
public import std.string;
public import std.stdio;
public import std.conv;
-import std.uri;
import std.uni;
import std.algorithm.comparison;
import std.algorithm.searching;
@@ -841,6 +883,35 @@ class Cgi {
/+
+
+ ubyte[] perRequestMemoryPool;
+ void[] perRequestMemoryPoolWithPointers;
+ // might want to just slice the buffer itself too when we happened to have gotten a full request inside it and don't need to decode
+ // then the buffer also can be recycled if it is set.
+
+ // we might also be able to set memory recyclable true by default, but then the property getters set it to false. but not all the things are property getters. but realistically anything except benchmarks are gonna get something lol so meh.
+
+ /+
+ struct VariableCollection {
+ string[] opIndex(string name) {
+
+ }
+ }
+
+ /++
+ Call this to indicate that you've not retained any reference to the request-local memory (including all strings returned from the Cgi object) outside the request (you can .idup anything you need to store) and it is thus free to be freed or reused by another request.
+
+ Most handlers should be able to call this; retaining memory is the exception in any cgi program, but since I can't prove it from inside the library, it plays it safe and lets the GC manage it unless you opt into this behavior. All cgi.d functions will duplicate strings if needed (e.g. session ids from cookies) so unless you're doing something yourself, this should be ok.
+
+ History:
+ Added
+ +/
+ public void recycleMemory() {
+
+ }
+ +/
+
+
/++
Cgi provides a per-request memory pool
@@ -897,6 +968,7 @@ class Cgi {
--accept 'content' // FIXME: better example
--last-event-id 'something'
--host 'something.com'
+ --session name=value (these are added to a mock session, changes to the session are printed out as dummy response headers)
Non-simulation arguments:
--port xxx listening port for non-cgi things (valid for the cgi interfaces)
@@ -962,8 +1034,13 @@ class Cgi {
auto info = breakUp(arg);
if(_cookie.length)
_cookie ~= "; ";
- _cookie ~= std.uri.encodeComponent(info[0]) ~ "=" ~ std.uri.encodeComponent(info[1]);
+ _cookie ~= encodeUriComponent(info[0]) ~ "=" ~ encodeUriComponent(info[1]);
}
+ if (nextArgIs == "session") {
+ auto info = breakUp(arg);
+ _commandLineSession[info[0]] = info[1];
+ }
+
else if (nextArgIs == "port") {
port = to!int(arg);
}
@@ -1042,7 +1119,7 @@ class Cgi {
if(_queryString.length)
_queryString ~= "&";
auto parts = breakUp(arg);
- _queryString ~= std.uri.encodeComponent(parts[0]) ~ "=" ~ std.uri.encodeComponent(parts[1]);
+ _queryString ~= encodeUriComponent(parts[0]) ~ "=" ~ encodeUriComponent(parts[1]);
}
}
}
@@ -1083,6 +1160,7 @@ class Cgi {
this.pathInfo = pathInfo;
this.queryString = queryString;
this.postBody = null;
+ this.requestContentType = null;
}
private {
@@ -1306,6 +1384,7 @@ class Cgi {
files = keepLastOf(filesArray);
post = keepLastOf(postArray);
this.postBody = pps.postBody;
+ this.requestContentType = contentType;
cleanUpPostDataState();
}
@@ -2133,6 +2212,8 @@ class Cgi {
files = keepLastOf(filesArray);
post = keepLastOf(postArray);
postBody = pps.postBody;
+ this.requestContentType = contentType;
+
cleanUpPostDataState();
}
@@ -2187,28 +2268,31 @@ class Cgi {
return assumeUnique(forTheLoveOfGod);
}
- /// Very simple method to require a basic auth username and password.
- /// If the http request doesn't include the required credentials, it throws a
- /// HTTP 401 error, and an exception.
- ///
- /// Note: basic auth does not provide great security, especially over unencrypted HTTP;
- /// the user's credentials are sent in plain text on every request.
- ///
- /// If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the
- /// application. Either use Apache's built in methods for basic authentication, or add
- /// something along these lines to your server configuration:
- ///
- /// RewriteEngine On
- /// RewriteCond %{HTTP:Authorization} ^(.*)
- /// RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
- ///
- /// To ensure the necessary data is available to cgi.d.
- void requireBasicAuth(string user, string pass, string message = null) {
+ /++
+ Very simple method to require a basic auth username and password.
+ If the http request doesn't include the required credentials, it throws a
+ HTTP 401 error, and an exception to cancel your handler. Do NOT catch the
+ `AuthorizationRequiredException` exception thrown by this if you want the
+ http basic auth prompt to work for the user!
+
+ Note: basic auth does not provide great security, especially over unencrypted HTTP;
+ the user's credentials are sent in plain text on every request.
+
+ If you are using Apache, the HTTP_AUTHORIZATION variable may not be sent to the
+ application. Either use Apache's built in methods for basic authentication, or add
+ something along these lines to your server configuration:
+
+ ```
+ RewriteEngine On
+ RewriteCond %{HTTP:Authorization} ^(.*)
+ RewriteRule ^(.*) - [E=HTTP_AUTHORIZATION:%1]
+ ```
+
+ To ensure the necessary data is available to cgi.d.
+ +/
+ void requireBasicAuth(string user, string pass, string message = null, string file = __FILE__, size_t line = __LINE__) {
if(authorization != "Basic " ~ Base64.encode(cast(immutable(ubyte)[]) (user ~ ":" ~ pass))) {
- setResponseStatus("401 Authorization Required");
- header ("WWW-Authenticate: Basic realm=\""~message~"\"");
- close();
- throw new Exception("Not authorized; got " ~ authorization);
+ throw new AuthorizationRequiredException("Basic", message, file, line);
}
}
@@ -2370,8 +2454,8 @@ class Cgi {
+/
void setCookie(string name, string data, long expiresIn = 0, string path = null, string domain = null, bool httpOnly = false, bool secure = false, SameSitePolicy sameSitePolicy = SameSitePolicy.Lax) {
assert(!outputtedResponseData);
- string cookie = std.uri.encodeComponent(name) ~ "=";
- cookie ~= std.uri.encodeComponent(data);
+ string cookie = encodeUriComponent(name) ~ "=";
+ cookie ~= encodeUriComponent(data);
if(path !is null)
cookie ~= "; path=" ~ path;
// FIXME: should I just be using max-age here? (also in cache below)
@@ -2728,6 +2812,8 @@ class Cgi {
return closed;
}
+ private SessionObject commandLineSessionObject;
+
/++
Gets a session object associated with the `cgi` request. You can use different type throughout your application.
+/
@@ -2743,6 +2829,22 @@ class Cgi {
return o;
}
} else {
+ // FIXME: the changes are not printed out at the end!
+ if(_commandLineSession !is null) {
+ if(commandLineSessionObject is null) {
+ auto clso = new MockSession!Data();
+ commandLineSessionObject = clso;
+
+
+ foreach(memberName; __traits(allMembers, Data)) {
+ if(auto str = memberName in _commandLineSession)
+ __traits(getMember, clso.store_, memberName) = to!(typeof(__traits(getMember, Data, memberName)))(*str);
+ }
+ }
+
+ return cast(typeof(return)) commandLineSessionObject;
+ }
+
// normal operation
return new BasicDataServerSession!Data(this);
}
@@ -2785,6 +2887,14 @@ class Cgi {
public immutable string postBody;
alias postJson = postBody; // old name
+ /++
+ The content type header of the request. The [postBody] member may hold the actual data (see [postBody] for details).
+
+ History:
+ Added January 26, 2024 (dub v11.4)
+ +/
+ public immutable string requestContentType;
+
/* Internal state flags */
private bool outputtedResponseData;
private bool noCache = true;
@@ -2831,6 +2941,9 @@ class Cgi {
immutable(string[string]) post; /// The data from the request's body, on POST requests. It parses application/x-www-form-urlencoded data (used by most web requests, including typical forms), and multipart/form-data requests (used by file uploads on web forms) into the same container, so you can always access them the same way. It makes no attempt to parse other content types. If you want to accept an XML Post body (for a web api perhaps), you'll need to handle the raw data yourself.
immutable(string[string]) cookies; /// Separates out the cookie header into individual name/value pairs (which is how you set them!)
+ /// added later
+ alias query = get;
+
/**
Represents user uploaded files.
@@ -2845,6 +2958,8 @@ class Cgi {
immutable(string[][string]) postArray; /// ditto for post
immutable(string[][string]) cookiesArray; /// ditto for cookies
+ private string[string] _commandLineSession;
+
// convenience function for appending to a uri without extra ?
// matches the name and effect of javascript's location.search property
string search() const {
@@ -3025,7 +3140,7 @@ struct Uri {
// idk if i want to keep these, since the functions they wrap are used many, many, many times in existing code, so this is either an unnecessary alias or a gratuitous break of compatibility
// the decode ones need to keep different names anyway because we can't overload on return values...
- static string encode(string s) { return std.uri.encodeComponent(s); }
+ static string encode(string s) { return encodeUriComponent(s); }
static string encode(string[string] s) { return encodeVariables(s); }
static string encode(string[][string] s) { return encodeVariables(s); }
@@ -3409,13 +3524,13 @@ string[][string] decodeVariables(string data, string separator = "&", string[]*
string name;
string value;
if(equal == -1) {
- name = decodeComponent(var);
+ name = decodeUriComponent(var);
value = "";
} else {
- //_get[decodeComponent(var[0..equal])] ~= decodeComponent(var[equal + 1 .. $].replace("+", " "));
+ //_get[decodeUriComponent(var[0..equal])] ~= decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
// stupid + -> space conversion.
- name = decodeComponent(var[0..equal].replace("+", " "));
- value = decodeComponent(var[equal + 1 .. $].replace("+", " "));
+ name = decodeUriComponent(var[0..equal].replace("+", " "));
+ value = decodeUriComponent(var[equal + 1 .. $].replace("+", " "));
}
_get[name] ~= value;
@@ -3448,7 +3563,7 @@ string encodeVariables(in string[string] data) {
else
outputted = true;
- ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
+ ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
}
return ret;
@@ -3465,7 +3580,7 @@ string encodeVariables(in string[][string] data) {
ret ~= "&";
else
outputted = true;
- ret ~= std.uri.encodeComponent(k) ~ "=" ~ std.uri.encodeComponent(v);
+ ret ~= encodeUriComponent(k) ~ "=" ~ encodeUriComponent(v);
}
}
@@ -3593,6 +3708,11 @@ mixin template CustomCgiDispatcherMain(CustomCgi, size_t maxContentLength, Prese
activePresenter = presenter;
scope(exit) activePresenter = null;
+ if(cgi.pathInfo.length == 0) {
+ cgi.setResponseLocation(cgi.scriptName ~ "/");
+ return;
+ }
+
if(cgi.dispatcher!DispatcherArgs(presenter))
return;
@@ -3746,8 +3866,17 @@ bool trySimulatedRequest(alias fun, CustomCgi = Cgi)(string[] args) if(is(Custom
if(args.length >= 3 && isCgiRequestMethod(args[1])) {
Cgi cgi = new CustomCgi(args);
scope(exit) cgi.dispose();
- fun(cgi);
- cgi.close();
+ try {
+ fun(cgi);
+ cgi.close();
+ } catch(AuthorizationRequiredException are) {
+ cgi.setResponseStatus("401 Authorization Required");
+ cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\"");
+ cgi.close();
+ }
+ writeln(); // just to put a blank line before the prompt cuz it annoys me
+ // FIXME: put in some footers to show what changes happened in the session
+ // could make the MockSession be some kind of ReflectableSessionObject or something
return true;
}
return false;
@@ -4046,9 +4175,34 @@ struct RequestServer {
} else
version(stdio_http) {
serveSingleHttpConnectionOnStdio!(fun, CustomCgi, maxContentLength)();
- } else {
- //version=plain_cgi;
+ } else
+ version(plain_cgi) {
handleCgiRequest!(fun, CustomCgi, maxContentLength)();
+ } else {
+ if(this.listenSpec.length) {
+ // FIXME: what about heterogeneous listen specs?
+ if(this.listenSpec[0].startsWith("scgi:"))
+ serveScgi!(fun, CustomCgi, maxContentLength)();
+ else
+ serveEmbeddedHttp!(fun, CustomCgi, maxContentLength)();
+ } else {
+ import std.process;
+ if("REQUEST_METHOD" in environment) {
+ // GATEWAY_INTERFACE must be set according to the spec for it to be a cgi request
+ // REQUEST_METHOD must also be set
+ handleCgiRequest!(fun, CustomCgi, maxContentLength)();
+ } else {
+ import std.stdio;
+ writeln("To start a local-only http server, use `thisprogram --listen http://localhost:PORT_NUMBER`");
+ writeln("To start a externally-accessible http server, use `thisprogram --listen http://:PORT_NUMBER`");
+ writeln("To start a scgi server, use `thisprogram --listen scgi://localhost:PORT_NUMBER`");
+ writeln("To test a request on the command line, use `thisprogram REQUEST /path arg=value`");
+ writeln("Or copy this program to your web server's cgi-bin folder to run it that way.");
+ writeln("If you need FastCGI, recompile this program with -version=fastcgi");
+ writeln();
+ writeln("Learn more at https://opendlang.org/library/arsd.cgi.html#Command-line-interface");
+ }
+ }
}
}
@@ -4155,6 +4309,17 @@ struct RequestServer {
}
}
+class AuthorizationRequiredException : Exception {
+ string type;
+ string realm;
+ this(string type, string realm, string file, size_t line) {
+ this.type = type;
+ this.realm = realm;
+
+ super("Authorization Required", file, line);
+ }
+}
+
private alias AliasSeq(T...) = T;
version(with_breaking_cgi_features)
@@ -4316,6 +4481,11 @@ void serveEmbeddedHttpdProcesses(alias fun, CustomCgi = Cgi)(RequestServer param
cgi.close();
if(cgi.websocketMode)
closeConnection = true;
+
+ } catch(AuthorizationRequiredException are) {
+ cgi.setResponseStatus("401 Authorization Required");
+ cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\"");
+ cgi.close();
} catch(ConnectionException ce) {
closeConnection = true;
} catch(Throwable t) {
@@ -4478,6 +4648,10 @@ void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMax
try {
fun(cgi);
cgi.close();
+ } catch(AuthorizationRequiredException are) {
+ cgi.setResponseStatus("401 Authorization Required");
+ cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\"");
+ cgi.close();
} catch(Throwable t) {
// log it to the error stream
FCGX_PutStr(cast(ubyte*) t.msg.ptr, t.msg.length, error);
@@ -4526,7 +4700,7 @@ void serveFastCgi(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMax
}
/// Returns the default listening port for the current cgi configuration. 8085 for embedded httpd, 4000 for scgi, irrelevant for others.
-ushort defaultListeningPort() {
+ushort defaultListeningPort() @safe {
version(netman_httpd)
return 8080;
else version(embedded_httpd_processes)
@@ -4540,7 +4714,7 @@ ushort defaultListeningPort() {
}
/// Default host for listening. 127.0.0.1 for scgi, null (aka all interfaces) for all others. If you want the server directly accessible from other computers on the network, normally use null. If not, 127.0.0.1 is a bit better. Settable with default handlers with --listening-host command line argument.
-string defaultListeningHost() {
+string defaultListeningHost() @safe {
version(netman_httpd)
return null;
else version(embedded_httpd_processes)
@@ -4630,6 +4804,10 @@ void handleCgiRequest(alias fun, CustomCgi = Cgi, long maxContentLength = defaul
try {
fun(cgi);
cgi.close();
+ } catch(AuthorizationRequiredException are) {
+ cgi.setResponseStatus("401 Authorization Required");
+ cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\"");
+ cgi.close();
} catch (Throwable t) {
version(CRuntime_Musl) {
// LockingTextWriter fails here
@@ -4758,6 +4936,11 @@ extern(Windows) private {
alias GROUP=uint;
alias LPWSAPROTOCOL_INFOW = void*;
SOCKET WSASocketW(int af, int type, int protocol, LPWSAPROTOCOL_INFOW lpProtocolInfo, GROUP g, DWORD dwFlags);
+ alias WSASend = arsd.core.WSASend;
+ alias WSARecv = arsd.core.WSARecv;
+ alias WSABUF = arsd.core.WSABUF;
+
+ /+
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);
@@ -4765,6 +4948,7 @@ extern(Windows) private {
ULONG len;
CHAR *buf;
}
+ +/
alias LPWSABUF = WSABUF*;
alias WSAOVERLAPPED = OVERLAPPED;
@@ -4917,7 +5101,7 @@ private class PseudoblockingOverlappedSocket : Socket {
override ptrdiff_t send(scope const(void)[] buf, SocketFlags flags) @trusted {
overlapped = overlapped.init;
buffer[0].len = cast(DWORD) buf.length;
- buffer[0].buf = cast(CHAR*) buf.ptr;
+ buffer[0].buf = cast(ubyte*) buf.ptr;
fiber.setPostYield( () {
if(!WSASend(handle, buffer.ptr, cast(DWORD) buffer.length, null, 0, &overlapped, null)) {
if(GetLastError() != 997) {
@@ -4932,7 +5116,7 @@ private class PseudoblockingOverlappedSocket : Socket {
override ptrdiff_t receive(scope void[] buf, SocketFlags flags) @trusted {
overlapped = overlapped.init;
buffer[0].len = cast(DWORD) buf.length;
- buffer[0].buf = cast(CHAR*) buf.ptr;
+ buffer[0].buf = cast(ubyte*) buf.ptr;
DWORD flags2 = 0;
@@ -4990,6 +5174,124 @@ void doThreadHttpConnection(CustomCgi, alias fun)(Socket connection) {
}
}
+/+
+
+/+
+ The represents a recyclable per-task arena allocator. The default is to let the GC manage the whole block as a large array, meaning if a reference into it is escaped, it waste memory but is not dangerous. If you don't escape any references to it and don't do anything special, the GC collects it.
+
+ But, if you call `cgi.recyclable = true`, the memory is retained for the next request on the thread. If a reference is escaped, it is the user's problem; it can be modified (and break the `immutable` guarantees!) and thus be memory unsafe. They're taking responsibility for doing it right when they call `escape`. But if they do it right and opt into recycling, the memory is all reused to give a potential boost without requiring the GC's involvement.
+
+ What if one request used an abnormally large amount of memory though? Will recycling it keep that pinned forever? No, that's why it keeps track of some stats. If a working set was significantly above average and not fully utilized for a while, it will just let the GC have it again despite your suggestion to recycle it.
+
+ Be warned that growing the memory block may release the old, smaller block for garbage collection. If you retained references to it, it may not be collectable and lead to some unnecessary memory growth. It is probably best to try to keep the things sized in a continuous block that doesn't have to grow often.
+
+ Internally, it is broken up into a few blocks:
+ * the request block. This holds the incoming request and associated data (parsed headers, variables, etc).
+ * the scannable block. this holds pointers arrays, classes, etc. associated with this request, so named because the GC scans it.
+ * the response block. This holds the output buffer.
+
+ And I may add more later if I decide to open this up to outside user code.
+
+ The scannable block is separate to limit the amount of work the GC has to do; no point asking it to scan that which need not be scanned.
+
+ The request and response blocks are separated because they will have different typical sizes, with the request likely being less predictable. Being able to release one to the GC while recycling the other might help, and having them grow independently (if needed) may also prevent some pain.
+
+ All of this are internal implementation details subject to change at any time without notice. It is valid for my recycle method to do absolutely nothing; the GC also eventually recycles memory!
+
+ Each active task can have its own recyclable memory object. When you recycle it, it is added to a thread-local freelist. If the list is excessively large, entries maybe discarded at random and left for the GC to prevent a temporary burst of activity from leading to a permanent waste of memory.
++/
+struct RecyclableMemory {
+ private ubyte[] inputBuffer;
+ private ubyte[] processedRequestBlock;
+ private void[] scannableBlock;
+ private ubyte[] outputBuffer;
+
+ RecyclableMemory* next;
+}
+
+/++
+ This emulates the D associative array interface with a different internal implementation.
+
+ string s = cgi.get["foo"]; // just does cgi.getArray[x][$-1];
+ string[] arr = cgi.getArray["foo"];
+
+ "foo" in cgi.get
+
+ foreach(k, v; cgi.get)
+
+ cgi.get.toAA // for compatibility
+
+ // and this can urldecode lazily tbh... in-place even, since %xx is always longer than a single char thing it turns into...
+ ... but how does it mark that it has already been processed in-place? it'd have to just add it to the index then.
+
+ deprecated alias toAA this;
++/
+struct VariableCollection {
+ private VariableArrayCollection* vac;
+
+ const(char[]) opIndex(scope const char[] key) {
+ return (*vac)[key][$-1];
+ }
+
+ const(char[]*) opBinaryRight(string op : "in")(scope const char[] key) {
+ return key in (*vac);
+ }
+
+ int opApply(int delegate(scope const(char)[] key, scope const(char)[] value) dg) {
+ foreach(k, v; *vac) {
+ if(auto res = dg(k, v[$-1]))
+ return res;
+ }
+ return 0;
+ }
+
+ immutable(string[string]) toAA() {
+ string[string] aa;
+ foreach(k, v; *vac)
+ aa[k.idup] = v[$-1].idup;
+ return aa;
+ }
+
+ deprecated alias toAA this;
+}
+
+struct VariableArrayCollection {
+ /+
+ This needs the actual implementation of looking it up. As it pulls data, it should
+ decode and index for later.
+
+ The index will go into a block attached to the cgi object and it should prolly be sorted
+ something like
+
+ [count of names]
+ [slice to name][count of values][slice to value, decoded in-place, ...]
+ ...
+ +/
+ private Cgi cgi;
+
+ const(char[][]) opIndex(scope const char[] key) {
+ return null;
+ }
+
+ const(char[][]*) opBinaryRight(string op : "in")(scope const char[] key) {
+ return null;
+ }
+
+ // int opApply(int delegate(scope const(char)[] key, scope const(char)[][] value) dg)
+
+ immutable(string[string]) toAA() {
+ return null;
+ }
+
+ deprecated alias toAA this;
+
+}
+
+struct HeaderCollection {
+
+}
++/
+
void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection = false)(Socket connection) {
scope(failure) {
// catch all for other errors
@@ -5065,6 +5367,10 @@ void doThreadHttpConnectionGuts(CustomCgi, alias fun, bool alwaysCloseConnection
cgi.close();
if(cgi.websocketMode)
closeConnection = true;
+ } catch(AuthorizationRequiredException are) {
+ cgi.setResponseStatus("401 Authorization Required");
+ cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\"");
+ cgi.close();
} catch(ConnectionException ce) {
// broken pipe or something, just abort the connection
closeConnection = true;
@@ -5201,6 +5507,11 @@ void doThreadScgiConnection(CustomCgi, alias fun, long maxContentLength)(Socket
fun(cgi);
cgi.close();
connection.close();
+
+ } catch(AuthorizationRequiredException are) {
+ cgi.setResponseStatus("401 Authorization Required");
+ cgi.header ("WWW-Authenticate: "~are.type~" realm=\""~are.realm~"\"");
+ cgi.close();
} catch(Throwable t) {
// no std err
if(!handleException(cgi, t)) {
@@ -5336,7 +5647,7 @@ version(fastcgi) {
int FCGX_HasSeenEOF(FCGX_Stream* stream);
c_int FCGX_FFlush(FCGX_Stream *stream);
- int FCGX_OpenSocket(in char*, int);
+ int FCGX_OpenSocket(const char*, int);
}
}
@@ -5567,7 +5878,7 @@ class BufferedInputRange {
return view;
}
- invariant() {
+ @system invariant() {
assert(view.ptr >= underlyingBuffer.ptr);
// it should never be equal, since if that happens view ought to be empty, and thus reusing the buffer
assert(view.ptr < underlyingBuffer.ptr + underlyingBuffer.length);
@@ -6584,6 +6895,8 @@ version(cgi_with_websocket) {
class WebSocket {
Cgi cgi;
+ private bool isClient = false;
+
private this(Cgi cgi) {
this.cgi = cgi;
@@ -6616,12 +6929,14 @@ version(cgi_with_websocket) {
return true;
}
- if(bfr.sourceClosed)
+ if(bfr.sourceClosed) {
return false;
+ }
bfr.popFront(0);
- if(bfr.sourceClosed)
+ if(bfr.sourceClosed) {
return false;
+ }
goto top;
}
@@ -6701,7 +7016,48 @@ version(cgi_with_websocket) {
string origin; /// Origin URL to send with the handshake, if desired.
string protocol; /// the protocol header, if desired.
- int pingFrequency = 5000; /// Amount of time (in msecs) of idleness after which to send an automatic ping
+ /++
+ Additional headers to put in the HTTP request. These should be formatted `Name: value`, like for example:
+
+ ---
+ Config config;
+ config.additionalHeaders ~= "Authorization: Bearer your_auth_token_here";
+ ---
+
+ History:
+ Added February 19, 2021 (included in dub version 9.2)
+ +/
+ string[] additionalHeaders;
+
+ /++
+ Amount of time (in msecs) of idleness after which to send an automatic ping
+
+ Please note how this interacts with [timeoutFromInactivity] - a ping counts as activity that
+ keeps the socket alive.
+ +/
+ int pingFrequency = 5000;
+
+ /++
+ Amount of time to disconnect when there's no activity. Note that automatic pings will keep the connection alive; this timeout only occurs if there's absolutely nothing, including no responses to websocket ping frames. Since the default [pingFrequency] is only seconds, this one minute should never elapse unless the connection is actually dead.
+
+ The one thing to keep in mind is if your program is busy and doesn't check input, it might consider this a time out since there's no activity. The reason is that your program was busy rather than a connection failure, but it doesn't care. You should avoid long processing periods anyway though!
+
+ History:
+ Added March 31, 2021 (included in dub version 9.4)
+ +/
+ Duration timeoutFromInactivity = 1.minutes;
+
+ /++
+ For https connections, if this is `true`, it will fail to connect if the TLS certificate can not be
+ verified. Setting this to `false` will skip this check and allow the connection to continue anyway.
+
+ History:
+ Added April 5, 2022 (dub v10.8)
+
+ Prior to this, it always used the global (but undocumented) `defaultVerifyPeer` setting, and sometimes
+ even if it was true, it would skip the verification. Now, it always respects this local setting.
+ +/
+ bool verifyPeer = true;
}
/++
@@ -6713,9 +7069,15 @@ version(cgi_with_websocket) {
/++
Closes the connection, sending a graceful teardown message to the other side.
+
+ Code 1000 is the normal closure code.
+
+ History:
+ The default `code` was changed to 1000 on January 9, 2023. Previously it was 0,
+ but also ignored anyway.
+/
/// Group: foundational
- void close(int code = 0, string reason = null)
+ void close(int code = 1000, string reason = null)
//in (reason.length < 123)
in { assert(reason.length < 123); } do
{
@@ -6723,31 +7085,43 @@ version(cgi_with_websocket) {
return; // it cool, we done
WebSocketFrame wss;
wss.fin = true;
+ wss.masked = this.isClient;
wss.opcode = WebSocketOpcode.close;
- wss.data = cast(ubyte[]) reason.dup;
+ wss.data = [ubyte((code >> 8) & 0xff), ubyte(code & 0xff)] ~ cast(ubyte[]) reason.dup;
wss.send(&llsend);
readyState_ = CLOSING;
+ closeCalled = true;
+
llclose();
}
+ private bool closeCalled;
+
/++
Sends a ping message to the server. This is done automatically by the library if you set a non-zero [Config.pingFrequency], but you can also send extra pings explicitly as well with this function.
+/
/// Group: foundational
- void ping() {
+ void ping(in ubyte[] data = null) {
WebSocketFrame wss;
wss.fin = true;
+ wss.masked = this.isClient;
wss.opcode = WebSocketOpcode.ping;
+ if(data !is null) wss.data = data.dup;
wss.send(&llsend);
}
- // automatically handled....
- void pong() {
+ /++
+ Sends a pong message to the server. This is normally done automatically in response to pings.
+ +/
+ /// Group: foundational
+ void pong(in ubyte[] data = null) {
WebSocketFrame wss;
wss.fin = true;
+ wss.masked = this.isClient;
wss.opcode = WebSocketOpcode.pong;
+ if(data !is null) wss.data = data.dup;
wss.send(&llsend);
}
@@ -6758,6 +7132,7 @@ version(cgi_with_websocket) {
void send(in char[] textData) {
WebSocketFrame wss;
wss.fin = true;
+ wss.masked = this.isClient;
wss.opcode = WebSocketOpcode.text;
wss.data = cast(ubyte[]) textData.dup;
wss.send(&llsend);
@@ -6769,6 +7144,7 @@ version(cgi_with_websocket) {
/// Group: foundational
void send(in ubyte[] binaryData) {
WebSocketFrame wss;
+ wss.masked = this.isClient;
wss.fin = true;
wss.opcode = WebSocketOpcode.binary;
wss.data = cast(ubyte[]) binaryData.dup;
@@ -6803,8 +7179,12 @@ version(cgi_with_websocket) {
return false;
if(!isDataPending())
return true;
- while(isDataPending())
- lowLevelReceive();
+
+ while(isDataPending()) {
+ if(lowLevelReceive() == false)
+ throw new ConnectionClosedException("Connection closed in middle of message");
+ }
+
goto checkAgain;
}
@@ -6882,23 +7262,40 @@ version(cgi_with_websocket) {
}
break;
case WebSocketOpcode.close:
- readyState_ = CLOSED;
+
+ //import std.stdio; writeln("closed ", cast(string) m.data);
+
+ ushort code = CloseEvent.StandardCloseCodes.noStatusCodePresent;
+ const(char)[] reason;
+
+ if(m.data.length >= 2) {
+ code = (m.data[0] << 8) | m.data[1];
+ reason = (cast(char[]) m.data[2 .. $]);
+ }
+
if(onclose)
- onclose();
+ onclose(CloseEvent(code, reason, true));
+
+ // if we receive one and haven't sent one back we're supposed to echo it back and close.
+ if(!closeCalled)
+ close(code, reason.idup);
+
+ readyState_ = CLOSED;
unregisterActiveSocket(this);
break;
case WebSocketOpcode.ping:
- pong();
+ // import std.stdio; writeln("ping received ", m.data);
+ pong(m.data);
break;
case WebSocketOpcode.pong:
+ // import std.stdio; writeln("pong received ", m.data);
// just really references it is still alive, nbd.
break;
default: // ignore though i could and perhaps should throw too
}
}
- // the recv thing can be invalidated so gotta copy it over ugh
if(d.length) {
m.data = m.data.dup();
}
@@ -6917,8 +7314,52 @@ version(cgi_with_websocket) {
} while(lowLevelReceive());
}
+ /++
+ Arguments for the close event. The `code` and `reason` are provided from the close message on the websocket, if they are present. The spec says code 1000 indicates a normal, default reason close, but reserves the code range from 3000-5000 for future definition; the 3000s can be registered with IANA and the 4000's are application private use. The `reason` should be user readable, but not displayed to the end user. `wasClean` is true if the server actually sent a close event, false if it just disconnected.
+
+ $(PITFALL
+ The `reason` argument references a temporary buffer and there's no guarantee it will remain valid once your callback returns. It may be freed and will very likely be overwritten. If you want to keep the reason beyond the callback, make sure you `.idup` it.
+ )
+
+ History:
+ Added March 19, 2023 (dub v11.0).
+ +/
+ static struct CloseEvent {
+ ushort code;
+ const(char)[] reason;
+ bool wasClean;
- void delegate() onclose; ///
+ string extendedErrorInformationUnstable;
+
+ /++
+ See https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 for details.
+ +/
+ enum StandardCloseCodes {
+ purposeFulfilled = 1000,
+ goingAway = 1001,
+ protocolError = 1002,
+ unacceptableData = 1003, // e.g. got text message when you can only handle binary
+ Reserved = 1004,
+ noStatusCodePresent = 1005, // not set by endpoint.
+ abnormalClosure = 1006, // not set by endpoint. closed without a Close control. FIXME: maybe keep a copy of errno around for these
+ inconsistentData = 1007, // e.g. utf8 validation failed
+ genericPolicyViolation = 1008,
+ messageTooBig = 1009,
+ clientRequiredExtensionMissing = 1010, // only the client should send this
+ unnexpectedCondition = 1011,
+ unverifiedCertificate = 1015, // not set by client
+ }
+ }
+
+ /++
+ The `CloseEvent` you get references a temporary buffer that may be overwritten after your handler returns. If you want to keep it or the `event.reason` member, remember to `.idup` it.
+
+ History:
+ The `CloseEvent` was changed to a [arsd.core.FlexibleDelegate] on March 19, 2023 (dub v11.0). Before that, `onclose` was a public member of type `void delegate()`. This change means setters still work with or without the [CloseEvent] argument.
+
+ Your onclose method is now also called on abnormal terminations. Check the `wasClean` member of the `CloseEvent` to know if it came from a close frame or other cause.
+ +/
+ arsd.core.FlexibleDelegate!(void delegate(CloseEvent event)) onclose;
void delegate() onerror; ///
void delegate(in char[]) ontextmessage; ///
void delegate(in ubyte[]) onbinarymessage; ///
@@ -6937,7 +7378,8 @@ version(cgi_with_websocket) {
onbinarymessage = dg;
}
- /* } end copy/paste */
+ /* } end copy/paste */
+
}
@@ -6983,7 +7425,9 @@ version(cgi_with_websocket) {
cgi.flush();
- return new WebSocket(cgi);
+ auto ws = new WebSocket(cgi);
+ ws.readyState_ = WebSocket.OPEN;
+ return ws;
}
// FIXME get websocket to work on other modes, not just embedded_httpd
@@ -8352,16 +8796,23 @@ final class ScheduledJobServerImplementation : ScheduledJobServer, EventIoServer
int epoll_fd() { return epoll_fd_; }
}
-///
+/++
+ History:
+ Added January 6, 2019
++/
version(with_addon_servers_connections)
interface EventSourceServer {
/++
sends this cgi request to the event server so it will be fed events. You should not do anything else with the cgi object after this.
- $(WARNING This API is extremely unstable. I might change it or remove it without notice.)
-
See_Also:
[sendEvent]
+
+ Bugs:
+ Not implemented on Windows!
+
+ History:
+ Officially stabilised on November 23, 2023 (dub v11.4). It actually worked pretty well in its original design.
+/
public static void adoptConnection(Cgi cgi, in char[] eventUrl) {
/*
@@ -8415,16 +8866,17 @@ interface EventSourceServer {
/++
Sends an event to the event server, starting it if necessary. The event server will distribute it to any listening clients, and store it for `lifetime` seconds for any later listening clients to catch up later.
- $(WARNING This API is extremely unstable. I might change it or remove it without notice.)
-
Params:
url = A string identifying this event "bucket". Listening clients must also connect to this same string. I called it `url` because I envision it being just passed as the url of the request.
event = the event type string, which is used in the Javascript addEventListener API on EventSource
data = the event data. Available in JS as `event.data`.
lifetime = the amount of time to keep this event for replaying on the event server.
- See_Also:
- [sendEventToEventServer]
+ Bugs:
+ Not implemented on Windows!
+
+ History:
+ Officially stabilised on November 23, 2023 (dub v11.4). It actually worked pretty well in its original design.
+/
public static void sendEvent(string url, string event, string data, int lifetime) {
auto s = openLocalServerConnection("/tmp/arsd_cgi_event_server", "--event-server");
@@ -9713,8 +10165,10 @@ q"css
ol.automatic-data-display {
margin: 0px;
+ /*
list-style-position: inside;
padding: 0px;
+ */
}
dl.automatic-data-display {
@@ -9764,11 +10218,13 @@ q"css
}
#site-container {
display: flex;
+ flex-wrap: wrap;
}
main {
flex: 1 1 auto;
order: 2;
min-height: calc(100vh - 64px - 64px);
+ min-width: 80ch;
padding: 4px;
padding-left: 1em;
}
@@ -10290,7 +10746,7 @@ html", true, true);
return dl;
} else static if(is(T == bool)) {
return Element.make("span", t ? "true" : "false", "automatic-data-display");
- } else static if(is(T == E[], E)) {
+ } else static if(is(T == E[], E) || is(T == E[N], E, size_t N)) {
static if(is(E : RestObject!Proxy, Proxy)) {
// treat RestObject similar to struct
auto table = cast(Table) Element.make("table");
@@ -11518,7 +11974,7 @@ auto serveStaticFile(string urlPrefix, string filename = null, string contentTyp
// man 2 sendfile
assert(urlPrefix[0] == '/');
if(filename is null)
- filename = decodeComponent(urlPrefix[1 .. $]); // FIXME is this actually correct?
+ filename = decodeUriComponent(urlPrefix[1 .. $]); // FIXME is this actually correct?
if(contentType is null) {
contentType = contentTypeFromFileExtension(filename);
}
@@ -11566,27 +12022,8 @@ auto serveStaticData(string urlPrefix, immutable(void)[] data, string contentTyp
}
string contentTypeFromFileExtension(string filename) {
- if(filename.endsWith(".png"))
- return "image/png";
- if(filename.endsWith(".apng"))
- return "image/apng";
- if(filename.endsWith(".svg"))
- return "image/svg+xml";
- if(filename.endsWith(".jpg"))
- return "image/jpeg";
- if(filename.endsWith(".html"))
- return "text/html";
- if(filename.endsWith(".css"))
- return "text/css";
- if(filename.endsWith(".js"))
- return "application/javascript";
- if(filename.endsWith(".wasm"))
- return "application/wasm";
- if(filename.endsWith(".mp3"))
- return "audio/mpeg";
- if(filename.endsWith(".pdf"))
- return "application/pdf";
- return null;
+ import arsd.core;
+ return FilePath(filename).contentTypeFromFileExtension();
}
/// This serves a directory full of static files, figuring out the content-types from file extensions.
@@ -11609,7 +12046,7 @@ auto serveStaticFileDirectory(string urlPrefix, string directory = null, bool re
assert(directory[$-1] == '/');
static bool internalHandler(string urlPrefix, Cgi cgi, Object presenter, DispatcherDetails details) {
- auto file = decodeComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct
+ auto file = decodeUriComponent(cgi.pathInfo[urlPrefix.length .. $]); // FIXME: is this actually correct
if(details.recursive) {
// never allow a backslash since it isn't in a typical url anyway and makes the following checks easier
@@ -11635,6 +12072,9 @@ auto serveStaticFileDirectory(string urlPrefix, string directory = null, bool re
return false;
}
+ if(file.length == 0)
+ return false;
+
auto contentType = contentTypeFromFileExtension(file);
auto fn = details.directory ~ file;
diff --git a/src/ext_depends/arsd/core.d b/src/ext_depends/arsd/core.d
index 49af24d..52f71ba 100644
--- a/src/ext_depends/arsd/core.d
+++ b/src/ext_depends/arsd/core.d
@@ -26,18 +26,110 @@
+/
module arsd.core;
+
+static if(__traits(compiles, () { import core.interpolation; })) {
+ import core.interpolation;
+
+ alias InterpolationHeader = core.interpolation.InterpolationHeader;
+ alias InterpolationFooter = core.interpolation.InterpolationFooter;
+ alias InterpolatedLiteral = core.interpolation.InterpolatedLiteral;
+ alias InterpolatedExpression = core.interpolation.InterpolatedExpression;
+} else {
+ // polyfill for old versions
+ struct InterpolationHeader {}
+ struct InterpolationFooter {}
+ struct InterpolatedLiteral(string literal) {}
+ struct InterpolatedExpression(string code) {}
+}
+
+version(use_arsd_core)
+ enum use_arsd_core = true;
+else
+ enum use_arsd_core = false;
+
+import core.attribute;
+static if(__traits(hasMember, core.attribute, "implicit"))
+ alias implicit = core.attribute.implicit;
+else
+ enum implicit;
+
+static if(__traits(hasMember, core.attribute, "standalone"))
+ alias standalone = core.attribute.standalone;
+else
+ enum standalone;
+
+
+
// FIXME: add callbacks on file open for tracing dependencies dynamically
// see for useful info: https://devblogs.microsoft.com/dotnet/how-async-await-really-works/
// see: https://wiki.openssl.org/index.php/Simple_TLS_Server
-import core.thread;
-import core.volatile;
-import core.atomic;
+// see: When you only want to track changes on a file or directory, be sure to open it using the O_EVTONLY flag.
+
+///ArsdUseCustomRuntime is used since other derived work from WebAssembly may be used and thus specified in the CLI
+version(Emscripten) {
+ version = EmptyEventLoop;
+ version = EmptyCoreEvent;
+ version = HasTimer;
+} else version(WebAssembly) version = ArsdUseCustomRuntime;
+else
+
+// note that kqueue might run an i/o loop on mac, ios, etc. but then NSApp.run on the io thread
+// but on bsd, you want the kqueue loop in i/o too....
+
+version(ArsdUseCustomRuntime)
+{
+ version = UseStdioWriteln;
+}
+else
+{
+ version(D_OpenD) {
+ version(OSX)
+ version=OSXCocoa;
+ version(iOS)
+ version=OSXCocoa;
+ }
+
+ version = HasFile;
+ version = HasSocket;
+ version = HasThread;
+ import core.stdc.errno;
+
+ version(Windows)
+ version = HasTimer;
+ version(linux)
+ version = HasTimer;
+ version(OSXCocoa)
+ version = HasTimer;
+}
+
+version(HasThread)
+{
+ import core.thread;
+ import core.volatile;
+ import core.atomic;
+}
+else
+{
+ // polyfill for missing core.time
+ /*
+ struct Duration {
+ static Duration max() { return Duration(); }
+ }
+ struct MonoTime {}
+ */
+}
+
import core.time;
-import core.stdc.errno;
+version(OSXCocoa) {
+ version(ArsdNoCocoa)
+ enum bool UseCocoa = false;
+ else
+ enum bool UseCocoa = true;
+}
import core.attribute;
static if(!__traits(hasMember, core.attribute, "mustuse"))
@@ -46,7 +138,13 @@ static if(!__traits(hasMember, core.attribute, "mustuse"))
// FIXME: add an arena allocator? can do task local destruction maybe.
// the three implementations are windows, epoll, and kqueue
-version(Windows) {
+
+version(Emscripten) {
+ import core.stdc.errno;
+ import core.atomic;
+ import core.volatile;
+
+} else version(Windows) {
version=Arsd_core_windows;
// import core.sys.windows.windows;
@@ -68,6 +166,13 @@ version(Windows) {
version=Arsd_core_kqueue;
import core.sys.freebsd.sys.event;
+
+ // the version in druntime doesn't have the default arg making it a pain to use when the freebsd
+ // version adds a new field
+ extern(D) void EV_SET(kevent_t* kevp, typeof(kevent_t.tupleof) args = kevent_t.tupleof.init)
+ {
+ *kevp = kevent_t(args);
+ }
} else version(DragonFlyBSD) {
// NOT ACTUALLY TESTED
version=Arsd_core_kqueue;
@@ -87,15 +192,36 @@ version(Windows) {
version=Arsd_core_kqueue;
import core.sys.darwin.sys.event;
+} else version(iOS) {
+ version=Arsd_core_kqueue;
+
+ import core.sys.darwin.sys.event;
+}
+
+// FIXME: pragma(linkerDirective, "-framework", "Cocoa") works in ldc
+version(OSXCocoa)
+ enum CocoaAvailable = true;
+else
+ enum CocoaAvailable = false;
+
+version(D_OpenD) {
+ version(OSXCocoa)
+ pragma(linkerDirective, "-framework", "Cocoa");
+} else {
+ version(OSXCocoa)
+ version(LDC)
+ pragma(linkerDirective, "-framework", "Cocoa");
}
version(Posix) {
import core.sys.posix.signal;
import core.sys.posix.unistd;
+ version(Emscripten) {} else {
import core.sys.posix.sys.un;
import core.sys.posix.sys.socket;
import core.sys.posix.netinet.in_;
+ }
}
// FIXME: the exceptions should actually give some explanatory text too (at least sometimes)
@@ -106,6 +232,110 @@ version(Posix) {
=========================
+/
+/++
+ Casts value `v` to type `T`.
+
+ $(TIP
+ This is a helper function for readability purposes.
+ The idea is to make type-casting as accessible as `to()` from `std.conv`.
+ )
+
+ ---
+ int i = cast(int)(foo * bar);
+ int i = castTo!int(foo * bar);
+
+ int j = cast(int) round(floatValue);
+ int j = round(floatValue).castTo!int;
+
+ int k = cast(int) floatValue + foobar;
+ int k = floatValue.castTo!int + foobar;
+
+ auto m = Point(
+ cast(int) calc(a.x, b.x),
+ cast(int) calc(a.y, b.y),
+ );
+ auto m = Point(
+ calc(a.x, b.x).castTo!int,
+ calc(a.y, b.y).castTo!int,
+ );
+ ---
+
+ History:
+ Added on April 24, 2024.
+ Renamed from `typeCast` to `castTo` on May 24, 2024.
+ +/
+auto ref T castTo(T, S)(auto ref S v) {
+ return cast(T) v;
+}
+
+///
+alias typeCast = castTo;
+
+/++
+ Treats the memory of one variable as if it is the type of another variable.
+
+ History:
+ Added January 20, 2025
++/
+ref T reinterpretCast(T, V)(return ref V value) @system {
+ return *cast(T*)& value;
+}
+
+/++
+ Does math as a 64 bit number, but saturates at int.min and int.max when converting back to a 32 bit int.
+
+ History:
+ Added January 1, 2025
++/
+alias NonOverflowingInt = NonOverflowingIntBase!(int.min, int.max);
+
+/// ditto
+alias NonOverflowingUint = NonOverflowingIntBase!(0, int.max);
+
+/// ditto
+struct NonOverflowingIntBase(int min, int max) {
+ this(long v) {
+ this.value = v;
+ }
+
+ private long value;
+
+ NonOverflowingInt opBinary(string op)(long rhs) {
+ return NonOverflowingInt(mixin("this.value", op, "rhs"));
+ }
+ NonOverflowingInt opBinary(string op)(NonOverflowingInt rhs) {
+ return this.opBinary!op(rhs.value);
+ }
+ NonOverflowingInt opUnary(string op)() {
+ return NonOverflowingInt(mixin(op, "this.value"));
+ }
+ NonOverflowingInt opOpAssign(string op)(long rhs) {
+ return this = this.opBinary!(op)(rhs);
+ }
+ NonOverflowingInt opOpAssign(string op)(NonOverflowingInt rhs) {
+ return this = this.opBinary!(op)(rhs.value);
+ }
+
+ int getValue() const {
+ if(value < min)
+ return min;
+ else if(value > max)
+ return max;
+ return cast(int) value;
+ }
+
+ alias getValue this;
+}
+
+unittest {
+ assert(-5.NonOverflowingInt - int.max == int.min);
+ assert(-5.NonOverflowingInt + 5 == 0);
+
+ assert(NonOverflowingInt(5) + int.max - 5 == int.max);
+ assert(NonOverflowingInt(5) + int.max - int.max - 5 == 0); // it truncates at the end of the op chain, not at intermediates
+ assert(NonOverflowingInt(0) + int.max * 2L == int.max); // note the L there is required to pass since the order of operations means mul done before it gets to the NonOverflowingInt controls
+}
+
// enum stringz : const(char)* { init = null }
/++
@@ -131,7 +361,7 @@ struct stringz {
/++
Borrows a slice of the pointer up to (but not including) the zero terminator.
+/
- const(char)[] borrow() const {
+ const(char)[] borrow() const @system {
if(raw is null)
return null;
@@ -143,22 +373,243 @@ struct stringz {
}
}
+/+
+ DateTime
+ year: 16 bits (-32k to +32k)
+ month: 4 bits
+ day: 5 bits
+
+ hour: 5 bits
+ minute: 6 bits
+ second: 6 bits
+
+ total: 25 bits + 17 bits = 42 bits
+
+ fractional seconds: 10 bits
+
+ accuracy flags: date_valid | time_valid = 2 bits
+
+ 54 bits used, 8 bits remain. reserve 1 for signed.
+
+ would need 11 bits for minute-precise dt offset but meh.
++/
+
+/++
+ A packed date/time/datetime representation added for use with LimitedVariant.
+
+ You should probably not use this much directly, it is mostly an internal storage representation.
++/
+struct PackedDateTime {
+ private ulong packedData;
+
+ string toString() const {
+ char[64] buffer;
+ size_t pos;
+
+ if(hasDate) {
+ pos += intToString(year, buffer[pos .. $], IntToStringArgs().withPadding(4)).length;
+ buffer[pos++] = '-';
+ pos += intToString(month, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
+ buffer[pos++] = '-';
+ pos += intToString(day, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
+ }
+
+ if(hasTime) {
+ if(pos)
+ buffer[pos++] = 'T';
+
+ pos += intToString(hours, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
+ buffer[pos++] = ':';
+ pos += intToString(minutes, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
+ buffer[pos++] = ':';
+ pos += intToString(seconds, buffer[pos .. $], IntToStringArgs().withPadding(2)).length;
+ if(fractionalSeconds) {
+ buffer[pos++] = '.';
+ pos += intToString(fractionalSeconds, buffer[pos .. $], IntToStringArgs().withPadding(4)).length;
+ }
+ }
+
+ return buffer[0 .. pos].idup;
+ }
+
+ /++
+ +/
+ int fractionalSeconds() const { return getFromMask(00, 10); }
+ /// ditto
+ void fractionalSeconds(int a) { setWithMask(a, 00, 10); }
+
+ /// ditto
+ int seconds() const { return getFromMask(10, 6); }
+ /// ditto
+ void seconds(int a) { setWithMask(a, 10, 6); }
+ /// ditto
+ int minutes() const { return getFromMask(16, 6); }
+ /// ditto
+ void minutes(int a) { setWithMask(a, 16, 6); }
+ /// ditto
+ int hours() const { return getFromMask(22, 5); }
+ /// ditto
+ void hours(int a) { setWithMask(a, 22, 5); }
+
+ /// ditto
+ int day() const { return getFromMask(27, 5); }
+ /// ditto
+ void day(int a) { setWithMask(a, 27, 5); }
+ /// ditto
+ int month() const { return getFromMask(32, 4); }
+ /// ditto
+ void month(int a) { setWithMask(a, 32, 4); }
+ /// ditto
+ int year() const { return getFromMask(36, 16); }
+ /// ditto
+ void year(int a) { setWithMask(a, 36, 16); }
+
+ /// ditto
+ bool hasTime() const { return cast(bool) getFromMask(52, 1); }
+ /// ditto
+ void hasTime(bool a) { setWithMask(a, 52, 1); }
+ /// ditto
+ bool hasDate() const { return cast(bool) getFromMask(53, 1); }
+ /// ditto
+ void hasDate(bool a) { setWithMask(a, 53, 1); }
+
+ private void setWithMask(int a, int bitOffset, int bitCount) {
+ auto mask = (1UL << bitCount) - 1;
+
+ packedData &= ~(mask << bitOffset);
+ packedData |= (a & mask) << bitOffset;
+ }
+
+ private int getFromMask(int bitOffset, int bitCount) const {
+ ulong packedData = this.packedData;
+ packedData >>= bitOffset;
+
+ ulong mask = (1UL << bitCount) - 1;
+
+ return cast(int) (packedData & mask);
+ }
+}
+
+unittest {
+ PackedDateTime dt;
+ dt.hours = 14;
+ dt.minutes = 30;
+ dt.seconds = 25;
+ dt.hasTime = true;
+
+ assert(dt.toString() == "14:30:25", dt.toString());
+
+ dt.hasTime = false;
+ dt.year = 2024;
+ dt.month = 5;
+ dt.day = 31;
+ dt.hasDate = true;
+
+ assert(dt.toString() == "2024-05-31", dt.toString());
+ dt.hasTime = true;
+ assert(dt.toString() == "2024-05-31T14:30:25", dt.toString());
+}
+
/++
- A limited variant to hold just a few types. It is made for the use of packing a small amount of extra data into error messages.
+ Basically a Phobos SysTime but standing alone as a simple 64 bit integer (but wrapped) for compatibility with LimitedVariant.
++/
+struct SimplifiedUtcTimestamp {
+ long timestamp;
+
+ string toString() const {
+ import core.stdc.time;
+ char[128] buffer;
+ auto ut = toUnixTime();
+ tm* t = gmtime(&ut);
+ if(t is null)
+ return "null time";
+
+ return buffer[0 .. strftime(buffer.ptr, buffer.length, "%Y-%m-%dT%H:%M:%SZ", t)].idup;
+ }
+
+ version(Windows)
+ alias time_t = int;
+
+ static SimplifiedUtcTimestamp fromUnixTime(time_t t) {
+ return SimplifiedUtcTimestamp(621_355_968_000_000_000L + t * 1_000_000_000L / 100);
+ }
+
+ time_t toUnixTime() const {
+ return cast(time_t) ((timestamp - 621_355_968_000_000_000L) / 1_000_000_0); // hnsec = 7 digits
+ }
+}
+
+unittest {
+ SimplifiedUtcTimestamp sut = SimplifiedUtcTimestamp.fromUnixTime(86_400);
+ assert(sut.toString() == "1970-01-02T00:00:00Z");
+}
+
+/++
+ A limited variant to hold just a few types. It is made for the use of packing a small amount of extra data into error messages and some transit across virtual function boundaries.
+/
/+
+ ALL OF THESE ARE SUBJECT TO CHANGE
+
* if length and ptr are both 0, it is null
* if ptr == 1, length is an integer
* if ptr == 2, length is an unsigned integer (suggest printing in hex)
* if ptr == 3, length is a combination of flags (suggest printing in binary)
* if ptr == 4, length is a unix permission thing (suggest printing in octal)
* if ptr == 5, length is a double float
+ * if ptr == 6, length is an Object ref (reinterpret casted to void*)
+
+ * if ptr == 7, length is a ticks count (from MonoTime)
+ * if ptr == 8, length is a utc timestamp (hnsecs)
+ * if ptr == 9, length is a duration (signed hnsecs)
+ * if ptr == 10, length is a date or date time (bit packed, see flags in data to determine if it is a Date, Time, or DateTime)
+ * if ptr == 11, length is a dchar
+ * if ptr == 12, length is a bool (redundant to int?)
+
+ 13, 14 reserved. prolly decimals. (4, 8 digits after decimal)
+
* if ptr == 15, length must be 0. this holds an empty, non-null, SSO string.
* if ptr >= 16 && < 24, length is reinterpret-casted a small string of length of (ptr & 0x7) + 1
+
* if length == size_t.max, ptr is interpreted as a stringz
* if ptr >= 1024, it is a non-null D string or byte array. It is a string if the length high bit is clear, a byte array if it is set. the length is what is left after you mask that out.
All other ptr values are reserved for future expansion.
+
+ It basically can store:
+ null
+ type details = must be 0
+ int (actually long)
+ type details = formatting hints
+ float (actually double)
+ type details = formatting hints
+ dchar (actually enum - upper half is the type tag, lower half is the member tag)
+ type details = ???
+ decimal
+ type details = precision specifier
+ object
+ type details = ???
+ timestamp
+ type details: ticks, utc timestamp, relative duration
+
+ sso
+ stringz
+
+ or it is bytes or a string; a normal D array (just bytes has a high bit set on length).
+
+ But there are subtypes of some of those; ints can just have formatting hints attached.
+ Could reserve 0-7 as low level type flag (null, int, float, pointer, object)
+ 15-24 still can be the sso thing
+
+ We have 10 bits really.
+
+ 00000 00000
+ ????? OOLLL
+
+ The ????? are type details bits.
+
+ 64 bits decmial to 4 points of precision needs... 14 bits for the small part (so max of 4 digits)? so 50 bits for the big part (max of about 1 quadrillion)
+ ...actually it can just be a dollars * 10000 + cents * 100.
+
+/
struct LimitedVariant {
@@ -172,6 +623,19 @@ struct LimitedVariant {
intBinary,
intOctal,
double_,
+ object,
+
+ monoTime,
+ utcTimestamp,
+ duration,
+ dateTime,
+
+ // FIXME boolean? char? decimal?
+ // could do enums by way of a pointer but kinda iffy
+
+ // maybe some kind of prefixed string too for stuff like xml and json or enums etc.
+
+ // fyi can also use stringzs or length-prefixed string pointers
emptySso,
stringSso,
stringz,
@@ -182,7 +646,9 @@ struct LimitedVariant {
}
/++
+ Each datum stored in the LimitedVariant has a tag associated with it.
+ Each tag belongs to one or more data families.
+/
Contains contains() const {
auto tag = cast(size_t) ptr;
@@ -194,6 +660,13 @@ struct LimitedVariant {
case 3: return Contains.intBinary;
case 4: return Contains.intOctal;
case 5: return Contains.double_;
+ case 6: return Contains.object;
+
+ case 7: return Contains.monoTime;
+ case 8: return Contains.utcTimestamp;
+ case 9: return Contains.duration;
+ case 10: return Contains.dateTime;
+
case 15: return length is null ? Contains.emptySso : Contains.invalid;
default:
if(tag >= 16 && tag < 24) {
@@ -210,6 +683,11 @@ struct LimitedVariant {
}
/// ditto
+ bool containsNull() const {
+ return contains() == Contains.null_;
+ }
+
+ /// ditto
bool containsInt() const {
with(Contains)
switch(contains) {
@@ -220,12 +698,33 @@ struct LimitedVariant {
}
}
+ // all specializations of int...
+
+ /// ditto
+ bool containsMonoTime() const {
+ return contains() == Contains.monoTime;
+ }
+ /// ditto
+ bool containsUtcTimestamp() const {
+ return contains() == Contains.utcTimestamp;
+ }
+ /// ditto
+ bool containsDuration() const {
+ return contains() == Contains.duration;
+ }
+ /// ditto
+ bool containsDateTime() const {
+ return contains() == Contains.dateTime;
+ }
+
+ // done int specializations
+
/// ditto
bool containsString() const {
with(Contains)
switch(contains) {
case null_, emptySso, stringSso, string:
- // case stringz:
+ case stringz:
return true;
default:
return false;
@@ -281,6 +780,8 @@ struct LimitedVariant {
return (cast(char*) &length)[0 .. len];
case string:
return (cast(const(char)*) ptr)[0 .. cast(size_t) length];
+ case stringz:
+ return arsd.core.stringz(cast(char*) ptr).borrow;
default:
Throw(); assert(0);
}
@@ -297,9 +798,11 @@ struct LimitedVariant {
/// ditto
double getDouble() const {
- if(containsDouble)
- return *cast(double*) &length;
- else
+ if(containsDouble) {
+ floathack hack;
+ hack.e = cast(void*) length; // casting away const
+ return hack.d;
+ } else
Throw();
assert(0);
}
@@ -317,6 +820,55 @@ struct LimitedVariant {
}
}
+ /// ditto
+ Object getObject() const {
+ with(Contains)
+ switch(contains()) {
+ case null_:
+ return null;
+ case object:
+ return cast(Object) length; // FIXME const correctness sigh
+ default:
+ Throw(); assert(0);
+ }
+ }
+
+ /// ditto
+ MonoTime getMonoTime() const {
+ if(containsMonoTime) {
+ MonoTime time;
+ __traits(getMember, time, "_ticks") = cast(long) length;
+ return time;
+ } else
+ Throw();
+ assert(0);
+ }
+ /// ditto
+ SimplifiedUtcTimestamp getUtcTimestamp() const {
+ if(containsUtcTimestamp)
+ return SimplifiedUtcTimestamp(cast(long) length);
+ else
+ Throw();
+ assert(0);
+ }
+ /// ditto
+ Duration getDuration() const {
+ if(containsDuration)
+ return hnsecs(cast(long) length);
+ else
+ Throw();
+ assert(0);
+ }
+ /// ditto
+ PackedDateTime getDateTime() const {
+ if(containsDateTime)
+ return PackedDateTime(cast(long) length);
+ else
+ Throw();
+ assert(0);
+ }
+
+
/++
+/
@@ -344,24 +896,37 @@ struct LimitedVariant {
return intHelper("0b", 2);
case intOctal:
return intHelper("0o", 8);
- case emptySso, stringSso, string:
+ case emptySso, stringSso, string, stringz:
return getString().idup;
case bytes:
auto b = getBytes();
return "<bytes>"; // FIXME
-
+ case object:
+ auto o = getObject();
+ return o is null ? "null" : o.toString();
+ case monoTime:
+ return getMonoTime.toString();
+ case utcTimestamp:
+ return getUtcTimestamp().toString();
+ case duration:
+ return getDuration().toString();
+ case dateTime:
+ return getDateTime().toString();
case double_:
- assert(0); // FIXME
- case stringz:
- assert(0); // FIXME
+ auto d = getDouble();
+
+ import core.stdc.stdio;
+ char[128] buffer;
+ auto count = snprintf(buffer.ptr, buffer.length, "%.17lf", d);
+ return buffer[0 .. count].idup;
case invalid:
return "<invalid>";
}
}
/++
-
+ Note for integral types that are not `int` and `long` (for example, `short` or `ubyte`), you might want to explicitly convert them to `int`.
+/
this(string s) {
ptr = cast(const(ubyte)*) s.ptr;
@@ -369,6 +934,17 @@ struct LimitedVariant {
}
/// ditto
+ this(const(char)* stringz) {
+ if(stringz !is null) {
+ ptr = cast(const(ubyte)*) stringz;
+ length = cast(void*) size_t.max;
+ } else {
+ ptr = null;
+ length = null;
+ }
+ }
+
+ /// ditto
this(const(ubyte)[] b) {
ptr = cast(const(ubyte)*) b.ptr;
length = cast(void*) (b.length | (1UL << (size_t.sizeof * 8 - 1)));
@@ -389,12 +965,55 @@ struct LimitedVariant {
}
/// ditto
- version(none)
+ this(int i, int base = 10) {
+ this(cast(long) i, base);
+ }
+
+ /// ditto
+ this(bool i) {
+ // FIXME?
+ this(cast(long) i);
+ }
+
+ /// ditto
this(double d) {
- // this crashes dmd! omg
- assert(0);
- // ptr = cast(ubyte*) 15;
- // length = cast(void*) *cast(size_t*) &d;
+ // the reinterpret cast hack crashes dmd! omg
+ ptr = cast(ubyte*) 5;
+
+ floathack h;
+ h.d = d;
+
+ this.length = h.e;
+ }
+
+ /// ditto
+ this(Object o) {
+ this.ptr = cast(ubyte*) 6;
+ this.length = cast(void*) o;
+ }
+
+ /// ditto
+ this(MonoTime a) {
+ this.ptr = cast(ubyte*) 7;
+ this.length = cast(void*) a.ticks;
+ }
+
+ /// ditto
+ this(SimplifiedUtcTimestamp a) {
+ this.ptr = cast(ubyte*) 8;
+ this.length = cast(void*) a.timestamp;
+ }
+
+ /// ditto
+ this(Duration a) {
+ this.ptr = cast(ubyte*) 9;
+ this.length = cast(void*) a.total!"hnsecs";
+ }
+
+ /// ditto
+ this(PackedDateTime a) {
+ this.ptr = cast(ubyte*) 10;
+ this.length = cast(void*) a.packedData;
}
}
@@ -415,6 +1034,16 @@ unittest {
assert(v3.getBytes() == [1, 2, 3]);
}
+private union floathack {
+ // in 32 bit we'll use float instead since it at least fits in the void*
+ static if(double.sizeof == (void*).sizeof) {
+ double d;
+ } else {
+ float d;
+ }
+ void* e;
+}
+
/++
This is a dummy type to indicate the end of normal arguments and the beginning of the file/line inferred args. It is meant to ensure you don't accidentally send a string that is interpreted as a filename when it was meant to be a normal argument to the function and trigger the wrong overload.
+/
@@ -504,7 +1133,7 @@ struct OwnedClass(Class) {
}
}
-
+// might move RecyclableMemory here
version(Posix)
package(arsd) void makeNonBlocking(int fd) {
@@ -941,7 +1570,7 @@ inout(char)[] stripRightInternal(return inout(char)[] s) {
}
/++
- Shortcut for converting some types to string without invoking Phobos (but it will as a last resort).
+ Shortcut for converting some types to string without invoking Phobos (but it may as a last resort).
History:
Moved from color.d to core.d in March 2023 (dub v11.0).
@@ -950,8 +1579,6 @@ string toStringInternal(T)(T t) {
char[32] buffer;
static if(is(T : string))
return t;
- else static if(is(T : long))
- return intToString(t, buffer[]).idup;
else static if(is(T == enum)) {
switch(t) {
foreach(memberName; __traits(allMembers, T)) {
@@ -961,9 +1588,20 @@ string toStringInternal(T)(T t) {
default:
return "<unknown>";
}
+ } else static if(is(T : long)) {
+ return intToString(t, buffer[]).idup;
+ } else static if(is(T : const E[], E)) {
+ string ret = "[";
+ foreach(idx, e; t) {
+ if(idx)
+ ret ~= ", ";
+ ret ~= toStringInternal(e);
+ }
+ ret ~= "]";
+ return ret;
} else {
- import std.conv;
- return to!string(t);
+ static assert(0, T.stringof ~ " makes compile too slow");
+ // import std.conv; return to!string(t);
}
}
@@ -1009,6 +1647,193 @@ unittest {
assert(flagsToString!MyFlags(2) == "b");
}
+private enum dchar replacementDchar = '\uFFFD';
+
+package size_t encodeUtf8(out char[4] buf, dchar c) @safe pure {
+ if (c <= 0x7F)
+ {
+ assert(isValidDchar(c));
+ buf[0] = cast(char) c;
+ return 1;
+ }
+ if (c <= 0x7FF)
+ {
+ assert(isValidDchar(c));
+ buf[0] = cast(char)(0xC0 | (c >> 6));
+ buf[1] = cast(char)(0x80 | (c & 0x3F));
+ return 2;
+ }
+ if (c <= 0xFFFF)
+ {
+ if (0xD800 <= c && c <= 0xDFFF)
+ c = replacementDchar;
+
+ assert(isValidDchar(c));
+ L3:
+ buf[0] = cast(char)(0xE0 | (c >> 12));
+ buf[1] = cast(char)(0x80 | ((c >> 6) & 0x3F));
+ buf[2] = cast(char)(0x80 | (c & 0x3F));
+ return 3;
+ }
+ if (c <= 0x10FFFF)
+ {
+ assert(isValidDchar(c));
+ buf[0] = cast(char)(0xF0 | (c >> 18));
+ buf[1] = cast(char)(0x80 | ((c >> 12) & 0x3F));
+ buf[2] = cast(char)(0x80 | ((c >> 6) & 0x3F));
+ buf[3] = cast(char)(0x80 | (c & 0x3F));
+ return 4;
+ }
+
+ assert(!isValidDchar(c));
+ c = replacementDchar;
+ goto L3;
+}
+
+
+
+private bool isValidDchar(dchar c) pure nothrow @safe @nogc
+{
+ return c < 0xD800 || (c > 0xDFFF && c <= 0x10FFFF);
+}
+
+// technically s is octets but meh
+package string encodeUriComponent(string s) {
+ char[3] encodeChar(char c) {
+ char[3] buffer;
+ buffer[0] = '%';
+
+ enum hexchars = "0123456789ABCDEF";
+ buffer[1] = hexchars[c >> 4];
+ buffer[2] = hexchars[c & 0x0f];
+
+ return buffer;
+ }
+
+ string n;
+ size_t previous = 0;
+ foreach(idx, char ch; s) {
+ if(
+ (ch >= 'A' && ch <= 'Z')
+ ||
+ (ch >= 'a' && ch <= 'z')
+ ||
+ (ch >= '0' && ch <= '9')
+ || ch == '-' || ch == '_' || ch == '.' || ch == '~' // unreserved set
+ || ch == '!' || ch == '*' || ch == '\''|| ch == '(' || ch == ')' // subdelims but allowed in uri component (phobos also no encode them)
+ ) {
+ // does not need encoding
+ } else {
+ n ~= s[previous .. idx];
+ n ~= encodeChar(ch);
+ previous = idx + 1;
+ }
+ }
+
+ if(n.length) {
+ n ~= s[previous .. $];
+ return n;
+ } else {
+ return s; // nothing needed encoding
+ }
+}
+unittest {
+ assert(encodeUriComponent("foo") == "foo");
+ assert(encodeUriComponent("f33Ao") == "f33Ao");
+ assert(encodeUriComponent("/") == "%2F");
+ assert(encodeUriComponent("/foo") == "%2Ffoo");
+ assert(encodeUriComponent("foo/") == "foo%2F");
+ assert(encodeUriComponent("foo/bar") == "foo%2Fbar");
+ assert(encodeUriComponent("foo/bar/") == "foo%2Fbar%2F");
+}
+
+// FIXME: I think if translatePlusToSpace we're supposed to do newline normalization too
+package string decodeUriComponent(string s, bool translatePlusToSpace = false) {
+ int skipping = 0;
+ size_t previous = 0;
+ string n = null;
+ foreach(idx, char ch; s) {
+ if(skipping) {
+ skipping--;
+ continue;
+ }
+
+ if(ch == '%') {
+ int hexDecode(char c) {
+ if(c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ else if(c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+ else if(c >= '0' && c <= '9')
+ return c - '0' + 0;
+ else
+ throw ArsdException!"Invalid percent-encoding"("Invalid char encountered", idx, s);
+ }
+
+ skipping = 2;
+ n ~= s[previous .. idx];
+
+ if(idx + 2 >= s.length)
+ throw ArsdException!"Invalid percent-encoding"("End of string reached", idx, s);
+
+ n ~= (hexDecode(s[idx + 1]) << 4) | hexDecode(s[idx + 2]);
+
+ previous = idx + 3;
+ } else if(translatePlusToSpace && ch == '+') {
+ n ~= s[previous .. idx];
+ n ~= " ";
+ previous = idx + 1;
+ }
+ }
+
+ if(n.length) {
+ n ~= s[previous .. $];
+ return n;
+ } else {
+ return s; // nothing needed decoding
+ }
+}
+
+unittest {
+ assert(decodeUriComponent("foo") == "foo");
+ assert(decodeUriComponent("%2F") == "/");
+ assert(decodeUriComponent("%2f") == "/");
+ assert(decodeUriComponent("%2Ffoo") == "/foo");
+ assert(decodeUriComponent("foo%2F") == "foo/");
+ assert(decodeUriComponent("foo%2Fbar") == "foo/bar");
+ assert(decodeUriComponent("foo%2Fbar%2F") == "foo/bar/");
+ assert(decodeUriComponent("%2F%2F%2F") == "///");
+
+ assert(decodeUriComponent("+") == "+");
+ assert(decodeUriComponent("+", true) == " ");
+}
+
+private auto toDelegate(T)(T t) {
+ // static assert(is(T == function)); // lol idk how to do what i actually want here
+
+ static if(is(T Return == return))
+ static if(is(typeof(*T) Params == __parameters)) {
+ static struct Wrapper {
+ Return call(Params params) {
+ return (cast(T) &this)(params);
+ }
+ }
+ return &((cast(Wrapper*) t).call);
+ } else static assert(0, "could not get params");
+ else static assert(0, "could not get return value");
+}
+
+@system unittest {
+ int function(int) fn;
+ fn = (a) { return a; };
+
+ int delegate(int) dg = toDelegate(fn);
+
+ assert(dg.ptr is fn); // it stores the original function as the context pointer
+ assert(dg.funcptr !is fn); // which is called through a lil trampoline
+ assert(dg(5) == 5); // and forwards the args correctly
+}
+
/++
This populates a struct from a list of values (or other expressions, but it only looks at the values) based on types of the members, with one exception: `bool` members.. maybe.
@@ -1405,7 +2230,7 @@ class InvalidArgumentsException : ArsdExceptionBase {
override void getAdditionalPrintableInformation(scope void delegate(string name, in char[] value) sink) const {
// FIXME: print the details better
foreach(arg; invalidArguments)
- sink("invalidArguments[]", arg.name ~ " " ~ arg.description);
+ sink(arg.name, arg.givenValue.toString ~ " - " ~ arg.description);
}
}
@@ -1592,7 +2417,7 @@ struct SystemErrorCode {
Constructs a string containing both the code and the explanation string.
+/
string toString() const {
- return codeAsString ~ " " ~ errorString;
+ return "[" ~ codeAsString ~ "] " ~ errorString;
}
/++
@@ -1607,14 +2432,14 @@ struct SystemErrorCode {
return intToString(code, buffer[]).idup;
case Type.win32:
buffer[0 .. 2] = "0x";
- return buffer[0 .. 2 + intToString(code, buffer[2 .. $], IntToStringArgs().withRadix(16).withPadding(8)).length].idup;
+ return buffer[0 .. 2 + intToString(cast(uint) code, buffer[2 .. $], IntToStringArgs().withRadix(16).withPadding(8)).length].idup;
}
}
/++
A text explanation of the code. See [codeAsString] for a string representation of the numeric representation.
+/
- string errorString() const {
+ string errorString() const @trusted {
final switch(type) {
case Type.errno:
import core.stdc.string;
@@ -1940,15 +2765,95 @@ interface ICoreEventLoop {
Runs the event loop for this thread until the `until` delegate returns `true`.
+/
final void run(scope bool delegate() until) {
- while(!until()) {
+ while(!exitApplicationRequested && !until()) {
runOnce();
}
}
+ private __gshared bool exitApplicationRequested;
+
+ final static void exitApplication() {
+ exitApplicationRequested = true;
+ // FIXME: wake up all the threads
+ }
+
+ /++
+ Returns details from a call to [runOnce]. Use the named methods here for details, or it can be used in a `while` loop directly thanks to its `opCast` automatic conversion to `bool`.
+
+ History:
+ Added December 28, 2023
+ +/
+ static struct RunOnceResult {
+ enum Possibilities {
+ CarryOn,
+ LocalExit,
+ GlobalExit,
+ Interrupted
+
+ }
+ Possibilities result;
+
+ /++
+ Returns `true` if the event loop should generally continue.
+
+ Might be false if the local loop was exited or if the application is supposed to exit. If this is `false`, check [applicationExitRequested] to determine if you should move on to other work or start your final cleanup process.
+ +/
+ bool shouldContinue() const {
+ return result == Possibilities.CarryOn;
+ }
+
+ /++
+ Returns `true` if [ICoreEventLoop.exitApplication] was called during this event, or if the user or operating system has requested the application exit.
+
+ Details might be available through other means.
+ +/
+ bool applicationExitRequested() const {
+ return result == Possibilities.GlobalExit;
+ }
+
+ /++
+ Returns [shouldContinue] when used in a context for an implicit bool (e.g. `if` statements).
+ +/
+ bool opCast(T : bool)() const {
+ reutrn shouldContinue();
+ }
+ }
+
/++
Runs a single iteration of the event loop for this thread. It will return when the first thing happens, but that thing might be totally uninteresting to anyone, or it might trigger significant work you'll wait on.
+
+ Note that running this externally instead of `run` gives only the $(I illusion) of control. You're actually better off setting a recurring timer if you need things to run on a clock tick, or a single-shot timer for a one time event. They're more likely to be called on schedule inside this function than outside it.
+
+ Parameters:
+ timeout = a timeout value for an idle loop. There is no guarantee you won't return earlier or later than this; the function might run longer than the timeout if it has work to do. Pass `Duration.max` (the default) for an infinite duration timeout (but remember, once it finds work to do, including a false-positive wakeup or interruption by the operating system, it will return early anyway).
+
+ History:
+ Prior to December 28, 2023, it returned `void` and took no arguments. This change is breaking, but since the entire module is documented as unstable, it was permitted to happen as that document provided prior notice.
+/
- void runOnce();
+ RunOnceResult runOnce(Duration timeout = Duration.max);
+
+ /++
+ Adds a delegate to be called on each loop iteration, called based on the `timingFlags`.
+
+
+ The order in which the delegates are called is undefined and may change with each iteration of the loop. Additionally, when and how many times a loop iterates is undefined; multiple events might be handled by each iteration, or sometimes, nothing will be handled and it woke up spuriously. Your delegates need to be ok with all of this.
+
+ Parameters:
+ dg = the delegate to call
+ timingFlags =
+ 0: never actually run the function; it can assert error if you pass this
+ 1: run before each loop OS wait call
+ 2: run after each loop OS wait call
+ 3: run both before and after each OS wait call
+ 4: single shot?
+ 8: no-coalesce? (if after was just run, it will skip the before loops unless this flag is set)
+
+ +/
+ void addDelegateOnLoopIteration(void delegate() dg, uint timingFlags);
+
+ final void addDelegateOnLoopIteration(void function() dg, uint timingFlags) {
+ addDelegateOnLoopIteration(toDelegate(dg), timingFlags);
+ }
// to send messages between threads, i'll queue up a function that just call dispatchMessage. can embed the arg inside the callback helper prolly.
// tho i might prefer to actually do messages w/ run payloads so it is easier to deduplicate i can still dedupe by insepcting the call args so idk
@@ -1973,7 +2878,10 @@ interface ICoreEventLoop {
} else version(Arsd_core_kqueue) {
// intentionally blank - all registrations are one-shot there
// FIXME: actually it might not have gone off yet, in that case we do need to delete the filter
- } else static assert(0);
+ } else version(EmptyCoreEvent) {
+
+ }
+ else static assert(0);
cb.release();
this = typeof(this).init;
@@ -1999,6 +2907,8 @@ interface ICoreEventLoop {
} else version(Arsd_core_kqueue) {
// intentionally blank - all registrations are one-shot there
// FIXME: actually it might not have gone off yet, in that case we do need to delete the filter
+ } else version(EmptyCoreEvent) {
+
} else static assert(0);
cb.release();
@@ -2018,6 +2928,31 @@ interface ICoreEventLoop {
RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb);
RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb);
}
+
+ version(Windows) {
+ @mustuse
+ static struct UnregisterToken {
+ private CoreEventLoopImplementation impl;
+ private HANDLE handle;
+ private CallbackHelper cb;
+
+ /++
+ Unregisters the handle from the event loop and releases the reference to the callback held by the event loop (which will probably free it).
+
+ You must call this when you're done. Normally, this will be right before you close the handle.
+ +/
+ void unregister() {
+ assert(impl !is null, "Cannot reuse unregister token");
+
+ impl.unregisterHandle(handle, cb);
+
+ cb.release();
+ this = typeof(this).init;
+ }
+ }
+
+ UnregisterToken addCallbackOnHandleReady(HANDLE handle, CallbackHelper cb);
+ }
}
/++
@@ -2072,9 +3007,24 @@ package(arsd) enum EventLoopType {
Tasks are assigned to a worker thread and may share it with other tasks.
+/
+/+
+private ThreadLocalGcRoots gcRoots;
+
+private struct ThreadLocalGcRoots {
+ // it actually would be kinda cool if i could tell the GC
+ // that only part of this array is actually used so it can skip
+ // scanning the rest. but meh.
+ const(void)*[] roots;
+
+ void* add(const(void)* what) {
+ roots ~= what;
+ return &roots[$-1];
+ }
+}
++/
// the GC may not be able to see this! remember, it can be hidden inside kernel buffers
-private class CallbackHelper {
+package(arsd) class CallbackHelper {
import core.memory;
void call() {
@@ -2086,10 +3036,12 @@ private class CallbackHelper {
void*[3] argsStore;
void addref() {
+ version(HasThread)
atomicOp!"+="(refcount, 1);
}
void release() {
+ version(HasThread)
if(atomicOp!"-="(refcount, 1) <= 0) {
if(flags & 1)
GC.removeRoot(cast(void*) this);
@@ -2104,6 +3056,7 @@ private class CallbackHelper {
}
this(void delegate() callback, bool addRoot = true) {
+ version(HasThread)
if(addRoot) {
GC.addRoot(cast(void*) this);
this.flags |= 1;
@@ -2114,29 +3067,384 @@ private class CallbackHelper {
}
}
+inout(char)[] trimSlashesRight(inout(char)[] txt) {
+ //if(txt.length && (txt[0] == '/' || txt[0] == '\\'))
+ //txt = txt[1 .. $];
+
+ if(txt.length && (txt[$-1] == '/' || txt[$-1] == '\\'))
+ txt = txt[0 .. $-1];
+
+ return txt;
+}
+
+enum TreatAsWindowsPath {
+ guess,
+ ifVersionWindows,
+ yes,
+ no,
+}
+
+// FIXME add uri from cgi/http2 and make sure the relative methods are reasonable compatible
+
/++
This represents a file. Technically, file paths aren't actually strings (for example, on Linux, they need not be valid utf-8, while a D string is supposed to be), even though we almost always use them like that.
This type is meant to represent a filename / path. I might not keep it around.
+/
struct FilePath {
- string path;
+ private string path;
+
+ this(string path) {
+ this.path = path;
+ }
- bool isNull() {
+ bool isNull() const {
return path is null;
}
- bool opCast(T:bool)() {
+ bool opCast(T:bool)() const {
return !isNull;
}
- string toString() {
+ string toString() const {
return path;
}
//alias toString this;
+
+ /+ +++++++++++++++++ +/
+ /+ String analysis +/
+ /+ +++++++++++++++++ +/
+
+ FilePath makeAbsolute(FilePath base, TreatAsWindowsPath treatAsWindowsPath = TreatAsWindowsPath.guess) const {
+ if(base.path.length == 0)
+ return this.removeExtraParts();
+ if(base.path[$-1] != '/' && base.path[$-1] != '\\')
+ base.path ~= '/';
+
+ bool isWindowsPath;
+ final switch(treatAsWindowsPath) {
+ case TreatAsWindowsPath.guess:
+ case TreatAsWindowsPath.yes:
+ isWindowsPath = true;
+ break;
+ case TreatAsWindowsPath.no:
+ isWindowsPath = false;
+ break;
+ case TreatAsWindowsPath.ifVersionWindows:
+ version(Windows)
+ isWindowsPath = true;
+ else
+ isWindowsPath = false;
+ break;
+ }
+ if(isWindowsPath) {
+ if(this.isUNC)
+ return this.removeExtraParts();
+ if(this.driveName)
+ return this.removeExtraParts();
+ if(this.path.length >= 1 && (this.path[0] == '/' || this.path[0] == '\\')) {
+ // drive-relative path, take the drive from the base
+ return FilePath(base.driveName ~ this.path).removeExtraParts();
+ }
+ // otherwise, take the dir name from the base and add us onto it
+ return FilePath(base.directoryName ~ this.path).removeExtraParts();
+ } else {
+ if(this.path.length >= 1 && this.path[0] == '/')
+ return this.removeExtraParts();
+ else
+ return FilePath(base.directoryName ~ this.path).removeExtraParts();
+ }
+ }
+
+ // dg returns true to continue, false to break
+ void foreachPathComponent(scope bool delegate(size_t index, in char[] component) dg) const {
+ size_t start;
+ size_t skip;
+ if(isUNC()) {
+ dg(start, this.path[start .. 2]);
+ start = 2;
+ skip = 2;
+ }
+ foreach(idx, ch; this.path) {
+ if(skip) { skip--; continue; }
+ if(ch == '/' || ch == '\\') {
+ if(!dg(start, this.path[start .. idx + 1]))
+ return;
+ start = idx + 1;
+ }
+ }
+ if(start != path.length)
+ dg(start, this.path[start .. $]);
+ }
+
+ // remove cases of // or /. or /.. Only valid to call this on an absolute path.
+ private FilePath removeExtraParts() const {
+ bool changeNeeded;
+ foreachPathComponent((idx, component) {
+ auto name = component.trimSlashesRight;
+ if(name.length == 0 && idx != 0)
+ changeNeeded = true;
+ if(name == "." || name == "..")
+ changeNeeded = true;
+ return !changeNeeded;
+ });
+
+ if(!changeNeeded)
+ return this;
+
+ string newPath;
+ foreachPathComponent((idx, component) {
+ auto name = component.trimSlashesRight;
+ if(component == `\\`) // must preserve unc paths
+ newPath ~= component;
+ else if(name.length == 0 && idx != 0)
+ {}
+ else if(name == ".")
+ {}
+ else if(name == "..") {
+ // remove the previous component, unless it is the first component
+ auto sofar = FilePath(newPath);
+ size_t previousComponentIndex;
+ sofar.foreachPathComponent((idx2, component2) {
+ if(idx2 != newPath.length)
+ previousComponentIndex = idx2;
+ return true;
+ });
+
+ if(previousComponentIndex && previousComponentIndex != newPath.length) {
+ newPath = newPath[0 .. previousComponentIndex];
+ //newPath.assumeSafeAppend();
+ }
+ } else {
+ newPath ~= component;
+ }
+
+ return true;
+ });
+
+ return FilePath(newPath);
+ }
+
+ // assuming we're looking at a Windows path...
+ bool isUNC() const {
+ return (path.length > 2 && path[0 .. 2] == `\\`);
+ }
+
+ // assuming we're looking at a Windows path...
+ string driveName() const {
+ if(path.length < 2)
+ return null;
+ if((path[0] >= 'A' && path[0] <= 'Z') || (path[0] >= 'a' && path[0] <= 'z')) {
+ if(path[1] == ':') {
+ if(path.length == 2 || path[2] == '\\' || path[2] == '/')
+ return path[0 .. 2];
+ }
+ }
+ return null;
+ }
+
+ /+
+ bool isAbsolute() {
+ if(path.length && path[0] == '/')
+ return true;
+
+ }
+
+ FilePath relativeTo() {
+
+ }
+
+ bool matchesGlobPattern(string globPattern) {
+
+ }
+
+ this(string directoryName, string filename) {}
+ this(string directoryName, string basename, string extension) {}
+
+ // remove ./, ../, stuff like that
+ FilePath normalize(FilePath relativeTo) {}
+ +/
+
+ /++
+ Returns the path with the directory cut off.
+ +/
+ string filename() {
+ foreach_reverse(idx, ch; path) {
+ if(ch == '\\' || ch == '/')
+ return path[idx + 1 .. $];
+ }
+ return path;
+ }
+
+ /++
+ Returns the path with the filename cut off.
+ +/
+ string directoryName() {
+ auto fn = this.filename();
+ if(fn is path)
+ return null;
+ return path[0 .. $ - fn.length];
+ }
+
+ /++
+ Returns the file extension, if present, including the last dot.
+ +/
+ string extension() {
+ foreach_reverse(idx, ch; path) {
+ if(ch == '.')
+ return path[idx .. $];
+ }
+ return null;
+ }
+
+ /++
+ Guesses the media (aka mime) content type from the file extension for this path.
+
+ Only has a few things supported. Returns null if it doesn't know.
+
+ History:
+ Moved from arsd.cgi to arsd.core.FilePath on October 28, 2024
+ +/
+ string contentTypeFromFileExtension() {
+ switch(this.extension) {
+ // images
+ case ".png":
+ return "image/png";
+ case ".apng":
+ return "image/apng";
+ case ".svg":
+ return "image/svg+xml";
+ case ".jpg":
+ case ".jpeg":
+ return "image/jpeg";
+
+ case ".txt":
+ return "text/plain";
+
+ case ".html":
+ return "text/html";
+ case ".css":
+ return "text/css";
+ case ".js":
+ return "application/javascript";
+ case ".wasm":
+ return "application/wasm";
+
+ case ".mp3":
+ return "audio/mpeg";
+
+ case ".pdf":
+ return "application/pdf";
+
+ default:
+ return null;
+ }
+ }
+}
+
+unittest {
+ FilePath fn;
+
+ fn = FilePath("dir/name.ext");
+ assert(fn.directoryName == "dir/");
+ assert(fn.filename == "name.ext");
+ assert(fn.extension == ".ext");
+
+ fn = FilePath(null);
+ assert(fn.directoryName is null);
+ assert(fn.filename is null);
+ assert(fn.extension is null);
+
+ fn = FilePath("file.txt");
+ assert(fn.directoryName is null);
+ assert(fn.filename == "file.txt");
+ assert(fn.extension == ".txt");
+
+ fn = FilePath("dir/");
+ assert(fn.directoryName == "dir/");
+ assert(fn.filename == "");
+ assert(fn.extension is null);
+
+ assert(fn.makeAbsolute(FilePath("/")).path == "/dir/");
+ assert(fn.makeAbsolute(FilePath("file.txt")).path == "file.txt/dir/"); // FilePaths as a base are ALWAYS treated as a directory
+ assert(FilePath("file.txt").makeAbsolute(fn).path == "dir/file.txt");
+
+ assert(FilePath("c:/file.txt").makeAbsolute(FilePath("d:/")).path == "c:/file.txt");
+ assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/")).path == "d:/file.txt");
+
+ assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/foo")).path == "d:/file.txt");
+ assert(FilePath("../file.txt").makeAbsolute(FilePath("d:/")).path == "d:/file.txt");
+ assert(FilePath("../file.txt").makeAbsolute(FilePath("/home/me")).path == "/home/file.txt");
+ assert(FilePath("../file.txt").makeAbsolute(FilePath(`\\arsd\me`)).path == `\\arsd\file.txt`);
+ assert(FilePath("../../file.txt").makeAbsolute(FilePath("/home/me")).path == "/file.txt");
+ assert(FilePath("../../../file.txt").makeAbsolute(FilePath("/home/me")).path == "/file.txt");
+
+ assert(FilePath("test/").makeAbsolute(FilePath("/home/me/")).path == "/home/me/test/");
+ assert(FilePath("/home/me/test/").makeAbsolute(FilePath("/home/me/test/")).path == "/home/me/test/");
+}
+
+version(HasFile)
+/++
+ History:
+ Added January 2, 2024
++/
+FilePath getCurrentWorkingDirectory() {
+ version(Windows) {
+ wchar[256] staticBuffer;
+ wchar[] buffer = staticBuffer[];
+
+ try_again:
+ auto ret = GetCurrentDirectoryW(cast(DWORD) buffer.length, buffer.ptr);
+ if(ret == 0)
+ throw new WindowsApiException("GetCurrentDirectoryW", GetLastError());
+ if(ret < buffer.length) {
+ return FilePath(makeUtf8StringFromWindowsString(buffer[0 .. ret]));
+ } else {
+ buffer.length = ret;
+ goto try_again;
+ }
+ } else version(Posix) {
+ char[128] staticBuffer;
+ char[] buffer = staticBuffer[];
+
+ try_again:
+ auto ret = getcwd(buffer.ptr, buffer.length);
+ if(ret is null && errno == ERANGE && buffer.length < 4096 / 2) {
+ buffer.length = buffer.length * 2;
+ goto try_again;
+ } else if(ret is null) {
+ throw new ErrnoApiException("getcwd", errno);
+ }
+ return FilePath(stringz(ret).borrow.idup);
+ } else
+ assert(0, "Not implemented");
+}
+
+/+
+struct FilePathGeneric {
+
+}
+
+struct FilePathWin32 {
+
}
+struct FilePathPosix {
+
+}
+
+struct FilePathWindowsUnc {
+
+}
+
+version(Windows)
+ alias FilePath = FilePathWin32;
+else
+ alias FilePath = FilePathPosix;
++/
+
+
/++
Represents a generic async, waitable request.
+/
@@ -2298,7 +3606,7 @@ version(Windows) {
/++
An `AbstractFile` represents a file handle on the operating system level. You cannot do much with it.
+/
-class AbstractFile {
+version(HasFile) class AbstractFile {
private {
NativeFileHandle handle;
}
@@ -2505,7 +3813,7 @@ class AbstractFile {
/++
+/
-class File : AbstractFile {
+version(HasFile) class File : AbstractFile {
/++
Opens a file in synchronous access mode.
@@ -2544,7 +3852,7 @@ class File : AbstractFile {
/++
Only one operation can be pending at any time in the current implementation.
+/
-class AsyncFile : AbstractFile {
+version(HasFile) class AsyncFile : AbstractFile {
/++
Opens a file in asynchronous access mode.
+/
@@ -2568,6 +3876,9 @@ class AsyncFile : AbstractFile {
}
}
+else class AsyncFile {
+ package(arsd) this(NativeFileHandle adoptPreSetup) {}
+}
/++
Reads or writes a file in one call. It might internally yield, but is generally blocking if it returns values. The callback ones depend on the implementation.
@@ -2812,7 +4123,7 @@ struct UserProvidedBuffer(T) {
}
}
- package(arsd) T[] slice() return {
+ package(arsd) T[] slice() {
return buffer[0 .. actualLength];
}
}
@@ -2832,6 +4143,368 @@ enum OnOutOfSpace {
}
+
+/+
+ The GC can be called from any thread, and a lot of cleanup must be done
+ on the gui thread. Since the GC can interrupt any locks - including being
+ triggered inside a critical section - it is vital to avoid deadlocks to get
+ these functions called from the right place.
+
+ If the buffer overflows, things are going to get leaked. I'm kinda ok with that
+ right now.
+
+ The cleanup function is run when the event loop gets around to it, which is just
+ whenever there's something there after it has been woken up for other work. It does
+ NOT wake up the loop itself - can't risk doing that from inside the GC in another thread.
+ (Well actually it might be ok but i don't wanna mess with it right now.)
++/
+package(arsd) struct CleanupQueue {
+ import core.stdc.stdlib;
+
+ void queue(alias func, T...)(T args) {
+ static struct Args {
+ T args;
+ }
+ static struct RealJob {
+ Job j;
+ Args a;
+ }
+ static void call(Job* data) {
+ auto rj = cast(RealJob*) data;
+ func(rj.a.args);
+ }
+
+ RealJob* thing = cast(RealJob*) malloc(RealJob.sizeof);
+ thing.j.call = &call;
+ thing.a.args = args;
+
+ buffer[tail++] = cast(Job*) thing;
+
+ // FIXME: set overflowed
+ }
+
+ void process() {
+ const tail = this.tail;
+
+ while(tail != head) {
+ Job* job = cast(Job*) buffer[head++];
+ job.call(job);
+ free(job);
+ }
+
+ if(overflowed)
+ throw new object.Exception("cleanup overflowed");
+ }
+
+ private:
+
+ ubyte tail; // must ONLY be written by queue
+ ubyte head; // must ONLY be written by process
+ bool overflowed;
+
+ static struct Job {
+ void function(Job*) call;
+ }
+
+ void*[256] buffer;
+}
+package(arsd) __gshared CleanupQueue cleanupQueue;
+
+
+
+
+/++
+ A timer that will trigger your function on a given interval.
+
+
+ You create a timer with an interval and a callback. It will continue
+ to fire on the interval until it is destroyed.
+
+ ---
+ auto timer = new Timer(50, { it happened!; });
+ timer.destroy();
+ ---
+
+ Timers can only be expected to fire when the event loop is running and only
+ once per iteration through the event loop.
+
+ History:
+ Prior to December 9, 2020, a timer pulse set too high with a handler too
+ slow could lock up the event loop. It now guarantees other things will
+ get a chance to run between timer calls, even if that means not keeping up
+ with the requested interval.
+
+ Originally part of arsd.simpledisplay, this code was integrated into
+ arsd.core on May 26, 2024 (committed on June 10).
++/
+version(HasTimer)
+class Timer {
+ // FIXME: absolute time vs relative time
+ // FIXME: real time?
+
+ // FIXME: I might add overloads for ones that take a count of
+ // how many elapsed since last time (on Windows, it will divide
+ // the ticks thing given, on Linux it is just available) and
+ // maybe one that takes an instance of the Timer itself too
+
+
+ /++
+ Creates an initialized, but unarmed timer. You must call other methods later.
+ +/
+ this(bool actuallyInitialize = true) {
+ if(actuallyInitialize)
+ initialize();
+ }
+
+ private void initialize() {
+ version(Windows) {
+ handle = CreateWaitableTimer(null, false, null);
+ if(handle is null)
+ throw new WindowsApiException("CreateWaitableTimer", GetLastError());
+ cbh = new CallbackHelper(&trigger);
+ } else version(Emscripten) {
+ assert(0);
+ } else version(linux) {
+ import core.sys.linux.timerfd;
+
+ fd = timerfd_create(CLOCK_MONOTONIC, 0);
+ if(fd == -1)
+ throw new Exception("timer create failed");
+
+ auto el = getThisThreadEventLoop(EventLoopType.Ui);
+ unregisterToken = el.addCallbackOnFdReadable(fd, new CallbackHelper(&trigger));
+ } else throw new NotYetImplementedException();
+ // FIXME: freebsd 12 has timer_fd and netbsd 10 too
+ }
+
+ /++
+ +/
+ void setPulseCallback(void delegate() onPulse) {
+ assert(onPulse !is null);
+ this.onPulse = onPulse;
+ }
+
+ /++
+ +/
+ void changeTime(int intervalInMilliseconds, bool repeats) {
+ this.intervalInMilliseconds = intervalInMilliseconds;
+ this.repeats = repeats;
+ changeTimeInternal(intervalInMilliseconds, repeats);
+ }
+
+ private void changeTimeInternal(int intervalInMilliseconds, bool repeats) {
+ version(Windows)
+ {
+ LARGE_INTEGER initialTime;
+ initialTime.QuadPart = -intervalInMilliseconds * 10000000L / 1000; // Windows wants hnsecs, we have msecs
+ if(!SetWaitableTimer(handle, &initialTime, repeats ? intervalInMilliseconds : 0, &timerCallback, cast(void*) cbh, false))
+ throw new WindowsApiException("SetWaitableTimer", GetLastError());
+ } else version(Emscripten) {
+ assert(0);
+ } else version(linux) {
+ import core.sys.linux.timerfd;
+
+ itimerspec value = makeItimerspec(intervalInMilliseconds, repeats);
+ if(timerfd_settime(fd, 0, &value, null) == -1) {
+ throw new ErrnoApiException("couldn't change pulse timer", errno);
+ }
+ } else {
+ throw new NotYetImplementedException();
+ }
+ // FIXME: freebsd 12 has timer_fd and netbsd 10 too
+ }
+
+ /++
+ +/
+ void pause() {
+ // FIXME this kinda makes little sense tbh
+ // when it restarts, it won't be on the same rhythm as it was at first...
+ changeTimeInternal(0, false);
+ }
+
+ /++
+ +/
+ void unpause() {
+ changeTimeInternal(this.intervalInMilliseconds, this.repeats);
+ }
+
+ /++
+ +/
+ void cancel() {
+ version(Windows)
+ CancelWaitableTimer(handle);
+ else
+ changeTime(0, false);
+ }
+
+
+ /++
+ Create a timer with a callback when it triggers.
+ +/
+ this(int intervalInMilliseconds, void delegate() onPulse, bool repeats = true) @trusted {
+ assert(onPulse !is null);
+
+ initialize();
+ setPulseCallback(onPulse);
+ changeTime(intervalInMilliseconds, repeats);
+ }
+
+ /++
+ Sets a one-of timer that happens some time after the given timestamp, then destroys itself
+ +/
+ this(SimplifiedUtcTimestamp when, void delegate() onTimeArrived) {
+ import core.stdc.time;
+ auto ts = when.toUnixTime;
+ auto now = time(null);
+ if(ts <= now) {
+ this(false);
+ onTimeArrived();
+ } else {
+ // FIXME: should use the OS facilities to set the actual time on the real time clock
+ auto dis = this;
+ this(cast(int)(ts - now) * 1000, () {
+ onTimeArrived();
+ dis.cancel();
+ dis.dispose();
+ }, false);
+ }
+ }
+
+ version(Windows) {} else {
+ ICoreEventLoop.UnregisterToken unregisterToken;
+ }
+
+ // just cuz I sometimes call it this.
+ alias dispose = destroy;
+
+ /++
+ Stop and destroy the timer object.
+
+ You should not use it again after destroying it.
+ +/
+ void destroy() {
+ version(Windows) {
+ cbh.release();
+ } else {
+ unregisterToken.unregister();
+ }
+
+ version(Windows) {
+ staticDestroy(handle);
+ handle = null;
+ } else version(linux) {
+ staticDestroy(fd);
+ fd = -1;
+ } else throw new NotYetImplementedException();
+ }
+
+ ~this() {
+ version(Windows) {} else
+ cleanupQueue.queue!unregister(unregisterToken);
+ version(Windows) { if(handle)
+ cleanupQueue.queue!staticDestroy(handle);
+ } else version(linux) { if(fd != -1)
+ cleanupQueue.queue!staticDestroy(fd);
+ }
+ }
+
+
+ private:
+
+ version(Windows)
+ static void staticDestroy(HANDLE handle) {
+ if(handle) {
+ // KillTimer(null, handle);
+ CancelWaitableTimer(cast(void*)handle);
+ CloseHandle(handle);
+ }
+ }
+ else version(linux)
+ static void staticDestroy(int fd) @system {
+ if(fd != -1) {
+ import unix = core.sys.posix.unistd;
+
+ unix.close(fd);
+ }
+ }
+
+ version(Windows) {} else
+ static void unregister(arsd.core.ICoreEventLoop.UnregisterToken urt) {
+ urt.unregister();
+ }
+
+
+ void delegate() onPulse;
+ int intervalInMilliseconds;
+ bool repeats;
+
+ int lastEventLoopRoundTriggered;
+
+ version(linux) {
+ static auto makeItimerspec(int intervalInMilliseconds, bool repeats) {
+ import core.sys.linux.timerfd;
+
+ itimerspec value;
+ value.it_value.tv_sec = cast(int) (intervalInMilliseconds / 1000);
+ value.it_value.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
+
+ if(repeats) {
+ value.it_interval.tv_sec = cast(int) (intervalInMilliseconds / 1000);
+ value.it_interval.tv_nsec = (intervalInMilliseconds % 1000) * 1000_000;
+ }
+
+ return value;
+ }
+ }
+
+ void trigger() {
+ version(linux) {
+ import unix = core.sys.posix.unistd;
+ long val;
+ unix.read(fd, &val, val.sizeof); // gotta clear the pipe
+ } else version(Windows) {
+ if(this.lastEventLoopRoundTriggered == eventLoopRound)
+ return; // never try to actually run faster than the event loop
+ lastEventLoopRoundTriggered = eventLoopRound;
+ } else throw new NotYetImplementedException();
+
+ if(onPulse)
+ onPulse();
+ }
+
+ version(Windows)
+ extern(Windows)
+ //static void timerCallback(HWND, UINT, UINT_PTR timer, DWORD dwTime) nothrow {
+ static void timerCallback(void* timer, DWORD lowTime, DWORD hiTime) nothrow {
+ auto cbh = cast(CallbackHelper) timer;
+ try
+ cbh.call();
+ catch(Throwable e) { sdpy_abort(e); assert(0); }
+ }
+
+ version(Windows) {
+ HANDLE handle;
+ CallbackHelper cbh;
+ } else version(linux) {
+ int fd = -1;
+ } else version(OSXCocoa) {
+ } else static assert(0, "timer not supported");
+}
+
+version(Windows)
+ private void sdpy_abort(Throwable e) nothrow {
+ try
+ MessageBoxA(null, (e.toString() ~ "\0").ptr, "Exception caught in WndProc", 0);
+ catch(Exception e)
+ MessageBoxA(null, "Exception.toString threw too!", "Exception caught in WndProc", 0);
+ ExitProcess(1);
+ }
+
+
+private int eventLoopRound = -1; // so things that assume 0 still work eg lastEventLoopRoundTriggered
+
+
+
/++
For functions that give you an unknown address, you can use this to hold it.
@@ -2846,7 +4519,7 @@ enum OnOutOfSpace {
interface lookup for bind (stream or dgram)
+/
-struct SocketAddress {
+version(HasSocket) struct SocketAddress {
import core.sys.posix.netdb;
/++
@@ -3089,7 +4762,7 @@ private version(Windows) {
}
}
-class AsyncSocket : AsyncFile {
+version(HasFile) class AsyncSocket : AsyncFile {
// otherwise: accept, bind, connect, shutdown, close.
static auto lastError() {
@@ -3288,7 +4961,7 @@ class AsyncSocket : AsyncFile {
NOT IMPLEMENTED / NOT STABLE
+/
-class AsyncConnectRequest : AsyncOperationRequest {
+version(HasSocket) class AsyncConnectRequest : AsyncOperationRequest {
// FIXME: i should take a list of addresses and take the first one that succeeds, so a getaddrinfo can be sent straight in.
this(AsyncSocket socket, SocketAddress address, ubyte[] dataToWrite) {
@@ -3301,7 +4974,7 @@ class AsyncConnectRequest : AsyncOperationRequest {
}
/++
+/
-class AsyncConnectResponse : AsyncOperationResponse {
+version(HasSocket) class AsyncConnectResponse : AsyncOperationResponse {
const SystemErrorCode errorCode;
this(SystemErrorCode errorCode) {
@@ -3321,7 +4994,7 @@ class AsyncConnectResponse : AsyncOperationResponse {
NOT IMPLEMENTED / NOT STABLE
+/
-class AsyncAcceptRequest : AsyncOperationRequest {
+version(HasSocket) class AsyncAcceptRequest : AsyncOperationRequest {
AsyncSocket socket;
override void start() {}
@@ -3378,7 +5051,7 @@ class AsyncAcceptRequest : AsyncOperationRequest {
}
/++
+/
-class AsyncAcceptResponse : AsyncOperationResponse {
+version(HasSocket) class AsyncAcceptResponse : AsyncOperationResponse {
AsyncSocket newSocket;
const SystemErrorCode errorCode;
@@ -3398,7 +5071,7 @@ class AsyncAcceptResponse : AsyncOperationResponse {
/++
+/
-class AsyncReceiveRequest : AsyncOperationRequest {
+version(HasSocket) class AsyncReceiveRequest : AsyncOperationRequest {
struct LowLevelOperation {
AsyncSocket file;
ubyte[] buffer;
@@ -3446,7 +5119,7 @@ class AsyncReceiveRequest : AsyncOperationRequest {
}
/++
+/
-class AsyncReceiveResponse : AsyncOperationResponse {
+version(HasSocket) class AsyncReceiveResponse : AsyncOperationResponse {
const ubyte[] bufferWritten;
const SystemErrorCode errorCode;
@@ -3462,7 +5135,7 @@ class AsyncReceiveResponse : AsyncOperationResponse {
/++
+/
-class AsyncSendRequest : AsyncOperationRequest {
+version(HasSocket) class AsyncSendRequest : AsyncOperationRequest {
struct LowLevelOperation {
AsyncSocket file;
const(ubyte)[] buffer;
@@ -3508,7 +5181,7 @@ class AsyncSendRequest : AsyncOperationRequest {
/++
+/
-class AsyncSendResponse : AsyncOperationResponse {
+version(HasSocket) class AsyncSendResponse : AsyncOperationResponse {
const ubyte[] bufferWritten;
const SystemErrorCode errorCode;
@@ -3530,7 +5203,7 @@ class AsyncSendResponse : AsyncOperationResponse {
NOT IMPLEMENTED / NOT STABLE
+/
-class StreamServer {
+version(HasSocket) class StreamServer {
AsyncSocket[] sockets;
this(SocketAddress[] listenTo, int backlog = 8) {
@@ -3577,7 +5250,7 @@ unittest {
NOT IMPLEMENTED / NOT STABLE
+/
-class DatagramListener {
+version(HasSocket) class DatagramListener {
// whenever a udp message arrives, it calls your callback
// can be on a specific thread or on any thread
@@ -3588,7 +5261,7 @@ class DatagramListener {
/++
Just in case I decide to change the implementation some day.
+/
-alias AsyncAnonymousPipe = AsyncFile;
+version(HasFile) alias AsyncAnonymousPipe = AsyncFile;
// AsyncAnonymousPipe connectNamedPipe(AsyncAnonymousPipe preallocated, string name)
@@ -3605,7 +5278,7 @@ alias AsyncAnonymousPipe = AsyncFile;
History:
previously in minigui as a private function. Moved to arsd.core on April 3, 2023
+/
-GetFilesResult getFiles(string directory, scope void delegate(string name, bool isDirectory) dg) {
+version(HasFile) GetFilesResult getFiles(string directory, scope void delegate(string name, bool isDirectory) dg) {
// FIXME: my buffers here aren't great lol
SavedArgument[1] argsForException() {
@@ -3630,6 +5303,14 @@ GetFilesResult getFiles(string directory, scope void delegate(string name, bool
string name = makeUtf8StringFromWindowsString(data.cFileName[0 .. findIndexOfZero(data.cFileName[])]);
+ /+
+ FILETIME ftLastWriteTime;
+ DWORD nFileSizeHigh;
+ DWORD nFileSizeLow;
+
+ but these not available on linux w/o statting each file!
+ +/
+
dg(name, (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ? true : false);
auto ret = FindNextFileW(handle, &data);
@@ -3732,6 +5413,10 @@ package(arsd) int indexOf(scope const(char)[] haystack, scope const(char)[] need
return -1;
}
+package(arsd) int indexOf(scope const(ubyte)[] haystack, scope const(char)[] needle) {
+ return indexOf(cast(const(char)[]) haystack, needle);
+}
+
unittest {
assert("foo".indexOf("f") == 0);
assert("foo".indexOf("o") == 1);
@@ -3787,7 +5472,7 @@ class DirectoryWatcher {
ubyte[] buffer;
extern(Windows)
- static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) {
+ static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) @system {
typeof(this) rr = cast(typeof(this)) (cast(void*) lpOverlapped - typeof(this).overlapped.offsetof);
// dwErrorCode
@@ -4076,14 +5761,16 @@ mixin template OverlappedIoRequest(Response, LowLevelOperation) {
OVERLAPPED overlapped;
extern(Windows)
- static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) {
+ static void overlappedCompletionRoutine(DWORD dwErrorCode, DWORD dwNumberOfBytesTransferred, LPOVERLAPPED lpOverlapped) @system {
typeof(this) rr = cast(typeof(this)) (cast(void*) lpOverlapped - typeof(this).overlapped.offsetof);
rr.response = typeof(rr.response)(SystemErrorCode(dwErrorCode), rr.llo.buffer[0 .. dwNumberOfBytesTransferred]);
rr.state_ = State.complete;
- // FIXME: on complete?
+ if(rr.oncomplete)
+ rr.oncomplete(rr);
+ // FIXME: on complete?
// this will queue our CallbackHelper and that should be run at the end of the event loop after it is woken up by the APC run
}
}
@@ -4111,6 +5798,9 @@ mixin template OverlappedIoRequest(Response, LowLevelOperation) {
else
response = typeof(response)(SystemErrorCode(0), llo.buffer[0 .. cast(size_t) ret]);
state_ = State.complete;
+
+ if(oncomplete)
+ oncomplete(this);
}
}
}
@@ -4131,6 +5821,7 @@ mixin template OverlappedIoRequest(Response, LowLevelOperation) {
version(Windows) {
if(llo(&overlapped, &overlappedCompletionRoutine)) {
// all good, though GetLastError() might have some informative info
+ //writeln(GetLastError());
} else {
// operation failed, the operation is always ReadFileEx or WriteFileEx so it won't give the io pending thing here
// should i issue error async? idk
@@ -4146,7 +5837,10 @@ mixin template OverlappedIoRequest(Response, LowLevelOperation) {
auto errno = errno;
if(ret == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) { // unable to complete right now, register and try when it is ready
- eventRegistration = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(this.llo.file.handle, this.getCb);
+ if(eventRegistration is typeof(eventRegistration).init)
+ eventRegistration = getThisThreadEventLoop().addCallbackOnFdReadableOneShot(this.llo.file.handle, this.getCb);
+ else
+ eventRegistration.rearm();
} else {
// i could set errors sync or async and since it couldn't even start, i think a sync exception is the right way
if(ret == -1)
@@ -4156,7 +5850,6 @@ mixin template OverlappedIoRequest(Response, LowLevelOperation) {
}
}
-
override void cancel() {
if(state_ == State.complete)
return; // it has already finished, just leave it alone, no point discarding what is already done
@@ -4206,12 +5899,26 @@ mixin template OverlappedIoRequest(Response, LowLevelOperation) {
return response;
}
+
+ /++
+ Repeats the operation, restarting the request.
+
+ This must only be called when the operation has already completed.
+ +/
+ void repeat() {
+ if(state_ != State.complete)
+ throw new Exception("wrong use, cannot repeat if not complete");
+ state_ = State.unused;
+ start();
+ }
+
+ void delegate(typeof(this) t) oncomplete;
}
/++
You can write to a file asynchronously by creating one of these.
+/
-final class AsyncWriteRequest : AsyncOperationRequest {
+version(HasSocket) final class AsyncWriteRequest : AsyncOperationRequest {
struct LowLevelOperation {
AsyncFile file;
ubyte[] buffer;
@@ -4262,10 +5969,17 @@ class AsyncWriteResponse : AsyncOperationResponse {
}
}
+// FIXME: on Windows, you may want two operations outstanding at once
+// so there's no delay between sequential ops. this system currently makes that
+// impossible since epoll won't let you register twice...
+
+// FIXME: if an op completes synchronously, and oncomplete calls repeat
+// you can get infinite recursion into the stack...
+
/++
+/
-final class AsyncReadRequest : AsyncOperationRequest {
+version(HasSocket) final class AsyncReadRequest : AsyncOperationRequest {
struct LowLevelOperation {
AsyncFile file;
ubyte[] buffer;
@@ -4335,13 +6049,13 @@ class AsyncReadResponse : AsyncOperationResponse {
runHelperFunction() - whomever it reports to is the parent
+/
-class ScheduableTask : Fiber {
+version(HasThread) class SchedulableTask : Fiber {
private void delegate() dg;
// linked list stuff
- private static ScheduableTask taskRoot;
- private ScheduableTask previous;
- private ScheduableTask next;
+ private static SchedulableTask taskRoot;
+ private SchedulableTask previous;
+ private SchedulableTask next;
// need the controlling thread to know how to wake it up if it receives a message
private Thread controllingThread;
@@ -4426,23 +6140,23 @@ void delegate(Throwable t) taskUncaughtException;
History:
Added August 11, 2023 (dub v11.1)
+/
-SchedulableTaskController inSchedulableTask() {
+version(HasThread) SchedulableTaskController inSchedulableTask() {
import core.thread.fiber;
if(auto fiber = Fiber.getThis) {
- return SchedulableTaskController(cast(ScheduableTask) fiber);
+ return SchedulableTaskController(cast(SchedulableTask) fiber);
}
return SchedulableTaskController(null);
}
/// ditto
-struct SchedulableTaskController {
- private this(ScheduableTask fiber) {
+version(HasThread) struct SchedulableTaskController {
+ private this(SchedulableTask fiber) {
this.fiber = fiber;
}
- private ScheduableTask fiber;
+ private SchedulableTask fiber;
/++
@@ -4488,7 +6202,7 @@ class TaskCancelledException : object.Exception {
}
}
-private class CoreWorkerThread : Thread {
+version(HasThread) private class CoreWorkerThread : Thread {
this(EventLoopType type) {
this.type = type;
@@ -4587,7 +6301,7 @@ private int numberOfCpus() {
Its destructor runs the event loop then waits to for the workers to finish to clean them up.
+/
// FIXME: single instance?
-struct ArsdCoreApplication {
+version(HasThread) struct ArsdCoreApplication {
private ICoreEventLoop impl;
/++
@@ -4641,12 +6355,39 @@ struct ArsdCoreApplication {
private class CoreEventLoopImplementation : ICoreEventLoop {
+ version(EmptyEventLoop) RunOnceResult runOnce(Duration timeout = Duration.max) { return RunOnceResult(RunOnceResult.Possibilities.LocalExit); }
+ version(EmptyCoreEvent)
+ {
+ UnregisterToken addCallbackOnFdReadable(int fd, CallbackHelper cb){return typeof(return).init;}
+ RearmToken addCallbackOnFdReadableOneShot(int fd, CallbackHelper cb){return typeof(return).init;}
+ RearmToken addCallbackOnFdWritableOneShot(int fd, CallbackHelper cb){return typeof(return).init;}
+ private void rearmFd(RearmToken token) {}
+ }
+
+
+ private {
+ static struct LoopIterationDelegate {
+ void delegate() dg;
+ uint flags;
+ }
+ LoopIterationDelegate[] loopIterationDelegates;
+
+ void runLoopIterationDelegates() {
+ foreach(lid; loopIterationDelegates)
+ lid.dg();
+ }
+ }
+
+ void addDelegateOnLoopIteration(void delegate() dg, uint timingFlags) {
+ loopIterationDelegates ~= LoopIterationDelegate(dg, timingFlags);
+ }
version(Arsd_core_kqueue) {
// this thread apc dispatches go as a custom event to the queue
// the other queues go through one byte at a time pipes (barf). freebsd 13 and newest nbsd have eventfd too tho so maybe i can use them but the other kqueue systems don't.
- void runOnce() {
+ RunOnceResult runOnce(Duration timeout = Duration.max) {
+ scope(exit) eventLoopRound++;
kevent_t[16] ev;
//timespec tout = timespec(1, 0);
auto nev = kevent(kqueuefd, null, 0, ev.ptr, ev.length, null/*&tout*/);
@@ -4668,6 +6409,10 @@ private class CoreEventLoopImplementation : ICoreEventLoop {
}
}
}
+
+ runLoopIterationDelegates();
+
+ return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
}
// FIXME: idk how to make one event that multiple kqueues can listen to w/o being shared
@@ -4786,12 +6531,32 @@ private class CoreEventLoopImplementation : ICoreEventLoop {
__gshared HANDLE iocpWorkers;
HANDLE[] handles;
+ CallbackHelper[] handlesCbs;
+
+ void unregisterHandle(HANDLE handle, CallbackHelper cb) {
+ foreach(idx, h; handles)
+ if(h is handle && handlesCbs[idx] is cb) {
+ handles[idx] = handles[$-1];
+ handles = handles[0 .. $-1].assumeSafeAppend;
+
+ handlesCbs[idx] = handlesCbs[$-1];
+ handlesCbs = handlesCbs[0 .. $-1].assumeSafeAppend;
+ }
+ }
+
+ UnregisterToken addCallbackOnHandleReady(HANDLE handle, CallbackHelper cb) {
+ handles ~= handle;
+ handlesCbs ~= cb;
+
+ return UnregisterToken(this, handle, cb);
+ }
// i think to terminate i just have to post the message at least once for every thread i know about, maybe a few more times for threads i don't know about.
bool isWorker; // if it is a worker we wait on the iocp, if not we wait on msg
- void runOnce() {
+ RunOnceResult runOnce(Duration timeout = Duration.max) {
+ scope(exit) eventLoopRound++;
if(isWorker) {
// this function is only supported on Windows Vista and up, so using this
// means dropping support for XP.
@@ -4809,7 +6574,8 @@ private class CoreEventLoopImplementation : ICoreEventLoop {
enum WAIT_OBJECT_0 = 0;
if(waitResult >= WAIT_OBJECT_0 && waitResult < handles.length + WAIT_OBJECT_0) {
auto h = handles[waitResult - WAIT_OBJECT_0];
- // FIXME: run the handle ready callback
+ auto cb = handlesCbs[waitResult - WAIT_OBJECT_0];
+ cb.call();
} else if(waitResult == handles.length + WAIT_OBJECT_0) {
// message ready
int count;
@@ -4826,9 +6592,7 @@ private class CoreEventLoopImplementation : ICoreEventLoop {
break; // take the opportunity to catch up on other events
if(ret == 0) { // WM_QUIT
- // EventLoop.quitApplication();
- assert(0); // FIXME
- //break;
+ exitApplication();
}
}
} else if(waitResult == 0x000000C0L /* WAIT_IO_COMPLETION */) {
@@ -4842,6 +6606,10 @@ private class CoreEventLoopImplementation : ICoreEventLoop {
// idk....
}
}
+
+ runLoopIterationDelegates();
+
+ return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
}
}
@@ -5123,13 +6891,14 @@ private class CoreEventLoopImplementation : ICoreEventLoop {
// the any thread poll is just registered in the this thread poll w/ exclusive. nobody actaully epoll_waits
// on the global one directly.
- void runOnce() {
+ RunOnceResult runOnce(Duration timeout = Duration.max) {
+ scope(exit) eventLoopRound++;
epoll_event[16] events;
auto ret = epoll_wait(epollfd, events.ptr, cast(int) events.length, -1); // FIXME: timeout
if(ret == -1) {
import core.stdc.errno;
if(errno == EINTR) {
- return;
+ return RunOnceResult(RunOnceResult.Possibilities.Interrupted);
}
throw new ErrnoApiException("epoll_wait", errno);
} else if(ret == 0) {
@@ -5146,6 +6915,10 @@ private class CoreEventLoopImplementation : ICoreEventLoop {
cbObject.call();
}
}
+
+ runLoopIterationDelegates();
+
+ return RunOnceResult(RunOnceResult.Possibilities.CarryOn);
}
// building blocks for low-level integration with the loop
@@ -5570,7 +7343,7 @@ class WritableStream {
It reads binary data.
+/
-class ReadableStream {
+version(HasThread) class ReadableStream {
this() {
@@ -5657,17 +7430,26 @@ class ReadableStream {
/// ditto
final T get(T : E[], E)(scope bool delegate(E e) isTerminatingSentinel, ByteOrder elementByteOrder = ByteOrder.irrelevant, string file = __FILE__, size_t line = __LINE__) {
- if(byteOrder == ByteOrder.irrelevant && E.sizeof > 1)
+ if(elementByteOrder == ByteOrder.irrelevant && E.sizeof > 1)
throw new InvalidArgumentsException("elementByteOrder", "byte order must be specified for type " ~ E.stringof ~ " because it is bigger than one byte", "ReadableStream.get", file, line);
- assert(0, "Not implemented");
+ T ret;
+
+ do {
+ try
+ ret ~= get!E(elementByteOrder);
+ catch(ArsdException!"is already closed" ae)
+ return ret;
+ } while(!isTerminatingSentinel(ret[$-1]));
+
+ return ret[0 .. $-1]; // cut off the terminating sentinel
}
/++
+/
bool isClosed() {
- return isClosed_;
+ return isClosed_ && currentBuffer.length == 0 && leftoverBuffer.length == 0;
}
// Control side of things
@@ -5698,6 +7480,9 @@ class ReadableStream {
You basically have to use this thing from a task
+/
protected void waitForAdditionalData() {
+ if(isClosed_)
+ throw ArsdException!("is already closed")();
+
Fiber task = Fiber.getThis;
assert(task !is null);
@@ -5795,9 +7580,6 @@ unittest {
---
Please note that this does not currently and I have no plans as of this writing to add support for any kind of direct file descriptor passing. It always pipes them back to the parent for processing. If you don't want this, call the lower level functions yourself; the reason this class is here is to aid integration in the arsd.core event loop. Of course, I might change my mind on this.
-
- Bugs:
- Not implemented at all on Windows yet.
+/
class ExternalProcess /*: AsyncOperationRequest*/ {
@@ -5814,8 +7596,7 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
synchronized(typeid(ExternalProcess)) {
if(pid in activeChildren) {
auto ac = activeChildren[pid];
- ac.completed = true;
- ac.status = status;
+ ac.markComplete(status);
activeChildren.remove(pid);
}
}
@@ -5827,13 +7608,18 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
/++
This is the native version for Windows.
+/
- this(string program, string commandLine) {
+ version(Windows)
+ this(FilePath program, string commandLine) {
version(Posix) {
assert(0, "not implemented command line to posix args yet");
+ } else version(Windows) {
+ this.program = program;
+ this.commandLine = commandLine;
}
else throw new NotYetImplementedException();
}
+ /+
this(string commandLine) {
version(Posix) {
assert(0, "not implemented command line to posix args yet");
@@ -5848,10 +7634,12 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
}
else throw new NotYetImplementedException();
}
+ +/
/++
This is the native version for Posix.
+/
+ version(Posix)
this(FilePath program, string[] args) {
version(Posix) {
this.program = program;
@@ -5860,12 +7648,13 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
else throw new NotYetImplementedException();
}
- // you can modify these before calling start
- int stdoutBufferSize = 32 * 1024;
- int stderrBufferSize = 8 * 1024;
+ /++
+ +/
void start() {
version(Posix) {
+ getThisThreadEventLoop(); // ensure it is initialized
+
int ret;
int[2] stdinPipes;
@@ -5878,7 +7667,7 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
close(stdinPipes[1]);
}
- stdinFd = stdinPipes[1];
+ auto stdinFd = stdinPipes[1];
int[2] stdoutPipes;
ret = pipe(stdoutPipes);
@@ -5890,7 +7679,7 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
close(stdoutPipes[1]);
}
- stdoutFd = stdoutPipes[0];
+ auto stdoutFd = stdoutPipes[0];
int[2] stderrPipes;
ret = pipe(stderrPipes);
@@ -5902,7 +7691,7 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
close(stderrPipes[1]);
}
- stderrFd = stderrPipes[0];
+ auto stderrFd = stderrPipes[0];
int[2] errorReportPipes;
@@ -6026,107 +7815,165 @@ class ExternalProcess /*: AsyncOperationRequest*/ {
ErrnoEnforce!close(errorReportPipes[0]);
- // and now register the ones we need to read with the event loop so it can call the callbacks
- // also need to listen to SIGCHLD to queue up the terminated callback. FIXME
+ makeNonBlocking(stdinFd);
+ makeNonBlocking(stdoutFd);
+ makeNonBlocking(stderrFd);
- stdoutUnregisterToken = getThisThreadEventLoop().addCallbackOnFdReadable(stdoutFd, new CallbackHelper(&stdoutReadable));
- stderrUnregisterToken = getThisThreadEventLoop().addCallbackOnFdReadable(stderrFd, new CallbackHelper(&stderrReadable));
+ _stdin = new AsyncFile(stdinFd);
+ _stdout = new AsyncFile(stdoutFd);
+ _stderr = new AsyncFile(stderrFd);
}
+ } else version(Windows) {
+ WCharzBuffer program = this.program.path;
+ WCharzBuffer cmdLine = this.commandLine;
+
+ PROCESS_INFORMATION pi;
+ STARTUPINFOW startupInfo;
+
+ SECURITY_ATTRIBUTES saAttr;
+ saAttr.nLength = SECURITY_ATTRIBUTES.sizeof;
+ saAttr.bInheritHandle = true;
+ saAttr.lpSecurityDescriptor = null;
+
+ HANDLE inreadPipe;
+ HANDLE inwritePipe;
+ if(MyCreatePipeEx(&inreadPipe, &inwritePipe, &saAttr, 0, 0, FILE_FLAG_OVERLAPPED) == 0)
+ throw new WindowsApiException("CreatePipe", GetLastError());
+ if(!SetHandleInformation(inwritePipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
+ throw new WindowsApiException("SetHandleInformation", GetLastError());
+
+ HANDLE outreadPipe;
+ HANDLE outwritePipe;
+ if(MyCreatePipeEx(&outreadPipe, &outwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0)
+ throw new WindowsApiException("CreatePipe", GetLastError());
+ if(!SetHandleInformation(outreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
+ throw new WindowsApiException("SetHandleInformation", GetLastError());
+
+ HANDLE errreadPipe;
+ HANDLE errwritePipe;
+ if(MyCreatePipeEx(&errreadPipe, &errwritePipe, &saAttr, 0, FILE_FLAG_OVERLAPPED, 0) == 0)
+ throw new WindowsApiException("CreatePipe", GetLastError());
+ if(!SetHandleInformation(errreadPipe, 1/*HANDLE_FLAG_INHERIT*/, 0))
+ throw new WindowsApiException("SetHandleInformation", GetLastError());
+
+ startupInfo.cb = startupInfo.sizeof;
+ startupInfo.dwFlags = STARTF_USESTDHANDLES;
+ startupInfo.hStdInput = inreadPipe;
+ startupInfo.hStdOutput = outwritePipe;
+ startupInfo.hStdError = errwritePipe;
+
+ auto result = CreateProcessW(
+ program.ptr,
+ cmdLine.ptr,
+ null, // process attributes
+ null, // thread attributes
+ true, // inherit handles; necessary for the std in/out/err ones to work
+ 0, // dwCreationFlags FIXME might be useful to change
+ null, // environment, might be worth changing
+ null, // current directory
+ &startupInfo,
+ &pi
+ );
+
+ if(!result)
+ throw new WindowsApiException("CreateProcessW", GetLastError());
+
+ _stdin = new AsyncFile(inwritePipe);
+ _stdout = new AsyncFile(outreadPipe);
+ _stderr = new AsyncFile(errreadPipe);
+
+ Win32Enforce!CloseHandle(inreadPipe);
+ Win32Enforce!CloseHandle(outwritePipe);
+ Win32Enforce!CloseHandle(errwritePipe);
+
+ Win32Enforce!CloseHandle(pi.hThread);
+
+ handle = pi.hProcess;
+
+ procRegistration = getThisThreadEventLoop.addCallbackOnHandleReady(handle, new CallbackHelper(&almostComplete));
}
}
- private version(Posix) {
- import core.sys.posix.unistd;
- import core.sys.posix.fcntl;
+ version(Windows) {
+ private HANDLE handle;
+ private FilePath program;
+ private string commandLine;
+ private ICoreEventLoop.UnregisterToken procRegistration;
- int stdinFd = -1;
- int stdoutFd = -1;
- int stderrFd = -1;
+ private final void almostComplete() {
+ // GetProcessTimes lol
+ Win32Enforce!GetExitCodeProcess(handle, cast(uint*) &_status);
- ICoreEventLoop.UnregisterToken stdoutUnregisterToken;
- ICoreEventLoop.UnregisterToken stderrUnregisterToken;
+ markComplete(_status);
- pid_t pid = -1;
+ procRegistration.unregister();
+ CloseHandle(handle);
+ this.completed = true;
+ }
+ } else version(Posix) {
+ import core.sys.posix.unistd;
+ import core.sys.posix.fcntl;
+
+ private pid_t pid = -1;
public void delegate() beforeExec;
- FilePath program;
- string[] args;
+ private FilePath program;
+ private string[] args;
+ }
- void stdoutReadable() {
- if(stdoutReadBuffer is null)
- stdoutReadBuffer = new ubyte[](stdoutBufferSize);
- auto ret = read(stdoutFd, stdoutReadBuffer.ptr, stdoutReadBuffer.length);
- if(ret == -1)
- throw new ErrnoApiException("read", errno);
- if(onStdoutAvailable) {
- onStdoutAvailable(stdoutReadBuffer[0 .. ret]);
- }
+ private final void markComplete(int status) {
+ completed = true;
+ _status = status;
- if(ret == 0) {
- stdoutUnregisterToken.unregister();
+ if(oncomplete)
+ oncomplete(this);
+ }
- close(stdoutFd);
- stdoutFd = -1;
- }
- }
- void stderrReadable() {
- if(stderrReadBuffer is null)
- stderrReadBuffer = new ubyte[](stderrBufferSize);
- auto ret = read(stderrFd, stderrReadBuffer.ptr, stderrReadBuffer.length);
- if(ret == -1)
- throw new ErrnoApiException("read", errno);
- if(onStderrAvailable) {
- onStderrAvailable(stderrReadBuffer[0 .. ret]);
- }
+ private AsyncFile _stdin;
+ private AsyncFile _stdout;
+ private AsyncFile _stderr;
- if(ret == 0) {
- stderrUnregisterToken.unregister();
+ /++
- close(stderrFd);
- stderrFd = -1;
- }
- }
+ +/
+ AsyncFile stdin() {
+ return _stdin;
+ }
+ /// ditto
+ AsyncFile stdout() {
+ return _stdout;
+ }
+ /// ditto
+ AsyncFile stderr() {
+ return _stderr;
}
- private ubyte[] stdoutReadBuffer;
- private ubyte[] stderrReadBuffer;
-
+ /++
+ +/
void waitForCompletion() {
getThisThreadEventLoop().run(&this.isComplete);
}
+ /++
+ +/
bool isComplete() {
return completed;
}
- bool completed;
- int status = int.min;
+ private bool completed;
+ private int _status = int.min;
/++
- If blocking, it will block the current task until the write succeeds.
-
- Write `null` as data to close the pipe. Once the pipe is closed, you must not try to write to it again.
+/
- void writeToStdin(in void[] data) {
- version(Posix) {
- if(data is null) {
- close(stdinFd);
- stdinFd = -1;
- } else {
- // FIXME: check the return value again and queue async writes
- auto ret = write(stdinFd, data.ptr, data.length);
- if(ret == -1)
- throw new ErrnoApiException("write", errno);
- }
- }
-
+ int status() {
+ return _status;
}
- void delegate(ubyte[] got) onStdoutAvailable;
- void delegate(ubyte[] got) onStderrAvailable;
- void delegate(int code) onTermination;
+ // void delegate(int code) onTermination;
+
+ void delegate(ExternalProcess) oncomplete;
// pty?
}
@@ -6198,6 +8045,596 @@ unittest {
}
/+
+ ================
+ LOGGER FRAMEWORK
+ ================
++/
+/++
+ DO NOT USE THIS YET IT IS NOT FUNCTIONAL NOR STABLE
+
+
+ The arsd.core logger works differently than many in that it works as a ring buffer of objects that are consumed (or missed; buffer overruns are possible) by a different thread instead of strings written to some file.
+
+ A library (or an application) defines a log source. They write to this source.
+
+ Applications then define log listeners, zero or more, which reads from various sources and does something with them.
+
+ Log calls, in this sense, are quite similar to asynchronous events that can be subscribed to by event handlers. The difference is events are generally not dropped - they might coalesce but are usually not just plain dropped in a buffer overrun - whereas logs can be. If the log consumer can't keep up, the details are just lost. The log producer will not wait for the consumer to catch up.
+
+
+ An application can also set a default subscriber which applies to all log objects throughout.
+
+ All log message objects must be capable of being converted to strings and to json.
+
+ Ad-hoc messages can be done with interpolated sequences.
+
+ Messages automatically get a timestamp. They can also have file/line and maybe even a call stack.
+
+ Examples:
+ ---
+ auto logger = new shared LoggerOf!GenericEmbeddableInterpolatedSequence;
+
+ mylogger.info(i"$this heartbeat");
+ ---
+
+ History:
+ Added May 27, 2024
+
+ Not actually implemented until February 6, 2025, when it changed from mixin template to class.
++/
+class LoggerOf(T, size_t bufferSize = 16) {
+ private LoggedMessage!T[bufferSize] ring;
+ private ulong writeBufferPosition;
+
+ import core.sync.mutex;
+ import core.sync.condition;
+
+ private Mutex mutex;
+ private Condition condition;
+ private bool active;
+ private int listenerCount;
+
+ this() shared {
+ mutex = new shared Mutex(cast(LoggerOf) this);
+ condition = new shared Condition(mutex);
+ active = true;
+ }
+
+ /++
+ Closes the log channel and waits for all listeners to finish pending work before returning.
+
+ Once the logger is closed, it cannot be used again.
+
+ You should close any logger you attached listeners to before returning from `main()`.
+ +/
+ void close() shared {
+ synchronized(this) {
+ active = false;
+ condition.notifyAll();
+
+ while(listenerCount > 0) {
+ condition.wait();
+ }
+ }
+ }
+
+ /++
+
+ Examples:
+
+ ---
+ // to write all messages to the console
+ logger.addListener((message, missedMessageCount) {
+ writeln(message);
+ });
+ ---
+
+ ---
+ // to only write warnings and errors
+ logger.addListener((message, missedMessageCount) {
+ if(message.level >= LogLevel.warn)
+ writeln(message);
+ });
+ ---
+
+ ---
+ // to ignore messages from arsd.core
+ logger.addListener((message, missedMessageCount) {
+ if(message.sourceLocation.moduleName != "arsd.core")
+ writeln(message);
+ });
+ ---
+ +/
+ LogListenerController addListener(void delegate(LoggedMessage!T message, int missedMessages) dg) shared {
+ static class Listener : Thread, LogListenerController {
+ shared LoggerOf logger;
+ ulong readBufferPosition;
+ void delegate(LoggedMessage!T, int) dg;
+
+ bool connected;
+
+ import core.sync.event;
+ Event event;
+
+ this(shared LoggerOf logger, void delegate(LoggedMessage!T msg, int) dg) {
+ this.dg = dg;
+ this.logger = logger;
+ this.connected = true;
+ this.isDaemon = true;
+
+ auto us = cast(LoggerOf) logger;
+ synchronized(logger)
+ us.listenerCount++;
+
+ event.initialize(true, false);
+ super(&run);
+ }
+
+ void disconnect() {
+ this.connected = false;
+ }
+
+ void run() {
+ auto us = cast(LoggerOf) logger;
+ /+
+ // can't do this due to https://github.com/ldc-developers/ldc/issues/4837
+ // so doing the try/catch below and putting this under it
+ scope(exit) {
+ synchronized(logger) {
+ us.listenerCount--;
+ logger.condition.notifyAll();
+ }
+ // mark us as complete for other listeners waiting as well
+ event.set();
+ }
+ +/
+
+ try {
+
+ LoggedMessage!T[bufferSize] buffer;
+ do {
+ int missedMessages = 0;
+ long n;
+ synchronized(logger) {
+ while(logger.active && connected && logger.writeBufferPosition < readBufferPosition) {
+ logger.condition.wait();
+ }
+
+ n = us.writeBufferPosition - readBufferPosition;
+ if(n > bufferSize) {
+ // we missed something...
+ missedMessages = cast(int) (n - bufferSize);
+ readBufferPosition = us.writeBufferPosition - bufferSize;
+ n = bufferSize;
+ }
+ auto startPos = readBufferPosition % bufferSize;
+ auto endPos = us.writeBufferPosition % bufferSize;
+ if(endPos > startPos) {
+ buffer[0 .. n] = us.ring[startPos .. endPos];
+ } else {
+ auto ourSplit = us.ring.length - startPos;
+ buffer[0 .. ourSplit] = us.ring[startPos .. $];
+ buffer[ourSplit .. ourSplit + endPos] = us.ring[0 .. endPos];
+ }
+ readBufferPosition = us.writeBufferPosition;
+ }
+ foreach(item; buffer[0 .. n]) {
+ if(!connected)
+ break;
+ dg(item, missedMessages);
+ missedMessages = 0;
+ }
+ } while(logger.active && connected);
+
+ } catch(Throwable t) {
+ // i guess i could try to log the exception for other listeners to pick up...
+
+ }
+
+ synchronized(logger) {
+ us.listenerCount--;
+ logger.condition.notifyAll();
+ }
+ // mark us as complete for other listeners waiting as well
+ event.set();
+
+ }
+
+ void waitForCompletion() {
+ event.wait();
+ }
+ }
+
+ auto listener = new Listener(this, dg);
+ listener.start();
+
+ return listener;
+ }
+
+ void log(LoggedMessage!T message) shared {
+ synchronized(this) {
+ auto unshared = cast() this;
+ unshared.ring[writeBufferPosition % bufferSize] = message;
+ unshared.writeBufferPosition += 1;
+
+ // import std.stdio; std.stdio.writeln(message);
+ condition.notifyAll();
+ }
+ }
+
+ /// ditto
+ void log(LogLevel level, T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ import core.stdc.time;
+ log(LoggedMessage!T(level, sourceLocation, SimplifiedUtcTimestamp.fromUnixTime(time(null)), Thread.getThis(), message));
+ }
+
+ /// ditto
+ void info(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.info, message, sourceLocation);
+ }
+ /// ditto
+ void trace(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.trace, message, sourceLocation);
+ }
+ /// ditto
+ void warn(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.warn, message, sourceLocation);
+ }
+ /// ditto
+ void error(T message, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.error, message, sourceLocation);
+ }
+
+ static if(is(T == GenericEmbeddableInterpolatedSequence)) {
+ pragma(inline, true)
+ final void info(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.info, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
+ }
+ pragma(inline, true)
+ final void trace(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.trace, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
+ }
+ pragma(inline, true)
+ final void warn(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.warn, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
+ }
+ pragma(inline, true)
+ final void error(T...)(InterpolationHeader header, T message, InterpolationFooter footer, SourceLocation sourceLocation = SourceLocation(__MODULE__, __LINE__)) shared {
+ log(LogLevel.error, GenericEmbeddableInterpolatedSequence(header, message, footer), sourceLocation);
+ }
+ }
+}
+
+/// ditto
+interface LogListenerController {
+ /++
+ Disconnects from the log producer as soon as possible, possibly leaving messages
+ behind in the log buffer. Once disconnected, the log listener will terminate
+ asynchronously and cannot be reused. Use [waitForCompletion] to block your thread
+ until the termination is complete.
+ +/
+ void disconnect();
+
+ /++
+ Waits for the listener to finish its pending work and terminate. You should call
+ [disconnect] first to make it start to exit.
+ +/
+ void waitForCompletion();
+}
+
+/// ditto
+struct SourceLocation {
+ string moduleName;
+ size_t line;
+}
+
+/// ditto
+struct LoggedMessage(T) {
+ LogLevel level;
+ SourceLocation sourceLocation;
+ SimplifiedUtcTimestamp timestamp;
+ Thread originatingThread;
+ T message;
+
+ // process id can be assumed by the listener,
+ // since it is always the same; logs are sent and received by the same process.
+
+ string toString() {
+ string ret;
+
+ ret ~= sourceLocation.moduleName;
+ ret ~= ":";
+ ret ~= toStringInternal(sourceLocation.line);
+ ret ~= " ";
+ if(originatingThread) {
+ char[16] buffer;
+ ret ~= originatingThread.name.length ? originatingThread.name : intToString(cast(long) originatingThread.id, buffer, IntToStringArgs().withRadix(16));
+ }
+ ret ~= "[";
+ ret ~= toStringInternal(level);
+ ret ~= "] ";
+ ret ~= timestamp.toString();
+ ret ~= " ";
+ ret ~= message.toString();
+
+ return ret;
+ }
+ // callstack?
+}
+
+/// ditto
+enum LogLevel {
+ trace,
+ info,
+ warn,
+ error,
+}
+
+private shared(LoggerOf!GenericEmbeddableInterpolatedSequence) _commonLogger;
+shared(LoggerOf!GenericEmbeddableInterpolatedSequence) logger() {
+ if(_commonLogger is null) {
+ synchronized {
+ if(_commonLogger is null)
+ _commonLogger = new shared LoggerOf!GenericEmbeddableInterpolatedSequence;
+ }
+ }
+
+ return _commonLogger;
+}
+
+/+
+// using this requires a newish compiler so we just uncomment when necessary
+unittest {
+ void main() {
+ auto logger = logger;// new shared LoggerOf!GenericEmbeddableInterpolatedSequence;
+ LogListenerController l1;
+ l1 = logger.addListener((msg, missedCount) {
+ if(missedCount)
+ writeln("T1: missed ", missedCount);
+ writeln("T1:" ~msg.toString());
+ //Thread.sleep(500.msecs);
+ //l1.disconnect();
+ Thread.sleep(1.msecs);
+ });
+ foreach(n; 0 .. 200) {
+ logger.info(i"hello world $n");
+ if(n % 6 == 0)
+ Thread.sleep(1.msecs);
+ }
+
+ logger.addListener((msg, missedCount) {
+ if(missedCount) writeln("T2 missed ", missedCount);
+ writeln("T2:" ~msg.toString());
+ });
+
+ Thread.sleep(500.msecs);
+ l1.disconnect;
+ l1.waitForCompletion;
+
+ logger.close();
+ }
+ //main;
+}
++/
+
+/+
+ =====================
+ TRANSLATION FRAMEWORK
+ =====================
++/
+
+/++
+ Represents a translatable string.
+
+
+ This depends on interpolated expression sequences to be ergonomic to use and in most cases, a function that uses this should take it as `tstring name...`; a typesafe variadic (this is also why it is a class rather than a struct - D only supports this particular feature on classes).
+
+ You can use `null` as a tstring. You can also construct it with UFCS: `i"foo".tstring`.
+
+ The actual translation engine should be set on the application level.
+
+ It is possible to get all translatable string templates compiled into the application at runtime.
+
+ History:
+ Added June 23, 2024
++/
+class tstring {
+ private GenericEmbeddableInterpolatedSequence geis;
+
+ /++
+ For a case where there is no plural specialization.
+ +/
+ this(Args...)(InterpolationHeader hdr, Args args, InterpolationFooter ftr) {
+ geis = GenericEmbeddableInterpolatedSequence(hdr, args, ftr);
+ tstringTemplateProcessor!(Args.length, Args) tp;
+ }
+
+ /+
+ /++
+ When here is a plural specialization this passes the default one.
+ +/
+ this(SArgs..., Pargs...)(
+ InterpolationHeader shdr, SArgs singularArgs, InterpolationFooter sftr,
+ InterpolationHeader phdr, PArgs pluralArgs, InterpolationFooter pftr
+ )
+ {
+ geis = GenericEmbeddableInterpolatedSequence(shdr, singularArgs, sftr);
+ //geis = GenericEmbeddableInterpolatedSequence(phdr, pluralArgs, pftr);
+
+ tstringTemplateProcessor!(Args.length, Args) tp;
+ }
+ +/
+
+ final override string toString() {
+ if(this is null)
+ return null;
+ if(translationEngine !is null)
+ return translationEngine.translate(geis);
+ else
+ return geis.toString();
+ }
+
+ static tstring opCall(Args...)(InterpolationHeader hdr, Args args, InterpolationFooter ftr) {
+ return new tstring(hdr, args, ftr);
+ }
+
+ /+ +++ +/
+
+ private static shared(TranslationEngine) translationEngine_ = null;
+
+ static shared(TranslationEngine) translationEngine() {
+ return translationEngine_;
+ }
+
+ static void translationEngine(shared TranslationEngine e) {
+ translationEngine_ = e;
+ if(e !is null) {
+ auto item = first;
+ while(item) {
+ e.handleTemplate(*item);
+ item = item.next;
+ }
+ }
+ }
+
+ public struct TranslatableElement {
+ string templ;
+ string pluralTempl;
+
+ TranslatableElement* next;
+ }
+
+ static __gshared TranslatableElement* first;
+
+ // FIXME: the template should be identified to the engine somehow
+
+ private static enum templateStringFor(Args...) = () {
+ int count;
+ string templ;
+ foreach(arg; Args) {
+ static if(is(arg == InterpolatedLiteral!str, string str))
+ templ ~= str;
+ else static if(is(arg == InterpolatedExpression!code, string code))
+ templ ~= "{" ~ cast(char)(++count + '0') ~ "}";
+ }
+ return templ;
+ }();
+
+ // this is here to inject static ctors so we can build up a runtime list from ct data
+ private static struct tstringTemplateProcessor(size_t pluralBegins, Args...) {
+ static __gshared TranslatableElement e = TranslatableElement(
+ templateStringFor!(Args[0 .. pluralBegins]),
+ templateStringFor!(Args[pluralBegins .. $]),
+ null /* next, filled in by the static ctor below */);
+
+ @standalone @system
+ shared static this() {
+ e.next = first;
+ first = &e;
+ }
+ }
+}
+
+/// ditto
+class TranslationEngine {
+ string translate(GenericEmbeddableInterpolatedSequence geis) shared {
+ return geis.toString();
+ }
+
+ /++
+ If the translation engine has been set early in the module
+ construction process (which it should be!)
+ +/
+ void handleTemplate(tstring.TranslatableElement t) shared {
+ }
+}
+
+private static template WillFitInGeis(Args...) {
+ static int lengthRequired() {
+ int place;
+ foreach(arg; Args) {
+ static if(is(arg == InterpolatedLiteral!str, string str)) {
+ if(place & 1) // can't put string in the data slot
+ place++;
+ place++;
+ } else static if(is(arg == InterpolationHeader) || is(arg == InterpolationFooter) || is(arg == InterpolatedExpression!code, string code)) {
+ // no storage required
+ } else {
+ if((place & 1) == 0) // can't put data in the string slot
+ place++;
+ place++;
+ }
+ }
+
+ if(place & 1)
+ place++;
+ return place / 2;
+ }
+
+ enum WillFitInGeis = lengthRequired() <= GenericEmbeddableInterpolatedSequence.seq.length;
+}
+
+
+/+
+ For making an array of istrings basically; it moves their CT magic to RT dynamic type.
++/
+struct GenericEmbeddableInterpolatedSequence {
+ static struct Element {
+ string str; // these are pointers to string literals every time
+ LimitedVariant lv;
+ }
+
+ Element[8] seq;
+
+ this(Args...)(InterpolationHeader, Args args, InterpolationFooter) {
+ int place;
+ bool stringUsedInPlace;
+ bool overflowed;
+
+ static assert(WillFitInGeis!(Args), "Your interpolated elements will not fit in the generic buffer.");
+
+ foreach(arg; args) {
+ static if(is(typeof(arg) == InterpolatedLiteral!str, string str)) {
+ if(stringUsedInPlace) {
+ place++;
+ stringUsedInPlace = false;
+ }
+
+ if(place == seq.length) {
+ overflowed = true;
+ break;
+ }
+ seq[place].str = str;
+ stringUsedInPlace = true;
+ } else static if(is(typeof(arg) == InterpolationHeader) || is(typeof(arg) == InterpolationFooter)) {
+ static assert(0, "Cannot embed interpolated sequences");
+ } else static if(is(typeof(arg) == InterpolatedExpression!code, string code)) {
+ // irrelevant
+ } else {
+ if(place == seq.length) {
+ overflowed = true;
+ break;
+ }
+ seq[place].lv = LimitedVariant(arg);
+ place++;
+ stringUsedInPlace = false;
+ }
+ }
+ }
+
+ string toString() {
+ string s;
+ foreach(item; seq) {
+ if(item.str !is null)
+ s ~= item.str;
+ if(!item.lv.containsNull())
+ s ~= item.lv.toString();
+ }
+ return s;
+ }
+}
+
+/+
=================
STDIO REPLACEMENT
=================
@@ -6225,20 +8662,57 @@ private void appendToBuffer(ref char[] buffer, ref int pos, long what) {
This always does text. See also WritableStream and WritableTextStream when they are implemented.
+/
-void writeln(T...)(T t) {
+void writeln(bool printInterpolatedCode = false, T...)(T t) {
char[256] bufferBacking;
char[] buffer = bufferBacking[];
int pos;
foreach(arg; t) {
- static if(is(typeof(arg) : const char[])) {
+ static if(is(typeof(arg) Base == enum)) {
+ appendToBuffer(buffer, pos, typeof(arg).stringof);
+ appendToBuffer(buffer, pos, ".");
+ appendToBuffer(buffer, pos, toStringInternal(arg));
+ appendToBuffer(buffer, pos, "(");
+ appendToBuffer(buffer, pos, cast(Base) arg);
+ appendToBuffer(buffer, pos, ")");
+ } else static if(is(typeof(arg) : const char[])) {
appendToBuffer(buffer, pos, arg);
} else static if(is(typeof(arg) : stringz)) {
appendToBuffer(buffer, pos, arg.borrow);
} else static if(is(typeof(arg) : long)) {
appendToBuffer(buffer, pos, arg);
+ } else static if(is(typeof(arg) : double)) {
+ import core.stdc.stdio;
+ char[128] fb;
+ auto count = snprintf(fb.ptr, fb.length, "%.4lf", arg);
+
+ appendToBuffer(buffer, pos, fb[0 .. count]);
+ } else static if(is(typeof(arg) == InterpolatedExpression!code, string code)) {
+ static if(printInterpolatedCode) {
+ appendToBuffer(buffer, pos, code);
+ appendToBuffer(buffer, pos, " = ");
+ }
} else static if(is(typeof(arg.toString()) : const char[])) {
appendToBuffer(buffer, pos, arg.toString());
+ } else static if(is(typeof(arg) A == struct)) {
+ appendToBuffer(buffer, pos, A.stringof);
+ appendToBuffer(buffer, pos, "(");
+ foreach(idx, item; arg.tupleof) {
+ if(idx)
+ appendToBuffer(buffer, pos, ", ");
+ appendToBuffer(buffer, pos, __traits(identifier, arg.tupleof[idx]));
+ appendToBuffer(buffer, pos, ": ");
+ appendToBuffer(buffer, pos, item);
+ }
+ appendToBuffer(buffer, pos, ")");
+ } else static if(is(typeof(arg) == E[], E)) {
+ appendToBuffer(buffer, pos, "[");
+ foreach(idx, item; arg) {
+ if(idx)
+ appendToBuffer(buffer, pos, ", ");
+ appendToBuffer(buffer, pos, item);
+ }
+ appendToBuffer(buffer, pos, "]");
} else {
appendToBuffer(buffer, pos, "<" ~ typeof(arg).stringof ~ ">");
}
@@ -6249,8 +8723,18 @@ void writeln(T...)(T t) {
actuallyWriteToStdout(buffer[0 .. pos]);
}
+debug void dump(T...)(T t, string file = __FILE__, size_t line = __LINE__) {
+ writeln!true(file, ":", line, ": ", t);
+}
+
private void actuallyWriteToStdout(scope char[] buffer) @trusted {
- version(Windows) {
+
+ version(UseStdioWriteln)
+ {
+ import std.stdio;
+ writeln(buffer);
+ }
+ else version(Windows) {
import core.sys.windows.wincon;
auto hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
@@ -6517,6 +9001,7 @@ struct Timeout {
}
Timeout setTimeout(void delegate() dg, int msecs, int permittedJitter = 20) {
+static assert(0);
return Timeout.init;
}
@@ -6648,7 +9133,7 @@ so the end result is you keep the last ones. it wouldn't report errors if multip
+/
-private version(Windows) extern(Windows) {
+package(arsd) version(Windows) extern(Windows) {
BOOL CancelIoEx(HANDLE, LPOVERLAPPED);
struct WSABUF {
@@ -6667,3 +9152,601 @@ private version(Windows) extern(Windows) {
int WSARecv(SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
int WSARecvFrom(SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, sockaddr*, LPINT, LPOVERLAPPED, LPOVERLAPPED_COMPLETION_ROUTINE);
}
+
+package(arsd) version(OSXCocoa) {
+
+/* Copy/paste chunk from Jacob Carlborg { */
+// from https://raw.githubusercontent.com/jacob-carlborg/druntime/550edd0a64f0eb2c4f35d3ec3d88e26b40ac779e/src/core/stdc/clang_block.d
+// with comments stripped (see docs in the original link), code reformatted, and some names changed to avoid potential conflicts
+
+import core.stdc.config;
+struct ObjCBlock(R = void, Params...) {
+private:
+ alias extern(C) R function(ObjCBlock*, Params) Invoke;
+
+ void* isa;
+ int flags;
+ int reserved = 0;
+ Invoke invoke;
+ Descriptor* descriptor;
+
+ // Imported variables go here
+ R delegate(Params) dg;
+
+ this(void* isa, int flags, Invoke invoke, R delegate(Params) dg) {
+ this.isa = isa;
+ this.flags = flags;
+ this.invoke = invoke;
+ this.dg = dg;
+ this.descriptor = &.objcblock_descriptor;
+ }
+}
+ObjCBlock!(R, Params) block(R, Params...)(R delegate(Params) dg) {
+ static if (Params.length == 0)
+ enum flags = 0x50000000;
+ else
+ enum flags = 0x40000000;
+
+ return ObjCBlock!(R, Params)(&_NSConcreteStackBlock, flags, &objcblock_invoke!(R, Params), dg);
+}
+
+private struct Descriptor {
+ c_ulong reserved;
+ c_ulong size;
+ const(char)* signature;
+}
+private extern(C) extern __gshared void*[32] _NSConcreteStackBlock;
+private __gshared auto objcblock_descriptor = Descriptor(0, ObjCBlock!().sizeof);
+private extern(C) R objcblock_invoke(R, Args...)(ObjCBlock!(R, Args)* block, Args args) {
+ return block.dg(args);
+}
+
+
+/* End copy/paste chunk from Jacob Carlborg } */
+
+
+/+
+To let Cocoa know that you intend to use multiple threads, all you have to do is spawn a single thread using the NSThread class and let that thread immediately exit. Your thread entry point need not do anything. Just the act of spawning a thread using NSThread is enough to ensure that the locks needed by the Cocoa frameworks are put in place.
+
+If you are not sure if Cocoa thinks your application is multithreaded or not, you can use the isMultiThreaded method of NSThread to check.
++/
+
+
+ struct DeifiedNSString {
+ char[16] sso;
+ const(char)[] str;
+
+ this(NSString s) {
+ auto len = s.length;
+ if(len <= sso.length / 4)
+ str = sso[];
+ else
+ str = new char[](len * 4);
+
+ NSUInteger count;
+ NSRange leftover;
+ auto ret = s.getBytes(cast(char*) str.ptr, str.length, &count, NSStringEncoding.NSUTF8StringEncoding, NSStringEncodingConversionOptions.none, NSRange(0, len), &leftover);
+ if(ret)
+ str = str[0 .. count];
+ else
+ throw new Exception("uh oh");
+ }
+ }
+
+ extern (Objective-C) {
+ import core.attribute; // : selector, optional;
+
+ alias NSUInteger = size_t;
+ alias NSInteger = ptrdiff_t;
+ alias unichar = wchar;
+ struct SEL_;
+ alias SEL_* SEL;
+ // this is called plain `id` in objective C but i fear mistakes with that in D. like sure it is a type instead of a variable like most things called id but i still think it is weird. i might change my mind later.
+ alias void* NSid; // FIXME? the docs say this is a pointer to an instance of a class, but that is not necessary a child of NSObject
+
+ extern class NSObject {
+ static NSObject alloc() @selector("alloc");
+ NSObject init() @selector("init");
+
+ void retain() @selector("retain");
+ void release() @selector("release");
+ void autorelease() @selector("autorelease");
+
+ void performSelectorOnMainThread(SEL aSelector, NSid arg, bool waitUntilDone) @selector("performSelectorOnMainThread:withObject:waitUntilDone:");
+ }
+
+ // this is some kind of generic in objc...
+ extern class NSArray : NSObject {
+ static NSArray arrayWithObjects(NSid* objects, NSUInteger count) @selector("arrayWithObjects:count:");
+ }
+
+ extern class NSString : NSObject {
+ override static NSString alloc() @selector("alloc");
+ override NSString init() @selector("init");
+
+ NSString initWithUTF8String(const scope char* str) @selector("initWithUTF8String:");
+
+ NSString initWithBytes(
+ const(ubyte)* bytes,
+ NSUInteger length,
+ NSStringEncoding encoding
+ ) @selector("initWithBytes:length:encoding:");
+
+ unichar characterAtIndex(NSUInteger index) @selector("characterAtIndex:");
+ NSUInteger length() @selector("length");
+ const char* UTF8String() @selector("UTF8String");
+
+ void getCharacters(wchar* buffer, NSRange range) @selector("getCharacters:range:");
+
+ bool getBytes(void* buffer, NSUInteger maxBufferCount, NSUInteger* usedBufferCount, NSStringEncoding encoding, NSStringEncodingConversionOptions options, NSRange range, NSRange* leftover) @selector("getBytes:maxLength:usedLength:encoding:options:range:remainingRange:");
+ }
+
+ struct NSRange {
+ NSUInteger loc;
+ NSUInteger len;
+ }
+
+ enum NSStringEncodingConversionOptions : NSInteger {
+ none = 0,
+ NSAllowLossyEncodingConversion = 1,
+ NSExternalRepresentationEncodingConversion = 2
+ }
+
+ enum NSEventType {
+ idk
+
+ }
+
+ enum NSEventModifierFlags : NSUInteger {
+ NSEventModifierFlagCapsLock = 1 << 16,
+ NSEventModifierFlagShift = 1 << 17,
+ NSEventModifierFlagControl = 1 << 18,
+ NSEventModifierFlagOption = 1 << 19, // aka Alt
+ NSEventModifierFlagCommand = 1 << 20, // aka super
+ NSEventModifierFlagNumericPad = 1 << 21,
+ NSEventModifierFlagHelp = 1 << 22,
+ NSEventModifierFlagFunction = 1 << 23,
+ NSEventModifierFlagDeviceIndependentFlagsMask = 0xffff0000UL
+ }
+
+ version(OSX)
+ extern class NSEvent : NSObject {
+ NSEventType type() @selector("type");
+
+ NSPoint locationInWindow() @selector("locationInWindow");
+ NSTimeInterval timestamp() @selector("timestamp");
+ NSWindow window() @selector("window"); // note: nullable
+ NSEventModifierFlags modifierFlags() @selector("modifierFlags");
+
+ NSString characters() @selector("characters");
+ NSString charactersIgnoringModifiers() @selector("charactersIgnoringModifiers");
+ ushort keyCode() @selector("keyCode");
+ ushort specialKey() @selector("specialKey");
+
+ static NSUInteger pressedMouseButtons() @selector("pressedMouseButtons");
+ NSPoint locationInWindow() @selector("locationInWindow"); // in screen coordinates
+ static NSPoint mouseLocation() @selector("mouseLocation"); // in screen coordinates
+ NSInteger buttonNumber() @selector("buttonNumber");
+
+ CGFloat deltaX() @selector("deltaX");
+ CGFloat deltaY() @selector("deltaY");
+ CGFloat deltaZ() @selector("deltaZ");
+
+ bool hasPreciseScrollingDeltas() @selector("hasPreciseScrollingDeltas");
+
+ CGFloat scrollingDeltaX() @selector("scrollingDeltaX");
+ CGFloat scrollingDeltaY() @selector("scrollingDeltaY");
+
+ // @property(getter=isDirectionInvertedFromDevice, readonly) BOOL directionInvertedFromDevice;
+ }
+
+ extern /* final */ class NSTimer : NSObject { // the docs say don't subclass this, but making it final breaks the bridge
+ override static NSTimer alloc() @selector("alloc");
+ override NSTimer init() @selector("init");
+
+ static NSTimer schedule(NSTimeInterval timeIntervalInSeconds, NSid target, SEL selector, NSid userInfo, bool repeats) @selector("scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:");
+
+ void fire() @selector("fire");
+ void invalidate() @selector("invalidate");
+
+ bool valid() @selector("isValid");
+ // @property(copy) NSDate *fireDate;
+ NSTimeInterval timeInterval() @selector("timeInterval");
+ NSid userInfo() @selector("userInfo");
+
+ NSTimeInterval tolerance() @selector("tolerance");
+ NSTimeInterval tolerance(NSTimeInterval) @selector("setTolerance:");
+ }
+
+ alias NSTimeInterval = double;
+
+ version(OSX)
+ extern class NSResponder : NSObject {
+ NSMenu menu() @selector("menu");
+ void menu(NSMenu menu) @selector("setMenu:");
+
+ void keyDown(NSEvent event) @selector("keyDown:");
+ void keyUp(NSEvent event) @selector("keyUp:");
+
+ // - (void)interpretKeyEvents:(NSArray<NSEvent *> *)eventArray;
+
+ void mouseDown(NSEvent event) @selector("mouseDown:");
+ void mouseDragged(NSEvent event) @selector("mouseDragged:");
+ void mouseUp(NSEvent event) @selector("mouseUp:");
+ void mouseMoved(NSEvent event) @selector("mouseMoved:");
+ void mouseEntered(NSEvent event) @selector("mouseEntered:");
+ void mouseExited(NSEvent event) @selector("mouseExited:");
+
+ void rightMouseDown(NSEvent event) @selector("rightMouseDown:");
+ void rightMouseDragged(NSEvent event) @selector("rightMouseDragged:");
+ void rightMouseUp(NSEvent event) @selector("rightMouseUp:");
+
+ void otherMouseDown(NSEvent event) @selector("otherMouseDown:");
+ void otherMouseDragged(NSEvent event) @selector("otherMouseDragged:");
+ void otherMouseUp(NSEvent event) @selector("otherMouseUp:");
+
+ void scrollWheel(NSEvent event) @selector("scrollWheel:");
+
+ // touch events should also be here btw among others
+ }
+
+ version(OSX)
+ extern class NSApplication : NSResponder {
+ static NSApplication shared_() @selector("sharedApplication");
+
+ NSApplicationDelegate delegate_() @selector("delegate");
+ void delegate_(NSApplicationDelegate) @selector("setDelegate:");
+
+ bool setActivationPolicy(NSApplicationActivationPolicy activationPolicy) @selector("setActivationPolicy:");
+
+ void activateIgnoringOtherApps(bool flag) @selector("activateIgnoringOtherApps:");
+
+ @property NSMenu mainMenu() @selector("mainMenu");
+ @property NSMenu mainMenu(NSMenu) @selector("setMainMenu:");
+
+ void run() @selector("run");
+
+ void terminate(void*) @selector("terminate:");
+ }
+
+ version(OSX)
+ extern interface NSApplicationDelegate {
+ void applicationWillFinishLaunching(NSNotification notification) @selector("applicationWillFinishLaunching:");
+ void applicationDidFinishLaunching(NSNotification notification) @selector("applicationDidFinishLaunching:");
+ bool applicationShouldTerminateAfterLastWindowClosed(NSNotification notification) @selector("applicationShouldTerminateAfterLastWindowClosed:");
+ }
+
+ extern class NSNotification : NSObject {
+ @property NSid object() @selector("object");
+ }
+
+ enum NSApplicationActivationPolicy : ptrdiff_t {
+ /* The application is an ordinary app that appears in the Dock and may have a user interface. This is the default for bundled apps, unless overridden in the Info.plist. */
+ regular,
+
+ /* The application does not appear in the Dock and does not have a menu bar, but it may be activated programmatically or by clicking on one of its windows. This corresponds to LSUIElement=1 in the Info.plist. */
+ accessory,
+
+ /* The application does not appear in the Dock and may not create windows or be activated. This corresponds to LSBackgroundOnly=1 in the Info.plist. This is also the default for unbundled executables that do not have Info.plists. */
+ prohibited
+ }
+
+ extern class NSGraphicsContext : NSObject {
+ static NSGraphicsContext currentContext() @selector("currentContext");
+ NSGraphicsContext graphicsPort() @selector("graphicsPort");
+ }
+
+ version(OSX)
+ extern class NSMenu : NSObject {
+ override static NSMenu alloc() @selector("alloc");
+
+ override NSMenu init() @selector("init");
+ NSMenu init(NSString title) @selector("initWithTitle:");
+
+ void setSubmenu(NSMenu menu, NSMenuItem item) @selector("setSubmenu:forItem:");
+ void addItem(NSMenuItem newItem) @selector("addItem:");
+
+ NSMenuItem addItem(
+ NSString title,
+ SEL selector,
+ NSString charCode
+ ) @selector("addItemWithTitle:action:keyEquivalent:");
+ }
+
+ version(OSX)
+ extern class NSMenuItem : NSObject {
+ override static NSMenuItem alloc() @selector("alloc");
+ override NSMenuItem init() @selector("init");
+
+ NSMenuItem init(
+ NSString title,
+ SEL selector,
+ NSString charCode
+ ) @selector("initWithTitle:action:keyEquivalent:");
+
+ void enabled(bool) @selector("setEnabled:");
+
+ NSResponder target(NSResponder) @selector("setTarget:");
+ }
+
+ enum NSWindowStyleMask : size_t {
+ borderless = 0,
+ titled = 1 << 0,
+ closable = 1 << 1,
+ miniaturizable = 1 << 2,
+ resizable = 1 << 3,
+
+ /* Specifies a window with textured background. Textured windows generally don't draw a top border line under the titlebar/toolbar. To get that line, use the NSUnifiedTitleAndToolbarWindowMask mask.
+ */
+ texturedBackground = 1 << 8,
+
+ /* Specifies a window whose titlebar and toolbar have a unified look - that is, a continuous background. Under the titlebar and toolbar a horizontal separator line will appear.
+ */
+ unifiedTitleAndToolbar = 1 << 12,
+
+ /* When set, the window will appear full screen. This mask is automatically toggled when toggleFullScreen: is called.
+ */
+ fullScreen = 1 << 14,
+
+ /* If set, the contentView will consume the full size of the window; it can be combined with other window style masks, but is only respected for windows with a titlebar.
+ Utilizing this mask opts-in to layer-backing. Utilize the contentLayoutRect or auto-layout contentLayoutGuide to layout views underneath the titlebar/toolbar area.
+ */
+ fullSizeContentView = 1 << 15,
+
+ /* The following are only applicable for NSPanel (or a subclass thereof)
+ */
+ utilityWindow = 1 << 4,
+ docModalWindow = 1 << 6,
+ nonactivatingPanel = 1 << 7, // Specifies that a panel that does not activate the owning application
+ hUDWindow = 1 << 13 // Specifies a heads up display panel
+ }
+
+ version(OSX)
+ extern class NSWindow : NSObject {
+ override static NSWindow alloc() @selector("alloc");
+
+ override NSWindow init() @selector("init");
+
+ NSWindow initWithContentRect(
+ NSRect contentRect,
+ NSWindowStyleMask style,
+ NSBackingStoreType bufferingType,
+ bool flag
+ ) @selector("initWithContentRect:styleMask:backing:defer:");
+
+ void makeKeyAndOrderFront(NSid sender) @selector("makeKeyAndOrderFront:");
+ NSView contentView() @selector("contentView");
+ void contentView(NSView view) @selector("setContentView:");
+ void orderFrontRegardless() @selector("orderFrontRegardless");
+ void center() @selector("center");
+
+ NSRect frame() @selector("frame");
+
+ NSRect contentRectForFrameRect(NSRect frameRect) @selector("contentRectForFrameRect:");
+
+ NSString title() @selector("title");
+ void title(NSString value) @selector("setTitle:");
+
+ void close() @selector("close");
+
+ NSWindowDelegate delegate_() @selector("delegate");
+ void delegate_(NSWindowDelegate) @selector("setDelegate:");
+
+ void setBackgroundColor(NSColor color) @selector("setBackgroundColor:");
+ }
+
+ version(OSX)
+ extern interface NSWindowDelegate {
+ @optional:
+ void windowDidResize(NSNotification notification) @selector("windowDidResize:");
+
+ NSSize windowWillResize(NSWindow sender, NSSize frameSize) @selector("windowWillResize:toSize:");
+
+ void windowWillClose(NSNotification notification) @selector("windowWillClose:");
+ }
+
+ version(OSX)
+ extern class NSView : NSResponder {
+ //override NSView init() @selector("init");
+ NSView initWithFrame(NSRect frameRect) @selector("initWithFrame:");
+
+ void addSubview(NSView view) @selector("addSubview:");
+
+ bool wantsLayer() @selector("wantsLayer");
+ void wantsLayer(bool value) @selector("setWantsLayer:");
+
+ CALayer layer() @selector("layer");
+ void uiDelegate(NSObject) @selector("setUIDelegate:");
+
+ void drawRect(NSRect rect) @selector("drawRect:");
+ bool isFlipped() @selector("isFlipped");
+ bool acceptsFirstResponder() @selector("acceptsFirstResponder");
+ bool setNeedsDisplay(bool) @selector("setNeedsDisplay:");
+
+ // DO NOT USE: https://issues.dlang.org/show_bug.cgi?id=19017
+ // an asm { pop RAX; } after getting the struct can kinda hack around this but still
+ @property NSRect frame() @selector("frame");
+ @property NSRect frame(NSRect rect) @selector("setFrame:");
+
+ void setFrameSize(NSSize newSize) @selector("setFrameSize:");
+ void setFrameOrigin(NSPoint newOrigin) @selector("setFrameOrigin:");
+
+ void addSubview(NSView what) @selector("addSubview:");
+ void removeFromSuperview() @selector("removeFromSuperview");
+ }
+
+ extern class NSFont : NSObject {
+ void set() @selector("set"); // sets it into the current graphics context
+ void setInContext(NSGraphicsContext context) @selector("setInContext:");
+
+ static NSFont fontWithName(NSString fontName, CGFloat fontSize) @selector("fontWithName:size:");
+ // fontWithDescriptor too
+ // fontWithName and matrix too
+ static NSFont systemFontOfSize(CGFloat fontSize) @selector("systemFontOfSize:");
+ // among others
+
+ @property CGFloat pointSize() @selector("pointSize");
+ @property bool isFixedPitch() @selector("isFixedPitch");
+ // fontDescriptor
+ @property NSString displayName() @selector("displayName");
+
+ @property CGFloat ascender() @selector("ascender");
+ @property CGFloat descender() @selector("descender"); // note it is negative
+ @property CGFloat capHeight() @selector("capHeight");
+ @property CGFloat leading() @selector("leading");
+ @property CGFloat xHeight() @selector("xHeight");
+ // among many more
+ }
+
+ extern class NSColor : NSObject {
+ override static NSColor alloc() @selector("alloc");
+ static NSColor redColor() @selector("redColor");
+ static NSColor whiteColor() @selector("whiteColor");
+
+ CGColorRef CGColor() @selector("CGColor");
+ }
+
+ extern class CALayer : NSObject {
+ CGFloat borderWidth() @selector("borderWidth");
+ void borderWidth(CGFloat value) @selector("setBorderWidth:");
+
+ CGColorRef borderColor() @selector("borderColor");
+ void borderColor(CGColorRef) @selector("setBorderColor:");
+ }
+
+
+ version(OSX)
+ extern class NSViewController : NSObject {
+ NSView view() @selector("view");
+ void view(NSView view) @selector("setView:");
+ }
+
+ enum NSBackingStoreType : size_t {
+ retained = 0,
+ nonretained = 1,
+ buffered = 2
+ }
+
+ enum NSStringEncoding : NSUInteger {
+ NSASCIIStringEncoding = 1, /* 0..127 only */
+ NSUTF8StringEncoding = 4,
+ NSUnicodeStringEncoding = 10,
+
+ NSUTF16StringEncoding = NSUnicodeStringEncoding,
+ NSUTF16BigEndianStringEncoding = 0x90000100,
+ NSUTF16LittleEndianStringEncoding = 0x94000100,
+ NSUTF32StringEncoding = 0x8c000100,
+ NSUTF32BigEndianStringEncoding = 0x98000100,
+ NSUTF32LittleEndianStringEncoding = 0x9c000100
+ }
+
+
+ struct CGColor;
+ alias CGColorRef = CGColor*;
+
+ // note on the watch os it is float, not double
+ alias CGFloat = double;
+
+ struct NSPoint {
+ CGFloat x;
+ CGFloat y;
+ }
+
+ struct NSSize {
+ CGFloat width;
+ CGFloat height;
+ }
+
+ struct NSRect {
+ NSPoint origin;
+ NSSize size;
+ }
+
+ alias NSPoint CGPoint;
+ alias NSSize CGSize;
+ alias NSRect CGRect;
+
+ pragma(inline, true) NSPoint NSMakePoint(CGFloat x, CGFloat y) {
+ NSPoint p;
+ p.x = x;
+ p.y = y;
+ return p;
+ }
+
+ pragma(inline, true) NSSize NSMakeSize(CGFloat w, CGFloat h) {
+ NSSize s;
+ s.width = w;
+ s.height = h;
+ return s;
+ }
+
+ pragma(inline, true) NSRect NSMakeRect(CGFloat x, CGFloat y, CGFloat w, CGFloat h) {
+ NSRect r;
+ r.origin.x = x;
+ r.origin.y = y;
+ r.size.width = w;
+ r.size.height = h;
+ return r;
+ }
+
+
+ }
+
+ // helper raii refcount object
+ static if(UseCocoa)
+ struct MacString {
+ union {
+ // must be wrapped cuz of bug in dmd
+ // referencing an init symbol when it should
+ // just be null. but the union makes it work
+ NSString s;
+ }
+
+ // FIXME: if a string literal it would be kinda nice to use
+ // the other function. but meh
+
+ this(scope const char[] str) {
+ this.s = NSString.alloc.initWithBytes(
+ cast(const(ubyte)*) str.ptr,
+ str.length,
+ NSStringEncoding.NSUTF8StringEncoding
+ );
+ }
+
+ NSString borrow() {
+ return s;
+ }
+
+ this(this) {
+ if(s !is null)
+ s.retain();
+ }
+
+ ~this() {
+ if(s !is null) {
+ s.release();
+ s = null;
+ }
+ }
+ }
+
+ extern(C) void NSLog(NSString, ...);
+ extern(C) SEL sel_registerName(const(char)* str);
+
+ version(OSX)
+ extern (Objective-C) __gshared NSApplication NSApp_;
+
+ version(OSX)
+ NSApplication NSApp() {
+ if(NSApp_ is null)
+ NSApp_ = NSApplication.shared_;
+ return NSApp_;
+ }
+
+ version(DigitalMars) {
+ // hacks to work around compiler bug
+ extern(C) __gshared void* _D4arsd4core17NSGraphicsContext7__ClassZ = null;
+ extern(C) __gshared void* _D4arsd4core6NSView7__ClassZ = null;
+ extern(C) __gshared void* _D4arsd4core8NSWindow7__ClassZ = null;
+ }
+}