├── korebuild-lock.txt ├── NuGetPackageVerifier.json ├── korebuild.json ├── CONTRIBUTING.md ├── NuGet.config ├── README.md ├── run.cmd ├── CODE-OF-CONDUCT.md ├── .vsts-pipelines └── builds │ ├── ci-internal.yml │ └── ci-public.yml ├── test ├── Directory.Build.props └── Microsoft.VisualStudio.Web.BrowserLink.Test │ ├── Microsoft.VisualStudio.Web.BrowserLink.Test.csproj │ ├── AssertWithMessage.cs │ ├── PathUtilTest.cs │ ├── MockScriptInjectionFilterContext.cs │ ├── MockResponseHandler.cs │ ├── MockHttpSocketAdapter.cs │ ├── TaskAssert.cs │ ├── ContentTypeUtilTest.cs │ ├── MockSocketAdapter.cs │ ├── TaskHelpersTest.cs │ └── DelayConnectingHttpSocketAdapterTest.cs ├── .gitignore ├── src └── Microsoft.VisualStudio.Web.BrowserLink │ ├── Properties │ └── AssemblyInfo.cs │ ├── StaticTaskResult.cs │ ├── Microsoft.VisualStudio.Web.BrowserLink.csproj │ ├── BrowserLinkMiddlewareFactory.cs │ ├── SendFilesWrapper.cs │ ├── PathUtil.cs │ ├── NativeMethods.cs │ ├── FailedConnectionHttpSocketAdapter.cs │ ├── ScriptInjectionFilterContext.cs │ ├── ContentTypeUtil.cs │ ├── PageExecutionContext.cs │ ├── BrowserLinkExtensions.cs │ ├── HostConnectionData.cs │ ├── TaskHelpers.cs │ ├── HttpAdapterRequestStream.cs │ ├── PageExecutionListenerFeature.cs │ ├── BrowserLinkMiddleWareUtil.cs │ ├── MappingDataWriter.cs │ ├── DelayConnectingHttpSocketAdapter.cs │ ├── baseline.netcore.json │ ├── SocketAdapter.cs │ ├── Common │ └── ArteryConstants.cs │ ├── SocketReader.cs │ ├── TextWriterDecorator.cs │ ├── BrowserLinkMiddleWare.cs │ ├── ScriptInjectionFilterStream.cs │ └── RevolvingBuffers.cs ├── Directory.Build.targets ├── version.props ├── SECURITY.md ├── .gitattributes ├── Directory.Build.props ├── BrowserLink.sln ├── run.ps1 ├── run.sh └── LICENSE.txt /korebuild-lock.txt: -------------------------------------------------------------------------------- 1 | version:3.0.0-alpha1-20181004.7 2 | commithash:27fabdaf2b1d4753c3d2749581694ca65d78f7f2 3 | -------------------------------------------------------------------------------- /NuGetPackageVerifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "Default": { 3 | "rules": [ 4 | "DefaultCompositeRule" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /korebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 3 | "channel": "master" 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. 5 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | BrowserLink 2 | ===== 3 | 4 | This project is part of ASP.NET Core and Visual Studio. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. 5 | -------------------------------------------------------------------------------- /run.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the code of conduct defined by the Contributor Covenant 4 | to clarify expected behavior in our community. 5 | 6 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). 7 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-internal.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | - release/* 4 | 5 | resources: 6 | repositories: 7 | - repository: buildtools 8 | type: git 9 | name: aspnet-BuildTools 10 | ref: refs/heads/master 11 | 12 | phases: 13 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 14 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-public.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - main 3 | - release/* 4 | 5 | # See https://github.com/aspnet/BuildTools 6 | resources: 7 | repositories: 8 | - repository: buildtools 9 | type: github 10 | endpoint: DotNet-Bot GitHub Connection 11 | name: aspnet/BuildTools 12 | ref: refs/heads/master 13 | 14 | phases: 15 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 16 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netcoreapp2.2 6 | $(DeveloperBuildTestTfms) 7 | 8 | $(StandardTestTfms);net461 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | .testPublish/ 6 | *.sln.ide/ 7 | _ReSharper.*/ 8 | packages/ 9 | artifacts/ 10 | .build/ 11 | PublishProfiles/ 12 | *.user 13 | *.suo 14 | *.cache 15 | *.docstates 16 | _ReSharper.* 17 | nuget.exe 18 | project.lock.json 19 | *net45.csproj 20 | *net451.csproj 21 | *k10.csproj 22 | *.psess 23 | *.vsp 24 | *.pidb 25 | *.userprefs 26 | *DS_Store 27 | *.ncrunchsolution 28 | *.*sdf 29 | *.ipch 30 | .vs/ 31 | global.json 32 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Microsoft.VisualStudio.Web.BrowserLink.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] 7 | 8 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/StaticTaskResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | internal static class StaticTaskResult 9 | { 10 | public static readonly Task True = Task.FromResult(true); 11 | public static readonly Task False = Task.FromResult(false); 12 | public static readonly Task NullString = Task.FromResult((string)null); 13 | public static readonly Task Complete = True; 14 | 15 | public static readonly Task HttpInternalServerError = Task.FromResult(500); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MicrosoftNETCoreApp20PackageVersion) 4 | $(MicrosoftNETCoreApp21PackageVersion) 5 | $(MicrosoftNETCoreApp22PackageVersion) 6 | $(NETStandardLibrary20PackageVersion) 7 | 8 | 99.9 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/Microsoft.VisualStudio.Web.BrowserLink.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3.0.0 4 | alpha1 5 | $(VersionPrefix) 6 | $(VersionPrefix)-$(VersionSuffix)-final 7 | t000 8 | a- 9 | $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) 10 | $(VersionSuffix)-$(BuildNumber) 11 | 12 | 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | The .NET Core and ASP.NET Core support policy, including supported versions can be found at the [.NET Core Support Policy Page](https://dotnet.microsoft.com/platform/support/policy/dotnet-core). 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Security issues and bugs should be reported privately to the Microsoft Security Response Center (MSRC), either by emailing secure@microsoft.com or via the portal at https://msrc.microsoft.com. 10 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your 11 | original message. Further information, including the MSRC PGP key, can be found in the [MSRC Report an Issue FAQ](https://www.microsoft.com/en-us/msrc/faqs-report-an-issue). 12 | 13 | Reports via MSRC may qualify for the .NET Core Bug Bounty. Details of the .NET Core Bug Bounty including terms and conditions are at [https://aka.ms/corebounty](https://aka.ms/corebounty). 14 | 15 | Please do not open issues for anything you think might have a security implication. 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | 52 | *.sh eol=lf 53 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/Microsoft.VisualStudio.Web.BrowserLink.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | A middleware that supports creating a communication channel between the development environment and one or more web browsers. 5 | netstandard2.0 6 | true 7 | aspnetcore;browserlink 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Microsoft Visual Studio 12 | https://github.com/aspnet/BrowserLink 13 | git 14 | $(MSBuildThisFileDirectory) 15 | $(MSBuildThisFileDirectory)build\Key.snk 16 | true 17 | true 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/BrowserLinkMiddlewareFactory.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | /// 9 | /// An instance of this class is created when Browser Link is registered 10 | /// in an application. It's job is to remember the base path of the 11 | /// application, so it can be passed on to the BrowserLinkMiddleware. 12 | /// 13 | internal class BrowserLinkMiddlewareFactory 14 | { 15 | private string _applicationBasePath; 16 | 17 | internal BrowserLinkMiddlewareFactory(string applicationBasePath) 18 | { 19 | _applicationBasePath = applicationBasePath; 20 | } 21 | 22 | public RequestDelegate CreateBrowserLinkMiddleware(RequestDelegate next) 23 | { 24 | BrowserLinkMiddleware middleware = new BrowserLinkMiddleware(_applicationBasePath, next); 25 | 26 | return middleware.Invoke; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/SendFilesWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Http.Features; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | internal class SendFilesWrapper : IHttpSendFileFeature 10 | { 11 | private HttpResponse _response; 12 | private IHttpSendFileFeature _wrapped; 13 | 14 | internal SendFilesWrapper(IHttpSendFileFeature wrapped, HttpResponse response) 15 | { 16 | _wrapped = wrapped; 17 | _response = response; 18 | } 19 | 20 | async Task IHttpSendFileFeature.SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) 21 | { 22 | // TODO: Send mapping data to VS 23 | 24 | if (_wrapped != null) 25 | { 26 | await _wrapped.SendFileAsync(path, offset, count, cancellation); 27 | return; 28 | } 29 | 30 | using (Stream readStream = File.OpenRead(path)) 31 | { 32 | readStream.Seek(offset, SeekOrigin.Begin); 33 | 34 | await readStream.CopyToAsync(_response.Body, 4096, cancellation); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/AssertWithMessage.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Microsoft.VisualStudio.Web.BrowserLink 4 | { 5 | /// 6 | /// These wrappers accept useful messages about Assert failures, but I'm 7 | /// not sure how to output them to Xunit. 8 | /// 9 | internal static class AssertWithMessage 10 | { 11 | public static void Equal(string expected, string actual, string messageFormat, params object[] messageArgs) 12 | { 13 | Assert.Equal(expected, actual); 14 | } 15 | 16 | public static void Equal(int expected, int actual, string messageFormat, params object[] messageArgs) 17 | { 18 | Assert.Equal(expected, actual); 19 | } 20 | 21 | public static void Equal(bool expected, bool actual, string messageFormat, params object[] messageArgs) 22 | { 23 | Assert.Equal(expected, actual); 24 | } 25 | 26 | public static void Null(object @object, string messageFormat, params object[] messageArgs) 27 | { 28 | Assert.Null(@object); 29 | } 30 | 31 | public static void NotNull(object @object, string messageFormat, params object[] messageArgs) 32 | { 33 | Assert.NotNull(@object); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/PathUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | /// 9 | /// Helpers for dealing with file paths 10 | /// 11 | internal static class PathUtil 12 | { 13 | /// 14 | /// Takes a path, and returns an equivalent path in a format that can 15 | /// be used for comparisons. 16 | /// 17 | public static string NormalizeDirectoryPath(string path) 18 | { 19 | if (path.Contains("/")) 20 | { 21 | path = path.Replace('/', '\\'); 22 | } 23 | 24 | if (!path.EndsWith("\\")) 25 | { 26 | path += "\\"; 27 | } 28 | 29 | return path.ToLowerInvariant(); 30 | } 31 | 32 | /// 33 | /// Compares two paths, assuming they were both produced by NormalizeDirectoryPath 34 | /// 35 | /// True if the paths are equivalent 36 | public static bool CompareNormalizedPaths(string path1, string path2) 37 | { 38 | return String.Equals(path1, path2, StringComparison.Ordinal); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | internal static class NativeMethods 10 | { 11 | 12 | [DllImport("urlmon.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = false)] 13 | internal static extern int FindMimeFromData( 14 | IntPtr pBC, 15 | [MarshalAs(UnmanagedType.LPWStr)] string pwzUrl, 16 | [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.I1, SizeParamIndex = 3)] 17 | byte[] pBuffer, 18 | int cbSize, 19 | [MarshalAs(UnmanagedType.LPWStr)] string pwzMimeProposed, 20 | int dwMimeFlags, 21 | out IntPtr ppwzMimeOut, 22 | int dwReserved); 23 | 24 | 25 | [DllImport("kernel32.dll", SetLastError = false, CharSet = CharSet.Unicode)] 26 | internal static extern IntPtr OpenFileMapping( 27 | uint dwDesiredAccess, 28 | bool bInheritHandle, 29 | string lpName); 30 | 31 | [DllImport("kernel32.dll", SetLastError = false)] 32 | internal static extern bool CloseHandle( 33 | IntPtr hHandle); 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/PathUtilTest.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Microsoft.VisualStudio.Web.BrowserLink 4 | { 5 | public class PathUtilTest 6 | { 7 | [Fact] 8 | public void NormalizeDirectoryPath_ChangesSlashesToBackslashes() 9 | { 10 | // Arrange 11 | string input = @"c:/my/project/path/"; 12 | string expected = @"c:\my\project\path\"; 13 | 14 | // Act 15 | string result = PathUtil.NormalizeDirectoryPath(input); 16 | 17 | // Assert 18 | Assert.Equal(expected, result); 19 | } 20 | 21 | [Fact] 22 | public void NormalizeDirectoryPath_AddsTrailingSlash() 23 | { 24 | // Arrange 25 | string input = @"c:/my/project/path"; 26 | string expected = @"c:\my\project\path\"; 27 | 28 | // Act 29 | string result = PathUtil.NormalizeDirectoryPath(input); 30 | 31 | // Assert 32 | Assert.Equal(expected, result); 33 | } 34 | 35 | [Fact] 36 | public void NormalizeDirectoryPath_ChangesToLowercase() 37 | { 38 | // Arrange 39 | string input = @"C:/My/Project/Path/"; 40 | string expected = @"c:\my\project\path\"; 41 | 42 | // Act 43 | string result = PathUtil.NormalizeDirectoryPath(input); 44 | 45 | // Assert 46 | Assert.Equal(expected, result); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/FailedConnectionHttpSocketAdapter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | /// 10 | /// Null-object implementation of a connection, which stands in for a connection 11 | /// that could not be created. 12 | /// 13 | internal class FailedConnectionHttpSocketAdapter : IHttpSocketAdapter 14 | { 15 | void IHttpSocketAdapter.AddRequestHeader(string name, string value) 16 | { 17 | } 18 | 19 | Task IHttpSocketAdapter.CompleteRequest() 20 | { 21 | return StaticTaskResult.Complete; 22 | } 23 | 24 | void IDisposable.Dispose() 25 | { 26 | } 27 | 28 | Task IHttpSocketAdapter.GetResponseHeader(string headerName) 29 | { 30 | return StaticTaskResult.NullString; 31 | } 32 | 33 | Task IHttpSocketAdapter.GetResponseStatusCode() 34 | { 35 | return StaticTaskResult.HttpInternalServerError; 36 | } 37 | 38 | void IHttpSocketAdapter.SetResponseHandler(ResponseHandler handler) 39 | { 40 | } 41 | 42 | Task IHttpSocketAdapter.WaitForResponseComplete() 43 | { 44 | return StaticTaskResult.Complete; 45 | } 46 | 47 | Task IHttpSocketAdapter.WriteToRequestAsync(byte[] buffer, int offset, int count) 48 | { 49 | return StaticTaskResult.Complete; 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/MockScriptInjectionFilterContext.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace Microsoft.VisualStudio.Web.BrowserLink 5 | { 6 | public class MockScriptInjectionFilterContext : IScriptInjectionFilterContext 7 | { 8 | private MemoryStream _responseBody = new MemoryStream(); 9 | private string _requestPath; 10 | private string _contentType; 11 | 12 | public MockScriptInjectionFilterContext(string requestPath = "http://localhost:2468/Default.html", string contentType = "text/html") 13 | { 14 | _requestPath = requestPath; 15 | _contentType = contentType; 16 | } 17 | 18 | public string GetResponseBody(Encoding encoding) 19 | { 20 | long originalPosition = _responseBody.Position; 21 | 22 | try 23 | { 24 | _responseBody.Seek(0, SeekOrigin.Begin); 25 | 26 | byte[] buffer = new byte[_responseBody.Length]; 27 | _responseBody.Read(buffer, 0, buffer.Length); 28 | 29 | return encoding.GetString(buffer, 0, buffer.Length); 30 | } 31 | finally 32 | { 33 | _responseBody.Position = originalPosition; 34 | } 35 | } 36 | 37 | string IScriptInjectionFilterContext.RequestPath 38 | { 39 | get { return _requestPath; } 40 | } 41 | 42 | Stream IScriptInjectionFilterContext.ResponseBody 43 | { 44 | get { return _responseBody; } 45 | } 46 | 47 | string IScriptInjectionFilterContext.ResponseContentType 48 | { 49 | get { return _contentType; } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/MockResponseHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | internal class MockResponseHandler 9 | { 10 | private StringBuilder _response = new StringBuilder(); 11 | private Encoding _encoding = Encoding.ASCII; 12 | 13 | private TaskCompletionSource _handlerTcs = null; 14 | private bool _block; 15 | 16 | internal MockResponseHandler(Encoding encoding) 17 | { 18 | _encoding = encoding; 19 | } 20 | 21 | internal string Response 22 | { 23 | get { return _response.ToString(); } 24 | } 25 | 26 | internal bool Block 27 | { 28 | get { return _block; } 29 | set 30 | { 31 | _block = value; 32 | 33 | if (!_block && _handlerTcs != null) 34 | { 35 | TaskCompletionSource completedTcs = _handlerTcs; 36 | 37 | _handlerTcs = null; 38 | 39 | completedTcs.SetResult(null); 40 | } 41 | } 42 | } 43 | 44 | internal Task HandlerMethod(byte[] buffer, int index, int count) 45 | { 46 | AssertWithMessage.Null(_handlerTcs, "More than one call to HandlerMethod is happening at the same time"); 47 | 48 | TaskCompletionSource handlerTcs = new TaskCompletionSource(); 49 | 50 | try 51 | { 52 | _response.Append(_encoding.GetString(buffer, index, count)); 53 | 54 | if (Block) 55 | { 56 | _handlerTcs = handlerTcs; 57 | } 58 | else 59 | { 60 | handlerTcs.SetResult(null); 61 | } 62 | } 63 | catch (Exception ex) 64 | { 65 | handlerTcs.SetException(ex); 66 | } 67 | 68 | return handlerTcs.Task; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/ScriptInjectionFilterContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.IO; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | /// 10 | /// IScriptInjectionFilterContext is an abstraction of the request data required 11 | /// by ScriptInjectionFilterStream. It is needed for unit testing, and eventually 12 | /// so ASP.NET and ProjectK can share filter code even though they have different 13 | /// HttpContext classes. 14 | /// 15 | internal interface IScriptInjectionFilterContext 16 | { 17 | /// 18 | /// The local (app-relative) path of the request. 19 | /// 20 | string RequestPath { get; } 21 | 22 | /// 23 | /// The response body, where filtered content will be written. 24 | /// 25 | Stream ResponseBody { get; } 26 | 27 | /// 28 | /// The Content-Type that is being returned with the response. 29 | /// 30 | string ResponseContentType { get; } 31 | } 32 | 33 | internal class ScriptInjectionFilterContext : IScriptInjectionFilterContext 34 | { 35 | private HttpContext _httpContext; 36 | 37 | internal ScriptInjectionFilterContext(HttpContext httpContext) 38 | { 39 | _httpContext = httpContext; 40 | } 41 | 42 | public string RequestPath 43 | { 44 | get 45 | { 46 | return _httpContext.Request.Path.HasValue 47 | ? _httpContext.Request.Path.ToString() 48 | : null; 49 | } 50 | } 51 | 52 | public Stream ResponseBody 53 | { 54 | get { return _httpContext.Response.Body; } 55 | } 56 | 57 | public string ResponseContentType 58 | { 59 | get { return _httpContext.Response.ContentType; } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /BrowserLink.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26127.0 5 | MinimumVisualStudioVersion = 15.0.26730.03 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7AC709A9-E84C-4C8F-8185-8A3D01DD7D7C}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.Web.BrowserLink", "src\Microsoft.VisualStudio.Web.BrowserLink\Microsoft.VisualStudio.Web.BrowserLink.csproj", "{616F445C-7A41-4E61-98E3-1B70D70F9F09}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7F6F47EA-CAF5-46E2-B29C-8B20291185B7}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.Web.BrowserLink.Test", "test\Microsoft.VisualStudio.Web.BrowserLink.Test\Microsoft.VisualStudio.Web.BrowserLink.Test.csproj", "{96146399-2C42-4CAA-ABCD-CF1E0F000CEC}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {616F445C-7A41-4E61-98E3-1B70D70F9F09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {616F445C-7A41-4E61-98E3-1B70D70F9F09}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {616F445C-7A41-4E61-98E3-1B70D70F9F09}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {616F445C-7A41-4E61-98E3-1B70D70F9F09}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {96146399-2C42-4CAA-ABCD-CF1E0F000CEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {96146399-2C42-4CAA-ABCD-CF1E0F000CEC}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {96146399-2C42-4CAA-ABCD-CF1E0F000CEC}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {96146399-2C42-4CAA-ABCD-CF1E0F000CEC}.Release|Any CPU.Build.0 = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(SolutionProperties) = preSolution 30 | HideSolutionNode = FALSE 31 | EndGlobalSection 32 | GlobalSection(NestedProjects) = preSolution 33 | {616F445C-7A41-4E61-98E3-1B70D70F9F09} = {7AC709A9-E84C-4C8F-8185-8A3D01DD7D7C} 34 | {96146399-2C42-4CAA-ABCD-CF1E0F000CEC} = {7F6F47EA-CAF5-46E2-B29C-8B20291185B7} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/ContentTypeUtil.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | /// 10 | /// Helpers for testing the content type of the response content. 11 | /// 12 | internal static class ContentTypeUtil 13 | { 14 | private const string HtmlContentType = "text/html"; 15 | private const string XhtmlContentType = "application/xhtml+xml"; 16 | 17 | public static bool IsSupportedContentTypes(string contentType) 18 | { 19 | if (String.IsNullOrEmpty(contentType)) 20 | { 21 | return false; 22 | } 23 | 24 | string[] parts = contentType.Split(';'); 25 | 26 | return String.Equals(parts[0].Trim(), HtmlContentType, StringComparison.OrdinalIgnoreCase) || String.Equals(parts[0].Trim(), XhtmlContentType, StringComparison.OrdinalIgnoreCase); 27 | } 28 | 29 | public static bool IsHtml(string requestUrl, byte[] buffer, int offset, int count) 30 | { 31 | IntPtr realContentTypePtr = IntPtr.Zero; 32 | 33 | try 34 | { 35 | if (offset != 0) 36 | { 37 | byte[] originalBuffer = buffer; 38 | 39 | if (count > 512) 40 | { 41 | count = 512; 42 | } 43 | 44 | buffer = new byte[count]; 45 | 46 | Array.Copy(originalBuffer, offset, buffer, 0, count); 47 | } 48 | 49 | int ret = NativeMethods.FindMimeFromData(IntPtr.Zero, requestUrl, buffer, buffer.Length, null, 0, out realContentTypePtr, 0); 50 | 51 | if (ret == 0 && realContentTypePtr != IntPtr.Zero) 52 | { 53 | return IsSupportedContentTypes(Marshal.PtrToStringUni(realContentTypePtr)); 54 | } 55 | else 56 | { 57 | return false; 58 | } 59 | } 60 | finally 61 | { 62 | if (realContentTypePtr != IntPtr.Zero) 63 | { 64 | Marshal.FreeCoTaskMem(realContentTypePtr); 65 | } 66 | } 67 | 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/PageExecutionContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.IO; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | /// 9 | /// A PageExecutionContext is created for each source file that is executed as 10 | /// part of the page. It remembers the path to the source file and the TextWriter 11 | /// being used, to pass on during BeginContext and EndContext calls. 12 | /// 13 | internal class PageExecutionContext 14 | { 15 | private string _sourceFilePath; 16 | private TextWriterDecorator _writer; 17 | private MappingDataWriter _mappingDataWriter; 18 | 19 | public PageExecutionContext(MappingDataWriter mappingDataWriter, string sourceFilePath, TextWriter writer) 20 | { 21 | _mappingDataWriter = mappingDataWriter; 22 | _sourceFilePath = sourceFilePath; 23 | _writer = writer as TextWriterDecorator; 24 | } 25 | 26 | /// 27 | /// Specifies the part of the source file that is currently being rendered. 28 | /// 29 | /// The start position of the range in the source file 30 | /// The length of the range in the source file 31 | /// True if the range is being written verbatim from the source file 32 | public void BeginContext(int position, int length, bool isLiteral) 33 | { 34 | try 35 | { 36 | if (_writer != null) 37 | { 38 | _mappingDataWriter.WriteBeginContext(position, length, isLiteral, _sourceFilePath, _writer.RenderedOutputIndex, _writer.OutputPosition); 39 | } 40 | } 41 | catch 42 | { } 43 | } 44 | 45 | /// 46 | /// Specifies that we are done rendering the part of the source file from 47 | /// BeginContext. Begin/End context calls can be nested within the same file. 48 | /// 49 | public void EndContext() 50 | { 51 | try 52 | { 53 | if (_writer != null) 54 | { 55 | _mappingDataWriter.WriteEndContext(_writer.RenderedOutputIndex, _writer.OutputPosition); 56 | } 57 | } 58 | catch 59 | { } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/BrowserLinkExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.VisualStudio.Web.BrowserLink; 7 | 8 | namespace Microsoft.AspNetCore.Builder 9 | { 10 | /// 11 | /// Implementation of extension methods for configuring Browser Link 12 | /// in an ASP.NET Core application. 13 | /// 14 | public static class BrowserLinkExtensions 15 | { 16 | /// 17 | /// This method is called to enable Browser Link in an application. It 18 | /// registers a factory method that creates BrowserLinkMiddleware for 19 | /// each request. 20 | /// 21 | public static IApplicationBuilder UseBrowserLink(this IApplicationBuilder app) 22 | { 23 | if (!IsMicrosoftRuntime) 24 | { 25 | return app; 26 | } 27 | 28 | try 29 | { 30 | string applicationBasePath; 31 | 32 | if (GetApplicationBasePath(app, out applicationBasePath)) 33 | { 34 | applicationBasePath = PathUtil.NormalizeDirectoryPath(applicationBasePath); 35 | 36 | HostConnectionUtil.SignalHostForStartup(applicationBasePath, blockUntilStarted: false); 37 | 38 | BrowserLinkMiddlewareFactory factory = new BrowserLinkMiddlewareFactory(applicationBasePath); 39 | 40 | return app.Use(factory.CreateBrowserLinkMiddleware); 41 | } 42 | else 43 | { 44 | // Browser Link doesn't work if we don't have an application path 45 | return app; 46 | } 47 | } 48 | catch 49 | { 50 | // Something went wrong initializing the runtime. Browser Link won't work. 51 | return app; 52 | } 53 | } 54 | 55 | private static bool IsMicrosoftRuntime 56 | { 57 | get { return Type.GetType("Mono.Runtime") == null; } 58 | } 59 | 60 | private static bool GetApplicationBasePath(IApplicationBuilder app, out string applicationBasePath) 61 | { 62 | IHostingEnvironment hostingEnvironment = app.ApplicationServices.GetService(typeof(IHostingEnvironment)) as IHostingEnvironment; 63 | 64 | if (hostingEnvironment != null) 65 | { 66 | applicationBasePath = hostingEnvironment.ContentRootPath; 67 | 68 | return applicationBasePath != null; 69 | } 70 | 71 | applicationBasePath = null; 72 | return false; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/HostConnectionData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | /// 9 | /// Represents an host connection published from a design tool's host process. 10 | /// 11 | internal class HostConnectionData 12 | { 13 | internal HostConnectionData( 14 | string connectionString, 15 | string sslConnectionString, 16 | string requestSignalName, 17 | string readySignalName, 18 | string injectScriptVerb, 19 | string mappingDataVerb, 20 | string serverDataVerb, 21 | IEnumerable projectPaths) 22 | { 23 | ConnectionString = connectionString; 24 | SslConnectionString = sslConnectionString; 25 | RequestSignalName = requestSignalName; 26 | ReadySignalName = readySignalName; 27 | InjectScriptVerb = injectScriptVerb; 28 | MappingDataVerb = mappingDataVerb; 29 | ServerDataVerb = serverDataVerb; 30 | ProjectPaths = projectPaths; 31 | } 32 | 33 | /// 34 | /// The name of the event to signal when you want the host to start. 35 | /// 36 | public string RequestSignalName { get; private set; } 37 | 38 | /// 39 | /// The name of the event to wait on after requesting the host to start. 40 | /// 41 | public string ReadySignalName { get; private set; } 42 | 43 | /// 44 | /// The string used to identify the host connection. 45 | /// 46 | public string ConnectionString { get; private set; } 47 | 48 | /// 49 | /// The string used to identify the SSL host connection. 50 | /// 51 | public string SslConnectionString { get; private set; } 52 | 53 | /// 54 | /// API verb for injecting the Browser Link script into the page 55 | /// 56 | public string InjectScriptVerb { get; private set; } 57 | 58 | /// 59 | /// API verb for posting mapping data 60 | /// 61 | public string MappingDataVerb { get; private set; } 62 | 63 | /// 64 | /// API verb for posting data about the server 65 | /// 66 | public string ServerDataVerb { get; private set; } 67 | 68 | /// 69 | /// The physical paths of projects loaded in this instance of the design tool. 70 | /// 71 | public IEnumerable ProjectPaths { get; private set; } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/TaskHelpers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.VisualStudio.Web.BrowserLink 9 | { 10 | internal static class TaskHelpers 11 | { 12 | /// 13 | /// Add a timeout to a task. If the timeout expires before the underlying 14 | /// task completes, the wrapper task returns a fixed value. 15 | /// 16 | /// The task being awaited 17 | /// The amount of time to wait 18 | /// The result returned if the timeout expires 19 | /// 20 | /// The will continue to execute after the 21 | /// expires, but tasks awaiting the wrapper will 22 | /// be unblocked. 23 | /// 24 | public static async Task WaitWithTimeout(Task task, TimeSpan timeout, ResultType resultIfTimedOut) 25 | { 26 | CancellationTokenSource cancelTimeout = new CancellationTokenSource(); 27 | 28 | Task timeoutTask = Task.Delay(timeout, cancelTimeout.Token).ContinueWith(x => resultIfTimedOut); 29 | 30 | Task completedTask = await Task.WhenAny(task, timeoutTask); 31 | 32 | cancelTimeout.Cancel(); 33 | 34 | return completedTask.Result; 35 | } 36 | 37 | /// 38 | /// Wrap a task so that it can be cancelled. If the task is cancelled before 39 | /// the underlying task completes, the wrapper task returns a fixed value. 40 | /// 41 | /// The task being awaited 42 | /// The cancellation token 43 | /// The result returned if the timeout expires 44 | /// A task that can be cancelled before it completes 45 | /// 46 | /// The will continue to execute after the 47 | /// is set, but tasks awaiting the 48 | /// wrapper will be unblocked. 49 | /// 50 | public static async Task WaitWithCancellation(Task task, CancellationToken cancellationToken, ResultType resultIfCancelled) 51 | { 52 | TaskCompletionSource cancelTcs = new TaskCompletionSource(); 53 | 54 | cancellationToken.Register(delegate () 55 | { 56 | cancelTcs.TrySetResult(resultIfCancelled); 57 | }); 58 | 59 | return await await Task.WhenAny(task, cancelTcs.Task); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/HttpAdapterRequestStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | /// 10 | /// This class wraps the output stream of an IHttpSocketAdapter into a Stream 11 | /// interface, for use with System.IO.*Writer classes. 12 | /// 13 | internal class HttpAdapterRequestStream : Stream 14 | { 15 | private RevolvingBuffers _buffers = new RevolvingBuffers(1024); 16 | 17 | internal HttpAdapterRequestStream(IHttpSocketAdapter adapter) 18 | { 19 | SendDataFromBuffersAsync(_buffers, adapter); 20 | } 21 | 22 | public override bool CanRead 23 | { 24 | get { return false; } 25 | } 26 | 27 | public override bool CanSeek 28 | { 29 | get { return false; } 30 | } 31 | 32 | public override bool CanWrite 33 | { 34 | get { return true; } 35 | } 36 | 37 | public override long Length 38 | { 39 | get { throw new NotSupportedException(); } 40 | } 41 | 42 | public override long Position 43 | { 44 | get { throw new NotSupportedException(); } 45 | set { throw new NotSupportedException(); } 46 | } 47 | 48 | public override void Flush() 49 | { 50 | var buffer = _buffers; 51 | if (buffer != null) 52 | { 53 | _buffers = null; 54 | 55 | buffer.WaitForBufferEmptyAsync().ContinueWith(task => 56 | { 57 | buffer.Dispose(); 58 | }); 59 | } 60 | } 61 | 62 | protected override void Dispose(bool disposing) 63 | { 64 | Flush(); 65 | } 66 | 67 | public override int Read(byte[] buffer, int offset, int count) 68 | { 69 | throw new NotSupportedException(); 70 | } 71 | 72 | public override long Seek(long offset, SeekOrigin origin) 73 | { 74 | throw new NotSupportedException(); 75 | } 76 | 77 | public override void SetLength(long value) 78 | { 79 | throw new NotSupportedException(); 80 | } 81 | 82 | public override void Write(byte[] buffer, int offset, int count) 83 | { 84 | _buffers.CopyDataToBuffer(buffer, offset, count); 85 | } 86 | 87 | /// 88 | /// This function is the pump that pushes data from the buffers into an 89 | /// HTTP connection. It asynchronously waits on data, then asynchronously 90 | /// waits while the data is sent, then waits on more data, etc. 91 | /// 92 | private static async void SendDataFromBuffersAsync(RevolvingBuffers buffer, IHttpSocketAdapter adapter) 93 | { 94 | while (true) 95 | { 96 | ArraySegment bufferToSend = await buffer.GetBufferedDataAsync(); 97 | if (bufferToSend.Count == 0) 98 | { 99 | break; 100 | } 101 | 102 | await adapter.WriteToRequestAsync(bufferToSend.Array, bufferToSend.Offset, bufferToSend.Count); 103 | } 104 | 105 | await adapter.CompleteRequest(); 106 | 107 | adapter.Dispose(); 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/PageExecutionListenerFeature.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | 8 | namespace Microsoft.VisualStudio.Web.BrowserLink 9 | { 10 | /// 11 | /// This feature is provided for page renderers, so they can provide mapping 12 | /// data as the page renders. 13 | /// 14 | internal class PageExecutionListenerFeature : IDisposable 15 | { 16 | private MappingDataWriter _mappingDataWriter; 17 | private List _writers = new List(); 18 | 19 | internal PageExecutionListenerFeature(IHttpSocketAdapter mappingDataSocket) 20 | { 21 | _mappingDataWriter = new MappingDataWriter(mappingDataSocket); 22 | } 23 | 24 | /// 25 | /// This method is called to give the listener a chance to replace the TextWriter 26 | /// used to render the page. 27 | /// 28 | /// The original TextWriter for the output stream 29 | /// A TextWriter that will be used for writing the output. 30 | public TextWriter DecorateWriter(TextWriter writer) 31 | { 32 | try 33 | { 34 | TextWriterDecorator decorator = new TextWriterDecorator(writer, this, _writers.Count); 35 | 36 | foreach (TextWriterDecorator existingWriter in _writers) 37 | { 38 | AddTextRelationship(existingWriter, decorator); 39 | } 40 | 41 | _writers.Add(decorator); 42 | 43 | return decorator; 44 | } 45 | catch 46 | { 47 | return writer; 48 | } 49 | } 50 | 51 | /// 52 | /// Create a context that associates a TextWriter with a source file. 53 | /// 54 | /// The path to the source file. 55 | /// The TextWriter used to render the source file 56 | /// A context that will be called back with mapping data within the source file. 57 | public PageExecutionContext GetContext(string sourceFilePath, TextWriter writer) 58 | { 59 | return new PageExecutionContext(_mappingDataWriter, sourceFilePath, writer); 60 | } 61 | 62 | internal void AddTextRelationship(TextWriterDecorator copyingToWriter, TextWriterDecorator copyingFromWriter) 63 | { 64 | _mappingDataWriter.WriteTextRelationship(copyingToWriter.RenderedOutputIndex, copyingFromWriter.RenderedOutputIndex, copyingToWriter.OutputPosition); 65 | } 66 | 67 | public void Dispose() 68 | { 69 | if (_mappingDataWriter != null) 70 | { 71 | try 72 | { 73 | SendEndOfData(); 74 | 75 | _mappingDataWriter.Dispose(); 76 | } 77 | catch { } 78 | finally 79 | { 80 | _mappingDataWriter = null; 81 | } 82 | } 83 | } 84 | 85 | private void SendEndOfData() 86 | { 87 | foreach (TextWriterDecorator writer in _writers) 88 | { 89 | _mappingDataWriter.WriteOutputDefinition(writer.RenderedOutputIndex, writer.RenderedOutput); 90 | } 91 | 92 | _mappingDataWriter.WriteEndOfData(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/BrowserLinkMiddleWareUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Http.Headers; 3 | using Microsoft.Net.Http.Headers; 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | internal static class BrowserLinkMiddleWareUtil 9 | { 10 | internal static List GetRequestPort(RequestHeaders requestHeader) 11 | { 12 | List requestPortList = new List(); 13 | 14 | if (requestHeader.IfNoneMatch != null) 15 | { 16 | for (int index = 0; index < requestHeader.IfNoneMatch.Count; ++index) 17 | { 18 | string[] strings = requestHeader.IfNoneMatch[index].ToString().Split(':'); 19 | 20 | if (strings.Length >= 2) 21 | { 22 | int port = -1; 23 | 24 | if (Int32.TryParse(strings[1].Substring(0, strings[1].Length - 1), out port)) 25 | { 26 | requestPortList.Add(port); 27 | } 28 | } 29 | } 30 | } 31 | 32 | return requestPortList; 33 | } 34 | 35 | internal static string GetRequestUrl(RequestHeaders requstHeader) 36 | { 37 | return requstHeader.Host.ToString(); 38 | } 39 | 40 | internal static int GetCurrentPort(string connectionString) 41 | { 42 | Uri uri; 43 | 44 | if (connectionString == null || !Uri.TryCreate(connectionString, UriKind.Absolute, out uri)) 45 | { 46 | return -1; 47 | } 48 | 49 | return uri.Port; 50 | } 51 | 52 | internal static void RemoveETagAndTimeStamp(RequestHeaders requestHeader) 53 | { 54 | requestHeader.IfNoneMatch = null; 55 | requestHeader.IfModifiedSince = null; 56 | } 57 | 58 | internal static void DeletePortFromETag(RequestHeaders requestHeader) 59 | { 60 | string newETag = ""; 61 | IList list = requestHeader.IfNoneMatch; 62 | 63 | for (int index = 0; index < list.Count; ++index) 64 | { 65 | String[] strings = list[index].ToString().Split(':'); 66 | 67 | if (strings.Length >= 2) 68 | { 69 | newETag = strings[0] + "\""; 70 | list[index] = new EntityTagHeaderValue(newETag); 71 | } 72 | } 73 | 74 | requestHeader.IfNoneMatch = list; 75 | } 76 | 77 | internal static void AddToETag(ResponseHeaders responseHeader, int port) 78 | { 79 | if (responseHeader.ETag != null) 80 | { 81 | string temp = responseHeader.ETag.ToString().Substring(0, responseHeader.ETag.ToString().Length - 1) + ":" + port + "\""; 82 | responseHeader.ETag = new EntityTagHeaderValue(temp); 83 | } 84 | } 85 | 86 | internal static bool IfMatch(List requestPortList, int currentPort) 87 | { 88 | foreach (int port in requestPortList) 89 | { 90 | if (port == currentPort) 91 | { 92 | return true; 93 | } 94 | } 95 | 96 | return false; 97 | } 98 | 99 | internal static int FilterRequestHeader(RequestHeaders requestHeader, string connectionString) 100 | { 101 | List requestPortList = GetRequestPort(requestHeader); 102 | int currentPort = GetCurrentPort(connectionString); 103 | 104 | if (!IfMatch(requestPortList, currentPort)) 105 | { 106 | RemoveETagAndTimeStamp(requestHeader); 107 | } 108 | else 109 | { 110 | DeletePortFromETag(requestHeader); 111 | } 112 | 113 | return currentPort; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/MockHttpSocketAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | internal class MockHttpSocketAdapter : IHttpSocketAdapter 10 | { 11 | private Dictionary> _responseHeaderRequests = new Dictionary>(); 12 | private TaskCompletionSource _statusCodeTask = new TaskCompletionSource(); 13 | private TaskCompletionSource _responseCompleteTask = new TaskCompletionSource(); 14 | 15 | public Dictionary RequestHeaders = new Dictionary(); 16 | private ResponseHandler _responseHandler; 17 | 18 | private StringBuilder _requestContent = new StringBuilder(); 19 | 20 | public void SendResponseStatusCode(int statusCode) 21 | { 22 | TaskAssert.NotCompleted(_statusCodeTask.Task, "MockHttpSocketAdapter: Response Status Code can only be set once."); 23 | 24 | _statusCodeTask.SetResult(statusCode); 25 | } 26 | 27 | public void SendResponseHeader(string name, string value) 28 | { 29 | TaskCompletionSource tcs; 30 | 31 | if (!_responseHeaderRequests.TryGetValue(name, out tcs)) 32 | { 33 | tcs = new TaskCompletionSource(); 34 | _responseHeaderRequests[name] = tcs; 35 | } 36 | 37 | TaskAssert.NotCompleted(tcs.Task, "MockHttpSocketAdapter: Response header '{0}' can only be set once.", name); 38 | 39 | tcs.SetResult(value); 40 | } 41 | 42 | public void SendResponseBodyContent(string content, Encoding encoding) 43 | { 44 | AssertWithMessage.NotNull(_responseHandler, "No response handler was set."); 45 | 46 | byte[] bytes = encoding.GetBytes(content); 47 | 48 | _responseHandler.Invoke(bytes, 0, bytes.Length); 49 | } 50 | 51 | public void SendResponseComplete() 52 | { 53 | TaskAssert.NotCompleted(_responseCompleteTask.Task, "MockHttpSocketAdapter: Response Complete can only be sent once."); 54 | 55 | _responseCompleteTask.SetResult(true); 56 | } 57 | 58 | public bool IsDisposed { get; private set; } 59 | public bool IsCompleted { get; private set; } 60 | public string RequestContent { get { return _requestContent.ToString(); } } 61 | public bool HasResponseHandler { get { return _responseHandler != null; } } 62 | 63 | 64 | void IHttpSocketAdapter.AddRequestHeader(string name, string value) 65 | { 66 | RequestHeaders.Add(name, value); 67 | } 68 | 69 | Task IHttpSocketAdapter.CompleteRequest() 70 | { 71 | IsCompleted = true; 72 | 73 | return StaticTaskResult.True; 74 | } 75 | 76 | void IDisposable.Dispose() 77 | { 78 | Assert.False(IsDisposed, "Calling Dispose on an adapter that was already disposed"); 79 | 80 | IsDisposed = true; 81 | } 82 | 83 | Task IHttpSocketAdapter.GetResponseHeader(string headerName) 84 | { 85 | TaskCompletionSource tcs; 86 | 87 | if (!_responseHeaderRequests.TryGetValue(headerName, out tcs)) 88 | { 89 | tcs = new TaskCompletionSource(); 90 | _responseHeaderRequests[headerName] = tcs; 91 | } 92 | 93 | return tcs.Task; 94 | } 95 | 96 | Task IHttpSocketAdapter.GetResponseStatusCode() 97 | { 98 | return _statusCodeTask.Task; 99 | } 100 | 101 | void IHttpSocketAdapter.SetResponseHandler(ResponseHandler handler) 102 | { 103 | AssertWithMessage.Null(_responseHandler, "SetResponseHandler was called more than once."); 104 | AssertWithMessage.NotNull(handler, "SetResponseHandler was called with a null handler."); 105 | 106 | _responseHandler = handler; 107 | } 108 | 109 | Task IHttpSocketAdapter.WaitForResponseComplete() 110 | { 111 | return _responseCompleteTask.Task; 112 | } 113 | 114 | Task IHttpSocketAdapter.WriteToRequestAsync(byte[] buffer, int offset, int count) 115 | { 116 | _requestContent.Append(Encoding.ASCII.GetString(buffer, offset, count)); 117 | 118 | return StaticTaskResult.True; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/TaskAssert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | internal static class TaskAssert 10 | { 11 | public static void ResultEquals(Task task, ResultType expected, string messageFormat = null, params object[] args) 12 | { 13 | TaskAssert.NotFaulted(task, messageFormat, args); 14 | TaskAssert.Completed(task, messageFormat, args); 15 | 16 | if (task.Result == null) 17 | { 18 | if (expected == null) 19 | { 20 | return; 21 | } 22 | } 23 | else if (task.Result.Equals(expected)) 24 | { 25 | return; 26 | } 27 | 28 | ThrowFailure( 29 | GetMethodFailureMessage(nameof(ResultEquals)), 30 | FormatMessage("Expected: <{0}>. Actual: <{1}>.", SafeParam(expected), SafeParam(task.Result)), 31 | FormatMessage(messageFormat, args)); 32 | } 33 | 34 | public static void Faulted(Task task, string messageFormat = null, params object[] args) 35 | { 36 | if (!task.IsFaulted) 37 | { 38 | TaskAssert.ThrowFailure( 39 | GetMethodFailureMessage(nameof(Faulted)), 40 | FormatMessage(messageFormat, args)); 41 | } 42 | } 43 | 44 | public static void NotFaulted(Task task, string messageFormat = null, params object[] args) 45 | { 46 | if (task.IsFaulted) 47 | { 48 | ThrowFailure( 49 | task.Exception, 50 | GetMethodFailureMessage(nameof(NotFaulted)), 51 | FormatMessage(task.Exception), 52 | FormatMessage(messageFormat, args)); 53 | } 54 | } 55 | 56 | public static void Completed(Task task, string messageFormat = null, params object[] args) 57 | { 58 | TaskAssert.NotFaulted(task, messageFormat, args); 59 | 60 | if (!task.IsCompleted) 61 | { 62 | ThrowFailure( 63 | GetMethodFailureMessage(nameof(Completed)), 64 | FormatMessage(messageFormat, args)); 65 | } 66 | } 67 | 68 | public static void NotCompleted(Task task, string messageFormat = null, params object[] args) 69 | { 70 | TaskAssert.NotFaulted(task, messageFormat, args); 71 | 72 | if (task.IsCompleted) 73 | { 74 | ThrowFailure( 75 | GetMethodFailureMessage(nameof(NotCompleted)), 76 | FormatMessage(messageFormat, args)); 77 | } 78 | } 79 | 80 | private static string FormatMessage(Exception exception) 81 | { 82 | exception = UnwrapException(exception); 83 | 84 | return exception.ToString(); 85 | } 86 | 87 | private static string FormatMessage(string messageFormat, params object[] messageArgs) 88 | { 89 | if (messageArgs == null || messageArgs.Length == 0) 90 | { 91 | return messageFormat; 92 | } 93 | else 94 | { 95 | return String.Format(messageFormat, messageArgs); 96 | } 97 | } 98 | 99 | private static void ThrowFailure(params string[] failureMessages) 100 | { 101 | ThrowFailure(null, failureMessages); 102 | } 103 | 104 | private static void ThrowFailure(Exception exception, params string[] failureMessages) 105 | { 106 | string failureMessage = String.Join(" ", failureMessages.Where(x => !String.IsNullOrWhiteSpace(x))); 107 | 108 | Assert.True(false, failureMessage); 109 | } 110 | 111 | private static Exception UnwrapException(Exception exception) 112 | { 113 | while (exception is AggregateException) 114 | { 115 | exception = ((AggregateException)exception).InnerExceptions[0]; 116 | } 117 | 118 | return exception; 119 | } 120 | 121 | private static string GetMethodFailureMessage(string methodName) 122 | { 123 | return String.Format("{0}.{1} failed.", nameof(TaskAssert), methodName); 124 | } 125 | 126 | private static object SafeParam(object param) 127 | { 128 | if (param == null) 129 | { 130 | return "(null)"; 131 | } 132 | else 133 | { 134 | return param; 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/ContentTypeUtilTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.AspNetCore.Testing.xunit; 4 | using Xunit; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | public class ContentTypeUtilTest 9 | { 10 | [ConditionalFact] 11 | [OSSkipCondition(OperatingSystems.Linux, SkipReason = "Uses native Windows methods")] 12 | [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Uses native Windows methods")] 13 | public void IsSupportedContentTypes_DetectsTextHtmlInContentType() 14 | { 15 | string[] htmlContentTypes = new string[] 16 | { 17 | "text/html", 18 | "TEXT/hTmL", 19 | "text/html; charset=utf-8", 20 | "text/html; any parameter", 21 | }; 22 | 23 | string[] nonHtmlContentTypes = new string[] 24 | { 25 | "text/htmlx", 26 | "text/html charset=utf-8", 27 | null, 28 | "", 29 | "text/htm", 30 | }; 31 | 32 | foreach (string htmlContentType in htmlContentTypes) 33 | { 34 | Assert.True(ContentTypeUtil.IsSupportedContentTypes(htmlContentType), htmlContentType); 35 | } 36 | 37 | foreach (string nonHtmlContentType in nonHtmlContentTypes) 38 | { 39 | Assert.False(ContentTypeUtil.IsSupportedContentTypes(nonHtmlContentType), nonHtmlContentType); 40 | } 41 | } 42 | 43 | [ConditionalFact] 44 | [OSSkipCondition(OperatingSystems.Linux, SkipReason = "Uses native Windows methods")] 45 | [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Uses native Windows methods")] 46 | public void IsSupportedContentTypes_DetectsXhtmlInContentType() 47 | { 48 | string[] xhtmlContentTypes = new string[] 49 | { 50 | "application/xhtml+xml", 51 | "APPLICATION/xhTml+Xml", 52 | "application/xhtml+xml; charset=utf-8", 53 | "application/xhtml+xml; any parameter", 54 | }; 55 | 56 | string[] nonXhtmlContentTypes = new string[] 57 | { 58 | "application/xml", 59 | "application/xhtml+xml charset=utf-8", 60 | null, 61 | "", 62 | "application/xhtm", 63 | }; 64 | 65 | foreach (string xhtmlContentType in xhtmlContentTypes) 66 | { 67 | Assert.True(ContentTypeUtil.IsSupportedContentTypes(xhtmlContentType), xhtmlContentType); 68 | } 69 | 70 | foreach (string nonXhtmlContentType in nonXhtmlContentTypes) 71 | { 72 | Assert.False(ContentTypeUtil.IsSupportedContentTypes(nonXhtmlContentType), nonXhtmlContentType); 73 | } 74 | } 75 | 76 | [ConditionalFact] 77 | [OSSkipCondition(OperatingSystems.Linux, SkipReason = "Uses native Windows methods")] 78 | [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Uses native Windows methods")] 79 | public void IsHtml_DetectsHtmlInBuffer() 80 | { 81 | // Arrange 82 | byte[] buffer = Encoding.ASCII.GetBytes(""); 83 | 84 | // Act 85 | bool result = ContentTypeUtil.IsHtml("default.html", buffer, 0, buffer.Length); 86 | 87 | // Assert 88 | Assert.True(result); 89 | } 90 | 91 | [ConditionalFact] 92 | [OSSkipCondition(OperatingSystems.Linux, SkipReason = "Uses native Windows methods")] 93 | [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Uses native Windows methods")] 94 | public void IsHtml_DetectsNonHtmlInBuffer() 95 | { 96 | // Arrange 97 | byte[] buffer = Encoding.ASCII.GetBytes("var j = 10;"); 98 | 99 | // Act 100 | bool result = ContentTypeUtil.IsHtml("default.html", buffer, 0, buffer.Length); 101 | 102 | // Assert 103 | Assert.False(result); 104 | } 105 | 106 | [ConditionalFact] 107 | [OSSkipCondition(OperatingSystems.Linux, SkipReason = "Uses native Windows methods")] 108 | [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "Uses native Windows methods")] 109 | public void IsHtml_DetectsHtmlInBufferWithOffset() 110 | { 111 | // Arrange 112 | byte[] buffer = Encoding.ASCII.GetBytes(""); 113 | byte[] bufferWithLeadingBytes = new byte[100 + buffer.Length]; 114 | 115 | Array.Copy(buffer, 0, bufferWithLeadingBytes, 100, buffer.Length); 116 | 117 | // Act 118 | bool result = ContentTypeUtil.IsHtml("default.html", bufferWithLeadingBytes, 100, buffer.Length); 119 | 120 | // Assert 121 | Assert.True(result); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/MappingDataWriter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | /// 10 | /// This feature is provided for page renderers, so they can provide mapping 11 | /// data as the page renders. 12 | /// 13 | internal class MappingDataWriter : IDisposable 14 | { 15 | private BinaryWriter _binaryWriter; 16 | private bool _wroteAnyData = false; 17 | 18 | internal MappingDataWriter(IHttpSocketAdapter mappingDataSocket) 19 | { 20 | _binaryWriter = new BinaryWriter(new HttpAdapterRequestStream(mappingDataSocket)); 21 | } 22 | 23 | public void Dispose() 24 | { 25 | if (_binaryWriter != null) 26 | { 27 | _binaryWriter.Flush(); 28 | _binaryWriter.Dispose(); 29 | _binaryWriter = null; 30 | } 31 | } 32 | 33 | public void WriteBeginContext(int sourceStartPosition, int sourceLength, bool isLiteral, string sourceFilePath, int renderedOutputIndex, int renderedPosition) 34 | { 35 | WriteType(BrowserLinkConstants.MappingDataType.BeginContext); 36 | 37 | WriteValue(BrowserLinkConstants.MappingDataValue.SourceStartPosition, sourceStartPosition); 38 | WriteValue(BrowserLinkConstants.MappingDataValue.SourceLength, sourceLength); 39 | WriteValue(BrowserLinkConstants.MappingDataValue.IsLiteral, isLiteral); 40 | WriteValue(BrowserLinkConstants.MappingDataValue.SourceFilePath, sourceFilePath); 41 | WriteValue(BrowserLinkConstants.MappingDataValue.RenderedOutputIndex, renderedOutputIndex); 42 | WriteValue(BrowserLinkConstants.MappingDataValue.RenderedPosition, renderedPosition); 43 | 44 | WriteEndOfDataBlock(); 45 | } 46 | 47 | public void WriteEndContext(int renderedOutputIndex, int renderedPosition) 48 | { 49 | WriteType(BrowserLinkConstants.MappingDataType.EndContext); 50 | 51 | WriteValue(BrowserLinkConstants.MappingDataValue.RenderedOutputIndex, renderedOutputIndex); 52 | WriteValue(BrowserLinkConstants.MappingDataValue.RenderedPosition, renderedPosition); 53 | 54 | WriteEndOfDataBlock(); 55 | } 56 | 57 | public void WriteOutputDefinition(int renderedOutputIndex, string renderedContent) 58 | { 59 | WriteType(BrowserLinkConstants.MappingDataType.RenderedOutputDefinition); 60 | 61 | WriteValue(BrowserLinkConstants.MappingDataValue.RenderedOutputIndex, renderedOutputIndex); 62 | WriteValue(BrowserLinkConstants.MappingDataValue.RenderedContent, renderedContent); 63 | 64 | WriteEndOfDataBlock(); 65 | } 66 | 67 | public void WriteTextRelationship(int parentRenderedOutputIndex, int childRenderedOutputIndex, int relativeRenderedPosition) 68 | { 69 | WriteType(BrowserLinkConstants.MappingDataType.RenderedOutputRelationship); 70 | 71 | WriteValue(BrowserLinkConstants.MappingDataValue.ParentRenderedOutputIndex, parentRenderedOutputIndex); 72 | WriteValue(BrowserLinkConstants.MappingDataValue.ChildRenderedOutputIndex, childRenderedOutputIndex); 73 | WriteValue(BrowserLinkConstants.MappingDataValue.RelativeRenderedPosition, relativeRenderedPosition); 74 | 75 | WriteEndOfDataBlock(); 76 | } 77 | 78 | public void WriteEndOfData() 79 | { 80 | if (_wroteAnyData) 81 | { 82 | WriteType(BrowserLinkConstants.MappingDataType.EndOfData); 83 | WriteEndOfDataBlock(); 84 | } 85 | } 86 | 87 | private void WriteType(BrowserLinkConstants.MappingDataType type) 88 | { 89 | _wroteAnyData = true; 90 | 91 | _binaryWriter.Write((int)type); 92 | } 93 | 94 | private void WriteValue(BrowserLinkConstants.MappingDataValue valueKey, int value) 95 | { 96 | _binaryWriter.Write((int)valueKey); 97 | _binaryWriter.Write((int)BrowserLinkConstants.MappingDataValueType.Int32Value); 98 | _binaryWriter.Write(value); 99 | } 100 | 101 | private void WriteValue(BrowserLinkConstants.MappingDataValue valueKey, string value) 102 | { 103 | _binaryWriter.Write((int)valueKey); 104 | _binaryWriter.Write((int)BrowserLinkConstants.MappingDataValueType.StringValue); 105 | _binaryWriter.Write(value); 106 | } 107 | 108 | private void WriteValue(BrowserLinkConstants.MappingDataValue valueKey, bool value) 109 | { 110 | _binaryWriter.Write((int)valueKey); 111 | _binaryWriter.Write((int)BrowserLinkConstants.MappingDataValueType.BooleanValue); 112 | _binaryWriter.Write(value); 113 | } 114 | 115 | private void WriteEndOfDataBlock() 116 | { 117 | _binaryWriter.Write((int)BrowserLinkConstants.MappingDataValue.EndOfDataValues); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/DelayConnectingHttpSocketAdapter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.VisualStudio.Web.BrowserLink 9 | { 10 | /// 11 | /// This class wraps an IHttpSocketAdapter so that it does not connect 12 | /// until the last possible moment, and it does not connect unless absolutely 13 | /// necessary. This way, a connection can be initialized early, but there is 14 | /// no performance hit unless the connection is used. 15 | /// 16 | /// 17 | /// Each method is implemented using one of two strategies: 18 | /// 1. If a method is really using the connection (sending or receiving data), 19 | /// make sure the connection has been created and pass the call through. 20 | /// 2. If a method is just initializing the connection (e.g. setting headers), 21 | /// pass the call through only if the connection already exists. Otherwise, 22 | /// store the call arguments and replay them after the connection is created. 23 | /// 24 | internal class DelayConnectingHttpSocketAdapter : IHttpSocketAdapter 25 | { 26 | private Task _connectedSocketTask = null; 27 | private Func> _connectFunction; 28 | 29 | private List> _headers = new List>(); 30 | private ResponseHandler _responseHandler = null; 31 | 32 | internal DelayConnectingHttpSocketAdapter(Func> connectFunction) 33 | { 34 | _connectFunction = connectFunction; 35 | } 36 | 37 | void IHttpSocketAdapter.AddRequestHeader(string name, string value) 38 | { 39 | if (_connectedSocketTask != null) 40 | { 41 | _connectedSocketTask.Result.AddRequestHeader(name, value); 42 | } 43 | else 44 | { 45 | _headers.Add(new KeyValuePair(name, value)); 46 | } 47 | } 48 | 49 | async Task IHttpSocketAdapter.CompleteRequest() 50 | { 51 | // If a connection hasn't been created yet, no data has been sent. 52 | // If there's no response listener, no data will be received. 53 | // If both of those are true, it's a safe bet that the connection is unnecessary. 54 | if (_connectedSocketTask == null && _responseHandler == null) 55 | { 56 | return; 57 | } 58 | else 59 | { 60 | IHttpSocketAdapter socket = await GetConnectedSocketAsync(); 61 | 62 | await socket.CompleteRequest(); 63 | } 64 | } 65 | 66 | void IDisposable.Dispose() 67 | { 68 | if (_connectedSocketTask != null) 69 | { 70 | _connectedSocketTask.Result.Dispose(); 71 | _connectedSocketTask = null; 72 | } 73 | } 74 | 75 | async Task IHttpSocketAdapter.GetResponseHeader(string headerName) 76 | { 77 | IHttpSocketAdapter socket = await GetConnectedSocketAsync(); 78 | 79 | return await socket.GetResponseHeader(headerName); 80 | } 81 | 82 | async Task IHttpSocketAdapter.GetResponseStatusCode() 83 | { 84 | IHttpSocketAdapter socket = await GetConnectedSocketAsync(); 85 | 86 | return await socket.GetResponseStatusCode(); 87 | } 88 | 89 | void IHttpSocketAdapter.SetResponseHandler(ResponseHandler handler) 90 | { 91 | if (_connectedSocketTask != null) 92 | { 93 | _connectedSocketTask.Result.SetResponseHandler(handler); 94 | } 95 | else 96 | { 97 | _responseHandler = handler; 98 | } 99 | } 100 | 101 | async Task IHttpSocketAdapter.WaitForResponseComplete() 102 | { 103 | IHttpSocketAdapter socket = await GetConnectedSocketAsync(); 104 | 105 | await socket.WaitForResponseComplete(); 106 | } 107 | 108 | async Task IHttpSocketAdapter.WriteToRequestAsync(byte[] buffer, int offset, int count) 109 | { 110 | IHttpSocketAdapter socket = await GetConnectedSocketAsync(); 111 | 112 | await socket.WriteToRequestAsync(buffer, offset, count); 113 | } 114 | 115 | private Task GetConnectedSocketAsync() 116 | { 117 | if (_connectedSocketTask == null) 118 | { 119 | _connectedSocketTask = CreateSocketConnectionAsync(); 120 | } 121 | 122 | return _connectedSocketTask; 123 | } 124 | 125 | private async Task CreateSocketConnectionAsync() 126 | { 127 | IHttpSocketAdapter socket = await _connectFunction.Invoke(); 128 | 129 | if (socket == null) 130 | { 131 | // Failed to create the connection, so instead we create this null 132 | // object to handle future requests with no-ops. 133 | socket = new FailedConnectionHttpSocketAdapter(); 134 | } 135 | else 136 | { 137 | if (_responseHandler != null) 138 | { 139 | socket.SetResponseHandler(_responseHandler); 140 | } 141 | 142 | foreach (KeyValuePair header in _headers) 143 | { 144 | socket.AddRequestHeader(header.Key, header.Value); 145 | } 146 | } 147 | 148 | return socket; 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/MockSocketAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace Microsoft.VisualStudio.Web.BrowserLink 8 | { 9 | internal class MockSocketAdapter : ISocketAdapter 10 | { 11 | private TaskCompletionSource _receiveTcs = null; 12 | 13 | private ArraySegment? _currentBytesToSend = null; 14 | private ArraySegment _receiverBuffer = new ArraySegment(); 15 | private Queue> _bytesToSend = new Queue>(); 16 | 17 | private StringBuilder _sentContent = new StringBuilder(); 18 | 19 | private bool _disposed = false; 20 | private bool _throwExceptionOnNextSendAsync = false; 21 | 22 | public string SentContent 23 | { 24 | get { return _sentContent.ToString(); } 25 | } 26 | 27 | private ArraySegment? CurrentBytesToSend 28 | { 29 | get 30 | { 31 | if (_currentBytesToSend == null && _bytesToSend.Count > 0) 32 | { 33 | _currentBytesToSend = _bytesToSend.Dequeue(); 34 | } 35 | 36 | return _currentBytesToSend; 37 | } 38 | } 39 | 40 | public bool IsClosed 41 | { 42 | get { return _disposed; } 43 | } 44 | 45 | internal void ThrowExceptionOnNextSendAsync() 46 | { 47 | _throwExceptionOnNextSendAsync = true; 48 | } 49 | 50 | internal void ThrowExceptionFromReceiveAsync(string message = "An error occurred.") 51 | { 52 | if (_receiveTcs != null) 53 | { 54 | _receiveTcs.TrySetException(new Exception(message)); 55 | _receiveTcs = null; 56 | } 57 | } 58 | 59 | public void SendString(string data, Encoding encoding) 60 | { 61 | byte[] bytes = encoding.GetBytes(data); 62 | 63 | SendBytes(bytes, 0, bytes.Length); 64 | } 65 | 66 | public void SendBytes(byte[] buffer, int offset, int count) 67 | { 68 | _bytesToSend.Enqueue(new ArraySegment(buffer, offset, count)); 69 | 70 | PushBytesToReceiver(); 71 | } 72 | 73 | private void PushBytesToReceiver() 74 | { 75 | if (_receiveTcs != null && CurrentBytesToSend != null) 76 | { 77 | int bytesLeftInReceiverBuffer = _receiverBuffer.Count; 78 | int positionInReceiverBuffer = _receiverBuffer.Offset; 79 | int bytesSent = 0; 80 | 81 | while (bytesLeftInReceiverBuffer > 0 && CurrentBytesToSend != null) 82 | { 83 | int bytesToSend = Math.Min(bytesLeftInReceiverBuffer, CurrentBytesToSend.Value.Count); 84 | 85 | Array.Copy(CurrentBytesToSend.Value.Array, CurrentBytesToSend.Value.Offset, _receiverBuffer.Array, positionInReceiverBuffer, bytesToSend); 86 | 87 | if (bytesToSend == CurrentBytesToSend.Value.Count) 88 | { 89 | _currentBytesToSend = null; 90 | } 91 | else 92 | { 93 | _currentBytesToSend = new ArraySegment( 94 | CurrentBytesToSend.Value.Array, 95 | CurrentBytesToSend.Value.Offset + bytesToSend, 96 | CurrentBytesToSend.Value.Count - bytesToSend); 97 | } 98 | 99 | bytesSent += bytesToSend; 100 | 101 | if (bytesToSend == bytesLeftInReceiverBuffer) 102 | { 103 | break; 104 | } 105 | else 106 | { 107 | bytesLeftInReceiverBuffer -= bytesToSend; 108 | positionInReceiverBuffer += bytesToSend; 109 | } 110 | } 111 | 112 | TaskCompletionSource completedTcs = _receiveTcs; 113 | _receiveTcs = null; 114 | 115 | completedTcs.SetResult(bytesSent); 116 | } 117 | } 118 | 119 | Task ISocketAdapter.ReceiveAsync(byte[] buffer, int offset, int count) 120 | { 121 | Assert.False(_disposed, "ReceiveAsync was called on a socket that has been disposed."); 122 | 123 | AssertWithMessage.Null(_receiveTcs, "Multiple calls to ReceiveAsync are occuring at the same time"); 124 | 125 | _receiveTcs = new TaskCompletionSource(); 126 | _receiverBuffer = new ArraySegment(buffer, offset, count); 127 | 128 | Task resultTask = _receiveTcs.Task; 129 | 130 | PushBytesToReceiver(); 131 | 132 | return resultTask; 133 | } 134 | 135 | Task ISocketAdapter.SendAsync(IList> buffers) 136 | { 137 | Assert.False(_disposed, "SendAsync was called on a socket that has been disposed."); 138 | 139 | TaskCompletionSource tcs = new TaskCompletionSource(); 140 | 141 | try 142 | { 143 | if (_throwExceptionOnNextSendAsync) 144 | { 145 | tcs.SetException(new Exception("SendAsync after ThrowExceptionOnNextSendAsync was called.")); 146 | } 147 | else 148 | { 149 | foreach (ArraySegment buffer in buffers) 150 | { 151 | _sentContent.Append(Encoding.ASCII.GetString(buffer.Array, buffer.Offset, buffer.Count)); 152 | } 153 | 154 | tcs.SetResult(null); 155 | } 156 | } 157 | catch (Exception ex) 158 | { 159 | tcs.SetException(ex); 160 | } 161 | 162 | return tcs.Task; 163 | } 164 | 165 | void IDisposable.Dispose() 166 | { 167 | _disposed = true; 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/baseline.netcore.json: -------------------------------------------------------------------------------- 1 | { 2 | "AssemblyIdentity": "Microsoft.VisualStudio.Web.BrowserLink, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", 3 | "Types": [ 4 | { 5 | "Name": "Microsoft.AspNetCore.Builder.BrowserLinkExtensions", 6 | "Visibility": "Public", 7 | "Kind": "Class", 8 | "Abstract": true, 9 | "Static": true, 10 | "Sealed": true, 11 | "ImplementedInterfaces": [], 12 | "Members": [ 13 | { 14 | "Kind": "Method", 15 | "Name": "UseBrowserLink", 16 | "Parameters": [ 17 | { 18 | "Name": "app", 19 | "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" 20 | } 21 | ], 22 | "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", 23 | "Static": true, 24 | "Extension": true, 25 | "Visibility": "Public", 26 | "GenericParameter": [] 27 | } 28 | ], 29 | "GenericParameters": [] 30 | }, 31 | { 32 | "Name": "Microsoft.VisualStudio.Web.BrowserLink.BrowserLinkConstants+MappingDataType", 33 | "Visibility": "Public", 34 | "Kind": "Enumeration", 35 | "Sealed": true, 36 | "ImplementedInterfaces": [], 37 | "Members": [ 38 | { 39 | "Kind": "Field", 40 | "Name": "Unknown", 41 | "Parameters": [], 42 | "GenericParameter": [], 43 | "Literal": "0" 44 | }, 45 | { 46 | "Kind": "Field", 47 | "Name": "BeginContext", 48 | "Parameters": [], 49 | "GenericParameter": [], 50 | "Literal": "1" 51 | }, 52 | { 53 | "Kind": "Field", 54 | "Name": "EndContext", 55 | "Parameters": [], 56 | "GenericParameter": [], 57 | "Literal": "2" 58 | }, 59 | { 60 | "Kind": "Field", 61 | "Name": "RenderedOutputDefinition", 62 | "Parameters": [], 63 | "GenericParameter": [], 64 | "Literal": "3" 65 | }, 66 | { 67 | "Kind": "Field", 68 | "Name": "RenderedOutputRelationship", 69 | "Parameters": [], 70 | "GenericParameter": [], 71 | "Literal": "4" 72 | }, 73 | { 74 | "Kind": "Field", 75 | "Name": "EndOfData", 76 | "Parameters": [], 77 | "GenericParameter": [], 78 | "Literal": "5" 79 | } 80 | ], 81 | "GenericParameters": [] 82 | }, 83 | { 84 | "Name": "Microsoft.VisualStudio.Web.BrowserLink.BrowserLinkConstants+MappingDataValueType", 85 | "Visibility": "Public", 86 | "Kind": "Enumeration", 87 | "Sealed": true, 88 | "ImplementedInterfaces": [], 89 | "Members": [ 90 | { 91 | "Kind": "Field", 92 | "Name": "UnknownValue", 93 | "Parameters": [], 94 | "GenericParameter": [], 95 | "Literal": "0" 96 | }, 97 | { 98 | "Kind": "Field", 99 | "Name": "Int32Value", 100 | "Parameters": [], 101 | "GenericParameter": [], 102 | "Literal": "1" 103 | }, 104 | { 105 | "Kind": "Field", 106 | "Name": "BooleanValue", 107 | "Parameters": [], 108 | "GenericParameter": [], 109 | "Literal": "2" 110 | }, 111 | { 112 | "Kind": "Field", 113 | "Name": "StringValue", 114 | "Parameters": [], 115 | "GenericParameter": [], 116 | "Literal": "3" 117 | } 118 | ], 119 | "GenericParameters": [] 120 | }, 121 | { 122 | "Name": "Microsoft.VisualStudio.Web.BrowserLink.BrowserLinkConstants+MappingDataValue", 123 | "Visibility": "Public", 124 | "Kind": "Enumeration", 125 | "Sealed": true, 126 | "ImplementedInterfaces": [], 127 | "Members": [ 128 | { 129 | "Kind": "Field", 130 | "Name": "SourceStartPosition", 131 | "Parameters": [], 132 | "GenericParameter": [], 133 | "Literal": "1" 134 | }, 135 | { 136 | "Kind": "Field", 137 | "Name": "SourceLength", 138 | "Parameters": [], 139 | "GenericParameter": [], 140 | "Literal": "2" 141 | }, 142 | { 143 | "Kind": "Field", 144 | "Name": "SourceFilePath", 145 | "Parameters": [], 146 | "GenericParameter": [], 147 | "Literal": "3" 148 | }, 149 | { 150 | "Kind": "Field", 151 | "Name": "RenderedPosition", 152 | "Parameters": [], 153 | "GenericParameter": [], 154 | "Literal": "4" 155 | }, 156 | { 157 | "Kind": "Field", 158 | "Name": "RenderedOutputIndex", 159 | "Parameters": [], 160 | "GenericParameter": [], 161 | "Literal": "5" 162 | }, 163 | { 164 | "Kind": "Field", 165 | "Name": "RenderedContent", 166 | "Parameters": [], 167 | "GenericParameter": [], 168 | "Literal": "6" 169 | }, 170 | { 171 | "Kind": "Field", 172 | "Name": "IsLiteral", 173 | "Parameters": [], 174 | "GenericParameter": [], 175 | "Literal": "7" 176 | }, 177 | { 178 | "Kind": "Field", 179 | "Name": "ParentRenderedOutputIndex", 180 | "Parameters": [], 181 | "GenericParameter": [], 182 | "Literal": "11" 183 | }, 184 | { 185 | "Kind": "Field", 186 | "Name": "ChildRenderedOutputIndex", 187 | "Parameters": [], 188 | "GenericParameter": [], 189 | "Literal": "12" 190 | }, 191 | { 192 | "Kind": "Field", 193 | "Name": "RelativeRenderedPosition", 194 | "Parameters": [], 195 | "GenericParameter": [], 196 | "Literal": "13" 197 | }, 198 | { 199 | "Kind": "Field", 200 | "Name": "EndOfDataValues", 201 | "Parameters": [], 202 | "GenericParameter": [], 203 | "Literal": "-1" 204 | } 205 | ], 206 | "GenericParameters": [] 207 | } 208 | ] 209 | } -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/TaskHelpersTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | public class TaskHelpersTest 9 | { 10 | [Fact] 11 | public void TaskHelpers_WaitWithTimeout_ReturnsCompletedTaskResult() 12 | { 13 | // Arrange 14 | TaskCompletionSource tcs = new TaskCompletionSource(); 15 | TimeSpan timeout = TimeSpan.FromMilliseconds(1000); 16 | 17 | tcs.SetResult("Hello"); 18 | 19 | // Act 20 | Task result = TaskHelpers.WaitWithTimeout(tcs.Task, timeout, null); 21 | 22 | // Assert 23 | TaskAssert.ResultEquals(result, "Hello"); 24 | } 25 | 26 | [Fact] 27 | public void TaskHelpers_WaitWithTimeout_ReturnsTaskResultWhenComplete() 28 | { 29 | // Arrange 30 | TaskCompletionSource tcs = new TaskCompletionSource(); 31 | TimeSpan timeout = TimeSpan.FromMilliseconds(1000); 32 | 33 | Task result = TaskHelpers.WaitWithTimeout(tcs.Task, timeout, null); 34 | 35 | // Act 36 | tcs.SetResult("Hello"); 37 | 38 | // Assert 39 | TaskAssert.ResultEquals(result, "Hello"); 40 | } 41 | 42 | [Fact (Skip = "https://github.com/aspnet/BrowserLink/issues/43")] 43 | public void TaskHelpers_WaitWithTimeout_ReturnsTimeoutResultIfTaskNotComplete() 44 | { 45 | // Arrange 46 | TaskCompletionSource tcs = new TaskCompletionSource(); 47 | TimeSpan timeout = TimeSpan.FromMilliseconds(500); 48 | 49 | Task result = TaskHelpers.WaitWithTimeout(tcs.Task, timeout, "Timed out"); 50 | 51 | // Act 52 | bool completed = result.Wait(millisecondsTimeout: 550); 53 | 54 | // Assert 55 | Assert.True(completed, "Task did not time out"); 56 | TaskAssert.ResultEquals(result, "Timed out"); 57 | } 58 | 59 | [Fact] 60 | public void TaskHelpers_WaitWithTimeout_HandlesTaskFault() 61 | { 62 | // Arrange 63 | TaskCompletionSource tcs = new TaskCompletionSource(); 64 | TimeSpan timeout = TimeSpan.FromMilliseconds(1000); 65 | 66 | Task result = TaskHelpers.WaitWithTimeout(tcs.Task, timeout, "Timed out"); 67 | 68 | // Act 69 | tcs.SetException(new Exception("MY TEST ERROR")); 70 | 71 | // Assert 72 | TaskAssert.Faulted(result); 73 | } 74 | 75 | [Fact] 76 | public void TaskHelpers_WaitWithCancellation_DoesNotCompleteUntilTaskCompletes() 77 | { 78 | // Arrange 79 | TaskCompletionSource tcs = new TaskCompletionSource(); 80 | CancellationTokenSource cts = new CancellationTokenSource(); 81 | 82 | // Act 83 | Task result = TaskHelpers.WaitWithCancellation(tcs.Task, cts.Token, null); 84 | 85 | // Assert 86 | TaskAssert.NotCompleted(result); 87 | } 88 | 89 | [Fact] 90 | public void TaskHelpers_WaitWithCancellation_ReturnsCompletedTask() 91 | { 92 | // Arrange 93 | Task task = Task.FromResult("Hello"); 94 | CancellationTokenSource cts = new CancellationTokenSource(); 95 | 96 | // Act 97 | Task result = TaskHelpers.WaitWithCancellation(task, cts.Token, null); 98 | 99 | // Assert 100 | TaskAssert.ResultEquals(result, "Hello"); 101 | } 102 | 103 | [Fact] 104 | public void TaskHelpers_WaitWithCancellation_ReturnsTaskWhenCompleted() 105 | { 106 | // Arrange 107 | TaskCompletionSource tcs = new TaskCompletionSource(); 108 | CancellationTokenSource cts = new CancellationTokenSource(); 109 | 110 | Task result = TaskHelpers.WaitWithCancellation(tcs.Task, cts.Token, null); 111 | 112 | // Act 113 | tcs.SetResult("Hello"); 114 | 115 | // Assert 116 | TaskAssert.ResultEquals(result, "Hello"); 117 | } 118 | 119 | [Fact] 120 | public void TaskHelpers_WaitWithCancellation_ReturnsCancelValueForCancelledToken() 121 | { 122 | // Arrange 123 | TaskCompletionSource tcs = new TaskCompletionSource(); 124 | CancellationTokenSource cts = new CancellationTokenSource(); 125 | 126 | cts.Cancel(); 127 | 128 | // Act 129 | Task result = TaskHelpers.WaitWithCancellation(tcs.Task, cts.Token, "Cancelled"); 130 | 131 | // Assert 132 | TaskAssert.ResultEquals(result, "Cancelled"); 133 | } 134 | 135 | [Fact] 136 | public void TaskHelpers_WaitWithCancellation_ReturnsCancelValueWhenTokenCancels() 137 | { 138 | // Arrange 139 | TaskCompletionSource tcs = new TaskCompletionSource(); 140 | CancellationTokenSource cts = new CancellationTokenSource(); 141 | 142 | Task result = TaskHelpers.WaitWithCancellation(tcs.Task, cts.Token, "Cancelled"); 143 | 144 | // Act 145 | cts.Cancel(); 146 | 147 | // Assert 148 | TaskAssert.ResultEquals(result, "Cancelled"); 149 | } 150 | 151 | [Fact] 152 | public void TaskHelpers_WaitWithCancellation_HandlesMultipleCancels() 153 | { 154 | // Arrange 155 | TaskCompletionSource tcs = new TaskCompletionSource(); 156 | CancellationTokenSource cts = new CancellationTokenSource(); 157 | 158 | Task result = TaskHelpers.WaitWithCancellation(tcs.Task, cts.Token, "Cancelled"); 159 | 160 | cts.Cancel(); 161 | 162 | // Act 163 | cts.Cancel(); 164 | 165 | // Assert 166 | TaskAssert.ResultEquals(result, "Cancelled"); 167 | } 168 | 169 | [Fact] 170 | public void TaskHelpers_WaitWithCancellation_ResturnsTaskResultIfTaskCompleteAndCanceledSimultaneously() 171 | { 172 | // Arrange 173 | TaskCompletionSource tcs = new TaskCompletionSource(); 174 | CancellationTokenSource cts = new CancellationTokenSource(); 175 | 176 | tcs.SetResult("Not Cancelled"); 177 | cts.Cancel(); 178 | 179 | // Act 180 | Task result = TaskHelpers.WaitWithCancellation(tcs.Task, cts.Token, "Cancelled"); 181 | 182 | // Assert 183 | TaskAssert.ResultEquals(result, "Not Cancelled"); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env powershell 2 | #requires -version 4 3 | 4 | <# 5 | .SYNOPSIS 6 | Executes KoreBuild commands. 7 | 8 | .DESCRIPTION 9 | Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. 10 | 11 | .PARAMETER Command 12 | The KoreBuild command to run. 13 | 14 | .PARAMETER Path 15 | The folder to build. Defaults to the folder containing this script. 16 | 17 | .PARAMETER Channel 18 | The channel of KoreBuild to download. Overrides the value from the config file. 19 | 20 | .PARAMETER DotNetHome 21 | The directory where .NET Core tools will be stored. 22 | 23 | .PARAMETER ToolsSource 24 | The base url where build tools can be downloaded. Overrides the value from the config file. 25 | 26 | .PARAMETER Update 27 | Updates KoreBuild to the latest version even if a lock file is present. 28 | 29 | .PARAMETER Reinstall 30 | Re-installs KoreBuild 31 | 32 | .PARAMETER ConfigFile 33 | The path to the configuration file that stores values. Defaults to korebuild.json. 34 | 35 | .PARAMETER ToolsSourceSuffix 36 | The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. 37 | 38 | .PARAMETER CI 39 | Sets up CI specific settings and variables. 40 | 41 | .PARAMETER Arguments 42 | Arguments to be passed to the command 43 | 44 | .NOTES 45 | This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. 46 | When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. 47 | 48 | The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set 49 | in the file are overridden by command line parameters. 50 | 51 | .EXAMPLE 52 | Example config file: 53 | ```json 54 | { 55 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 56 | "channel": "master", 57 | "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" 58 | } 59 | ``` 60 | #> 61 | [CmdletBinding(PositionalBinding = $false)] 62 | param( 63 | [Parameter(Mandatory = $true, Position = 0)] 64 | [string]$Command, 65 | [string]$Path = $PSScriptRoot, 66 | [Alias('c')] 67 | [string]$Channel, 68 | [Alias('d')] 69 | [string]$DotNetHome, 70 | [Alias('s')] 71 | [string]$ToolsSource, 72 | [Alias('u')] 73 | [switch]$Update, 74 | [switch]$Reinstall, 75 | [string]$ToolsSourceSuffix, 76 | [string]$ConfigFile = $null, 77 | [switch]$CI, 78 | [Parameter(ValueFromRemainingArguments = $true)] 79 | [string[]]$Arguments 80 | ) 81 | 82 | Set-StrictMode -Version 2 83 | $ErrorActionPreference = 'Stop' 84 | 85 | # 86 | # Functions 87 | # 88 | 89 | function Get-KoreBuild { 90 | 91 | $lockFile = Join-Path $Path 'korebuild-lock.txt' 92 | 93 | if (!(Test-Path $lockFile) -or $Update) { 94 | Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix 95 | } 96 | 97 | $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 98 | if (!$version) { 99 | Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" 100 | } 101 | $version = $version.TrimStart('version:').Trim() 102 | $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) 103 | 104 | if ($Reinstall -and (Test-Path $korebuildPath)) { 105 | Remove-Item -Force -Recurse $korebuildPath 106 | } 107 | 108 | if (!(Test-Path $korebuildPath)) { 109 | Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" 110 | New-Item -ItemType Directory -Path $korebuildPath | Out-Null 111 | $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" 112 | 113 | try { 114 | $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" 115 | Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix 116 | if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { 117 | # Use built-in commands where possible as they are cross-plat compatible 118 | Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath 119 | } 120 | else { 121 | # Fallback to old approach for old installations of PowerShell 122 | Add-Type -AssemblyName System.IO.Compression.FileSystem 123 | [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) 124 | } 125 | } 126 | catch { 127 | Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore 128 | throw 129 | } 130 | finally { 131 | Remove-Item $tmpfile -ErrorAction Ignore 132 | } 133 | } 134 | 135 | return $korebuildPath 136 | } 137 | 138 | function Join-Paths([string]$path, [string[]]$childPaths) { 139 | $childPaths | ForEach-Object { $path = Join-Path $path $_ } 140 | return $path 141 | } 142 | 143 | function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { 144 | if ($RemotePath -notlike 'http*') { 145 | Copy-Item $RemotePath $LocalPath 146 | return 147 | } 148 | 149 | $retries = 10 150 | while ($retries -gt 0) { 151 | $retries -= 1 152 | try { 153 | Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath 154 | return 155 | } 156 | catch { 157 | Write-Verbose "Request failed. $retries retries remaining" 158 | } 159 | } 160 | 161 | Write-Error "Download failed: '$RemotePath'." 162 | } 163 | 164 | # 165 | # Main 166 | # 167 | 168 | # Load configuration or set defaults 169 | 170 | $Path = Resolve-Path $Path 171 | if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } 172 | 173 | if (Test-Path $ConfigFile) { 174 | try { 175 | $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json 176 | if ($config) { 177 | if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } 178 | if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} 179 | } 180 | } 181 | catch { 182 | Write-Host -ForegroundColor Red $Error[0] 183 | Write-Error "$ConfigFile contains invalid JSON." 184 | exit 1 185 | } 186 | } 187 | 188 | if (!$DotNetHome) { 189 | $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` 190 | elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` 191 | elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` 192 | else { Join-Path $PSScriptRoot '.dotnet'} 193 | } 194 | 195 | if (!$Channel) { $Channel = 'master' } 196 | if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } 197 | 198 | # Execute 199 | 200 | $korebuildPath = Get-KoreBuild 201 | Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') 202 | 203 | try { 204 | Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI 205 | Invoke-KoreBuildCommand $Command @Arguments 206 | } 207 | finally { 208 | Remove-Module 'KoreBuild' -ErrorAction Ignore 209 | } 210 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/SocketAdapter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using System.Threading.Tasks; 9 | 10 | namespace Microsoft.VisualStudio.Web.BrowserLink 11 | { 12 | /// 13 | /// SocketAdapter wraps a System.Net.Socket, and provides Task-based async 14 | /// methods for interacting with the Socket. It also provides an abstraction 15 | /// layer for unit tests, so that they don't have to work with real sockets. 16 | /// 17 | internal interface ISocketAdapter : IDisposable 18 | { 19 | Task ReceiveAsync(byte[] buffer, int offset, int count); 20 | 21 | Task SendAsync(IList> buffers); 22 | } 23 | 24 | internal class SocketAdapter : ISocketAdapter 25 | { 26 | private Socket _socket; 27 | private SocketAsyncEventArgs _sendEventArgs = new SocketAsyncEventArgs(); 28 | private SocketAsyncEventArgs _receiveEventArgs = new SocketAsyncEventArgs(); 29 | 30 | public SocketAdapter(Socket socket) 31 | { 32 | _socket = socket; 33 | 34 | _sendEventArgs.Completed += OnAsyncComplete; 35 | _receiveEventArgs.Completed += OnAsyncComplete; 36 | } 37 | 38 | internal static async Task OpenSocketAsync(Uri url) 39 | { 40 | try 41 | { 42 | Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); 43 | 44 | SocketAdapter adapter = new SocketAdapter(socket); 45 | 46 | await adapter.ConnectAsync(url); 47 | 48 | return adapter; 49 | } 50 | catch 51 | { 52 | // Handle any socket error and return null 53 | } 54 | 55 | return null; 56 | } 57 | 58 | public void Dispose() 59 | { 60 | if (_socket != null) 61 | { 62 | _socket.Dispose(); 63 | _socket = null; 64 | } 65 | 66 | if (_sendEventArgs != null) 67 | { 68 | _sendEventArgs.Completed -= OnAsyncComplete; 69 | 70 | _sendEventArgs.Dispose(); 71 | _sendEventArgs = null; 72 | } 73 | 74 | if (_receiveEventArgs != null) 75 | { 76 | _receiveEventArgs.Completed -= OnAsyncComplete; 77 | 78 | _receiveEventArgs.Dispose(); 79 | _receiveEventArgs = null; 80 | } 81 | } 82 | 83 | private Task ConnectAsync(Uri url) 84 | { 85 | TaskCompletionSource tcs = new TaskCompletionSource(); 86 | 87 | try 88 | { 89 | _sendEventArgs.UserToken = tcs; 90 | _sendEventArgs.RemoteEndPoint = new DnsEndPoint(url.Host, url.Port); 91 | 92 | _socket.ConnectAsync(_sendEventArgs); 93 | } 94 | catch (Exception ex) 95 | { 96 | tcs.SetException(ex); 97 | } 98 | 99 | return tcs.Task; 100 | } 101 | 102 | public Task ReceiveAsync(byte[] buffer, int offset, int count) 103 | { 104 | TaskCompletionSource tcs = new TaskCompletionSource(); 105 | 106 | if (_receiveEventArgs.UserToken != null) 107 | { 108 | // UserToken is set with a TCS whenever a receive is in progress, and is 109 | // cleared as soon as the send is complete. Do not allow two receive 110 | // operations at the same time. 111 | tcs.SetException(new InvalidOperationException("Attempted to read data when a read was already in progress.")); 112 | } 113 | else 114 | { 115 | try 116 | { 117 | _receiveEventArgs.SetBuffer(buffer, offset, count); 118 | _receiveEventArgs.UserToken = tcs; 119 | 120 | bool pending = _socket.ReceiveAsync(_receiveEventArgs); 121 | 122 | if (!pending) 123 | { 124 | // Operation completed synchronously 125 | OnAsyncComplete(_socket, _receiveEventArgs); 126 | } 127 | } 128 | catch (Exception ex) 129 | { 130 | tcs.SetException(ex); 131 | } 132 | } 133 | 134 | return tcs.Task; 135 | } 136 | 137 | public Task SendAsync(IList> buffers) 138 | { 139 | TaskCompletionSource tcs = new TaskCompletionSource(); 140 | 141 | if (_sendEventArgs.UserToken != null) 142 | { 143 | // UserToken is set with a TCS whenever a send is in progress, and is 144 | // cleared as soon as the send is complete. Do not allow two send 145 | // operations at the same time. 146 | tcs.SetException(new InvalidOperationException("Attempted to send data when a send was already in progress.")); 147 | } 148 | else 149 | { 150 | try 151 | { 152 | if (buffers.Count > 0) 153 | { 154 | _sendEventArgs.BufferList = buffers; 155 | _sendEventArgs.UserToken = tcs; 156 | 157 | bool pending = _socket.SendAsync(_sendEventArgs); 158 | 159 | if (!pending) 160 | { 161 | // Operation completed asynchronously 162 | OnAsyncComplete(_socket, _sendEventArgs); 163 | } 164 | } 165 | else 166 | { 167 | tcs.SetResult(0); 168 | } 169 | } 170 | catch (Exception ex) 171 | { 172 | tcs.SetException(ex); 173 | } 174 | } 175 | 176 | return tcs.Task; 177 | } 178 | 179 | private static void OnAsyncComplete(object sender, SocketAsyncEventArgs e) 180 | { 181 | TaskCompletionSource tcs = e.UserToken as TaskCompletionSource; 182 | 183 | // Clear this immediately! When the result is set on the TCS, it could 184 | // trigger more send or receive requests, and they will fail if it looks 185 | // like a request is still in progress. 186 | e.UserToken = null; 187 | 188 | try 189 | { 190 | if (e.SocketError == SocketError.OperationAborted) 191 | { 192 | tcs.SetResult(0); 193 | } 194 | else if (e.SocketError != SocketError.Success) 195 | { 196 | tcs.SetException(new SocketException((int)e.SocketError)); 197 | } 198 | else 199 | { 200 | tcs.SetResult(e.BytesTransferred); 201 | } 202 | } 203 | catch (Exception ex) 204 | { 205 | tcs.SetException(ex); 206 | } 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/Common/ArteryConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.VisualStudio.Web.BrowserLink 2 | { 3 | /// 4 | /// Well-known strings that are shared between the EurekaPackage in Visual Studio 5 | /// and the HttpModule running in ASP.NET. 6 | /// 7 | internal static class BrowserLinkConstants 8 | { 9 | /// 10 | /// Name of the index file when VS is elevated. This file can be accessed 11 | /// by any user, including the IIS user. 12 | /// 13 | public const string ElevatedIndexFileName = @"Global\PageInspector.Artery"; 14 | 15 | /// 16 | /// Name of the index file when VS is not elevated. This file can only be 17 | /// accessed by the current user, i.e. not by IIS. 18 | /// 19 | public const string NonElevatedIndexFileName = "PageInspector.Artery"; 20 | 21 | public static readonly string[] IndexFileNames = new string[] 22 | { 23 | ElevatedIndexFileName, 24 | NonElevatedIndexFileName, 25 | }; 26 | 27 | /// 28 | /// Suffix appended to the instance file name to get the name of the 29 | /// Request Signal, which is set to indicate that Artery should start. 30 | /// 31 | public const string RequestSignalSuffix = ".RequestSignal"; 32 | 33 | /// 34 | /// Suffix appended to the instance file name to get the name of the 35 | /// Ready Signal, which blocks ASP.NET until the Artery server is ready. 36 | /// 37 | public const string ReadySignalSuffix = ".ReadySignal"; 38 | 39 | /// 40 | /// Request header sent from the Browser Link runtime to identify which 41 | /// page request the messages correspond to. 42 | /// 43 | public const string RequestIdHeaderName = "BrowserLink-RequestID"; 44 | 45 | /// 46 | /// Request sent from the Browser Link runtime to identify whether it's 47 | /// a "http" one or a "https" one. 48 | /// 49 | public const string RequestScheme = "Scheme"; 50 | 51 | /// 52 | /// The url of the host used for project match. 53 | /// 54 | public const string RequestHostUrl = "HostUrl"; 55 | 56 | /// 57 | /// Suffix added to the end of the instance file name to identify a 58 | /// Version 2 instance file 59 | /// 60 | public const string Version2Suffix = ".v2"; 61 | 62 | // Keys for data in the instance file (v2) 63 | public const string HostNameKey = "host-name"; 64 | public const string FetchScriptVerbKey = "verb-fetch-script"; 65 | public const string InjectScriptVerbKey = "verb-inject-script"; 66 | public const string MappingDataVerbKey = "verb-mapping-data"; 67 | public const string HttpPortKey = "http-port"; 68 | public const string HttpsPortKey = "https-port"; 69 | public const string ServerDataVerbKey = "verb-server-data"; 70 | public const string ProjectDataKey = "project"; 71 | 72 | /// 73 | /// Constants representing the type of mapping data to follow 74 | /// 75 | public enum MappingDataType 76 | { 77 | /// 78 | /// This value is never sent over the wire. It is used internally for data 79 | /// blocks that are unrecognized by the receiver. 80 | /// 81 | Unknown = 0, 82 | 83 | /// 84 | /// Push a source context onto the stack. Data to follow includes: 85 | /// SourceStartPosition 86 | /// SourceLength 87 | /// SourceFilePath 88 | /// RenderedPosition 89 | /// RenderedOutputIndex 90 | /// IsLiteral 91 | /// 92 | BeginContext = 1, 93 | 94 | /// 95 | /// Pop a source context from the stack. Data to follow includes: 96 | /// RenderedOutputIndex 97 | /// RenderedPosition 98 | /// 99 | EndContext = 2, 100 | 101 | /// 102 | /// Define a rendered output. Data to follow includes: 103 | /// RenderedOutputIndex 104 | /// RenderedContent 105 | /// 106 | RenderedOutputDefinition = 3, 107 | 108 | /// 109 | /// Define a relationship between to rendered outputs. Data to follow includes: 110 | /// ParentRenderedOutputIndex 111 | /// ChildRenderedOutputindex 112 | /// RelativeRenderedPosition 113 | /// 114 | RenderedOutputRelationship = 4, 115 | 116 | /// 117 | /// Mapping data is complete. 118 | /// 119 | EndOfData = 5, 120 | } 121 | 122 | public enum MappingDataValueType 123 | { 124 | UnknownValue = 0, 125 | Int32Value = 1, 126 | BooleanValue = 2, 127 | StringValue = 3, 128 | } 129 | 130 | public enum MappingDataValue 131 | { 132 | /// 133 | /// (Int32) The first character position of the range of the source file 134 | /// that is currently being rendered. 135 | /// 136 | SourceStartPosition = 1, 137 | 138 | /// 139 | /// (Int32) The length of the range of the source file that is currently 140 | /// being rendered. 141 | /// 142 | SourceLength = 2, 143 | 144 | /// 145 | /// (string) The path to the source file that is currently being rendered. 146 | /// 147 | SourceFilePath = 3, 148 | 149 | /// 150 | /// (Int32) The current position in the rendered output that is being written. 151 | /// 152 | RenderedPosition = 4, 153 | 154 | /// 155 | /// (Int32) The index of the rendered output that is being written. 156 | /// 157 | RenderedOutputIndex = 5, 158 | 159 | /// 160 | /// (string) The final content of a rendered output 161 | /// 162 | RenderedContent = 6, 163 | 164 | /// 165 | /// (Boolean) True if a source range is being exactly copied from the source 166 | /// file to the rendered output. This means character-by-character mapping 167 | /// can be done in this source range. 168 | /// 169 | IsLiteral = 7, 170 | 171 | 172 | /// 173 | /// (Int32) For a rendered output relationship, this is the index of the 174 | /// rendered output that will contain the other output. 175 | /// 176 | ParentRenderedOutputIndex = 11, 177 | 178 | /// 179 | /// (Int32) For a rendered output relationship, this is the index of the 180 | /// rendered output that is contained by the other output. 181 | /// 182 | ChildRenderedOutputIndex = 12, 183 | 184 | /// 185 | /// (Int32) For a rendered output relationship, this is the position where 186 | /// the parent rendered output will contain the child rendered output. 187 | /// 188 | RelativeRenderedPosition = 13, 189 | 190 | 191 | /// 192 | /// No more data values in this block. 193 | /// 194 | EndOfDataValues = -1 195 | } 196 | 197 | /// 198 | /// Preamble bytes that are returned from BrowserLinkFilterOwinModule to force 199 | /// headers to be returned early. 200 | /// 201 | public static readonly byte[] FilterPreamble = new byte[] { 0xFF }; 202 | } 203 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/SocketReader.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Text; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.VisualStudio.Web.BrowserLink 10 | { 11 | /// 12 | /// This is a helper class for reading data from a Socket. It encapsulates 13 | /// the ability to alternately read decoded characters and lines, or raw 14 | /// data, from the same data source. 15 | /// 16 | internal class SocketReader 17 | { 18 | private ISocketAdapter _socket; 19 | private DecoderAdapter _decoderAdapter = new DecoderAdapter(); 20 | 21 | private byte[] _buffer = new byte[10240]; 22 | private int _bufferCurrentPosition = 0; 23 | private int _bufferStopPosition = 0; 24 | 25 | internal SocketReader(ISocketAdapter socket) 26 | { 27 | _socket = socket; 28 | } 29 | 30 | /// 31 | /// Sets the Encoding that will be used for ReadChar and ReadLine. 32 | /// 33 | public void SetEncoding(Encoding encoding) 34 | { 35 | _decoderAdapter.SetDecoder(encoding.GetDecoder()); 36 | } 37 | 38 | /// 39 | /// Read characters from the socket until a CRLF is encountered. 40 | /// 41 | /// 42 | /// A task that completes when enough data has been returned by the server. 43 | /// The task returns the text of the line, excluding the CRLF. 44 | /// 45 | public async Task ReadLine(CancellationToken cancellationToken) 46 | { 47 | StringBuilder line = new StringBuilder(); 48 | 49 | bool foundCr = false; 50 | 51 | while (true) 52 | { 53 | char character = await ReadChar(cancellationToken); 54 | 55 | if (cancellationToken.IsCancellationRequested) 56 | { 57 | break; 58 | } 59 | 60 | if (character == '\r') 61 | { 62 | if (!foundCr) 63 | { 64 | foundCr = true; 65 | 66 | continue; 67 | } 68 | } 69 | else if (foundCr) 70 | { 71 | if (character == '\n') 72 | { 73 | break; 74 | } 75 | else 76 | { 77 | foundCr = false; 78 | 79 | line.Append('\r'); 80 | } 81 | } 82 | 83 | line.Append(character); 84 | } 85 | 86 | return line.ToString(); 87 | } 88 | 89 | /// 90 | /// Read a single character from the socket. 91 | /// 92 | /// A task that completes when enough data has been returned from the server. 93 | public async Task ReadChar(CancellationToken cancellationToken) 94 | { 95 | char result; 96 | 97 | while (!DecodeChar(out result)) 98 | { 99 | if (cancellationToken.IsCancellationRequested) 100 | { 101 | break; 102 | } 103 | 104 | await ReceiveMoreBytes(cancellationToken); 105 | } 106 | 107 | return result; 108 | } 109 | 110 | /// 111 | /// Reads some raw data into a ResponseHandler method. 112 | /// 113 | /// 114 | /// A task that completes when enough data has been returned by the server. 115 | /// The task returns the number of bytes sent to the ResponseHandler. 116 | /// 117 | public async Task ReadBytesIntoResponseHandler(long totalBytesToRead, ResponseHandler handler, CancellationToken cancellationToken) 118 | { 119 | long totalBytesRead = 0; 120 | 121 | while (totalBytesToRead > 0) 122 | { 123 | if (cancellationToken.IsCancellationRequested) 124 | { 125 | break; 126 | } 127 | 128 | int bytesRemainingInBuffer = _bufferStopPosition - _bufferCurrentPosition; 129 | 130 | if (bytesRemainingInBuffer == 0) 131 | { 132 | await ReceiveMoreBytes(cancellationToken); 133 | 134 | bytesRemainingInBuffer = _bufferStopPosition - _bufferCurrentPosition; 135 | } 136 | 137 | int bytesToRead; 138 | if (totalBytesToRead < bytesRemainingInBuffer) 139 | { 140 | // This cast is safe because totalBytesToRead < bytesRemainingInBuffer, 141 | // and bytesRemainingInBuffer is an int. 142 | bytesToRead = (int)totalBytesToRead; 143 | } 144 | else 145 | { 146 | bytesToRead = bytesRemainingInBuffer; 147 | } 148 | 149 | await handler.Invoke(_buffer, _bufferCurrentPosition, bytesToRead); 150 | 151 | _bufferCurrentPosition += bytesToRead; 152 | totalBytesRead += bytesToRead; 153 | totalBytesToRead -= bytesToRead; 154 | } 155 | 156 | return (int)totalBytesRead; 157 | } 158 | 159 | private async Task ReceiveMoreBytes(CancellationToken cancellationToken) 160 | { 161 | int bytesRemainingInBuffer = _bufferStopPosition - _bufferCurrentPosition; 162 | int bytesReceived = 0; 163 | 164 | if (bytesRemainingInBuffer > 0) 165 | { 166 | Array.Copy(_buffer, _bufferCurrentPosition, _buffer, 0, bytesRemainingInBuffer); 167 | 168 | _bufferCurrentPosition = 0; 169 | _bufferStopPosition = bytesRemainingInBuffer; 170 | } 171 | 172 | int spaceInBuffer = _buffer.Length - bytesRemainingInBuffer; 173 | 174 | if (spaceInBuffer > 0) 175 | { 176 | bytesReceived = await TaskHelpers.WaitWithCancellation( 177 | _socket.ReceiveAsync(_buffer, bytesRemainingInBuffer, spaceInBuffer), 178 | cancellationToken, 0); 179 | 180 | _bufferCurrentPosition = 0; 181 | _bufferStopPosition = bytesRemainingInBuffer + bytesReceived; 182 | } 183 | 184 | return bytesReceived > 0; 185 | } 186 | 187 | private bool DecodeChar(out char result) 188 | { 189 | return _decoderAdapter.DecodeCharacter(_buffer, ref _bufferCurrentPosition, _bufferStopPosition - _bufferCurrentPosition, out result); 190 | } 191 | 192 | /// 193 | /// This helper class maintains state used in decoding characters 194 | /// that may be multi-byte, when all the bytes might not be returned 195 | /// by the server at the same time (or they might fall on the edge of 196 | /// the buffer used to read data from the server). 197 | /// 198 | private class DecoderAdapter 199 | { 200 | private Decoder _decoder = Encoding.ASCII.GetDecoder(); 201 | 202 | private char[] characterBuffer = new char[1]; 203 | 204 | public void SetDecoder(Decoder decoder) 205 | { 206 | _decoder = decoder; 207 | } 208 | 209 | public bool DecodeCharacter(byte[] buffer, ref int bufferPosition, int bufferBytes, out char result) 210 | { 211 | if (bufferBytes >= 1) 212 | { 213 | int bytesToUse = 1; 214 | int charactersReturned = 0; 215 | 216 | while (bytesToUse < bufferBytes) 217 | { 218 | charactersReturned = _decoder.GetCharCount(buffer, bufferPosition, bytesToUse); 219 | 220 | if (charactersReturned == 1) 221 | { 222 | break; 223 | } 224 | 225 | bytesToUse++; 226 | } 227 | 228 | charactersReturned = _decoder.GetChars(buffer, bufferPosition, bytesToUse, characterBuffer, 0); 229 | 230 | bufferPosition += bytesToUse; 231 | 232 | if (charactersReturned == 1) 233 | { 234 | result = characterBuffer[0]; 235 | return true; 236 | } 237 | } 238 | 239 | result = '\0'; 240 | return false; 241 | } 242 | } 243 | } 244 | } -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # 6 | # variables 7 | # 8 | 9 | RESET="\033[0m" 10 | RED="\033[0;31m" 11 | YELLOW="\033[0;33m" 12 | MAGENTA="\033[0;95m" 13 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 14 | [ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" 15 | verbose=false 16 | update=false 17 | reinstall=false 18 | repo_path="$DIR" 19 | channel='' 20 | tools_source='' 21 | tools_source_suffix='' 22 | ci=false 23 | 24 | # 25 | # Functions 26 | # 27 | __usage() { 28 | echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" 29 | echo "" 30 | echo "Arguments:" 31 | echo " command The command to be run." 32 | echo " ... Arguments passed to the command. Variable number of arguments allowed." 33 | echo "" 34 | echo "Options:" 35 | echo " --verbose Show verbose output." 36 | echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." 37 | echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." 38 | echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." 39 | echo " --path The directory to build. Defaults to the directory containing the script." 40 | echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." 41 | echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." 42 | echo " -u|--update Update to the latest KoreBuild even if the lock file is present." 43 | echo " --reinstall Reinstall KoreBuild." 44 | echo " --ci Apply CI specific settings and environment variables." 45 | echo "" 46 | echo "Description:" 47 | echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." 48 | echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." 49 | 50 | if [[ "${1:-}" != '--no-exit' ]]; then 51 | exit 2 52 | fi 53 | } 54 | 55 | get_korebuild() { 56 | local version 57 | local lock_file="$repo_path/korebuild-lock.txt" 58 | if [ ! -f "$lock_file" ] || [ "$update" = true ]; then 59 | __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" 60 | fi 61 | version="$(grep 'version:*' -m 1 "$lock_file")" 62 | if [[ "$version" == '' ]]; then 63 | __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" 64 | return 1 65 | fi 66 | version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" 67 | local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" 68 | 69 | if [ "$reinstall" = true ] && [ -d "$korebuild_path" ]; then 70 | rm -rf "$korebuild_path" 71 | fi 72 | 73 | { 74 | if [ ! -d "$korebuild_path" ]; then 75 | mkdir -p "$korebuild_path" 76 | local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" 77 | tmpfile="$(mktemp)" 78 | echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" 79 | if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then 80 | unzip -q -d "$korebuild_path" "$tmpfile" 81 | fi 82 | rm "$tmpfile" || true 83 | fi 84 | 85 | source "$korebuild_path/KoreBuild.sh" 86 | } || { 87 | if [ -d "$korebuild_path" ]; then 88 | echo "Cleaning up after failed installation" 89 | rm -rf "$korebuild_path" || true 90 | fi 91 | return 1 92 | } 93 | } 94 | 95 | __error() { 96 | echo -e "${RED}error: $*${RESET}" 1>&2 97 | } 98 | 99 | __warn() { 100 | echo -e "${YELLOW}warning: $*${RESET}" 101 | } 102 | 103 | __machine_has() { 104 | hash "$1" > /dev/null 2>&1 105 | return $? 106 | } 107 | 108 | __get_remote_file() { 109 | local remote_path=$1 110 | local local_path=$2 111 | local remote_path_suffix=$3 112 | 113 | if [[ "$remote_path" != 'http'* ]]; then 114 | cp "$remote_path" "$local_path" 115 | return 0 116 | fi 117 | 118 | local failed=false 119 | if __machine_has wget; then 120 | wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 121 | else 122 | failed=true 123 | fi 124 | 125 | if [ "$failed" = true ] && __machine_has curl; then 126 | failed=false 127 | curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 128 | fi 129 | 130 | if [ "$failed" = true ]; then 131 | __error "Download failed: $remote_path" 1>&2 132 | return 1 133 | fi 134 | } 135 | 136 | # 137 | # main 138 | # 139 | 140 | command="${1:-}" 141 | shift 142 | 143 | while [[ $# -gt 0 ]]; do 144 | case $1 in 145 | -\?|-h|--help) 146 | __usage --no-exit 147 | exit 0 148 | ;; 149 | -c|--channel|-Channel) 150 | shift 151 | channel="${1:-}" 152 | [ -z "$channel" ] && __usage 153 | ;; 154 | --config-file|-ConfigFile) 155 | shift 156 | config_file="${1:-}" 157 | [ -z "$config_file" ] && __usage 158 | if [ ! -f "$config_file" ]; then 159 | __error "Invalid value for --config-file. $config_file does not exist." 160 | exit 1 161 | fi 162 | ;; 163 | -d|--dotnet-home|-DotNetHome) 164 | shift 165 | DOTNET_HOME="${1:-}" 166 | [ -z "$DOTNET_HOME" ] && __usage 167 | ;; 168 | --path|-Path) 169 | shift 170 | repo_path="${1:-}" 171 | [ -z "$repo_path" ] && __usage 172 | ;; 173 | -s|--tools-source|-ToolsSource) 174 | shift 175 | tools_source="${1:-}" 176 | [ -z "$tools_source" ] && __usage 177 | ;; 178 | --tools-source-suffix|-ToolsSourceSuffix) 179 | shift 180 | tools_source_suffix="${1:-}" 181 | [ -z "$tools_source_suffix" ] && __usage 182 | ;; 183 | -u|--update|-Update) 184 | update=true 185 | ;; 186 | --reinstall|-[Rr]einstall) 187 | reinstall=true 188 | ;; 189 | --ci|-[Cc][Ii]) 190 | ci=true 191 | ;; 192 | --verbose|-Verbose) 193 | verbose=true 194 | ;; 195 | --) 196 | shift 197 | break 198 | ;; 199 | *) 200 | break 201 | ;; 202 | esac 203 | shift 204 | done 205 | 206 | if ! __machine_has unzip; then 207 | __error 'Missing required command: unzip' 208 | exit 1 209 | fi 210 | 211 | if ! __machine_has curl && ! __machine_has wget; then 212 | __error 'Missing required command. Either wget or curl is required.' 213 | exit 1 214 | fi 215 | 216 | [ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" 217 | if [ -f "$config_file" ]; then 218 | if __machine_has jq ; then 219 | if jq '.' "$config_file" >/dev/null ; then 220 | config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" 221 | config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" 222 | else 223 | __error "$config_file contains invalid JSON." 224 | exit 1 225 | fi 226 | elif __machine_has python ; then 227 | if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 228 | config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 229 | config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 230 | else 231 | __error "$config_file contains invalid JSON." 232 | exit 1 233 | fi 234 | elif __machine_has python3 ; then 235 | if python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 236 | config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 237 | config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 238 | else 239 | __error "$config_file contains invalid JSON." 240 | exit 1 241 | fi 242 | else 243 | __error 'Missing required command: jq or python. Could not parse the JSON file.' 244 | exit 1 245 | fi 246 | 247 | [ ! -z "${config_channel:-}" ] && channel="$config_channel" 248 | [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" 249 | fi 250 | 251 | [ -z "$channel" ] && channel='master' 252 | [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' 253 | 254 | get_korebuild 255 | set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" "$ci" 256 | invoke_korebuild_command "$command" "$@" 257 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/TextWriterDecorator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.VisualStudio.Web.BrowserLink 10 | { 11 | /// 12 | /// A wrapper for a TextWriter that allows us to record mapping information 13 | /// as content is written to the output. 14 | /// 15 | internal class TextWriterDecorator : TextWriter 16 | { 17 | private TextWriter _decoratedWriter; 18 | private StringWriter _bufferWriter; 19 | private PageExecutionListenerFeature _listener; 20 | 21 | internal TextWriterDecorator(TextWriter decoratedWriter, PageExecutionListenerFeature listener, int renderdOutputIndex) 22 | { 23 | _decoratedWriter = decoratedWriter; 24 | _listener = listener; 25 | 26 | _bufferWriter = new StringWriter(_decoratedWriter.FormatProvider); 27 | _bufferWriter.NewLine = _decoratedWriter.NewLine; 28 | 29 | RenderedOutputIndex = renderdOutputIndex; 30 | } 31 | 32 | /// 33 | /// Returns the current character position where output is being written. 34 | /// 35 | internal int OutputPosition 36 | { 37 | get { return _bufferWriter.GetStringBuilder().Length; } 38 | } 39 | 40 | /// 41 | /// An ID used to identify this writer withing the current page request. 42 | /// 43 | internal int RenderedOutputIndex 44 | { 45 | get; 46 | private set; 47 | } 48 | 49 | /// 50 | /// The complete content that has been written to this writer. 51 | /// 52 | internal string RenderedOutput 53 | { 54 | get { return _bufferWriter.GetStringBuilder().ToString(); } 55 | } 56 | 57 | #region TextWriter implementation 58 | // All TextWriter method implementations need to, at a minimum, pass exactly 59 | // the same method call to the decorated writer. Then they can do optional 60 | // recording operations. Usually this means writing the same data to a 61 | // second writer to remember what was written. 62 | 63 | public override Encoding Encoding 64 | { 65 | get { return _decoratedWriter.Encoding; } 66 | } 67 | 68 | protected override void Dispose(bool disposing) 69 | { 70 | if (disposing) 71 | { 72 | _decoratedWriter.Dispose(); 73 | 74 | _bufferWriter.Dispose(); 75 | } 76 | } 77 | 78 | public override void Flush() 79 | { 80 | _decoratedWriter.Flush(); 81 | } 82 | 83 | public override Task FlushAsync() 84 | { 85 | return _decoratedWriter.FlushAsync(); 86 | } 87 | 88 | public override IFormatProvider FormatProvider 89 | { 90 | get { return _decoratedWriter.FormatProvider; } 91 | } 92 | 93 | public override string NewLine 94 | { 95 | get { return _decoratedWriter.NewLine; } 96 | 97 | set 98 | { 99 | _decoratedWriter.NewLine = value; 100 | 101 | _bufferWriter.NewLine = value; 102 | } 103 | } 104 | 105 | public override void Write(char value) 106 | { 107 | _decoratedWriter.Write(value); 108 | 109 | _bufferWriter.Write(value); 110 | } 111 | 112 | public override void Write(bool value) 113 | { 114 | _bufferWriter.Write(value); 115 | 116 | _decoratedWriter.Write(value); 117 | } 118 | 119 | public override void Write(char[] buffer) 120 | { 121 | _bufferWriter.Write(buffer); 122 | 123 | _decoratedWriter.Write(buffer); 124 | } 125 | 126 | public override void Write(char[] buffer, int index, int count) 127 | { 128 | _bufferWriter.Write(buffer, index, count); 129 | 130 | _decoratedWriter.Write(buffer, index, count); 131 | } 132 | 133 | public override void Write(decimal value) 134 | { 135 | _bufferWriter.Write(value); 136 | 137 | _decoratedWriter.Write(value); 138 | } 139 | 140 | public override void Write(double value) 141 | { 142 | _bufferWriter.Write(value); 143 | 144 | _decoratedWriter.Write(value); 145 | } 146 | 147 | public override void Write(float value) 148 | { 149 | _bufferWriter.Write(value); 150 | 151 | _decoratedWriter.Write(value); 152 | } 153 | 154 | public override void Write(int value) 155 | { 156 | _bufferWriter.Write(value); 157 | 158 | _decoratedWriter.Write(value); 159 | } 160 | 161 | public override void Write(long value) 162 | { 163 | _bufferWriter.Write(value); 164 | 165 | _decoratedWriter.Write(value); 166 | } 167 | 168 | public override void Write(object value) 169 | { 170 | _bufferWriter.Write(value); 171 | 172 | _decoratedWriter.Write(value); 173 | } 174 | 175 | public override void Write(string value) 176 | { 177 | _bufferWriter.Write(value); 178 | 179 | _decoratedWriter.Write(value); 180 | } 181 | 182 | public override void Write(uint value) 183 | { 184 | _bufferWriter.Write(value); 185 | 186 | _decoratedWriter.Write(value); 187 | } 188 | 189 | public override void Write(ulong value) 190 | { 191 | _bufferWriter.Write(value); 192 | 193 | _decoratedWriter.Write(value); 194 | } 195 | 196 | public override Task WriteAsync(char value) 197 | { 198 | _bufferWriter.Write(value); 199 | 200 | return _decoratedWriter.WriteAsync(value); 201 | } 202 | 203 | public override Task WriteAsync(char[] buffer, int index, int count) 204 | { 205 | _bufferWriter.Write(buffer, index, count); 206 | 207 | return _decoratedWriter.WriteAsync(buffer, index, count); 208 | } 209 | 210 | public override Task WriteAsync(string value) 211 | { 212 | _bufferWriter.Write(value); 213 | 214 | return _decoratedWriter.WriteAsync(value); 215 | } 216 | 217 | public override void WriteLine() 218 | { 219 | _bufferWriter.WriteLine(); 220 | 221 | _decoratedWriter.WriteLine(); 222 | } 223 | 224 | public override void WriteLine(bool value) 225 | { 226 | _bufferWriter.WriteLine(value); 227 | 228 | _decoratedWriter.WriteLine(value); 229 | } 230 | 231 | public override void WriteLine(char value) 232 | { 233 | _bufferWriter.WriteLine(value); 234 | 235 | _decoratedWriter.WriteLine(value); 236 | } 237 | 238 | public override void WriteLine(char[] buffer) 239 | { 240 | _bufferWriter.WriteLine(buffer); 241 | 242 | _decoratedWriter.WriteLine(buffer); 243 | } 244 | 245 | public override void WriteLine(char[] buffer, int index, int count) 246 | { 247 | _bufferWriter.WriteLine(buffer, index, count); 248 | 249 | _decoratedWriter.WriteLine(buffer, index, count); 250 | } 251 | 252 | public override void WriteLine(decimal value) 253 | { 254 | _bufferWriter.WriteLine(value); 255 | 256 | _decoratedWriter.WriteLine(value); 257 | } 258 | 259 | public override void WriteLine(double value) 260 | { 261 | _bufferWriter.WriteLine(value); 262 | 263 | _decoratedWriter.WriteLine(value); 264 | } 265 | 266 | public override void WriteLine(float value) 267 | { 268 | _bufferWriter.WriteLine(value); 269 | 270 | _decoratedWriter.WriteLine(value); 271 | } 272 | 273 | public override void WriteLine(int value) 274 | { 275 | _bufferWriter.WriteLine(value); 276 | 277 | _decoratedWriter.WriteLine(value); 278 | } 279 | 280 | public override void WriteLine(long value) 281 | { 282 | _bufferWriter.WriteLine(value); 283 | 284 | _decoratedWriter.WriteLine(value); 285 | } 286 | 287 | public override void WriteLine(object value) 288 | { 289 | _bufferWriter.WriteLine(value); 290 | 291 | _decoratedWriter.WriteLine(value); 292 | } 293 | 294 | public override void WriteLine(string value) 295 | { 296 | _bufferWriter.WriteLine(value); 297 | 298 | _decoratedWriter.WriteLine(value); 299 | } 300 | 301 | public override void WriteLine(uint value) 302 | { 303 | _bufferWriter.WriteLine(value); 304 | 305 | _decoratedWriter.WriteLine(value); 306 | } 307 | 308 | public override void WriteLine(ulong value) 309 | { 310 | _bufferWriter.WriteLine(value); 311 | 312 | _decoratedWriter.WriteLine(value); 313 | } 314 | 315 | public override Task WriteLineAsync() 316 | { 317 | _bufferWriter.WriteLine(); 318 | 319 | return _decoratedWriter.WriteLineAsync(); 320 | } 321 | 322 | public override Task WriteLineAsync(char value) 323 | { 324 | _bufferWriter.WriteLine(value); 325 | 326 | return _decoratedWriter.WriteLineAsync(value); 327 | } 328 | 329 | public override Task WriteLineAsync(char[] buffer, int index, int count) 330 | { 331 | _bufferWriter.WriteLine(buffer, index, count); 332 | 333 | return _decoratedWriter.WriteLineAsync(buffer, index, count); 334 | } 335 | 336 | public override Task WriteLineAsync(string value) 337 | { 338 | _bufferWriter.WriteLine(value); 339 | 340 | return _decoratedWriter.WriteLineAsync(value); 341 | } 342 | #endregion 343 | } 344 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/BrowserLinkMiddleWare.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Http.Headers; 8 | using Microsoft.AspNetCore.Http.Features; 9 | 10 | namespace Microsoft.VisualStudio.Web.BrowserLink 11 | { 12 | /// 13 | /// This class is created once, and invoked for each request. It puts a filter on 14 | /// the Response.Body, which will inject Browser Link's script links into the 15 | /// content (if the content is HTML). 16 | /// 17 | internal class BrowserLinkMiddleware 18 | { 19 | // Number of timeouts allowed before we stop trying to connect to the host 20 | private const int FilterRequestTimeoutLimit = 2; 21 | 22 | // Number of timeouts that occurred while attempting to connect to the host 23 | private static int _filterRequestTimeouts = 0; 24 | 25 | private RequestDelegate _next; 26 | private string _applicationPath; 27 | 28 | internal BrowserLinkMiddleware(string applicationPath, RequestDelegate next) 29 | { 30 | _applicationPath = applicationPath; 31 | _next = next; 32 | } 33 | 34 | /// 35 | /// This method is called to process the response. 36 | /// 37 | internal Task Invoke(HttpContext context) 38 | { 39 | string requestId = Guid.NewGuid().ToString("N"); 40 | RequestHeaders requestHeader = new RequestHeaders(context.Request.Headers); 41 | 42 | string hostUrl = BrowserLinkMiddleWareUtil.GetRequestUrl(requestHeader); 43 | 44 | IHttpSocketAdapter injectScriptSocket = GetSocketConnectionToHost(_applicationPath, requestId, "injectScriptLink", context.Request.IsHttps, hostUrl); 45 | 46 | if (injectScriptSocket != null) 47 | { 48 | return ExecuteWithFilter(injectScriptSocket, requestId, context); 49 | } 50 | else 51 | { 52 | if (requestHeader.IfNoneMatch != null && BrowserLinkMiddleWareUtil.GetRequestPort(requestHeader).Count != 0) 53 | { 54 | BrowserLinkMiddleWareUtil.RemoveETagAndTimeStamp(requestHeader); 55 | } 56 | 57 | return ExecuteWithoutFilter(context); 58 | } 59 | } 60 | 61 | private PageExecutionListenerFeature AddPageExecutionListenerFeatureTo(HttpContext context, string requestId) 62 | { 63 | RequestHeaders requestHeader = new RequestHeaders(context.Request.Headers); 64 | 65 | string hostUrl = BrowserLinkMiddleWareUtil.GetRequestUrl(requestHeader); 66 | 67 | IHttpSocketAdapter mappingDataSocket = GetSocketConnectionToHost(_applicationPath, requestId, "sendMappingData", context.Request.IsHttps, hostUrl); 68 | 69 | if (mappingDataSocket != null) 70 | { 71 | PageExecutionListenerFeature listener = new PageExecutionListenerFeature(mappingDataSocket); 72 | 73 | context.Features.Set(listener); 74 | 75 | return listener; 76 | } 77 | 78 | return null; 79 | } 80 | 81 | private async Task ExecuteWithFilter(IHttpSocketAdapter injectScriptSocket, string requestId, HttpContext httpContext) 82 | { 83 | ScriptInjectionFilterContext filterContext = new ScriptInjectionFilterContext(httpContext); 84 | int currentPort = -1; 85 | 86 | PreprocessRequestHeader(httpContext, ref currentPort); 87 | 88 | RequestHeaders requestHeader = new RequestHeaders(httpContext.Request.Headers); 89 | 90 | if (currentPort == -1) 91 | { 92 | BrowserLinkMiddleWareUtil.RemoveETagAndTimeStamp(requestHeader); 93 | } 94 | 95 | using (ScriptInjectionFilterStream filter = new ScriptInjectionFilterStream(injectScriptSocket, filterContext)) 96 | { 97 | httpContext.Response.Body = filter; 98 | httpContext.Response.OnStarting(delegate () 99 | { 100 | if (ContentTypeUtil.IsSupportedContentTypes(httpContext.Response.ContentType)) 101 | { 102 | httpContext.Response.ContentLength = null; 103 | } 104 | 105 | ResponseHeaders responseHeader = new ResponseHeaders(httpContext.Response.Headers); 106 | 107 | BrowserLinkMiddleWareUtil.AddToETag(responseHeader, currentPort); 108 | 109 | return StaticTaskResult.True; 110 | }); 111 | 112 | IHttpSendFileFeature originalSendFile = httpContext.Features.Get(); 113 | httpContext.Features.Set(new SendFilesWrapper(originalSendFile, httpContext.Response)); 114 | 115 | using (AddPageExecutionListenerFeatureTo(httpContext, requestId)) 116 | { 117 | await _next(httpContext); 118 | 119 | await filter.WaitForFilterComplete(); 120 | 121 | if (filter.ScriptInjectionTimedOut) 122 | { 123 | _filterRequestTimeouts++; 124 | } 125 | else 126 | { 127 | _filterRequestTimeouts = 0; 128 | } 129 | } 130 | } 131 | } 132 | 133 | private Task ExecuteWithoutFilter(HttpContext context) 134 | { 135 | return _next(context); 136 | } 137 | 138 | private static IHttpSocketAdapter GetSocketConnectionToHost(string applicationPath, string requestId, string rpcMethod, bool isHttps, string hostUrl) 139 | { 140 | // The host should send an initial response immediately after 141 | // the connection is established. If it fails to do so multiple times, 142 | // stop trying. Each timeout is delaying a response to the browser. 143 | // 144 | // This will only reset when the server process is restarted. 145 | if (_filterRequestTimeouts >= FilterRequestTimeoutLimit) 146 | { 147 | return null; 148 | } 149 | 150 | if (FindAndSignalHostConnection(applicationPath)) 151 | { 152 | return new DelayConnectingHttpSocketAdapter(async delegate() 153 | { 154 | Uri connectionString; 155 | 156 | if (GetHostConnectionString(applicationPath, out connectionString)) 157 | { 158 | IHttpSocketAdapter httpSocket = await HttpSocketAdapter.OpenHttpSocketAsync("GET", new Uri(connectionString, rpcMethod)); 159 | 160 | AddRequestHeaders(httpSocket, requestId, isHttps, hostUrl); 161 | 162 | return httpSocket; 163 | } 164 | 165 | return null; 166 | }); 167 | } 168 | 169 | return null; 170 | } 171 | 172 | private static void AddRequestHeaders(IHttpSocketAdapter httpSocket, string requestId, bool isHttps, string hostUrl) 173 | { 174 | httpSocket.AddRequestHeader(BrowserLinkConstants.RequestIdHeaderName, requestId); 175 | 176 | if (isHttps) 177 | { 178 | httpSocket.AddRequestHeader(BrowserLinkConstants.RequestScheme, "https"); 179 | } 180 | else 181 | { 182 | httpSocket.AddRequestHeader(BrowserLinkConstants.RequestScheme, "http"); 183 | } 184 | 185 | httpSocket.AddRequestHeader(BrowserLinkConstants.RequestHostUrl, hostUrl); 186 | } 187 | 188 | private static bool FindAndSignalHostConnection(string applicationPath) 189 | { 190 | HostConnectionData connectionData; 191 | 192 | if (HostConnectionUtil.FindHostConnection(applicationPath, out connectionData)) 193 | { 194 | return HostConnectionUtil.SignalHostForStartup(connectionData, blockUntilStarted: false); 195 | } 196 | 197 | return false; 198 | } 199 | 200 | private static bool GetHostConnectionString(string applicationPath, out Uri connectionString) 201 | { 202 | HostConnectionData connectionData; 203 | 204 | if (GetHostConnectionData(applicationPath, out connectionData)) 205 | { 206 | return Uri.TryCreate(connectionData.ConnectionString, UriKind.Absolute, out connectionString); 207 | } 208 | 209 | connectionString = null; 210 | return false; 211 | } 212 | 213 | private static bool GetHostConnectionData(string applicationPath, out HostConnectionData connectionData) 214 | { 215 | if (HostConnectionUtil.FindHostConnection(applicationPath, out connectionData)) 216 | { 217 | return EnsureHostServerStarted(applicationPath, ref connectionData); 218 | } 219 | 220 | connectionData = null; 221 | return false; 222 | } 223 | 224 | private static bool EnsureHostServerStarted(string applicationPath, ref HostConnectionData connectionData) 225 | { 226 | if (String.IsNullOrEmpty(connectionData.ConnectionString)) 227 | { 228 | if (!HostConnectionUtil.SignalHostForStartup(connectionData)) 229 | { 230 | return false; 231 | } 232 | 233 | if (!HostConnectionUtil.FindHostConnection(applicationPath, out connectionData)) 234 | { 235 | return false; 236 | } 237 | 238 | if (String.IsNullOrEmpty(connectionData.ConnectionString)) 239 | { 240 | return false; 241 | } 242 | } 243 | 244 | return true; 245 | } 246 | 247 | private void PreprocessRequestHeader(HttpContext httpContext, ref int currentPort) 248 | { 249 | RequestHeaders requestHeader = new RequestHeaders(httpContext.Request.Headers); 250 | 251 | if (requestHeader.IfNoneMatch != null) 252 | { 253 | HostConnectionData connectionData; 254 | 255 | if (GetHostConnectionData(_applicationPath, out connectionData)) 256 | { 257 | currentPort = BrowserLinkMiddleWareUtil.FilterRequestHeader(requestHeader, connectionData.ConnectionString); 258 | } 259 | } 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/ScriptInjectionFilterStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.VisualStudio.Web.BrowserLink 10 | { 11 | /// 12 | /// This stream implementation is a passthrough filter. It's job is to add 13 | /// links to the Browser Link connection scripts at the end of HTML content. 14 | /// 15 | /// It does this using a connection to the host, where the actual filtering 16 | /// work is done. If anything goes wrong with the host connection, or 17 | /// if the content being written is not actually HTML, then the filter goes 18 | /// into passthrough mode and returns all content to the output stream unchanged. 19 | /// 20 | internal class ScriptInjectionFilterStream : Stream 21 | { 22 | private enum FilterState 23 | { 24 | NothingSentToFilter, 25 | ContentSentToFilter, 26 | Passthrough, 27 | } 28 | 29 | private IHttpSocketAdapter _injectScriptSocket; 30 | private IScriptInjectionFilterContext _context; 31 | private Stream _outputStream; 32 | 33 | private FilterState _filterState = FilterState.NothingSentToFilter; 34 | 35 | internal ScriptInjectionFilterStream(IHttpSocketAdapter injectScriptSocket, IScriptInjectionFilterContext context) 36 | { 37 | _context = context; 38 | _outputStream = _context.ResponseBody; 39 | _injectScriptSocket = injectScriptSocket; 40 | 41 | _injectScriptSocket.SetResponseHandler(CreateResponseHandler(_outputStream)); 42 | } 43 | 44 | /// 45 | /// Returns true if the filter is in Passthrough mode. All writes will 46 | /// be passed to the output stream unmodified. 47 | /// 48 | public bool IsPassthrough 49 | { 50 | get { return _filterState == FilterState.Passthrough; } 51 | } 52 | 53 | /// 54 | /// Returns true if any data has been sent to the host. At this point, 55 | /// the filter cannot go into passthrough mode to handle a failure, because 56 | /// the data that was sent to the host is lost. 57 | /// 58 | public bool SentContentToFilter 59 | { 60 | get { return _filterState == FilterState.ContentSentToFilter; } 61 | } 62 | 63 | /// 64 | /// Returns true if the initial request to the host timed out without 65 | /// returning a response code. 66 | /// 67 | public bool ScriptInjectionTimedOut 68 | { 69 | get; 70 | private set; 71 | } 72 | 73 | /// 74 | /// Call this method to signal the host that all the content for filtering 75 | /// has been sent, and then wait for the host to return the filtered content. 76 | /// 77 | /// A task that completes when the host has returned the filtered contet. 78 | public async Task WaitForFilterComplete() 79 | { 80 | if (SentContentToFilter) 81 | { 82 | await _injectScriptSocket.CompleteRequest(); 83 | 84 | await _injectScriptSocket.WaitForResponseComplete(); 85 | } 86 | 87 | CloseInjectScriptSocketAndBecomePassthrough(); 88 | } 89 | 90 | protected override void Dispose(bool disposing) 91 | { 92 | CloseInjectScriptSocketAndBecomePassthrough(); 93 | } 94 | 95 | public override bool CanRead 96 | { 97 | get { return false; } 98 | } 99 | 100 | public override bool CanSeek 101 | { 102 | get { return false; } 103 | } 104 | 105 | public override bool CanWrite 106 | { 107 | get { return true; } 108 | } 109 | 110 | public override long Length 111 | { 112 | get { throw new NotSupportedException(); } 113 | } 114 | 115 | public override long Position 116 | { 117 | get { throw new NotSupportedException(); } 118 | set { throw new NotSupportedException(); } 119 | } 120 | 121 | public override void Flush() 122 | { 123 | Task.WaitAll(FlushAsync()); 124 | } 125 | 126 | /// 127 | /// In order to flush the content to the output stream, we need to wait 128 | /// for the content that was sent to the host to return. Once that 129 | /// is done, the connection to the host is closed, and the stream 130 | /// goes into passthrough mode. So effectively, any writes that come 131 | /// after a call to flush will not be filtered. 132 | /// 133 | public override async Task FlushAsync(CancellationToken cancellationToken) 134 | { 135 | await WaitForFilterComplete(); 136 | 137 | await _outputStream.FlushAsync(); 138 | } 139 | 140 | public override int Read(byte[] buffer, int offset, int count) 141 | { 142 | throw new NotSupportedException(); 143 | } 144 | 145 | public override long Seek(long offset, SeekOrigin origin) 146 | { 147 | throw new NotSupportedException(); 148 | } 149 | 150 | public override void SetLength(long value) 151 | { 152 | throw new NotSupportedException(); 153 | } 154 | 155 | public override void Write(byte[] buffer, int offset, int count) 156 | { 157 | Task.WaitAll(WriteAsync(buffer, offset, count)); 158 | } 159 | 160 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 161 | { 162 | bool firstWrite = (_filterState == FilterState.NothingSentToFilter); 163 | 164 | if (firstWrite) 165 | { 166 | DetermineIfFilterShouldBePassthrough(buffer, offset, count); 167 | 168 | AddResponseHeadersToFilterRequest(); 169 | } 170 | 171 | if (IsPassthrough) 172 | { 173 | return WriteToOutputStreamAsync(buffer, offset, count, ref cancellationToken); 174 | } 175 | else 176 | { 177 | _filterState = FilterState.ContentSentToFilter; 178 | 179 | return WriteToInjectScriptSocketAsync(firstWrite, buffer, offset, count, cancellationToken); 180 | } 181 | } 182 | 183 | private Task WriteToOutputStreamAsync(byte[] buffer, int offset, int count, ref CancellationToken cancellationToken) 184 | { 185 | return _outputStream.WriteAsync(buffer, offset, count, cancellationToken); 186 | } 187 | 188 | private Task WriteToInjectScriptSocketAsync(bool firstWrite, byte[] buffer, int offset, int count, CancellationToken cancellationToken) 189 | { 190 | if (firstWrite) 191 | { 192 | return FirstWriteToInjectScriptSocketAsync(buffer, offset, count, cancellationToken); 193 | } 194 | else 195 | { 196 | return _injectScriptSocket.WriteToRequestAsync(buffer, offset, count); 197 | } 198 | } 199 | 200 | private async Task FirstWriteToInjectScriptSocketAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 201 | { 202 | try 203 | { 204 | await _injectScriptSocket.WriteToRequestAsync(buffer, offset, count); 205 | 206 | Task statusCodeTask = _injectScriptSocket.GetResponseStatusCode(); 207 | 208 | int statusCode = await TaskHelpers.WaitWithTimeout( 209 | statusCodeTask, 210 | timeout: TimeSpan.FromMilliseconds(1000), 211 | resultIfTimedOut: 504 /*Gateway Timeout*/); 212 | 213 | if (statusCode == 200) 214 | { 215 | return; 216 | } 217 | 218 | if (statusCode == 504) 219 | { 220 | ScriptInjectionTimedOut = true; 221 | } 222 | } 223 | catch 224 | { 225 | // Fall through to error case 226 | } 227 | 228 | // If the initial send fails, we can switch to passthrough mode 229 | // and retry this write. Browser Link won't work, but at least 230 | // the page will be returned to the browser. 231 | CloseInjectScriptSocketAndBecomePassthrough(); 232 | 233 | await WriteAsync(buffer, offset, count, cancellationToken); 234 | } 235 | 236 | private static ResponseHandler CreateResponseHandler(Stream outputStream) 237 | { 238 | // The host will send a well-known preamble before sending response data. 239 | // The purpose is just to get the headers back before processing starts. 240 | // The preamble should be skipped. 241 | 242 | bool skippedFilterPreamble = false; 243 | 244 | return async delegate (byte[] buffer, int offset, int count) 245 | { 246 | if (!skippedFilterPreamble) 247 | { 248 | offset += BrowserLinkConstants.FilterPreamble.Length; 249 | count -= BrowserLinkConstants.FilterPreamble.Length; 250 | 251 | skippedFilterPreamble = true; 252 | } 253 | 254 | if (count > 0) 255 | { 256 | await outputStream.WriteAsync(buffer, offset, count); 257 | } 258 | }; 259 | } 260 | 261 | private void AddResponseHeadersToFilterRequest() 262 | { 263 | if (!IsPassthrough) 264 | { 265 | _injectScriptSocket.AddRequestHeader("Content-Type", _context.ResponseContentType); 266 | } 267 | } 268 | 269 | private void DetermineIfFilterShouldBePassthrough(byte[] buffer, int offset, int count) 270 | { 271 | string contentTypeFromHeader = _context.ResponseContentType; 272 | string path = _context.RequestPath; 273 | 274 | if (!ContentTypeUtil.IsSupportedContentTypes(contentTypeFromHeader)) 275 | { 276 | CloseInjectScriptSocketAndBecomePassthrough(); 277 | } 278 | 279 | else if (!ContentTypeUtil.IsHtml(path, buffer, offset, count)) 280 | { 281 | CloseInjectScriptSocketAndBecomePassthrough(); 282 | } 283 | } 284 | 285 | private void CloseInjectScriptSocketAndBecomePassthrough() 286 | { 287 | if (_injectScriptSocket != null) 288 | { 289 | _injectScriptSocket.Dispose(); 290 | _injectScriptSocket = null; 291 | } 292 | 293 | _filterState = FilterState.Passthrough; 294 | } 295 | } 296 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) .NET Foundation and Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/Microsoft.VisualStudio.Web.BrowserLink/RevolvingBuffers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.VisualStudio.Web.BrowserLink 9 | { 10 | /// 11 | /// A first-in-first-out data buffering system that reuses existing buffers 12 | /// to minimize the amount of memory required. 13 | /// 14 | /// The type of data being stored 15 | internal sealed class RevolvingBuffers : IDisposable 16 | { 17 | private int _defaultBufferSize; 18 | private object _lockObject = new object(); 19 | 20 | private LinkedList _buffers = new LinkedList(); 21 | private LinkedListNode _currentInputBuffer; 22 | private LinkedListNode _currentOutputBuffer; 23 | 24 | private TaskCompletionSource> _getBufferedData = null; 25 | private TaskCompletionSource _waitForBufferEmpty = null; 26 | 27 | /// 28 | /// Initialize the RevolvingBuffers 29 | /// 30 | /// 31 | /// The default size of an individual buffer. Larger buffers may be created 32 | /// to accomodate larger sets of input data. 33 | /// 34 | internal RevolvingBuffers(int defaultBufferSize) 35 | { 36 | _defaultBufferSize = defaultBufferSize; 37 | 38 | _currentInputBuffer = _buffers.AddFirst(new Buffer(defaultBufferSize)); 39 | } 40 | 41 | private Buffer InputBuffer 42 | { 43 | get { return _currentInputBuffer.Value; } 44 | } 45 | 46 | private Buffer OutputBuffer 47 | { 48 | get { return _currentOutputBuffer.Value; } 49 | } 50 | 51 | private bool HasAsyncReader 52 | { 53 | get { return _getBufferedData != null; } 54 | } 55 | 56 | #if DEBUG 57 | // FOR UNIT TESTING ONLY 58 | internal int BufferCount 59 | { 60 | get { return _buffers.Count; } 61 | } 62 | #endif 63 | 64 | /// 65 | /// Clear and free all the buffers. This also alerts asynchronous readers 66 | /// that no more data is coming. 67 | /// 68 | public void Dispose() 69 | { 70 | lock (_lockObject) 71 | { 72 | GiveBufferToAsyncReader(default(ArraySegment)); 73 | 74 | _buffers.Clear(); 75 | _currentOutputBuffer = null; 76 | _currentInputBuffer = null; 77 | } 78 | } 79 | 80 | /// 81 | /// Copy data into the buffers. 82 | /// 83 | /// Segment of an array to copy 84 | public void CopyDataToBuffer(ArraySegment arraySegment) 85 | { 86 | CopyDataToBuffer(arraySegment.Array, arraySegment.Offset, arraySegment.Count); 87 | } 88 | 89 | /// 90 | /// Copy data into the buffers. 91 | /// 92 | /// An array containing the data to copy 93 | /// The starting index of the data to copy 94 | /// The number of data elements to copy 95 | public void CopyDataToBuffer(DataType[] data, int offset, int count) 96 | { 97 | lock (_lockObject) 98 | { 99 | EnsureSpaceInInputBuffer(count); 100 | 101 | CopyDataToInputBuffer(data, offset, count); 102 | 103 | if (HasAsyncReader && AdvanceToNextOutputBuffer()) 104 | { 105 | GiveBufferToAsyncReader(OutputBuffer.CreateArraySegment()); 106 | } 107 | } 108 | } 109 | 110 | /// 111 | /// Synchronously reads data from the buffer. 112 | /// 113 | /// 114 | /// An ArraySegment containing buffered data, or an empty buffer if there is no 115 | /// data to read. 116 | /// 117 | /// 118 | /// The ArraySegment remains valid until GetBufferedData/Async is called again. 119 | /// After the next call to this method, the buffer in the ArraySegment can be 120 | /// reused for future data. 121 | /// 122 | public ArraySegment GetBufferedData() 123 | { 124 | lock (_lockObject) 125 | { 126 | if (AdvanceToNextOutputBuffer()) 127 | { 128 | return OutputBuffer.CreateArraySegment(); 129 | } 130 | else 131 | { 132 | SignalBufferEmpty(); 133 | 134 | return default(ArraySegment); 135 | } 136 | } 137 | } 138 | 139 | /// 140 | /// Asynchronously reads data from the buffer. 141 | /// 142 | /// 143 | /// A task that returns an ArraySegment containing buffered data. If there 144 | /// is no buffered data, the task does not complete until more data is 145 | /// buffered, or the buffers are disposed. 146 | /// 147 | /// 148 | /// The ArraySegment remains valid until GetBufferedData/Async is called again. 149 | /// After the next call to this method, the buffer in the ArraySegment can be 150 | /// reused for future data. 151 | /// 152 | public Task> GetBufferedDataAsync() 153 | { 154 | lock (_lockObject) 155 | { 156 | if (AdvanceToNextOutputBuffer()) 157 | { 158 | return Task.FromResult(OutputBuffer.CreateArraySegment()); 159 | } 160 | else 161 | { 162 | _getBufferedData = new TaskCompletionSource>(); 163 | 164 | // We need a local reference to the task here, because SignalBufferEmpty() 165 | // could end up calling Dispose(), and that will complete and then clear 166 | // _getBufferedData. We still want to return the completed task to the caller 167 | // of this method. 168 | Task> task = _getBufferedData.Task; 169 | 170 | SignalBufferEmpty(); 171 | 172 | return task; 173 | } 174 | } 175 | } 176 | 177 | /// 178 | /// Wait until all data has been read from the buffer. 179 | /// 180 | /// A task that completes when the last data has been read. 181 | public Task WaitForBufferEmptyAsync() 182 | { 183 | lock (_lockObject) 184 | { 185 | if (_buffers.First.Value.DataCount == 0) 186 | { 187 | return StaticTaskResult.True; 188 | } 189 | 190 | _waitForBufferEmpty = new TaskCompletionSource(); 191 | 192 | return _waitForBufferEmpty.Task; 193 | } 194 | } 195 | 196 | private void CopyDataToInputBuffer(DataType[] data, int offset, int count) 197 | { 198 | Array.Copy(data, offset, InputBuffer.Data, InputBuffer.DataCount, count); 199 | 200 | InputBuffer.DataCount += count; 201 | } 202 | 203 | private void EnsureSpaceInInputBuffer(int requiredCount) 204 | { 205 | if (InputBuffer.AvailableCount < requiredCount) 206 | { 207 | AdvanceToNextInputBuffer(requiredCount); 208 | } 209 | } 210 | 211 | private bool AdvanceToNextInputBuffer(int requiredCount = 0) 212 | { 213 | LinkedListNode nextEmptyBuffer = GetNextEmptyInputBuffer(_currentInputBuffer); 214 | 215 | if (nextEmptyBuffer == null) 216 | { 217 | nextEmptyBuffer = _buffers.AddLast(CreateNewBuffer(requiredCount)); 218 | } 219 | else if (nextEmptyBuffer.Value.AvailableCount < requiredCount) 220 | { 221 | nextEmptyBuffer = _buffers.AddBefore(nextEmptyBuffer, CreateNewBuffer(requiredCount)); 222 | } 223 | 224 | if (_currentInputBuffer != nextEmptyBuffer) 225 | { 226 | _currentInputBuffer = nextEmptyBuffer; 227 | 228 | // Buffer was advanced 229 | return true; 230 | } 231 | else 232 | { 233 | // Buffer did not change 234 | return false; 235 | } 236 | } 237 | 238 | private LinkedListNode GetNextEmptyInputBuffer(LinkedListNode currentBuffer) 239 | { 240 | if (!currentBuffer.Value.HasAnyData) 241 | { 242 | return currentBuffer; 243 | } 244 | else 245 | { 246 | return currentBuffer.Next; 247 | } 248 | } 249 | 250 | private Buffer CreateNewBuffer(int requiredCount) 251 | { 252 | int bufferSize = _defaultBufferSize; 253 | 254 | while (bufferSize < requiredCount) 255 | { 256 | bufferSize = bufferSize * 2; 257 | } 258 | 259 | return new Buffer(bufferSize); 260 | } 261 | 262 | private bool AdvanceToNextOutputBuffer() 263 | { 264 | RecycleCurrentOutputBuffer(); 265 | 266 | // Output buffer is always first in the list 267 | _currentOutputBuffer = _buffers.First; 268 | 269 | if (_currentOutputBuffer == _currentInputBuffer) 270 | { 271 | if (!AdvanceToNextInputBuffer()) 272 | { 273 | _currentOutputBuffer = null; 274 | } 275 | } 276 | 277 | return _currentOutputBuffer != null; 278 | } 279 | 280 | private void RecycleCurrentOutputBuffer() 281 | { 282 | if (_currentOutputBuffer != null) 283 | { 284 | _currentOutputBuffer.Value.DataCount = 0; 285 | 286 | // The buffer is moved to the end of the list, putting it in line 287 | // as a potential input buffer 288 | _buffers.AddLast(_currentOutputBuffer.Value); 289 | _buffers.Remove(_currentOutputBuffer); 290 | } 291 | } 292 | 293 | private void GiveBufferToAsyncReader(ArraySegment buffer) 294 | { 295 | if (_getBufferedData != null) 296 | { 297 | // _getBufferedData must be set to null before calling TrySetResult. The 298 | // handler will likely try to read more data immediately, which will set 299 | // a new _getBufferedData. If we clear _getBufferedData after TrySetResult, 300 | // that will erase the new _getBufferedData instead of the old one. 301 | TaskCompletionSource> tcs = _getBufferedData; 302 | _getBufferedData = null; 303 | 304 | tcs.TrySetResult(buffer); 305 | } 306 | } 307 | 308 | private void SignalBufferEmpty() 309 | { 310 | if (_waitForBufferEmpty != null) 311 | { 312 | // _waitForBufferEmpty must be set to null before calling TrySetResult. It 313 | // is unlikely that the handler would call WaitForBufferEmpty again, but if 314 | // they do, and we clear _waitForBufferEmpty after TrySetResult, that will 315 | // erase the new _waitForBufferEmpty instead of the old one. 316 | TaskCompletionSource tcs = _waitForBufferEmpty; 317 | _waitForBufferEmpty = null; 318 | 319 | tcs.TrySetResult(true); 320 | } 321 | } 322 | 323 | private class Buffer 324 | { 325 | internal Buffer(int size) 326 | { 327 | Data = new DataType[size]; 328 | } 329 | 330 | public DataType[] Data 331 | { 332 | get; 333 | private set; 334 | } 335 | 336 | public int DataCount 337 | { 338 | get; 339 | set; 340 | } 341 | 342 | public int AvailableCount 343 | { 344 | get { return Data.Length - DataCount; } 345 | } 346 | 347 | public bool HasAnyData 348 | { 349 | get { return DataCount > 0; } 350 | } 351 | 352 | public ArraySegment CreateArraySegment() 353 | { 354 | return new ArraySegment(Data, 0, DataCount); 355 | } 356 | } 357 | } 358 | } 359 | -------------------------------------------------------------------------------- /test/Microsoft.VisualStudio.Web.BrowserLink.Test/DelayConnectingHttpSocketAdapterTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace Microsoft.VisualStudio.Web.BrowserLink 7 | { 8 | public class DelayConnectingHttpSocketAdapterTest 9 | { 10 | private MockHttpSocketAdapter _createdAdapter = null; 11 | private string _responseContent = String.Empty; 12 | 13 | [Fact] 14 | public void DelayConnectingHttpSocketAdapter_CompleteRequest_DoesNotConnectIfNoDataAndNoResponse() 15 | { 16 | // Arrange 17 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(DoNotConnect); 18 | 19 | // Act 20 | Task result = delayAdapter.CompleteRequest(); 21 | 22 | // Assert 23 | TaskAssert.Completed(result, "CompleteRequest should complete immediately when there is no request."); 24 | } 25 | 26 | [Fact] 27 | public void DelayConnectingHttpSocketAdapter_CompleteRequest_CompletesIfThereIsAResponseHandler() 28 | { 29 | // Arrange 30 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 31 | 32 | delayAdapter.SetResponseHandler(HandleAsciiResponse); 33 | 34 | // Act 35 | Task result = delayAdapter.CompleteRequest(); 36 | 37 | // Assert 38 | Assert.True(_createdAdapter != null, "Adapter should have been created"); 39 | Assert.True(_createdAdapter.IsCompleted, "Adapter should be completed"); 40 | } 41 | 42 | [Fact] 43 | public void DelayConnectingHttpSocketAdapter_CompleteRequest_DoesNotRecreateConnection() 44 | { 45 | // Arrange 46 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 47 | 48 | byte[] bytesToWrite = Encoding.ASCII.GetBytes("Hello"); 49 | delayAdapter.WriteToRequestAsync(bytesToWrite, 0, bytesToWrite.Length); 50 | 51 | // Act 52 | delayAdapter.CompleteRequest(); 53 | 54 | // Assert 55 | Assert.True(_createdAdapter != null, "Adapter was not created."); 56 | Assert.True(_createdAdapter.IsCompleted, "Adapter was not Completed."); 57 | } 58 | 59 | [Fact] 60 | public void DelayConnectingHttpSocketAdapter_WriteToRequestAsync_ConnectsAndWritesData() 61 | { 62 | // Arrange 63 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 64 | 65 | byte[] bytesToWrite = Encoding.ASCII.GetBytes("Hello, world!"); 66 | 67 | // Act 68 | Task result = delayAdapter.WriteToRequestAsync(bytesToWrite, 7, 5); 69 | 70 | // Assert 71 | Assert.True(result != null, "No Task was returned."); 72 | Assert.True(_createdAdapter != null, "Adapter was not created."); 73 | Assert.Equal("world", _createdAdapter.RequestContent); 74 | } 75 | 76 | [Fact] 77 | public void DelayConnectingHttpSocketAdapter_WriteToRequestAsync_DoesNothingAfterFailureToConnect() 78 | { 79 | // Arrange 80 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(FailToConnect); 81 | 82 | byte[] bytesToWrite = Encoding.ASCII.GetBytes("Hello, world!"); 83 | 84 | // Act 85 | Task result = delayAdapter.WriteToRequestAsync(bytesToWrite, 7, 5); 86 | 87 | // Assert 88 | TaskAssert.Completed(result); 89 | } 90 | 91 | [Fact] 92 | public void DelayConnectingHttpSocketAdapter_WriteToRequestAsync_DoesNotAttemptToConnectAfterFailure() 93 | { 94 | // Arrange 95 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(FailToConnect); 96 | 97 | byte[] bytesToWrite = Encoding.ASCII.GetBytes("Hello, world!"); 98 | delayAdapter.WriteToRequestAsync(bytesToWrite, 7, 5); 99 | 100 | // Act 101 | Task result = delayAdapter.WriteToRequestAsync(bytesToWrite, 7, 5); 102 | 103 | // Assert 104 | // If we got here, a second connection was not attempted 105 | TaskAssert.Completed(result); 106 | } 107 | 108 | [Fact] 109 | public void DelayConnectingHttpSocketAdapter_WriteToRequestAsync_WritesDataToSameConnection() 110 | { 111 | // Arrange 112 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 113 | 114 | byte[] bytesToWrite = Encoding.ASCII.GetBytes("Hello, world!"); 115 | delayAdapter.WriteToRequestAsync(bytesToWrite, 0, 7); 116 | 117 | // Act 118 | delayAdapter.WriteToRequestAsync(bytesToWrite, 7, 5); 119 | 120 | // Assert 121 | Assert.True(_createdAdapter != null, "Adapter was not created."); 122 | Assert.Equal("Hello, world", _createdAdapter.RequestContent); 123 | } 124 | 125 | [Fact] 126 | public void DelayConnectingHttpSocketAdapter_GetResponseStatusCode_ConnectsAndRequestsStatusCode() 127 | { 128 | // Arrange 129 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 130 | 131 | // Act 132 | Task result = delayAdapter.GetResponseStatusCode(); 133 | 134 | // Assert 135 | Assert.True(_createdAdapter != null, "Adapter was not created."); 136 | TaskAssert.NotCompleted(result, "Status code should not be returned yet."); 137 | 138 | // Act 139 | _createdAdapter.SendResponseStatusCode(304); 140 | 141 | // Assert 142 | TaskAssert.ResultEquals(result, 304); 143 | } 144 | 145 | [Fact] 146 | public void DelayConnectingHttpSocketAdapter_GetResponseHeader_ConnectsAndGetsHeader() 147 | { 148 | // Arrange 149 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 150 | 151 | // Act 152 | Task result = delayAdapter.GetResponseHeader("Content-Length"); 153 | 154 | // Assert 155 | Assert.True(_createdAdapter != null, "Adapter was not created."); 156 | TaskAssert.NotCompleted(result, "Status code should not be returned yet."); 157 | 158 | // Act 159 | _createdAdapter.SendResponseHeader("Content-Length", "1234"); 160 | 161 | // Assert 162 | TaskAssert.ResultEquals(result, "1234"); 163 | } 164 | 165 | [Fact] 166 | public void DelayConnectingHttpSocketAdapter_WaitForResponseComplete_ConnectsAndWaits() 167 | { 168 | // Arrange 169 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 170 | 171 | // Act 172 | Task result = delayAdapter.WaitForResponseComplete(); 173 | 174 | // Assert 175 | Assert.True(_createdAdapter != null, "Adapter was not created."); 176 | TaskAssert.NotCompleted(result, "Status code should not be returned yet."); 177 | 178 | // Act 179 | _createdAdapter.SendResponseComplete(); 180 | 181 | // Assert 182 | TaskAssert.Completed(result); 183 | } 184 | 185 | [Fact] 186 | public void DelayConnectingHttpSocketAdapter_AddRequestHeader_DoesNotConnect() 187 | { 188 | // Arrange 189 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(DoNotConnect); 190 | 191 | // Act 192 | delayAdapter.AddRequestHeader("Hello", "World"); 193 | 194 | // Assert 195 | // If we got here, the connection was not created 196 | } 197 | 198 | [Fact] 199 | public void DelayConnectingHttpSocketAdapter_AddRequestHeader_AddsHeadersAfterConnection() 200 | { 201 | // Arrange 202 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 203 | 204 | delayAdapter.AddRequestHeader("Hello", "World"); 205 | delayAdapter.AddRequestHeader("Hello2", "Planet"); 206 | 207 | // Act 208 | delayAdapter.WaitForResponseComplete(); 209 | 210 | // Assert 211 | Assert.True(_createdAdapter.RequestHeaders.ContainsKey("Hello"), "Header 'Hello' should be added."); 212 | Assert.Equal("World", _createdAdapter.RequestHeaders["Hello"]); 213 | Assert.True(_createdAdapter.RequestHeaders.ContainsKey("Hello2"), "Header 'Hello2' should be added."); 214 | Assert.Equal("Planet", _createdAdapter.RequestHeaders["Hello2"]); 215 | } 216 | 217 | [Fact] 218 | public void DelayConnectingHttpSocketAdapter_AddRequestHeader_AddsHeadersToExistingConnection() 219 | { 220 | // Arrange 221 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 222 | 223 | delayAdapter.GetResponseStatusCode(); 224 | 225 | // Act 226 | delayAdapter.AddRequestHeader("Hello", "World"); 227 | 228 | // Assert 229 | Assert.True(_createdAdapter.RequestHeaders.ContainsKey("Hello"), "Header 'Hello' should be added."); 230 | Assert.Equal("World", _createdAdapter.RequestHeaders["Hello"]); 231 | } 232 | 233 | [Fact] 234 | public void DelayConnectingHttpSocketAdapter_SetResponseHandler_DoesNotConnect() 235 | { 236 | // Arrange 237 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(DoNotConnect); 238 | 239 | // Act 240 | delayAdapter.SetResponseHandler(HandleAsciiResponse); 241 | 242 | // Assert 243 | // If we got here, the connection was not created 244 | } 245 | 246 | [Fact] 247 | public void DelayConnectingHttpSocketAdapter_SetResponseHandler_SetsHandlerOnConnection() 248 | { 249 | // Arrange 250 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 251 | 252 | delayAdapter.SetResponseHandler(HandleAsciiResponse); 253 | 254 | // Act 255 | delayAdapter.WaitForResponseComplete(); 256 | 257 | // Assert 258 | Assert.True(_createdAdapter.HasResponseHandler, "Response handler was not set."); 259 | 260 | // Act 261 | _createdAdapter.SendResponseBodyContent("This is a test", Encoding.ASCII); 262 | 263 | // Assert 264 | Assert.Equal("This is a test", _responseContent); 265 | } 266 | 267 | [Fact] 268 | public void DelayConnectingHttpSocketAdapter_SetResponseHandler_SetsHandlerOnExistingConnection() 269 | { 270 | // Arrange 271 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 272 | 273 | delayAdapter.GetResponseStatusCode(); 274 | 275 | // Act 276 | delayAdapter.SetResponseHandler(HandleAsciiResponse); 277 | 278 | // Assert 279 | Assert.True(_createdAdapter.HasResponseHandler, "Response handler was not set."); 280 | 281 | // Act 282 | _createdAdapter.SendResponseBodyContent("This is a test", Encoding.ASCII); 283 | 284 | // Assert 285 | Assert.Equal("This is a test", _responseContent); 286 | } 287 | 288 | [Fact] 289 | public void DelayConnectingHttpSocketAdapter_Dispose_DisposesExistingConnection() 290 | { 291 | // Arrange 292 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 293 | 294 | delayAdapter.WaitForResponseComplete(); 295 | 296 | // Act 297 | delayAdapter.Dispose(); 298 | 299 | // Assert 300 | Assert.True(_createdAdapter.IsDisposed); 301 | } 302 | 303 | [Fact] 304 | public void DelayConnectingHttpSocketAdapter_Dispose_DoesNothingIfNotConnected() 305 | { 306 | // Arrange 307 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 308 | 309 | // Act 310 | delayAdapter.Dispose(); 311 | 312 | // Assert 313 | // Should do nothing 314 | } 315 | 316 | [Fact] 317 | public void DelayConnectingHttpSocketAdapter_Dispose_DoesNotDisposeASecondTime() 318 | { 319 | // Arrange 320 | IHttpSocketAdapter delayAdapter = new DelayConnectingHttpSocketAdapter(ConnectOnlyOnce); 321 | 322 | delayAdapter.CompleteRequest(); 323 | delayAdapter.Dispose(); 324 | 325 | // Act 326 | delayAdapter.Dispose(); 327 | 328 | // Assert 329 | // If we got here, the adapter was not Disposed a second time 330 | } 331 | 332 | private Task ConnectOnlyOnce() 333 | { 334 | Assert.True(_createdAdapter == null, "A second connection is being created."); 335 | 336 | return Task.FromResult(_createdAdapter = new MockHttpSocketAdapter()); 337 | } 338 | 339 | private static Task DoNotConnect() 340 | { 341 | Assert.True(false, "IHttpSocketAdapter should not be created."); 342 | 343 | return null; 344 | } 345 | 346 | private Task FailToConnect() 347 | { 348 | Assert.True(_createdAdapter == null, "Attempted to create an adapter after a previous attempt failed."); 349 | 350 | _createdAdapter = new MockHttpSocketAdapter(); 351 | 352 | return Task.FromResult((IHttpSocketAdapter)null); 353 | } 354 | 355 | private Task HandleAsciiResponse(byte[] buffer, int offset, int count) 356 | { 357 | _responseContent += Encoding.ASCII.GetString(buffer, offset, count); 358 | 359 | return StaticTaskResult.True; 360 | } 361 | } 362 | } 363 | --------------------------------------------------------------------------------