diff --git a/MLS.Agent.Tests/ApiViaHttpTests.cs b/MLS.Agent.Tests/ApiViaHttpTests.cs index 1fc5364c1..42f1f097b 100644 --- a/MLS.Agent.Tests/ApiViaHttpTests.cs +++ b/MLS.Agent.Tests/ApiViaHttpTests.cs @@ -802,7 +802,27 @@ public async Task Can_serve_blazor_console_code_runner() result.Should().Contain("Loading..."); } } - + + [Fact] + public async Task Can_serve_from_webassembly_controller() + { + var (name, addSource) = await Create.NupkgWithBlazorEnabled(); + using (var agent = new AgentService(new StartupOptions(addPackageSource: new WorkspaceServer.PackageSource(addSource.FullName)))) + { + var response = await agent.GetAsync($@"/LocalCodeRunner/{name}"); + + response.EnsureSuccess(); + var result = await response.Content.ReadAsStringAsync(); + result.Should().Contain("Loading..."); + + response = await agent.GetAsync($@"/LocalCodeRunner/{name}/interop.js"); + + response.EnsureSuccess(); + result = await response.Content.ReadAsStringAsync(); + result.Should().Contain("DotNet.invokeMethodAsync"); + } + } + [Fact] public async Task Can_serve_nodatime_code_runner() { diff --git a/MLS.Agent/Controllers/EmbeddableController.cs b/MLS.Agent/Controllers/EmbeddableController.cs index 9dfaef94b..b3425614a 100644 --- a/MLS.Agent/Controllers/EmbeddableController.cs +++ b/MLS.Agent/Controllers/EmbeddableController.cs @@ -41,6 +41,8 @@ public IActionResult Html() private string GetClientParameters() { + // Uncomment to enable testing /ide without going through orchestrator + // var referrer = "http://localhost:4242"; var referrer = HttpContext.Request.Headers["referer"].ToString(); if (!string.IsNullOrWhiteSpace(referrer) && Uri.TryCreate(referrer, UriKind.Absolute, out var uri)) diff --git a/MLS.Agent/Controllers/WebAssemblyController.cs b/MLS.Agent/Controllers/WebAssemblyController.cs new file mode 100644 index 000000000..c1b59b121 --- /dev/null +++ b/MLS.Agent/Controllers/WebAssemblyController.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.DotNet.Try.Markdown; +using MLS.Agent.CommandLine; +using WorkspaceServer; +using WorkspaceServer.Packaging; + +namespace MLS.Agent.Controllers +{ + public class WebAssemblyController : Controller + { + private PackageRegistry _registry; + + public WebAssemblyController(PackageRegistry packageRegistry) + { + _registry = packageRegistry ?? throw new ArgumentNullException(nameof(packageRegistry)); + } + + [HttpGet] + [Route("/LocalCodeRunner/{packageName}/")] + [Route("/LocalCodeRunner/{packageName}/{*requestedPath}")] + public async Task GetFile(string packageName, string requestedPath = "Index.html") + { + var package = await _registry.Get(packageName); + var asset = package.Assets.OfType().FirstOrDefault(); + if (asset == null) + { + return NotFound(); + } + + var file = asset.DirectoryAccessor.GetFullyQualifiedPath(new RelativeFilePath(requestedPath)); + if (!file.Exists) + { + file = asset.DirectoryAccessor.GetFullyQualifiedFilePath("index.html"); + return await FileContents(file); + } + + return await FileContents(file); + } + + private async Task FileContents(FileSystemInfo file) + { + var contentType = GetContentType(file.FullName); + var bytes = await System.IO.File.ReadAllBytesAsync(file.FullName); + + return File(bytes, contentType); + } + + private string GetContentType(string path) + { + var extension = Path.GetExtension(path); + switch (extension) + { + case ".dll": + return "application/octet-stream"; + case ".json": + return "application/json"; + case ".wasm": + return "application/wasm"; + case ".woff": + return "application/font-woff"; + case ".woff2": + return "application/font-woff"; + case ".js": + return "application/javascript"; + default: + return "text/html"; + } + } + } +} \ No newline at end of file diff --git a/MLS.Agent/Startup.cs b/MLS.Agent/Startup.cs index ece32cbd2..c4d24c39e 100644 --- a/MLS.Agent/Startup.cs +++ b/MLS.Agent/Startup.cs @@ -176,7 +176,6 @@ public void Configure( app.UseDefaultFiles() .UseStaticFilesFromToolLocation() - .UseRouter(new StaticFilesProxyRouter()) .UseMvc(); operation.Succeed(); diff --git a/MLS.Agent/StaticFilesProxyRouter.cs b/MLS.Agent/StaticFilesProxyRouter.cs deleted file mode 100644 index bf0641f8d..000000000 --- a/MLS.Agent/StaticFilesProxyRouter.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) .NET Foundation and contributors. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Routing; - -namespace MLS.Agent -{ - public class StaticFilesProxyRouter : IRouter - { - // TODO: (StaticFilesProxyRouter) remove this class and move these resources into the agent repo so they can be served locally without need for an internet connection - private readonly HttpClient _httpClient = new HttpClient - { - BaseAddress = new Uri("http://localhost:27261/") - }; - - public async Task RouteAsync(RouteContext context) - { - - var path = context.HttpContext.Request.Path; - - if (path.Value.EndsWith(".js") || - path.Value.EndsWith(".css") || - path.Value.EndsWith(".png") || - path.Value.EndsWith(".ico")) - { - var response = await _httpClient.GetAsync(path.Value); - - if (response.IsSuccessStatusCode) - { - context.Handler = async httpContext => - { - var responseFromTryDotNet = await response.Content.ReadAsStreamAsync(); - - await responseFromTryDotNet.CopyToAsync(httpContext.Response.Body); - }; - } - } - } - - public VirtualPathData GetVirtualPath(VirtualPathContext context) - { - return null; - } - } -} \ No newline at end of file diff --git a/MLS.PackageTool/Program.cs b/MLS.PackageTool/Program.cs index 15c722df4..33313a92f 100644 --- a/MLS.PackageTool/Program.cs +++ b/MLS.PackageTool/Program.cs @@ -82,7 +82,7 @@ public static string BuildDirectoryLocation() => Path.Combine(Path.GetDirectoryName(AssemblyLocation()), "project", "build"); public static string WasmDirectoryLocation() => - Path.Combine(Path.GetDirectoryName(AssemblyLocation()), "project", "wasm"); + Path.Combine(Path.GetDirectoryName(AssemblyLocation()), "project", "wasm", "MLS.Blazor", "dist"); public static string AssemblyLocation() { diff --git a/WorkspaceServer.Tests/PrebuiltBlazorPackageLocatorTests.cs b/WorkspaceServer.Tests/PrebuiltBlazorPackageLocatorTests.cs index a6d67ea45..a84128d25 100644 --- a/WorkspaceServer.Tests/PrebuiltBlazorPackageLocatorTests.cs +++ b/WorkspaceServer.Tests/PrebuiltBlazorPackageLocatorTests.cs @@ -32,7 +32,7 @@ public async Task Discovers_built_blazor_package() var locator = new PrebuiltBlazorPackageLocator(); var asset = await locator.Locate(packageName); - asset.DirectoryAccessor.DirectoryExists("MLS.Blazor").Should().Be(true); + asset.DirectoryAccessor.FileExists("index.html").Should().Be(true); } public void Dispose() diff --git a/WorkspaceServer/PackageRegistry.cs b/WorkspaceServer/PackageRegistry.cs index f6b578540..6fa0d0717 100644 --- a/WorkspaceServer/PackageRegistry.cs +++ b/WorkspaceServer/PackageRegistry.cs @@ -22,6 +22,7 @@ public class PackageRegistry : private readonly ConcurrentDictionary> _packageBuilders = new ConcurrentDictionary>(); private readonly ConcurrentDictionary> _packages = new ConcurrentDictionary>(); + private readonly ConcurrentDictionary> _packages2 = new ConcurrentDictionary>(); private readonly List _strategies = new List(); public PackageRegistry( @@ -87,26 +88,29 @@ public async Task Get(string packageName, Budget budget = null) // FIX: (Get) move this into the cache var package = await GetPackage2(descriptor); - if (package == null) + if (package == null || !(package is T)) { package = await GetPackageFromPackageBuilder(packageName, budget, descriptor); } - return (T) package; + return (T)package; } - private async Task GetPackage2(PackageDescriptor descriptor) + private Task GetPackage2(PackageDescriptor descriptor) where T : class, IPackage { - foreach (var packgeFinder in _packageFinders) + return _packages2.GetOrAdd(descriptor, async descriptor2 => { - if (await packgeFinder.Find(descriptor) is T pkg) + foreach (var packgeFinder in _packageFinders) { - return pkg; + if (await packgeFinder.Find(descriptor) is T pkg) + { + return pkg; + } } - } - return default; + return default; + }); } private Task GetPackageFromPackageBuilder(string packageName, Budget budget, PackageDescriptor descriptor) diff --git a/WorkspaceServer/Packaging/PackageDescriptor.cs b/WorkspaceServer/Packaging/PackageDescriptor.cs index 54f3238d2..6f5ba6a6a 100644 --- a/WorkspaceServer/Packaging/PackageDescriptor.cs +++ b/WorkspaceServer/Packaging/PackageDescriptor.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; namespace WorkspaceServer.Packaging { @@ -27,6 +28,23 @@ public PackageDescriptor( internal bool IsPathSpecified { get; } + public override bool Equals(object obj) + { + return obj is PackageDescriptor descriptor && + Name == descriptor.Name && + Version == descriptor.Version && + IsPathSpecified == descriptor.IsPathSpecified; + } + + public override int GetHashCode() + { + var hashCode = -858720875; + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Name); + hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Version); + hashCode = hashCode * -1521134295 + IsPathSpecified.GetHashCode(); + return hashCode; + } + public override string ToString() => Name; } } \ No newline at end of file diff --git a/WorkspaceServer/Packaging/WebAssemblyAssetFinder.cs b/WorkspaceServer/Packaging/WebAssemblyAssetFinder.cs index f8fbddc0d..e71fd4f68 100644 --- a/WorkspaceServer/Packaging/WebAssemblyAssetFinder.cs +++ b/WorkspaceServer/Packaging/WebAssemblyAssetFinder.cs @@ -23,7 +23,7 @@ public WebAssemblyAssetFinder(DirectoryInfo workingDirectory, PackageSource addS _addSource = addSource; } - async Task IPackageFinder.Find(PackageDescriptor descriptor) + async Task IPackageFinder.Find(PackageDescriptor descriptor) { if (descriptor.IsPathSpecified) { @@ -34,10 +34,10 @@ async Task IPackageFinder.Find(Package if (candidate.Exists) { var package = await CreatePackage(descriptor, candidate); - return (IMightSupportBlazor)package; + return package as TPackage; } - return (IMightSupportBlazor)(await TryInstallAndLocateTool(descriptor)); + return (await TryInstallAndLocateTool(descriptor) as TPackage); } private async Task TryInstallAndLocateTool(PackageDescriptor packageDesciptor)