diff --git a/frameworks/CSharp/appmpower/appmpower-odbc-my.dockerfile b/frameworks/CSharp/appmpower/appmpower-odbc-my.dockerfile index a5bbf22ae6f..9df17020863 100644 --- a/frameworks/CSharp/appmpower/appmpower-odbc-my.dockerfile +++ b/frameworks/CSharp/appmpower/appmpower-odbc-my.dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0.100 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.100 AS build RUN apt-get update RUN apt-get -yqq install clang zlib1g-dev RUN apt-get update @@ -8,12 +8,12 @@ COPY src . RUN dotnet publish -c Release -o out /p:Database=mysql # Construct the actual image that will run -FROM mcr.microsoft.com/dotnet/aspnet:8.0.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0 AS runtime RUN apt-get update # The following installs standard versions unixodbc and pgsqlodbc # unixodbc still needs to be installed even if compiled locally -RUN apt-get install -y unixodbc wget curl +RUN apt-get install -y unixodbc-dev unixodbc wget curl RUN apt-get update WORKDIR /odbc @@ -45,6 +45,8 @@ WORKDIR /app COPY --from=build /app/out ./ RUN cp /usr/lib/libm* /app +#RUN cp /usr/lib/aarch64-linux-gnu/libodbc* /app +RUN cp /usr/lib/x86_64-linux-gnu/libodbc* /app EXPOSE 8080 diff --git a/frameworks/CSharp/appmpower/appmpower-odbc-pg.dockerfile b/frameworks/CSharp/appmpower/appmpower-odbc-pg.dockerfile index 4080684bab6..ebeab3b6186 100644 --- a/frameworks/CSharp/appmpower/appmpower-odbc-pg.dockerfile +++ b/frameworks/CSharp/appmpower/appmpower-odbc-pg.dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0.100 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.100 AS build RUN apt-get update RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5 @@ -7,10 +7,10 @@ COPY src . RUN dotnet publish -c Release -o out /p:Database=postgresql # Construct the actual image that will run -FROM mcr.microsoft.com/dotnet/aspnet:8.0.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0 AS runtime RUN apt-get update -RUN apt-get install -y unixodbc odbc-postgresql +RUN apt-get install -y unixodbc-dev unixodbc odbc-postgresql # unixodbc still needs to be installed even if compiled locally ENV PATH=/usr/local/unixODBC/bin:$PATH @@ -27,6 +27,10 @@ ENV ASPNETCORE_URLS http://+:8080 WORKDIR /app COPY --from=build /app/out ./ +#RUN cp /usr/lib/aarch64-linux-gnu/libodbc* /app +RUN cp /usr/lib/x86_64-linux-gnu/libodbc* /app + + EXPOSE 8080 ENTRYPOINT ["./appMpower"] \ No newline at end of file diff --git a/frameworks/CSharp/appmpower/appmpower.dockerfile b/frameworks/CSharp/appmpower/appmpower.dockerfile index 3d521c490d8..d10a8ad3c2e 100644 --- a/frameworks/CSharp/appmpower/appmpower.dockerfile +++ b/frameworks/CSharp/appmpower/appmpower.dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0.100 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0.100 AS build RUN apt-get update RUN apt-get -yqq install clang zlib1g-dev libkrb5-dev libtinfo5 @@ -8,7 +8,7 @@ COPY src . RUN dotnet publish -c Release -o out # Construct the actual image that will run -FROM mcr.microsoft.com/dotnet/aspnet:8.0.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:9.0.0 AS runtime # Full PGO ENV DOTNET_TieredPGO 1 ENV DOTNET_TC_QuickJitForLoops 1 diff --git a/frameworks/CSharp/appmpower/src/appMpower.Orm/NativeMethods.cs b/frameworks/CSharp/appmpower/src/appMpower.Orm/NativeMethods.cs index 64337e6269e..6c20aded37a 100644 --- a/frameworks/CSharp/appmpower/src/appMpower.Orm/NativeMethods.cs +++ b/frameworks/CSharp/appmpower/src/appMpower.Orm/NativeMethods.cs @@ -17,6 +17,9 @@ public static class NativeMethods private readonly static WorldSerializer _worldSerializer = new WorldSerializer(); private readonly static WorldsSerializer _worldsSerializer = new WorldsSerializer(); + private readonly static FortunesSerializer _fortunesSerializer = new FortunesSerializer(); + private static readonly byte[] _delimiter = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; + [UnmanagedCallersOnly(EntryPoint = "Dbms")] public static void Dbms(int dbms) @@ -66,6 +69,7 @@ public static unsafe IntPtr Db(int* length, IntPtr* handlePointer) */ } + /* [UnmanagedCallersOnly(EntryPoint = "Fortunes")] public static unsafe IntPtr Fortunes(int* length, IntPtr* handlePointer) { @@ -81,6 +85,61 @@ public static unsafe IntPtr Fortunes(int* length, IntPtr* handlePointer) return byteArrayPointer; } + */ + + [UnmanagedCallersOnly(EntryPoint = "Fortunes")] + public static unsafe IntPtr Fortunes(int* length, IntPtr* handlePointer) + { + List fortunes = RawDb.LoadFortunesRows().GetAwaiter().GetResult(); + + int totalSize = 0; + + foreach (var fortune in fortunes) + { + totalSize += sizeof(int) // for Id + + Encoding.UTF8.GetByteCount(fortune.Message ?? "") // for Message + + _delimiter.Length; // for delimiter + } + + // Allocate the total buffer + byte[] buffer = new byte[totalSize]; + int offset = 0; + + // Write each object to the buffer + foreach (var fortune in fortunes) + { + // Write Id + BitConverter.TryWriteBytes(buffer.AsSpan(offset, sizeof(int)), fortune.Id); + offset += sizeof(int); + + // Write Message + int descriptionLength = Encoding.UTF8.GetBytes(fortune.Message ?? "", buffer.AsSpan(offset)); + offset += descriptionLength; + + // Write Delimiter + _delimiter.CopyTo(buffer, offset); + offset += _delimiter.Length; + } + + byte[] byteArray = buffer.ToArray(); + *length = byteArray.Length; + + /* + var memoryStream = new MemoryStream(); + using var utf8JsonWriter = new Utf8JsonWriter(memoryStream, _jsonWriterOptions); + + _fortunesSerializer.Serialize(utf8JsonWriter, fortunes); + + byte[] byteArray = memoryStream.ToArray(); + *length = (int)utf8JsonWriter.BytesCommitted; + */ + + GCHandle handle = GCHandle.Alloc(byteArray, GCHandleType.Pinned); + IntPtr byteArrayPointer = handle.AddrOfPinnedObject(); + *handlePointer = GCHandle.ToIntPtr(handle); + + return byteArrayPointer; + } [UnmanagedCallersOnly(EntryPoint = "Query")] public static unsafe IntPtr Query(int queries, int* length, IntPtr* handlePointer) diff --git a/frameworks/CSharp/appmpower/src/appMpower.Orm/Serializers/FortunesSerializer.cs b/frameworks/CSharp/appmpower/src/appMpower.Orm/Serializers/FortunesSerializer.cs new file mode 100644 index 00000000000..d1697c0c0cc --- /dev/null +++ b/frameworks/CSharp/appmpower/src/appMpower.Orm/Serializers/FortunesSerializer.cs @@ -0,0 +1,24 @@ +using System.Text.Json; +using appMpower.Orm.Objects; + +namespace appMpower.Orm.Serializers +{ + public class FortunesSerializer : IJsonSerializer> + { + public void Serialize(Utf8JsonWriter utf8JsonWriter, List fortunes) + { + utf8JsonWriter.WriteStartArray(); + + foreach (Fortune fortune in fortunes) + { + utf8JsonWriter.WriteStartObject(); + utf8JsonWriter.WriteNumber("id", fortune.Id); + utf8JsonWriter.WriteString("message", fortune.Message); + utf8JsonWriter.WriteEndObject(); + } + + utf8JsonWriter.WriteEndArray(); + utf8JsonWriter.Flush(); + } + } +} \ No newline at end of file diff --git a/frameworks/CSharp/appmpower/src/appMpower.Orm/appMpower.Orm.csproj b/frameworks/CSharp/appmpower/src/appMpower.Orm/appMpower.Orm.csproj index dd9c929f19f..858f6f66ae4 100644 --- a/frameworks/CSharp/appmpower/src/appMpower.Orm/appMpower.Orm.csproj +++ b/frameworks/CSharp/appmpower/src/appMpower.Orm/appMpower.Orm.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable true @@ -36,7 +36,7 @@ - + diff --git a/frameworks/CSharp/appmpower/src/appMpower/Middleware/FortunesMiddleware.cs b/frameworks/CSharp/appmpower/src/appMpower/Middleware/FortunesMiddleware.cs index 2acf9af1716..6321d515887 100644 --- a/frameworks/CSharp/appmpower/src/appMpower/Middleware/FortunesMiddleware.cs +++ b/frameworks/CSharp/appmpower/src/appMpower/Middleware/FortunesMiddleware.cs @@ -2,7 +2,11 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; using System.Threading.Tasks; +using appMpower.Objects; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -11,11 +15,21 @@ namespace appMpower; public class FortunesMiddleware { + static readonly HtmlEncoder htmlEncoder = CreateHtmlEncoder(); + static HtmlEncoder CreateHtmlEncoder() + { + var settings = new TextEncoderSettings(UnicodeRanges.BasicLatin, UnicodeRanges.Katakana, UnicodeRanges.Hiragana); + settings.AllowCharacter('\u2014'); // allow EM DASH through + return HtmlEncoder.Create(settings); + } + private readonly static KeyValuePair _headerServer = new KeyValuePair("Server", new StringValues("k")); private readonly static KeyValuePair _headerContentType = new KeyValuePair("Content-Type", new StringValues("text/html; charset=UTF-8")); + private static readonly byte[] _delimiter = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; + private readonly RequestDelegate _next; public FortunesMiddleware(RequestDelegate next) @@ -27,6 +41,66 @@ public unsafe Task Invoke(HttpContext httpContext) { if (httpContext.Request.Path.StartsWithSegments("/fortunes", StringComparison.Ordinal)) { + int payloadLength; + IntPtr handlePointer; + + IntPtr bytePointer = NativeMethods.Fortunes(out payloadLength, out handlePointer); + + /* + byte[] json = new byte[payloadLength]; + Marshal.Copy(bytePointer, json, 0, payloadLength); + NativeMethods.FreeHandlePointer(handlePointer); + + string s = Encoding.UTF8.GetString(json, 0, json.Length); + + var options = new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }; + + List fortunes = JsonSerializer.Deserialize>(s, options); + + var response = httpContext.Response; + response.Headers.Add(_headerServer); + + var result = Results.Extensions.RazorSlice>(fortunes); + result.HtmlEncoder = htmlEncoder; + + return result.ExecuteAsync(httpContext); + */ + + byte[] byteArray = new byte[payloadLength]; + Marshal.Copy(bytePointer, byteArray, 0, payloadLength); + + List fortunes = new List(); + + // Convert the byte array into segments split by the delimiter + int delimiterLength = _delimiter.Length; + int start = 0; + int index; + + while ((index = FindDelimiterIndex(byteArray, _delimiter, start)) >= 0) + { + // Use a span over the segment of bytes for the current object + var objectDataSpan = new ReadOnlySpan(byteArray, start, index - start); + Fortune fortune = ConvertBytesToObject(objectDataSpan); + fortunes.Add(fortune); + + // Move past the delimiter + start = index + delimiterLength; + } + + NativeMethods.FreeHandlePointer(handlePointer); + + var response = httpContext.Response; + response.Headers.Add(_headerServer); + + var result = Results.Extensions.RazorSlice>(fortunes); + result.HtmlEncoder = htmlEncoder; + + return result.ExecuteAsync(httpContext); + + /* var response = httpContext.Response; response.Headers.Add(_headerServer); response.Headers.Add(_headerContentType); @@ -43,10 +117,51 @@ public unsafe Task Invoke(HttpContext httpContext) new KeyValuePair("Content-Length", payloadLength.ToString())); return response.Body.WriteAsync(json, 0, payloadLength); + */ } return _next(httpContext); } + + private static int FindDelimiterIndex(byte[] array, byte[] delimiter, int startIndex) + { + int endIndex = array.Length - delimiter.Length; + + for (int i = startIndex; i <= endIndex; i++) + { + bool isMatch = true; + + for (int j = 0; j < delimiter.Length; j++) + { + if (array[i + j] != delimiter[j]) + { + isMatch = false; + break; + } + } + + if (isMatch) + { + return i; + } + } + + return -1; + } + + private static Fortune ConvertBytesToObject(ReadOnlySpan data) + { + int offset = 0; + + // Read Id + int id = BitConverter.ToInt32(data.Slice(offset, sizeof(int))); + offset += sizeof(int); + + // Read Message (remaining bytes in the span) + string message = Encoding.UTF8.GetString(data.Slice(offset)); + + return new Fortune(id, message); + } } public static class FortunesMiddlewareExtensions diff --git a/frameworks/CSharp/appmpower/src/appMpower/Objects/Fortune.cs b/frameworks/CSharp/appmpower/src/appMpower/Objects/Fortune.cs new file mode 100644 index 00000000000..187a3c06980 --- /dev/null +++ b/frameworks/CSharp/appmpower/src/appMpower/Objects/Fortune.cs @@ -0,0 +1,22 @@ +using System; + +namespace appMpower.Objects +{ + public struct Fortune : IComparable, IComparable + { + public Fortune(int id, string message) + { + Id = id; + Message = message; + } + + public int Id { get; set; } + + public string Message { get; set; } + + public int CompareTo(object obj) => throw new InvalidOperationException("The non-generic CompareTo should not be used"); + + // Performance critical, using culture insensitive comparison + public int CompareTo(Fortune other) => string.CompareOrdinal(Message, other.Message); + } +} \ No newline at end of file diff --git a/frameworks/CSharp/appmpower/src/appMpower/Slices/Fortunes.cshtml b/frameworks/CSharp/appmpower/src/appMpower/Slices/Fortunes.cshtml new file mode 100644 index 00000000000..ebf2c38d5ec --- /dev/null +++ b/frameworks/CSharp/appmpower/src/appMpower/Slices/Fortunes.cshtml @@ -0,0 +1,2 @@ +@inherits RazorSliceHttpResult> +Fortunes@foreach (var item in Model){}
idmessage
@WriteNumber(item.Id, default, CultureInfo.InvariantCulture, false)@item.Message
\ No newline at end of file diff --git a/frameworks/CSharp/appmpower/src/appMpower/Slices/_ViewImports.cshtml b/frameworks/CSharp/appmpower/src/appMpower/Slices/_ViewImports.cshtml new file mode 100644 index 00000000000..98a692ef65a --- /dev/null +++ b/frameworks/CSharp/appmpower/src/appMpower/Slices/_ViewImports.cshtml @@ -0,0 +1,10 @@ +@inherits RazorSliceHttpResult + +@using System.Globalization; +@using Microsoft.AspNetCore.Razor; +@using Microsoft.AspNetCore.Http.HttpResults; +@using RazorSlices; +@using appMpower.Objects; + +@tagHelperPrefix __disable_tagHelpers__: +@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor \ No newline at end of file diff --git a/frameworks/CSharp/appmpower/src/appMpower/appMpower.csproj b/frameworks/CSharp/appmpower/src/appMpower/appMpower.csproj index 5661d01ce61..ba6fe6f7059 100644 --- a/frameworks/CSharp/appmpower/src/appMpower/appMpower.csproj +++ b/frameworks/CSharp/appmpower/src/appMpower/appMpower.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 Exe true @@ -24,6 +24,7 @@ +