diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..9fadc8f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,59 @@ +name: Build, Test, and Publish + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: read # Required for reading repository content + packages: write # Required for publishing packages to NuGet + +jobs: + build: + runs-on: ubuntu-latest + + steps: + # Checkout the repository + - name: Checkout code + uses: actions/checkout@v3 + + # Set up .NET SDK + - name: Set up .NET SDK + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '9.0' # Specify your .NET SDK version + + # Restore dependencies + - name: Restore dependencies + run: dotnet restore Unhinged/Unhinged.csproj + + # Build the project + - name: Build project + run: dotnet build Unhinged/Unhinged.csproj --configuration Release --no-restore + + - name: Pack NuGet package + run: dotnet pack Unhinged/Unhinged.csproj --configuration Release --output ./artifacts + + # Run tests only for .NET 9 + #- name: Run tests (only for .NET 9) + # run: | + # if [[ "$(dotnet --version)" == 9.* ]]; then + # dotnet test Unhinged.Tests/Unhinged.Tests.csproj --verbosity normal + # else + # echo "Skipping tests: Not running on .NET 9." + # fi + + - name: Publish to NuGet + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: dotnet nuget push ./artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key $NUGET_API_KEY --skip-duplicate + + # Publish to GitHub Packages + - name: Publish to GitHub Packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: dotnet nuget push ./artifacts/*.nupkg --source https://nuget.pkg.github.com/MDA2AV/index.json --api-key $GITHUB_TOKEN --skip-duplicate diff --git a/Unhinged.Playground/Program.cs b/Unhinged.Playground/Program.cs new file mode 100644 index 0000000..bec08e7 --- /dev/null +++ b/Unhinged.Playground/Program.cs @@ -0,0 +1,70 @@ +// ReSharper disable always SuggestVarOrType_BuiltInTypes +// (var is avoided intentionally in this project so that concrete types are visible at call sites.) +// ReSharper disable always StackAllocInsideLoop + +using System.Runtime.CompilerServices; +using System.Text.Json; +using Unhinged; + +#pragma warning disable CA2014 + +[SkipLocalsInit] +internal static class Program +{ + public static void Main(string[] args) + { + var builder = UnhingedEngine + .CreateBuilder() + .SetNWorkersSolver(() => Environment.ProcessorCount / 2) + .SetBacklog(16384) + .SetMaxEventsPerWake(128) + .SetMaxNumberConnectionsPerWorker(32) + .SetPort(8080) + .SetSlabSizes(16 * 1024, 16 * 1024) + .InjectRequestHandler(RequestHandler); + + var engine = builder.Build(); + + engine.Run(); + } + + private static void RequestHandler(Connection connection) + { + if(connection.HashedRoute == 291830056) // /json + CommitJsonResponse(connection); + + else if (connection.HashedRoute == 3454831873) // /plaintext + CommitPlainTextResponse(connection); + } + + [ThreadStatic] private static Utf8JsonWriter? t_utf8JsonWriter; + private static readonly JsonContext SerializerContext = JsonContext.Default; + private static void CommitJsonResponse(Connection connection) + { + connection.WriteBuffer.Write("HTTP/1.1 200 OK\r\n"u8 + + "Server: W\r\n"u8 + + "Content-Type: application/json; charset=UTF-8\r\n"u8 + + "Content-Length: 27\r\n"u8); + connection.WriteBuffer.Write(DateHelper.HeaderBytes); + + t_utf8JsonWriter ??= new Utf8JsonWriter(connection.WriteBuffer, new JsonWriterOptions { SkipValidation = true }); + t_utf8JsonWriter.Reset(connection.WriteBuffer); + + // Creating(Allocating) a new JsonMessage every request + var message = new JsonMessage { Message = "Hello, World!" }; + // Serializing it every request + JsonSerializer.Serialize(t_utf8JsonWriter, message, SerializerContext.JsonMessage); + } + + private static void CommitPlainTextResponse(Connection connection) + { + connection.WriteBuffer.Write("HTTP/1.1 200 OK\r\n"u8 + + "Server: W\r\n"u8 + + "Content-Type: text/plain\r\n"u8 + + //"Content-Length: 13\r\n\r\nHello, World!"u8); + "Content-Length: 13\r\n"u8); + connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes); + connection.WriteBuffer.Write("Hello, World!"u8); + } +} + diff --git a/Unhinged.Playground/Unhinged.Playground.csproj b/Unhinged.Playground/Unhinged.Playground.csproj new file mode 100644 index 0000000..f228836 --- /dev/null +++ b/Unhinged.Playground/Unhinged.Playground.csproj @@ -0,0 +1,26 @@ + + + + Exe + net9.0 + enable + enable + true + true + true + true + + + linux-x64 + true + + + + + + + + + + + diff --git a/Unhinged.sln b/Unhinged.sln index 446fce4..931f379 100644 --- a/Unhinged.sln +++ b/Unhinged.sln @@ -1,6 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unhinged", "Unhinged\Unhinged.csproj", "{01D62FEF-383D-4446-846B-7503D217B1D7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unhinged", "Unhinged\Unhinged.csproj", "{4DA83CE5-5152-4142-BD80-EBCCED4694CF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unhinged.Playground", "Unhinged.Playground\Unhinged.Playground.csproj", "{5D135CA9-4854-458E-A48C-287CF1A0DEC2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -8,9 +10,13 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {01D62FEF-383D-4446-846B-7503D217B1D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {01D62FEF-383D-4446-846B-7503D217B1D7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {01D62FEF-383D-4446-846B-7503D217B1D7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {01D62FEF-383D-4446-846B-7503D217B1D7}.Release|Any CPU.Build.0 = Release|Any CPU + {4DA83CE5-5152-4142-BD80-EBCCED4694CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4DA83CE5-5152-4142-BD80-EBCCED4694CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4DA83CE5-5152-4142-BD80-EBCCED4694CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4DA83CE5-5152-4142-BD80-EBCCED4694CF}.Release|Any CPU.Build.0 = Release|Any CPU + {5D135CA9-4854-458E-A48C-287CF1A0DEC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D135CA9-4854-458E-A48C-287CF1A0DEC2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D135CA9-4854-458E-A48C-287CF1A0DEC2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D135CA9-4854-458E-A48C-287CF1A0DEC2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Unhinged/Engine/Connection.cs b/Unhinged/Engine/Connection.cs index 793b4e2..a8502d0 100644 --- a/Unhinged/Engine/Connection.cs +++ b/Unhinged/Engine/Connection.cs @@ -25,7 +25,7 @@ public unsafe class Connection : IDisposable public readonly FixedBufferWriter WriteBuffer; // Fnv1a32 hashed route - internal uint HashedRoute { get; set; } + public uint HashedRoute { get; set; } /// Used to size the slabs (typically per-worker slab size). /// Bytes per connection for receive. diff --git a/Unhinged/Program.cs b/Unhinged/Program.cs deleted file mode 100644 index bf2c109..0000000 --- a/Unhinged/Program.cs +++ /dev/null @@ -1,529 +0,0 @@ -using System.Text.Json; - -namespace Unhinged; - -// ReSharper disable always SuggestVarOrType_BuiltInTypes -// (var is avoided intentionally in this project so that concrete types are visible at call sites.) -// ReSharper disable always StackAllocInsideLoop -#pragma warning disable CA2014 - -[SkipLocalsInit] -internal static class Program -{ - public static void Main(string[] args) - { - var builder = UnhingedEngine - .CreateBuilder() - .SetNWorkersSolver(() => Environment.ProcessorCount / 2) - .SetBacklog(16384) - .SetMaxEventsPerWake(128) - .SetMaxNumberConnectionsPerWorker(32) - .SetPort(8080) - .SetSlabSizes(16 * 1024, 16 * 1024) - .InjectRequestHandler(RequestHandler); - - var engine = builder.Build(); - - engine.Run(); - } - - private static void RequestHandler(Connection connection) - { - if(connection.HashedRoute == 291830056) // /json - CommitJsonResponse(connection); - - else if (connection.HashedRoute == 3454831873) // /plaintext - CommitPlainTextResponse(connection); - } - - [ThreadStatic] private static Utf8JsonWriter? t_utf8JsonWriter; - private static readonly JsonContext SerializerContext = JsonContext.Default; - private static void CommitJsonResponse(Connection connection) - { - connection.WriteBuffer.Write("HTTP/1.1 200 OK\r\n"u8 + - "Server: W\r\n"u8 + - "Content-Type: application/json; charset=UTF-8\r\n"u8 + - "Content-Length: 27\r\n"u8); - connection.WriteBuffer.Write(DateHelper.HeaderBytes); - - t_utf8JsonWriter ??= new Utf8JsonWriter(connection.WriteBuffer, new JsonWriterOptions { SkipValidation = true }); - t_utf8JsonWriter.Reset(connection.WriteBuffer); - - // Creating(Allocating) a new JsonMessage every request - var message = new JsonMessage { Message = "Hello, World!" }; - // Serializing it every request - JsonSerializer.Serialize(t_utf8JsonWriter, message, SerializerContext.JsonMessage); - } - - private static void CommitPlainTextResponse(Connection connection) - { - connection.WriteBuffer.Write("HTTP/1.1 200 OK\r\n"u8 + - "Server: W\r\n"u8 + - "Content-Type: text/plain\r\n"u8 + - "Content-Length: 13\r\n\r\n"u8); - //"Content-Length: 13\r\n"u8); - //connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes); - connection.WriteBuffer.Write("Hello, World!"u8); - } -} - - -/* -[SkipLocalsInit] internal static unsafe class Program -{ - private const int Backlog = 16384; // listen() backlog hint to the kernel - private const int MaxNumberConnectionsPerWorker = 1024; - private const int InSlabSize = 16 * 1024; - private const int OutSlabSize = 16 * 1024; - private const int MaxEventsPerWake = 16; - private const int MaxStackSizePerThread = 1024 * 1024; - - // ===== Entry point ===== - public static void Main(string[] args) - { - Console.WriteLine($"Arch={RuntimeInformation.ProcessArchitecture}, Packed={(Packed ? 12 : 16)}-byte epoll_event"); - - const int port = 8080; - // Choose a bounded number of workers (heuristic tuned for moderate -c values like 512) - int workers = Math.Max(8, Math.Min(Environment.ProcessorCount / 2, 16)); // good start for -c512 - - // Create and bind the listening socket (non-blocking) - var (listenFd, acceptBlocking) = CreateListenSocket(port); - - // Spin up workers. Each worker owns an epoll instance and an eventfd notifier. - var W = new Worker[workers]; - for (int i = 0; i < workers; i++) - { - W[i] = new Worker(i, MaxEventsPerWake); // MaxEvents per epoll_wait for this worker - int iCap = i; // capture loop variable safely - var t = new Thread(() => WorkerLoop(W[iCap]), MaxStackSizePerThread) // 1MB - { - IsBackground = true, Name = $"worker-{iCap}" - }; - t.Start(); - } - - // Single acceptor thread (current thread): accepts and hands off connections. - AcceptorLoop(listenFd, W); - } - - // ===== Socket setup ===== - private static (int listenFd, bool blocking) CreateListenSocket(int port) - { - // Create a TCP socket with close-on-exec. - int fd = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, IPPROTO_TCP); - if (fd < 0) throw new Exception($"socket failed errno={Marshal.GetLastPInvokeError()}"); - - // Allow reusing the address to ease restarts. - int one = 1; - setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, ref one, sizeof(int)); - - // Put the listening socket in non-blocking mode. - int fl = fcntl(fd, F_GETFL, 0); - if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK); - - // Bind 0.0.0.0:port - var addr = new sockaddr_in - { - sin_family = (ushort)AF_INET, - sin_port = Htons((ushort)port), - sin_addr = new in_addr { s_addr = 0 }, // 0.0.0.0 (INADDR_ANY) - sin_zero = new byte[8] - }; - if (bind(fd, ref addr, (uint)Marshal.SizeOf()) != 0) - throw new Exception($"bind failed errno={Marshal.GetLastPInvokeError()}"); - if (listen(fd, Backlog) != 0) - throw new Exception($"listen failed errno={Marshal.GetLastPInvokeError()}"); - - return (fd, false); - } - - // ===== Acceptor: accept + handoff to least-busy worker ===== - private static void AcceptorLoop(int listenFd, Worker[] workers) - { - // Use epoll to sleep the acceptor until the listening socket becomes readable. - int ep = epoll_create1(EPOLL_CLOEXEC); - if (ep < 0) throw new Exception("epoll_create1 (acceptor) failed"); - byte* ev = stackalloc byte[EvSize]; - // Monitor the listening fd for incoming connections and errors. - WriteEpollEvent(ev, EPOLLIN | EPOLLERR | EPOLLHUP, listenFd); - - Console.WriteLine($"Configuring listening epoll for socket {listenFd}"); - - if (epoll_ctl(ep, EPOLL_CTL_ADD, listenFd, (IntPtr)ev) != 0) - throw new Exception("epoll_ctl ADD listen failed"); - - // Buffer to receive epoll events (capacity for MaxEventsPerWake events per wait call). - // NOTE: MaxEventsPerWake is an operational batch size, not an OS limit. Increase if needed. - IntPtr eventsBuf = Marshal.AllocHGlobal(EvSize * MaxEventsPerWake); - - for (;;) - { - // Block until at least one event is available on the listening epoll set. - int n = epoll_wait(ep, eventsBuf, MaxEventsPerWake, -1); - if (n < 0) { if (Marshal.GetLastPInvokeError() == EINTR) continue; throw new Exception("epoll_wait acceptor"); } - - for (int i = 0; i < n; i++) - { - ReadEpollEvent((byte*)eventsBuf + i * EvSize, out uint events, out int fd); - if (fd != listenFd) continue; // We only expect the listen fd on the acceptor set - if ((events & (EPOLLERR | EPOLLHUP)) != 0) continue; // Skip if error/hangup on listen fd - if ((events & EPOLLIN) == 0) continue; // Only proceed on EPOLLIN - - // Drain accepts until EAGAIN: batched acceptance to reduce wakeups. - for (;;) - { - int cfd = accept4(listenFd, IntPtr.Zero, IntPtr.Zero, SOCK_NONBLOCK | SOCK_CLOEXEC); - if (cfd >= 0) - { - // Tweak connection-level options for latency and fast close. - int one = 1; - setsockopt(cfd, IPPROTO_TCP, TCP_NODELAY, ref one, sizeof(int)); - var lg = new Linger { l_onoff = 0, l_linger = 0 }; // disable linger - setsockopt(cfd, SOL_SOCKET, SO_LINGER, ref lg, (uint)Marshal.SizeOf()); - - // Choose the least-busy worker by Current (approximate in-flight count) - int w = ChooseLeastBusy(workers); - workers[w].Inbox.Enqueue(cfd); // hand off fd to worker queue - Interlocked.Increment(ref workers[w].Current); // bump worker load metric - //Console.WriteLine($"Incremented {workers[w].Ep} with cfg {cfd} to {workers[w].Current}"); - - // Wake the worker via eventfd. We write 8 bytes (uint64). - ulong inc = 1; - write(workers[w].NotifyEfd, (IntPtr)(&inc), 8); - continue; // continue draining accepts in this epoll tick - } - - int err = Marshal.GetLastPInvokeError(); - if (err == EINTR) continue; // retry - if (err is EAGAIN or EWOULDBLOCK) break; // no more to accept right now - // On transient or unexpected accept error, break to next epoll tick. - break; - } - } - } - } - - // Pick the worker with the lowest Current count. - private static int ChooseLeastBusy(Worker[] workers) - { - int best = 0; int bestLoad = int.MaxValue; - for (int i = 0; i < workers.Length; i++) - { - int load = Volatile.Read(ref workers[i].Current); - if (load < bestLoad) { bestLoad = load; best = i; } - } - return best; - } - - private static readonly ObjectPool ConnectionPool = - new DefaultObjectPool(new ConnectionPoolPolicy(), 1024*32); - - private class ConnectionPoolPolicy : PooledObjectPolicy - { - public override Connection Create() => new(MaxNumberConnectionsPerWorker, InSlabSize, OutSlabSize); - public override bool Return(Connection context) - { - // Potentially reset buffers here. - return true; - } - } - - // ===== Worker loop ===== - private static void WorkerLoop(Worker W) - { - // Per-worker connection table - var conns = new Dictionary(capacity: MaxNumberConnectionsPerWorker); - - for (;;) - { - // Wait for I/O events or a wakeup from the acceptor (via NotifyEfd) - int n = epoll_wait(W.Ep, W.EventsBuf, W.MaxEvents, -1); - if (n < 0) { if (Marshal.GetLastPInvokeError() == EINTR) continue; throw new Exception("epoll_wait worker"); } - - for (int i = 0; i < n; i++) - { - ReadEpollEvent((byte*)W.EventsBuf + i * EvSize, out uint evs, out int fd); - - // 1) Notification path: new fds from acceptor via eventfd - if (fd == W.NotifyEfd) - { - // Drain the eventfd counter (consume all 64-bit values written). - ulong tmp; - while (read(W.NotifyEfd, (IntPtr)(&tmp), 8) > 0) { } - // Pull all pending client fds from the inbox and register with epoll - while (W.Inbox.TryDequeue(out int cfd)) - { - // We care about readable input and remote half-close; errors/hups too. - byte* ev = stackalloc byte[EvSize]; - WriteEpollEvent(ev, EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP, cfd); - epoll_ctl(W.Ep, EPOLL_CTL_ADD, cfd, (IntPtr)ev); - - // Adding a new connection to the pool, setting the file descriptor for the client socket - // and the byte* pointing to the stack allocated write buffer segment - conns[cfd] = ConnectionPool.Get(); - } - continue; - } - - // 2) Early close on error/hup conditions - if ((evs & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) != 0) - { - CloseConn(fd, conns, W); - continue; - } - - // 3) Read-ready path - if ((evs & EPOLLIN) != 0) - { - if (!conns.TryGetValue(fd, out var c)) { CloseQuiet(fd, conns); continue; } - - // Ensure free space at the tail; compact or grow if necessary. - // TODO: This logic needs rework, doesn't sense - //int avail = c.Buf.Length - c.Tail; - int avail = InSlabSize - c.Tail; - - // If the receiving buffer has no space available, that means that either there are - // a lot of requests to be served, or a huge request is being received and is larger than the array size. - // We cannot resize the buffer for now, it is costly, implement it in the future - if (avail == 0) - { - throw new NotImplementedException("No available space in the receiving buffer"); - - // Check if there are requests available to be handled - // TODO: Implement this - - // If not, resize the receiving buffer (very difficult if dealing with stack allocated buffer) - } - - // Read as much as possible until EAGAIN or buffer is full. - // Read until EAGAIN (socket drained) - while (true) - { - long got; - //fixed (byte* p = &c.Buf[c.Tail]) - // got = recv(fd, (IntPtr)p, (ulong)avail, 0); - got = recv(fd, c.ReceiveBuffer, (ulong)avail, 0); - - if (got > 0) - { - c.Tail += (int)got; - continue; - - } - if (got == 0) { CloseConn(fd, conns, W); break; } // peer closed - - int err = Marshal.GetLastPInvokeError(); - if (err is EAGAIN or EWOULDBLOCK) - { - // Nothing more to read (drained) - // Try to parse for complete requests - // TODO: Possibility of after handling all requests in buffer, try to read more data in this same loop? - // TODO: That could be an issue because this could give more airtime to a specific fd - // TODO: We want to release the loop to move into another fd's? - break; - } - if (err is ECONNRESET or ECONNABORTED or EPIPE) { CloseConn(fd, conns, W); break; } - CloseConn(fd, conns, W); break; // default: close on unexpected errors - } - - var dataToBeFlushedAvailable = TryParseRequests(c); - if (dataToBeFlushedAvailable) - { - var tryEmptyResult = TryEmptyWriteBuffer(c, ref fd); - if (tryEmptyResult == EmptyAttemptResult.Complete) - { - // All requests were flushed, stay EPOLLIN - // Move on to the next event - continue; - } - if (tryEmptyResult == EmptyAttemptResult.Incomplete) - { - // There is still data to be flushed in the buffer, arm EPOLLOUT - ArmEpollOut(ref fd, W.Ep); - continue; - } - if (tryEmptyResult == EmptyAttemptResult.CloseConnection) - { - CloseConn(fd, conns, W); - continue; - } - } - - // Move on to the next event... - continue; - } - - // 4) Write-ready path - if ((evs & EPOLLOUT) != 0) - { - if (!conns.TryGetValue(fd, out var c)) { CloseQuiet(fd, conns); continue; } - - var tryEmptyResult = TryEmptyWriteBuffer(c, ref fd); - if (tryEmptyResult == EmptyAttemptResult.Complete) - { - // All requests were flushed, arm EPOLLIN - // Move on to the next event - ArmEpollIn(ref fd, W.Ep); - continue; - } - if (tryEmptyResult == EmptyAttemptResult.Incomplete) - { - // There is still data to be flushed in the buffer, stay EPOLLOUT - continue; - } - if (tryEmptyResult == EmptyAttemptResult.CloseConnection) - { - CloseConn(fd, conns, W); - continue; - } - } - } - } - } - - private static void ArmEpollIn(ref int fd, int ep) - { - byte* ev = stackalloc byte[EvSize]; - WriteEpollEvent(ev, EPOLLIN | EPOLLRDHUP | EPOLLERR | EPOLLHUP, fd); - epoll_ctl(ep, EPOLL_CTL_MOD, fd, (IntPtr)ev); - } - - private static void ArmEpollOut(ref int fd, int ep) - { - byte* ev = stackalloc byte[EvSize]; - WriteEpollEvent(ev, EPOLLOUT | EPOLLRDHUP | EPOLLERR | EPOLLHUP, fd); - epoll_ctl(ep, EPOLL_CTL_MOD, fd, (IntPtr)ev); - } - - private static bool TryParseRequests(Connection connection) - { - bool hasDataToFlush = false; - - while (true) - { - // Try getting a full request header, if unsuccessful signal caller more data is needed - //int idx = FindCrlfCrlf(connection.Buf, connection.Head, connection.Tail); - int idx = FindCrlfCrlf(connection.ReceiveBuffer, connection.Head, connection.Tail); - if (idx < 0) - break; - connection.Head = idx + 4; // advance past CRLFCRLF - - // A full request was received, handle it! - CommitResponse(connection); - - hasDataToFlush = true; - } - - // If there is unprocessed data in the receiving buffer (incomplete request) which is not at buffer start - // Move the incomplete request to the buffer start and reset head and tail to 0 - if (connection.Head > 0 && connection.Head < connection.Tail) - { - Buffer.MemoryCopy( - connection.ReceiveBuffer + connection.Head, - connection.ReceiveBuffer, - InSlabSize, - connection.Tail - connection.Head); - } - - //Reset the receiving buffer - connection.Head = connection.Tail = 0; - return hasDataToFlush; - } - - private enum EmptyAttemptResult { Complete, Incomplete, CloseConnection } - - private static EmptyAttemptResult TryEmptyWriteBuffer(Connection connection, ref int fd) - { - while (true) - { - byte* headPointer = connection.WriteBuffer.Ptr + connection.WriteBuffer.Head; - long remaining = (long)(connection.WriteBuffer.Tail - connection.WriteBuffer.Head); - - // TODO: Check if send can return negative values - long n = send(fd, headPointer, remaining, MSG_NOSIGNAL); - - if (n > 0) - { - // Check if all data was sent - if (n == remaining) - { - // Remaining data was flushed/sent - - // Reset Writing buffer, point to the start - connection.WriteBuffer.Reset(); - - // If all data was sent, signal caller - return EmptyAttemptResult.Complete; - } - - // At least some data was flushed - // Update the Head - connection.WriteBuffer.Head += (int)n; - - // Some data was flushed but not all, try again (possibly until EAGAIN) - continue; - } - - // Wasn't able to flush, why? - int err = (n == 0) ? EAGAIN : Marshal.GetLastPInvokeError(); - if (err == EAGAIN) - { - // Notify the caller that we must keep trying to flush the data (arm EPOLLOUT) - return EmptyAttemptResult.Incomplete; - } - - // Other error, signal caller to close the connection - return EmptyAttemptResult.CloseConnection; - } - } - - private static void CommitResponse(Connection connection) - { - //TODO: Parse route - CommitPlainTextResponse(connection); - //CommitJsonResponse(connection); - } - - [ThreadStatic] private static Utf8JsonWriter? t_utf8JsonWriter; - private static readonly JsonContext SerializerContext = JsonContext.Default; - private static void CommitJsonResponse(Connection connection) - { - connection.WriteBuffer.Write("HTTP/1.1 200 OK\r\n"u8 + - "Server: W\r\n"u8 + - "Content-Type: application/json; charset=UTF-8\r\n"u8 + - "Content-Length: 27\r\n"u8); - connection.WriteBuffer.Write(DateHelper.HeaderBytes); - - t_utf8JsonWriter ??= new Utf8JsonWriter(connection.WriteBuffer, new JsonWriterOptions { SkipValidation = true }); - t_utf8JsonWriter.Reset(connection.WriteBuffer); - - var message = new JsonMessage { Message = "Hello, World!" }; - JsonSerializer.Serialize(t_utf8JsonWriter, message, SerializerContext.JsonMessage); - } - - private static void CommitPlainTextResponse(Connection connection) - { - connection.WriteBuffer.WriteUnmanaged("HTTP/1.1 200 OK\r\n"u8 + - "Server: W\r\n"u8 + - "Content-Type: text/plain\r\n"u8 + - "Content-Length: 13\r\n"u8); - connection.WriteBuffer.WriteUnmanaged(DateHelper.HeaderBytes); - connection.WriteBuffer.Write("Hello, World!"u8); - } - - // ===== Close helpers ===== - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CloseConn(int fd, Dictionary map, Worker W) - { - // Remove from map, close fd, and decrement the worker's load counter. - ConnectionPool.Return(map[fd]); - map.Remove(fd); - CloseQuiet(fd, map); - Interlocked.Decrement(ref W.Current); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void CloseQuiet(int fd, Dictionary map) { try { close(fd); } catch { } } -} -*/ \ No newline at end of file diff --git a/Unhinged/SerializableObjects/JsonMessage.cs b/Unhinged/SerializableObjects/JsonMessage.cs index 47e321d..d2c8317 100644 --- a/Unhinged/SerializableObjects/JsonMessage.cs +++ b/Unhinged/SerializableObjects/JsonMessage.cs @@ -11,4 +11,4 @@ public struct JsonMessage { public string Message { get; set; } } [JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization | JsonSourceGenerationMode.Metadata)] [JsonSerializable(typeof(JsonMessage))] -internal partial class JsonContext : JsonSerializerContext { } \ No newline at end of file +public partial class JsonContext : JsonSerializerContext { } \ No newline at end of file diff --git a/Unhinged/Unhinged.csproj b/Unhinged/Unhinged.csproj index 65935b5..c7487ef 100644 --- a/Unhinged/Unhinged.csproj +++ b/Unhinged/Unhinged.csproj @@ -1,26 +1,28 @@  - Exe net9.0 enable enable true - true - true - true - - linux-x64 - true + Unhinged + Diogo Martins + https://github.com/MDA2AV/Unhinged + git + 9.0.0 + 9.0.0 + 9.0.0 + README.md + Unhinged + LICENSE + https://github.com/MDA2AV/Unhinged + https://github.com/MDA2AV/Unhinged + - - - - - + diff --git a/Unhinged/Utilities/DateHelper.cs b/Unhinged/Utilities/DateHelper.cs index 8d9a4f4..73a64a8 100644 --- a/Unhinged/Utilities/DateHelper.cs +++ b/Unhinged/Utilities/DateHelper.cs @@ -3,6 +3,7 @@ // (var is avoided intentionally in this project so that concrete types are visible at call sites.) // ReSharper disable always StackAllocInsideLoop // ReSharper disable always ClassCannotBeInstantiated + #pragma warning disable CA2014 namespace Unhinged; diff --git a/Unhinged/Utilities/HashUtils.cs b/Unhinged/Utilities/HashUtils.cs index 41e15bf..7159764 100644 --- a/Unhinged/Utilities/HashUtils.cs +++ b/Unhinged/Utilities/HashUtils.cs @@ -3,6 +3,7 @@ // (var is avoided intentionally in this project so that concrete types are visible at call sites.) // ReSharper disable always StackAllocInsideLoop // ReSharper disable always ClassCannotBeInstantiated + #pragma warning disable CA2014 namespace Unhinged; diff --git a/Unhinged/Writers/FixedBufferWriter.cs b/Unhinged/Writers/FixedBufferWriter.cs index 9a525e8..9d62499 100644 --- a/Unhinged/Writers/FixedBufferWriter.cs +++ b/Unhinged/Writers/FixedBufferWriter.cs @@ -5,8 +5,6 @@ // ReSharper disable always ClassCannotBeInstantiated #pragma warning disable CA2014 -using System.Buffers; - namespace Unhinged; /// diff --git a/Unhinged/_usings.cs b/Unhinged/_usings.cs index d4db1be..46d86cc 100644 --- a/Unhinged/_usings.cs +++ b/Unhinged/_usings.cs @@ -1,3 +1,4 @@ +global using System; global using System.Buffers; global using System.Buffers.Binary; global using System.Buffers.Text;