Code dump of WIP razor compiler server
This builds, but isn't fully implemented yet.t
This commit is contained in:
parent
8d6b86c76a
commit
34954c3668
13
Razor.sln
13
Razor.sln
|
|
@ -1,6 +1,6 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27107.3000
|
||||
VisualStudioVersion = 15.0.27130.2010
|
||||
MinimumVisualStudioVersion = 15.0.26730.03
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3C0D6505-79B3-49D0-B4C3-176F0F1836ED}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
|
|
@ -92,6 +92,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.
|
|||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{C2C98051-0F39-47F2-80B6-E72B29159F2C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Razor.Tools", "src\Microsoft.AspNetCore.Razor.Tools\Microsoft.AspNetCore.Razor.Tools.csproj", "{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -378,6 +380,14 @@ Global
|
|||
{6205467F-E381-4C42-AEEC-763BD62B3D5E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6205467F-E381-4C42-AEEC-763BD62B3D5E}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6205467F-E381-4C42-AEEC-763BD62B3D5E}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -418,6 +428,7 @@ Global
|
|||
{AFD77E2F-1A4A-4C2C-9EA9-7E48C8926780} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
||||
{323553F0-14AB-4FBD-9CF0-1CC0BE8056F8} = {92463391-81BE-462B-AC3C-78C6C760741F}
|
||||
{6205467F-E381-4C42-AEEC-763BD62B3D5E} = {C2C98051-0F39-47F2-80B6-E72B29159F2C}
|
||||
{3E7F2D49-3B45-45A8-9893-F73EC1EEBAAB} = {3C0D6505-79B3-49D0-B4C3-176F0F1836ED}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0035341D-175A-4D05-95E6-F1C2785A1E26}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
// 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.Reflection;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal class Application : CommandLineApplication
|
||||
{
|
||||
public Application(CancellationToken cancellationToken)
|
||||
{
|
||||
CancellationToken = cancellationToken;
|
||||
|
||||
Name = "rzc";
|
||||
FullName = "Microsoft ASP.NET Core Razor CLI tool";
|
||||
Description = "CLI interface to perform Razor operations.";
|
||||
ShortVersionGetter = GetInformationalVersion;
|
||||
|
||||
HelpOption("-?|-h|--help");
|
||||
|
||||
Commands.Add(new DiscoverCommand(this));
|
||||
Commands.Add(new ServerCommand(this));
|
||||
}
|
||||
|
||||
public CancellationToken CancellationToken { get; }
|
||||
|
||||
public new int Execute(params string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return base.Execute(ExpandResponseFiles(args));
|
||||
}
|
||||
catch (AggregateException ex) when (ex.InnerException != null)
|
||||
{
|
||||
Error.WriteLine(ex.InnerException.Message);
|
||||
Error.WriteLine(ex.InnerException.StackTrace);
|
||||
return 1;
|
||||
}
|
||||
catch (CommandParsingException ex)
|
||||
{
|
||||
// Don't show a call stack when we have unneeded arguments, just print the error message.
|
||||
// The code that throws this exception will print help, so no need to do it here.
|
||||
Error.WriteLine(ex.Message);
|
||||
return 1;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// This is a cancellation, not a failure.
|
||||
Error.WriteLine("Cancelled");
|
||||
return 1;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Error.WriteLine(ex.Message);
|
||||
Error.WriteLine(ex.StackTrace);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetInformationalVersion()
|
||||
{
|
||||
var assembly = typeof(Application).GetTypeInfo().Assembly;
|
||||
var attribute = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
||||
return attribute.InformationalVersion;
|
||||
}
|
||||
|
||||
private static string[] ExpandResponseFiles(string[] args)
|
||||
{
|
||||
var expandedArgs = new List<string>();
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (!arg.StartsWith("@", StringComparison.Ordinal))
|
||||
{
|
||||
expandedArgs.Add(arg);
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileName = arg.Substring(1);
|
||||
expandedArgs.AddRange(File.ReadLines(fileName));
|
||||
}
|
||||
}
|
||||
|
||||
return expandedArgs.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
// 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.IO.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CommandLine;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal abstract class Client : IDisposable
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
var timeoutMilliseconds = timeout == null ? Timeout.Infinite : (int)timeout.Value.TotalMilliseconds;
|
||||
|
||||
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.
|
||||
CompilerServerLogger.Log("Attempt to open named pipe '{0}'", pipeName);
|
||||
|
||||
var stream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
CompilerServerLogger.Log("Attempt to connect named pipe '{0}'", pipeName);
|
||||
try
|
||||
{
|
||||
await stream.ConnectAsync(timeoutMilliseconds, cancellationToken);
|
||||
}
|
||||
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.
|
||||
CompilerServerLogger.Log($"Connecting to server timed out after {timeoutMilliseconds} ms");
|
||||
return null;
|
||||
}
|
||||
|
||||
CompilerServerLogger.Log("Named pipe '{0}' connected", pipeName);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// The original code in Roslyn checks that the server pipe is owned by the same user for security.
|
||||
// We plan to rely on the BCL for this but it's not yet implemented:
|
||||
// See https://github.com/dotnet/corefx/issues/25427
|
||||
|
||||
return new NamedPipeClient(stream);
|
||||
}
|
||||
catch (Exception e) when (!(e is TaskCanceledException || e is OperationCanceledException))
|
||||
{
|
||||
CompilerServerLogger.LogException(e, "Exception while connecting to process");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract Stream Stream { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
private class NamedPipeClient : Client
|
||||
{
|
||||
public NamedPipeClient(NamedPipeClientStream stream)
|
||||
{
|
||||
Stream = stream;
|
||||
}
|
||||
|
||||
public override Stream Stream { get; }
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Stream.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
// 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 Microsoft.Extensions.CommandLineUtils;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal abstract class CommandBase : CommandLineApplication
|
||||
{
|
||||
protected CommandBase(Application parent, string name)
|
||||
: base(throwOnUnexpectedArg: true)
|
||||
{
|
||||
if (parent == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(parent));
|
||||
}
|
||||
|
||||
base.Parent = parent;
|
||||
Name = name;
|
||||
|
||||
Help = HelpOption("-?|-h|--help");
|
||||
OnExecute((Func<Task<int>>)ExecuteAsync);
|
||||
}
|
||||
|
||||
protected new Application Parent => (Application)base.Parent;
|
||||
|
||||
protected CancellationToken Cancelled => Parent?.CancellationToken ?? default;
|
||||
|
||||
protected CommandOption Help { get; }
|
||||
|
||||
protected virtual bool ValidateArguments()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract Task<int> ExecuteCoreAsync();
|
||||
|
||||
private async Task<int> ExecuteAsync()
|
||||
{
|
||||
if (!ValidateArguments())
|
||||
{
|
||||
ShowHelp();
|
||||
return 1;
|
||||
}
|
||||
|
||||
return await ExecuteCoreAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// 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.Generic;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis.CommandLine;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal abstract class CompilerHost
|
||||
{
|
||||
public static CompilerHost Create()
|
||||
{
|
||||
return new DefaultCompilerHost();
|
||||
}
|
||||
|
||||
public abstract BuildResponse Execute(BuildRequest request, CancellationToken cancellationToken);
|
||||
|
||||
private class DefaultCompilerHost : CompilerHost
|
||||
{
|
||||
public override BuildResponse Execute(BuildRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!TryParseArguments(request, out var parsed))
|
||||
{
|
||||
return new RejectedBuildResponse();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool TryParseArguments(BuildRequest request, out (string workingDirectory, string tempDirectory, string[] args) parsed)
|
||||
{
|
||||
string workingDirectory = null;
|
||||
string tempDirectory = null;
|
||||
|
||||
// The parsed arguments will contain 'string.Empty' in place of the arguments that we don't want to pass
|
||||
// to the compiler.
|
||||
var args = new List<string>(request.Arguments.Count);
|
||||
|
||||
for (var i = 0; i < request.Arguments.Count; i++)
|
||||
{
|
||||
args[i] = string.Empty;
|
||||
|
||||
var argument = request.Arguments[i];
|
||||
if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.CurrentDirectory)
|
||||
{
|
||||
workingDirectory = argument.Value;
|
||||
}
|
||||
else if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.TempDirectory)
|
||||
{
|
||||
tempDirectory = argument.Value;
|
||||
}
|
||||
else if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.CommandLineArgument)
|
||||
{
|
||||
args[i] = argument.Value;
|
||||
}
|
||||
}
|
||||
|
||||
CompilerServerLogger.Log($"WorkingDirectory = '{workingDirectory}'");
|
||||
CompilerServerLogger.Log($"TempDirectory = '{tempDirectory}'");
|
||||
for (var i = 0; i < args.Count; i++)
|
||||
{
|
||||
CompilerServerLogger.Log($"Argument[{i}] = '{request.Arguments[i]}'");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(workingDirectory))
|
||||
{
|
||||
CompilerServerLogger.Log($"Rejecting build due to missing working directory");
|
||||
|
||||
parsed = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(tempDirectory))
|
||||
{
|
||||
CompilerServerLogger.Log($"Rejecting build due to missing temp directory");
|
||||
|
||||
parsed = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(tempDirectory))
|
||||
{
|
||||
CompilerServerLogger.Log($"Rejecting build due to missing temp directory");
|
||||
|
||||
parsed = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
parsed = (workingDirectory, tempDirectory, args.ToArray());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal abstract class Connection : IDisposable
|
||||
{
|
||||
public string Identifier { get; protected set; }
|
||||
|
||||
public Stream Stream { get; protected set; }
|
||||
|
||||
public abstract Task WaitForDisconnectAsync(CancellationToken cancellationToken);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
// 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.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CommandLine;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
// Heavily influenced by:
|
||||
// https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Server/VBCSCompiler/NamedPipeClientConnection.cs#L17
|
||||
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
|
||||
private const int PipeBufferSize = 0x10000;
|
||||
|
||||
public static ConnectionHost Create(string pipeName)
|
||||
{
|
||||
return new NamedPipeConnectionHost(pipeName);
|
||||
}
|
||||
|
||||
public abstract Task<Connection> WaitForConnectionAsync(CancellationToken cancellationToken);
|
||||
|
||||
private class NamedPipeConnectionHost : ConnectionHost
|
||||
{
|
||||
public NamedPipeConnectionHost(string pipeName)
|
||||
{
|
||||
PipeName = pipeName;
|
||||
}
|
||||
|
||||
public string PipeName { get; }
|
||||
|
||||
public async override Task<Connection> WaitForConnectionAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Create the pipe and begin waiting for a connection. This doesn't block, but could fail
|
||||
// in certain circumstances, such as the OS refusing to create the pipe for some reason
|
||||
// or the pipe was disconnected before we starting listening.
|
||||
//
|
||||
// Also, note that we're waiting on CoreFx to implement some security features for us.
|
||||
// https://github.com/dotnet/corefx/issues/24040
|
||||
var pipeStream = new NamedPipeServerStream(
|
||||
PipeName,
|
||||
PipeDirection.InOut,
|
||||
NamedPipeServerStream.MaxAllowedServerInstances, // Maximum connections.
|
||||
PipeTransmissionMode.Byte,
|
||||
PipeOptions.Asynchronous | PipeOptions.WriteThrough,
|
||||
PipeBufferSize, // Default input buffer
|
||||
PipeBufferSize);// Default output buffer
|
||||
|
||||
CompilerServerLogger.Log("Waiting for new connection");
|
||||
await pipeStream.WaitForConnectionAsync(cancellationToken);
|
||||
CompilerServerLogger.Log("Pipe connection detected.");
|
||||
|
||||
if (Environment.Is64BitProcess || Memory.IsMemoryAvailable())
|
||||
{
|
||||
CompilerServerLogger.Log("Memory available - accepting connection");
|
||||
return new NamedPipeConnection(pipeStream, GetNextIdentifier());
|
||||
}
|
||||
|
||||
pipeStream.Close();
|
||||
throw new Exception("Insufficient resources to process new connection.");
|
||||
}
|
||||
}
|
||||
|
||||
private class NamedPipeConnection : Connection
|
||||
{
|
||||
public NamedPipeConnection(NamedPipeServerStream stream, string identifier)
|
||||
{
|
||||
base.Stream = stream;
|
||||
Identifier = identifier;
|
||||
}
|
||||
|
||||
public new NamedPipeServerStream Stream => (NamedPipeServerStream)base.Stream;
|
||||
|
||||
public async override Task WaitForDisconnectAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// 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 && Stream.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 of the
|
||||
CompilerServerLogger.LogException(e, $"Error poking pipe {Identifier}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
CompilerServerLogger.Log($"Pipe {Identifier}: Closing.");
|
||||
|
||||
try
|
||||
{
|
||||
Stream.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// The client connection failing to close isn't fatal to the server process. It is simply a client
|
||||
// for which we can no longer communicate and that's okay because the Close method indicates we are
|
||||
// done with the client already.
|
||||
var message = string.Format($"Pipe {Identifier}: Error closing pipe.");
|
||||
CompilerServerLogger.LogException(ex, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal struct ConnectionResult
|
||||
{
|
||||
public readonly Reason CloseReason;
|
||||
public readonly TimeSpan? KeepAlive;
|
||||
|
||||
public ConnectionResult(Reason closeReason, TimeSpan? keepAlive = null)
|
||||
{
|
||||
CloseReason = closeReason;
|
||||
KeepAlive = keepAlive;
|
||||
}
|
||||
|
||||
public enum Reason
|
||||
{
|
||||
/// <summary>
|
||||
/// There was an error creating the request object and a compilation was never created.
|
||||
/// </summary>
|
||||
CompilationNotStarted,
|
||||
|
||||
/// <summary>
|
||||
/// The compilation completed and results were provided to the client.
|
||||
/// </summary>
|
||||
CompilationCompleted,
|
||||
|
||||
/// <summary>
|
||||
/// The compilation process was initiated and the client disconnected before the results could be provided to them.
|
||||
/// </summary>
|
||||
ClientDisconnect,
|
||||
|
||||
/// <summary>
|
||||
/// There was an unhandled exception processing the result.
|
||||
/// </summary>
|
||||
ClientException,
|
||||
|
||||
/// <summary>
|
||||
/// There was a request from the client to shutdown the server.
|
||||
/// </summary>
|
||||
ClientShutdownRequest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal static class DebugMode
|
||||
{
|
||||
public static void HandleDebugSwitch(ref string[] args)
|
||||
{
|
||||
if (args.Length > 0 && string.Equals("--debug", args[0], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
args = args.Skip(1).ToArray();
|
||||
|
||||
while (!Debugger.IsAttached)
|
||||
{
|
||||
Console.WriteLine("Waiting for debugger in pid: {0}", Process.GetCurrentProcess().Id);
|
||||
Thread.Sleep(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
// 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.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc.Razor.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
using Microsoft.VisualStudio.LanguageServices.Razor;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal class DiscoverCommand : CommandBase
|
||||
{
|
||||
public DiscoverCommand(Application parent)
|
||||
: base(parent, "discover")
|
||||
{
|
||||
TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue);
|
||||
Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true);
|
||||
}
|
||||
|
||||
public CommandArgument Assemblies { get; }
|
||||
|
||||
public CommandOption TagHelperManifest { get; }
|
||||
|
||||
protected override bool ValidateArguments()
|
||||
{
|
||||
if (string.IsNullOrEmpty(TagHelperManifest.Value()))
|
||||
{
|
||||
Error.WriteLine($"{TagHelperManifest.ValueName} must be specified.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Assemblies.Values.Count == 0)
|
||||
{
|
||||
Error.WriteLine($"{Assemblies.Name} must have at least one value.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Task<int> ExecuteCoreAsync()
|
||||
{
|
||||
var result = ExecuteCore(
|
||||
outputFilePath: TagHelperManifest.Value(),
|
||||
assemblies: Assemblies.Values.ToArray());
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
|
||||
private int ExecuteCore(string outputFilePath, string[] assemblies)
|
||||
{
|
||||
var metadataReferences = new MetadataReference[assemblies.Length];
|
||||
for (var i = 0; i < assemblies.Length; i++)
|
||||
{
|
||||
metadataReferences[i] = MetadataReference.CreateFromFile(assemblies[i]);
|
||||
}
|
||||
|
||||
var engine = RazorEngine.Create((b) =>
|
||||
{
|
||||
RazorExtensions.Register(b);
|
||||
|
||||
b.Features.Add(new DefaultMetadataReferenceFeature() { References = metadataReferences });
|
||||
b.Features.Add(new CompilationTagHelperFeature());
|
||||
|
||||
// TagHelperDescriptorProviders (actually do tag helper discovery)
|
||||
b.Features.Add(new Microsoft.CodeAnalysis.Razor.DefaultTagHelperDescriptorProvider());
|
||||
b.Features.Add(new ViewComponentTagHelperDescriptorProvider());
|
||||
});
|
||||
|
||||
var feature = engine.Features.OfType<ITagHelperFeature>().Single();
|
||||
var tagHelpers = feature.GetDescriptors();
|
||||
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
Serialize(stream, tagHelpers);
|
||||
|
||||
stream.Position = 0L;
|
||||
|
||||
var newHash = Hash(stream);
|
||||
var existingHash = Hash(outputFilePath);
|
||||
|
||||
if (!HashesEqual(newHash, existingHash))
|
||||
{
|
||||
stream.Position = 0;
|
||||
using (var output = File.OpenWrite(outputFilePath))
|
||||
{
|
||||
stream.CopyTo(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static byte[] Hash(string path)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return Array.Empty<byte>();
|
||||
}
|
||||
|
||||
using (var stream = File.OpenRead(path))
|
||||
{
|
||||
return Hash(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] Hash(Stream stream)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
sha.ComputeHash(stream);
|
||||
return sha.Hash;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HashesEqual(byte[] x, byte[] y)
|
||||
{
|
||||
if (x.Length != y.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var i = 0; i < x.Length; i++)
|
||||
{
|
||||
if (x[i] != y[i])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void Serialize(Stream stream, IReadOnlyList<TagHelperDescriptor> tagHelpers)
|
||||
{
|
||||
using (var writer = new StreamWriter(stream, Encoding.UTF8, bufferSize: 4096, leaveOpen: true))
|
||||
{
|
||||
var serializer = new JsonSerializer();
|
||||
serializer.Converters.Add(new TagHelperDescriptorJsonConverter());
|
||||
serializer.Converters.Add(new RazorDiagnosticJsonConverter());
|
||||
|
||||
serializer.Serialize(writer, tagHelpers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal abstract class EventBus
|
||||
{
|
||||
public static readonly EventBus Default = new DefaultEventBus();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server updates the keep alive value.
|
||||
/// </summary>
|
||||
public virtual void UpdateKeepAlive(TimeSpan timeSpan)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called each time the server listens for new connections.
|
||||
/// </summary>
|
||||
public virtual void ConnectionListening()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a connection to the server occurs.
|
||||
/// </summary>
|
||||
public virtual void ConnectionReceived()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when one or more connections have completed processing. The number of connections
|
||||
/// processed is provided in <paramref name="count"/>.
|
||||
/// </summary>
|
||||
public virtual void ConnectionCompleted(int count)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a bad client connection was detected and the server will be shutting down as a
|
||||
/// result.
|
||||
/// </summary>
|
||||
public virtual void ConnectionRudelyEnded()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server is shutting down because the keep alive timeout was reached.
|
||||
/// </summary>
|
||||
public virtual void KeepAliveReached()
|
||||
{
|
||||
}
|
||||
|
||||
private class DefaultEventBus : EventBus
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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.TagHelperTool
|
||||
{
|
||||
internal static class Memory
|
||||
{
|
||||
public static bool IsMemoryAvailable()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Description>Razor is a markup syntax for adding server-side logic to web pages. This assembly contains infrastructure supporting Razor MSBuild integration.</Description>
|
||||
|
||||
<TargetFramework>netcoreapp2.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>rzc</AssemblyName>
|
||||
|
||||
<!-- This is not a package, it is part of Microsoft.AspNetCore.Razor.Design. -->
|
||||
<IsPackable>false</IsPackable>
|
||||
<EnableApiCheck>false</EnableApiCheck>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Microsoft.VisualStudio.LanguageServices.Razor\RazorDiagnosticJsonConverter.cs">
|
||||
<Link>Shared\RazorDiagnosticJsonConverter.cs</Link>
|
||||
</Compile>
|
||||
<Compile Include="..\Microsoft.VisualStudio.LanguageServices.Razor\TagHelperDescriptorJsonConverter.cs">
|
||||
<Link>Shared\TagHelperDescriptorJsonConverter.cs</Link>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.CommandLineUtils.Sources" Version="$(MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion)" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.AspNetCore.Mvc.Razor.Extensions\Microsoft.AspNetCore.Mvc.Razor.Extensions.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// 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.TagHelperTool
|
||||
{
|
||||
internal static class MutexName
|
||||
{
|
||||
public static string GetClientMutexName(string pipeName)
|
||||
{
|
||||
return $"{pipeName}.client";
|
||||
}
|
||||
|
||||
public static string GetServerMutexName(string pipeName)
|
||||
{
|
||||
return $"{pipeName}.server";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal static class PipeName
|
||||
{
|
||||
// We want each pipe to unique and predictable based on the inputs of:
|
||||
// - user (security)
|
||||
// - elevation status (security)
|
||||
// - path of tool on disk (version)
|
||||
//
|
||||
// This allows us to meet the security and version compat requirements just by selecting a pipe name.
|
||||
//
|
||||
// https://github.com/dotnet/corefx/issues/25427 will actually enforce the security, but we still
|
||||
// want these guarantees when we try to connect so we can expect it to succeed.
|
||||
//
|
||||
// This is similar to (and based on) the code used by Roslyn in VBCSCompiler:
|
||||
// https://github.com/dotnet/roslyn/blob/c273b6a9f19570a344c274ae89185b3a2b64d93d/src/Compilers/Shared/BuildServerConnection.cs#L528
|
||||
public static string ComputeDefault()
|
||||
{
|
||||
// Include a prefix so we can't conflict with VBCSCompiler if we somehow end up in the same directory.
|
||||
// That would be a pretty wacky bug to try and unravel.
|
||||
var baseName = ComputeBaseName("Razor:" + AppDomain.CurrentDomain.BaseDirectory);
|
||||
|
||||
// Prefix with username and elevation
|
||||
bool isAdmin = false;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
#if WINDOWS_HACK_LOL
|
||||
var currentIdentity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(currentIdentity);
|
||||
isAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
#endif
|
||||
}
|
||||
|
||||
var userName = Environment.UserName;
|
||||
if (userName == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $"{userName}.{isAdmin}.{baseName}";
|
||||
}
|
||||
|
||||
private static string ComputeBaseName(string baseDirectory)
|
||||
{
|
||||
// Normalize away trailing slashes. File APIs are not consistent about including it, so it's
|
||||
// best to normalize and avoid ending up with two servers running accidentally.
|
||||
baseDirectory = baseDirectory.TrimEnd(Path.DirectorySeparatorChar);
|
||||
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(baseDirectory));
|
||||
return Convert.ToBase64String(bytes)
|
||||
.Replace("/", "_")
|
||||
.Replace("=", string.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
DebugMode.HandleDebugSwitch(ref args);
|
||||
|
||||
var cancel = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (sender, e) => { cancel.Cancel(); };
|
||||
|
||||
var application = new Application(cancel.Token);
|
||||
return application.Execute(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,486 @@
|
|||
// 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;
|
||||
using Microsoft.CodeAnalysis.CompilerServer;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
// Heavily influenced by:
|
||||
// https://github.com/dotnet/roslyn/blob/14aed138a01c448143b9acf0fe77a662e3dfe2f4/src/Compilers/Server/ServerShared/ServerDispatcher.cs#L15
|
||||
internal abstract class RequestDispatcher
|
||||
{
|
||||
public static RequestDispatcher Create(ConnectionHost connectionHost, CompilerHost compilerHost, CancellationToken cancellationToken)
|
||||
{
|
||||
return new DefaultRequestDispatcher(connectionHost, compilerHost, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default time the server will stay alive after the last request disconnects.
|
||||
/// </summary>
|
||||
public static readonly TimeSpan DefaultServerKeepAlive = TimeSpan.FromMinutes(10);
|
||||
|
||||
/// <summary>
|
||||
/// Time to delay after the last connection before initiating a garbage collection
|
||||
/// in the server.
|
||||
/// </summary>
|
||||
public static readonly TimeSpan GCTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
public abstract void Run();
|
||||
|
||||
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 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");
|
||||
|
||||
|
||||
// TODO: this is where we actually process the request.
|
||||
// Take a look at BuildProtocolUtil
|
||||
var response = (BuildResponse)null;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,598 @@
|
|||
// 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
|
||||
/// Language RequestLanguage 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 RequestLanguage Language;
|
||||
public readonly ReadOnlyCollection<Argument> Arguments;
|
||||
|
||||
public BuildRequest(uint protocolVersion, RequestLanguage language, IEnumerable<Argument> arguments)
|
||||
{
|
||||
ProtocolVersion = protocolVersion;
|
||||
Language = language;
|
||||
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(
|
||||
RequestLanguage language,
|
||||
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, language, requestArgs);
|
||||
}
|
||||
|
||||
public static BuildRequest CreateShutdown()
|
||||
{
|
||||
var requestArgs = new[] { new Argument(ArgumentId.Shutdown, argumentIndex: 0, value: "") };
|
||||
return new BuildRequest(BuildProtocolConstants.ProtocolVersion, RequestLanguage.CSharpCompile, 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();
|
||||
var language = (RequestLanguage)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,
|
||||
language,
|
||||
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((uint)Language);
|
||||
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) { }
|
||||
}
|
||||
|
||||
// The id numbers below are just random. It's useful to use id numbers
|
||||
// that won't occur accidentally for debugging.
|
||||
internal enum RequestLanguage
|
||||
{
|
||||
CSharpCompile = 0x44532521,
|
||||
VisualBasicCompile = 0x44532522,
|
||||
}
|
||||
|
||||
/// <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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
// 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.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Microsoft.CodeAnalysis.CommandLine;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.CompilerServer
|
||||
{
|
||||
internal static class BuildProtocolUtil
|
||||
{
|
||||
internal static RunRequest GetRunRequest(BuildRequest req)
|
||||
{
|
||||
string currentDirectory;
|
||||
string libDirectory;
|
||||
string tempDirectory;
|
||||
string[] arguments = GetCommandLineArguments(req, out currentDirectory, out tempDirectory, out libDirectory);
|
||||
string language = "";
|
||||
switch (req.Language)
|
||||
{
|
||||
case RequestLanguage.CSharpCompile:
|
||||
language = LanguageNames.CSharp;
|
||||
break;
|
||||
case RequestLanguage.VisualBasicCompile:
|
||||
language = LanguageNames.VisualBasic;
|
||||
break;
|
||||
}
|
||||
|
||||
return new RunRequest(language, currentDirectory, tempDirectory, libDirectory, arguments);
|
||||
}
|
||||
|
||||
internal static string[] GetCommandLineArguments(BuildRequest req, out string currentDirectory, out string tempDirectory, out string libDirectory)
|
||||
{
|
||||
currentDirectory = null;
|
||||
libDirectory = null;
|
||||
tempDirectory = null;
|
||||
List<string> commandLineArguments = new List<string>();
|
||||
|
||||
foreach (BuildRequest.Argument arg in req.Arguments)
|
||||
{
|
||||
if (arg.ArgumentId == BuildProtocolConstants.ArgumentId.CurrentDirectory)
|
||||
{
|
||||
currentDirectory = arg.Value;
|
||||
}
|
||||
else if (arg.ArgumentId == BuildProtocolConstants.ArgumentId.TempDirectory)
|
||||
{
|
||||
tempDirectory = arg.Value;
|
||||
}
|
||||
else if (arg.ArgumentId == BuildProtocolConstants.ArgumentId.LibEnvVariable)
|
||||
{
|
||||
libDirectory = arg.Value;
|
||||
}
|
||||
else if (arg.ArgumentId == BuildProtocolConstants.ArgumentId.CommandLineArgument)
|
||||
{
|
||||
int argIndex = arg.ArgumentIndex;
|
||||
while (argIndex >= commandLineArguments.Count)
|
||||
commandLineArguments.Add("");
|
||||
commandLineArguments[argIndex] = arg.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return commandLineArguments.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,518 @@
|
|||
// 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.Security.AccessControl;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Principal;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
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 ServerNameDesktop = "VBCSCompiler.exe";
|
||||
internal const string ServerNameCoreClr = "VBCSCompiler.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;
|
||||
|
||||
internal static async Task<BuildResponse> RunServerCompilationCore(
|
||||
RequestLanguage language,
|
||||
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 = 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 = GetServerMutexName(pipeName);
|
||||
bool wasServerRunning = true;
|
||||
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(language,
|
||||
buildPaths.WorkingDirectory,
|
||||
buildPaths.TempDirectory,
|
||||
arguments,
|
||||
keepAlive,
|
||||
libEnvVariable);
|
||||
|
||||
return await TryCompile(pipe, request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return new RejectedBuildResponse();
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
bool isRunningOnCoreClr = true;
|
||||
string expectedPath;
|
||||
string processArguments;
|
||||
if (isRunningOnCoreClr)
|
||||
{
|
||||
// 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}"" ""-pipename:{pipeName}""";
|
||||
|
||||
if (!File.Exists(expectedCompilerPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// The server should be in the same directory as the client
|
||||
expectedPath = Path.Combine(clientDir, ServerNameDesktop);
|
||||
processArguments = $@"""-pipename:{pipeName}""";
|
||||
|
||||
if (!File.Exists(expectedPath))
|
||||
{
|
||||
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
|
||||
|
||||
|
||||
internal static string GetServerMutexName(string pipeName)
|
||||
{
|
||||
return $"{pipeName}.server";
|
||||
}
|
||||
|
||||
internal static string GetClientMutexName(string pipeName)
|
||||
{
|
||||
return $"{pipeName}.client";
|
||||
}
|
||||
|
||||
/// <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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.CodeAnalysis
|
||||
{
|
||||
class CommonCompiler
|
||||
{
|
||||
internal const int Failed = 1;
|
||||
internal const int Succeeded = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
// 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 Roslyn.Utilities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.CommandLine
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for logging information about what happens in the server and client parts of the
|
||||
/// Roslyn command line compiler and build tasks. Useful for debugging what is going on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// To use the logging, set the environment variable RoslynCommandLineLogFile to the name
|
||||
/// of a file to log to. This file is logged to by both client and server components.
|
||||
/// </remarks>
|
||||
internal class CompilerServerLogger
|
||||
{
|
||||
// Environment variable, if set, to enable logging and set the file to log to.
|
||||
private const string environmentVariable = "RoslynCommandLineLogFile";
|
||||
|
||||
private static readonly Stream s_loggingStream;
|
||||
private static string s_prefix = "---";
|
||||
|
||||
/// <summary>
|
||||
/// Static class initializer that initializes logging.
|
||||
/// </summary>
|
||||
static CompilerServerLogger()
|
||||
{
|
||||
s_loggingStream = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Check if the environment
|
||||
string loggingFileName = Environment.GetEnvironmentVariable(environmentVariable);
|
||||
|
||||
if (loggingFileName != null)
|
||||
{
|
||||
// If the environment variable contains the path of a currently existing directory,
|
||||
// then use a process-specific name for the log file and put it in that directory.
|
||||
// Otherwise, assume that the environment variable specifies the name of the log file.
|
||||
if (Directory.Exists(loggingFileName))
|
||||
{
|
||||
loggingFileName = Path.Combine(loggingFileName, $"server.{loggingFileName}.{GetCurrentProcessId()}.log");
|
||||
}
|
||||
|
||||
// Open allowing sharing. We allow multiple processes to log to the same file, so we use share mode to allow that.
|
||||
s_loggingStream = new FileStream(loggingFileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogException(e, "Failed to create logging stream");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the logging prefix that describes our role.
|
||||
/// Typically a 3-letter abbreviation. If logging happens before this, it's logged with "---".
|
||||
/// </summary>
|
||||
public static void Initialize(string outputPrefix)
|
||||
{
|
||||
s_prefix = outputPrefix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log an exception. Also logs information about inner exceptions.
|
||||
/// </summary>
|
||||
public static void LogException(Exception e, string reason)
|
||||
{
|
||||
if (s_loggingStream != null)
|
||||
{
|
||||
Log("Exception '{0}' occurred during '{1}'. Stack trace:\r\n{2}", e.Message, reason, e.StackTrace);
|
||||
|
||||
int innerExceptionLevel = 0;
|
||||
|
||||
e = e.InnerException;
|
||||
while (e != null)
|
||||
{
|
||||
Log("Inner exception[{0}] '{1}'. Stack trace: \r\n{1}", innerExceptionLevel, e.Message, e.StackTrace);
|
||||
e = e.InnerException;
|
||||
innerExceptionLevel += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log a line of text to the logging file, with string.Format arguments.
|
||||
/// </summary>
|
||||
public static void Log(string format, params object[] arguments)
|
||||
{
|
||||
if (s_loggingStream != null)
|
||||
{
|
||||
Log(string.Format(format, arguments));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log a line of text to the logging file.
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
public static void Log(string message)
|
||||
{
|
||||
if (s_loggingStream != null)
|
||||
{
|
||||
string prefix = GetLoggingPrefix();
|
||||
|
||||
string output = prefix + message + "\r\n";
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(output);
|
||||
|
||||
// Because multiple processes might be logging to the same file, we always seek to the end,
|
||||
// write, and flush.
|
||||
s_loggingStream.Seek(0, SeekOrigin.End);
|
||||
s_loggingStream.Write(bytes, 0, bytes.Length);
|
||||
s_loggingStream.Flush();
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetCurrentProcessId()
|
||||
{
|
||||
var process = Process.GetCurrentProcess();
|
||||
return process.Id;
|
||||
}
|
||||
|
||||
private static int GetCurrentThreadId()
|
||||
{
|
||||
var thread = Thread.CurrentThread;
|
||||
return thread.ManagedThreadId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string that prefixes all log entries. Shows the process, thread, and time.
|
||||
/// </summary>
|
||||
private static string GetLoggingPrefix()
|
||||
{
|
||||
return string.Format("{0} PID={1} TID={2} Ticks={3}: ", s_prefix, GetCurrentProcessId(), GetCurrentThreadId(), Environment.TickCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.CompilerServer
|
||||
{
|
||||
class ICompilerServerHost
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
// 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.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.CodeAnalysis.CommandLine
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
internal struct STARTUPINFO
|
||||
{
|
||||
internal Int32 cb;
|
||||
internal string lpReserved;
|
||||
internal string lpDesktop;
|
||||
internal string lpTitle;
|
||||
internal Int32 dwX;
|
||||
internal Int32 dwY;
|
||||
internal Int32 dwXSize;
|
||||
internal Int32 dwYSize;
|
||||
internal Int32 dwXCountChars;
|
||||
internal Int32 dwYCountChars;
|
||||
internal Int32 dwFillAttribute;
|
||||
internal Int32 dwFlags;
|
||||
internal Int16 wShowWindow;
|
||||
internal Int16 cbReserved2;
|
||||
internal IntPtr lpReserved2;
|
||||
internal IntPtr hStdInput;
|
||||
internal IntPtr hStdOutput;
|
||||
internal IntPtr hStdError;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct PROCESS_INFORMATION
|
||||
{
|
||||
public IntPtr hProcess;
|
||||
public IntPtr hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interop methods.
|
||||
/// </summary>
|
||||
internal static class NativeMethods
|
||||
{
|
||||
#region Constants
|
||||
|
||||
internal static readonly IntPtr NullPtr = IntPtr.Zero;
|
||||
internal static readonly IntPtr InvalidIntPtr = new IntPtr((int)-1);
|
||||
|
||||
internal const uint NORMAL_PRIORITY_CLASS = 0x0020;
|
||||
internal const uint CREATE_NO_WINDOW = 0x08000000;
|
||||
internal const Int32 STARTF_USESTDHANDLES = 0x00000100;
|
||||
internal const int ERROR_SUCCESS = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// CloseHandle
|
||||
//------------------------------------------------------------------------------
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CloseHandle(IntPtr hObject);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// CreateProcess
|
||||
//------------------------------------------------------------------------------
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool CreateProcess
|
||||
(
|
||||
string lpApplicationName,
|
||||
[In, Out]StringBuilder lpCommandLine,
|
||||
IntPtr lpProcessAttributes,
|
||||
IntPtr lpThreadAttributes,
|
||||
[In, MarshalAs(UnmanagedType.Bool)]
|
||||
bool bInheritHandles,
|
||||
uint dwCreationFlags,
|
||||
IntPtr lpEnvironment,
|
||||
string lpCurrentDirectory,
|
||||
[In] ref STARTUPINFO lpStartupInfo,
|
||||
out PROCESS_INFORMATION lpProcessInformation
|
||||
);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern IntPtr GetCommandLine();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
// 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.IO;
|
||||
|
||||
namespace Roslyn.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// This class provides simple properties for determining whether the current platform is Windows or Unix-based.
|
||||
/// We intentionally do not use System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(...) because
|
||||
/// it incorrectly reports 'true' for 'Windows' in desktop builds running on Unix-based platforms via Mono.
|
||||
/// </summary>
|
||||
internal static class PlatformInformation
|
||||
{
|
||||
public static bool IsWindows => Path.DirectorySeparatorChar == '\\';
|
||||
public static bool IsUnix => Path.DirectorySeparatorChar == '/';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal class ServerCommand : CommandBase
|
||||
{
|
||||
public ServerCommand(Application parent)
|
||||
: base(parent, "server")
|
||||
{
|
||||
Pipe = Option("-p|--pipe", "name of named pipe", CommandOptionType.SingleValue);
|
||||
}
|
||||
|
||||
public CommandOption Pipe { get; }
|
||||
|
||||
protected override bool ValidateArguments()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Pipe.Value()))
|
||||
{
|
||||
Pipe.Values.Add(PipeName.ComputeDefault());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Task<int> ExecuteCoreAsync()
|
||||
{
|
||||
// Make sure there's only one server with the same identity at a time.
|
||||
using (var mutex = new Mutex(initiallyOwned: true, name: MutexName.GetServerMutexName(Pipe.Value()), createdNew: out var holdsMutex))
|
||||
{
|
||||
if (!holdsMutex)
|
||||
{
|
||||
// Another server is running, just exit.
|
||||
Error.Write("Another server already running...");
|
||||
return Task.FromResult(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var host = ConnectionHost.Create(Pipe.Value());
|
||||
var compilerHost = CompilerHost.Create();
|
||||
var dispatcher = RequestDispatcher.Create(host, compilerHost, Cancelled);
|
||||
dispatcher.Run();
|
||||
}
|
||||
finally
|
||||
{
|
||||
mutex.ReleaseMutex();
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
// 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.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CommandLine;
|
||||
using Microsoft.Extensions.CommandLineUtils;
|
||||
|
||||
namespace Microsoft.AspNetCore.Razor.TagHelperTool
|
||||
{
|
||||
internal class ShutdownCommand : CommandBase
|
||||
{
|
||||
public ShutdownCommand(Application parent)
|
||||
: base(parent, "shutdown")
|
||||
{
|
||||
Pipe = Option("-p|--pipe", "name of named pipe", CommandOptionType.SingleValue);
|
||||
Wait = Option("-w|--wait", "wait for shutdown", CommandOptionType.NoValue);
|
||||
}
|
||||
|
||||
public CommandOption Pipe { get; }
|
||||
|
||||
public CommandOption Wait { get; }
|
||||
|
||||
protected override bool ValidateArguments()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Pipe.Value()))
|
||||
{
|
||||
Pipe.Values.Add(PipeName.ComputeDefault());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async override Task<int> ExecuteCoreAsync()
|
||||
{
|
||||
if (!IsServerRunning())
|
||||
{
|
||||
// server isn't running right now
|
||||
Out.Write("Server is not running.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var client = await Client.ConnectAsync(Pipe.Value(), timeout: null, cancellationToken: Cancelled))
|
||||
{
|
||||
var request = BuildRequest.CreateShutdown();
|
||||
await request.WriteAsync(client.Stream, Cancelled).ConfigureAwait(false);
|
||||
|
||||
var response = ((ShutdownBuildResponse)await BuildResponse.ReadAsync(client.Stream, Cancelled));
|
||||
|
||||
if (Wait.HasValue())
|
||||
{
|
||||
try
|
||||
{
|
||||
var process = Process.GetProcessById(response.ServerProcessId);
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// There is an inherent race here with the server process. If it has already shutdown
|
||||
// by the time we try to access it then the operation has succeed.
|
||||
}
|
||||
|
||||
Out.Write("Server pid:{0} shut down", response.ServerProcessId);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception) when (IsServerRunning())
|
||||
{
|
||||
// Ignore an exception that occurred while the server was shutting down.
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private bool IsServerRunning()
|
||||
{
|
||||
if (Mutex.TryOpenExisting(MutexName.GetServerMutexName(Pipe.Value()), out var mutex))
|
||||
{
|
||||
mutex.Dispose();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue