diff --git a/AspNetCore.sln b/AspNetCore.sln index bd7fc769c8b8ce413726be19113ce3ac12e4ed5d..d5fda15eba652496d53cde865b47cf359b312abb 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1445,6 +1445,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWeb EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.NET.Sdk.BlazorWebAssembly.Tools", "src\Components\WebAssembly\Sdk\tools\Microsoft.NET.Sdk.BlazorWebAssembly.Tools.csproj", "{175E5CD8-92D4-46BB-882E-3A930D3302D4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh", "src\Tools\dotnet-watch\BrowserRefresh\src\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj", "{A5CE25E9-89E1-4F2C-9B89-0C161707E700}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Watch.BrowserRefresh.Tests", "src\Tools\dotnet-watch\BrowserRefresh\test\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", "{E6A23627-8D63-4DF1-A4F2-8881172C1FE6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -6843,6 +6847,30 @@ Global {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x64.Build.0 = Release|Any CPU {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.ActiveCfg = Release|Any CPU {175E5CD8-92D4-46BB-882E-3A930D3302D4}.Release|x86.Build.0 = Release|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x64.ActiveCfg = Debug|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x64.Build.0 = Debug|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x86.ActiveCfg = Debug|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Debug|x86.Build.0 = Debug|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|Any CPU.Build.0 = Release|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x64.ActiveCfg = Release|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x64.Build.0 = Release|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x86.ActiveCfg = Release|Any CPU + {A5CE25E9-89E1-4F2C-9B89-0C161707E700}.Release|x86.Build.0 = Release|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x64.Build.0 = Debug|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Debug|x86.Build.0 = Debug|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|Any CPU.Build.0 = Release|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x64.ActiveCfg = Release|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x64.Build.0 = Release|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x86.ActiveCfg = Release|Any CPU + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -7567,6 +7595,8 @@ Global {83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} {525EBCB4-A870-470B-BC90-845306C337D1} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} {175E5CD8-92D4-46BB-882E-3A930D3302D4} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE} + {A5CE25E9-89E1-4F2C-9B89-0C161707E700} = {B6118E15-C37A-4B05-B4DF-97FE99790417} + {E6A23627-8D63-4DF1-A4F2-8881172C1FE6} = {B6118E15-C37A-4B05-B4DF-97FE99790417} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/Directory.Build.targets b/Directory.Build.targets index a0de4f05b67ccd0051aa632fe1ccbb485c94b3c2..d4a4e9a2a04d9b8db7434d1b95247458a835b60b 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -147,7 +147,7 @@ <KnownFrameworkReference Condition="'$(UseAspNetCoreSharedRuntime)' != 'true'" Remove="Microsoft.AspNetCore.App" /> <KnownFrameworkReference Remove="Microsoft.WindowsDesktop.App" /> - <KnownFrameworkReference Condition="'$(UseAspNetCoreSharedRuntime)' == 'true'" Update="Microsoft.AspNetCore.App"> + <KnownFrameworkReference Condition="'$(UseAspNetCoreSharedRuntime)' == 'true' AND '$(DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp)' != 'true'" Update="Microsoft.AspNetCore.App"> <LatestRuntimeFrameworkVersion>$(SharedFxVersion)</LatestRuntimeFrameworkVersion> <DefaultRuntimeFrameworkVersion Condition="'$(IsServicingBuild)' != 'true'">$(SharedFxVersion)</DefaultRuntimeFrameworkVersion> <TargetingPackVersion Condition="'$(IsServicingBuild)' != 'true'">$(SharedFxVersion)</TargetingPackVersion> diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 99d1e377217fae5890123dfc8782bc7666cd1637..45f0a37b30596dd4dd164332804e8c711a75a74f 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -217,3 +217,54 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +License notice for West Wind Live Reload ASP.NET Core Middleware +============================================= + + +MIT License +----------- + +Copyright (c) 2019-2020 West Wind Technologies + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +License notice for cli-spinners +============================================= + +MIT License + +Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 6dbfca4646bdd90fabdd0e10140852287365ab67..e39540d453138177b7a66db354d9a9303c4bb10a 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -66,6 +66,7 @@ <ProjectReferenceProvider Include="Microsoft.AspNetCore.CookiePolicy" ProjectPath="$(RepoRoot)src\Security\CookiePolicy\src\Microsoft.AspNetCore.CookiePolicy.csproj" /> <ProjectReferenceProvider Include="Microsoft.Web.Xdt.Extensions" ProjectPath="$(RepoRoot)src\SiteExtensions\Microsoft.Web.Xdt.Extensions\src\Microsoft.Web.Xdt.Extensions.csproj" /> <ProjectReferenceProvider Include="dotnet-getdocument" ProjectPath="$(RepoRoot)src\Tools\dotnet-getdocument\src\dotnet-getdocument.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Watch.BrowserRefresh" ProjectPath="$(RepoRoot)src\Tools\dotnet-watch\BrowserRefresh\src\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj" /> <ProjectReferenceProvider Include="Microsoft.Extensions.ApiDescription.Client" ProjectPath="$(RepoRoot)src\Tools\Extensions.ApiDescription.Client\src\Microsoft.Extensions.ApiDescription.Client.csproj" /> <ProjectReferenceProvider Include="Microsoft.Extensions.ApiDescription.Server" ProjectPath="$(RepoRoot)src\Tools\Extensions.ApiDescription.Server\src\Microsoft.Extensions.ApiDescription.Server.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.DeveloperCertificates.XPlat" ProjectPath="$(RepoRoot)src\Tools\FirstRunCertGenerator\src\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj" /> diff --git a/eng/Workarounds.targets b/eng/Workarounds.targets index 4a00c1786beefcdfdf942f698d5be539bed8e1da..a2cc87f1a07be18b6d844ab89541ae5e44e6c6d4 100644 --- a/eng/Workarounds.targets +++ b/eng/Workarounds.targets @@ -20,7 +20,7 @@ The web sdk adds an implicit framework reference. This removes it until we can update our build to use framework references. --> <ItemGroup> - <FrameworkReference Remove="Microsoft.AspNetCore.App" /> + <FrameworkReference Remove="Microsoft.AspNetCore.App" Condition="'$(DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp)' != 'true'" /> <!-- Required because the Razor SDK will generate attributes --> <Reference Include="Microsoft.AspNetCore.Mvc" Condition="'$(UsingMicrosoftNETSdkWeb)' == 'true' AND '$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(GenerateRazorAssemblyInfo)' == 'true'" /> </ItemGroup> diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index 813a7aa9084422453b20d9dc4d6ba22a8e9ef9cf..7e3d29f08b213179664df0c524fb46c8617d1ad2 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -17,7 +17,11 @@ "src\\Tools\\dotnet-user-secrets\\src\\dotnet-user-secrets.csproj", "src\\Tools\\dotnet-user-secrets\\test\\dotnet-user-secrets.Tests.csproj", "src\\Tools\\dotnet-watch\\src\\dotnet-watch.csproj", - "src\\Tools\\dotnet-watch\\test\\dotnet-watch.Tests.csproj" + "src\\Tools\\dotnet-watch\\BrowserRefresh\\src\\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj", + "src\\Tools\\dotnet-watch\\BrowserRefresh\\test\\Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj", + "src\\Tools\\dotnet-watch\\test\\dotnet-watch.Tests.csproj", + "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", + "src\\Hosting\\TestHost\\src\\Microsoft.AspNetCore.TestHost.csproj" ] } } \ No newline at end of file diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/BrowserRefreshMiddleware.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/BrowserRefreshMiddleware.cs new file mode 100644 index 0000000000000000000000000000000000000000..210b4f73e8902affedd0a4e49fbf72806c44b136 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/src/BrowserRefreshMiddleware.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Watch.BrowserRefresh +{ + public class BrowserRefreshMiddleware + { + private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public BrowserRefreshMiddleware(RequestDelegate next, ILogger<BrowserRefreshMiddleware> logger) => + (_next, _logger) = (next, logger); + + public async Task InvokeAsync(HttpContext context) + { + // We only need to support this for requests that could be initiated by a browser. + if (IsBrowserRequest(context)) + { + // Use a custom StreamWrapper to rewrite output on Write/WriteAsync + using var responseStreamWrapper = new ResponseStreamWrapper(context, _logger); + var originalBodyFeature = context.Features.Get<IHttpResponseBodyFeature>(); + context.Features.Set<IHttpResponseBodyFeature>(new StreamResponseBodyFeature(responseStreamWrapper)); + + try + { + await _next(context); + } + finally + { + context.Features.Set(originalBodyFeature); + } + + if (responseStreamWrapper.IsHtmlResponse && _logger.IsEnabled(LogLevel.Debug)) + { + if (responseStreamWrapper.ScriptInjectionPerformed) + { + Log.BrowserConfiguredForRefreshes(_logger); + } + else + { + Log.FailedToConfiguredForRefreshes(_logger); + } + } + } + else + { + await _next(context); + } + } + + internal static bool IsBrowserRequest(HttpContext context) + { + var request = context.Request; + if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsPost(request.Method)) + { + return false; + } + + var typedHeaders = request.GetTypedHeaders(); + if (!(typedHeaders.Accept is IList<MediaTypeHeaderValue> acceptHeaders)) + { + return false; + } + + for (var i = 0; i < acceptHeaders.Count; i++) + { + if (acceptHeaders[i].IsSubsetOf(_textHtmlMediaType)) + { + return true; + } + } + + return false; + } + + internal static class Log + { + private static readonly Action<ILogger, Exception?> _setupResponseForBrowserRefresh = LoggerMessage.Define( + LogLevel.Debug, + new EventId(1, "SetUpResponseForBrowserRefresh"), + "Response markup is scheduled to include browser refresh script injection."); + + private static readonly Action<ILogger, Exception?> _browserConfiguredForRefreshes = LoggerMessage.Define( + LogLevel.Debug, + new EventId(2, "BrowserConfiguredForRefreshes"), + "Response markup was updated to include browser refresh script injection."); + + private static readonly Action<ILogger, Exception?> _failedToConfigureForRefreshes = LoggerMessage.Define( + LogLevel.Debug, + new EventId(3, "FailedToConfiguredForRefreshes"), + "Unable to configure browser refresh script injection on the response."); + + public static void SetupResponseForBrowserRefresh(ILogger logger) => _setupResponseForBrowserRefresh(logger, null); + public static void BrowserConfiguredForRefreshes(ILogger logger) => _browserConfiguredForRefreshes(logger, null); + public static void FailedToConfiguredForRefreshes(ILogger logger) => _failedToConfigureForRefreshes(logger, null); + } + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/HostingFilter.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/HostingFilter.cs new file mode 100644 index 0000000000000000000000000000000000000000..02135dca0dd6cf47b92fd0aa8abc0124bb3e8efc --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/src/HostingFilter.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +[assembly: HostingStartup(typeof(Microsoft.AspNetCore.Watch.BrowserRefresh.HostingStartup))] + +namespace Microsoft.AspNetCore.Watch.BrowserRefresh +{ + internal sealed class HostingStartup : IHostingStartup, IStartupFilter + { + public void Configure(IWebHostBuilder builder) + { + builder.ConfigureServices(services => services.TryAddEnumerable(ServiceDescriptor.Singleton<IStartupFilter>(this))); + } + + public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next) + { + return app => + { + app.UseMiddleware<BrowserRefreshMiddleware>(); + next(app); + }; + } + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj b/src/Tools/dotnet-watch/BrowserRefresh/src/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj new file mode 100644 index 0000000000000000000000000000000000000000..017eb2028c9dccf7a4bcece4c119b762c772e3ab --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/src/Microsoft.AspNetCore.Watch.BrowserRefresh.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <!-- This feature is supported in projects targeting 3.1 or later.--> + <TargetFramework>netcoreapp3.1</TargetFramework> + <IsPackable>false</IsPackable> + <Nullable>enable</Nullable> + <IsShipping>false</IsShipping> + <UseAspNetCoreSharedRuntime>true</UseAspNetCoreSharedRuntime> + <DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp>true</DoNotApplyWorkaroundsToMicrosoftAspNetCoreApp> + <ExcludeFromSourceBuild>false</ExcludeFromSourceBuild> + </PropertyGroup> + + <ItemGroup> + <FrameworkReference Include="Microsoft.AspNetCore.App" /> + <EmbeddedResource Include="WebSocketScriptInjection.js" /> + </ItemGroup> + +</Project> diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/ResponseStreamWrapper.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/ResponseStreamWrapper.cs new file mode 100644 index 0000000000000000000000000000000000000000..dfc5d465034a061887b5ac62c4d4ca9c3672c115 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/src/ResponseStreamWrapper.cs @@ -0,0 +1,153 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +// Based on https://github.com/RickStrahl/Westwind.AspnetCore.LiveReload/blob/128b5f524e86954e997f2c453e7e5c1dcc3db746/Westwind.AspnetCore.LiveReload/ResponseStreamWrapper.cs + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Watch.BrowserRefresh +{ + /// <summary> + /// Wraps the Response Stream to inject the WebSocket HTML into + /// an HTML Page. + /// </summary> + public class ResponseStreamWrapper : Stream + { + private static readonly MediaTypeHeaderValue _textHtmlMediaType = new MediaTypeHeaderValue("text/html"); + private readonly Stream _baseStream; + private readonly HttpContext _context; + private readonly ILogger _logger; + private bool? _isHtmlResponse; + + public ResponseStreamWrapper(HttpContext context, ILogger logger) + { + _context = context; + _baseStream = context.Response.Body; + _logger = logger; + } + + public override bool CanRead => false; + public override bool CanSeek => false; + public override bool CanWrite => true; + public override long Length { get; } + public override long Position { get; set; } + public bool ScriptInjectionPerformed { get; private set; } + + public bool IsHtmlResponse => _isHtmlResponse ?? false; + + public override void Flush() + { + OnWrite(); + _baseStream.Flush(); + } + + public override Task FlushAsync(CancellationToken cancellationToken) + { + OnWrite(); + return _baseStream.FlushAsync(cancellationToken); + } + + public override void Write(ReadOnlySpan<byte> buffer) + { + OnWrite(); + if (IsHtmlResponse && !ScriptInjectionPerformed) + { + ScriptInjectionPerformed = WebSocketScriptInjection.Instance.TryInjectLiveReloadScript(_baseStream, buffer); + } + else + { + _baseStream.Write(buffer); + } + } + + public override void WriteByte(byte value) + { + OnWrite(); + _baseStream.WriteByte(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + OnWrite(); + + if (IsHtmlResponse && !ScriptInjectionPerformed) + { + ScriptInjectionPerformed = WebSocketScriptInjection.Instance.TryInjectLiveReloadScript(_baseStream, buffer.AsSpan(offset, count)); + } + else + { + _baseStream.Write(buffer, offset, count); + } + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + OnWrite(); + + if (IsHtmlResponse && !ScriptInjectionPerformed) + { + ScriptInjectionPerformed = await WebSocketScriptInjection.Instance.TryInjectLiveReloadScriptAsync(_baseStream, buffer.AsMemory(offset, count), cancellationToken); + } + else + { + await _baseStream.WriteAsync(buffer, offset, count, cancellationToken); + } + } + + public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + OnWrite(); + + if (IsHtmlResponse && !ScriptInjectionPerformed) + { + ScriptInjectionPerformed = await WebSocketScriptInjection.Instance.TryInjectLiveReloadScriptAsync(_baseStream, buffer, cancellationToken); + } + else + { + await _baseStream.WriteAsync(buffer, cancellationToken); + } + } + + private void OnWrite() + { + if (_isHtmlResponse.HasValue) + { + return; + } + + var response = _context.Response; + + _isHtmlResponse = + (response.StatusCode == StatusCodes.Status200OK || response.StatusCode == StatusCodes.Status500InternalServerError) && + MediaTypeHeaderValue.TryParse(response.ContentType, out var mediaType) && + mediaType.IsSubsetOf(_textHtmlMediaType) && + (!mediaType.Charset.HasValue || mediaType.Charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase)); + + if (_isHtmlResponse.Value) + { + BrowserRefreshMiddleware.Log.SetupResponseForBrowserRefresh(_logger); + + // Since we're changing the markup content, reset the content-length + response.Headers.ContentLength = null; + } + } + + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => throw new NotSupportedException(); + + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/StartupHook.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/StartupHook.cs new file mode 100644 index 0000000000000000000000000000000000000000..f0dbc50e61fa938a2a88e2874bab3733e7e1e2b5 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/src/StartupHook.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +internal class StartupHook +{ + public static void Initialize() + { + // This method exists to make startup hook load successfully. We do not need to do anything interesting here. + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.cs b/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.cs new file mode 100644 index 0000000000000000000000000000000000000000..f0e85e2fa8e33edf53a6bca1597ef22b5b2e7ed4 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.cs @@ -0,0 +1,86 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Watch.BrowserRefresh +{ + /// <summary> + /// Helper class that handles the HTML injection into + /// a string or byte array. + /// </summary> + public class WebSocketScriptInjection + { + private const string BodyMarker = "</body>"; + + private readonly byte[] _bodyBytes = Encoding.UTF8.GetBytes(BodyMarker); + private readonly byte[] _scriptInjectionBytes; + + public static WebSocketScriptInjection Instance { get; } = new WebSocketScriptInjection( + GetWebSocketClientJavaScript(Environment.GetEnvironmentVariable("ASPNETCORE_AUTO_RELOAD_WS_ENDPOINT"))); + + public WebSocketScriptInjection(string clientScript) + { + _scriptInjectionBytes = Encoding.UTF8.GetBytes(clientScript); + } + + public bool TryInjectLiveReloadScript(Stream baseStream, ReadOnlySpan<byte> buffer) + { + var index = buffer.LastIndexOf(_bodyBytes); + if (index == -1) + { + baseStream.Write(buffer); + return false; + } + + if (index > 0) + { + baseStream.Write(buffer.Slice(0, index)); + buffer = buffer[index..]; + } + + // Write the injected script + baseStream.Write(_scriptInjectionBytes); + + // Write the rest of the buffer/HTML doc + baseStream.Write(buffer); + return true; + } + + public async ValueTask<bool> TryInjectLiveReloadScriptAsync(Stream baseStream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + var index = buffer.Span.LastIndexOf(_bodyBytes); + if (index == -1) + { + await baseStream.WriteAsync(buffer, cancellationToken); + return false; + } + + if (index > 0) + { + await baseStream.WriteAsync(buffer.Slice(0, index), cancellationToken); + buffer = buffer[index..]; + } + + // Write the injected script + await baseStream.WriteAsync(_scriptInjectionBytes, cancellationToken); + + // Write the rest of the buffer/HTML doc + await baseStream.WriteAsync(buffer, cancellationToken); + return true; + } + + internal static string GetWebSocketClientJavaScript(string? hostString) + { + var jsFileName = "Microsoft.AspNetCore.Watch.BrowserRefresh.WebSocketScriptInjection.js"; + using var reader = new StreamReader(typeof(WebSocketScriptInjection).Assembly.GetManifestResourceStream(jsFileName)!); + var script = reader.ReadToEnd().Replace("{{hostString}}", hostString); + + return $"<script>{script}</script>"; + } + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.js b/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.js new file mode 100644 index 0000000000000000000000000000000000000000..d957e285bd20a0b607c7c81c83981a7d7ebae1f0 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/src/WebSocketScriptInjection.js @@ -0,0 +1,23 @@ +setTimeout(function () { + // dotnet-watch browser reload script + let connection; + try { + connection = new WebSocket('{{hostString}}'); + } catch (ex) { + console.debug(ex); + return; + } + connection.onmessage = function (message) { + if (message.data === 'Reload') { + console.debug('Server is ready. Reloading...'); + location.reload(); + } else if (message.data === 'Wait') { + console.debug('File changes detected. Waiting for application to rebuild.'); + const t = document.title; const r = ['☱', '☲', '☴']; let i = 0; + setInterval(function () { document.title = r[i++ % r.length] + ' ' + t; }, 240); + } + } + connection.onerror = function (event) { console.debug('dotnet-watch reload socket error.', event) } + connection.onclose = function () { console.debug('dotnet-watch reload socket closed.') } + connection.onopen = function () { console.debug('dotnet-watch reload socket connected.') } +}, 500); diff --git a/src/Tools/dotnet-watch/BrowserRefresh/test/BrowserRefreshMiddlewareTest.cs b/src/Tools/dotnet-watch/BrowserRefresh/test/BrowserRefreshMiddlewareTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..31610af9f4cb718219676bed3dd4d56c638d2dc0 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/test/BrowserRefreshMiddlewareTest.cs @@ -0,0 +1,106 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using Xunit; + +namespace Microsoft.AspNetCore.Watch.BrowserRefresh +{ + public class BrowserRefreshMiddlewareTest + { + [Theory] + [InlineData("DELETE")] + [InlineData("head")] + [InlineData("Put")] + public void IsBrowserRequest_ReturnsFalse_ForNonGetOrPostRequests(string method) + { + // Arrange + var context = new DefaultHttpContext + { + Request = + { + Method = method, + Headers = + { + ["Accept"] = "application/html", + }, + }, + }; + + // Act + var result = BrowserRefreshMiddleware.IsBrowserRequest(context); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsBrowserRequest_ReturnsFalse_IsRequestDoesNotAcceptHtml() + { + // Arrange + var context = new DefaultHttpContext + { + Request = + { + Method = "GET", + Headers = + { + ["Accept"] = "application/xml", + }, + }, + }; + + // Act + var result = BrowserRefreshMiddleware.IsBrowserRequest(context); + + // Assert + Assert.False(result); + } + + [Fact] + public void IsBrowserRequest_ReturnsTrue_ForGetRequestsThatAcceptHtml() + { + // Arrange + var context = new DefaultHttpContext + { + Request = + { + Method = "GET", + Headers = + { + ["Accept"] = "application/json,text/html;q=0.9", + }, + }, + }; + + // Act + var result = BrowserRefreshMiddleware.IsBrowserRequest(context); + + // Assert + Assert.True(result); + } + + [Fact] + public void IsBrowserRequest_ReturnsTrue_ForRequestsThatAcceptAnyHtml() + { + // Arrange + var context = new DefaultHttpContext + { + Request = + { + Method = "Post", + Headers = + { + ["Accept"] = "application/json,text/*+html;q=0.9", + }, + }, + }; + + // Act + var result = BrowserRefreshMiddleware.IsBrowserRequest(context); + + // Assert + Assert.True(result); + } + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj b/src/Tools/dotnet-watch/BrowserRefresh/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj new file mode 100644 index 0000000000000000000000000000000000000000..1769c1cf2c282bc32eb09baed28cde9e9410cd70 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj @@ -0,0 +1,34 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> + <RootNamespace>Microsoft.AspNetCore.Watch.BrowserRefresh</RootNamespace> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.TestHost" /> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.AspNetCore.StaticFiles" /> + </ItemGroup> + + <ItemGroup> + <!-- + The M.AspNetCore.App 3.1 framework reference in the BrowserReferesh project makes it difficult to reference it from the test project + We'll simply test against it as source. + --> + <Compile Include="..\src\BrowserRefreshMiddleware.cs" LinkBase="src" /> + <Compile Include="..\src\ResponseStreamWrapper.cs" LinkBase="src" /> + <Compile Include="..\src\WebSocketScriptInjection.cs" LinkBase="src" /> + <EmbeddedResource Include="..\src\WebSocketScriptInjection.js" /> + </ItemGroup> + + <ItemGroup> + <Content Include="wwwroot\*" CopyToOutputDirectory="PreserveNewest" Pack="true" /> + </ItemGroup> + + <ItemGroup> + <None Remove="wwwroot\favicon.ico" /> + </ItemGroup> + +</Project> diff --git a/src/Tools/dotnet-watch/BrowserRefresh/test/ResponseStreamWrapperTest.cs b/src/Tools/dotnet-watch/BrowserRefresh/test/ResponseStreamWrapperTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..a62d00ab6a0ba229ad49d2f5a2cd87d615e7488f --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/test/ResponseStreamWrapperTest.cs @@ -0,0 +1,316 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Microsoft.AspNetCore.Watch.BrowserRefresh.Tests +{ + public class ResponseStreamWrapperTest + { + private const string BrowserAcceptHeader = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; + + [Fact] + public async Task HtmlIsInjectedForStaticFiles() + { + // Arrange + using var host = await StartHostAsync(); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/index.html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsInjectedForLargeStaticFiles() + { + // Arrange + using var host = await StartHostAsync(); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/largefile.html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsInjectedForDynamicallyGeneratedMarkup() + { + // Arrange + using var host = await StartHostAsync(routes => + { + routes.MapGet("/dynamic-html", async context => + { + context.Response.Headers["Content-Type"] = "text/html;charset=utf-8"; + await context.Response.WriteAsync("<html><body><h1>Hello world</h1></body></html>"); + }); + }); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/dynamic-html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsInjectedForWriteAsyncMarkupWithContentLength() + { + // Arrange + var responseContent = Encoding.UTF8.GetBytes("<html><body><h1>Hello world</h1></body></html>"); + using var host = await StartHostAsync(routes => + { + routes.MapGet("/dynamic-html", async context => + { + context.Response.ContentLength = responseContent.Length; + context.Response.Headers["Content-Type"] = "text/html;charset=utf-8"; + await context.Response.Body.WriteAsync(responseContent); + }); + }); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/dynamic-html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsInjectedForWriteAsyncMarkupWithMultipleWrites() + { + // Arrange + using var host = await StartHostAsync(routes => + { + routes.MapGet("/dynamic-html", async context => + { + context.Response.Headers["Content-Type"] = "text/html;charset=utf-8"; + + await context.Response.WriteAsync("<html>"); + await context.Response.WriteAsync("<body>"); + await context.Response.WriteAsync("<ul>"); + for (var i = 0; i < 100; i++) + { + await context.Response.WriteAsync($"<li>{i}</li>"); + } + await context.Response.WriteAsync("</ul>"); + await context.Response.WriteAsync("</body>"); + await context.Response.WriteAsync("</html>"); + }); + }); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/dynamic-html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsInjectedForPostResponses() + { + // Arrange + using var host = await StartHostAsync(routes => + { + routes.MapPost("/mvc-view", async context => + { + context.Response.Headers["Content-Type"] = "text/html;charset=utf-8"; + + await context.Response.WriteAsync("<html>"); + await context.Response.WriteAsync("<body>"); + await context.Response.WriteAsync("<ul>"); + for (var i = 0; i < 100; i++) + { + await context.Response.WriteAsync($"<li>{i}</li>"); + } + await context.Response.WriteAsync("</ul>"); + await context.Response.WriteAsync("</body>"); + await context.Response.WriteAsync("</html>"); + }); + }); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/mvc-view").AddHeader("Accept", BrowserAcceptHeader).SendAsync("POST"); + + // Assert + response.EnsureSuccessStatusCode(); + + Assert.False(response.Content.Headers.TryGetValues("Content-Length", out _), "We shouldn't send a Content-Length header."); + var content = await response.Content.ReadAsStringAsync(); + Assert.Contains("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsNotInjectedForNonBrowserRequests() + { + // Arrange + using var host = await StartHostAsync(); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/favicon.ico").AddHeader("Accept", "application/json").SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsNotInjectedForNonHtmlResponses() + { + // Arrange + using var host = await StartHostAsync(); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/favicon.ico").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsNotInjectedForNon200Responses() + { + // Arrange + using var host = await StartHostAsync(); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/file-does-not-exist.html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + var content = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsNotInjectedForNonGetOrPostResponses() + { + // Arrange + var responseContent = "<html><body><h1>Hello world</h1></body></html>"; + using var host = await StartHostAsync(routes => + { + routes.MapMethods( + "/dynamic-html", + new[] { "HEAD", "GET", "DELETE" }, + async context => + { + context.Response.Headers["Content-Type"] = "text/html;charset=utf-8"; + await context.Response.WriteAsync(responseContent); + }); + }); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/dynamic-html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("HEAD"); + + // Assert + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsNotInjectedForJsonResponses() + { + // Arrange + var responseContent = "<html><body><h1>Hello world</h1></body></html>"; + using var host = await StartHostAsync(routes => + { + routes.MapGet( + "/dynamic-html", + async context => + { + context.Response.Headers["Content-Type"] = "application/json;charset=utf-8"; + await context.Response.WriteAsync(responseContent); + }); + }); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/dynamic-html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("dotnet-watch browser reload script", content); + } + + [Fact] + public async Task HtmlIsNotInjectedForNonUtf8Responses() + { + // Arrange + var responseContent = "<html><body><h1>Hello world</h1></body></html>"; + using var host = await StartHostAsync(routes => + { + routes.MapGet( + "/dynamic-html", + async context => + { + context.Response.Headers["Content-Type"] = "text/html;charset=utf-16"; + await context.Response.Body.WriteAsync(Encoding.Unicode.GetBytes(responseContent)); + }); + }); + using var server = host.GetTestServer(); + var response = await server.CreateRequest("/dynamic-html").AddHeader("Accept", BrowserAcceptHeader).SendAsync("GET"); + + // Assert + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + Assert.DoesNotContain("dotnet-watch browser reload script", content); + } + + private static async Task<IHost> StartHostAsync(Action<IEndpointRouteBuilder>? routeBuilder = null) + { + var host = new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .Configure(app => + { + app.UseMiddleware<BrowserRefreshMiddleware>(); + app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(Path.Combine(AppContext.BaseDirectory, "wwwroot")) }); + + if (routeBuilder != null) + { + app.UseRouting(); + app.UseEndpoints(routeBuilder); + } + }) + .ConfigureServices(services => services.AddRouting()); + }).Build(); + + await host.StartAsync(); + return host; + } + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/test/WebSockerScriptInjectionTest.cs b/src/Tools/dotnet-watch/BrowserRefresh/test/WebSockerScriptInjectionTest.cs new file mode 100644 index 0000000000000000000000000000000000000000..ceb53404d165ed4afbb2ee361ba3afef40bc26bb --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/test/WebSockerScriptInjectionTest.cs @@ -0,0 +1,206 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.Watch.BrowserRefresh +{ + public class WebSockerScriptInjectionTest + { + private const string ClientScript = "<script><!--My cool script--></script>"; + private readonly WebSocketScriptInjection ScriptInjection = new WebSocketScriptInjection(ClientScript); + + [Fact] + public async Task TryInjectLiveReloadScriptAsync_DoesNotInjectMarkup_IfInputDoesNotContainBodyTag() + { + // Arrange + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("<div>this is not a real body tag.</div>"); + + // Act + var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); + + // Assert + Assert.False(result); + Assert.Equal(input, stream.ToArray()); + } + + [Fact] + public async Task TryInjectLiveReloadScriptAsync_InjectsMarkupIfBodyTagAppearsInTheMiddle() + { + // Arrange + var expected = +$@"<footer> + This is the footer +</footer> +{ClientScript}</body> +</html>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes( +@"<footer> + This is the footer +</footer> +</body> +</html>"); + + // Act + var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output, ignoreLineEndingDifferences: true); + } + + [Fact] + public async Task TryInjectLiveReloadScriptAsync_WithOffsetBodyTagAppearsInMiddle() + { + // Arrange + var expected = $"</table>{ClientScript}</body>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("unused</table></body>"); + + // Act + var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input.AsMemory(6)); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public async Task TryInjectLiveReloadScriptAsync_WithOffsetBodyTagAppearsAtStartOfOffset() + { + // Arrange + var expected = $"{ClientScript}</body>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("unused</body>"); + + // Act + var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input.AsMemory(6)); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public async Task TryInjectLiveReloadScriptAsync_InjectsMarkupIfBodyTagAppearsAtTheStartOfOutput() + { + // Arrange + var expected = $"{ClientScript}</body></html>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("</body></html>"); + + // Act + var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public async Task TryInjectLiveReloadScriptAsync_InjectsMarkupIfBodyTagAppearsByItself() + { + // Arrange + var expected = $"{ClientScript}</body>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("</body>"); + + // Act + var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public async Task TryInjectLiveReloadScriptAsync_MultipleBodyTags() + { + // Arrange + var expected = $"<p></body>some text</p>{ClientScript}</body>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("abc<p></body>some text</p></body>").AsMemory(3); + + // Act + var result = await ScriptInjection.TryInjectLiveReloadScriptAsync(stream, input); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public void TryInjectLiveReloadScript_NoBodyTag() + { + // Arrange + var expected = "<p>Hello world</p>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes(expected).AsSpan(); + + // Act + var result = ScriptInjection.TryInjectLiveReloadScript(stream, input); + + // Assert + Assert.False(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public void TryInjectLiveReloadScript_NoOffset() + { + // Arrange + var expected = $"</table>{ClientScript}</body>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("</table></body>").AsSpan(); + + // Act + var result = ScriptInjection.TryInjectLiveReloadScript(stream, input); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public void TryInjectLiveReloadScript_WithOffset() + { + // Arrange + var expected = $"</table>{ClientScript}</body>"; + var stream = new MemoryStream(); + var input = Encoding.UTF8.GetBytes("unused</table></body>").AsSpan(6); + + // Act + var result = ScriptInjection.TryInjectLiveReloadScript(stream, input); + + // Assert + Assert.True(result); + var output = Encoding.UTF8.GetString(stream.ToArray()); + Assert.Equal(expected, output); + } + + [Fact] + public void GetWebSocketClientJavaScript_Works() + { + // Act + var script = WebSocketScriptInjection.GetWebSocketClientJavaScript("some-host"); + + // Assert + Assert.Contains("// dotnet-watch browser reload script", script); + Assert.Contains("'some-host'", script); + } + } +} diff --git a/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/favicon.ico b/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..63e859b476eff5055e0e557aaa151ca8223fbeef Binary files /dev/null and b/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/favicon.ico differ diff --git a/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/index.html b/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/index.html new file mode 100644 index 0000000000000000000000000000000000000000..3ad0950b86135331c310611850d2c70d690bec51 --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/index.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> + +<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8" /> + <title></title> +</head> +<body> + <h1>Hello world</h1> +</body> +</html> diff --git a/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/largefile.html b/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/largefile.html new file mode 100644 index 0000000000000000000000000000000000000000..b43e8b1c7e1fc5553e92906b5398345934815b6f --- /dev/null +++ b/src/Tools/dotnet-watch/BrowserRefresh/test/wwwroot/largefile.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> + +<html lang="en" xmlns="http://www.w3.org/1999/xhtml"> +<head> + <meta charset="utf-8" /> + <title></title> +</head> +<body> + <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. At consectetur lorem donec massa sapien faucibus et. Tristique senectus et netus et malesuada fames. A lacus vestibulum sed arcu. At varius vel pharetra vel turpis nunc eget lorem. Quam pellentesque nec nam aliquam. Et ultrices neque ornare aenean euismod elementum. Nullam eget felis eget nunc lobortis mattis aliquam faucibus purus. Laoreet id donec ultrices tincidunt arcu non sodales neque. Ac placerat vestibulum lectus mauris ultrices eros in cursus. Volutpat sed cras ornare arcu dui. Elit eget gravida cum sociis natoque. Vitae tempus quam pellentesque nec nam aliquam sem. Arcu bibendum at varius vel pharetra. Lectus quam id leo in vitae.</p> + <p>In dictum non consectetur a erat nam at. Dictum fusce ut placerat orci nulla. Aliquam malesuada bibendum arcu vitae elementum curabitur. Ornare aenean euismod elementum nisi quis eleifend quam. Varius duis at consectetur lorem donec massa sapien. Volutpat commodo sed egestas egestas fringilla phasellus faucibus scelerisque eleifend. Egestas sed tempus urna et. Dolor morbi non arcu risus quis varius quam quisque. Platea dictumst vestibulum rhoncus est pellentesque elit ullamcorper dignissim cras. Purus viverra accumsan in nisl. Lobortis feugiat vivamus at augue eget arcu dictum varius. Mauris cursus mattis molestie a. Tortor at auctor urna nunc id. Suscipit tellus mauris a diam maecenas. Aenean pharetra magna ac placerat vestibulum. Donec ac odio tempor orci. At tempor commodo ullamcorper a lacus vestibulum sed. Sed vulputate odio ut enim blandit. Massa vitae tortor condimentum lacinia. Pulvinar etiam non quam lacus suspendisse faucibus interdum.</p> + <p>Enim eu turpis egestas pretium. Mus mauris vitae ultricies leo integer malesuada nunc vel. Massa sed elementum tempus egestas sed sed risus pretium quam. In metus vulputate eu scelerisque felis imperdiet proin. Faucibus scelerisque eleifend donec pretium vulputate sapien. Arcu non odio euismod lacinia at quis. Facilisis magna etiam tempor orci eu. Morbi non arcu risus quis varius quam quisque id diam. Quis eleifend quam adipiscing vitae proin. Ut placerat orci nulla pellentesque dignissim enim. Aliquet sagittis id consectetur purus ut faucibus pulvinar elementum. Nunc aliquet bibendum enim facilisis. Non tellus orci ac auctor augue mauris augue. Venenatis urna cursus eget nunc. Amet porttitor eget dolor morbi non. Fames ac turpis egestas integer eget aliquet nibh praesent tristique. Orci porta non pulvinar neque laoreet suspendisse interdum consectetur.</p> + <p>Volutpat diam ut venenatis tellus in. Odio pellentesque diam volutpat commodo. Diam quam nulla porttitor massa id. Ultricies leo integer malesuada nunc vel. Auctor elit sed vulputate mi sit. Sit amet est placerat in egestas erat. Id eu nisl nunc mi ipsum. Vulputate ut pharetra sit amet aliquam. Justo laoreet sit amet cursus sit amet. Felis donec et odio pellentesque diam volutpat commodo. Sit amet risus nullam eget felis. Vulputate mi sit amet mauris commodo quis imperdiet. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus. Egestas maecenas pharetra convallis posuere morbi leo urna molestie at. Pulvinar mattis nunc sed blandit. Auctor neque vitae tempus quam pellentesque nec nam.</p> + <p>In metus vulputate eu scelerisque felis imperdiet proin fermentum. Netus et malesuada fames ac turpis egestas integer eget. Sit amet massa vitae tortor condimentum lacinia. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl vel. Venenatis a condimentum vitae sapien pellentesque habitant morbi tristique. Vestibulum lectus mauris ultrices eros in cursus turpis massa tincidunt. Ut aliquam purus sit amet luctus venenatis lectus magna. Sed adipiscing diam donec adipiscing tristique risus nec. Ut porttitor leo a diam. Sem integer vitae justo eget magna fermentum.</p> + <p>Pretium vulputate sapien nec sagittis aliquam malesuada bibendum arcu. Leo a diam sollicitudin tempor id eu nisl nunc. Fames ac turpis egestas sed. Velit euismod in pellentesque massa placerat duis ultricies lacus sed. Enim nunc faucibus a pellentesque sit amet porttitor eget. Scelerisque felis imperdiet proin fermentum leo vel orci. Adipiscing tristique risus nec feugiat in. Tortor id aliquet lectus proin nibh nisl. Tortor vitae purus faucibus ornare. Sit amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Commodo elit at imperdiet dui. Nulla pellentesque dignissim enim sit. Orci a scelerisque purus semper eget duis at tellus at. Non curabitur gravida arcu ac. Libero enim sed faucibus turpis in eu mi.</p> + <p>A arcu cursus vitae congue. In mollis nunc sed id semper risus. Vestibulum rhoncus est pellentesque elit ullamcorper. Pellentesque sit amet porttitor eget. Quis auctor elit sed vulputate mi sit amet mauris. Imperdiet proin fermentum leo vel. Suspendisse interdum consectetur libero id faucibus nisl tincidunt eget. Ut tristique et egestas quis ipsum suspendisse ultrices gravida dictum. Feugiat scelerisque varius morbi enim nunc faucibus. Vitae nunc sed velit dignissim sodales. Quis varius quam quisque id. Dignissim convallis aenean et tortor at risus viverra adipiscing. Viverra suspendisse potenti nullam ac tortor vitae purus faucibus. At risus viverra adipiscing at in tellus integer feugiat. In fermentum et sollicitudin ac orci. Et ligula ullamcorper malesuada proin libero. Est placerat in egestas erat imperdiet. Nunc vel risus commodo viverra maecenas accumsan. Dui id ornare arcu odio ut.</p> + <p>Amet cursus sit amet dictum sit. Egestas sed tempus urna et pharetra pharetra massa. At imperdiet dui accumsan sit. Tempus iaculis urna id volutpat lacus laoreet non curabitur gravida. Morbi leo urna molestie at elementum. Eros in cursus turpis massa tincidunt dui ut ornare. Lorem donec massa sapien faucibus. A cras semper auctor neque vitae tempus quam. Libero enim sed faucibus turpis in. Vitae semper quis lectus nulla at. Tempor id eu nisl nunc mi. Ut lectus arcu bibendum at varius vel pharetra vel. Massa enim nec dui nunc mattis enim.</p> + <p>Quam nulla porttitor massa id. Cursus vitae congue mauris rhoncus aenean. Purus viverra accumsan in nisl nisi scelerisque eu. Tincidunt eget nullam non nisi est sit amet facilisis. Ac feugiat sed lectus vestibulum. Nisl purus in mollis nunc sed id semper risus in. Nunc scelerisque viverra mauris in aliquam sem fringilla. Diam maecenas ultricies mi eget mauris pharetra et ultrices neque. In ornare quam viverra orci sagittis eu volutpat odio facilisis. Ultricies tristique nulla aliquet enim tortor at auctor. Ipsum a arcu cursus vitae congue mauris rhoncus.</p> + <p>Massa massa ultricies mi quis hendrerit dolor. Iaculis at erat pellentesque adipiscing. Ut tellus elementum sagittis vitae et leo duis ut diam. Tellus in metus vulputate eu. Ac auctor augue mauris augue. Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero. In massa tempor nec feugiat nisl pretium fusce. Lorem ipsum dolor sit amet consectetur adipiscing. Facilisis mauris sit amet massa vitae tortor condimentum lacinia quis. Et leo duis ut diam quam. Accumsan in nisl nisi scelerisque eu ultrices. Et netus et malesuada fames. Orci sagittis eu volutpat odio facilisis.</p> + <p>Posuere ac ut consequat semper viverra nam libero. In aliquam sem fringilla ut. Id nibh tortor id aliquet lectus proin nibh nisl. Sed viverra ipsum nunc aliquet bibendum. Pretium fusce id velit ut tortor pretium viverra suspendisse potenti. Leo vel fringilla est ullamcorper eget nulla facilisi etiam dignissim. Pharetra magna ac placerat vestibulum lectus mauris. Commodo elit at imperdiet dui. Eget est lorem ipsum dolor sit amet consectetur adipiscing elit. Turpis massa tincidunt dui ut ornare lectus sit amet. A scelerisque purus semper eget duis. Amet nisl purus in mollis nunc sed id semper. Bibendum arcu vitae elementum curabitur vitae nunc sed velit dignissim. Ante in nibh mauris cursus mattis molestie a iaculis at. Cras tincidunt lobortis feugiat vivamus at augue. Consectetur purus ut faucibus pulvinar elementum integer. Netus et malesuada fames ac turpis egestas maecenas pharetra. At tempor commodo ullamcorper a lacus vestibulum sed arcu non.</p> + <p>Consectetur adipiscing elit duis tristique sollicitudin. Nunc id cursus metus aliquam eleifend. Id velit ut tortor pretium viverra suspendisse potenti nullam. Cursus turpis massa tincidunt dui ut ornare lectus. Nulla posuere sollicitudin aliquam ultrices. Viverra accumsan in nisl nisi scelerisque. Pulvinar etiam non quam lacus suspendisse. Velit euismod in pellentesque massa placerat duis ultricies lacus. Quis auctor elit sed vulputate. Gravida cum sociis natoque penatibus et magnis.</p> + <p>Fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Et leo duis ut diam quam nulla porttitor. Suspendisse interdum consectetur libero id faucibus nisl. Elit duis tristique sollicitudin nibh sit amet. Et egestas quis ipsum suspendisse ultrices gravida dictum fusce. Eu facilisis sed odio morbi. Tellus pellentesque eu tincidunt tortor. Id porta nibh venenatis cras sed felis eget velit. Massa tincidunt dui ut ornare lectus sit. Venenatis tellus in metus vulputate. Egestas quis ipsum suspendisse ultrices gravida dictum fusce ut. Aliquet lectus proin nibh nisl condimentum id venenatis a condimentum. Id ornare arcu odio ut. Donec massa sapien faucibus et. Et leo duis ut diam quam nulla porttitor massa. Id venenatis a condimentum vitae sapien pellentesque habitant morbi. Sed id semper risus in. Vitae nunc sed velit dignissim sodales ut eu. Euismod elementum nisi quis eleifend quam. Aliquet enim tortor at auctor urna nunc id cursus.</p> + <p>Elementum nibh tellus molestie nunc non blandit massa. In ornare quam viverra orci sagittis. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Ut eu sem integer vitae justo eget magna fermentum iaculis. Vel eros donec ac odio tempor. Venenatis a condimentum vitae sapien pellentesque habitant morbi tristique senectus. Pellentesque pulvinar pellentesque habitant morbi tristique senectus. Cursus turpis massa tincidunt dui ut. Tristique et egestas quis ipsum suspendisse ultrices. Nunc consequat interdum varius sit amet mattis vulputate enim. Nulla malesuada pellentesque elit eget gravida cum. Eget aliquet nibh praesent tristique magna sit amet. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Sagittis vitae et leo duis ut diam quam nulla. Venenatis lectus magna fringilla urna porttitor. In pellentesque massa placerat duis ultricies lacus sed.</p> + <p>Odio facilisis mauris sit amet massa vitae tortor condimentum lacinia. Platea dictumst quisque sagittis purus sit amet. Pharetra convallis posuere morbi leo urna molestie at. Nullam eget felis eget nunc lobortis mattis. Cras semper auctor neque vitae tempus quam pellentesque nec nam. Porttitor rhoncus dolor purus non enim. Lorem ipsum dolor sit amet. Urna molestie at elementum eu facilisis. Magna eget est lorem ipsum dolor. Suspendisse faucibus interdum posuere lorem ipsum dolor sit amet consectetur. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Elementum sagittis vitae et leo duis ut diam quam nulla. Dolor morbi non arcu risus quis varius quam quisque. Nunc aliquet bibendum enim facilisis gravida neque convallis a.</p> + <p>Tortor consequat id porta nibh venenatis cras sed felis. Congue eu consequat ac felis donec. Risus at ultrices mi tempus. Et magnis dis parturient montes nascetur ridiculus mus mauris vitae. Malesuada proin libero nunc consequat interdum varius sit amet mattis. Turpis tincidunt id aliquet risus feugiat in ante. Lectus proin nibh nisl condimentum id venenatis. Aliquam ultrices sagittis orci a. Netus et malesuada fames ac turpis. Suspendisse ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Arcu ac tortor dignissim convallis aenean et tortor at. Dignissim sodales ut eu sem integer. Auctor urna nunc id cursus metus aliquam eleifend. Nulla facilisi morbi tempus iaculis urna id volutpat. Bibendum ut tristique et egestas quis ipsum suspendisse. Magna etiam tempor orci eu lobortis elementum nibh tellus molestie. Magna fermentum iaculis eu non diam phasellus vestibulum lorem.</p> + <p>Laoreet non curabitur gravida arcu. Egestas tellus rutrum tellus pellentesque. Platea dictumst vestibulum rhoncus est pellentesque elit. Praesent elementum facilisis leo vel fringilla est ullamcorper. Augue eget arcu dictum varius duis at consectetur lorem donec. Vulputate enim nulla aliquet porttitor lacus. Volutpat commodo sed egestas egestas fringilla phasellus faucibus. Non curabitur gravida arcu ac tortor dignissim convallis aenean. Amet nisl purus in mollis nunc. Feugiat in ante metus dictum at tempor commodo ullamcorper a. Mauris cursus mattis molestie a iaculis at erat. Amet porttitor eget dolor morbi non arcu risus. Velit aliquet sagittis id consectetur purus ut faucibus. Pulvinar etiam non quam lacus suspendisse faucibus interdum. Euismod nisi porta lorem mollis aliquam ut porttitor. Vulputate odio ut enim blandit volutpat maecenas. Commodo nulla facilisi nullam vehicula ipsum a. Nunc vel risus commodo viverra maecenas accumsan lacus vel facilisis.</p> + <p>Fringilla ut morbi tincidunt augue interdum velit euismod in pellentesque. Arcu risus quis varius quam quisque id. Amet est placerat in egestas erat. Cras sed felis eget velit aliquet. Netus et malesuada fames ac turpis egestas. Quis varius quam quisque id diam vel quam elementum pulvinar. Diam vel quam elementum pulvinar. Mattis rhoncus urna neque viverra justo. Tincidunt eget nullam non nisi. Velit dignissim sodales ut eu sem integer vitae. Feugiat in fermentum posuere urna nec tincidunt. Quam pellentesque nec nam aliquam sem et tortor. In nisl nisi scelerisque eu ultrices vitae auctor eu augue.</p> + <p>Commodo sed egestas egestas fringilla phasellus faucibus scelerisque. Non diam phasellus vestibulum lorem sed risus ultricies. Dignissim convallis aenean et tortor at. Purus sit amet volutpat consequat mauris nunc congue nisi. Faucibus nisl tincidunt eget nullam non nisi est sit amet. Amet consectetur adipiscing elit ut aliquam purus sit amet luctus. Amet consectetur adipiscing elit ut aliquam purus. Nulla facilisi cras fermentum odio eu feugiat pretium nibh ipsum. Sed felis eget velit aliquet sagittis id. Felis eget nunc lobortis mattis aliquam faucibus purus in. Ornare quam viverra orci sagittis. Urna condimentum mattis pellentesque id nibh tortor id. Non consectetur a erat nam at lectus. Nisl suscipit adipiscing bibendum est ultricies integer. Urna nec tincidunt praesent semper. Venenatis urna cursus eget nunc scelerisque viverra mauris. Ut aliquam purus sit amet luctus venenatis lectus. Mattis rhoncus urna neque viverra justo nec ultrices dui sapien. Ac orci phasellus egestas tellus rutrum.</p> + <p>Leo vel orci porta non pulvinar. At auctor urna nunc id cursus metus. Massa id neque aliquam vestibulum morbi blandit. Ut eu sem integer vitae justo. Neque laoreet suspendisse interdum consectetur libero id faucibus. Duis ut diam quam nulla porttitor massa id. Justo eget magna fermentum iaculis eu non diam phasellus vestibulum. Nec nam aliquam sem et tortor consequat. Nisi scelerisque eu ultrices vitae auctor eu augue. Aliquet bibendum enim facilisis gravida neque convallis. Morbi non arcu risus quis varius quam quisque id.</p> + <p>Nulla facilisi morbi tempus iaculis urna. Elementum nisi quis eleifend quam adipiscing vitae proin sagittis nisl. Enim diam vulputate ut pharetra sit. Parturient montes nascetur ridiculus mus. Et molestie ac feugiat sed. Quis risus sed vulputate odio ut enim blandit volutpat maecenas. Imperdiet dui accumsan sit amet nulla facilisi morbi tempus. Lorem sed risus ultricies tristique nulla aliquet enim tortor. Feugiat scelerisque varius morbi enim. Ac placerat vestibulum lectus mauris. Purus in mollis nunc sed. Lorem ipsum dolor sit amet consectetur adipiscing. Arcu cursus vitae congue mauris rhoncus aenean vel.</p> + <p>Enim lobortis scelerisque fermentum dui faucibus in. Cras ornare arcu dui vivamus arcu felis bibendum ut tristique. Diam sit amet nisl suscipit. Laoreet suspendisse interdum consectetur libero id faucibus nisl tincidunt. Sed felis eget velit aliquet sagittis id consectetur purus. Id volutpat lacus laoreet non curabitur. Fermentum odio eu feugiat pretium nibh. Vestibulum lectus mauris ultrices eros in cursus turpis massa tincidunt. Et sollicitudin ac orci phasellus egestas tellus. Pellentesque habitant morbi tristique senectus et netus et. Sit amet volutpat consequat mauris. At augue eget arcu dictum varius duis at consectetur. Eget nunc lobortis mattis aliquam faucibus purus in massa. Fringilla ut morbi tincidunt augue interdum velit euismod in. Eu mi bibendum neque egestas congue. Id venenatis a condimentum vitae sapien pellentesque. Odio facilisis mauris sit amet massa vitae tortor condimentum. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam. Nascetur ridiculus mus mauris vitae ultricies leo. Felis eget nunc lobortis mattis aliquam faucibus purus in massa.</p> + <p>Tristique risus nec feugiat in fermentum. Consectetur lorem donec massa sapien faucibus et. Ac placerat vestibulum lectus mauris. Mauris pellentesque pulvinar pellentesque habitant morbi. Morbi tempus iaculis urna id volutpat lacus laoreet. Vitae semper quis lectus nulla at volutpat diam ut. Eu non diam phasellus vestibulum lorem. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Nascetur ridiculus mus mauris vitae ultricies leo integer. Vitae turpis massa sed elementum.</p> + <p>Quam lacus suspendisse faucibus interdum posuere lorem ipsum. Sit amet aliquam id diam maecenas ultricies mi. Praesent semper feugiat nibh sed pulvinar proin gravida. Nunc vel risus commodo viverra maecenas accumsan lacus. Eros donec ac odio tempor. Blandit turpis cursus in hac. Diam vel quam elementum pulvinar etiam non quam. Blandit volutpat maecenas volutpat blandit aliquam etiam erat velit. Volutpat commodo sed egestas egestas fringilla phasellus. Turpis egestas integer eget aliquet. Neque egestas congue quisque egestas diam in arcu cursus euismod. Tincidunt augue interdum velit euismod. Tristique et egestas quis ipsum suspendisse ultrices gravida. Quam viverra orci sagittis eu. Viverra ipsum nunc aliquet bibendum enim facilisis gravida. Consectetur lorem donec massa sapien faucibus et molestie ac feugiat. Integer quis auctor elit sed vulputate.</p> + <p>Feugiat vivamus at augue eget arcu. Turpis in eu mi bibendum neque egestas congue. Aenean pharetra magna ac placerat vestibulum lectus mauris. Est ullamcorper eget nulla facilisi etiam dignissim. Quisque non tellus orci ac auctor augue mauris augue neque. Adipiscing enim eu turpis egestas. Tristique risus nec feugiat in fermentum posuere. Donec et odio pellentesque diam volutpat commodo sed egestas. Eleifend donec pretium vulputate sapien. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Amet porttitor eget dolor morbi non arcu risus quis varius. Elit ullamcorper dignissim cras tincidunt lobortis. Habitant morbi tristique senectus et netus. Auctor elit sed vulputate mi sit amet mauris commodo. Ut sem nulla pharetra diam. Massa sapien faucibus et molestie ac feugiat sed lectus vestibulum. Quam lacus suspendisse faucibus interdum posuere lorem ipsum dolor sit. Mauris vitae ultricies leo integer malesuada.</p> + <p>Ipsum dolor sit amet consectetur adipiscing elit. Habitant morbi tristique senectus et netus. Volutpat sed cras ornare arcu dui vivamus arcu felis bibendum. Diam sollicitudin tempor id eu nisl nunc mi ipsum. Augue neque gravida in fermentum et sollicitudin ac orci phasellus. Sagittis eu volutpat odio facilisis mauris. Tortor consequat id porta nibh venenatis. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque. Suspendisse interdum consectetur libero id faucibus nisl tincidunt eget nullam. Mattis molestie a iaculis at erat pellentesque adipiscing commodo. Magnis dis parturient montes nascetur ridiculus mus mauris vitae. Posuere sollicitudin aliquam ultrices sagittis.</p> + <p>Porta lorem mollis aliquam ut porttitor. Pretium lectus quam id leo in vitae turpis massa sed. Dictum sit amet justo donec enim diam vulputate ut. Felis eget velit aliquet sagittis id consectetur. Venenatis cras sed felis eget velit aliquet. Non tellus orci ac auctor augue. Id aliquet risus feugiat in ante metus dictum at tempor. Vulputate odio ut enim blandit volutpat maecenas volutpat. Urna nec tincidunt praesent semper feugiat nibh. Ac ut consequat semper viverra nam libero. Commodo quis imperdiet massa tincidunt nunc pulvinar. Purus sit amet volutpat consequat mauris nunc congue nisi. Tortor condimentum lacinia quis vel eros donec ac. Pellentesque habitant morbi tristique senectus. Ac felis donec et odio pellentesque diam volutpat. Etiam dignissim diam quis enim lobortis scelerisque fermentum dui faucibus. Quam id leo in vitae.</p> + <p>Fusce ut placerat orci nulla pellentesque dignissim enim. Mauris a diam maecenas sed. Enim praesent elementum facilisis leo vel fringilla est. Cursus metus aliquam eleifend mi in nulla posuere sollicitudin. Egestas erat imperdiet sed euismod nisi porta lorem mollis aliquam. Viverra suspendisse potenti nullam ac tortor. Tempus iaculis urna id volutpat lacus laoreet non. Sed viverra ipsum nunc aliquet. Nulla facilisi cras fermentum odio eu feugiat. Sed augue lacus viverra vitae congue. Pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat vivamus. Feugiat pretium nibh ipsum consequat nisl vel pretium. Tristique senectus et netus et malesuada fames ac. Nunc mattis enim ut tellus elementum sagittis. Quis viverra nibh cras pulvinar mattis nunc sed blandit. Sapien eget mi proin sed libero enim sed. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl vel. Aliquet enim tortor at auctor urna. Cursus turpis massa tincidunt dui ut ornare. Pulvinar elementum integer enim neque.</p> + <p>Urna nec tincidunt praesent semper feugiat. Ultrices gravida dictum fusce ut placerat orci nulla pellentesque dignissim. Phasellus faucibus scelerisque eleifend donec pretium vulputate sapien nec sagittis. Eget dolor morbi non arcu risus. Feugiat sed lectus vestibulum mattis ullamcorper velit sed. Urna et pharetra pharetra massa massa ultricies mi quis hendrerit. Nibh cras pulvinar mattis nunc sed blandit. A diam maecenas sed enim ut sem viverra aliquet eget. Sed augue lacus viverra vitae. Orci phasellus egestas tellus rutrum tellus pellentesque. Fames ac turpis egestas integer eget. Vel pretium lectus quam id leo in vitae turpis. Tincidunt lobortis feugiat vivamus at augue. Pellentesque habitant morbi tristique senectus. Elit eget gravida cum sociis natoque penatibus et magnis dis. Nunc mattis enim ut tellus elementum sagittis vitae. Enim praesent elementum facilisis leo vel fringilla est. Non pulvinar neque laoreet suspendisse interdum consectetur libero. Sit amet aliquam id diam maecenas ultricies mi. Arcu non odio euismod lacinia.</p> + <p>Ut morbi tincidunt augue interdum velit euismod in. Ac turpis egestas sed tempus. Non consectetur a erat nam at lectus urna duis. Massa vitae tortor condimentum lacinia. Vestibulum morbi blandit cursus risus at ultrices mi tempus imperdiet. Nisl nunc mi ipsum faucibus vitae. Ornare lectus sit amet est placerat in. Tempus egestas sed sed risus pretium quam vulputate. Odio ut enim blandit volutpat maecenas. Et netus et malesuada fames ac turpis egestas sed tempus. Massa ultricies mi quis hendrerit dolor magna eget est lorem. Ultrices vitae auctor eu augue ut lectus. At quis risus sed vulputate odio. Eget magna fermentum iaculis eu non diam. Et tortor at risus viverra. Sed sed risus pretium quam vulputate dignissim suspendisse in est. Libero volutpat sed cras ornare arcu dui vivamus. Et pharetra pharetra massa massa ultricies mi quis hendrerit dolor.</p> + <p>Lorem ipsum dolor sit amet. In nisl nisi scelerisque eu ultrices vitae auctor eu. Adipiscing diam donec adipiscing tristique risus nec. Elit eget gravida cum sociis natoque. Malesuada proin libero nunc consequat interdum varius sit amet. Nulla facilisi nullam vehicula ipsum a arcu cursus. Ultricies tristique nulla aliquet enim tortor at auctor urna. Nunc faucibus a pellentesque sit amet porttitor. Turpis in eu mi bibendum neque egestas. Rutrum quisque non tellus orci ac auctor augue mauris augue. Et molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit. Nec nam aliquam sem et. Egestas erat imperdiet sed euismod. Felis bibendum ut tristique et egestas quis. Nulla pellentesque dignissim enim sit amet venenatis urna. Euismod in pellentesque massa placerat duis ultricies lacus sed. At risus viverra adipiscing at in tellus integer feugiat. Ut lectus arcu bibendum at varius vel pharetra vel turpis. Vulputate dignissim suspendisse in est ante in nibh.</p> + <p>Accumsan tortor posuere ac ut consequat semper viverra nam libero. Vitae congue eu consequat ac felis. Nullam ac tortor vitae purus faucibus ornare suspendisse sed. Convallis convallis tellus id interdum. Vel elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. Tortor id aliquet lectus proin. Aliquam malesuada bibendum arcu vitae elementum curabitur vitae. Enim sed faucibus turpis in. Dignissim convallis aenean et tortor. Quisque sagittis purus sit amet volutpat. Facilisis volutpat est velit egestas dui. Ipsum nunc aliquet bibendum enim facilisis gravida neque convallis a.</p> + <p>Sit amet consectetur adipiscing elit ut aliquam purus. Dolor purus non enim praesent elementum facilisis leo vel fringilla. Ac tortor dignissim convallis aenean et tortor at risus viverra. Consectetur adipiscing elit duis tristique sollicitudin. Viverra justo nec ultrices dui sapien eget mi proin sed. Massa tempor nec feugiat nisl pretium. Morbi tincidunt ornare massa eget egestas purus. Est lorem ipsum dolor sit amet consectetur adipiscing elit. Natoque penatibus et magnis dis parturient montes nascetur ridiculus. Vulputate enim nulla aliquet porttitor lacus.</p> + <p>Mauris pharetra et ultrices neque ornare aenean. Ut sem nulla pharetra diam sit amet nisl. Eu sem integer vitae justo eget magna. Vitae justo eget magna fermentum iaculis. Quisque id diam vel quam elementum. Bibendum enim facilisis gravida neque convallis a. Ultricies tristique nulla aliquet enim tortor at. Id diam maecenas ultricies mi eget mauris pharetra et ultrices. Urna condimentum mattis pellentesque id nibh tortor id aliquet. Vitae tortor condimentum lacinia quis vel. Amet luctus venenatis lectus magna fringilla. Aliquam ultrices sagittis orci a scelerisque purus. Condimentum mattis pellentesque id nibh tortor. Diam volutpat commodo sed egestas egestas fringilla phasellus faucibus. Rhoncus est pellentesque elit ullamcorper dignissim cras tincidunt lobortis feugiat. Blandit aliquam etiam erat velit. Morbi blandit cursus risus at. Massa placerat duis ultricies lacus sed turpis tincidunt id. At lectus urna duis convallis convallis.</p> + <p>Faucibus a pellentesque sit amet. Massa placerat duis ultricies lacus sed. At in tellus integer feugiat. Nibh sit amet commodo nulla facilisi nullam vehicula ipsum. At tellus at urna condimentum mattis pellentesque. Arcu bibendum at varius vel. Euismod in pellentesque massa placerat duis. Augue lacus viverra vitae congue eu consequat. Vel risus commodo viverra maecenas accumsan lacus vel. Amet facilisis magna etiam tempor orci eu lobortis elementum nibh. Eget est lorem ipsum dolor sit amet consectetur adipiscing elit. Arcu ac tortor dignissim convallis aenean et tortor at risus. Massa ultricies mi quis hendrerit dolor magna. Et netus et malesuada fames ac turpis. Egestas quis ipsum suspendisse ultrices gravida dictum fusce ut placerat. Massa enim nec dui nunc mattis enim ut. Est ante in nibh mauris cursus mattis molestie a iaculis. Ut eu sem integer vitae justo eget magna fermentum iaculis. Amet venenatis urna cursus eget nunc scelerisque viverra mauris.</p> + <p>Malesuada pellentesque elit eget gravida cum sociis natoque penatibus. Proin fermentum leo vel orci. Sagittis purus sit amet volutpat consequat. Id semper risus in hendrerit gravida. Senectus et netus et malesuada fames ac turpis. Dolor purus non enim praesent. Phasellus egestas tellus rutrum tellus pellentesque eu tincidunt. Lacus suspendisse faucibus interdum posuere. Mollis aliquam ut porttitor leo. Sollicitudin aliquam ultrices sagittis orci a scelerisque purus. Arcu non sodales neque sodales ut etiam sit amet. Id eu nisl nunc mi ipsum faucibus vitae. Nibh sed pulvinar proin gravida hendrerit. Massa tincidunt nunc pulvinar sapien et ligula ullamcorper malesuada. Hac habitasse platea dictumst vestibulum. Dictum at tempor commodo ullamcorper a lacus. Potenti nullam ac tortor vitae purus faucibus ornare suspendisse.</p> + <p>Massa enim nec dui nunc mattis enim. Viverra orci sagittis eu volutpat odio facilisis mauris sit. Egestas sed sed risus pretium quam. Tempus quam pellentesque nec nam aliquam sem et tortor consequat. Enim nulla aliquet porttitor lacus. Leo urna molestie at elementum eu facilisis. Potenti nullam ac tortor vitae purus. Tristique et egestas quis ipsum suspendisse ultrices gravida dictum fusce. Aliquam ut porttitor leo a. Nisi vitae suscipit tellus mauris a diam maecenas sed.</p> + <p>Dui sapien eget mi proin sed libero enim. Iaculis at erat pellentesque adipiscing commodo elit at. Ligula ullamcorper malesuada proin libero nunc consequat interdum. Mattis rhoncus urna neque viverra justo nec ultrices. Aenean sed adipiscing diam donec adipiscing tristique. Massa vitae tortor condimentum lacinia quis vel eros. Viverra nibh cras pulvinar mattis nunc sed blandit libero volutpat. Feugiat pretium nibh ipsum consequat nisl vel pretium lectus. Amet facilisis magna etiam tempor orci eu lobortis elementum nibh. Scelerisque viverra mauris in aliquam. Eu turpis egestas pretium aenean pharetra magna. Ullamcorper eget nulla facilisi etiam dignissim diam. Massa id neque aliquam vestibulum morbi. Non arcu risus quis varius quam quisque id diam vel. Ipsum suspendisse ultrices gravida dictum. Mauris pharetra et ultrices neque ornare aenean euismod. Convallis tellus id interdum velit. Tristique sollicitudin nibh sit amet commodo nulla facilisi nullam vehicula. Faucibus in ornare quam viverra orci sagittis eu volutpat odio.</p> + <p>Cras sed felis eget velit aliquet sagittis. Ut placerat orci nulla pellentesque dignissim enim sit. Vitae nunc sed velit dignissim sodales. Volutpat sed cras ornare arcu dui vivamus arcu felis. Pharetra sit amet aliquam id diam. Blandit volutpat maecenas volutpat blandit aliquam etiam erat velit. Dui id ornare arcu odio ut sem. Eleifend mi in nulla posuere sollicitudin aliquam ultrices sagittis orci. Sed vulputate mi sit amet mauris. Lorem dolor sed viverra ipsum nunc aliquet bibendum enim facilisis. Lobortis mattis aliquam faucibus purus in massa tempor nec feugiat. Proin nibh nisl condimentum id venenatis.</p> + <p>Purus non enim praesent elementum facilisis. Purus sit amet volutpat consequat mauris nunc. Placerat orci nulla pellentesque dignissim enim sit amet venenatis. Arcu risus quis varius quam quisque id diam vel. Nulla at volutpat diam ut venenatis tellus in metus vulputate. Vel turpis nunc eget lorem dolor sed viverra. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Ultrices vitae auctor eu augue ut lectus arcu bibendum. Augue lacus viverra vitae congue eu consequat ac felis donec. Donec ac odio tempor orci dapibus. Nisi porta lorem mollis aliquam ut porttitor leo. Sed vulputate mi sit amet mauris commodo quis imperdiet massa.</p> + <p>Consectetur adipiscing elit duis tristique sollicitudin. Nunc id cursus metus aliquam eleifend. Id velit ut tortor pretium viverra suspendisse potenti nullam. Cursus turpis massa tincidunt dui ut ornare lectus. Nulla posuere sollicitudin aliquam ultrices. Viverra accumsan in nisl nisi scelerisque. Pulvinar etiam non quam lacus suspendisse. Velit euismod in pellentesque massa placerat duis ultricies lacus. Quis auctor elit sed vulputate. Gravida cum sociis natoque penatibus et magnis.</p> + <p>Fermentum posuere urna nec tincidunt praesent semper feugiat nibh. Et leo duis ut diam quam nulla porttitor. Suspendisse interdum consectetur libero id faucibus nisl. Elit duis tristique sollicitudin nibh sit amet. Et egestas quis ipsum suspendisse ultrices gravida dictum fusce. Eu facilisis sed odio morbi. Tellus pellentesque eu tincidunt tortor. Id porta nibh venenatis cras sed felis eget velit. Massa tincidunt dui ut ornare lectus sit. Venenatis tellus in metus vulputate. Egestas quis ipsum suspendisse ultrices gravida dictum fusce ut. Aliquet lectus proin nibh nisl condimentum id venenatis a condimentum. Id ornare arcu odio ut. Donec massa sapien faucibus et. Et leo duis ut diam quam nulla porttitor massa. Id venenatis a condimentum vitae sapien pellentesque habitant morbi. Sed id semper risus in. Vitae nunc sed velit dignissim sodales ut eu. Euismod elementum nisi quis eleifend quam. Aliquet enim tortor at auctor urna nunc id cursus.</p> + <p>Elementum nibh tellus molestie nunc non blandit massa. In ornare quam viverra orci sagittis. Sapien faucibus et molestie ac feugiat sed lectus vestibulum mattis. Ut eu sem integer vitae justo eget magna fermentum iaculis. Vel eros donec ac odio tempor. Venenatis a condimentum vitae sapien pellentesque habitant morbi tristique senectus. Pellentesque pulvinar pellentesque habitant morbi tristique senectus. Cursus turpis massa tincidunt dui ut. Tristique et egestas quis ipsum suspendisse ultrices. Nunc consequat interdum varius sit amet mattis vulputate enim. Nulla malesuada pellentesque elit eget gravida cum. Eget aliquet nibh praesent tristique magna sit amet. Malesuada pellentesque elit eget gravida cum sociis natoque penatibus et. Sagittis vitae et leo duis ut diam quam nulla. Venenatis lectus magna fringilla urna porttitor. In pellentesque massa placerat duis ultricies lacus sed.</p> + <p>Odio facilisis mauris sit amet massa vitae tortor condimentum lacinia. Platea dictumst quisque sagittis purus sit amet. Pharetra convallis posuere morbi leo urna molestie at. Nullam eget felis eget nunc lobortis mattis. Cras semper auctor neque vitae tempus quam pellentesque nec nam. Porttitor rhoncus dolor purus non enim. Lorem ipsum dolor sit amet. Urna molestie at elementum eu facilisis. Magna eget est lorem ipsum dolor. Suspendisse faucibus interdum posuere lorem ipsum dolor sit amet consectetur. Ipsum faucibus vitae aliquet nec ullamcorper sit amet risus nullam. Elementum sagittis vitae et leo duis ut diam quam nulla. Dolor morbi non arcu risus quis varius quam quisque. Nunc aliquet bibendum enim facilisis gravida neque convallis a.</p> + <p>Tortor consequat id porta nibh venenatis cras sed felis. Congue eu consequat ac felis donec. Risus at ultrices mi tempus. Et magnis dis parturient montes nascetur ridiculus mus mauris vitae. Malesuada proin libero nunc consequat interdum varius sit amet mattis. Turpis tincidunt id aliquet risus feugiat in ante. Lectus proin nibh nisl condimentum id venenatis. Aliquam ultrices sagittis orci a. Netus et malesuada fames ac turpis. Suspendisse ultrices gravida dictum fusce ut placerat orci nulla pellentesque. Arcu ac tortor dignissim convallis aenean et tortor at. Dignissim sodales ut eu sem integer. Auctor urna nunc id cursus metus aliquam eleifend. Nulla facilisi morbi tempus iaculis urna id volutpat. Bibendum ut tristique et egestas quis ipsum suspendisse. Magna etiam tempor orci eu lobortis elementum nibh tellus molestie. Magna fermentum iaculis eu non diam phasellus vestibulum lorem.</p> + <p>Laoreet non curabitur gravida arcu. Egestas tellus rutrum tellus pellentesque. Platea dictumst vestibulum rhoncus est pellentesque elit. Praesent elementum facilisis leo vel fringilla est ullamcorper. Augue eget arcu dictum varius duis at consectetur lorem donec. Vulputate enim nulla aliquet porttitor lacus. Volutpat commodo sed egestas egestas fringilla phasellus faucibus. Non curabitur gravida arcu ac tortor dignissim convallis aenean. Amet nisl purus in mollis nunc. Feugiat in ante metus dictum at tempor commodo ullamcorper a. Mauris cursus mattis molestie a iaculis at erat. Amet porttitor eget dolor morbi non arcu risus. Velit aliquet sagittis id consectetur purus ut faucibus. Pulvinar etiam non quam lacus suspendisse faucibus interdum. Euismod nisi porta lorem mollis aliquam ut porttitor. Vulputate odio ut enim blandit volutpat maecenas. Commodo nulla facilisi nullam vehicula ipsum a. Nunc vel risus commodo viverra maecenas accumsan lacus vel facilisis.</p> + <p>Fringilla ut morbi tincidunt augue interdum velit euismod in pellentesque. Arcu risus quis varius quam quisque id. Amet est placerat in egestas erat. Cras sed felis eget velit aliquet. Netus et malesuada fames ac turpis egestas. Quis varius quam quisque id diam vel quam elementum pulvinar. Diam vel quam elementum pulvinar. Mattis rhoncus urna neque viverra justo. Tincidunt eget nullam non nisi. Velit dignissim sodales ut eu sem integer vitae. Feugiat in fermentum posuere urna nec tincidunt. Quam pellentesque nec nam aliquam sem et tortor. In nisl nisi scelerisque eu ultrices vitae auctor eu augue.</p> + <p>Commodo sed egestas egestas fringilla phasellus faucibus scelerisque. Non diam phasellus vestibulum lorem sed risus ultricies. Dignissim convallis aenean et tortor at. Purus sit amet volutpat consequat mauris nunc congue nisi. Faucibus nisl tincidunt eget nullam non nisi est sit amet. Amet consectetur adipiscing elit ut aliquam purus sit amet luctus. Amet consectetur adipiscing elit ut aliquam purus. Nulla facilisi cras fermentum odio eu feugiat pretium nibh ipsum. Sed felis eget velit aliquet sagittis id. Felis eget nunc lobortis mattis aliquam faucibus purus in. Ornare quam viverra orci sagittis. Urna condimentum mattis pellentesque id nibh tortor id. Non consectetur a erat nam at lectus. Nisl suscipit adipiscing bibendum est ultricies integer. Urna nec tincidunt praesent semper. Venenatis urna cursus eget nunc scelerisque viverra mauris. Ut aliquam purus sit amet luctus venenatis lectus. Mattis rhoncus urna neque viverra justo nec ultrices dui sapien. Ac orci phasellus egestas tellus rutrum.</p> + <p>Leo vel orci porta non pulvinar. At auctor urna nunc id cursus metus. Massa id neque aliquam vestibulum morbi blandit. Ut eu sem integer vitae justo. Neque laoreet suspendisse interdum consectetur libero id faucibus. Duis ut diam quam nulla porttitor massa id. Justo eget magna fermentum iaculis eu non diam phasellus vestibulum. Nec nam aliquam sem et tortor consequat. Nisi scelerisque eu ultrices vitae auctor eu augue. Aliquet bibendum enim facilisis gravida neque convallis. Morbi non arcu risus quis varius quam quisque id.</p> + <p>Nulla facilisi morbi tempus iaculis urna. Elementum nisi quis eleifend quam adipiscing vitae proin sagittis nisl. Enim diam vulputate ut pharetra sit. Parturient montes nascetur ridiculus mus. Et molestie ac feugiat sed. Quis risus sed vulputate odio ut enim blandit volutpat maecenas. Imperdiet dui accumsan sit amet nulla facilisi morbi tempus. Lorem sed risus ultricies tristique nulla aliquet enim tortor. Feugiat scelerisque varius morbi enim. Ac placerat vestibulum lectus mauris. Purus in mollis nunc sed. Lorem ipsum dolor sit amet consectetur adipiscing. Arcu cursus vitae congue mauris rhoncus aenean vel.</p> + <p>Enim lobortis scelerisque fermentum dui faucibus in. Cras ornare arcu dui vivamus arcu felis bibendum ut tristique. Diam sit amet nisl suscipit. Laoreet suspendisse interdum consectetur libero id faucibus nisl tincidunt. Sed felis eget velit aliquet sagittis id consectetur purus. Id volutpat lacus laoreet non curabitur. Fermentum odio eu feugiat pretium nibh. Vestibulum lectus mauris ultrices eros in cursus turpis massa tincidunt. Et sollicitudin ac orci phasellus egestas tellus. Pellentesque habitant morbi tristique senectus et netus et. Sit amet volutpat consequat mauris. At augue eget arcu dictum varius duis at consectetur. Eget nunc lobortis mattis aliquam faucibus purus in massa. Fringilla ut morbi tincidunt augue interdum velit euismod in. Eu mi bibendum neque egestas congue. Id venenatis a condimentum vitae sapien pellentesque. Odio facilisis mauris sit amet massa vitae tortor condimentum. Egestas tellus rutrum tellus pellentesque eu tincidunt tortor aliquam. Nascetur ridiculus mus mauris vitae ultricies leo. Felis eget nunc lobortis mattis aliquam faucibus purus in massa.</p> + <p>Tristique risus nec feugiat in fermentum. Consectetur lorem donec massa sapien faucibus et. Ac placerat vestibulum lectus mauris. Mauris pellentesque pulvinar pellentesque habitant morbi. Morbi tempus iaculis urna id volutpat lacus laoreet. Vitae semper quis lectus nulla at volutpat diam ut. Eu non diam phasellus vestibulum lorem. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Nascetur ridiculus mus mauris vitae ultricies leo integer. Vitae turpis massa sed elementum.</p> +</body> +</html> diff --git a/src/Tools/dotnet-watch/README.md b/src/Tools/dotnet-watch/README.md index d3944206fa087291e3513b04ce44307147c1233f..944edb91c48980e38bece27bc7c8196b7f1eefdd 100644 --- a/src/Tools/dotnet-watch/README.md +++ b/src/Tools/dotnet-watch/README.md @@ -30,7 +30,8 @@ Some configuration options can be passed to `dotnet watch` through environment v | ---------------------------------------------- | -------------------------------------------------------- | | DOTNET_USE_POLLING_FILE_WATCHER | If set to "1" or "true", `dotnet watch` will use a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes. | | DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM | By default, `dotnet watch` optimizes the build by avoiding certain operations such as running restore or re-evaluating the set of watched files on every file change. If set to "1" or "true", these optimizations are disabled. | - +| DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER | `dotnet watch run` will attempt to launch browsers for web apps with `launchBrowser` configured in `launchSettings.json`. If set to "1" or "true", this behavior is suppressed. | +| DOTNET_WATCH_SUPPRESS_MSBUILD_INCREMENTALISM | `dotnet watch run` will attempt to refresh browsers when it detects file changes. If set to "1" or "true", this behavior is suppressed. This behavior is also suppressed if DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER is set. | ### MSBuild dotnet-watch can be configured from the MSBuild project file being watched. diff --git a/src/Tools/dotnet-watch/src/BrowserRefreshServer.cs b/src/Tools/dotnet-watch/src/BrowserRefreshServer.cs index f173980dba3dd132d0eb01895733e9cc009919f8..e0bd940ccabe85eedffd2bc8614144fffd5c851e 100644 --- a/src/Tools/dotnet-watch/src/BrowserRefreshServer.cs +++ b/src/Tools/dotnet-watch/src/BrowserRefreshServer.cs @@ -84,7 +84,7 @@ namespace Microsoft.DotNet.Watcher.Tools } catch (Exception ex) { - _reporter.Output($"Refresh server error: {ex}"); + _reporter.Verbose($"Refresh server error: {ex}"); } } @@ -98,10 +98,9 @@ namespace Microsoft.DotNet.Watcher.Tools if (_refreshServer != null) { - await _refreshServer.StopAsync(); _refreshServer.Dispose(); } - + _taskCompletionSource.TrySetResult(); } } diff --git a/src/Tools/dotnet-watch/src/LaunchBrowserFilter.cs b/src/Tools/dotnet-watch/src/LaunchBrowserFilter.cs index de8b189f3f9c83f73e955c19d52659bc770b2762..8e524780fd50beb169695618197dce1029acfdc1 100644 --- a/src/Tools/dotnet-watch/src/LaunchBrowserFilter.cs +++ b/src/Tools/dotnet-watch/src/LaunchBrowserFilter.cs @@ -20,10 +20,11 @@ namespace Microsoft.DotNet.Watcher.Tools private readonly byte[] ReloadMessage = Encoding.UTF8.GetBytes("Reload"); private readonly byte[] WaitMessage = Encoding.UTF8.GetBytes("Wait"); private static readonly Regex NowListeningRegex = new Regex(@"^\s*Now listening on: (?<url>.*)$", RegexOptions.None | RegexOptions.Compiled, TimeSpan.FromSeconds(10)); - private readonly bool _runningInTest; private readonly bool _suppressLaunchBrowser; + private readonly bool _suppressBrowserRefresh; private readonly string _browserPath; + private bool _canLaunchBrowser; private Process _browserProcess; private bool _browserLaunched; @@ -36,6 +37,10 @@ namespace Microsoft.DotNet.Watcher.Tools { var suppressLaunchBrowser = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_LAUNCH_BROWSER"); _suppressLaunchBrowser = (suppressLaunchBrowser == "1" || suppressLaunchBrowser == "true"); + + var suppressBrowserRefresh = Environment.GetEnvironmentVariable("DOTNET_WATCH_SUPPRESS_BROWSER_REFRESH"); + _suppressBrowserRefresh = (suppressBrowserRefresh == "1" || suppressBrowserRefresh == "true"); + _runningInTest = Environment.GetEnvironmentVariable("__DOTNET_WATCH_RUNNING_AS_TEST") == "true"; _browserPath = Environment.GetEnvironmentVariable("DOTNET_WATCH_BROWSER_PATH"); } @@ -57,14 +62,20 @@ namespace Microsoft.DotNet.Watcher.Tools _canLaunchBrowser = true; _launchPath = launchPath; _cancellationToken = cancellationToken; + context.ProcessSpec.OnOutput += OnOutput; - _refreshServer = new BrowserRefreshServer(context.Reporter); - var serverUrl = await _refreshServer.StartAsync(cancellationToken); + if (!_suppressBrowserRefresh) + { + _refreshServer = new BrowserRefreshServer(context.Reporter); + var serverUrl = await _refreshServer.StartAsync(cancellationToken); - context.Reporter.Verbose($"Refresh server running at {serverUrl}."); - context.ProcessSpec.EnvironmentVariables["DOTNET_WATCH_REFRESH_URL"] = serverUrl; + context.Reporter.Verbose($"Refresh server running at {serverUrl}."); + context.ProcessSpec.EnvironmentVariables["ASPNETCORE_AUTO_RELOAD_WS_ENDPOINT"] = serverUrl; - context.ProcessSpec.OnOutput += OnOutput; + var pathToMiddleware = Path.Combine(AppContext.BaseDirectory, "middleware", "Microsoft.AspNetCore.Watch.BrowserRefresh.dll"); + context.ProcessSpec.EnvironmentVariables["DOTNET_STARTUP_HOOKS"] = pathToMiddleware; + context.ProcessSpec.EnvironmentVariables["ASPNETCORE_HOSTINGSTARTUPASSEMBLIES"] = "Microsoft.AspNetCore.Watch.BrowserRefresh"; + } } } @@ -73,11 +84,21 @@ namespace Microsoft.DotNet.Watcher.Tools if (context.Iteration > 0) { // We've detected a change. Notify the browser. - await _refreshServer.SendMessage(WaitMessage, cancellationToken); + await SendMessage(WaitMessage, cancellationToken); } } } + private Task SendMessage(byte[] message, CancellationToken cancellationToken) + { + if (_refreshServer is null) + { + return Task.CompletedTask; + } + + return _refreshServer.SendMessage(message, cancellationToken); + } + private void OnOutput(object sender, DataReceivedEventArgs eventArgs) { // We've redirected the output, but want to ensure that it continues to appear in the user's console. @@ -114,7 +135,7 @@ namespace Microsoft.DotNet.Watcher.Tools else { _reporter.Verbose("Reloading browser."); - _ = _refreshServer.SendMessage(ReloadMessage, _cancellationToken); + _ = SendMessage(ReloadMessage, _cancellationToken); } } } diff --git a/src/Tools/dotnet-watch/src/dotnet-watch.csproj b/src/Tools/dotnet-watch/src/dotnet-watch.csproj index ab64b4460c2728f6577f36740e9308ab2cba9478..4127085da79a545cd1dfb58733bec4a009bb316a 100644 --- a/src/Tools/dotnet-watch/src/dotnet-watch.csproj +++ b/src/Tools/dotnet-watch/src/dotnet-watch.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> @@ -22,8 +22,37 @@ <ItemGroup> <Reference Include="Microsoft.AspNetCore" /> <Reference Include="Microsoft.AspNetCore.WebSockets" /> + + <ProjectReference + Include="..\BrowserRefresh\src\Microsoft.AspNetCore.Watch.BrowserRefresh.csproj" + PrivateAssets="All" + ReferenceOutputAssembly="false" + SkipGetTargetFrameworkProperties="true" + UndefineProperties="TargetFramework;TargetFrameworks" /> </ItemGroup> + <Target Name="_CopyMiddlewareForLocal" BeforeTargets="PrepareForRun"> + <PropertyGroup> + <BrowserMiddlewarePath>$(ArtifactsBinDir)Microsoft.AspNetCore.Watch.BrowserRefresh\$(Configuration)\netcoreapp3.1\Microsoft.AspNetCore.Watch.BrowserRefresh.dll</BrowserMiddlewarePath> + </PropertyGroup> + + <ItemGroup> + <None Include="$(BrowserMiddlewarePath)" Pack="true" PackagePath="tools\$(DefaultNetCoreTargetFramework)\any\middleware"/> + </ItemGroup> + + <Copy + SourceFiles="$(BrowserMiddlewarePath)" + DestinationFolder="$(OutputPath)middleware" + OverwriteReadOnlyFiles="true" + SkipUnchangedFiles="true"/> + </Target> + + <Target Name="PopulateNuspec" BeforeTargets="InitializeStandardNuspecProperties"> + <ItemGroup> + <None Include="$(ArtifactsBinDir)Microsoft.AspNetCore.Watch.BrowserRefresh\$(Configuration)\netcoreapp3.1\Microsoft.AspNetCore.Watch.BrowserRefresh.dll" Pack="true" PackagePath="middleware"/> + </ItemGroup> + </Target> + <Target Name="_FixupRuntimeConfig" BeforeTargets="_GenerateRuntimeConfigurationFilesInputCache"> <ItemGroup> <_RuntimeFramework Include="@(RuntimeFramework)" /> diff --git a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj index 200f875a302a30f89c16734ad0e4fc91001e30e9..d171daa39d847a7366f9f2d0046a7162dba0e0fb 100644 --- a/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj +++ b/src/Tools/dotnet-watch/test/dotnet-watch.Tests.csproj @@ -32,6 +32,9 @@ <MSBuild Projects="..\src\dotnet-watch.csproj" Targets="Publish" Properties="PublishDir=$(OutputPath)\tool\;Configuration=$(Configuration)" /> + + <Copy SourceFiles="$(ArtifactsBinDir)Microsoft.AspNetCore.Watch.BrowserRefresh\$(Configuration)\netcoreapp3.1\Microsoft.AspNetCore.Watch.BrowserRefresh.dll" + DestinationFolder="$(OutputPath)\tool\middleware" /> </Target> <!-- Do not publish in source build --> @@ -39,6 +42,9 @@ <MSBuild Projects="..\src\dotnet-watch.csproj" Targets="Publish" Properties="PublishDir=$(PublishDir)\tool\;Configuration=$(Configuration)" /> + + <Copy SourceFiles="$(ArtifactsBinDir)Microsoft.AspNetCore.Watch.BrowserRefresh\$(Configuration)\netcoreapp3.1\Microsoft.AspNetCore.Watch.BrowserRefresh.dll" + DestinationFolder="$(PublishDir)\tool\middleware" /> </Target> </Project>