dotnet / dotnet-wasi-sdk

An SDK for building .NET projects as standalone WASI-compliant modules
MIT License
279 stars 12 forks source link

Wasi.AspNetCore.BundledFiles.WasiBundledFileProvider.Watch is not implemented #4

Open Maetis opened 1 year ago

Maetis commented 1 year ago

Description

I tried to use the Wasi.AspNetCore.BundledFiles package in order to bundle the static files of my web, and I got an error about a not implemented method.

Reproduction Steps

$ dotnet --version
7.0.100-rc.2.22477.23 (Linux x64)

$ dotnet new webapp -o MyWebApp
$ cd MyWebApp
$ dotnet add package Wasi.Sdk --prerelease
$ dotnet add package Wasi.AspNetCore.Server.Native --prerelease
$ dotnet add package Wasi.AspNetCore.BundledFiles --prerelease

And follow steps in How to use: ASP.NET Core applications for code modifications.

$ dotnet run -c Release

Expected behavior

Web app work!

Actual behavior

Building...
info: Microsoft.Hosting.Lifetime
      Now listening on: http://localhost:8080
info: Microsoft.AspNetCore.Hosting.Diagnostics
      Request starting HTTP/1.1 GET http://localhost:8080/ - -
warn: Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware
      Failed to determine the https port for redirect.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware
      Executing endpoint '/Index'
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker
      Route matched with {page = "/Index"}. Executing page /Index
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker
      Executing handler method WASMWebApp.Pages.IndexModel.OnGet - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker
      Executed handler method OnGet, returned result .
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker
      Executing an implicit handler method - ModelState is Valid
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker
      Executed an implicit handler method, returned result Microsoft.AspNetCore.Mvc.RazorPages.PageResult.
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker
      Executed page /Index in 239.386ms
info: Microsoft.AspNetCore.Routing.EndpointMiddleware
      Executed endpoint '/Index'
erro: Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware
      An unhandled exception has occurred while executing the request.
System.NotImplementedException: The method or operation is not implemented.
   at Wasi.AspNetCore.BundledFiles.WasiBundledFileProvider.Watch(String filter)
   at Microsoft.AspNetCore.Mvc.Razor.Infrastructure.DefaultFileVersionProvider.AddFileVersionToPath(PathString requestPathBase, String path)
   at Microsoft.AspNetCore.Mvc.TagHelpers.LinkTagHelper.Process(TagHelperContext context, TagHelperOutput output)
   at Microsoft.AspNetCore.Razor.TagHelpers.TagHelper.ProcessAsync(TagHelperContext context, TagHelperOutput output)
   at Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner.RunAsync(TagHelperExecutionContext executionContext)
   at WASMWebApp.Pages.Shared.Pages_Shared__Layout.<ExecuteAsync>b__24_0()
   at Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperExecutionContext.SetOutputContentAsync()
   at WASMWebApp.Pages.Shared.Pages_Shared__Layout.ExecuteAsync()
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageCoreAsync(IRazorPage page, ViewContext context)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderPageAsync(IRazorPage page, ViewContext context, Boolean invokeViewStarts)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderLayoutAsync(ViewContext context, ViewBufferTextWriter bodyWriter)
   at Microsoft.AspNetCore.Mvc.Razor.RazorView.RenderAsync(ViewContext context)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
   at Microsoft.AspNetCore.Mvc.ViewFeatures.ViewExecutor.ExecuteAsync(ViewContext viewContext, String contentType, Nullable`1 statusCode)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<<InvokeNextResultFilterAsync>g__Awaited|30_0>d`2[[Microsoft.AspNetCore.Mvc.Filters.IResultFilter, Microsoft.AspNetCore.Mvc.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60],[Microsoft.AspNetCore.Mvc.Filters.IAsyncResultFilter, Microsoft.AspNetCore.Mvc.Abstractions, Version=7.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60]].MoveNext()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[IResultFilter,IAsyncResultFilter](State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.<Invoke>g__Awaited|8_0(ExceptionHandlerMiddlewareImpl middleware, HttpContext context, Task task)

Configuration

Please provide more information on your .NET configuration:

Which version of .NET is the code running on? 7.0.100-rc.2.22477.23 What OS and version, and what distro if applicable? Ubuntu 22.04 LTS (WSL) What is the architecture (x64, x86, ARM, ARM64)? x64 WASM runner? wasmtime-cli 2.0.0

raffaeler commented 1 year ago

I found a simple workaround:

  1. Add the following class to your project

Do not rename the namespace or class otherwise the Mono interop will not work

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;

using System.Collections;
using System.Runtime.CompilerServices;

namespace Wasi.AspNetCore.BundledFiles;

// Do NOT change the namespace or type name
// Wasi.AspNetCore.BundledFiles.WasiBundledFileProvider
// Otherwise Mono will not find the interop call (GetEmbeddedFile)
public class WasiBundledFileProvider : IFileProvider
{
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern unsafe byte* GetEmbeddedFile(string name, out int length);

    private readonly static DateTime FakeLastModified = new DateTime(2000, 1, 1);

    public IDirectoryContents GetDirectoryContents(string subpath)
    {
        return new BundledDirectoryContents(this, subpath);
    }

    public unsafe IFileInfo GetFileInfo(string subpath)
    {
        var subpathWithoutLeadingSlash = subpath.AsSpan(1);
        var fileBytes = GetEmbeddedFile($"wwwroot/{subpathWithoutLeadingSlash}", out var length);
        return fileBytes == null
            ? new NotFoundFileInfo(subpath)
            : new BundledFileInfo(subpath, length, FakeLastModified, fileBytes);
    }

    public IChangeToken Watch(string filter)
    {
        return NullChangeToken.Singleton;
    }

    unsafe class BundledFileInfo : IFileInfo
    {
        private byte* _fileBytes;

        public BundledFileInfo(string name, long length, DateTime lastModified, byte* fileBytes)
        {
            Name = name;
            LastModified = lastModified;
            Length = length;
            _fileBytes = fileBytes;
        }

        public bool Exists => true;

        public bool IsDirectory => false;

        public DateTimeOffset LastModified { get; }

        public long Length { get; }

        public string Name { get; }

        public string? PhysicalPath => null;

        public Stream CreateReadStream()
            => new UnmanagedMemoryStream(_fileBytes, Length);
    }

    class BundledDirectoryContents : IDirectoryContents
    {
        private readonly IFileProvider _owner;
        private readonly string _subpath;

        public BundledDirectoryContents(IFileProvider owner, string subpath)
        {
            _owner = owner;
            _subpath = subpath;
        }

        public bool Exists => _subpath == "/";

        IEnumerator IEnumerable.GetEnumerator()
            => GetEnumerator();

        public IEnumerator<IFileInfo> GetEnumerator()
        {
            // TODO: Mechanism for enumerating everything in a bundled directory
            // Currently this only recognizes index.html files to support UseDefaultFiles
            if (_subpath == "/")
            {
                var fileInfo = _owner.GetFileInfo("/index.html");
                if (fileInfo.Exists)
                {
                    yield return fileInfo;
                }
            }
        }
    }
}

This is obviously the culprit:

public IChangeToken Watch(string filter)
{
    return NullChangeToken.Singleton;
}
  1. Configure the StaticFileOptions either using UseStaticFiles or UseBundledStaticFiles:
app.UseStaticFiles(new StaticFileOptions()
{
    FileProvider = new WasiBundledFileProvider()
});