diff --git a/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.csproj b/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.csproj index c3a002c1f8..2ec3cab3bc 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.csproj +++ b/src/Microsoft.AspNetCore.Razor.Design/Microsoft.AspNetCore.Razor.Design.csproj @@ -1,7 +1,7 @@ - + Razor is a markup syntax for adding server-side logic to web pages. This package contains MSBuild support for Razor. @@ -28,21 +28,22 @@ - - - - + + + + - + - - + + + @@ -63,47 +64,38 @@ First, build the project, then copy it to the ouput directory, then add it as packable content. --> - + - - + + - - + + - + - + - - + + - - + + - - - + + + <_RazorTool Include="$(OutputPath)tools\**\*" Exclude="$(OutputPath)tools\**\*.xml;$(OutputPath)tools\**\*.pdb" /> @@ -116,6 +108,6 @@ - + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets index 3cb1e75bb0..4c3949fa69 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets +++ b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.CodeGeneration.targets @@ -15,6 +15,7 @@ <_RazorGenerateToolAssembly>$(_RazorMSBuildRoot)tools\Microsoft.AspNetCore.Razor.GenerateTool.dll <_RazorTagHelperToolAssembly>$(_RazorMSBuildRoot)tools\Microsoft.AspNetCore.Razor.TagHelperTool.dll + <_RazorBuildServerAssembly>$(_RazorMSBuildRoot)tools\rzc.dll <_RazorGenerateInputsHash> @@ -74,8 +75,10 @@ Debug="$(_RazorDebugTagHelperTask)" DebugTool="$(_RazorDebugTagHelperTool)" ToolAssembly="$(_RazorTagHelperToolAssembly)" + UseServer="$(UseRazorBuildServer)" ServerAssembly="$(_RazorBuildServerAssembly)" Assemblies="@(RazorReferencePath)" + ProjectRoot="$(MSBuildProjectDirectory)" TagHelperManifest="$(_RazorTagHelperOutputCache)"> true - + <_RazorMSBuildRoot Condition="'$(_RazorMSBuildRoot)'==''">$(MSBuildThisFileDirectory)..\..\ diff --git a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets index 5fcb47a7ce..43b44933d6 100644 --- a/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets +++ b/src/Microsoft.AspNetCore.Razor.Design/build/netstandard2.0/Microsoft.AspNetCore.Razor.Design.targets @@ -56,9 +56,10 @@ true + + false - @@ -121,14 +122,14 @@ diff --git a/src/Microsoft.AspNetCore.Razor.TagHelperTool/Application.cs b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Application.cs index 0fbcf34602..992482a82c 100644 --- a/src/Microsoft.AspNetCore.Razor.TagHelperTool/Application.cs +++ b/src/Microsoft.AspNetCore.Razor.TagHelperTool/Application.cs @@ -20,8 +20,9 @@ namespace Microsoft.AspNetCore.Razor.TagHelperTool HelpOption("-?|-h|--help"); - TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue); Assemblies = Argument("assemblies", "assemblies to search for tag helpers", multipleValues: true); + TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue); + ProjectRoot = Option("-p", "project root directory", CommandOptionType.SingleValue); new RunCommand().Configure(this); } @@ -30,6 +31,8 @@ namespace Microsoft.AspNetCore.Razor.TagHelperTool public CommandOption TagHelperManifest { get; } + public CommandOption ProjectRoot { get; } + public new int Execute(params string[] args) { try diff --git a/src/Microsoft.AspNetCore.Razor.TagHelperTool/RunCommand.cs b/src/Microsoft.AspNetCore.Razor.TagHelperTool/RunCommand.cs index 1258a0fbb7..8cedb19955 100644 --- a/src/Microsoft.AspNetCore.Razor.TagHelperTool/RunCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.TagHelperTool/RunCommand.cs @@ -32,12 +32,15 @@ namespace Microsoft.AspNetCore.Razor.TagHelperTool } return ExecuteCore( + projectDirectory: application.ProjectRoot.Value(), outputFilePath: application.TagHelperManifest.Value(), assemblies: application.Assemblies.Values.ToArray()); } - private int ExecuteCore(string outputFilePath, string[] assemblies) + private int ExecuteCore(string projectDirectory, string outputFilePath, string[] assemblies) { + outputFilePath = Path.Combine(projectDirectory, outputFilePath); + var metadataReferences = new MetadataReference[assemblies.Length]; for (var i = 0; i < assemblies.Length; i++) { @@ -147,6 +150,11 @@ namespace Microsoft.AspNetCore.Razor.TagHelperTool return false; } + if (string.IsNullOrEmpty(application.ProjectRoot.Value())) + { + application.ProjectRoot.Values.Add(Environment.CurrentDirectory); + } + return true; } } diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs b/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs index 5e13753101..e9ef11dae4 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/DotnetToolTask.cs @@ -2,16 +2,24 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Threading; +using Microsoft.AspNetCore.Razor.Tools; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Microsoft.CodeAnalysis.CommandLine; using Microsoft.Extensions.CommandLineUtils; +using Roslyn.Utilities; namespace Microsoft.AspNetCore.Razor.Tasks { public abstract class DotNetToolTask : ToolTask { + private CancellationTokenSource _razorServerCts; + public bool Debug { get; set; } public bool DebugTool { get; set; } @@ -19,6 +27,11 @@ namespace Microsoft.AspNetCore.Razor.Tasks [Required] public string ToolAssembly { get; set; } + [Required] + public string ServerAssembly { get; set; } + + public bool UseServer { get; set; } + protected override string ToolName => "dotnet"; // If we're debugging then make all of the stdout gets logged in MSBuild @@ -26,6 +39,8 @@ namespace Microsoft.AspNetCore.Razor.Tasks protected override MessageImportance StandardErrorLoggingImportance => MessageImportance.High; + internal abstract RequestCommand Command { get; } + protected override string GenerateFullPathToTool() { #if NETSTANDARD2_0 @@ -62,12 +77,19 @@ namespace Microsoft.AspNetCore.Razor.Tasks } } - if (TryExecuteOnServer(out var result)) + return base.Execute(); + } + + protected override int ExecuteTool(string pathToTool, string responseFileCommands, string commandLineCommands) + { + if (UseServer && + !string.IsNullOrEmpty(ServerAssembly) && + TryExecuteOnServer(pathToTool, responseFileCommands, commandLineCommands, out var result)) { return result; } - return base.Execute(); + return base.ExecuteTool(pathToTool, responseFileCommands, commandLineCommands); } protected override void LogToolCommand(string message) @@ -82,10 +104,80 @@ namespace Microsoft.AspNetCore.Razor.Tasks } } - protected virtual bool TryExecuteOnServer(out bool result) + public override void Cancel() { - result = false; + base.Cancel(); + + _razorServerCts?.Cancel(); + } + + protected virtual bool TryExecuteOnServer(string pathToTool, string responseFileCommands, string commandLineCommands, out int result) + { + CompilerServerLogger.Log("Server execution started."); + using (_razorServerCts = new CancellationTokenSource()) + { + CompilerServerLogger.Log($"CommandLine = '{commandLineCommands}'"); + CompilerServerLogger.Log($"BuildResponseFile = '{responseFileCommands}'"); + + // The server contains the tools for discovering tag helpers and generating Razor code. + var clientDir = Path.GetDirectoryName(ServerAssembly); + var workingDir = CurrentDirectoryToUse(); + var tempDir = BuildServerConnection.GetTempPath(workingDir); + + var buildPaths = new BuildPathsAlt( + clientDir, + // MSBuild doesn't need the .NET SDK directory + sdkDir: null, + workingDir: workingDir, + tempDir: tempDir); + + var responseTask = BuildServerConnection.RunServerCompilation( + Command, + GetArguments(responseFileCommands), + buildPaths, + keepAlive: null, + cancellationToken: _razorServerCts.Token); + + responseTask.Wait(_razorServerCts.Token); + + var response = responseTask.Result; + if (response.Type == BuildResponse.ResponseType.Completed && + response is CompletedBuildResponse completedResponse) + { + result = completedResponse.ReturnCode; + + CompilerServerLogger.Log($"Server execution completed with return code {result}."); + + return true; + } + } + + CompilerServerLogger.Log("Server execution failed."); + result = -1; + return false; } + + /// + /// Get the current directory that the compiler should run in. + /// + private string CurrentDirectoryToUse() + { + // ToolTask has a method for this. But it may return null. Use the process directory + // if ToolTask didn't override. MSBuild uses the process directory. + string workingDirectory = GetWorkingDirectory(); + if (string.IsNullOrEmpty(workingDirectory)) + { + workingDirectory = Directory.GetCurrentDirectory(); + } + return workingDirectory; + } + + private List GetArguments(string responseFileCommands) + { + var responseFileArguments = + CommandLineUtilities.SplitCommandLineIntoArguments(responseFileCommands, removeHashComments: true); + return responseFileArguments.ToList(); + } } } diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/Microsoft.AspNetCore.Razor.Tasks.csproj b/src/Microsoft.AspNetCore.Razor.Tasks/Microsoft.AspNetCore.Razor.Tasks.csproj index 95ec512e34..70b5027083 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/Microsoft.AspNetCore.Razor.Tasks.csproj +++ b/src/Microsoft.AspNetCore.Razor.Tasks/Microsoft.AspNetCore.Razor.Tasks.csproj @@ -15,5 +15,30 @@ + + + Shared\BuildServerConnection.cs + + + Shared\NativeMethods.cs + + + Shared\CompilerServerLogger.cs + + + Shared\PlatformInformation.cs + + + Shared\BuildProtocol.cs + + + Shared\CommandLineUtilities.cs + + + Shared\PipeName.cs + + + Shared\MutexName.cs + diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs index e485252587..15fdf00319 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorGenerate.cs @@ -3,6 +3,7 @@ using System.Text; using Microsoft.Build.Framework; +using Microsoft.CodeAnalysis.CommandLine; namespace Microsoft.AspNetCore.Razor.Tasks { @@ -20,6 +21,8 @@ namespace Microsoft.AspNetCore.Razor.Tasks [Required] public string TagHelperManifest { get; set; } + internal override RequestCommand Command => RequestCommand.RazorGenerate; + protected override string GenerateResponseFileCommands() { var builder = new StringBuilder(); diff --git a/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs index 100741e574..6e9023b978 100644 --- a/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs +++ b/src/Microsoft.AspNetCore.Razor.Tasks/RazorTagHelper.cs @@ -5,6 +5,7 @@ using System.IO; using System.Text; using Microsoft.Build.Framework; +using Microsoft.CodeAnalysis.CommandLine; namespace Microsoft.AspNetCore.Razor.Tasks { @@ -17,7 +18,9 @@ namespace Microsoft.AspNetCore.Razor.Tasks [Required] public string TagHelperManifest { get; set; } - public string ServerAssembly { get; set; } + public string ProjectRoot { get; set; } + + internal override RequestCommand Command => RequestCommand.RazorTagHelper; protected override bool SkipTaskExecution() { @@ -43,6 +46,9 @@ namespace Microsoft.AspNetCore.Razor.Tasks builder.AppendLine("-o"); builder.AppendLine(TagHelperManifest); + builder.AppendLine("-p"); + builder.AppendLine(ProjectRoot); + return builder.ToString(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs index 388ad0a81f..21678b4195 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Application.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Application.cs @@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Razor.Tools Commands.Add(new DiscoverCommand(this)); Commands.Add(new ServerCommand(this)); + Commands.Add(new ShutdownCommand(this)); } public CancellationToken CancellationToken { get; } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs index 7fe17169cf..7721490412 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/CompilerHost.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using System.Linq; using System.Threading; using Microsoft.CodeAnalysis.CommandLine; +using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.AspNetCore.Razor.Tools { @@ -25,7 +27,23 @@ namespace Microsoft.AspNetCore.Razor.Tools return new RejectedBuildResponse(); } - return null; + var app = new Application(cancellationToken); + var commandArgs = parsed.args.ToArray(); + + CommandBase command = null; + if (request.Command == RequestCommand.RazorGenerate) + { + command = new GenerateCommand(app); + } + else if (request.Command == RequestCommand.RazorTagHelper) + { + command = new DiscoverCommand(app); + } + + var exitCode = command?.Execute(commandArgs) ?? 0; + var output = command?.Out.ToString() ?? string.Empty; + + return new CompletedBuildResponse(exitCode, utf8output: false, output: output); } private bool TryParseArguments(BuildRequest request, out (string workingDirectory, string tempDirectory, string[] args) parsed) @@ -33,14 +51,10 @@ namespace Microsoft.AspNetCore.Razor.Tools 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(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) { @@ -52,7 +66,7 @@ namespace Microsoft.AspNetCore.Razor.Tools } else if (argument.ArgumentId == BuildProtocolConstants.ArgumentId.CommandLineArgument) { - args[i] = argument.Value; + args.Add(argument.Value); } } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs index b0fc7297d5..e431a6cc5b 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/DiscoverCommand.cs @@ -23,14 +23,17 @@ namespace Microsoft.AspNetCore.Razor.Tools 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); + TagHelperManifest = Option("-o", "output file", CommandOptionType.SingleValue); + ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); } public CommandArgument Assemblies { get; } public CommandOption TagHelperManifest { get; } + public CommandOption ProjectDirectory { get; } + protected override bool ValidateArguments() { if (string.IsNullOrEmpty(TagHelperManifest.Value())) @@ -45,19 +48,27 @@ namespace Microsoft.AspNetCore.Razor.Tools return false; } + if (string.IsNullOrEmpty(ProjectDirectory.Value())) + { + ProjectDirectory.Values.Add(Environment.CurrentDirectory); + } + return true; } protected override Task ExecuteCoreAsync() { var result = ExecuteCore( + projectDirectory: ProjectDirectory.Value(), outputFilePath: TagHelperManifest.Value(), assemblies: Assemblies.Values.ToArray()); return Task.FromResult(result); } - private int ExecuteCore(string outputFilePath, string[] assemblies) + private int ExecuteCore(string projectDirectory, string outputFilePath, string[] assemblies) { + outputFilePath = Path.Combine(projectDirectory, outputFilePath); + var metadataReferences = new MetadataReference[assemblies.Length]; for (var i = 0; i < assemblies.Length; i++) { diff --git a/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs new file mode 100644 index 0000000000..63b4e16efa --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/GenerateCommand.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.Razor.Extensions; +using Microsoft.AspNetCore.Razor.Language; +using Microsoft.Extensions.CommandLineUtils; +using Microsoft.VisualStudio.LanguageServices.Razor; +using Newtonsoft.Json; + +namespace Microsoft.AspNetCore.Razor.Tools +{ + internal class GenerateCommand : CommandBase + { + public GenerateCommand(Application parent) + : base(parent, "generate") + { + Sources = Argument("sources", ".cshtml files to compile", multipleValues: true); + ProjectDirectory = Option("-p", "project root directory", CommandOptionType.SingleValue); + OutputDirectory = Option("-o", "output directory", CommandOptionType.SingleValue); + TagHelperManifest = Option("-t", "tag helper manifest file", CommandOptionType.SingleValue); + } + + public CommandArgument Sources { get; } + + public CommandOption OutputDirectory { get; } + + public CommandOption ProjectDirectory { get; } + + public CommandOption TagHelperManifest { get; } + + protected override Task ExecuteCoreAsync() + { + var result = ExecuteCore( + projectDirectory: ProjectDirectory.Value() ?? Environment.CurrentDirectory, + outputDirectory: OutputDirectory.Value(), + tagHelperManifest: TagHelperManifest.Value(), + sources: Sources.Values.ToArray()); + + return Task.FromResult(result); + } + + protected override bool ValidateArguments() + { + if (string.IsNullOrEmpty(OutputDirectory.Value())) + { + Error.WriteLine($"{OutputDirectory.ValueName} not specified."); + return false; + } + + if (Sources.Values.Count == 0) + { + Error.WriteLine($"{Sources.Name} should have at least one value."); + return false; + } + + return true; + } + + private int ExecuteCore(string projectDirectory, string outputDirectory, string tagHelperManifest, string[] sources) + { + tagHelperManifest = Path.Combine(projectDirectory, tagHelperManifest); + outputDirectory = Path.Combine(projectDirectory, outputDirectory); + + var tagHelpers = GetTagHelpers(tagHelperManifest); + + var engine = RazorEngine.Create(b => + { + RazorExtensions.Register(b); + + b.Features.Add(new StaticTagHelperFeature() { TagHelpers = tagHelpers, }); + }); + + var templateEngine = new MvcRazorTemplateEngine(engine, RazorProject.Create(projectDirectory)); + + var sourceItems = GetRazorFiles(projectDirectory, sources); + var results = GenerateCode(templateEngine, sourceItems); + + var success = true; + + foreach (var result in results) + { + if (result.CSharpDocument.Diagnostics.Count > 0) + { + success = false; + foreach (var error in result.CSharpDocument.Diagnostics) + { + Console.Error.WriteLine(error.ToString()); + } + } + + var outputFilePath = Path.Combine(outputDirectory, Path.ChangeExtension(result.ViewFileInfo.ViewEnginePath.Substring(1), ".cs")); + File.WriteAllText(outputFilePath, result.CSharpDocument.GeneratedCode); + } + + return success ? 0 : -1; + } + + private IReadOnlyList GetTagHelpers(string tagHelperManifest) + { + if (!File.Exists(tagHelperManifest)) + { + return Array.Empty(); + } + + using (var stream = File.OpenRead(tagHelperManifest)) + { + var reader = new JsonTextReader(new StreamReader(stream)); + + var serializer = new JsonSerializer(); + serializer.Converters.Add(new RazorDiagnosticJsonConverter()); + serializer.Converters.Add(new TagHelperDescriptorJsonConverter()); + + return serializer.Deserialize>(reader); + } + } + + private List GetRazorFiles(string projectDirectory, string[] sources) + { + var trimLength = projectDirectory.EndsWith("/") ? projectDirectory.Length - 1 : projectDirectory.Length; + + var items = new List(sources.Length); + for (var i = 0; i < sources.Length; i++) + { + var fullPath = Path.Combine(projectDirectory, sources[i]); + if (fullPath.StartsWith(projectDirectory, StringComparison.OrdinalIgnoreCase)) + { + var viewEnginePath = fullPath.Substring(trimLength).Replace('\\', '/'); + items.Add(new SourceItem(fullPath, viewEnginePath)); + } + } + + return items; + } + + private OutputItem[] GenerateCode(RazorTemplateEngine templateEngine, IReadOnlyList sources) + { + var outputs = new OutputItem[sources.Count]; + Parallel.For(0, outputs.Length, new ParallelOptions() { MaxDegreeOfParallelism = 4 }, i => + { + var source = sources[i]; + + var csharpDocument = templateEngine.GenerateCode(source.ViewEnginePath); + outputs[i] = new OutputItem(source, csharpDocument); + }); + + return outputs; + } + + private struct OutputItem + { + public OutputItem( + SourceItem viewFileInfo, + RazorCSharpDocument cSharpDocument) + { + ViewFileInfo = viewFileInfo; + CSharpDocument = cSharpDocument; + } + + public SourceItem ViewFileInfo { get; } + + public RazorCSharpDocument CSharpDocument { get; } + } + + private struct SourceItem + { + public SourceItem(string fullPath, string viewEnginePath) + { + FullPath = fullPath; + ViewEnginePath = viewEnginePath; + } + + public string FullPath { get; } + + public string ViewEnginePath { get; } + + public Stream CreateReadStream() + { + // We are setting buffer size to 1 to prevent FileStream from allocating it's internal buffer + // 0 causes constructor to throw + var bufferSize = 1; + return new FileStream( + FullPath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + bufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + } + } + + private class StaticTagHelperFeature : ITagHelperFeature + { + public RazorEngine Engine { get; set; } + + public IReadOnlyList TagHelpers { get; set; } + + public IReadOnlyList GetDescriptors() => TagHelpers; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/PipeName.cs b/src/Microsoft.AspNetCore.Razor.Tools/PipeName.cs index d66df5b7ed..7a575cb6b9 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/PipeName.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/PipeName.cs @@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Razor.Tools return null; } - return $"{userName}.{isAdmin}.{baseName}"; + return $"{userName}.{(isAdmin ? 'T' : 'F')}.{baseName}"; } private static string ComputeBaseName(string baseDirectory) @@ -59,6 +59,7 @@ namespace Microsoft.AspNetCore.Razor.Tools { var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(baseDirectory)); return Convert.ToBase64String(bytes) + .Substring(0, 25) // We only have ~50 total characters on Mac, so strip that down .Replace("/", "_") .Replace("=", string.Empty); } diff --git a/src/Microsoft.AspNetCore.Razor.Tools/RequestDispatcher.cs b/src/Microsoft.AspNetCore.Razor.Tools/RequestDispatcher.cs index fb3d732be7..3fd18ff273 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/RequestDispatcher.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/RequestDispatcher.cs @@ -446,10 +446,7 @@ namespace Microsoft.AspNetCore.Razor.Tools { CompilerServerLogger.Log("Begin processing request"); - - // TODO: this is where we actually process the request. - // Take a look at BuildProtocolUtil - var response = (BuildResponse)null; + var response = _compilerHost.Execute(buildRequest, cancellationToken); CompilerServerLogger.Log("End processing request"); return response; diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildProtocol.cs b/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildProtocol.cs index 9d90a35ecc..205949d3c2 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildProtocol.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildProtocol.cs @@ -39,13 +39,13 @@ namespace Microsoft.CodeAnalysis.CommandLine internal class BuildRequest { public readonly uint ProtocolVersion; - public readonly RequestLanguage Language; + public readonly RequestCommand Command; public readonly ReadOnlyCollection Arguments; - public BuildRequest(uint protocolVersion, RequestLanguage language, IEnumerable arguments) + public BuildRequest(uint protocolVersion, RequestCommand command, IEnumerable arguments) { ProtocolVersion = protocolVersion; - Language = language; + Command = command; Arguments = new ReadOnlyCollection(arguments.ToList()); if (Arguments.Count > ushort.MaxValue) @@ -80,7 +80,7 @@ namespace Microsoft.CodeAnalysis.CommandLine } public static BuildRequest Create( - RequestLanguage language, + RequestCommand command, string workingDirectory, string tempDirectory, IList args, @@ -115,13 +115,13 @@ namespace Microsoft.CodeAnalysis.CommandLine requestArgs.Add(new Argument(ArgumentId.CommandLineArgument, i, arg)); } - return new BuildRequest(BuildProtocolConstants.ProtocolVersion, language, requestArgs); + return new BuildRequest(BuildProtocolConstants.ProtocolVersion, command, requestArgs); } public static BuildRequest CreateShutdown() { var requestArgs = new[] { new Argument(ArgumentId.Shutdown, argumentIndex: 0, value: "") }; - return new BuildRequest(BuildProtocolConstants.ProtocolVersion, RequestLanguage.CSharpCompile, requestArgs); + return new BuildRequest(BuildProtocolConstants.ProtocolVersion, RequestCommand.None, requestArgs); } public bool IsShutdownRequest() @@ -163,7 +163,7 @@ namespace Microsoft.CodeAnalysis.CommandLine using (var reader = new BinaryReader(new MemoryStream(requestBuffer), Encoding.Unicode)) { var protocolVersion = reader.ReadUInt32(); - var language = (RequestLanguage)reader.ReadUInt32(); + var command = (RequestCommand)reader.ReadUInt32(); uint argumentCount = reader.ReadUInt32(); var argumentsBuilder = new List((int)argumentCount); @@ -175,7 +175,7 @@ namespace Microsoft.CodeAnalysis.CommandLine } return new BuildRequest(protocolVersion, - language, + command, argumentsBuilder); } } @@ -191,7 +191,7 @@ namespace Microsoft.CodeAnalysis.CommandLine // Format the request. Log("Formatting request"); writer.Write(ProtocolVersion); - writer.Write((uint)Language); + writer.Write((uint)Command); writer.Write(Arguments.Count); foreach (Argument arg in Arguments) { @@ -506,10 +506,11 @@ namespace Microsoft.CodeAnalysis.CommandLine // The id numbers below are just random. It's useful to use id numbers // that won't occur accidentally for debugging. - internal enum RequestLanguage + internal enum RequestCommand { - CSharpCompile = 0x44532521, - VisualBasicCompile = 0x44532522, + None = 0x44532621, + RazorTagHelper = 0x44532622, + RazorGenerate = 0x44532623, } /// diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildProtocolUtil.cs b/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildProtocolUtil.cs deleted file mode 100644 index a4c521083a..0000000000 --- a/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildProtocolUtil.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.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 commandLineArguments = new List(); - - 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(); - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildServerConnection.cs b/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildServerConnection.cs index 6b38bcf392..990d64c1c0 100644 --- a/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildServerConnection.cs +++ b/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/BuildServerConnection.cs @@ -8,12 +8,10 @@ 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.AspNetCore.Razor.Tools; using Microsoft.Win32.SafeHandles; using Roslyn.Utilities; using static Microsoft.CodeAnalysis.CommandLine.CompilerServerLogger; @@ -56,8 +54,7 @@ namespace Microsoft.CodeAnalysis.CommandLine internal sealed class BuildServerConnection { - internal const string ServerNameDesktop = "VBCSCompiler.exe"; - internal const string ServerNameCoreClr = "VBCSCompiler.dll"; + internal const string ServerNameCoreClr = "rzc.dll"; // Spend up to 1s connecting to existing process (existing processes should be always responsive). internal const int TimeOutMsExistingProcess = 1000; @@ -65,8 +62,29 @@ namespace Microsoft.CodeAnalysis.CommandLine // Spend up to 20s connecting to a new process, to allow time for it to start. internal const int TimeOutMsNewProcess = 20000; + public static Task RunServerCompilation( + RequestCommand command, + List arguments, + BuildPathsAlt buildPaths, + string keepAlive, + CancellationToken cancellationToken) + { + var pipeName = PipeName.ComputeDefault(); + + return RunServerCompilationCore( + command, + arguments, + buildPaths, + pipeName: pipeName, + keepAlive: keepAlive, + libEnvVariable: null, + timeoutOverride: null, + tryCreateServerFunc: TryCreateServerCore, + cancellationToken: cancellationToken); + } + internal static async Task RunServerCompilationCore( - RequestLanguage language, + RequestCommand language, List arguments, BuildPathsAlt buildPaths, string pipeName, @@ -89,7 +107,7 @@ namespace Microsoft.CodeAnalysis.CommandLine var clientDir = buildPaths.ClientDirectory; var timeoutNewProcess = timeoutOverride ?? TimeOutMsNewProcess; var timeoutExistingProcess = timeoutOverride ?? TimeOutMsExistingProcess; - var clientMutexName = GetClientMutexName(pipeName); + var clientMutexName = MutexName.GetClientMutexName(pipeName); Task pipeTask = null; using (var clientMutex = new Mutex(initiallyOwned: true, name: clientMutexName, @@ -115,8 +133,8 @@ namespace Microsoft.CodeAnalysis.CommandLine } // Check for an already running server - var serverMutexName = GetServerMutexName(pipeName); - bool wasServerRunning = true; + var serverMutexName = MutexName.GetServerMutexName(pipeName); + bool wasServerRunning = WasServerMutexOpen(serverMutexName); var timeout = wasServerRunning ? timeoutExistingProcess : timeoutNewProcess; if (wasServerRunning || tryCreateServerFunc(clientDir, pipeName)) @@ -152,6 +170,18 @@ namespace Microsoft.CodeAnalysis.CommandLine return new RejectedBuildResponse(); } + internal static bool WasServerMutexOpen(string mutexName) + { + Mutex mutex; + var open = Mutex.TryOpenExisting(mutexName, out mutex); + if (open) + { + mutex.Dispose(); + return true; + } + return false; + } + /// /// Try to compile using the server. Returns a null-containing Task if a response /// from the server cannot be retrieved. @@ -312,31 +342,17 @@ namespace Microsoft.CodeAnalysis.CommandLine 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}"""; + // The server should be in the same directory as the client + var expectedCompilerPath = Path.Combine(clientDir, ServerNameCoreClr); + expectedPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") ?? "dotnet"; + processArguments = $@"""{expectedCompilerPath}"" server -p {pipeName}"; - if (!File.Exists(expectedPath)) - { - return false; - } + if (!File.Exists(expectedCompilerPath)) + { + return false; } if (PlatformInformation.IsWindows) @@ -456,17 +472,6 @@ namespace Microsoft.CodeAnalysis.CommandLine } #endif - - internal static string GetServerMutexName(string pipeName) - { - return $"{pipeName}.server"; - } - - internal static string GetClientMutexName(string pipeName) - { - return $"{pipeName}.client"; - } - /// /// Gets the value of the temporary path for the current environment assuming the working directory /// is . This function must emulate as diff --git a/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/CommandLineUtilities.cs b/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/CommandLineUtilities.cs new file mode 100644 index 0000000000..caaa38d52f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Tools/Roslyn/CommandLineUtilities.cs @@ -0,0 +1,155 @@ +// 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.Collections.Generic; +using System.Text; + +namespace Roslyn.Utilities +{ + /* + * Copied from https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/InternalUtilities/CommandLineUtilities.cs + */ + internal static class CommandLineUtilities + { + /// + /// Split a command line by the same rules as Main would get the commands except the original + /// state of backslashes and quotes are preserved. For example in normal Windows command line + /// parsing the following command lines would produce equivalent Main arguments: + /// + /// - /r:a,b + /// - /r:"a,b" + /// + /// This method will differ as the latter will have the quotes preserved. The only case where + /// quotes are removed is when the entire argument is surrounded by quotes without any inner + /// quotes. + /// + /// + /// Rules for command line parsing, according to MSDN: + /// + /// Arguments are delimited by white space, which is either a space or a tab. + /// + /// A string surrounded by double quotation marks ("string") is interpreted + /// as a single argument, regardless of white space contained within. + /// A quoted string can be embedded in an argument. + /// + /// A double quotation mark preceded by a backslash (\") is interpreted as a + /// literal double quotation mark character ("). + /// + /// Backslashes are interpreted literally, unless they immediately precede a + /// double quotation mark. + /// + /// If an even number of backslashes is followed by a double quotation mark, + /// one backslash is placed in the argv array for every pair of backslashes, + /// and the double quotation mark is interpreted as a string delimiter. + /// + /// If an odd number of backslashes is followed by a double quotation mark, + /// one backslash is placed in the argv array for every pair of backslashes, + /// and the double quotation mark is "escaped" by the remaining backslash, + /// causing a literal double quotation mark (") to be placed in argv. + /// + public static IEnumerable SplitCommandLineIntoArguments(string commandLine, bool removeHashComments) + { + char? unused; + return SplitCommandLineIntoArguments(commandLine, removeHashComments, out unused); + } + + public static IEnumerable SplitCommandLineIntoArguments(string commandLine, bool removeHashComments, out char? illegalChar) + { + var builder = new StringBuilder(commandLine.Length); + var list = new List(); + var i = 0; + + illegalChar = null; + while (i < commandLine.Length) + { + while (i < commandLine.Length && char.IsWhiteSpace(commandLine[i])) + { + i++; + } + + if (i == commandLine.Length) + { + break; + } + + if (commandLine[i] == '#' && removeHashComments) + { + break; + } + + var quoteCount = 0; + builder.Length = 0; + while (i < commandLine.Length && (!char.IsWhiteSpace(commandLine[i]) || (quoteCount % 2 != 0))) + { + var current = commandLine[i]; + switch (current) + { + case '\\': + { + var slashCount = 0; + do + { + builder.Append(commandLine[i]); + i++; + slashCount++; + } while (i < commandLine.Length && commandLine[i] == '\\'); + + // Slashes not followed by a quote character can be ignored for now + if (i >= commandLine.Length || commandLine[i] != '"') + { + break; + } + + // If there is an odd number of slashes then it is escaping the quote + // otherwise it is just a quote. + if (slashCount % 2 == 0) + { + quoteCount++; + } + + builder.Append('"'); + i++; + break; + } + + case '"': + builder.Append(current); + quoteCount++; + i++; + break; + + default: + if ((current >= 0x1 && current <= 0x1f) || current == '|') + { + if (illegalChar == null) + { + illegalChar = current; + } + } + else + { + builder.Append(current); + } + + i++; + break; + } + } + + // If the quote string is surrounded by quotes with no interior quotes then + // remove the quotes here. + if (quoteCount == 2 && builder[0] == '"' && builder[builder.Length - 1] == '"') + { + builder.Remove(0, length: 1); + builder.Remove(builder.Length - 1, length: 1); + } + + if (builder.Length > 0) + { + list.Add(builder.ToString()); + } + } + + return list; + } + } +} \ No newline at end of file