Refactoring, naming cleanup and tests for build server
- Multiple renames and cleanup - Added tests for ServerProtocol and RequestDispatcher - Added ServerLifecycleTest
This commit is contained in:
parent
42c3102cd4
commit
004ff204aa
11
Razor.sln
11
Razor.sln
|
|
@ -94,6 +94,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.Sdk", "src\Microsoft.AspNetCore.Razor.Sdk\Microsoft.AspNetCore.Razor.Sdk.csproj", "{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.Sdk", "src\Microsoft.AspNetCore.Razor.Sdk\Microsoft.AspNetCore.Razor.Sdk.csproj", "{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Razor.Tools.Test", "test\Microsoft.AspNetCore.Razor.Tools.Test\Microsoft.AspNetCore.Razor.Tools.Test.csproj", "{6EA56B2B-89EC-4C38-A384-97D203375B06}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -388,6 +390,14 @@ Global
|
||||||
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}.ReleaseNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}.ReleaseNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}.ReleaseNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9}.ReleaseNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
@ -429,6 +439,7 @@ Global
|
||||||
{933101DA-C4CC-401A-AA01-2784E1025B7F} = {92463391-81BE-462B-AC3C-78C6C760741F}
|
{933101DA-C4CC-401A-AA01-2784E1025B7F} = {92463391-81BE-462B-AC3C-78C6C760741F}
|
||||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
||||||
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
{7D9ECCEE-71D1-4A42-ABEE-876AFA1B4FC9} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
||||||
|
{6EA56B2B-89EC-4C38-A384-97D203375B06} = {92463391-81BE-462B-AC3C-78C6C760741F}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26}
|
SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Microsoft.AspNetCore.Razor.Tools;
|
||||||
using Microsoft.Build.Framework;
|
using Microsoft.Build.Framework;
|
||||||
using Microsoft.Build.Utilities;
|
using Microsoft.Build.Utilities;
|
||||||
using Microsoft.CodeAnalysis.CommandLine;
|
using Microsoft.CodeAnalysis.CommandLine;
|
||||||
|
|
@ -106,37 +107,35 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
||||||
_razorServerCts?.Cancel();
|
_razorServerCts?.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual bool TryExecuteOnServer(string pathToTool, string responseFileCommands, string commandLineCommands, out int result)
|
protected virtual bool TryExecuteOnServer(
|
||||||
|
string pathToTool,
|
||||||
|
string responseFileCommands,
|
||||||
|
string commandLineCommands,
|
||||||
|
out int result)
|
||||||
{
|
{
|
||||||
CompilerServerLogger.Log("Server execution started.");
|
CompilerServerLogger.Log("Server execution started.");
|
||||||
using (_razorServerCts = new CancellationTokenSource())
|
using (_razorServerCts = new CancellationTokenSource())
|
||||||
{
|
{
|
||||||
CompilerServerLogger.Log($"CommandLine = '{commandLineCommands}'");
|
CompilerServerLogger.Log($"CommandLine = '{commandLineCommands}'");
|
||||||
CompilerServerLogger.Log($"BuildResponseFile = '{responseFileCommands}'");
|
CompilerServerLogger.Log($"ServerResponseFile = '{responseFileCommands}'");
|
||||||
|
|
||||||
// The server contains the tools for discovering tag helpers and generating Razor code.
|
// The server contains the tools for discovering tag helpers and generating Razor code.
|
||||||
var clientDir = Path.GetDirectoryName(ToolAssembly);
|
var clientDir = Path.GetDirectoryName(ToolAssembly);
|
||||||
var workingDir = CurrentDirectoryToUse();
|
var workingDir = CurrentDirectoryToUse();
|
||||||
var tempDir = BuildServerConnection.GetTempPath(workingDir);
|
var tempDir = ServerConnection.GetTempPath(workingDir);
|
||||||
|
var serverPaths = new ServerPaths(
|
||||||
var buildPaths = new BuildPathsAlt(
|
|
||||||
clientDir,
|
clientDir,
|
||||||
// MSBuild doesn't need the .NET SDK directory
|
|
||||||
sdkDir: null,
|
|
||||||
workingDir: workingDir,
|
workingDir: workingDir,
|
||||||
tempDir: tempDir);
|
tempDir: tempDir);
|
||||||
|
|
||||||
var responseTask = BuildServerConnection.RunServerCompilation(
|
var arguments = GetArguments(responseFileCommands);
|
||||||
GetArguments(responseFileCommands),
|
|
||||||
buildPaths,
|
|
||||||
keepAlive: null,
|
|
||||||
cancellationToken: _razorServerCts.Token);
|
|
||||||
|
|
||||||
|
var responseTask = ServerConnection.RunOnServer(arguments, serverPaths, _razorServerCts.Token);
|
||||||
responseTask.Wait(_razorServerCts.Token);
|
responseTask.Wait(_razorServerCts.Token);
|
||||||
|
|
||||||
var response = responseTask.Result;
|
var response = responseTask.Result;
|
||||||
if (response.Type == BuildResponse.ResponseType.Completed &&
|
if (response.Type == ServerResponse.ResponseType.Completed &&
|
||||||
response is CompletedBuildResponse completedResponse)
|
response is CompletedServerResponse completedResponse)
|
||||||
{
|
{
|
||||||
result = completedResponse.ReturnCode;
|
result = completedResponse.ReturnCode;
|
||||||
|
|
||||||
|
|
@ -159,7 +158,7 @@ namespace Microsoft.AspNetCore.Razor.Tasks
|
||||||
{
|
{
|
||||||
// ToolTask has a method for this. But it may return null. Use the process directory
|
// ToolTask has a method for this. But it may return null. Use the process directory
|
||||||
// if ToolTask didn't override. MSBuild uses the process directory.
|
// if ToolTask didn't override. MSBuild uses the process directory.
|
||||||
string workingDirectory = GetWorkingDirectory();
|
var workingDirectory = GetWorkingDirectory();
|
||||||
if (string.IsNullOrEmpty(workingDirectory))
|
if (string.IsNullOrEmpty(workingDirectory))
|
||||||
{
|
{
|
||||||
workingDirectory = Directory.GetCurrentDirectory();
|
workingDirectory = Directory.GetCurrentDirectory();
|
||||||
|
|
|
||||||
|
|
@ -16,29 +16,26 @@
|
||||||
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCorePackageVersion)" />
|
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="$(MicrosoftBuildUtilitiesCorePackageVersion)" />
|
||||||
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
|
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
|
||||||
|
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\BuildServerConnection.cs">
|
|
||||||
<Link>Shared\BuildServerConnection.cs</Link>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\NativeMethods.cs">
|
|
||||||
<Link>Shared\NativeMethods.cs</Link>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\CompilerServerLogger.cs">
|
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\CompilerServerLogger.cs">
|
||||||
<Link>Shared\CompilerServerLogger.cs</Link>
|
<Link>Shared\CompilerServerLogger.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\PlatformInformation.cs">
|
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\PlatformInformation.cs">
|
||||||
<Link>Shared\PlatformInformation.cs</Link>
|
<Link>Shared\PlatformInformation.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\BuildProtocol.cs">
|
|
||||||
<Link>Shared\BuildProtocol.cs</Link>
|
|
||||||
</Compile>
|
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\CommandLineUtilities.cs">
|
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Roslyn\CommandLineUtilities.cs">
|
||||||
<Link>Shared\CommandLineUtilities.cs</Link>
|
<Link>Shared\CommandLineUtilities.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\ServerProtocol\*.cs">
|
||||||
|
<Link>Shared\ServerProtocol\%(FileName)</Link>
|
||||||
|
</Compile>
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\PipeName.cs">
|
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\PipeName.cs">
|
||||||
<Link>Shared\PipeName.cs</Link>
|
<Link>Shared\PipeName.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\MutexName.cs">
|
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\MutexName.cs">
|
||||||
<Link>Shared\MutexName.cs</Link>
|
<Link>Shared\MutexName.cs</Link>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="..\Microsoft.AspNetCore.Razor.Tools\Client.cs">
|
||||||
|
<Link>Shared\Client.cs</Link>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,23 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
internal abstract class Client : IDisposable
|
internal abstract class Client : IDisposable
|
||||||
{
|
{
|
||||||
|
private static int counter;
|
||||||
|
|
||||||
|
public abstract Stream Stream { get; }
|
||||||
|
|
||||||
|
public abstract string Identifier { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Task WaitForDisconnectAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
// Based on: https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Shared/BuildServerConnection.cs#L290
|
// Based on: https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Shared/BuildServerConnection.cs#L290
|
||||||
public static async Task<Client> ConnectAsync(string pipeName, TimeSpan? timeout, CancellationToken cancellationToken)
|
public static async Task<Client> ConnectAsync(string pipeName, TimeSpan? timeout, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
@ -49,7 +66,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
// We plan to rely on the BCL for this but it's not yet implemented:
|
// We plan to rely on the BCL for this but it's not yet implemented:
|
||||||
// See https://github.com/dotnet/corefx/issues/25427
|
// See https://github.com/dotnet/corefx/issues/25427
|
||||||
|
|
||||||
return new NamedPipeClient(stream);
|
return new NamedPipeClient(stream, GetNextIdentifier());
|
||||||
}
|
}
|
||||||
catch (Exception e) when (!(e is TaskCanceledException || e is OperationCanceledException))
|
catch (Exception e) when (!(e is TaskCanceledException || e is OperationCanceledException))
|
||||||
{
|
{
|
||||||
|
|
@ -58,25 +75,55 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Stream Stream { get; }
|
private static string GetNextIdentifier()
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
Dispose(disposing: true);
|
var id = Interlocked.Increment(ref counter);
|
||||||
|
return "clientconnection-" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
private class NamedPipeClient : Client
|
private class NamedPipeClient : Client
|
||||||
{
|
{
|
||||||
public NamedPipeClient(NamedPipeClientStream stream)
|
public NamedPipeClient(NamedPipeClientStream stream, string identifier)
|
||||||
{
|
{
|
||||||
Stream = stream;
|
Stream = stream;
|
||||||
|
Identifier = identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Stream Stream { get; }
|
public override Stream Stream { get; }
|
||||||
|
|
||||||
|
public override string Identifier { get; }
|
||||||
|
|
||||||
|
public async override Task WaitForDisconnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!(Stream is PipeStream pipeStream))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have to poll for disconnection by reading, PipeStream.IsConnected isn't reliable unless you
|
||||||
|
// actually do a read - which will cause it to update its state.
|
||||||
|
while (!cancellationToken.IsCancellationRequested && pipeStream.IsConnected)
|
||||||
|
{
|
||||||
|
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CompilerServerLogger.Log($"Before poking pipe {Identifier}.");
|
||||||
|
await Stream.ReadAsync(Array.Empty<byte>(), 0, 0, cancellationToken);
|
||||||
|
CompilerServerLogger.Log($"After poking pipe {Identifier}.");
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// It is okay for this call to fail. Errors will be reflected in the
|
||||||
|
// IsConnected property which will be read on the next iteration.
|
||||||
|
CompilerServerLogger.LogException(e, $"Error poking pipe {Identifier}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.CodeAnalysis.CommandLine;
|
using Microsoft.CodeAnalysis.CommandLine;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Tools
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
|
|
@ -16,15 +15,15 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
return new DefaultCompilerHost();
|
return new DefaultCompilerHost();
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract BuildResponse Execute(BuildRequest request, CancellationToken cancellationToken);
|
public abstract ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken);
|
||||||
|
|
||||||
private class DefaultCompilerHost : CompilerHost
|
private class DefaultCompilerHost : CompilerHost
|
||||||
{
|
{
|
||||||
public override BuildResponse Execute(BuildRequest request, CancellationToken cancellationToken)
|
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (!TryParseArguments(request, out var parsed))
|
if (!TryParseArguments(request, out var parsed))
|
||||||
{
|
{
|
||||||
return new RejectedBuildResponse();
|
return new RejectedServerResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
var app = new Application(cancellationToken);
|
var app = new Application(cancellationToken);
|
||||||
|
|
@ -33,10 +32,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
var exitCode = app.Execute(commandArgs);
|
var exitCode = app.Execute(commandArgs);
|
||||||
var output = app.Out.ToString() ?? string.Empty;
|
var output = app.Out.ToString() ?? string.Empty;
|
||||||
|
|
||||||
return new CompletedBuildResponse(exitCode, utf8output: false, output: output);
|
return new CompletedServerResponse(exitCode, utf8output: false, output: output);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryParseArguments(BuildRequest request, out (string workingDirectory, string tempDirectory, string[] args) parsed)
|
private bool TryParseArguments(ServerRequest request, out (string workingDirectory, string tempDirectory, string[] args) parsed)
|
||||||
{
|
{
|
||||||
string workingDirectory = null;
|
string workingDirectory = null;
|
||||||
string tempDirectory = null;
|
string tempDirectory = null;
|
||||||
|
|
@ -46,15 +45,15 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
for (var i = 0; i < request.Arguments.Count; i++)
|
for (var i = 0; i < request.Arguments.Count; i++)
|
||||||
{
|
{
|
||||||
var argument = request.Arguments[i];
|
var argument = request.Arguments[i];
|
||||||
if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.CurrentDirectory)
|
if (argument.Id == RequestArgument.ArgumentId.CurrentDirectory)
|
||||||
{
|
{
|
||||||
workingDirectory = argument.Value;
|
workingDirectory = argument.Value;
|
||||||
}
|
}
|
||||||
else if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.TempDirectory)
|
else if (argument.Id == RequestArgument.ArgumentId.TempDirectory)
|
||||||
{
|
{
|
||||||
tempDirectory = argument.Value;
|
tempDirectory = argument.Value;
|
||||||
}
|
}
|
||||||
else if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.CommandLineArgument)
|
else if (argument.Id == RequestArgument.ArgumentId.CommandLineArgument)
|
||||||
{
|
{
|
||||||
args.Add(argument.Value);
|
args.Add(argument.Value);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.IO.Pipes;
|
using System.IO.Pipes;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -13,23 +14,23 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
// https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Server/VBCSCompiler/NamedPipeClientConnection.cs#L17
|
// https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Server/VBCSCompiler/NamedPipeClientConnection.cs#L17
|
||||||
internal abstract class ConnectionHost
|
internal abstract class ConnectionHost
|
||||||
{
|
{
|
||||||
private static int counter;
|
|
||||||
|
|
||||||
private static string GetNextIdentifier()
|
|
||||||
{
|
|
||||||
var id = Interlocked.Increment(ref counter);
|
|
||||||
return "connection-" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size of the buffers to use: 64K
|
// Size of the buffers to use: 64K
|
||||||
private const int PipeBufferSize = 0x10000;
|
private const int PipeBufferSize = 0x10000;
|
||||||
|
|
||||||
|
private static int counter;
|
||||||
|
|
||||||
|
public abstract Task<Connection> WaitForConnectionAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
public static ConnectionHost Create(string pipeName)
|
public static ConnectionHost Create(string pipeName)
|
||||||
{
|
{
|
||||||
return new NamedPipeConnectionHost(pipeName);
|
return new NamedPipeConnectionHost(pipeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract Task<Connection> WaitForConnectionAsync(CancellationToken cancellationToken);
|
private static string GetNextIdentifier()
|
||||||
|
{
|
||||||
|
var id = Interlocked.Increment(ref counter);
|
||||||
|
return "connection-" + id;
|
||||||
|
}
|
||||||
|
|
||||||
private class NamedPipeConnectionHost : ConnectionHost
|
private class NamedPipeConnectionHost : ConnectionHost
|
||||||
{
|
{
|
||||||
|
|
@ -76,17 +77,20 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
public NamedPipeConnection(NamedPipeServerStream stream, string identifier)
|
public NamedPipeConnection(NamedPipeServerStream stream, string identifier)
|
||||||
{
|
{
|
||||||
base.Stream = stream;
|
Stream = stream;
|
||||||
Identifier = identifier;
|
Identifier = identifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public new NamedPipeServerStream Stream => (NamedPipeServerStream)base.Stream;
|
|
||||||
|
|
||||||
public async override Task WaitForDisconnectAsync(CancellationToken cancellationToken)
|
public async override Task WaitForDisconnectAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
if (!(Stream is PipeStream pipeStream))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We have to poll for disconnection by reading, PipeStream.IsConnected isn't reliable unless you
|
// We have to poll for disconnection by reading, PipeStream.IsConnected isn't reliable unless you
|
||||||
// actually do a read - which will cause it to update its state.
|
// actually do a read - which will cause it to update its state.
|
||||||
while (!cancellationToken.IsCancellationRequested && Stream.IsConnected)
|
while (!cancellationToken.IsCancellationRequested && pipeStream.IsConnected)
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
|
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
|
||||||
|
|
||||||
|
|
@ -102,7 +106,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
// It is okay for this call to fail. Errors will be reflected in the
|
// It is okay for this call to fail. Errors will be reflected in the
|
||||||
// IsConnected property which will be read on the next iteration of the
|
// IsConnected property which will be read on the next iteration.
|
||||||
CompilerServerLogger.LogException(e, $"Error poking pipe {Identifier}.");
|
CompilerServerLogger.LogException(e, $"Error poking pipe {Identifier}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,471 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis.CommandLine;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
// Heavily influenced by:
|
||||||
|
// https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Server/ServerShared/ServerDispatcher.cs#L15
|
||||||
|
internal class DefaultRequestDispatcher : RequestDispatcher
|
||||||
|
{
|
||||||
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
private readonly CompilerHost _compilerHost;
|
||||||
|
private readonly ConnectionHost _connectionHost;
|
||||||
|
private readonly EventBus _eventBus;
|
||||||
|
|
||||||
|
private KeepAlive _keepAlive;
|
||||||
|
private State _state;
|
||||||
|
private Task _timeoutTask;
|
||||||
|
private Task _gcTask;
|
||||||
|
private Task<Connection> _listenTask;
|
||||||
|
private CancellationTokenSource _listenCancellationTokenSource;
|
||||||
|
private List<Task<ConnectionResult>> _connections = new List<Task<ConnectionResult>>();
|
||||||
|
|
||||||
|
public DefaultRequestDispatcher(
|
||||||
|
ConnectionHost connectionHost,
|
||||||
|
CompilerHost compilerHost,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
EventBus eventBus = null,
|
||||||
|
TimeSpan? keepAlive = null)
|
||||||
|
{
|
||||||
|
_connectionHost = connectionHost;
|
||||||
|
_compilerHost = compilerHost;
|
||||||
|
_cancellationToken = cancellationToken;
|
||||||
|
|
||||||
|
_eventBus = eventBus ?? EventBus.Default;
|
||||||
|
|
||||||
|
var keepAliveTimeout = DefaultServerKeepAlive;
|
||||||
|
if (keepAlive.HasValue)
|
||||||
|
{
|
||||||
|
keepAliveTimeout = keepAlive.Value;
|
||||||
|
}
|
||||||
|
_keepAlive = new KeepAlive(keepAliveTimeout, isDefault: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The server accepts connections until we reach a state that requires a shutdown. At that
|
||||||
|
// time no new connections will be accepted and the server will drain existing connections.
|
||||||
|
//
|
||||||
|
// The idea is that it's better to let clients fallback to in-proc (and slow down) than it is to keep
|
||||||
|
// running in an undesired state.
|
||||||
|
public override void Run()
|
||||||
|
{
|
||||||
|
_state = State.Running;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Listen();
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Debug.Assert(_listenTask != null);
|
||||||
|
|
||||||
|
MaybeCreateTimeoutTask();
|
||||||
|
MaybeCreateGCTask();
|
||||||
|
WaitForAnyCompletion(_cancellationToken);
|
||||||
|
CheckCompletedTasks(_cancellationToken);
|
||||||
|
}
|
||||||
|
while (_connections.Count > 0 || _state == State.Running);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_state = State.Completed;
|
||||||
|
_gcTask = null;
|
||||||
|
_timeoutTask = null;
|
||||||
|
|
||||||
|
if (_listenTask != null)
|
||||||
|
{
|
||||||
|
CloseListenTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void CheckCompletedTasks(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
HandleCancellation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_listenTask.IsCompleted)
|
||||||
|
{
|
||||||
|
HandleCompletedListenTask(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_timeoutTask?.IsCompleted == true)
|
||||||
|
{
|
||||||
|
HandleCompletedTimeoutTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_gcTask?.IsCompleted == true)
|
||||||
|
{
|
||||||
|
HandleCompletedGCTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleCompletedConnections();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCancellation()
|
||||||
|
{
|
||||||
|
Debug.Assert(_listenTask != null);
|
||||||
|
|
||||||
|
// If cancellation has been requested then the server needs to be in the process
|
||||||
|
// of shutting down.
|
||||||
|
_state = State.ShuttingDown;
|
||||||
|
|
||||||
|
CloseListenTask();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Task.WaitAll(_connections.ToArray());
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// It's expected that some will throw exceptions, in particular OperationCanceledException. It's
|
||||||
|
// okay for them to throw so long as they complete.
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleCompletedConnections();
|
||||||
|
Debug.Assert(_connections.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The server farms out work to Task values and this method needs to wait until at least one of them
|
||||||
|
/// has completed.
|
||||||
|
/// </summary>
|
||||||
|
private void WaitForAnyCompletion(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var all = new List<Task>();
|
||||||
|
all.AddRange(_connections);
|
||||||
|
all.Add(_timeoutTask);
|
||||||
|
all.Add(_listenTask);
|
||||||
|
all.Add(_gcTask);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var waitArray = all.Where(x => x != null).ToArray();
|
||||||
|
Task.WaitAny(waitArray, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Thrown when the provided cancellationToken is cancelled. This is handled in the caller,
|
||||||
|
// here it just serves to break out of the WaitAny call.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Listen()
|
||||||
|
{
|
||||||
|
Debug.Assert(_listenTask == null);
|
||||||
|
Debug.Assert(_timeoutTask == null);
|
||||||
|
|
||||||
|
_listenCancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_listenTask = _connectionHost.WaitForConnectionAsync(_listenCancellationTokenSource.Token);
|
||||||
|
_eventBus.ConnectionListening();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseListenTask()
|
||||||
|
{
|
||||||
|
Debug.Assert(_listenTask != null);
|
||||||
|
|
||||||
|
_listenCancellationTokenSource.Cancel();
|
||||||
|
_listenCancellationTokenSource = null;
|
||||||
|
_listenTask = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCompletedListenTask(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_eventBus.ConnectionReceived();
|
||||||
|
|
||||||
|
// Don't accept any new connections once we're in shutdown mode, instead gracefully reject the request.
|
||||||
|
// This should cause the client to run in process.
|
||||||
|
var accept = _state == State.Running;
|
||||||
|
var connectionTask = AcceptConnection(_listenTask, accept, cancellationToken);
|
||||||
|
_connections.Add(connectionTask);
|
||||||
|
|
||||||
|
// Timeout and GC are only done when there are no active connections. Now that we have a new
|
||||||
|
// connection cancel out these tasks.
|
||||||
|
_timeoutTask = null;
|
||||||
|
_gcTask = null;
|
||||||
|
|
||||||
|
// Begin listening again for new connections.
|
||||||
|
_listenTask = null;
|
||||||
|
Listen();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCompletedTimeoutTask()
|
||||||
|
{
|
||||||
|
_eventBus.KeepAliveReached();
|
||||||
|
_listenCancellationTokenSource.Cancel();
|
||||||
|
_timeoutTask = null;
|
||||||
|
_state = State.ShuttingDown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCompletedGCTask()
|
||||||
|
{
|
||||||
|
_gcTask = null;
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
}
|
||||||
|
|
||||||
|
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||||
|
GC.Collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MaybeCreateTimeoutTask()
|
||||||
|
{
|
||||||
|
// If there are no active clients running then the server needs to be in a timeout mode.
|
||||||
|
if (_connections.Count == 0 && _timeoutTask == null)
|
||||||
|
{
|
||||||
|
Debug.Assert(_listenTask != null);
|
||||||
|
_timeoutTask = Task.Delay(_keepAlive.TimeSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MaybeCreateGCTask()
|
||||||
|
{
|
||||||
|
if (_connections.Count == 0 && _gcTask == null)
|
||||||
|
{
|
||||||
|
_gcTask = Task.Delay(GCTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks the completed connection objects.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>False if the server needs to begin shutting down</returns>
|
||||||
|
private void HandleCompletedConnections()
|
||||||
|
{
|
||||||
|
var shutdown = false;
|
||||||
|
var processedCount = 0;
|
||||||
|
var i = 0;
|
||||||
|
while (i < _connections.Count)
|
||||||
|
{
|
||||||
|
var current = _connections[i];
|
||||||
|
if (!current.IsCompleted)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_connections.RemoveAt(i);
|
||||||
|
processedCount++;
|
||||||
|
|
||||||
|
var result = current.Result;
|
||||||
|
if (result.KeepAlive.HasValue)
|
||||||
|
{
|
||||||
|
var updated = _keepAlive.Update(result.KeepAlive.Value);
|
||||||
|
if (updated.Equals(_keepAlive))
|
||||||
|
{
|
||||||
|
_eventBus.UpdateKeepAlive(updated.TimeSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (result.CloseReason)
|
||||||
|
{
|
||||||
|
case ConnectionResult.Reason.CompilationCompleted:
|
||||||
|
case ConnectionResult.Reason.CompilationNotStarted:
|
||||||
|
// These are all normal end states. Nothing to do here.
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConnectionResult.Reason.ClientDisconnect:
|
||||||
|
// Have to assume the worst here which is user pressing Ctrl+C at the command line and
|
||||||
|
// hence wanting all compilation to end.
|
||||||
|
_eventBus.ConnectionRudelyEnded();
|
||||||
|
shutdown = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ConnectionResult.Reason.ClientException:
|
||||||
|
case ConnectionResult.Reason.ClientShutdownRequest:
|
||||||
|
_eventBus.ConnectionRudelyEnded();
|
||||||
|
shutdown = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException($"Unexpected enum value {result.CloseReason}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processedCount > 0)
|
||||||
|
{
|
||||||
|
_eventBus.ConnectionCompleted(processedCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shutdown)
|
||||||
|
{
|
||||||
|
_state = State.ShuttingDown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<ConnectionResult> AcceptConnection(Task<Connection> task, bool accept, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Connection connection;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
connection = await task;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Unable to establish a connection with the client. The client is responsible for
|
||||||
|
// handling this case. Nothing else for us to do here.
|
||||||
|
CompilerServerLogger.LogException(ex, "Error creating client named pipe");
|
||||||
|
return new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (connection)
|
||||||
|
{
|
||||||
|
ServerRequest request;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CompilerServerLogger.Log("Begin reading request.");
|
||||||
|
request = await ServerRequest.ReadAsync(connection.Stream, cancellationToken).ConfigureAwait(false);
|
||||||
|
CompilerServerLogger.Log("End reading request.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
CompilerServerLogger.LogException(e, "Error reading build request.");
|
||||||
|
return new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.IsShutdownRequest())
|
||||||
|
{
|
||||||
|
// Reply with the PID of this process so that the client can wait for it to exit.
|
||||||
|
var response = new ShutdownServerResponse(Process.GetCurrentProcess().Id);
|
||||||
|
await response.WriteAsync(connection.Stream, cancellationToken);
|
||||||
|
|
||||||
|
// We can safely disconnect the client, then when this connection gets cleaned up by the event loop
|
||||||
|
// the server will go to a shutdown state.
|
||||||
|
return new ConnectionResult(ConnectionResult.Reason.ClientShutdownRequest);
|
||||||
|
}
|
||||||
|
else if (!accept)
|
||||||
|
{
|
||||||
|
// We're already in shutdown mode, respond gracefully so the client can run in-process.
|
||||||
|
var response = new RejectedServerResponse();
|
||||||
|
await response.WriteAsync(connection.Stream, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
return new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If we get here then this is a real request that we will accept and process.
|
||||||
|
//
|
||||||
|
// Kick off both the compilation and a task to monitor the pipe for closing.
|
||||||
|
var buildCancelled = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
|
||||||
|
var watcher = connection.WaitForDisconnectAsync(buildCancelled.Token);
|
||||||
|
var worker = ExecuteRequestAsync(request, buildCancelled.Token);
|
||||||
|
|
||||||
|
// await will end when either the work is complete or the connection is closed.
|
||||||
|
await Task.WhenAny(worker, watcher);
|
||||||
|
|
||||||
|
// Do an 'await' on the completed task, preference being compilation, to force
|
||||||
|
// any exceptions to be realized in this method for logging.
|
||||||
|
ConnectionResult.Reason reason;
|
||||||
|
if (worker.IsCompleted)
|
||||||
|
{
|
||||||
|
var response = await worker;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CompilerServerLogger.Log("Begin writing response.");
|
||||||
|
await response.WriteAsync(connection.Stream, cancellationToken);
|
||||||
|
CompilerServerLogger.Log("End writing response.");
|
||||||
|
|
||||||
|
reason = ConnectionResult.Reason.CompilationCompleted;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
reason = ConnectionResult.Reason.ClientDisconnect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await watcher;
|
||||||
|
reason = ConnectionResult.Reason.ClientDisconnect;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin the tear down of the Task which didn't complete.
|
||||||
|
buildCancelled.Cancel();
|
||||||
|
|
||||||
|
return new ConnectionResult(reason, request.KeepAlive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
CompilerServerLogger.LogException(ex, "Error handling connection");
|
||||||
|
return new ConnectionResult(ConnectionResult.Reason.ClientException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task<ServerResponse> ExecuteRequestAsync(ServerRequest buildRequest, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
Func<ServerResponse> func = () =>
|
||||||
|
{
|
||||||
|
CompilerServerLogger.Log("Begin processing request");
|
||||||
|
|
||||||
|
var response = _compilerHost.Execute(buildRequest, cancellationToken);
|
||||||
|
|
||||||
|
CompilerServerLogger.Log("End processing request");
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
var task = new Task<ServerResponse>(func, cancellationToken, TaskCreationOptions.LongRunning);
|
||||||
|
task.Start();
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum State
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Server running and accepting all requests
|
||||||
|
/// </summary>
|
||||||
|
Running,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server processing existing requests, responding to shutdown commands but is not accepting
|
||||||
|
/// new build requests.
|
||||||
|
/// </summary>
|
||||||
|
ShuttingDown,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Server is done.
|
||||||
|
/// </summary>
|
||||||
|
Completed,
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct KeepAlive
|
||||||
|
{
|
||||||
|
public TimeSpan TimeSpan;
|
||||||
|
public bool IsDefault;
|
||||||
|
|
||||||
|
public KeepAlive(TimeSpan timeSpan, bool isDefault)
|
||||||
|
{
|
||||||
|
TimeSpan = timeSpan;
|
||||||
|
IsDefault = isDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeepAlive Update(TimeSpan timeSpan)
|
||||||
|
{
|
||||||
|
if (IsDefault || timeSpan > TimeSpan)
|
||||||
|
{
|
||||||
|
return new KeepAlive(timeSpan, isDefault: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
var baseName = ComputeBaseName("Razor:" + AppDomain.CurrentDomain.BaseDirectory);
|
var baseName = ComputeBaseName("Razor:" + AppDomain.CurrentDomain.BaseDirectory);
|
||||||
|
|
||||||
// Prefix with username and elevation
|
// Prefix with username and elevation
|
||||||
bool isAdmin = false;
|
var isAdmin = false;
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
{
|
{
|
||||||
#if WINDOWS_HACK_LOL
|
#if WINDOWS_HACK_LOL
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Razor.Tools.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
|
||||||
|
|
||||||
|
|
@ -2,26 +2,12 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Runtime;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.CodeAnalysis.CommandLine;
|
|
||||||
using Microsoft.CodeAnalysis.CompilerServer;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Tools
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
// Heavily influenced by:
|
|
||||||
// https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Server/ServerShared/ServerDispatcher.cs#L15
|
|
||||||
internal abstract class RequestDispatcher
|
internal abstract class RequestDispatcher
|
||||||
{
|
{
|
||||||
public static RequestDispatcher Create(ConnectionHost connectionHost, CompilerHost compilerHost, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return new DefaultRequestDispatcher(connectionHost, compilerHost, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default time the server will stay alive after the last request disconnects.
|
/// Default time the server will stay alive after the last request disconnects.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -35,448 +21,9 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
||||||
public abstract void Run();
|
public abstract void Run();
|
||||||
|
|
||||||
private enum State
|
public static RequestDispatcher Create(ConnectionHost connectionHost, CompilerHost compilerHost, CancellationToken cancellationToken, EventBus eventBus, TimeSpan? keepAlive = null)
|
||||||
{
|
{
|
||||||
/// <summary>
|
return new DefaultRequestDispatcher(connectionHost, compilerHost, cancellationToken, eventBus, keepAlive);
|
||||||
/// Server running and accepting all requests
|
|
||||||
/// </summary>
|
|
||||||
Running,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Server processing existing requests, responding to shutdown commands but is not accepting
|
|
||||||
/// new build requests.
|
|
||||||
/// </summary>
|
|
||||||
ShuttingDown,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Server is done.
|
|
||||||
/// </summary>
|
|
||||||
Completed,
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DefaultRequestDispatcher : RequestDispatcher
|
|
||||||
{
|
|
||||||
private readonly CancellationToken _cancellationToken;
|
|
||||||
private readonly CompilerHost _compilerHost;
|
|
||||||
private readonly ConnectionHost _connectionHost;
|
|
||||||
private readonly EventBus _eventBus;
|
|
||||||
|
|
||||||
private KeepAlive _keepAlive;
|
|
||||||
private State _state;
|
|
||||||
private Task _timeoutTask;
|
|
||||||
private Task _gcTask;
|
|
||||||
private Task<Connection> _listenTask;
|
|
||||||
private CancellationTokenSource _listenCancellationTokenSource;
|
|
||||||
private List<Task<ConnectionResult>> _connections = new List<Task<ConnectionResult>>();
|
|
||||||
|
|
||||||
public DefaultRequestDispatcher(ConnectionHost connectionHost, CompilerHost compilerHost, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_connectionHost = connectionHost;
|
|
||||||
_compilerHost = compilerHost;
|
|
||||||
_cancellationToken = cancellationToken;
|
|
||||||
|
|
||||||
_eventBus = EventBus.Default;
|
|
||||||
_keepAlive = new KeepAlive(DefaultServerKeepAlive, isDefault: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The server accepts connections until we reach a state that requires a shutdown. At that
|
|
||||||
// time no new connections will be accepted and the server will drain existing connections.
|
|
||||||
//
|
|
||||||
// The idea is that it's better to let clients fallback to in-proc (and slow down) than it is to keep
|
|
||||||
// running in an undesired state.
|
|
||||||
public override void Run()
|
|
||||||
{
|
|
||||||
_state = State.Running;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Listen();
|
|
||||||
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Debug.Assert(_listenTask != null);
|
|
||||||
|
|
||||||
MaybeCreateTimeoutTask();
|
|
||||||
MaybeCreateGCTask();
|
|
||||||
WaitForAnyCompletion(_cancellationToken);
|
|
||||||
CheckCompletedTasks(_cancellationToken);
|
|
||||||
}
|
|
||||||
while (_connections.Count > 0 || _state == State.Running);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_state = State.Completed;
|
|
||||||
_gcTask = null;
|
|
||||||
_timeoutTask = null;
|
|
||||||
|
|
||||||
if (_listenTask != null)
|
|
||||||
{
|
|
||||||
CloseListenTask();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void CheckCompletedTasks(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
HandleCancellation();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_listenTask.IsCompleted)
|
|
||||||
{
|
|
||||||
HandleCompletedListenTask(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_timeoutTask?.IsCompleted == true)
|
|
||||||
{
|
|
||||||
HandleCompletedTimeoutTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_gcTask?.IsCompleted == true)
|
|
||||||
{
|
|
||||||
HandleCompletedGCTask();
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleCompletedConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCancellation()
|
|
||||||
{
|
|
||||||
Debug.Assert(_listenTask != null);
|
|
||||||
|
|
||||||
// If cancellation has been requested then the server needs to be in the process
|
|
||||||
// of shutting down.
|
|
||||||
_state = State.ShuttingDown;
|
|
||||||
|
|
||||||
CloseListenTask();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Task.WaitAll(_connections.ToArray());
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// It's expected that some will throw exceptions, in particular OperationCanceledException. It's
|
|
||||||
// okay for them to throw so long as they complete.
|
|
||||||
}
|
|
||||||
|
|
||||||
HandleCompletedConnections();
|
|
||||||
Debug.Assert(_connections.Count == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The server farms out work to Task values and this method needs to wait until at least one of them
|
|
||||||
/// has completed.
|
|
||||||
/// </summary>
|
|
||||||
private void WaitForAnyCompletion(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var all = new List<Task>();
|
|
||||||
all.AddRange(_connections);
|
|
||||||
all.Add(_timeoutTask);
|
|
||||||
all.Add(_listenTask);
|
|
||||||
all.Add(_gcTask);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var waitArray = all.Where(x => x != null).ToArray();
|
|
||||||
Task.WaitAny(waitArray, cancellationToken);
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
// Thrown when the provided cancellationToken is cancelled. This is handled in the caller,
|
|
||||||
// here it just serves to break out of the WaitAny call.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Listen()
|
|
||||||
{
|
|
||||||
Debug.Assert(_listenTask == null);
|
|
||||||
Debug.Assert(_timeoutTask == null);
|
|
||||||
|
|
||||||
_listenCancellationTokenSource = new CancellationTokenSource();
|
|
||||||
_listenTask = _connectionHost.WaitForConnectionAsync(_listenCancellationTokenSource.Token);
|
|
||||||
_eventBus.ConnectionListening();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void CloseListenTask()
|
|
||||||
{
|
|
||||||
Debug.Assert(_listenTask != null);
|
|
||||||
|
|
||||||
_listenCancellationTokenSource.Cancel();
|
|
||||||
_listenCancellationTokenSource = null;
|
|
||||||
_listenTask = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCompletedListenTask(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_eventBus.ConnectionReceived();
|
|
||||||
|
|
||||||
// Don't accept any new connections once we're in shutdown mode, instead gracefully reject the request.
|
|
||||||
// This should cause the client to run in process.
|
|
||||||
var accept = _state == State.Running;
|
|
||||||
var connectionTask = AcceptConnection(_listenTask, accept, cancellationToken);
|
|
||||||
_connections.Add(connectionTask);
|
|
||||||
|
|
||||||
// Timeout and GC are only done when there are no active connections. Now that we have a new
|
|
||||||
// connection cancel out these tasks.
|
|
||||||
_timeoutTask = null;
|
|
||||||
_gcTask = null;
|
|
||||||
|
|
||||||
// Begin listening again for new connections.
|
|
||||||
_listenTask = null;
|
|
||||||
Listen();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCompletedTimeoutTask()
|
|
||||||
{
|
|
||||||
_eventBus.KeepAliveReached();
|
|
||||||
_listenCancellationTokenSource.Cancel();
|
|
||||||
_timeoutTask = null;
|
|
||||||
_state = State.ShuttingDown;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCompletedGCTask()
|
|
||||||
{
|
|
||||||
_gcTask = null;
|
|
||||||
for (int i = 0; i < 10; i++)
|
|
||||||
{
|
|
||||||
GC.Collect();
|
|
||||||
GC.WaitForPendingFinalizers();
|
|
||||||
}
|
|
||||||
|
|
||||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
|
||||||
GC.Collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MaybeCreateTimeoutTask()
|
|
||||||
{
|
|
||||||
// If there are no active clients running then the server needs to be in a timeout mode.
|
|
||||||
if (_connections.Count == 0 && _timeoutTask == null)
|
|
||||||
{
|
|
||||||
Debug.Assert(_listenTask != null);
|
|
||||||
_timeoutTask = Task.Delay(_keepAlive.TimeSpan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MaybeCreateGCTask()
|
|
||||||
{
|
|
||||||
if (_connections.Count == 0 && _gcTask == null)
|
|
||||||
{
|
|
||||||
_gcTask = Task.Delay(GCTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks the completed connection objects.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>False if the server needs to begin shutting down</returns>
|
|
||||||
private void HandleCompletedConnections()
|
|
||||||
{
|
|
||||||
var shutdown = false;
|
|
||||||
var processedCount = 0;
|
|
||||||
var i = 0;
|
|
||||||
while (i < _connections.Count)
|
|
||||||
{
|
|
||||||
var current = _connections[i];
|
|
||||||
if (!current.IsCompleted)
|
|
||||||
{
|
|
||||||
i++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_connections.RemoveAt(i);
|
|
||||||
processedCount++;
|
|
||||||
|
|
||||||
var result = current.Result;
|
|
||||||
if (result.KeepAlive.HasValue)
|
|
||||||
{
|
|
||||||
var updated = _keepAlive.Update(result.KeepAlive.Value);
|
|
||||||
if (updated.Equals(_keepAlive))
|
|
||||||
{
|
|
||||||
_eventBus.UpdateKeepAlive(updated.TimeSpan);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (result.CloseReason)
|
|
||||||
{
|
|
||||||
case ConnectionResult.Reason.CompilationCompleted:
|
|
||||||
case ConnectionResult.Reason.CompilationNotStarted:
|
|
||||||
// These are all normal end states. Nothing to do here.
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ConnectionResult.Reason.ClientDisconnect:
|
|
||||||
// Have to assume the worst here which is user pressing Ctrl+C at the command line and
|
|
||||||
// hence wanting all compilation to end.
|
|
||||||
_eventBus.ConnectionRudelyEnded();
|
|
||||||
shutdown = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ConnectionResult.Reason.ClientException:
|
|
||||||
case ConnectionResult.Reason.ClientShutdownRequest:
|
|
||||||
_eventBus.ConnectionRudelyEnded();
|
|
||||||
shutdown = true;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException($"Unexpected enum value {result.CloseReason}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (processedCount > 0)
|
|
||||||
{
|
|
||||||
_eventBus.ConnectionCompleted(processedCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shutdown)
|
|
||||||
{
|
|
||||||
_state = State.ShuttingDown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<ConnectionResult> AcceptConnection(Task<Connection> task, bool accept, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Connection connection;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
connection = await task;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Unable to establish a connection with the client. The client is responsible for
|
|
||||||
// handling this case. Nothing else for us to do here.
|
|
||||||
CompilerServerLogger.LogException(ex, "Error creating client named pipe");
|
|
||||||
return new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (connection)
|
|
||||||
{
|
|
||||||
BuildRequest request;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CompilerServerLogger.Log("Begin reading request.");
|
|
||||||
request = await BuildRequest.ReadAsync(connection.Stream, cancellationToken).ConfigureAwait(false);
|
|
||||||
CompilerServerLogger.Log("End reading request.");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
CompilerServerLogger.LogException(e, "Error reading build request.");
|
|
||||||
return new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.IsShutdownRequest())
|
|
||||||
{
|
|
||||||
// Reply with the PID of this process so that the client can wait for it to exit.
|
|
||||||
var response = new ShutdownBuildResponse(Process.GetCurrentProcess().Id);
|
|
||||||
await response.WriteAsync(connection.Stream, cancellationToken);
|
|
||||||
|
|
||||||
// We can safely disconnect the client, then when this connection gets cleaned up by the event loop
|
|
||||||
// the server will go to a shutdown state.
|
|
||||||
return new ConnectionResult(ConnectionResult.Reason.ClientShutdownRequest);
|
|
||||||
}
|
|
||||||
else if (!accept)
|
|
||||||
{
|
|
||||||
// We're already in shutdown mode, respond gracefully so the client can run in-process.
|
|
||||||
var response = new RejectedBuildResponse();
|
|
||||||
await response.WriteAsync(connection.Stream, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return new ConnectionResult(ConnectionResult.Reason.CompilationNotStarted);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If we get here then this is a real request that we will accept and process.
|
|
||||||
//
|
|
||||||
// Kick off both the compilation and a task to monitor the pipe for closing.
|
|
||||||
var buildCancelled = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
|
|
||||||
var watcher = connection.WaitForDisconnectAsync(buildCancelled.Token);
|
|
||||||
var worker = ExecuteRequestAsync(request, buildCancelled.Token);
|
|
||||||
|
|
||||||
// await will end when either the work is complete or the connection is closed.
|
|
||||||
await Task.WhenAny(worker, watcher);
|
|
||||||
|
|
||||||
// Do an 'await' on the completed task, preference being compilation, to force
|
|
||||||
// any exceptions to be realized in this method for logging.
|
|
||||||
ConnectionResult.Reason reason;
|
|
||||||
if (worker.IsCompleted)
|
|
||||||
{
|
|
||||||
var response = await worker;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
CompilerServerLogger.Log("Begin writing response.");
|
|
||||||
await response.WriteAsync(connection.Stream, cancellationToken);
|
|
||||||
CompilerServerLogger.Log("End writing response.");
|
|
||||||
|
|
||||||
reason = ConnectionResult.Reason.CompilationCompleted;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
reason = ConnectionResult.Reason.ClientDisconnect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await watcher;
|
|
||||||
reason = ConnectionResult.Reason.ClientDisconnect;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Begin the tear down of the Task which didn't complete.
|
|
||||||
buildCancelled.Cancel();
|
|
||||||
|
|
||||||
return new ConnectionResult(reason, request.KeepAlive);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
CompilerServerLogger.LogException(ex, "Error handling connection");
|
|
||||||
return new ConnectionResult(ConnectionResult.Reason.ClientException);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task<BuildResponse> ExecuteRequestAsync(BuildRequest buildRequest, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
Func<BuildResponse> func = () =>
|
|
||||||
{
|
|
||||||
CompilerServerLogger.Log("Begin processing request");
|
|
||||||
|
|
||||||
var response = _compilerHost.Execute(buildRequest, cancellationToken);
|
|
||||||
|
|
||||||
CompilerServerLogger.Log("End processing request");
|
|
||||||
return response;
|
|
||||||
};
|
|
||||||
|
|
||||||
var task = new Task<BuildResponse>(func, cancellationToken, TaskCreationOptions.LongRunning);
|
|
||||||
task.Start();
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct KeepAlive
|
|
||||||
{
|
|
||||||
public TimeSpan TimeSpan;
|
|
||||||
public bool IsDefault;
|
|
||||||
|
|
||||||
public KeepAlive(TimeSpan timeSpan, bool isDefault)
|
|
||||||
{
|
|
||||||
TimeSpan = timeSpan;
|
|
||||||
IsDefault = isDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeepAlive Update(TimeSpan timeSpan)
|
|
||||||
{
|
|
||||||
if (IsDefault || timeSpan > TimeSpan)
|
|
||||||
{
|
|
||||||
return new KeepAlive(timeSpan, isDefault: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,586 +0,0 @@
|
||||||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using static Microsoft.CodeAnalysis.CommandLine.BuildProtocolConstants;
|
|
||||||
using static Microsoft.CodeAnalysis.CommandLine.CompilerServerLogger;
|
|
||||||
|
|
||||||
// This file describes data structures about the protocol from client program to server that is
|
|
||||||
// used. The basic protocol is this.
|
|
||||||
//
|
|
||||||
// After the server pipe is connected, it forks off a thread to handle the connection, and creates
|
|
||||||
// a new instance of the pipe to listen for new clients. When it gets a request, it validates
|
|
||||||
// the security and elevation level of the client. If that fails, it disconnects the client. Otherwise,
|
|
||||||
// it handles the request, sends a response (described by Response class) back to the client, then
|
|
||||||
// disconnects the pipe and ends the thread.
|
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.CommandLine
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a request from the client. A request is as follows.
|
|
||||||
///
|
|
||||||
/// Field Name Type Size (bytes)
|
|
||||||
/// ----------------------------------------------------
|
|
||||||
/// Length Integer 4
|
|
||||||
/// Argument Count UInteger 4
|
|
||||||
/// Arguments Argument[] Variable
|
|
||||||
///
|
|
||||||
/// See <see cref="Argument"/> for the format of an
|
|
||||||
/// Argument.
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
internal class BuildRequest
|
|
||||||
{
|
|
||||||
public readonly uint ProtocolVersion;
|
|
||||||
public readonly ReadOnlyCollection<Argument> Arguments;
|
|
||||||
|
|
||||||
public BuildRequest(uint protocolVersion, IEnumerable<Argument> arguments)
|
|
||||||
{
|
|
||||||
ProtocolVersion = protocolVersion;
|
|
||||||
Arguments = new ReadOnlyCollection<Argument>(arguments.ToList());
|
|
||||||
|
|
||||||
if (Arguments.Count > ushort.MaxValue)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(
|
|
||||||
nameof(arguments),
|
|
||||||
$"Too many arguments: maximum of {ushort.MaxValue} arguments allowed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public TimeSpan? KeepAlive
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
TimeSpan? keepAlive = null;
|
|
||||||
foreach (var argument in Arguments)
|
|
||||||
{
|
|
||||||
if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.KeepAlive)
|
|
||||||
{
|
|
||||||
// If the value is not a valid integer for any reason,ignore it and continue with the current timeout.
|
|
||||||
// The client is responsible for validating the argument.
|
|
||||||
if (int.TryParse(argument.Value, out var result))
|
|
||||||
{
|
|
||||||
// Keep alive times are specified in seconds
|
|
||||||
keepAlive = TimeSpan.FromSeconds(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keepAlive;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BuildRequest Create(
|
|
||||||
string workingDirectory,
|
|
||||||
string tempDirectory,
|
|
||||||
IList<string> args,
|
|
||||||
string keepAlive = null,
|
|
||||||
string libDirectory = null)
|
|
||||||
{
|
|
||||||
Log("Creating BuildRequest");
|
|
||||||
Log($"Working directory: {workingDirectory}");
|
|
||||||
Log($"Temp directory: {tempDirectory}");
|
|
||||||
Log($"Lib directory: {libDirectory ?? "null"}");
|
|
||||||
|
|
||||||
var requestLength = args.Count + 1 + (libDirectory == null ? 0 : 1);
|
|
||||||
var requestArgs = new List<Argument>(requestLength);
|
|
||||||
|
|
||||||
requestArgs.Add(new Argument(ArgumentId.CurrentDirectory, 0, workingDirectory));
|
|
||||||
requestArgs.Add(new Argument(ArgumentId.TempDirectory, 0, tempDirectory));
|
|
||||||
|
|
||||||
if (keepAlive != null)
|
|
||||||
{
|
|
||||||
requestArgs.Add(new Argument(ArgumentId.KeepAlive, 0, keepAlive));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (libDirectory != null)
|
|
||||||
{
|
|
||||||
requestArgs.Add(new Argument(ArgumentId.LibEnvVariable, 0, libDirectory));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < args.Count; ++i)
|
|
||||||
{
|
|
||||||
var arg = args[i];
|
|
||||||
Log($"argument[{i}] = {arg}");
|
|
||||||
requestArgs.Add(new Argument(ArgumentId.CommandLineArgument, i, arg));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BuildRequest(BuildProtocolConstants.ProtocolVersion, requestArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static BuildRequest CreateShutdown()
|
|
||||||
{
|
|
||||||
var requestArgs = new[]
|
|
||||||
{
|
|
||||||
new Argument(ArgumentId.Shutdown, argumentIndex: 0, value: ""),
|
|
||||||
new Argument(ArgumentId.CommandLineArgument, argumentIndex: 1, value: "shutdown"),
|
|
||||||
};
|
|
||||||
return new BuildRequest(BuildProtocolConstants.ProtocolVersion, requestArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsShutdownRequest()
|
|
||||||
{
|
|
||||||
return Arguments.Count >= 1 && Arguments[0].ArgumentId == ArgumentId.Shutdown;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read a Request from the given stream.
|
|
||||||
///
|
|
||||||
/// The total request size must be less than 1MB.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>null if the Request was too large, the Request otherwise.</returns>
|
|
||||||
public static async Task<BuildRequest> ReadAsync(Stream inStream, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
// Read the length of the request
|
|
||||||
var lengthBuffer = new byte[4];
|
|
||||||
Log("Reading length of request");
|
|
||||||
await ReadAllAsync(inStream, lengthBuffer, 4, cancellationToken).ConfigureAwait(false);
|
|
||||||
var length = BitConverter.ToInt32(lengthBuffer, 0);
|
|
||||||
|
|
||||||
// Back out if the request is > 1MB
|
|
||||||
if (length > 0x100000)
|
|
||||||
{
|
|
||||||
Log("Request is over 1MB in length, cancelling read.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Read the full request
|
|
||||||
var requestBuffer = new byte[length];
|
|
||||||
await ReadAllAsync(inStream, requestBuffer, length, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
Log("Parsing request");
|
|
||||||
// Parse the request into the Request data structure.
|
|
||||||
using (var reader = new BinaryReader(new MemoryStream(requestBuffer), Encoding.Unicode))
|
|
||||||
{
|
|
||||||
var protocolVersion = reader.ReadUInt32();
|
|
||||||
uint argumentCount = reader.ReadUInt32();
|
|
||||||
|
|
||||||
var argumentsBuilder = new List<Argument>((int)argumentCount);
|
|
||||||
|
|
||||||
for (int i = 0; i < argumentCount; i++)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
argumentsBuilder.Add(BuildRequest.Argument.ReadFromBinaryReader(reader));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BuildRequest(protocolVersion, argumentsBuilder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Write a Request to the stream.
|
|
||||||
/// </summary>
|
|
||||||
public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken = default(CancellationToken))
|
|
||||||
{
|
|
||||||
using (var memoryStream = new MemoryStream())
|
|
||||||
using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode))
|
|
||||||
{
|
|
||||||
// Format the request.
|
|
||||||
Log("Formatting request");
|
|
||||||
writer.Write(ProtocolVersion);
|
|
||||||
writer.Write(Arguments.Count);
|
|
||||||
foreach (Argument arg in Arguments)
|
|
||||||
{
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
arg.WriteToBinaryWriter(writer);
|
|
||||||
}
|
|
||||||
writer.Flush();
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Write the length of the request
|
|
||||||
int length = checked((int)memoryStream.Length);
|
|
||||||
|
|
||||||
// Back out if the request is > 1 MB
|
|
||||||
if (memoryStream.Length > 0x100000)
|
|
||||||
{
|
|
||||||
Log("Request is over 1MB in length, cancelling write");
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the request to the server
|
|
||||||
Log("Writing length of request.");
|
|
||||||
await outStream.WriteAsync(BitConverter.GetBytes(length), 0, 4,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Log("Writing request of size {0}", length);
|
|
||||||
// Write the request
|
|
||||||
memoryStream.Position = 0;
|
|
||||||
await memoryStream.CopyToAsync(outStream, bufferSize: length, cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A command line argument to the compilation.
|
|
||||||
/// An argument is formatted as follows:
|
|
||||||
///
|
|
||||||
/// Field Name Type Size (bytes)
|
|
||||||
/// --------------------------------------------------
|
|
||||||
/// ID UInteger 4
|
|
||||||
/// Index UInteger 4
|
|
||||||
/// Value String Variable
|
|
||||||
///
|
|
||||||
/// Strings are encoded via a length prefix as a signed
|
|
||||||
/// 32-bit integer, followed by an array of characters.
|
|
||||||
/// </summary>
|
|
||||||
public struct Argument
|
|
||||||
{
|
|
||||||
public readonly ArgumentId ArgumentId;
|
|
||||||
public readonly int ArgumentIndex;
|
|
||||||
public readonly string Value;
|
|
||||||
|
|
||||||
public Argument(ArgumentId argumentId,
|
|
||||||
int argumentIndex,
|
|
||||||
string value)
|
|
||||||
{
|
|
||||||
ArgumentId = argumentId;
|
|
||||||
ArgumentIndex = argumentIndex;
|
|
||||||
Value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Argument ReadFromBinaryReader(BinaryReader reader)
|
|
||||||
{
|
|
||||||
var argId = (ArgumentId)reader.ReadInt32();
|
|
||||||
var argIndex = reader.ReadInt32();
|
|
||||||
string value = ReadLengthPrefixedString(reader);
|
|
||||||
return new Argument(argId, argIndex, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteToBinaryWriter(BinaryWriter writer)
|
|
||||||
{
|
|
||||||
writer.Write((int)ArgumentId);
|
|
||||||
writer.Write(ArgumentIndex);
|
|
||||||
WriteLengthPrefixedString(writer, Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for all possible responses to a request.
|
|
||||||
/// The ResponseType enum should list all possible response types
|
|
||||||
/// and ReadResponse creates the appropriate response subclass based
|
|
||||||
/// on the response type sent by the client.
|
|
||||||
/// The format of a response is:
|
|
||||||
///
|
|
||||||
/// Field Name Field Type Size (bytes)
|
|
||||||
/// -------------------------------------------------
|
|
||||||
/// responseLength int (positive) 4
|
|
||||||
/// responseType enum ResponseType 4
|
|
||||||
/// responseBody Response subclass variable
|
|
||||||
/// </summary>
|
|
||||||
internal abstract class BuildResponse
|
|
||||||
{
|
|
||||||
public enum ResponseType
|
|
||||||
{
|
|
||||||
// The client and server are using incompatible protocol versions.
|
|
||||||
MismatchedVersion,
|
|
||||||
|
|
||||||
// The build request completed on the server and the results are contained
|
|
||||||
// in the message.
|
|
||||||
Completed,
|
|
||||||
|
|
||||||
// The build request could not be run on the server due because it created
|
|
||||||
// an unresolvable inconsistency with analyzers.
|
|
||||||
AnalyzerInconsistency,
|
|
||||||
|
|
||||||
// The shutdown request completed and the server process information is
|
|
||||||
// contained in the message.
|
|
||||||
Shutdown,
|
|
||||||
|
|
||||||
// The request was rejected by the server.
|
|
||||||
Rejected,
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract ResponseType Type { get; }
|
|
||||||
|
|
||||||
public async Task WriteAsync(Stream outStream,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
using (var memoryStream = new MemoryStream())
|
|
||||||
using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode))
|
|
||||||
{
|
|
||||||
// Format the response
|
|
||||||
Log("Formatting Response");
|
|
||||||
writer.Write((int)Type);
|
|
||||||
|
|
||||||
AddResponseBody(writer);
|
|
||||||
writer.Flush();
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Send the response to the client
|
|
||||||
|
|
||||||
// Write the length of the response
|
|
||||||
int length = checked((int)memoryStream.Length);
|
|
||||||
|
|
||||||
Log("Writing response length");
|
|
||||||
// There is no way to know the number of bytes written to
|
|
||||||
// the pipe stream. We just have to assume all of them are written.
|
|
||||||
await outStream.WriteAsync(BitConverter.GetBytes(length),
|
|
||||||
0,
|
|
||||||
4,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
// Write the response
|
|
||||||
Log("Writing response of size {0}", length);
|
|
||||||
memoryStream.Position = 0;
|
|
||||||
await memoryStream.CopyToAsync(outStream, bufferSize: length, cancellationToken: cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract void AddResponseBody(BinaryWriter writer);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// May throw exceptions if there are pipe problems.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stream"></param>
|
|
||||||
/// <param name="cancellationToken"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static async Task<BuildResponse> ReadAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken))
|
|
||||||
{
|
|
||||||
Log("Reading response length");
|
|
||||||
// Read the response length
|
|
||||||
var lengthBuffer = new byte[4];
|
|
||||||
await ReadAllAsync(stream, lengthBuffer, 4, cancellationToken).ConfigureAwait(false);
|
|
||||||
var length = BitConverter.ToUInt32(lengthBuffer, 0);
|
|
||||||
|
|
||||||
// Read the response
|
|
||||||
Log("Reading response of length {0}", length);
|
|
||||||
var responseBuffer = new byte[length];
|
|
||||||
await ReadAllAsync(stream,
|
|
||||||
responseBuffer,
|
|
||||||
responseBuffer.Length,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
using (var reader = new BinaryReader(new MemoryStream(responseBuffer), Encoding.Unicode))
|
|
||||||
{
|
|
||||||
var responseType = (ResponseType)reader.ReadInt32();
|
|
||||||
|
|
||||||
switch (responseType)
|
|
||||||
{
|
|
||||||
case ResponseType.Completed:
|
|
||||||
return CompletedBuildResponse.Create(reader);
|
|
||||||
case ResponseType.MismatchedVersion:
|
|
||||||
return new MismatchedVersionBuildResponse();
|
|
||||||
case ResponseType.AnalyzerInconsistency:
|
|
||||||
return new AnalyzerInconsistencyBuildResponse();
|
|
||||||
case ResponseType.Shutdown:
|
|
||||||
return ShutdownBuildResponse.Create(reader);
|
|
||||||
case ResponseType.Rejected:
|
|
||||||
return new RejectedBuildResponse();
|
|
||||||
default:
|
|
||||||
throw new InvalidOperationException("Received invalid response type from server.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a Response from the server. A response is as follows.
|
|
||||||
///
|
|
||||||
/// Field Name Type Size (bytes)
|
|
||||||
/// --------------------------------------------------
|
|
||||||
/// Length UInteger 4
|
|
||||||
/// ReturnCode Integer 4
|
|
||||||
/// Output String Variable
|
|
||||||
/// ErrorOutput String Variable
|
|
||||||
///
|
|
||||||
/// Strings are encoded via a character count prefix as a
|
|
||||||
/// 32-bit integer, followed by an array of characters.
|
|
||||||
///
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class CompletedBuildResponse : BuildResponse
|
|
||||||
{
|
|
||||||
public readonly int ReturnCode;
|
|
||||||
public readonly bool Utf8Output;
|
|
||||||
public readonly string Output;
|
|
||||||
public readonly string ErrorOutput;
|
|
||||||
|
|
||||||
public CompletedBuildResponse(int returnCode,
|
|
||||||
bool utf8output,
|
|
||||||
string output)
|
|
||||||
{
|
|
||||||
ReturnCode = returnCode;
|
|
||||||
Utf8Output = utf8output;
|
|
||||||
Output = output;
|
|
||||||
|
|
||||||
// This field existed to support writing to Console.Error. The compiler doesn't ever write to
|
|
||||||
// this field or Console.Error. This field is only kept around in order to maintain the existing
|
|
||||||
// protocol semantics.
|
|
||||||
ErrorOutput = string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ResponseType Type => ResponseType.Completed;
|
|
||||||
|
|
||||||
public static CompletedBuildResponse Create(BinaryReader reader)
|
|
||||||
{
|
|
||||||
var returnCode = reader.ReadInt32();
|
|
||||||
var utf8Output = reader.ReadBoolean();
|
|
||||||
var output = ReadLengthPrefixedString(reader);
|
|
||||||
var errorOutput = ReadLengthPrefixedString(reader);
|
|
||||||
if (!string.IsNullOrEmpty(errorOutput))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new CompletedBuildResponse(returnCode, utf8Output, output);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void AddResponseBody(BinaryWriter writer)
|
|
||||||
{
|
|
||||||
writer.Write(ReturnCode);
|
|
||||||
writer.Write(Utf8Output);
|
|
||||||
WriteLengthPrefixedString(writer, Output);
|
|
||||||
WriteLengthPrefixedString(writer, ErrorOutput);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class ShutdownBuildResponse : BuildResponse
|
|
||||||
{
|
|
||||||
public readonly int ServerProcessId;
|
|
||||||
|
|
||||||
public ShutdownBuildResponse(int serverProcessId)
|
|
||||||
{
|
|
||||||
ServerProcessId = serverProcessId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override ResponseType Type => ResponseType.Shutdown;
|
|
||||||
|
|
||||||
protected override void AddResponseBody(BinaryWriter writer)
|
|
||||||
{
|
|
||||||
writer.Write(ServerProcessId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ShutdownBuildResponse Create(BinaryReader reader)
|
|
||||||
{
|
|
||||||
var serverProcessId = reader.ReadInt32();
|
|
||||||
return new ShutdownBuildResponse(serverProcessId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class MismatchedVersionBuildResponse : BuildResponse
|
|
||||||
{
|
|
||||||
public override ResponseType Type => ResponseType.MismatchedVersion;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// MismatchedVersion has no body.
|
|
||||||
/// </summary>
|
|
||||||
protected override void AddResponseBody(BinaryWriter writer) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class AnalyzerInconsistencyBuildResponse : BuildResponse
|
|
||||||
{
|
|
||||||
public override ResponseType Type => ResponseType.AnalyzerInconsistency;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AnalyzerInconsistency has no body.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writer"></param>
|
|
||||||
protected override void AddResponseBody(BinaryWriter writer) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class RejectedBuildResponse : BuildResponse
|
|
||||||
{
|
|
||||||
public override ResponseType Type => ResponseType.Rejected;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AnalyzerInconsistency has no body.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writer"></param>
|
|
||||||
protected override void AddResponseBody(BinaryWriter writer) { }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Constants about the protocol.
|
|
||||||
/// </summary>
|
|
||||||
internal static class BuildProtocolConstants
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The version number for this protocol.
|
|
||||||
/// </summary>
|
|
||||||
public const uint ProtocolVersion = 2;
|
|
||||||
|
|
||||||
// Arguments for CSharp and VB Compiler
|
|
||||||
public enum ArgumentId
|
|
||||||
{
|
|
||||||
// The current directory of the client
|
|
||||||
CurrentDirectory = 0x51147221,
|
|
||||||
|
|
||||||
// A comment line argument. The argument index indicates which one (0 .. N)
|
|
||||||
CommandLineArgument,
|
|
||||||
|
|
||||||
// The "LIB" environment variable of the client
|
|
||||||
LibEnvVariable,
|
|
||||||
|
|
||||||
// Request a longer keep alive time for the server
|
|
||||||
KeepAlive,
|
|
||||||
|
|
||||||
// Request a server shutdown from the client
|
|
||||||
Shutdown,
|
|
||||||
|
|
||||||
// The directory to use for temporary operations.
|
|
||||||
TempDirectory,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read a string from the Reader where the string is encoded
|
|
||||||
/// as a length prefix (signed 32-bit integer) followed by
|
|
||||||
/// a sequence of characters.
|
|
||||||
/// </summary>
|
|
||||||
public static string ReadLengthPrefixedString(BinaryReader reader)
|
|
||||||
{
|
|
||||||
var length = reader.ReadInt32();
|
|
||||||
return new String(reader.ReadChars(length));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Write a string to the Writer where the string is encoded
|
|
||||||
/// as a length prefix (signed 32-bit integer) follows by
|
|
||||||
/// a sequence of characters.
|
|
||||||
/// </summary>
|
|
||||||
public static void WriteLengthPrefixedString(BinaryWriter writer, string value)
|
|
||||||
{
|
|
||||||
writer.Write(value.Length);
|
|
||||||
writer.Write(value.ToCharArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This task does not complete until we are completely done reading.
|
|
||||||
/// </summary>
|
|
||||||
internal static async Task ReadAllAsync(
|
|
||||||
Stream stream,
|
|
||||||
byte[] buffer,
|
|
||||||
int count,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
int totalBytesRead = 0;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
Log("Attempting to read {0} bytes from the stream",
|
|
||||||
count - totalBytesRead);
|
|
||||||
int bytesRead = await stream.ReadAsync(buffer,
|
|
||||||
totalBytesRead,
|
|
||||||
count - totalBytesRead,
|
|
||||||
cancellationToken).ConfigureAwait(false);
|
|
||||||
if (bytesRead == 0)
|
|
||||||
{
|
|
||||||
Log("Unexpected -- read 0 bytes from the stream.");
|
|
||||||
throw new EndOfStreamException("Reached end of stream before end of read.");
|
|
||||||
}
|
|
||||||
Log("Read {0} bytes", bytesRead);
|
|
||||||
totalBytesRead += bytesRead;
|
|
||||||
} while (totalBytesRead < count);
|
|
||||||
Log("Finished read");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,520 +0,0 @@
|
||||||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.Pipes;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Razor.Tools;
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
|
||||||
using Roslyn.Utilities;
|
|
||||||
using static Microsoft.CodeAnalysis.CommandLine.CompilerServerLogger;
|
|
||||||
using static Microsoft.CodeAnalysis.CommandLine.NativeMethods;
|
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.CommandLine
|
|
||||||
{
|
|
||||||
internal struct BuildPathsAlt
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The path which contains the compiler binaries and response files.
|
|
||||||
/// </summary>
|
|
||||||
internal string ClientDirectory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The path in which the compilation takes place.
|
|
||||||
/// </summary>
|
|
||||||
internal string WorkingDirectory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The path which contains mscorlib. This can be null when specified by the user or running in a
|
|
||||||
/// CoreClr environment.
|
|
||||||
/// </summary>
|
|
||||||
internal string SdkDirectory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The temporary directory a compilation should use instead of <see cref="Path.GetTempPath"/>. The latter
|
|
||||||
/// relies on global state individual compilations should ignore.
|
|
||||||
/// </summary>
|
|
||||||
internal string TempDirectory { get; }
|
|
||||||
|
|
||||||
internal BuildPathsAlt(string clientDir, string workingDir, string sdkDir, string tempDir)
|
|
||||||
{
|
|
||||||
ClientDirectory = clientDir;
|
|
||||||
WorkingDirectory = workingDir;
|
|
||||||
SdkDirectory = sdkDir;
|
|
||||||
TempDirectory = tempDir;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class BuildServerConnection
|
|
||||||
{
|
|
||||||
internal const string ServerNameCoreClr = "rzc.dll";
|
|
||||||
|
|
||||||
// Spend up to 1s connecting to existing process (existing processes should be always responsive).
|
|
||||||
internal const int TimeOutMsExistingProcess = 1000;
|
|
||||||
|
|
||||||
// Spend up to 20s connecting to a new process, to allow time for it to start.
|
|
||||||
internal const int TimeOutMsNewProcess = 20000;
|
|
||||||
|
|
||||||
public static Task<BuildResponse> RunServerCompilation(
|
|
||||||
List<string> arguments,
|
|
||||||
BuildPathsAlt buildPaths,
|
|
||||||
string keepAlive,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
var pipeName = PipeName.ComputeDefault();
|
|
||||||
|
|
||||||
return RunServerCompilationCore(
|
|
||||||
arguments,
|
|
||||||
buildPaths,
|
|
||||||
pipeName: pipeName,
|
|
||||||
keepAlive: keepAlive,
|
|
||||||
libEnvVariable: null,
|
|
||||||
timeoutOverride: null,
|
|
||||||
tryCreateServerFunc: TryCreateServerCore,
|
|
||||||
cancellationToken: cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static async Task<BuildResponse> RunServerCompilationCore(
|
|
||||||
List<string> arguments,
|
|
||||||
BuildPathsAlt buildPaths,
|
|
||||||
string pipeName,
|
|
||||||
string keepAlive,
|
|
||||||
string libEnvVariable,
|
|
||||||
int? timeoutOverride,
|
|
||||||
Func<string, string, bool> tryCreateServerFunc,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
if (pipeName == null)
|
|
||||||
{
|
|
||||||
return new RejectedBuildResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buildPaths.TempDirectory == null)
|
|
||||||
{
|
|
||||||
return new RejectedBuildResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
var clientDir = buildPaths.ClientDirectory;
|
|
||||||
var timeoutNewProcess = timeoutOverride ?? TimeOutMsNewProcess;
|
|
||||||
var timeoutExistingProcess = timeoutOverride ?? TimeOutMsExistingProcess;
|
|
||||||
var clientMutexName = MutexName.GetClientMutexName(pipeName);
|
|
||||||
Task<NamedPipeClientStream> pipeTask = null;
|
|
||||||
using (var clientMutex = new Mutex(initiallyOwned: true,
|
|
||||||
name: clientMutexName,
|
|
||||||
createdNew: out var holdsMutex))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!holdsMutex)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
holdsMutex = clientMutex.WaitOne(timeoutNewProcess);
|
|
||||||
|
|
||||||
if (!holdsMutex)
|
|
||||||
{
|
|
||||||
return new RejectedBuildResponse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (AbandonedMutexException)
|
|
||||||
{
|
|
||||||
holdsMutex = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for an already running server
|
|
||||||
var serverMutexName = MutexName.GetServerMutexName(pipeName);
|
|
||||||
bool wasServerRunning = WasServerMutexOpen(serverMutexName);
|
|
||||||
var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess;
|
|
||||||
|
|
||||||
if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName))
|
|
||||||
{
|
|
||||||
pipeTask = TryConnectToServerAsync(pipeName, timeout, cancellationToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (holdsMutex)
|
|
||||||
{
|
|
||||||
clientMutex.ReleaseMutex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pipeTask != null)
|
|
||||||
{
|
|
||||||
var pipe = await pipeTask.ConfigureAwait(false);
|
|
||||||
if (pipe != null)
|
|
||||||
{
|
|
||||||
var request = BuildRequest.Create(
|
|
||||||
buildPaths.WorkingDirectory,
|
|
||||||
buildPaths.TempDirectory,
|
|
||||||
arguments,
|
|
||||||
keepAlive,
|
|
||||||
libEnvVariable);
|
|
||||||
|
|
||||||
return await TryCompile(pipe, request, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RejectedBuildResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool WasServerMutexOpen(string mutexName)
|
|
||||||
{
|
|
||||||
Mutex mutex;
|
|
||||||
var open = Mutex.TryOpenExisting(mutexName, out mutex);
|
|
||||||
if (open)
|
|
||||||
{
|
|
||||||
mutex.Dispose();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Try to compile using the server. Returns a null-containing Task if a response
|
|
||||||
/// from the server cannot be retrieved.
|
|
||||||
/// </summary>
|
|
||||||
private static async Task<BuildResponse> TryCompile(NamedPipeClientStream pipeStream,
|
|
||||||
BuildRequest request,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
BuildResponse response;
|
|
||||||
using (pipeStream)
|
|
||||||
{
|
|
||||||
// Write the request
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log("Begin writing request");
|
|
||||||
await request.WriteAsync(pipeStream, cancellationToken).ConfigureAwait(false);
|
|
||||||
Log("End writing request");
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogException(e, "Error writing build request.");
|
|
||||||
return new RejectedBuildResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for the compilation and a monitor to detect if the server disconnects
|
|
||||||
var serverCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
|
||||||
|
|
||||||
Log("Begin reading response");
|
|
||||||
|
|
||||||
var responseTask = BuildResponse.ReadAsync(pipeStream, serverCts.Token);
|
|
||||||
var monitorTask = CreateMonitorDisconnectTask(pipeStream, "client", serverCts.Token);
|
|
||||||
await Task.WhenAny(responseTask, monitorTask).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Log("End reading response");
|
|
||||||
|
|
||||||
if (responseTask.IsCompleted)
|
|
||||||
{
|
|
||||||
// await the task to log any exceptions
|
|
||||||
try
|
|
||||||
{
|
|
||||||
response = await responseTask.ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
LogException(e, "Error reading response");
|
|
||||||
response = new RejectedBuildResponse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log("Server disconnect");
|
|
||||||
response = new RejectedBuildResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cancel whatever task is still around
|
|
||||||
serverCts.Cancel();
|
|
||||||
Debug.Assert(response != null);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The IsConnected property on named pipes does not detect when the client has disconnected
|
|
||||||
/// if we don't attempt any new I/O after the client disconnects. We start an async I/O here
|
|
||||||
/// which serves to check the pipe for disconnection.
|
|
||||||
/// </summary>
|
|
||||||
internal static async Task CreateMonitorDisconnectTask(
|
|
||||||
PipeStream pipeStream,
|
|
||||||
string identifier = null,
|
|
||||||
CancellationToken cancellationToken = default(CancellationToken))
|
|
||||||
{
|
|
||||||
var buffer = Array.Empty<byte>();
|
|
||||||
|
|
||||||
while (!cancellationToken.IsCancellationRequested && pipeStream.IsConnected)
|
|
||||||
{
|
|
||||||
// Wait a tenth of a second before trying again
|
|
||||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Log($"Before poking pipe {identifier}.");
|
|
||||||
await pipeStream.ReadAsync(buffer, 0, 0, cancellationToken).ConfigureAwait(false);
|
|
||||||
Log($"After poking pipe {identifier}.");
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
// It is okay for this call to fail. Errors will be reflected in the
|
|
||||||
// IsConnected property which will be read on the next iteration of the
|
|
||||||
LogException(e, $"Error poking pipe {identifier}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connect to the pipe for a given directory and return it.
|
|
||||||
/// Throws on cancellation.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="pipeName">Name of the named pipe to connect to.</param>
|
|
||||||
/// <param name="timeoutMs">Timeout to allow in connecting to process.</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token to cancel connection to server.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// An open <see cref="NamedPipeClientStream"/> to the server process or null on failure.
|
|
||||||
/// </returns>
|
|
||||||
internal static async Task<NamedPipeClientStream> TryConnectToServerAsync(
|
|
||||||
string pipeName,
|
|
||||||
int timeoutMs,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
NamedPipeClientStream pipeStream;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Machine-local named pipes are named "\\.\pipe\<pipename>".
|
|
||||||
// We use the SHA1 of the directory the compiler exes live in as the pipe name.
|
|
||||||
// The NamedPipeClientStream class handles the "\\.\pipe\" part for us.
|
|
||||||
Log("Attempt to open named pipe '{0}'", pipeName);
|
|
||||||
|
|
||||||
pipeStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
Log("Attempt to connect named pipe '{0}'", pipeName);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await pipeStream.ConnectAsync(timeoutMs, cancellationToken).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception e) when (e is IOException || e is TimeoutException)
|
|
||||||
{
|
|
||||||
// Note: IOException can also indicate timeout. From docs:
|
|
||||||
// TimeoutException: Could not connect to the server within the
|
|
||||||
// specified timeout period.
|
|
||||||
// IOException: The server is connected to another client and the
|
|
||||||
// time-out period has expired.
|
|
||||||
|
|
||||||
Log($"Connecting to server timed out after {timeoutMs} ms");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Log("Named pipe '{0}' connected", pipeName);
|
|
||||||
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
|
||||||
|
|
||||||
// Verify that we own the pipe.
|
|
||||||
if (!CheckPipeConnectionOwnership(pipeStream))
|
|
||||||
{
|
|
||||||
Log("Owner of named pipe is incorrect");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pipeStream;
|
|
||||||
}
|
|
||||||
catch (Exception e) when (!(e is TaskCanceledException || e is OperationCanceledException))
|
|
||||||
{
|
|
||||||
LogException(e, "Exception while connecting to process");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static bool TryCreateServerCore(string clientDir, string pipeName)
|
|
||||||
{
|
|
||||||
string expectedPath;
|
|
||||||
string processArguments;
|
|
||||||
|
|
||||||
// The server should be in the same directory as the client
|
|
||||||
var expectedCompilerPath = Path.Combine(clientDir, ServerNameCoreClr);
|
|
||||||
expectedPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? "dotnet";
|
|
||||||
processArguments = $@"""{expectedCompilerPath}"" server -p {pipeName}";
|
|
||||||
|
|
||||||
if (!File.Exists(expectedCompilerPath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PlatformInformation.IsWindows)
|
|
||||||
{
|
|
||||||
// As far as I can tell, there isn't a way to use the Process class to
|
|
||||||
// create a process with no stdin/stdout/stderr, so we use P/Invoke.
|
|
||||||
// This code was taken from MSBuild task starting code.
|
|
||||||
|
|
||||||
STARTUPINFO startInfo = new STARTUPINFO();
|
|
||||||
startInfo.cb = Marshal.SizeOf(startInfo);
|
|
||||||
startInfo.hStdError = InvalidIntPtr;
|
|
||||||
startInfo.hStdInput = InvalidIntPtr;
|
|
||||||
startInfo.hStdOutput = InvalidIntPtr;
|
|
||||||
startInfo.dwFlags = STARTF_USESTDHANDLES;
|
|
||||||
uint dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NO_WINDOW;
|
|
||||||
|
|
||||||
PROCESS_INFORMATION processInfo;
|
|
||||||
|
|
||||||
Log("Attempting to create process '{0}'", expectedPath);
|
|
||||||
|
|
||||||
var builder = new StringBuilder($@"""{expectedPath}"" {processArguments}");
|
|
||||||
|
|
||||||
bool success = CreateProcess(
|
|
||||||
lpApplicationName: null,
|
|
||||||
lpCommandLine: builder,
|
|
||||||
lpProcessAttributes: NullPtr,
|
|
||||||
lpThreadAttributes: NullPtr,
|
|
||||||
bInheritHandles: false,
|
|
||||||
dwCreationFlags: dwCreationFlags,
|
|
||||||
lpEnvironment: NullPtr, // Inherit environment
|
|
||||||
lpCurrentDirectory: clientDir,
|
|
||||||
lpStartupInfo: ref startInfo,
|
|
||||||
lpProcessInformation: out processInfo);
|
|
||||||
|
|
||||||
if (success)
|
|
||||||
{
|
|
||||||
Log("Successfully created process with process id {0}", processInfo.dwProcessId);
|
|
||||||
CloseHandle(processInfo.hProcess);
|
|
||||||
CloseHandle(processInfo.hThread);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Log("Failed to create process. GetLastError={0}", Marshal.GetLastWin32Error());
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var startInfo = new ProcessStartInfo()
|
|
||||||
{
|
|
||||||
FileName = expectedPath,
|
|
||||||
Arguments = processArguments,
|
|
||||||
UseShellExecute = false,
|
|
||||||
WorkingDirectory = clientDir,
|
|
||||||
RedirectStandardInput = true,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
Process.Start(startInfo);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Check to ensure that the named pipe server we connected to is owned by the same
|
|
||||||
/// user.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// The type is embedded in assemblies that need to run cross platform. While this particular
|
|
||||||
/// code will never be hit when running on non-Windows platforms it does need to work when
|
|
||||||
/// on Windows. To facilitate that we use reflection to make the check here to enable it to
|
|
||||||
/// compile into our cross plat assemblies.
|
|
||||||
/// </remarks>
|
|
||||||
private static bool CheckPipeConnectionOwnership(NamedPipeClientStream pipeStream)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if NETSTANDARD1_3
|
|
||||||
internal static bool CheckIdentityUnix(PipeStream stream)
|
|
||||||
{
|
|
||||||
// Identity verification is unavailable in the MSBuild task,
|
|
||||||
// but verification is not needed client-side so that's okay.
|
|
||||||
// (unavailable due to lack of internal reflection capabilities in netstandard1.3)
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
[DllImport("System.Native", EntryPoint = "SystemNative_GetEUid")]
|
|
||||||
private static extern uint GetEUid();
|
|
||||||
|
|
||||||
[DllImport("System.Native", EntryPoint = "SystemNative_GetPeerID", SetLastError = true)]
|
|
||||||
private static extern int GetPeerID(SafeHandle socket, out uint euid);
|
|
||||||
|
|
||||||
internal static bool CheckIdentityUnix(PipeStream stream)
|
|
||||||
{
|
|
||||||
var flags = BindingFlags.Instance | BindingFlags.NonPublic;
|
|
||||||
var handle = (SafePipeHandle)typeof(PipeStream).GetField("_handle", flags).GetValue(stream);
|
|
||||||
var handle2 = (SafeHandle)typeof(SafePipeHandle).GetField("_namedPipeSocketHandle", flags).GetValue(handle);
|
|
||||||
|
|
||||||
uint myID = GetEUid();
|
|
||||||
|
|
||||||
if (GetPeerID(handle, out uint peerID) == -1)
|
|
||||||
{
|
|
||||||
throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
||||||
}
|
|
||||||
|
|
||||||
return myID == peerID;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the value of the temporary path for the current environment assuming the working directory
|
|
||||||
/// is <paramref name="workingDir"/>. This function must emulate <see cref="Path.GetTempPath"/> as
|
|
||||||
/// closely as possible.
|
|
||||||
/// </summary>
|
|
||||||
public static string GetTempPath(string workingDir)
|
|
||||||
{
|
|
||||||
if (PlatformInformation.IsUnix)
|
|
||||||
{
|
|
||||||
// Unix temp path is fine: it does not use the working directory
|
|
||||||
// (it uses ${TMPDIR} if set, otherwise, it returns /tmp)
|
|
||||||
return Path.GetTempPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
var tmp = Environment.GetEnvironmentVariable("TMP");
|
|
||||||
if (Path.IsPathRooted(tmp))
|
|
||||||
{
|
|
||||||
return tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
var temp = Environment.GetEnvironmentVariable("TEMP");
|
|
||||||
if (Path.IsPathRooted(temp))
|
|
||||||
{
|
|
||||||
return temp;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(workingDir))
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(tmp))
|
|
||||||
{
|
|
||||||
return Path.Combine(workingDir, tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(temp))
|
|
||||||
{
|
|
||||||
return Path.Combine(workingDir, temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var userProfile = Environment.GetEnvironmentVariable("USERPROFILE");
|
|
||||||
if (Path.IsPathRooted(userProfile))
|
|
||||||
{
|
|
||||||
return userProfile;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Environment.GetEnvironmentVariable("SYSTEMROOT");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -48,8 +48,7 @@ namespace Roslyn.Utilities
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static IEnumerable<string> SplitCommandLineIntoArguments(string commandLine, bool removeHashComments)
|
public static IEnumerable<string> SplitCommandLineIntoArguments(string commandLine, bool removeHashComments)
|
||||||
{
|
{
|
||||||
char? unused;
|
return SplitCommandLineIntoArguments(commandLine, removeHashComments, out var unused);
|
||||||
return SplitCommandLineIntoArguments(commandLine, removeHashComments, out unused);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IEnumerable<string> SplitCommandLineIntoArguments(string commandLine, bool removeHashComments, out char? illegalChar)
|
public static IEnumerable<string> SplitCommandLineIntoArguments(string commandLine, bool removeHashComments, out char? illegalChar)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis
|
|
||||||
{
|
|
||||||
class CommonCompiler
|
|
||||||
{
|
|
||||||
internal const int Failed = 1;
|
|
||||||
internal const int Succeeded = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Immutable;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.CodeAnalysis.CommandLine;
|
|
||||||
|
|
||||||
using static Microsoft.CodeAnalysis.CommandLine.CompilerServerLogger;
|
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.CompilerServer
|
|
||||||
{
|
|
||||||
internal struct RunRequest
|
|
||||||
{
|
|
||||||
public string Language { get; }
|
|
||||||
public string CurrentDirectory { get; }
|
|
||||||
public string TempDirectory { get; }
|
|
||||||
public string LibDirectory { get; }
|
|
||||||
public string[] Arguments { get; }
|
|
||||||
|
|
||||||
public RunRequest(string language, string currentDirectory, string tempDirectory, string libDirectory, string[] arguments)
|
|
||||||
{
|
|
||||||
Language = language;
|
|
||||||
CurrentDirectory = currentDirectory;
|
|
||||||
TempDirectory = tempDirectory;
|
|
||||||
LibDirectory = libDirectory;
|
|
||||||
Arguments = arguments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal abstract class CompilerServerHost : ICompilerServerHost
|
|
||||||
{
|
|
||||||
public abstract IAnalyzerAssemblyLoader AnalyzerAssemblyLoader { get; }
|
|
||||||
|
|
||||||
public abstract Func<string, MetadataReferenceProperties, PortableExecutableReference> AssemblyReferenceProvider { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Directory that contains the compiler executables and the response files.
|
|
||||||
/// </summary>
|
|
||||||
public string ClientDirectory { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Directory that contains mscorlib. Can be null when the host is executing in a CoreCLR context.
|
|
||||||
/// </summary>
|
|
||||||
public string SdkDirectory { get; }
|
|
||||||
|
|
||||||
protected CompilerServerHost(string clientDirectory, string sdkDirectory)
|
|
||||||
{
|
|
||||||
ClientDirectory = clientDirectory;
|
|
||||||
SdkDirectory = sdkDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract bool CheckAnalyzers(string baseDirectory, ImmutableArray<CommandLineAnalyzerReference> analyzers);
|
|
||||||
|
|
||||||
public bool TryCreateCompiler(RunRequest request, out CommonCompiler compiler)
|
|
||||||
{
|
|
||||||
compiler = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BuildResponse RunCompilation(RunRequest request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.CodeAnalysis.CommandLine
|
||||||
internal class CompilerServerLogger
|
internal class CompilerServerLogger
|
||||||
{
|
{
|
||||||
// Environment variable, if set, to enable logging and set the file to log to.
|
// Environment variable, if set, to enable logging and set the file to log to.
|
||||||
private const string environmentVariable = "RoslynCommandLineLogFile";
|
private const string EnvironmentVariable = "RAZORBUILDSERVER_LOG";
|
||||||
|
|
||||||
private static readonly Stream s_loggingStream;
|
private static readonly Stream s_loggingStream;
|
||||||
private static string s_prefix = "---";
|
private static string s_prefix = "---";
|
||||||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.CodeAnalysis.CommandLine
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Check if the environment
|
// Check if the environment
|
||||||
string loggingFileName = Environment.GetEnvironmentVariable(environmentVariable);
|
string loggingFileName = Environment.GetEnvironmentVariable(EnvironmentVariable);
|
||||||
|
|
||||||
if (loggingFileName != null)
|
if (loggingFileName != null)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.CompilerServer
|
|
||||||
{
|
|
||||||
internal interface IDiagnosticListener
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the server updates the keep alive value.
|
|
||||||
/// </summary>
|
|
||||||
void UpdateKeepAlive(TimeSpan timeSpan);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called each time the server listens for new connections.
|
|
||||||
/// </summary>
|
|
||||||
void ConnectionListening();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when a connection to the server occurs.
|
|
||||||
/// </summary>
|
|
||||||
void ConnectionReceived();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when one or more connections have completed processing. The number of connections
|
|
||||||
/// processed is provided in <paramref name="count"/>.
|
|
||||||
/// </summary>
|
|
||||||
void ConnectionCompleted(int count);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when a bad client connection was detected and the server will be shutting down as a
|
|
||||||
/// result.
|
|
||||||
/// </summary>
|
|
||||||
void ConnectionRudelyEnded();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called when the server is shutting down because the keep alive timeout was reached.
|
|
||||||
/// </summary>
|
|
||||||
void KeepAliveReached();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed class EmptyDiagnosticListener : IDiagnosticListener
|
|
||||||
{
|
|
||||||
public void UpdateKeepAlive(TimeSpan timeSpan)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConnectionListening()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConnectionReceived()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConnectionCompleted(int count)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ConnectionRudelyEnded()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void KeepAliveReached()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.CompilerServer
|
|
||||||
{
|
|
||||||
class ICompilerServerHost
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
@ -13,10 +14,13 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
: base(parent, "server")
|
: base(parent, "server")
|
||||||
{
|
{
|
||||||
Pipe = Option("-p|--pipe", "name of named pipe", CommandOptionType.SingleValue);
|
Pipe = Option("-p|--pipe", "name of named pipe", CommandOptionType.SingleValue);
|
||||||
|
KeepAlive = Option("-k|--keep-alive", "sets the default idle timeout for the server in seconds", CommandOptionType.SingleValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public CommandOption Pipe { get; }
|
public CommandOption Pipe { get; }
|
||||||
|
|
||||||
|
public CommandOption KeepAlive { get; }
|
||||||
|
|
||||||
protected override bool ValidateArguments()
|
protected override bool ValidateArguments()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(Pipe.Value()))
|
if (string.IsNullOrEmpty(Pipe.Value()))
|
||||||
|
|
@ -41,10 +45,20 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
TimeSpan? keepAlive = null;
|
||||||
|
if (KeepAlive.HasValue())
|
||||||
|
{
|
||||||
|
var value = KeepAlive.Value();
|
||||||
|
if (int.TryParse(value, out var result))
|
||||||
|
{
|
||||||
|
// Keep alive times are specified in seconds
|
||||||
|
keepAlive = TimeSpan.FromSeconds(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var host = ConnectionHost.Create(Pipe.Value());
|
var host = ConnectionHost.Create(Pipe.Value());
|
||||||
var compilerHost = CompilerHost.Create();
|
var compilerHost = CompilerHost.Create();
|
||||||
var dispatcher = RequestDispatcher.Create(host, compilerHost, Cancelled);
|
ExecuteServerCore(host, compilerHost, Cancelled, eventBus: null, keepAlive: keepAlive);
|
||||||
dispatcher.Run();
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
@ -54,5 +68,11 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void ExecuteServerCore(ConnectionHost host, CompilerHost compilerHost, CancellationToken cancellationToken, EventBus eventBus, TimeSpan? keepAlive)
|
||||||
|
{
|
||||||
|
var dispatcher = RequestDispatcher.Create(host, compilerHost, cancellationToken, eventBus, keepAlive);
|
||||||
|
dispatcher.Run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a Response from the server. A response is as follows.
|
||||||
|
///
|
||||||
|
/// Field Name Type Size (bytes)
|
||||||
|
/// --------------------------------------------------
|
||||||
|
/// Length UInteger 4
|
||||||
|
/// ReturnCode Integer 4
|
||||||
|
/// Output String Variable
|
||||||
|
/// ErrorOutput String Variable
|
||||||
|
///
|
||||||
|
/// Strings are encoded via a character count prefix as a
|
||||||
|
/// 32-bit integer, followed by an array of characters.
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CompletedServerResponse : ServerResponse
|
||||||
|
{
|
||||||
|
public readonly int ReturnCode;
|
||||||
|
public readonly bool Utf8Output;
|
||||||
|
public readonly string Output;
|
||||||
|
public readonly string ErrorOutput;
|
||||||
|
|
||||||
|
public CompletedServerResponse(int returnCode, bool utf8output, string output)
|
||||||
|
{
|
||||||
|
ReturnCode = returnCode;
|
||||||
|
Utf8Output = utf8output;
|
||||||
|
Output = output;
|
||||||
|
|
||||||
|
// This field existed to support writing to Console.Error. The compiler doesn't ever write to
|
||||||
|
// this field or Console.Error. This field is only kept around in order to maintain the existing
|
||||||
|
// protocol semantics.
|
||||||
|
ErrorOutput = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ResponseType Type => ResponseType.Completed;
|
||||||
|
|
||||||
|
public static CompletedServerResponse Create(BinaryReader reader)
|
||||||
|
{
|
||||||
|
var returnCode = reader.ReadInt32();
|
||||||
|
var utf8Output = reader.ReadBoolean();
|
||||||
|
var output = ServerProtocol.ReadLengthPrefixedString(reader);
|
||||||
|
var errorOutput = ServerProtocol.ReadLengthPrefixedString(reader);
|
||||||
|
if (!string.IsNullOrEmpty(errorOutput))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CompletedServerResponse(returnCode, utf8Output, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddResponseBody(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(ReturnCode);
|
||||||
|
writer.Write(Utf8Output);
|
||||||
|
ServerProtocol.WriteLengthPrefixedString(writer, Output);
|
||||||
|
ServerProtocol.WriteLengthPrefixedString(writer, ErrorOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal sealed class MismatchedVersionServerResponse : ServerResponse
|
||||||
|
{
|
||||||
|
public override ResponseType Type => ResponseType.MismatchedVersion;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MismatchedVersion has no body.
|
||||||
|
/// </summary>
|
||||||
|
protected override void AddResponseBody(BinaryWriter writer) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace Microsoft.CodeAnalysis.CommandLine
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
internal struct STARTUPINFO
|
internal struct STARTUPINFO
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal sealed class RejectedServerResponse : ServerResponse
|
||||||
|
{
|
||||||
|
public override ResponseType Type => ResponseType.Rejected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// RejectedResponse has no body.
|
||||||
|
/// </summary>
|
||||||
|
protected override void AddResponseBody(BinaryWriter writer) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A command line argument to the compilation.
|
||||||
|
/// An argument is formatted as follows:
|
||||||
|
///
|
||||||
|
/// Field Name Type Size (bytes)
|
||||||
|
/// --------------------------------------------------
|
||||||
|
/// ID UInteger 4
|
||||||
|
/// Index UInteger 4
|
||||||
|
/// Value String Variable
|
||||||
|
///
|
||||||
|
/// Strings are encoded via a length prefix as a signed
|
||||||
|
/// 32-bit integer, followed by an array of characters.
|
||||||
|
/// </summary>
|
||||||
|
internal struct RequestArgument
|
||||||
|
{
|
||||||
|
public readonly ArgumentId Id;
|
||||||
|
public readonly int ArgumentIndex;
|
||||||
|
public readonly string Value;
|
||||||
|
|
||||||
|
public RequestArgument(ArgumentId argumentId, int argumentIndex, string value)
|
||||||
|
{
|
||||||
|
Id = argumentId;
|
||||||
|
ArgumentIndex = argumentIndex;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RequestArgument ReadFromBinaryReader(BinaryReader reader)
|
||||||
|
{
|
||||||
|
var argId = (ArgumentId)reader.ReadInt32();
|
||||||
|
var argIndex = reader.ReadInt32();
|
||||||
|
var value = ServerProtocol.ReadLengthPrefixedString(reader);
|
||||||
|
return new RequestArgument(argId, argIndex, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteToBinaryWriter(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write((int)Id);
|
||||||
|
writer.Write(ArgumentIndex);
|
||||||
|
ServerProtocol.WriteLengthPrefixedString(writer, Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ArgumentId
|
||||||
|
{
|
||||||
|
// The current directory of the client
|
||||||
|
CurrentDirectory = 0x51147221,
|
||||||
|
|
||||||
|
// A comment line argument. The argument index indicates which one (0 .. N)
|
||||||
|
CommandLineArgument,
|
||||||
|
|
||||||
|
// Request a longer keep alive time for the server
|
||||||
|
KeepAlive,
|
||||||
|
|
||||||
|
// Request a server shutdown from the client
|
||||||
|
Shutdown,
|
||||||
|
|
||||||
|
// The directory to use for temporary operations.
|
||||||
|
TempDirectory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,330 @@
|
||||||
|
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Roslyn.Utilities;
|
||||||
|
using static Microsoft.CodeAnalysis.CommandLine.CompilerServerLogger;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal static class ServerConnection
|
||||||
|
{
|
||||||
|
private const string ServerName = "rzc.dll";
|
||||||
|
|
||||||
|
// Spend up to 1s connecting to existing process (existing processes should be always responsive).
|
||||||
|
private const int TimeOutMsExistingProcess = 1000;
|
||||||
|
|
||||||
|
// Spend up to 20s connecting to a new process, to allow time for it to start.
|
||||||
|
private const int TimeOutMsNewProcess = 20000;
|
||||||
|
|
||||||
|
public static bool WasServerMutexOpen(string mutexName)
|
||||||
|
{
|
||||||
|
var open = Mutex.TryOpenExisting(mutexName, out var mutex);
|
||||||
|
if (open)
|
||||||
|
{
|
||||||
|
mutex.Dispose();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the value of the temporary path for the current environment assuming the working directory
|
||||||
|
/// is <paramref name="workingDir"/>. This function must emulate <see cref="Path.GetTempPath"/> as
|
||||||
|
/// closely as possible.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetTempPath(string workingDir)
|
||||||
|
{
|
||||||
|
if (PlatformInformation.IsUnix)
|
||||||
|
{
|
||||||
|
// Unix temp path is fine: it does not use the working directory
|
||||||
|
// (it uses ${TMPDIR} if set, otherwise, it returns /tmp)
|
||||||
|
return Path.GetTempPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmp = Environment.GetEnvironmentVariable("TMP");
|
||||||
|
if (Path.IsPathRooted(tmp))
|
||||||
|
{
|
||||||
|
return tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var temp = Environment.GetEnvironmentVariable("TEMP");
|
||||||
|
if (Path.IsPathRooted(temp))
|
||||||
|
{
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(workingDir))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(tmp))
|
||||||
|
{
|
||||||
|
return Path.Combine(workingDir, tmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(temp))
|
||||||
|
{
|
||||||
|
return Path.Combine(workingDir, temp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var userProfile = Environment.GetEnvironmentVariable("USERPROFILE");
|
||||||
|
if (Path.IsPathRooted(userProfile))
|
||||||
|
{
|
||||||
|
return userProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Environment.GetEnvironmentVariable("SYSTEMROOT");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Task<ServerResponse> RunOnServer(
|
||||||
|
List<string> arguments,
|
||||||
|
ServerPaths buildPaths,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
string keepAlive = null)
|
||||||
|
{
|
||||||
|
var pipeName = PipeName.ComputeDefault();
|
||||||
|
|
||||||
|
return RunOnServerCore(
|
||||||
|
arguments,
|
||||||
|
buildPaths,
|
||||||
|
pipeName: pipeName,
|
||||||
|
keepAlive: keepAlive,
|
||||||
|
timeoutOverride: null,
|
||||||
|
tryCreateServerFunc: TryCreateServerCore,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<ServerResponse> RunOnServerCore(
|
||||||
|
List<string> arguments,
|
||||||
|
ServerPaths buildPaths,
|
||||||
|
string pipeName,
|
||||||
|
string keepAlive,
|
||||||
|
int? timeoutOverride,
|
||||||
|
Func<string, string, bool> tryCreateServerFunc,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (pipeName == null)
|
||||||
|
{
|
||||||
|
return new RejectedServerResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildPaths.TempDirectory == null)
|
||||||
|
{
|
||||||
|
return new RejectedServerResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientDir = buildPaths.ClientDirectory;
|
||||||
|
var timeoutNewProcess = timeoutOverride ?? TimeOutMsNewProcess;
|
||||||
|
var timeoutExistingProcess = timeoutOverride ?? TimeOutMsExistingProcess;
|
||||||
|
var clientMutexName = MutexName.GetClientMutexName(pipeName);
|
||||||
|
Task<Client> pipeTask = null;
|
||||||
|
using (var clientMutex = new Mutex(initiallyOwned: true, name: clientMutexName, createdNew: out var holdsMutex))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!holdsMutex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
holdsMutex = clientMutex.WaitOne(timeoutNewProcess);
|
||||||
|
|
||||||
|
if (!holdsMutex)
|
||||||
|
{
|
||||||
|
return new RejectedServerResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (AbandonedMutexException)
|
||||||
|
{
|
||||||
|
holdsMutex = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for an already running server
|
||||||
|
var serverMutexName = MutexName.GetServerMutexName(pipeName);
|
||||||
|
var wasServerRunning = WasServerMutexOpen(serverMutexName);
|
||||||
|
var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess;
|
||||||
|
|
||||||
|
if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName))
|
||||||
|
{
|
||||||
|
pipeTask = Client.ConnectAsync(pipeName, TimeSpan.FromMilliseconds(timeout), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (holdsMutex)
|
||||||
|
{
|
||||||
|
clientMutex.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pipeTask != null)
|
||||||
|
{
|
||||||
|
var client = await pipeTask.ConfigureAwait(false);
|
||||||
|
if (client != null)
|
||||||
|
{
|
||||||
|
var request = ServerRequest.Create(
|
||||||
|
buildPaths.WorkingDirectory,
|
||||||
|
buildPaths.TempDirectory,
|
||||||
|
arguments,
|
||||||
|
keepAlive);
|
||||||
|
|
||||||
|
return await TryProcessRequest(client, request, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RejectedServerResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to process the request using the server. Returns a null-containing Task if a response
|
||||||
|
/// from the server cannot be retrieved.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<ServerResponse> TryProcessRequest(
|
||||||
|
Client client,
|
||||||
|
ServerRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
ServerResponse response;
|
||||||
|
using (client)
|
||||||
|
{
|
||||||
|
// Write the request
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Log("Begin writing request");
|
||||||
|
await request.WriteAsync(client.Stream, cancellationToken).ConfigureAwait(false);
|
||||||
|
Log("End writing request");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogException(e, "Error writing build request.");
|
||||||
|
return new RejectedServerResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the compilation and a monitor to detect if the server disconnects
|
||||||
|
var serverCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||||
|
|
||||||
|
Log("Begin reading response");
|
||||||
|
|
||||||
|
var responseTask = ServerResponse.ReadAsync(client.Stream, serverCts.Token);
|
||||||
|
var monitorTask = client.WaitForDisconnectAsync(serverCts.Token);
|
||||||
|
await Task.WhenAny(responseTask, monitorTask).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Log("End reading response");
|
||||||
|
|
||||||
|
if (responseTask.IsCompleted)
|
||||||
|
{
|
||||||
|
// await the task to log any exceptions
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response = await responseTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
LogException(e, "Error reading response");
|
||||||
|
response = new RejectedServerResponse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("Server disconnect");
|
||||||
|
response = new RejectedServerResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel whatever task is still around
|
||||||
|
serverCts.Cancel();
|
||||||
|
Debug.Assert(response != null);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCreateServerCore(string clientDir, string pipeName)
|
||||||
|
{
|
||||||
|
string expectedPath;
|
||||||
|
string processArguments;
|
||||||
|
|
||||||
|
// The server should be in the same directory as the client
|
||||||
|
var expectedCompilerPath = Path.Combine(clientDir, ServerName);
|
||||||
|
expectedPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? "dotnet";
|
||||||
|
processArguments = $@"""{expectedCompilerPath}"" server -p {pipeName}";
|
||||||
|
|
||||||
|
if (!File.Exists(expectedCompilerPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PlatformInformation.IsWindows)
|
||||||
|
{
|
||||||
|
// As far as I can tell, there isn't a way to use the Process class to
|
||||||
|
// create a process with no stdin/stdout/stderr, so we use P/Invoke.
|
||||||
|
// This code was taken from MSBuild task starting code.
|
||||||
|
|
||||||
|
var startInfo = new STARTUPINFO();
|
||||||
|
startInfo.cb = Marshal.SizeOf(startInfo);
|
||||||
|
startInfo.hStdError = NativeMethods.InvalidIntPtr;
|
||||||
|
startInfo.hStdInput = NativeMethods.InvalidIntPtr;
|
||||||
|
startInfo.hStdOutput = NativeMethods.InvalidIntPtr;
|
||||||
|
startInfo.dwFlags = NativeMethods.STARTF_USESTDHANDLES;
|
||||||
|
var dwCreationFlags = NativeMethods.NORMAL_PRIORITY_CLASS | NativeMethods.CREATE_NO_WINDOW;
|
||||||
|
|
||||||
|
Log("Attempting to create process '{0}'", expectedPath);
|
||||||
|
|
||||||
|
var builder = new StringBuilder($@"""{expectedPath}"" {processArguments}");
|
||||||
|
|
||||||
|
var success = NativeMethods.CreateProcess(
|
||||||
|
lpApplicationName: null,
|
||||||
|
lpCommandLine: builder,
|
||||||
|
lpProcessAttributes: NativeMethods.NullPtr,
|
||||||
|
lpThreadAttributes: NativeMethods.NullPtr,
|
||||||
|
bInheritHandles: false,
|
||||||
|
dwCreationFlags: dwCreationFlags,
|
||||||
|
lpEnvironment: NativeMethods.NullPtr, // Inherit environment
|
||||||
|
lpCurrentDirectory: clientDir,
|
||||||
|
lpStartupInfo: ref startInfo,
|
||||||
|
lpProcessInformation: out var processInfo);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Log("Successfully created process with process id {0}", processInfo.dwProcessId);
|
||||||
|
NativeMethods.CloseHandle(processInfo.hProcess);
|
||||||
|
NativeMethods.CloseHandle(processInfo.hThread);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log("Failed to create process. GetLastError={0}", Marshal.GetLastWin32Error());
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var startInfo = new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
FileName = expectedPath,
|
||||||
|
Arguments = processArguments,
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = clientDir,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Process.Start(startInfo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal struct ServerPaths
|
||||||
|
{
|
||||||
|
internal ServerPaths(string clientDir, string workingDir, string tempDir)
|
||||||
|
{
|
||||||
|
ClientDirectory = clientDir;
|
||||||
|
WorkingDirectory = workingDir;
|
||||||
|
TempDirectory = tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path which contains the Razor compiler binaries and response files.
|
||||||
|
/// </summary>
|
||||||
|
internal string ClientDirectory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path in which the Razor compilation takes place.
|
||||||
|
/// </summary>
|
||||||
|
internal string WorkingDirectory { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The temporary directory a compilation should use instead of <see cref="Path.GetTempPath"/>. The latter
|
||||||
|
/// relies on global state individual compilations should ignore.
|
||||||
|
/// </summary>
|
||||||
|
internal string TempDirectory { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis.CommandLine;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal static class ServerProtocol
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The version number for this protocol.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly uint ProtocolVersion = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read a string from the Reader where the string is encoded
|
||||||
|
/// as a length prefix (signed 32-bit integer) followed by
|
||||||
|
/// a sequence of characters.
|
||||||
|
/// </summary>
|
||||||
|
public static string ReadLengthPrefixedString(BinaryReader reader)
|
||||||
|
{
|
||||||
|
var length = reader.ReadInt32();
|
||||||
|
return new string(reader.ReadChars(length));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write a string to the Writer where the string is encoded
|
||||||
|
/// as a length prefix (signed 32-bit integer) follows by
|
||||||
|
/// a sequence of characters.
|
||||||
|
/// </summary>
|
||||||
|
public static void WriteLengthPrefixedString(BinaryWriter writer, string value)
|
||||||
|
{
|
||||||
|
writer.Write(value.Length);
|
||||||
|
writer.Write(value.ToCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This task does not complete until we are completely done reading.
|
||||||
|
/// </summary>
|
||||||
|
internal static async Task ReadAllAsync(
|
||||||
|
Stream stream,
|
||||||
|
byte[] buffer,
|
||||||
|
int count,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var totalBytesRead = 0;
|
||||||
|
do
|
||||||
|
{
|
||||||
|
CompilerServerLogger.Log("Attempting to read {0} bytes from the stream", count - totalBytesRead);
|
||||||
|
var bytesRead = await stream.ReadAsync(
|
||||||
|
buffer,
|
||||||
|
totalBytesRead,
|
||||||
|
count - totalBytesRead,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (bytesRead == 0)
|
||||||
|
{
|
||||||
|
CompilerServerLogger.Log("Unexpected -- read 0 bytes from the stream.");
|
||||||
|
throw new EndOfStreamException("Reached end of stream before end of read.");
|
||||||
|
}
|
||||||
|
CompilerServerLogger.Log("Read {0} bytes", bytesRead);
|
||||||
|
totalBytesRead += bytesRead;
|
||||||
|
} while (totalBytesRead < count);
|
||||||
|
CompilerServerLogger.Log("Finished read");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static Microsoft.CodeAnalysis.CommandLine.CompilerServerLogger;
|
||||||
|
|
||||||
|
// After the server pipe is connected, it forks off a thread to handle the connection, and creates
|
||||||
|
// a new instance of the pipe to listen for new clients. When it gets a request, it validates
|
||||||
|
// the security and elevation level of the client. If that fails, it disconnects the client. Otherwise,
|
||||||
|
// it handles the request, sends a response (described by Response class) back to the client, then
|
||||||
|
// disconnects the pipe and ends the thread.
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a request from the client. A request is as follows.
|
||||||
|
///
|
||||||
|
/// Field Name Type Size (bytes)
|
||||||
|
/// ----------------------------------------------------
|
||||||
|
/// Length Integer 4
|
||||||
|
/// Argument Count UInteger 4
|
||||||
|
/// Arguments Argument[] Variable
|
||||||
|
///
|
||||||
|
/// See <see cref="RequestArgument"/> for the format of an
|
||||||
|
/// Argument.
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
internal class ServerRequest
|
||||||
|
{
|
||||||
|
public ServerRequest(uint protocolVersion, IEnumerable<RequestArgument> arguments)
|
||||||
|
{
|
||||||
|
ProtocolVersion = protocolVersion;
|
||||||
|
Arguments = new ReadOnlyCollection<RequestArgument>(arguments.ToList());
|
||||||
|
|
||||||
|
if (Arguments.Count > ushort.MaxValue)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(arguments),
|
||||||
|
$"Too many arguments: maximum of {ushort.MaxValue} arguments allowed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint ProtocolVersion { get; }
|
||||||
|
|
||||||
|
public ReadOnlyCollection<RequestArgument> Arguments { get; }
|
||||||
|
|
||||||
|
public TimeSpan? KeepAlive
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
TimeSpan? keepAlive = null;
|
||||||
|
foreach (var argument in Arguments)
|
||||||
|
{
|
||||||
|
if (argument.Id == RequestArgument.ArgumentId.KeepAlive)
|
||||||
|
{
|
||||||
|
// If the value is not a valid integer for any reason, ignore it and continue with the current timeout.
|
||||||
|
// The client is responsible for validating the argument.
|
||||||
|
if (int.TryParse(argument.Value, out var result))
|
||||||
|
{
|
||||||
|
// Keep alive times are specified in seconds
|
||||||
|
keepAlive = TimeSpan.FromSeconds(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keepAlive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsShutdownRequest()
|
||||||
|
{
|
||||||
|
return Arguments.Count >= 1 && Arguments[0].Id == RequestArgument.ArgumentId.Shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerRequest Create(
|
||||||
|
string workingDirectory,
|
||||||
|
string tempDirectory,
|
||||||
|
IList<string> args,
|
||||||
|
string keepAlive = null)
|
||||||
|
{
|
||||||
|
Log("Creating ServerRequest");
|
||||||
|
Log($"Working directory: {workingDirectory}");
|
||||||
|
Log($"Temp directory: {tempDirectory}");
|
||||||
|
|
||||||
|
var requestLength = args.Count + 1;
|
||||||
|
var requestArgs = new List<RequestArgument>(requestLength)
|
||||||
|
{
|
||||||
|
new RequestArgument(RequestArgument.ArgumentId.CurrentDirectory, 0, workingDirectory),
|
||||||
|
new RequestArgument(RequestArgument.ArgumentId.TempDirectory, 0, tempDirectory)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (keepAlive != null)
|
||||||
|
{
|
||||||
|
requestArgs.Add(new RequestArgument(RequestArgument.ArgumentId.KeepAlive, 0, keepAlive));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < args.Count; ++i)
|
||||||
|
{
|
||||||
|
var arg = args[i];
|
||||||
|
Log($"argument[{i}] = {arg}");
|
||||||
|
requestArgs.Add(new RequestArgument(RequestArgument.ArgumentId.CommandLineArgument, i, arg));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerRequest(ServerProtocol.ProtocolVersion, requestArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ServerRequest CreateShutdown()
|
||||||
|
{
|
||||||
|
var requestArgs = new[]
|
||||||
|
{
|
||||||
|
new RequestArgument(RequestArgument.ArgumentId.Shutdown, argumentIndex: 0, value: ""),
|
||||||
|
new RequestArgument(RequestArgument.ArgumentId.CommandLineArgument, argumentIndex: 1, value: "shutdown"),
|
||||||
|
};
|
||||||
|
return new ServerRequest(ServerProtocol.ProtocolVersion, requestArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read a Request from the given stream.
|
||||||
|
///
|
||||||
|
/// The total request size must be less than 1MB.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>null if the Request was too large, the Request otherwise.</returns>
|
||||||
|
public static async Task<ServerRequest> ReadAsync(Stream inStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Read the length of the request
|
||||||
|
var lengthBuffer = new byte[4];
|
||||||
|
Log("Reading length of request");
|
||||||
|
await ServerProtocol.ReadAllAsync(inStream, lengthBuffer, 4, cancellationToken).ConfigureAwait(false);
|
||||||
|
var length = BitConverter.ToInt32(lengthBuffer, 0);
|
||||||
|
|
||||||
|
// Back out if the request is > 1MB
|
||||||
|
if (length > 0x100000)
|
||||||
|
{
|
||||||
|
Log("Request is over 1MB in length, cancelling read.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Read the full request
|
||||||
|
var requestBuffer = new byte[length];
|
||||||
|
await ServerProtocol.ReadAllAsync(inStream, requestBuffer, length, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Log("Parsing request");
|
||||||
|
// Parse the request into the Request data structure.
|
||||||
|
using (var reader = new BinaryReader(new MemoryStream(requestBuffer), Encoding.Unicode))
|
||||||
|
{
|
||||||
|
var protocolVersion = reader.ReadUInt32();
|
||||||
|
var argumentCount = reader.ReadUInt32();
|
||||||
|
|
||||||
|
var argumentsBuilder = new List<RequestArgument>((int)argumentCount);
|
||||||
|
|
||||||
|
for (var i = 0; i < argumentCount; i++)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
argumentsBuilder.Add(RequestArgument.ReadFromBinaryReader(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerRequest(protocolVersion, argumentsBuilder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write a Request to the stream.
|
||||||
|
/// </summary>
|
||||||
|
public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode))
|
||||||
|
{
|
||||||
|
// Format the request.
|
||||||
|
Log("Formatting request");
|
||||||
|
writer.Write(ProtocolVersion);
|
||||||
|
writer.Write(Arguments.Count);
|
||||||
|
foreach (var arg in Arguments)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
arg.WriteToBinaryWriter(writer);
|
||||||
|
}
|
||||||
|
writer.Flush();
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Write the length of the request
|
||||||
|
var length = checked((int)memoryStream.Length);
|
||||||
|
|
||||||
|
// Back out if the request is > 1 MB
|
||||||
|
if (memoryStream.Length > 0x100000)
|
||||||
|
{
|
||||||
|
Log("Request is over 1MB in length, cancelling write");
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request to the server
|
||||||
|
Log("Writing length of request.");
|
||||||
|
await outStream
|
||||||
|
.WriteAsync(BitConverter.GetBytes(length), 0, 4, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
Log("Writing request of size {0}", length);
|
||||||
|
// Write the request
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
await memoryStream
|
||||||
|
.CopyToAsync(outStream, bufferSize: length, cancellationToken: cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,133 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using static Microsoft.CodeAnalysis.CommandLine.CompilerServerLogger;
|
||||||
|
|
||||||
|
// After the server pipe is connected, it forks off a thread to handle the connection, and creates
|
||||||
|
// a new instance of the pipe to listen for new clients. When it gets a request, it validates
|
||||||
|
// the security and elevation level of the client. If that fails, it disconnects the client. Otherwise,
|
||||||
|
// it handles the request, sends a response (described by Response class) back to the client, then
|
||||||
|
// disconnects the pipe and ends the thread.
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for all possible responses to a request.
|
||||||
|
/// The ResponseType enum should list all possible response types
|
||||||
|
/// and ReadResponse creates the appropriate response subclass based
|
||||||
|
/// on the response type sent by the client.
|
||||||
|
/// The format of a response is:
|
||||||
|
///
|
||||||
|
/// Field Name Field Type Size (bytes)
|
||||||
|
/// -------------------------------------------------
|
||||||
|
/// responseLength int (positive) 4
|
||||||
|
/// responseType enum ResponseType 4
|
||||||
|
/// responseBody Response subclass variable
|
||||||
|
/// </summary>
|
||||||
|
internal abstract class ServerResponse
|
||||||
|
{
|
||||||
|
public enum ResponseType
|
||||||
|
{
|
||||||
|
// The client and server are using incompatible protocol versions.
|
||||||
|
MismatchedVersion,
|
||||||
|
|
||||||
|
// The build request completed on the server and the results are contained
|
||||||
|
// in the message.
|
||||||
|
Completed,
|
||||||
|
|
||||||
|
// The shutdown request completed and the server process information is
|
||||||
|
// contained in the message.
|
||||||
|
Shutdown,
|
||||||
|
|
||||||
|
// The request was rejected by the server.
|
||||||
|
Rejected,
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract ResponseType Type { get; }
|
||||||
|
|
||||||
|
public async Task WriteAsync(Stream outStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
using (var memoryStream = new MemoryStream())
|
||||||
|
using (var writer = new BinaryWriter(memoryStream, Encoding.Unicode))
|
||||||
|
{
|
||||||
|
// Format the response
|
||||||
|
Log("Formatting Response");
|
||||||
|
writer.Write((int)Type);
|
||||||
|
|
||||||
|
AddResponseBody(writer);
|
||||||
|
writer.Flush();
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Send the response to the client
|
||||||
|
|
||||||
|
// Write the length of the response
|
||||||
|
var length = checked((int)memoryStream.Length);
|
||||||
|
|
||||||
|
Log("Writing response length");
|
||||||
|
// There is no way to know the number of bytes written to
|
||||||
|
// the pipe stream. We just have to assume all of them are written.
|
||||||
|
await outStream
|
||||||
|
.WriteAsync(BitConverter.GetBytes(length), 0, 4, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Write the response
|
||||||
|
Log("Writing response of size {0}", length);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
await memoryStream
|
||||||
|
.CopyToAsync(outStream, bufferSize: length, cancellationToken: cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void AddResponseBody(BinaryWriter writer);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// May throw exceptions if there are pipe problems.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream"></param>
|
||||||
|
/// <param name="cancellationToken"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static async Task<ServerResponse> ReadAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken))
|
||||||
|
{
|
||||||
|
Log("Reading response length");
|
||||||
|
// Read the response length
|
||||||
|
var lengthBuffer = new byte[4];
|
||||||
|
await ServerProtocol.ReadAllAsync(stream, lengthBuffer, 4, cancellationToken).ConfigureAwait(false);
|
||||||
|
var length = BitConverter.ToUInt32(lengthBuffer, 0);
|
||||||
|
|
||||||
|
// Read the response
|
||||||
|
Log("Reading response of length {0}", length);
|
||||||
|
var responseBuffer = new byte[length];
|
||||||
|
await ServerProtocol.ReadAllAsync(
|
||||||
|
stream,
|
||||||
|
responseBuffer,
|
||||||
|
responseBuffer.Length,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
|
using (var reader = new BinaryReader(new MemoryStream(responseBuffer), Encoding.Unicode))
|
||||||
|
{
|
||||||
|
var responseType = (ResponseType)reader.ReadInt32();
|
||||||
|
|
||||||
|
switch (responseType)
|
||||||
|
{
|
||||||
|
case ResponseType.Completed:
|
||||||
|
return CompletedServerResponse.Create(reader);
|
||||||
|
case ResponseType.MismatchedVersion:
|
||||||
|
return new MismatchedVersionServerResponse();
|
||||||
|
case ResponseType.Shutdown:
|
||||||
|
return ShutdownServerResponse.Create(reader);
|
||||||
|
case ResponseType.Rejected:
|
||||||
|
return new RejectedServerResponse();
|
||||||
|
default:
|
||||||
|
throw new InvalidOperationException("Received invalid response type from server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal sealed class ShutdownServerResponse : ServerResponse
|
||||||
|
{
|
||||||
|
public readonly int ServerProcessId;
|
||||||
|
|
||||||
|
public ShutdownServerResponse(int serverProcessId)
|
||||||
|
{
|
||||||
|
ServerProcessId = serverProcessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override ResponseType Type => ResponseType.Shutdown;
|
||||||
|
|
||||||
|
protected override void AddResponseBody(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(ServerProcessId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ShutdownServerResponse Create(BinaryReader reader)
|
||||||
|
{
|
||||||
|
var serverProcessId = reader.ReadInt32();
|
||||||
|
return new ShutdownServerResponse(serverProcessId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.CodeAnalysis.CommandLine;
|
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Razor.Tools
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
|
@ -46,10 +45,10 @@ namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
{
|
{
|
||||||
using (var client = await Client.ConnectAsync(Pipe.Value(), timeout: null, cancellationToken: Cancelled))
|
using (var client = await Client.ConnectAsync(Pipe.Value(), timeout: null, cancellationToken: Cancelled))
|
||||||
{
|
{
|
||||||
var request = BuildRequest.CreateShutdown();
|
var request = ServerRequest.CreateShutdown();
|
||||||
await request.WriteAsync(client.Stream, Cancelled).ConfigureAwait(false);
|
await request.WriteAsync(client.Stream, Cancelled).ConfigureAwait(false);
|
||||||
|
|
||||||
var response = ((ShutdownBuildResponse)await BuildResponse.ReadAsync(client.Stream, Cancelled));
|
var response = ((ShutdownServerResponse)await ServerResponse.ReadAsync(client.Stream, Cancelled));
|
||||||
|
|
||||||
if (Wait.HasValue())
|
if (Wait.HasValue())
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,551 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
public class DefaultRequestDispatcherTest
|
||||||
|
{
|
||||||
|
private static ServerRequest EmptyServerRequest => new ServerRequest(1, Array.Empty<RequestArgument>());
|
||||||
|
|
||||||
|
private static ServerResponse EmptyServerResponse => new CompletedServerResponse(
|
||||||
|
returnCode: 0,
|
||||||
|
utf8output: false,
|
||||||
|
output: string.Empty);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptConnection_ReadingRequestFails_ClosesConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stream = Mock.Of<Stream>();
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var connectionHost = CreateConnectionHost();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None);
|
||||||
|
var connection = CreateConnection(stream);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await dispatcher.AcceptConnection(
|
||||||
|
Task.FromResult<Connection>(connection), accept: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ConnectionResult.Reason.CompilationNotStarted, result.CloseReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A failure to write the results to the client is considered a client disconnection. Any error
|
||||||
|
/// from when the build starts to when the write completes should be handled this way.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptConnection_WritingResultsFails_ClosesConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
await EmptyServerRequest.WriteAsync(memoryStream, CancellationToken.None).ConfigureAwait(true);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
|
||||||
|
var stream = new Mock<Stream>(MockBehavior.Strict);
|
||||||
|
stream
|
||||||
|
.Setup(x => x.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||||
|
.Returns((byte[] array, int start, int length, CancellationToken ct) => memoryStream.ReadAsync(array, start, length, ct));
|
||||||
|
|
||||||
|
var connection = CreateConnection(stream.Object);
|
||||||
|
var compilerHost = CreateCompilerHost(c =>
|
||||||
|
{
|
||||||
|
c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
return EmptyServerResponse;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var connectionHost = CreateConnectionHost();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// We expect WriteAsync to fail because the mock stream doesn't have a corresponding setup.
|
||||||
|
var connectionResult = await dispatcher.AcceptConnection(
|
||||||
|
Task.FromResult<Connection>(connection), accept: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ConnectionResult.Reason.ClientDisconnect, connectionResult.CloseReason);
|
||||||
|
Assert.Null(connectionResult.KeepAlive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure the Connection correctly handles the case where a client disconnects while in the
|
||||||
|
/// middle of executing a request.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptConnection_ClientDisconnectsWhenExecutingRequest_ClosesConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connectionHost = Mock.Of<ConnectionHost>();
|
||||||
|
|
||||||
|
// Fake a long running task here that we can validate later on.
|
||||||
|
var buildTaskSource = new TaskCompletionSource<bool>();
|
||||||
|
var buildTaskCancellationToken = default(CancellationToken);
|
||||||
|
var compilerHost = CreateCompilerHost(c =>
|
||||||
|
{
|
||||||
|
c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
Task.WaitAll(buildTaskSource.Task);
|
||||||
|
return EmptyServerResponse;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None);
|
||||||
|
var readyTaskSource = new TaskCompletionSource<bool>();
|
||||||
|
var disconnectTaskSource = new TaskCompletionSource<bool>();
|
||||||
|
var connectionTask = CreateConnectionWithEmptyServerRequest(c =>
|
||||||
|
{
|
||||||
|
c.WaitForDisconnectAsyncFunc = (ct) =>
|
||||||
|
{
|
||||||
|
buildTaskCancellationToken = ct;
|
||||||
|
readyTaskSource.SetResult(true);
|
||||||
|
return disconnectTaskSource.Task;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var handleTask = dispatcher.AcceptConnection(
|
||||||
|
connectionTask, accept: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Wait until WaitForDisconnectAsync task is actually created and running.
|
||||||
|
await readyTaskSource.Task.ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// Now simulate a disconnect by the client.
|
||||||
|
disconnectTaskSource.SetResult(true);
|
||||||
|
var connectionResult = await handleTask;
|
||||||
|
buildTaskSource.SetResult(true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ConnectionResult.Reason.ClientDisconnect, connectionResult.CloseReason);
|
||||||
|
Assert.Null(connectionResult.KeepAlive);
|
||||||
|
Assert.True(buildTaskCancellationToken.IsCancellationRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptConnection_AcceptFalse_RejectsBuildRequest()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stream = new TestableStream();
|
||||||
|
await EmptyServerRequest.WriteAsync(stream.ReadStream, CancellationToken.None);
|
||||||
|
stream.ReadStream.Position = 0;
|
||||||
|
|
||||||
|
var connection = CreateConnection(stream);
|
||||||
|
var connectionHost = CreateConnectionHost();
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connectionResult = await dispatcher.AcceptConnection(
|
||||||
|
Task.FromResult<Connection>(connection), accept: false, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ConnectionResult.Reason.CompilationNotStarted, connectionResult.CloseReason);
|
||||||
|
stream.WriteStream.Position = 0;
|
||||||
|
var response = await ServerResponse.ReadAsync(stream.WriteStream).ConfigureAwait(false);
|
||||||
|
Assert.Equal(ServerResponse.ResponseType.Rejected, response.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptConnection_ShutdownRequest_ReturnsShutdownResponse()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var stream = new TestableStream();
|
||||||
|
await ServerRequest.CreateShutdown().WriteAsync(stream.ReadStream, CancellationToken.None);
|
||||||
|
stream.ReadStream.Position = 0;
|
||||||
|
|
||||||
|
var connection = CreateConnection(stream);
|
||||||
|
var connectionHost = CreateConnectionHost();
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connectionResult = await dispatcher.AcceptConnection(
|
||||||
|
Task.FromResult<Connection>(connection), accept: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ConnectionResult.Reason.ClientShutdownRequest, connectionResult.CloseReason);
|
||||||
|
stream.WriteStream.Position = 0;
|
||||||
|
var response = await ServerResponse.ReadAsync(stream.WriteStream).ConfigureAwait(false);
|
||||||
|
Assert.Equal(ServerResponse.ResponseType.Shutdown, response.Type);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptConnection_ConnectionHostThrowsWhenConnecting_ClosesConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connectionHost = new Mock<ConnectionHost>(MockBehavior.Strict);
|
||||||
|
connectionHost.Setup(c => c.WaitForConnectionAsync(It.IsAny<CancellationToken>())).Throws(new Exception());
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost.Object, compilerHost, CancellationToken.None);
|
||||||
|
var connection = CreateConnection(Mock.Of<Stream>());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connectionResult = await dispatcher.AcceptConnection(
|
||||||
|
Task.FromResult<Connection>(connection), accept: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ConnectionResult.Reason.CompilationNotStarted, connectionResult.CloseReason);
|
||||||
|
Assert.Null(connectionResult.KeepAlive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AcceptConnection_ClientConnectionThrowsWhenConnecting_ClosesConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var connectionHost = CreateConnectionHost();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None);
|
||||||
|
var connectionTask = Task.FromException<Connection>(new Exception());
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connectionResult = await dispatcher.AcceptConnection(
|
||||||
|
connectionTask, accept: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(ConnectionResult.Reason.CompilationNotStarted, connectionResult.CloseReason);
|
||||||
|
Assert.Null(connectionResult.KeepAlive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Dispatcher_ClientConnectionThrowsWhenExecutingRequest_ClosesConnection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var called = false;
|
||||||
|
var connectionTask = CreateConnectionWithEmptyServerRequest(c =>
|
||||||
|
{
|
||||||
|
c.WaitForDisconnectAsyncFunc = (ct) =>
|
||||||
|
{
|
||||||
|
called = true;
|
||||||
|
throw new Exception();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var connectionHost = CreateConnectionHost();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var connectionResult = await dispatcher.AcceptConnection(
|
||||||
|
connectionTask, accept: true, cancellationToken: CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(called);
|
||||||
|
Assert.Equal(ConnectionResult.Reason.ClientException, connectionResult.CloseReason);
|
||||||
|
Assert.Null(connectionResult.KeepAlive);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispatcher_NoConnections_HitsKeepAliveTimeout()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var keepAlive = TimeSpan.FromSeconds(3);
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var connectionHost = new Mock<ConnectionHost>();
|
||||||
|
connectionHost
|
||||||
|
.Setup(x => x.WaitForConnectionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(new TaskCompletionSource<Connection>().Task);
|
||||||
|
|
||||||
|
var eventBus = new TestableEventBus();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost.Object, compilerHost, CancellationToken.None, eventBus, keepAlive);
|
||||||
|
var startTime = DateTime.Now;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
dispatcher.Run();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(eventBus.HitKeepAliveTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure server respects keep alive and shuts down after processing a single connection.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Dispatcher_ProcessSingleConnection_HitsKeepAliveTimeout()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var connectionTask = CreateConnectionWithEmptyServerRequest();
|
||||||
|
var keepAlive = TimeSpan.FromSeconds(1);
|
||||||
|
var compilerHost = CreateCompilerHost(c =>
|
||||||
|
{
|
||||||
|
c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
return EmptyServerResponse;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var connectionHost = CreateConnectionHost(connectionTask, new TaskCompletionSource<Connection>().Task);
|
||||||
|
|
||||||
|
var eventBus = new TestableEventBus();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None, eventBus, keepAlive);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
dispatcher.Run();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, eventBus.CompletedCount);
|
||||||
|
Assert.True(eventBus.LastProcessedTime.HasValue);
|
||||||
|
Assert.True(eventBus.HitKeepAliveTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure server respects keep alive and shuts down after processing multiple connections.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void Dispatcher_ProcessMultipleConnections_HitsKeepAliveTimeout()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var count = 5;
|
||||||
|
var list = new List<Task<Connection>>();
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var connectionTask = CreateConnectionWithEmptyServerRequest();
|
||||||
|
list.Add(connectionTask);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new TaskCompletionSource<Connection>().Task);
|
||||||
|
var connectionHost = CreateConnectionHost(list.ToArray());
|
||||||
|
var compilerHost = CreateCompilerHost(c =>
|
||||||
|
{
|
||||||
|
c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
return EmptyServerResponse;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var keepAlive = TimeSpan.FromSeconds(1);
|
||||||
|
var eventBus = new TestableEventBus();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None, eventBus, keepAlive);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
dispatcher.Run();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(count, eventBus.CompletedCount);
|
||||||
|
Assert.True(eventBus.LastProcessedTime.HasValue);
|
||||||
|
Assert.True(eventBus.HitKeepAliveTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensure server respects keep alive and shuts down after processing simultaneous connections.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task Dispatcher_ProcessSimultaneousConnections_HitsKeepAliveTimeout()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var totalCount = 2;
|
||||||
|
var readySource = new TaskCompletionSource<bool>();
|
||||||
|
var list = new List<TaskCompletionSource<bool>>();
|
||||||
|
var connectionHost = new Mock<ConnectionHost>();
|
||||||
|
connectionHost
|
||||||
|
.Setup(x => x.WaitForConnectionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns((CancellationToken ct) =>
|
||||||
|
{
|
||||||
|
if (list.Count < totalCount)
|
||||||
|
{
|
||||||
|
var source = new TaskCompletionSource<bool>();
|
||||||
|
var connectionTask = CreateConnectionWithEmptyServerRequest(c =>
|
||||||
|
{
|
||||||
|
c.WaitForDisconnectAsyncFunc = _ => source.Task;
|
||||||
|
});
|
||||||
|
list.Add(source);
|
||||||
|
return connectionTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
readySource.SetResult(true);
|
||||||
|
return new TaskCompletionSource<Connection>().Task;
|
||||||
|
});
|
||||||
|
|
||||||
|
var compilerHost = CreateCompilerHost(c =>
|
||||||
|
{
|
||||||
|
c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
return EmptyServerResponse;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
var keepAlive = TimeSpan.FromSeconds(1);
|
||||||
|
var eventBus = new TestableEventBus();
|
||||||
|
var dispatcherTask = Task.Run(() =>
|
||||||
|
{
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost.Object, compilerHost, CancellationToken.None, eventBus, keepAlive);
|
||||||
|
dispatcher.Run();
|
||||||
|
});
|
||||||
|
|
||||||
|
await readySource.Task;
|
||||||
|
foreach (var source in list)
|
||||||
|
{
|
||||||
|
source.SetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await dispatcherTask;
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(totalCount, eventBus.CompletedCount);
|
||||||
|
Assert.True(eventBus.LastProcessedTime.HasValue);
|
||||||
|
Assert.True(eventBus.HitKeepAliveTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Dispatcher_ClientConnectionThrows_BeginsShutdown()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var listenCancellationToken = default(CancellationToken);
|
||||||
|
var firstConnectionTask = CreateConnectionWithEmptyServerRequest(c =>
|
||||||
|
{
|
||||||
|
c.WaitForDisconnectAsyncFunc = (ct) =>
|
||||||
|
{
|
||||||
|
listenCancellationToken = ct;
|
||||||
|
return Task.Delay(Timeout.Infinite, ct).ContinueWith<Connection>(_ => null);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
var secondConnectionTask = CreateConnectionWithEmptyServerRequest(c =>
|
||||||
|
{
|
||||||
|
c.WaitForDisconnectAsyncFunc = (ct) => throw new Exception();
|
||||||
|
});
|
||||||
|
|
||||||
|
var compilerHost = CreateCompilerHost();
|
||||||
|
var connectionHost = CreateConnectionHost(
|
||||||
|
firstConnectionTask,
|
||||||
|
secondConnectionTask,
|
||||||
|
new TaskCompletionSource<Connection>().Task);
|
||||||
|
var keepAlive = TimeSpan.FromSeconds(10);
|
||||||
|
var eventBus = new TestableEventBus();
|
||||||
|
var dispatcher = new DefaultRequestDispatcher(connectionHost, compilerHost, CancellationToken.None, eventBus, keepAlive);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
dispatcher.Run();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(eventBus.HasDetectedBadConnection);
|
||||||
|
Assert.True(listenCancellationToken.IsCancellationRequested);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestableConnection CreateConnection(Stream stream, string identifier = null)
|
||||||
|
{
|
||||||
|
return new TestableConnection(stream, identifier ?? "identifier");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Connection> CreateConnectionWithEmptyServerRequest(Action<TestableConnection> configureConnection = null)
|
||||||
|
{
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
await EmptyServerRequest.WriteAsync(memoryStream, CancellationToken.None);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
var connection = CreateConnection(memoryStream);
|
||||||
|
configureConnection?.Invoke(connection);
|
||||||
|
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ConnectionHost CreateConnectionHost(params Task<Connection>[] connections)
|
||||||
|
{
|
||||||
|
var host = new Mock<ConnectionHost>();
|
||||||
|
if (connections.Length > 0)
|
||||||
|
{
|
||||||
|
var index = 0;
|
||||||
|
host
|
||||||
|
.Setup(x => x.WaitForConnectionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns((CancellationToken ct) => connections[index++]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return host.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestableCompilerHost CreateCompilerHost(Action<TestableCompilerHost> configureCompilerHost = null)
|
||||||
|
{
|
||||||
|
var compilerHost = new TestableCompilerHost();
|
||||||
|
configureCompilerHost?.Invoke(compilerHost);
|
||||||
|
|
||||||
|
return compilerHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestableCompilerHost : CompilerHost
|
||||||
|
{
|
||||||
|
internal Func<ServerRequest, CancellationToken, ServerResponse> ExecuteFunc;
|
||||||
|
|
||||||
|
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (ExecuteFunc != null)
|
||||||
|
{
|
||||||
|
return ExecuteFunc(request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return EmptyServerResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestableConnection : Connection
|
||||||
|
{
|
||||||
|
internal Func<CancellationToken, Task> WaitForDisconnectAsyncFunc;
|
||||||
|
|
||||||
|
public TestableConnection(Stream stream, string identifier)
|
||||||
|
{
|
||||||
|
Stream = stream;
|
||||||
|
Identifier = identifier;
|
||||||
|
WaitForDisconnectAsyncFunc = ct => Task.Delay(Timeout.Infinite, ct);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WaitForDisconnectAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return WaitForDisconnectAsyncFunc(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestableStream : Stream
|
||||||
|
{
|
||||||
|
internal readonly MemoryStream ReadStream = new MemoryStream();
|
||||||
|
internal readonly MemoryStream WriteStream = new MemoryStream();
|
||||||
|
|
||||||
|
public override bool CanRead => true;
|
||||||
|
public override bool CanSeek => false;
|
||||||
|
public override bool CanWrite => true;
|
||||||
|
public override long Length { get { throw new NotImplementedException(); } }
|
||||||
|
public override long Position
|
||||||
|
{
|
||||||
|
get { throw new NotImplementedException(); }
|
||||||
|
set { throw new NotImplementedException(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
return ReadStream.Read(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return ReadStream.ReadAsync(buffer, offset, count, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override long Seek(long offset, SeekOrigin origin)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetLength(long value)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
WriteStream.Write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return WriteStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal sealed class ServerData : IDisposable
|
||||||
|
{
|
||||||
|
internal CancellationTokenSource CancellationTokenSource { get; }
|
||||||
|
internal Task<ServerStats> ServerTask { get; }
|
||||||
|
internal Task ListenTask { get; }
|
||||||
|
internal string PipeName { get; }
|
||||||
|
|
||||||
|
internal ServerData(CancellationTokenSource cancellationTokenSource, string pipeName, Task<ServerStats> serverTask, Task listenTask)
|
||||||
|
{
|
||||||
|
CancellationTokenSource = cancellationTokenSource;
|
||||||
|
PipeName = pipeName;
|
||||||
|
ServerTask = serverTask;
|
||||||
|
ListenTask = listenTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<ServerStats> CancelAndCompleteAsync()
|
||||||
|
{
|
||||||
|
CancellationTokenSource.Cancel();
|
||||||
|
return await ServerTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task Verify(int connections, int completed)
|
||||||
|
{
|
||||||
|
var stats = await CancelAndCompleteAsync().ConfigureAwait(false);
|
||||||
|
Assert.Equal(connections, stats.Connections);
|
||||||
|
Assert.Equal(completed, stats.CompletedConnections);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!CancellationTokenSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
CancellationTokenSource.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerTask.Wait();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal struct ServerStats
|
||||||
|
{
|
||||||
|
internal readonly int Connections;
|
||||||
|
internal readonly int CompletedConnections;
|
||||||
|
|
||||||
|
internal ServerStats(int connections, int completedConnections)
|
||||||
|
{
|
||||||
|
Connections = connections;
|
||||||
|
CompletedConnections = completedConnections;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.CodeAnalysis.CommandLine;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal static class ServerUtilities
|
||||||
|
{
|
||||||
|
internal static string DefaultClientDirectory { get; } = Path.GetDirectoryName(typeof(ServerUtilities).Assembly.Location);
|
||||||
|
|
||||||
|
internal static ServerPaths CreateBuildPaths(string workingDir, string tempDir)
|
||||||
|
{
|
||||||
|
return new ServerPaths(
|
||||||
|
clientDir: DefaultClientDirectory,
|
||||||
|
workingDir: workingDir,
|
||||||
|
tempDir: tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ServerData CreateServer(
|
||||||
|
string pipeName = null,
|
||||||
|
CompilerHost compilerHost = null,
|
||||||
|
ConnectionHost connectionHost = null)
|
||||||
|
{
|
||||||
|
pipeName = pipeName ?? Guid.NewGuid().ToString();
|
||||||
|
compilerHost = compilerHost ?? CompilerHost.Create();
|
||||||
|
connectionHost = connectionHost ?? ConnectionHost.Create(pipeName);
|
||||||
|
|
||||||
|
var serverStatsSource = new TaskCompletionSource<ServerStats>();
|
||||||
|
var serverListenSource = new TaskCompletionSource<bool>();
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var mutexName = MutexName.GetServerMutexName(pipeName);
|
||||||
|
var thread = new Thread(_ =>
|
||||||
|
{
|
||||||
|
var eventBus = new TestableEventBus();
|
||||||
|
eventBus.Listening += (sender, e) => { serverListenSource.TrySetResult(true); };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RunServer(
|
||||||
|
pipeName,
|
||||||
|
connectionHost,
|
||||||
|
compilerHost,
|
||||||
|
cts.Token,
|
||||||
|
eventBus,
|
||||||
|
Timeout.InfiniteTimeSpan);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
var serverStats = new ServerStats(connections: eventBus.ConnectionCount, completedConnections: eventBus.CompletedCount);
|
||||||
|
serverStatsSource.SetResult(serverStats);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
thread.Start();
|
||||||
|
|
||||||
|
// The contract of this function is that it will return once the server has started. Spin here until
|
||||||
|
// we can verify the server has started or simply failed to start.
|
||||||
|
while (ServerConnection.WasServerMutexOpen(mutexName) != true && thread.IsAlive)
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServerData(cts, pipeName, serverStatsSource.Task, serverListenSource.Task);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<ServerResponse> Send(string pipeName, ServerRequest request)
|
||||||
|
{
|
||||||
|
using (var client = await Client.ConnectAsync(pipeName, timeout: null, cancellationToken: default).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
await request.WriteAsync(client.Stream).ConfigureAwait(false);
|
||||||
|
return await ServerResponse.ReadAsync(client.Stream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<int> SendShutdown(string pipeName)
|
||||||
|
{
|
||||||
|
var response = await Send(pipeName, ServerRequest.CreateShutdown());
|
||||||
|
return ((ShutdownServerResponse)response).ServerProcessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int RunServer(
|
||||||
|
string pipeName,
|
||||||
|
ConnectionHost host,
|
||||||
|
CompilerHost compilerHost,
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
EventBus eventBus = null,
|
||||||
|
TimeSpan? keepAlive = null)
|
||||||
|
{
|
||||||
|
var command = new TestableServerCommand(host, compilerHost, cancellationToken, eventBus, keepAlive);
|
||||||
|
var args = new List<string>
|
||||||
|
{
|
||||||
|
"-p",
|
||||||
|
pipeName
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = command.Execute(args.ToArray());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestableServerCommand : ServerCommand
|
||||||
|
{
|
||||||
|
private readonly ConnectionHost _host;
|
||||||
|
private readonly CompilerHost _compilerHost;
|
||||||
|
private readonly EventBus _eventBus;
|
||||||
|
private readonly CancellationToken _cancellationToken;
|
||||||
|
private readonly TimeSpan? _keepAlive;
|
||||||
|
|
||||||
|
|
||||||
|
public TestableServerCommand(
|
||||||
|
ConnectionHost host,
|
||||||
|
CompilerHost compilerHost,
|
||||||
|
CancellationToken ct,
|
||||||
|
EventBus eventBus,
|
||||||
|
TimeSpan? keepAlive)
|
||||||
|
: base(new Application(ct))
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
_compilerHost = compilerHost;
|
||||||
|
_cancellationToken = ct;
|
||||||
|
_eventBus = eventBus;
|
||||||
|
_keepAlive = keepAlive;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void ExecuteServerCore(
|
||||||
|
ConnectionHost host,
|
||||||
|
CompilerHost compilerHost,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
EventBus eventBus,
|
||||||
|
TimeSpan? keepAlive = null)
|
||||||
|
{
|
||||||
|
base.ExecuteServerCore(
|
||||||
|
_host ?? host,
|
||||||
|
_compilerHost ?? compilerHost,
|
||||||
|
_cancellationToken,
|
||||||
|
_eventBus ?? eventBus,
|
||||||
|
_keepAlive ?? keepAlive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
internal class TestableEventBus : EventBus
|
||||||
|
{
|
||||||
|
public int ListeningCount;
|
||||||
|
public int ConnectionCount;
|
||||||
|
public int CompletedCount;
|
||||||
|
public DateTime? LastProcessedTime;
|
||||||
|
public TimeSpan? KeepAlive;
|
||||||
|
public bool HasDetectedBadConnection;
|
||||||
|
public bool HitKeepAliveTimeout;
|
||||||
|
public event EventHandler Listening;
|
||||||
|
|
||||||
|
public override void ConnectionListening()
|
||||||
|
{
|
||||||
|
ListeningCount++;
|
||||||
|
Listening?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ConnectionReceived()
|
||||||
|
{
|
||||||
|
ConnectionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ConnectionCompleted(int count)
|
||||||
|
{
|
||||||
|
CompletedCount += count;
|
||||||
|
LastProcessedTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void UpdateKeepAlive(TimeSpan timeSpan)
|
||||||
|
{
|
||||||
|
KeepAlive = timeSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ConnectionRudelyEnded()
|
||||||
|
{
|
||||||
|
HasDetectedBadConnection = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void KeepAliveReached()
|
||||||
|
{
|
||||||
|
HitKeepAliveTimeout = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Razor.Tools\Microsoft.AspNetCore.Razor.Tools.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
||||||
|
<PackageReference Include="Moq" Version="$(MoqPackageVersion)" />
|
||||||
|
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
public class ServerLifecycleTest
|
||||||
|
{
|
||||||
|
private static ServerRequest EmptyServerRequest => new ServerRequest(1, Array.Empty<RequestArgument>());
|
||||||
|
|
||||||
|
private static ServerResponse EmptyServerResponse => new CompletedServerResponse(
|
||||||
|
returnCode: 0,
|
||||||
|
utf8output: false,
|
||||||
|
output: string.Empty);
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ServerStartup_MutexAlreadyAcquired_Fails()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var pipeName = Guid.NewGuid().ToString("N");
|
||||||
|
var mutexName = MutexName.GetServerMutexName(pipeName);
|
||||||
|
var compilerHost = new Mock<CompilerHost>(MockBehavior.Strict);
|
||||||
|
var host = new Mock<ConnectionHost>(MockBehavior.Strict);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
using (var mutex = new Mutex(initiallyOwned: true, name: mutexName, createdNew: out var holdsMutex))
|
||||||
|
{
|
||||||
|
Assert.True(holdsMutex);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = ServerUtilities.RunServer(pipeName, host.Object, compilerHost.Object);
|
||||||
|
|
||||||
|
// Assert failure
|
||||||
|
Assert.Equal(1, result);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mutex.ReleaseMutex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ServerStartup_SuccessfullyAcquiredMutex()
|
||||||
|
{
|
||||||
|
// Arrange, Act & Assert
|
||||||
|
var pipeName = Guid.NewGuid().ToString("N");
|
||||||
|
var mutexName = MutexName.GetServerMutexName(pipeName);
|
||||||
|
var compilerHost = new Mock<CompilerHost>(MockBehavior.Strict);
|
||||||
|
var host = new Mock<ConnectionHost>(MockBehavior.Strict);
|
||||||
|
host
|
||||||
|
.Setup(x => x.WaitForConnectionAsync(It.IsAny<CancellationToken>()))
|
||||||
|
.Returns(() =>
|
||||||
|
{
|
||||||
|
// Use a thread instead of Task to guarantee this code runs on a different
|
||||||
|
// thread and we can validate the mutex state.
|
||||||
|
var source = new TaskCompletionSource<bool>();
|
||||||
|
var thread = new Thread(_ =>
|
||||||
|
{
|
||||||
|
Mutex mutex = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Assert.True(Mutex.TryOpenExisting(mutexName, out mutex));
|
||||||
|
Assert.False(mutex.WaitOne(millisecondsTimeout: 0));
|
||||||
|
source.SetResult(true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
source.SetException(ex);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mutex?.Dispose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Synchronously wait here. Don't returned a Task value because we need to
|
||||||
|
// ensure the above check completes before the server hits a timeout and
|
||||||
|
// releases the mutex.
|
||||||
|
thread.Start();
|
||||||
|
source.Task.Wait();
|
||||||
|
|
||||||
|
return new TaskCompletionSource<Connection>().Task;
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = ServerUtilities.RunServer(pipeName, host.Object, compilerHost.Object, keepAlive: TimeSpan.FromSeconds(1));
|
||||||
|
Assert.Equal(0, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ServerRunning_ShutdownRequest_processesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
using (var serverData = ServerUtilities.CreateServer())
|
||||||
|
{
|
||||||
|
// Act
|
||||||
|
var serverProcessId = await ServerUtilities.SendShutdown(serverData.PipeName);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(Process.GetCurrentProcess().Id, serverProcessId);
|
||||||
|
await serverData.Verify(connections: 1, completed: 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A shutdown request should not abort an existing compilation. It should be allowed to run to
|
||||||
|
/// completion.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ServerRunning_ShutdownRequest_DoesNotAbortCompilation()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var completionSource = new TaskCompletionSource<bool>();
|
||||||
|
var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
// We want this to keep running even after the shutdown is seen.
|
||||||
|
completionSource.Task.Wait();
|
||||||
|
return EmptyServerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
|
||||||
|
{
|
||||||
|
var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// The compilation is now in progress, send the shutdown.
|
||||||
|
await ServerUtilities.SendShutdown(serverData.PipeName);
|
||||||
|
Assert.False(compileTask.IsCompleted);
|
||||||
|
|
||||||
|
// Now let the task complete.
|
||||||
|
completionSource.SetResult(true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var response = await compileTask;
|
||||||
|
Assert.Equal(ServerResponse.ResponseType.Completed, response.Type);
|
||||||
|
Assert.Equal(0, ((CompletedServerResponse)response).ReturnCode);
|
||||||
|
|
||||||
|
await serverData.Verify(connections: 2, completed: 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Multiple clients should be able to send shutdown requests to the server.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ServerRunning_MultipleShutdownRequests_HandlesSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var completionSource = new TaskCompletionSource<bool>();
|
||||||
|
var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
// We want this to keep running even after the shutdown is seen.
|
||||||
|
completionSource.Task.Wait();
|
||||||
|
return EmptyServerResponse;
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
|
||||||
|
{
|
||||||
|
var compileTask = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
for (var i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
// The compilation is now in progress, send the shutdown.
|
||||||
|
var processId = await ServerUtilities.SendShutdown(serverData.PipeName);
|
||||||
|
Assert.Equal(Process.GetCurrentProcess().Id, processId);
|
||||||
|
Assert.False(compileTask.IsCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let the task complete.
|
||||||
|
completionSource.SetResult(true);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var response = await compileTask;
|
||||||
|
Assert.Equal(ServerResponse.ResponseType.Completed, response.Type);
|
||||||
|
Assert.Equal(0, ((CompletedServerResponse)response).ReturnCode);
|
||||||
|
|
||||||
|
await serverData.Verify(connections: 11, completed: 11);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ServerRunning_CancelCompilation_CancelsSuccessfully()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
const int requestCount = 5;
|
||||||
|
var count = 0;
|
||||||
|
var completionSource = new TaskCompletionSource<bool>();
|
||||||
|
var host = CreateCompilerHost(c => c.ExecuteFunc = (req, ct) =>
|
||||||
|
{
|
||||||
|
if (Interlocked.Increment(ref count) == requestCount)
|
||||||
|
{
|
||||||
|
completionSource.SetResult(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ct.WaitHandle.WaitOne();
|
||||||
|
return new RejectedServerResponse();
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var serverData = ServerUtilities.CreateServer(compilerHost: host))
|
||||||
|
{
|
||||||
|
var tasks = new List<Task<ServerResponse>>();
|
||||||
|
for (var i = 0; i < requestCount; i++)
|
||||||
|
{
|
||||||
|
var task = ServerUtilities.Send(serverData.PipeName, EmptyServerRequest);
|
||||||
|
tasks.Add(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Act
|
||||||
|
// Wait until all of the connections are being processed by the server.
|
||||||
|
completionSource.Task.Wait();
|
||||||
|
|
||||||
|
// Now cancel
|
||||||
|
var stats = await serverData.CancelAndCompleteAsync();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(requestCount, stats.Connections);
|
||||||
|
Assert.Equal(requestCount, count);
|
||||||
|
|
||||||
|
foreach (var task in tasks)
|
||||||
|
{
|
||||||
|
// We expect this to throw because the stream is already closed.
|
||||||
|
await Assert.ThrowsAsync<EndOfStreamException>(() => task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TestableCompilerHost CreateCompilerHost(Action<TestableCompilerHost> configureCompilerHost = null)
|
||||||
|
{
|
||||||
|
var compilerHost = new TestableCompilerHost();
|
||||||
|
configureCompilerHost?.Invoke(compilerHost);
|
||||||
|
|
||||||
|
return compilerHost;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestableCompilerHost : CompilerHost
|
||||||
|
{
|
||||||
|
internal Func<ServerRequest, CancellationToken, ServerResponse> ExecuteFunc;
|
||||||
|
|
||||||
|
public override ServerResponse Execute(ServerRequest request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (ExecuteFunc != null)
|
||||||
|
{
|
||||||
|
return ExecuteFunc(request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return EmptyServerResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Razor.Tools
|
||||||
|
{
|
||||||
|
public class ServerProtocolTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ServerResponse_WriteRead_RoundtripsProperly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var response = new CompletedServerResponse(42, utf8output: false, output: "a string");
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await response.WriteAsync(memoryStream, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(memoryStream.Position > 0);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
var result = (CompletedServerResponse)await ServerResponse.ReadAsync(memoryStream, CancellationToken.None);
|
||||||
|
Assert.Equal(42, result.ReturnCode);
|
||||||
|
Assert.False(result.Utf8Output);
|
||||||
|
Assert.Equal("a string", result.Output);
|
||||||
|
Assert.Equal("", result.ErrorOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ServerRequest_WriteRead_RoundtripsProperly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var request = new ServerRequest(
|
||||||
|
ServerProtocol.ProtocolVersion,
|
||||||
|
ImmutableArray.Create(
|
||||||
|
new RequestArgument(RequestArgument.ArgumentId.CurrentDirectory, argumentIndex: 0, value: "directory"),
|
||||||
|
new RequestArgument(RequestArgument.ArgumentId.CommandLineArgument, argumentIndex: 1, value: "file")));
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await request.WriteAsync(memoryStream, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.True(memoryStream.Position > 0);
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
var read = await ServerRequest.ReadAsync(memoryStream, CancellationToken.None);
|
||||||
|
Assert.Equal(ServerProtocol.ProtocolVersion, read.ProtocolVersion);
|
||||||
|
Assert.Equal(2, read.Arguments.Count);
|
||||||
|
Assert.Equal(RequestArgument.ArgumentId.CurrentDirectory, read.Arguments[0].Id);
|
||||||
|
Assert.Equal(0, read.Arguments[0].ArgumentIndex);
|
||||||
|
Assert.Equal("directory", read.Arguments[0].Value);
|
||||||
|
Assert.Equal(RequestArgument.ArgumentId.CommandLineArgument, read.Arguments[1].Id);
|
||||||
|
Assert.Equal(1, read.Arguments[1].ArgumentIndex);
|
||||||
|
Assert.Equal("file", read.Arguments[1].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CreateShutdown_CreatesCorrectShutdownRequest()
|
||||||
|
{
|
||||||
|
// Arrange & Act
|
||||||
|
var request = ServerRequest.CreateShutdown();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, request.Arguments.Count);
|
||||||
|
|
||||||
|
var argument1 = request.Arguments[0];
|
||||||
|
Assert.Equal(RequestArgument.ArgumentId.Shutdown, argument1.Id);
|
||||||
|
Assert.Equal(0, argument1.ArgumentIndex);
|
||||||
|
Assert.Equal("", argument1.Value);
|
||||||
|
|
||||||
|
var argument2 = request.Arguments[1];
|
||||||
|
Assert.Equal(RequestArgument.ArgumentId.CommandLineArgument, argument2.Id);
|
||||||
|
Assert.Equal(1, argument2.ArgumentIndex);
|
||||||
|
Assert.Equal("shutdown", argument2.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShutdownRequest_WriteRead_RoundtripsProperly()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
var request = ServerRequest.CreateShutdown();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await request.WriteAsync(memoryStream, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
var read = await ServerRequest.ReadAsync(memoryStream, CancellationToken.None);
|
||||||
|
|
||||||
|
var argument1 = request.Arguments[0];
|
||||||
|
Assert.Equal(RequestArgument.ArgumentId.Shutdown, argument1.Id);
|
||||||
|
Assert.Equal(0, argument1.ArgumentIndex);
|
||||||
|
Assert.Equal("", argument1.Value);
|
||||||
|
|
||||||
|
var argument2 = request.Arguments[1];
|
||||||
|
Assert.Equal(RequestArgument.ArgumentId.CommandLineArgument, argument2.Id);
|
||||||
|
Assert.Equal(1, argument2.ArgumentIndex);
|
||||||
|
Assert.Equal("shutdown", argument2.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ShutdownResponse_WriteRead_RoundtripsProperly()
|
||||||
|
{
|
||||||
|
// Arrange & Act 1
|
||||||
|
var memoryStream = new MemoryStream();
|
||||||
|
var response = new ShutdownServerResponse(42);
|
||||||
|
|
||||||
|
// Assert 1
|
||||||
|
Assert.Equal(ServerResponse.ResponseType.Shutdown, response.Type);
|
||||||
|
|
||||||
|
// Act 2
|
||||||
|
await response.WriteAsync(memoryStream, CancellationToken.None);
|
||||||
|
|
||||||
|
// Assert 2
|
||||||
|
memoryStream.Position = 0;
|
||||||
|
var read = await ServerResponse.ReadAsync(memoryStream, CancellationToken.None);
|
||||||
|
Assert.Equal(ServerResponse.ResponseType.Shutdown, read.Type);
|
||||||
|
var typed = (ShutdownServerResponse)read;
|
||||||
|
Assert.Equal(42, typed.ServerProcessId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue