Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[browser] Integrate DevServer into WasmAppHost #88985

Merged
merged 29 commits into from
Jul 29, 2023
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
54d2152
Copy dev server from blazor
maraf Jul 11, 2023
55cac05
Add target path
maraf Jul 11, 2023
b9697d9
Merge remote-tracking branch 'upstream/main' into WasmBrowserHost
maraf Jul 14, 2023
af5bfd3
Add debugging. Copy pattern from WebServer
maraf Jul 14, 2023
9901130
Use DevServer when SWA manifest exists
maraf Jul 17, 2023
0c64fb7
Target path is not needed
maraf Jul 17, 2023
8e7d010
Namespace for DevServer classes
maraf Jul 18, 2023
819bd54
Make HTMLPath optional
maraf Jul 18, 2023
dd8531e
Merge remote-tracking branch 'upstream/main' into WasmBrowserHost
maraf Jul 18, 2023
5e2ea4b
Runtimeconfig template
maraf Jul 18, 2023
7ae7a4b
WIP
maraf Jul 18, 2023
d4b55a2
Merge remote-tracking branch 'upstream/main' into WasmBrowserHost
maraf Jul 19, 2023
56bc2e3
DevServer without MainAssembly by scanning for swa manifest in file s…
maraf Jul 21, 2023
df4d5fa
Set run command and arguments in WasmSDK
maraf Jul 21, 2023
38019bb
Include WasmAppHost in Wasm SDK pack
maraf Jul 21, 2023
404797c
Use dotnet run in TestAppScenarios
maraf Jul 21, 2023
b66ae38
Workaround for GenerateRuntimeConfigurationFiles
maraf Jul 21, 2023
3d6a3ed
Feedback
maraf Jul 24, 2023
09212f1
Load BrowserDebugHost from "current folder"
maraf Jul 24, 2023
29f442c
Feedback from @radical
maraf Jul 25, 2023
d0d85cf
Merge remote-tracking branch 'upstream/main' into WasmBrowserHost
maraf Jul 26, 2023
ec957e4
Merge remote-tracking branch 'upstream/main' into WasmBrowserHost
maraf Jul 27, 2023
ea68a69
Use separate argument to pick devserver
maraf Jul 27, 2023
d142f65
Add message about debugger
maraf Jul 27, 2023
4694490
Remove ForPublish from tests
maraf Jul 28, 2023
0923728
Remove GenerateRuntimeConfigurationFiles=false from SDK and GenerateR…
maraf Jul 28, 2023
ea8f894
Unify message for debugger on https
maraf Jul 28, 2023
010cb1f
Merge remote-tracking branch 'upstream/main' into WasmBrowserHost
maraf Jul 28, 2023
659adae
UseStaticWebAssets should default to false
maraf Jul 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,20 @@

<ItemGroup>
<ProjectReference Include="$(RepoTasksDir)Microsoft.NET.Sdk.WebAssembly.Pack.Tasks\Microsoft.NET.Sdk.WebAssembly.Pack.Tasks.csproj" />
<ProjectReference Include="$(RepoRoot)src\mono\wasm\host\WasmAppHost.csproj" />
<PackageFile Include="build\*.props;build\*.targets;build\*.web.config" TargetPath="build" />
</ItemGroup>

<Target Name="_PrepareForPack" BeforeTargets="GetPackageFiles">
<ItemGroup>
<PackageFile Include="$(SdkTargetsPath)" TargetPath="Sdk" />

<_WasmAppHostFiles Include="$(WasmAppHostDir)\*" TargetPath="WasmAppHost" />
<PackageFile Include="@(_WasmAppHostFiles)" />
</ItemGroup>

<Error Text="Could not find WasmAppHost files in $(WasmAppHostDir)" Condition="@(_WasmAppHostFiles->Count()) == 0" />
</Target>

<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.targets))" />
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ Copyright (c) .NET Foundation. All rights reserved.
-->
<Project ToolsVersion="14.0">

<PropertyGroup>
<_UseBlazorDevServer>$(RunArguments.Contains('blazor-devserver.dll').ToString().ToLower())</_UseBlazorDevServer>
</PropertyGroup>
<PropertyGroup Condition="'$(_WebAssemblyUserRunParameters)' == '' and '$(_UseBlazorDevServer)' == 'false'">
<RunCommand Condition="'$(DOTNET_HOST_PATH)' != '' and Exists($(DOTNET_HOST_PATH))">$(DOTNET_HOST_PATH)</RunCommand>
<RunCommand Condition="'$(RunCommand)' == ''">dotnet</RunCommand>

<WasmAppHostDir>$(MSBuildThisFileDirectory)..\WasmAppHost</WasmAppHostDir>
maraf marked this conversation as resolved.
Show resolved Hide resolved
<_RuntimeConfigJsonPath>$([MSBuild]::NormalizePath($(OutputPath), '$(AssemblyName).runtimeconfig.json'))</_RuntimeConfigJsonPath>
<RunArguments>exec &quot;$([MSBuild]::NormalizePath($(WasmAppHostDir), 'WasmAppHost.dll'))&quot; --runtime-config &quot;$(_RuntimeConfigJsonPath)&quot; $(WasmHostArguments)</RunArguments>
<RunWorkingDirectory>$(OutputPath)</RunWorkingDirectory>
</PropertyGroup>

<PropertyGroup>
<EnableDefaultContentItems Condition=" '$(EnableDefaultContentItems)' == '' ">true</EnableDefaultContentItems>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ protected string GetBinLogFilePath(string suffix = null)

protected async Task<RunResult> RunSdkStyleApp(RunOptions options)
{
string runArgs = $"{s_xharnessRunnerCommand} wasm webserver --app=. --web-server-use-default-files";
string workingDirectory = Path.GetFullPath(Path.Combine(FindBlazorBinFrameworkDir(options.Configuration, forPublish: options.ForPublish), ".."));
string runArgs = $"run -c {options.Configuration} -p:GenerateRuntimeConfigurationFiles=true";
maraf marked this conversation as resolved.
Show resolved Hide resolved
string workingDirectory = _projectDir;

using var runCommand = new RunCommand(s_buildEnv, _testOutput)
.WithWorkingDirectory(workingDirectory);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,8 @@ public SatelliteLoadingTests(ITestOutputHelper output, SharedBuildPerTestClassFi
public async Task LoadSatelliteAssembly()
{
CopyTestAsset("WasmBasicTestApp", "SatelliteLoadingTests");
PublishProject("Debug");

var result = await RunSdkStyleApp(new(Configuration: "Debug", ForPublish: true, TestScenario: "SatelliteAssembliesTest"));
var result = await RunSdkStyleApp(new(Configuration: "Debug", TestScenario: "SatelliteAssembliesTest"));
maraf marked this conversation as resolved.
Show resolved Hide resolved
Assert.Collection(
result.TestOutput,
m => Assert.Equal("default: hello", m),
Expand Down
5 changes: 0 additions & 5 deletions src/mono/wasm/host/BrowserArguments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,4 @@ public void ParseJsonProperties(IDictionary<string, JsonElement>? properties)
if (properties?.TryGetValue("forward-console", out JsonElement forwardConsoleElement) == true)
ForwardConsoleOutput = forwardConsoleElement.GetBoolean();
}

public void Validate()
{
CommonConfiguration.CheckPathOrInAppPath(CommonConfig.AppPath, HTMLPath, "html-path");
maraf marked this conversation as resolved.
Show resolved Hide resolved
}
}
132 changes: 107 additions & 25 deletions src/mono/wasm/host/BrowserHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.WebAssembly.AppHost.DevServer;
using Microsoft.WebAssembly.Diagnostics;

#nullable enable
Expand All @@ -35,7 +38,6 @@ public static async Task<int> InvokeAsync(CommonConfiguration commonArgs,
CancellationToken token)
{
var args = new BrowserArguments(commonArgs);
args.Validate();
maraf marked this conversation as resolved.
Show resolved Hide resolved
var host = new BrowserHost(args, logger);
await host.RunAsync(loggerFactory, token);

Expand Down Expand Up @@ -75,8 +77,7 @@ private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken toke
? aspnetUrls.Split(';', StringSplitOptions.RemoveEmptyEntries)
: new string[] { $"http://127.0.0.1:{_args.CommonConfig.HostProperties.WebServerPort}", "https://127.0.0.1:0" };

(ServerURLs serverURLs, IWebHost host) = await StartWebServerAsync(_args.CommonConfig.AppPath,
_args.ForwardConsoleOutput ?? false,
(ServerURLs serverURLs, IWebHost host) = await StartWebServerAsync(_args,
urls,
token);

Expand All @@ -88,29 +89,104 @@ private async Task RunAsync(ILoggerFactory loggerFactory, CancellationToken toke
await host.WaitForShutdownAsync(token);
}

private async Task<(ServerURLs, IWebHost)> StartWebServerAsync(string appPath, bool forwardConsole, string[] urls, CancellationToken token)
private async Task<(ServerURLs, IWebHost)> StartWebServerAsync(BrowserArguments args, string[] urls, CancellationToken token)
{
WasmTestMessagesProcessor? logProcessor = null;
if (forwardConsole)
Func<WebSocket, Task>? onConsoleConnected = null;
if (args.ForwardConsoleOutput ?? false)
{
logProcessor = new(_logger);
WasmTestMessagesProcessor logProcessor = new(_logger);
onConsoleConnected = socket => RunConsoleMessagesPump(socket, logProcessor!, token);
}

WebServerOptions options = new
(
OnConsoleConnected: forwardConsole
? socket => RunConsoleMessagesPump(socket, logProcessor!, token)
: null,
ContentRootPath: Path.GetFullPath(appPath),
WebServerUseCors: true,
WebServerUseCrossOriginPolicy: true,
Urls: urls
);

(ServerURLs serverURLs, IWebHost host) = await WebServer.StartAsync(options, _logger, token);
// If we are using new browser template, use dev server
if (TryCreateDevServerOptions(args, urls, onConsoleConnected, out var devServerOptions))
return await DevServer.DevServer.StartAsync(devServerOptions, _logger, token);

// Otherwise for old template, use web server
WebServerOptions webServerOptions = CreateWebServerOptions(urls, args.CommonConfig.AppPath, onConsoleConnected);
(ServerURLs serverURLs, IWebHost host) = await WebServer.StartAsync(webServerOptions, _logger, token);
maraf marked this conversation as resolved.
Show resolved Hide resolved
return (serverURLs, host);
}

private static WebServerOptions CreateWebServerOptions(string[] urls, string appPath, Func<WebSocket, Task>? onConsoleConnected) => new
(
OnConsoleConnected: onConsoleConnected,
ContentRootPath: Path.GetFullPath(appPath),
WebServerUseCors: true,
WebServerUseCrossOriginPolicy: true,
Urls: urls
);

private static bool TryCreateDevServerOptions(BrowserArguments args, string[] urls, Func<WebSocket, Task>? onConsoleConnected, [NotNullWhen(true)] out DevServerOptions? devServerOptions)
{
const string staticWebAssetsV1Extension = ".StaticWebAssets.xml";
const string staticWebAssetsV2Extension = ".staticwebassets.runtime.json";

devServerOptions = null;

string targetDirectory = GetTargetDirectory(args.CommonConfig.RuntimeConfigPath);
if (args.CommonConfig.HostProperties.MainAssembly != null)
{
// If we have main assembly name, try to find static web assets manifest by precise name.

var mainAssemblyPath = Path.Combine(targetDirectory, args.CommonConfig.HostProperties.MainAssembly);
var staticWebAssetsPath = Path.ChangeExtension(mainAssemblyPath, staticWebAssetsV2Extension);
if (File.Exists(staticWebAssetsPath))
{
devServerOptions = CreateDevServerOptions(urls, staticWebAssetsPath, onConsoleConnected);
}
else
{
staticWebAssetsPath = Path.ChangeExtension(mainAssemblyPath, staticWebAssetsV1Extension);
if (File.Exists(staticWebAssetsPath))
devServerOptions = CreateDevServerOptions(urls, staticWebAssetsPath, onConsoleConnected);
}
}
else
{
// If we don't have main assembly name, try to find static web assets manifest by search in the directory.

var staticWebAssetsPath = FindFirstFileWithExtension(targetDirectory, staticWebAssetsV2Extension)
?? FindFirstFileWithExtension(targetDirectory, staticWebAssetsV1Extension);

if (staticWebAssetsPath != null)
devServerOptions = CreateDevServerOptions(urls, staticWebAssetsPath, onConsoleConnected);
}

return devServerOptions != null;
}

private static DevServerOptions CreateDevServerOptions(string[] urls, string staticWebAssetsPath, Func<WebSocket, Task>? onConsoleConnected) => new
(
OnConsoleConnected: onConsoleConnected,
StaticWebAssetsPath: staticWebAssetsPath,
WebServerUseCors: true,
WebServerUseCrossOriginPolicy: true,
Urls: urls
);

private static string GetTargetDirectory(string? runtimeConfigPath)
maraf marked this conversation as resolved.
Show resolved Hide resolved
{
if (!string.IsNullOrEmpty(runtimeConfigPath))
{
runtimeConfigPath = Path.GetFullPath(runtimeConfigPath);
string? targetPath = Path.GetDirectoryName(runtimeConfigPath);
if (!string.IsNullOrEmpty(targetPath))
return targetPath;
}

return Environment.CurrentDirectory;
maraf marked this conversation as resolved.
Show resolved Hide resolved
}

private static string? FindFirstFileWithExtension(string directory, string extension)
{
string[] files = Directory.EnumerateFiles(directory, "*" + extension).ToArray();
if (files.Length == 0)
return null;

return files[0];
maraf marked this conversation as resolved.
Show resolved Hide resolved
}

private async Task RunConsoleMessagesPump(WebSocket socket, WasmTestMessagesProcessor messagesProcessor, CancellationToken token)
{
byte[] buff = new byte[4000];
Expand Down Expand Up @@ -169,7 +245,7 @@ private string[] BuildUrls(ServerURLs serverURLs, IEnumerable<string> passThroug
}

string query = sb.ToString();
string filename = Path.GetFileName(_args.HTMLPath!);
string? filename = _args.HTMLPath != null ? Path.GetFileName(_args.HTMLPath) : null;
string httpUrl = BuildUrl(serverURLs.Http, filename, query);

return string.IsNullOrEmpty(serverURLs.Https)
Expand All @@ -180,11 +256,17 @@ private string[] BuildUrls(ServerURLs serverURLs, IEnumerable<string> passThroug
BuildUrl(serverURLs.Https!, filename, query)
});

static string BuildUrl(string baseUrl, string htmlFileName, string query)
=> new UriBuilder(baseUrl)
static string BuildUrl(string baseUrl, string? htmlFileName, string query)
{
var uriBuilder = new UriBuilder(baseUrl)
{
Query = query,
Path = htmlFileName
}.ToString();
Query = query
};

if (htmlFileName != null)
uriBuilder.Path = htmlFileName;

return uriBuilder.ToString();
}
}
}
22 changes: 11 additions & 11 deletions src/mono/wasm/host/CommonConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ internal sealed class CommonConfiguration
public WasmHostProperties HostProperties { get; init; }
public IEnumerable<string> HostArguments { get; init; }
public bool Silent { get; private set; } = true;
public string? RuntimeConfigPath { get; private set; }

private string? hostArg;
private string? _runtimeConfigPath;

public static CommonConfiguration FromCommandLineArguments(string[] args) => new CommonConfiguration(args);

Expand All @@ -35,13 +35,13 @@ private CommonConfiguration(string[] args)
{
{ "debug|d", "Start debug server", _ => Debugging = true },
{ "host|h=", "Host config name", v => hostArg = v },
{ "runtime-config|r=", "runtimeconfig.json path for the app", v => _runtimeConfigPath = v },
{ "runtime-config|r=", "runtimeconfig.json path for the app", v => RuntimeConfigPath = v },
{ "extra-host-arg=", "Extra argument to be passed to the host", hostArgsList.Add },
{ "no-silent", "Verbose output from WasmAppHost", _ => Silent = false }
};

RemainingArgs = options.Parse(args);
if (string.IsNullOrEmpty(_runtimeConfigPath))
if (string.IsNullOrEmpty(RuntimeConfigPath))
{
string[] configs = Directory.EnumerateFiles(Environment.CurrentDirectory, "*.runtimeconfig.json").ToArray();
if (configs.Length == 0)
Expand All @@ -50,31 +50,31 @@ private CommonConfiguration(string[] args)
if (configs.Length > 1)
throw new CommandLineException($"Found multiple runtimeconfig.json files: {string.Join(", ", configs)}. Use --runtime-config= to specify one");

_runtimeConfigPath = Path.GetFullPath(configs[0]);
RuntimeConfigPath = Path.GetFullPath(configs[0]);
}

AppPath = Path.GetDirectoryName(_runtimeConfigPath) ?? ".";
AppPath = Path.GetDirectoryName(RuntimeConfigPath) ?? ".";

if (string.IsNullOrEmpty(_runtimeConfigPath) || !File.Exists(_runtimeConfigPath))
throw new CommandLineException($"Cannot find runtime config at {_runtimeConfigPath}");
if (string.IsNullOrEmpty(RuntimeConfigPath) || !File.Exists(RuntimeConfigPath))
throw new CommandLineException($"Cannot find runtime config at {RuntimeConfigPath}");

RuntimeConfig? rconfig = JsonSerializer.Deserialize<RuntimeConfig>(
File.ReadAllText(_runtimeConfigPath),
File.ReadAllText(RuntimeConfigPath),
new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNameCaseInsensitive = true
});
if (rconfig == null)
throw new CommandLineException($"Failed to deserialize {_runtimeConfigPath}");
throw new CommandLineException($"Failed to deserialize {RuntimeConfigPath}");

if (rconfig.RuntimeOptions == null)
throw new CommandLineException($"Failed to deserialize {_runtimeConfigPath} - rconfig.RuntimeOptions");
throw new CommandLineException($"Failed to deserialize {RuntimeConfigPath} - rconfig.RuntimeOptions");

HostProperties = rconfig.RuntimeOptions.WasmHostProperties;
if (HostProperties == null)
throw new CommandLineException($"Could not find any {nameof(RuntimeOptions.WasmHostProperties)} in {_runtimeConfigPath}");
throw new CommandLineException($"Could not find any {nameof(RuntimeOptions.WasmHostProperties)} in {RuntimeConfigPath}");

if (HostProperties.HostConfigs is null || HostProperties.HostConfigs.Count == 0)
throw new CommandLineException($"no perHostConfigs found");
Expand Down
Loading