Auto rebuild when reloading after a file change.
This commit is contained in:
parent
945995199c
commit
3d787d7988
|
|
@ -4,6 +4,9 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DefaultWebContentItemExcludes>$(DefaultWebContentItemExcludes);wwwroot\**</DefaultWebContentItemExcludes>
|
<DefaultWebContentItemExcludes>$(DefaultWebContentItemExcludes);wwwroot\**</DefaultWebContentItemExcludes>
|
||||||
|
|
||||||
|
<!-- By default, enable auto rebuilds for debug builds. Note that the server will not enable it in production environments regardless. -->
|
||||||
|
<BlazorRebuildOnFileChange Condition="'$(Configuration)' == 'Debug'">true</BlazorRebuildOnFileChange>
|
||||||
|
|
||||||
<!-- We can remove this after updating to newer Razor tooling, where it's enabled by default -->
|
<!-- We can remove this after updating to newer Razor tooling, where it's enabled by default -->
|
||||||
<UseRazorBuildServer>true</UseRazorBuildServer>
|
<UseRazorBuildServer>true</UseRazorBuildServer>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(MSBuildProjectFullPath)" Overwrite="true" Encoding="Unicode"/>
|
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(MSBuildProjectFullPath)" Overwrite="true" Encoding="Unicode"/>
|
||||||
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(OutDir)$(AssemblyName).dll" Overwrite="false" Encoding="Unicode"/>
|
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Lines="$(OutDir)$(AssemblyName).dll" Overwrite="false" Encoding="Unicode"/>
|
||||||
|
<WriteLinesToFile File="$(BlazorMetadataFilePath)" Condition="'$(BlazorRebuildOnFileChange)'=='true'" Lines="autorebuild:true" Overwrite="false" Encoding="Unicode"/>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ContentWithTargetPath Include="$(BlazorMetadataFilePath)" TargetPath="$(BlazorMetadataFileName)" CopyToOutputDirectory="PreserveNewest" />
|
<ContentWithTargetPath Include="$(BlazorMetadataFilePath)" TargetPath="$(BlazorMetadataFileName)" CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Blazor.Server;
|
||||||
|
using Microsoft.AspNetCore.Blazor.Server.AutoRebuild;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Builder
|
||||||
|
{
|
||||||
|
internal static class AutoRebuildExtensions
|
||||||
|
{
|
||||||
|
// Note that we don't need to watch typical static-file extensions (.css, .js, etc.)
|
||||||
|
// because anything in wwwroot is just served directly from disk on each reload. But
|
||||||
|
// as a special case, we do watch index.html because it needs compilation.
|
||||||
|
// TODO: Make the set of extensions and exclusions configurable in csproj
|
||||||
|
private static string[] _includedSuffixes = new[] { ".cs", ".cshtml", "index.html" };
|
||||||
|
private static string[] _excludedDirectories = new[] { "obj", "bin" };
|
||||||
|
|
||||||
|
public static void UseAutoRebuild(this IApplicationBuilder appBuilder, BlazorConfig config)
|
||||||
|
{
|
||||||
|
// Currently this only supports VS for Windows. Later on we can add
|
||||||
|
// an IRebuildService implementation for VS for Mac, etc.
|
||||||
|
if (!VSForWindowsRebuildService.TryCreate(out var rebuildService))
|
||||||
|
{
|
||||||
|
return; // You're not on Windows, or you didn't launch this process from VS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume we're up to date when the app starts.
|
||||||
|
var buildToken = new RebuildToken(new DateTime(1970, 1, 1)) { BuildTask = Task.CompletedTask, };
|
||||||
|
|
||||||
|
WatchFileSystem(config, () =>
|
||||||
|
{
|
||||||
|
// Don't start the recompilation immediately. We only start it when the next
|
||||||
|
// HTTP request arrives, because it's annoying if the IDE is constantly rebuilding
|
||||||
|
// when you're making changes to multiple files and aren't ready to reload
|
||||||
|
// in the browser yet.
|
||||||
|
//
|
||||||
|
// Replacing the token means that new requests that come in will trigger a rebuild,
|
||||||
|
// and will all 'join' that build until a new file change occurs.
|
||||||
|
buildToken = new RebuildToken(DateTime.Now);
|
||||||
|
});
|
||||||
|
|
||||||
|
appBuilder.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var token = buildToken;
|
||||||
|
if (token.BuildTask == null)
|
||||||
|
{
|
||||||
|
// The build is out of date, but a new build is not yet started.
|
||||||
|
//
|
||||||
|
// We can count on VS to only allow one build at a time, this is a safe race
|
||||||
|
// because if we request a second concurrent build, it will 'join' the current one.
|
||||||
|
var task = rebuildService.PerformRebuildAsync(
|
||||||
|
config.SourceMSBuildPath,
|
||||||
|
token.LastChange);
|
||||||
|
token.BuildTask = task;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the general case it's safe to await this task, it will be a completed task
|
||||||
|
// if everything is up to date.
|
||||||
|
await token.BuildTask;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// If there's no listener on the other end of the pipe, or if anything
|
||||||
|
// else goes wrong, we just let the incoming request continue.
|
||||||
|
// There's nowhere useful to log this information so if people report
|
||||||
|
// problems we'll just have to get a repro and debug it.
|
||||||
|
// If it was an error on the VS side, it logs to the output window.
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WatchFileSystem(BlazorConfig config, Action onWrite)
|
||||||
|
{
|
||||||
|
var clientAppRootDir = Path.GetDirectoryName(config.SourceMSBuildPath);
|
||||||
|
var excludePathPrefixes = _excludedDirectories.Select(subdir
|
||||||
|
=> Path.Combine(clientAppRootDir, subdir) + Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
var fsw = new FileSystemWatcher(clientAppRootDir);
|
||||||
|
fsw.Created += OnEvent;
|
||||||
|
fsw.Changed += OnEvent;
|
||||||
|
fsw.Deleted += OnEvent;
|
||||||
|
fsw.Renamed += OnEvent;
|
||||||
|
fsw.IncludeSubdirectories = true;
|
||||||
|
fsw.EnableRaisingEvents = true;
|
||||||
|
|
||||||
|
void OnEvent(object sender, FileSystemEventArgs eventArgs)
|
||||||
|
{
|
||||||
|
if (!File.Exists(eventArgs.FullPath))
|
||||||
|
{
|
||||||
|
// It's probably a directory rather than a file
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_includedSuffixes.Any(ext => eventArgs.Name.EndsWith(ext, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
// Not a candiate file type
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludePathPrefixes.Any(prefix => eventArgs.FullPath.StartsWith(prefix, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
// In an excluded subdirectory
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represents a three-state value for the state of the build
|
||||||
|
//
|
||||||
|
// BuildTask == null means the build is out of date, but no build has started
|
||||||
|
// BuildTask.IsCompleted == false means the build has been started, but has not completed
|
||||||
|
// BuildTask.IsCompleted == true means the build has completed
|
||||||
|
private class RebuildToken
|
||||||
|
{
|
||||||
|
public RebuildToken(DateTime lastChange)
|
||||||
|
{
|
||||||
|
LastChange = lastChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime LastChange { get; }
|
||||||
|
|
||||||
|
public Task BuildTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Server.AutoRebuild
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a mechanism for rebuilding a .NET project. For example, it
|
||||||
|
/// could be a way of signalling to a VS process to perform a build.
|
||||||
|
/// </summary>
|
||||||
|
internal interface IRebuildService
|
||||||
|
{
|
||||||
|
Task<bool> PerformRebuildAsync(string projectFullPath, DateTime ifNotBuiltSince);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
// 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.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Server.AutoRebuild
|
||||||
|
{
|
||||||
|
internal static class ProcessUtils
|
||||||
|
{
|
||||||
|
// Based on https://stackoverflow.com/a/3346055
|
||||||
|
|
||||||
|
public static Process GetParent(Process process)
|
||||||
|
{
|
||||||
|
var result = new ProcessBasicInformation();
|
||||||
|
var handle = process.Handle;
|
||||||
|
var status = NtQueryInformationProcess(handle, 0, ref result, Marshal.SizeOf(result), out var returnLength);
|
||||||
|
if (status != 0)
|
||||||
|
{
|
||||||
|
throw new Win32Exception(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parentProcessId = result.InheritedFromUniqueProcessId.ToInt32();
|
||||||
|
return parentProcessId > 0 ? Process.GetProcessById(parentProcessId) : null;
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return null; // Process not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("ntdll.dll")]
|
||||||
|
private static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref ProcessBasicInformation processInformation, int processInformationLength, out int returnLength);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
struct ProcessBasicInformation
|
||||||
|
{
|
||||||
|
// These members must match PROCESS_BASIC_INFORMATION
|
||||||
|
public IntPtr Reserved1;
|
||||||
|
public IntPtr PebBaseAddress;
|
||||||
|
public IntPtr Reserved2_0;
|
||||||
|
public IntPtr Reserved2_1;
|
||||||
|
public IntPtr UniqueProcessId;
|
||||||
|
public IntPtr InheritedFromUniqueProcessId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Server.AutoRebuild
|
||||||
|
{
|
||||||
|
internal static class StreamProtocolExtensions
|
||||||
|
{
|
||||||
|
public static async Task WriteStringAsync(this Stream stream, string str)
|
||||||
|
{
|
||||||
|
var utf8Bytes = Encoding.UTF8.GetBytes(str);
|
||||||
|
await stream.WriteAsync(BitConverter.GetBytes(utf8Bytes.Length), 0, 4);
|
||||||
|
await stream.WriteAsync(utf8Bytes, 0, utf8Bytes.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task WriteDateTimeAsync(this Stream stream, DateTime value)
|
||||||
|
{
|
||||||
|
var ticksBytes = BitConverter.GetBytes(value.Ticks);
|
||||||
|
await stream.WriteAsync(ticksBytes, 0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> ReadBoolAsync(this Stream stream)
|
||||||
|
{
|
||||||
|
var responseBuf = new byte[1];
|
||||||
|
await stream.ReadAsync(responseBuf, 0, 1);
|
||||||
|
return responseBuf[0] == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int> ReadIntAsync(this Stream stream)
|
||||||
|
{
|
||||||
|
var responseBuf = new byte[4];
|
||||||
|
await stream.ReadAsync(responseBuf, 0, 4);
|
||||||
|
return BitConverter.ToInt32(responseBuf, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Blazor.Server.AutoRebuild
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Finds the VS process that launched this app process (if any), and uses
|
||||||
|
/// named pipes to communicate with its AutoRebuild listener (if any).
|
||||||
|
/// </summary>
|
||||||
|
internal class VSForWindowsRebuildService : IRebuildService
|
||||||
|
{
|
||||||
|
private const int _connectionTimeoutMilliseconds = 3000;
|
||||||
|
private readonly Process _vsProcess;
|
||||||
|
|
||||||
|
public static bool TryCreate(out VSForWindowsRebuildService result)
|
||||||
|
{
|
||||||
|
var vsProcess = FindAncestorVSProcess();
|
||||||
|
if (vsProcess != null)
|
||||||
|
{
|
||||||
|
result = new VSForWindowsRebuildService(vsProcess);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PerformRebuildAsync(string projectFullPath, DateTime ifNotBuiltSince)
|
||||||
|
{
|
||||||
|
var pipeName = $"BlazorAutoRebuild\\{_vsProcess.Id}";
|
||||||
|
using (var pipeClient = new NamedPipeClientStream(pipeName))
|
||||||
|
{
|
||||||
|
await pipeClient.ConnectAsync(_connectionTimeoutMilliseconds);
|
||||||
|
|
||||||
|
// Protocol:
|
||||||
|
// 1. Receive protocol version number from the VS listener
|
||||||
|
// If we're incompatible with it, send back special string "abort" and end
|
||||||
|
// 2. Send the project path to the VS listener
|
||||||
|
// 3. Send the 'if not rebuilt since' timestamp to the VS listener
|
||||||
|
// 4. Wait for it to send back a bool representing the result
|
||||||
|
// Keep in sync with AutoRebuildService.cs in the BlazorExtension project
|
||||||
|
// In the future we may extend this to getting back build error details
|
||||||
|
var remoteProtocolVersion = await pipeClient.ReadIntAsync();
|
||||||
|
if (remoteProtocolVersion == 1)
|
||||||
|
{
|
||||||
|
await pipeClient.WriteStringAsync(projectFullPath);
|
||||||
|
await pipeClient.WriteDateTimeAsync(ifNotBuiltSince);
|
||||||
|
return await pipeClient.ReadBoolAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await pipeClient.WriteStringAsync("abort");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private VSForWindowsRebuildService(Process vsProcess)
|
||||||
|
{
|
||||||
|
_vsProcess = vsProcess ?? throw new ArgumentNullException(nameof(vsProcess));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Process FindAncestorVSProcess()
|
||||||
|
{
|
||||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateProcess = Process.GetCurrentProcess();
|
||||||
|
while (candidateProcess != null && !candidateProcess.HasExited)
|
||||||
|
{
|
||||||
|
// It's unlikely that anyone's going to have a non-VS process in the process
|
||||||
|
// hierarchy called 'devenv', but if that turns out to be a scenario, we could
|
||||||
|
// (for example) write the VS PID to the obj directory during build, and then
|
||||||
|
// only consider processes with that ID. We still want to be sure there really
|
||||||
|
// is such a process in our ancestor chain, otherwise if you did "dotnet run"
|
||||||
|
// in a command prompt, we'd be confused and think it was launched from VS.
|
||||||
|
if (candidateProcess.ProcessName.Equals("devenv", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return candidateProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
candidateProcess = ProcessUtils.GetParent(candidateProcess);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// There's probably some permissions issue that prevents us from seeing
|
||||||
|
// further up the ancestor list, so we have to stop looking here.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,11 @@ namespace Microsoft.AspNetCore.Builder
|
||||||
OnPrepareResponse = SetCacheHeaders
|
OnPrepareResponse = SetCacheHeaders
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (env.IsDevelopment() && config.EnableAutoRebuilding)
|
||||||
|
{
|
||||||
|
applicationBuilder.UseAutoRebuild(config);
|
||||||
|
}
|
||||||
|
|
||||||
// First, match the request against files in the client app dist directory
|
// First, match the request against files in the client app dist directory
|
||||||
applicationBuilder.UseStaticFiles(distDirStaticFiles);
|
applicationBuilder.UseStaticFiles(distDirStaticFiles);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
||||||
public string WebRootPath { get; }
|
public string WebRootPath { get; }
|
||||||
public string DistPath
|
public string DistPath
|
||||||
=> Path.Combine(Path.GetDirectoryName(SourceOutputAssemblyPath), "dist");
|
=> Path.Combine(Path.GetDirectoryName(SourceOutputAssemblyPath), "dist");
|
||||||
|
public bool EnableAutoRebuilding { get; }
|
||||||
|
|
||||||
public static BlazorConfig Read(string assemblyPath)
|
public static BlazorConfig Read(string assemblyPath)
|
||||||
=> new BlazorConfig(assemblyPath);
|
=> new BlazorConfig(assemblyPath);
|
||||||
|
|
@ -41,6 +42,8 @@ namespace Microsoft.AspNetCore.Blazor.Server
|
||||||
{
|
{
|
||||||
WebRootPath = webRootPath;
|
WebRootPath = webRootPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnableAutoRebuilding = configLines.Contains("autorebuild:true", StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,147 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using Microsoft.VisualStudio.Shell.Interop;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Pipes;
|
||||||
|
using System.Security.AccessControl;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Package = Microsoft.VisualStudio.Shell.Package;
|
||||||
|
using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.BlazorExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The counterpart to VSForWindowsRebuildService.cs in the Blazor.Server project.
|
||||||
|
/// Listens for named pipe connections and rebuilds projects on request.
|
||||||
|
/// </summary>
|
||||||
|
internal class AutoRebuildService
|
||||||
|
{
|
||||||
|
private const int _protocolVersion = 1;
|
||||||
|
private readonly BuildEventsWatcher _buildEventsWatcher;
|
||||||
|
private readonly string _pipeName;
|
||||||
|
|
||||||
|
public AutoRebuildService(BuildEventsWatcher buildEventsWatcher)
|
||||||
|
{
|
||||||
|
_buildEventsWatcher = buildEventsWatcher ?? throw new ArgumentNullException(nameof(buildEventsWatcher));
|
||||||
|
_pipeName = $"BlazorAutoRebuild\\{Process.GetCurrentProcess().Id}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Listen()
|
||||||
|
{
|
||||||
|
AddBuildServiceNamedPipeServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddBuildServiceNamedPipeServer()
|
||||||
|
{
|
||||||
|
Task.Factory.StartNew(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var identity = WindowsIdentity.GetCurrent();
|
||||||
|
var identifier = identity.Owner;
|
||||||
|
var security = new PipeSecurity();
|
||||||
|
|
||||||
|
// Restrict access to just this account.
|
||||||
|
var rule = new PipeAccessRule(identifier, PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow);
|
||||||
|
security.AddAccessRule(rule);
|
||||||
|
security.SetOwner(identifier);
|
||||||
|
|
||||||
|
// And our current elevation level
|
||||||
|
var principal = new WindowsPrincipal(identity);
|
||||||
|
var isServerElevated = principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||||
|
|
||||||
|
using (var serverPipe = new NamedPipeServerStream(
|
||||||
|
_pipeName,
|
||||||
|
PipeDirection.InOut,
|
||||||
|
NamedPipeServerStream.MaxAllowedServerInstances,
|
||||||
|
PipeTransmissionMode.Byte,
|
||||||
|
PipeOptions.Asynchronous | PipeOptions.WriteThrough,
|
||||||
|
0x10000, // 64k input buffer
|
||||||
|
0x10000, // 64k output buffer
|
||||||
|
security,
|
||||||
|
HandleInheritability.None))
|
||||||
|
{
|
||||||
|
// As soon as we receive a connection, spin up another background
|
||||||
|
// listener to wait for the next connection
|
||||||
|
await serverPipe.WaitForConnectionAsync();
|
||||||
|
AddBuildServiceNamedPipeServer();
|
||||||
|
|
||||||
|
await HandleRequestAsync(serverPipe, isServerElevated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await AttemptLogErrorAsync(
|
||||||
|
$"Error in Blazor AutoRebuildService:\n{ex.Message}\n{ex.StackTrace}");
|
||||||
|
}
|
||||||
|
}, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRequestAsync(NamedPipeServerStream stream, bool isServerElevated)
|
||||||
|
{
|
||||||
|
// Protocol:
|
||||||
|
// 1. Send a "protocol version" number to the client
|
||||||
|
// 2. Receive the project path from the client
|
||||||
|
// If it is the special string "abort", gracefully disconnect and end
|
||||||
|
// This is to allow for mismatches between server and client protocol version
|
||||||
|
// 3. Receive the "if not built since" timestamp from the client
|
||||||
|
// 4. Perform the build, then send back the success/failure result flag
|
||||||
|
// Keep in sync with VSForWindowsRebuildService.cs in the Blazor.Server project
|
||||||
|
// In the future we may extend this to getting back build error details
|
||||||
|
await stream.WriteIntAsync(_protocolVersion);
|
||||||
|
var projectPath = await stream.ReadStringAsync();
|
||||||
|
|
||||||
|
// We can't do the security check for elevation until we read from the stream.
|
||||||
|
if (isServerElevated != IsClientElevated(stream))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectPath.Equals("abort", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowExistingBuildsSince = await stream.ReadDateTimeAsync();
|
||||||
|
var buildResult = await _buildEventsWatcher.PerformBuildAsync(projectPath, allowExistingBuildsSince);
|
||||||
|
await stream.WriteBoolAsync(buildResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AttemptLogErrorAsync(string message)
|
||||||
|
{
|
||||||
|
if (!ThreadHelper.CheckAccess())
|
||||||
|
{
|
||||||
|
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
var outputWindow = (IVsOutputWindow)Package.GetGlobalService(typeof(SVsOutputWindow));
|
||||||
|
if (outputWindow != null)
|
||||||
|
{
|
||||||
|
outputWindow.GetPane(VSConstants.OutputWindowPaneGuid.BuildOutputPane_guid, out var pane);
|
||||||
|
if (pane != null)
|
||||||
|
{
|
||||||
|
pane.OutputString(message);
|
||||||
|
pane.Activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool? IsClientElevated(NamedPipeServerStream stream)
|
||||||
|
{
|
||||||
|
bool? isClientElevated = null;
|
||||||
|
stream.RunAsClient(() =>
|
||||||
|
{
|
||||||
|
var identity = WindowsIdentity.GetCurrent(ifImpersonating: true);
|
||||||
|
var principal = new WindowsPrincipal(identity);
|
||||||
|
isClientElevated = principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||||
|
});
|
||||||
|
|
||||||
|
return isClientElevated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.VisualStudio.ProjectSystem.Properties;
|
||||||
|
using Microsoft.VisualStudio.Shell;
|
||||||
|
using Microsoft.VisualStudio.Shell.Interop;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.BlazorExtension
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Watches for Blazor project build events, starts new builds, and tracks builds in progress.
|
||||||
|
/// </summary>
|
||||||
|
internal class BuildEventsWatcher : IVsUpdateSolutionEvents2
|
||||||
|
{
|
||||||
|
private const string BlazorProjectCapability = "Blazor";
|
||||||
|
private readonly IVsSolution _vsSolution;
|
||||||
|
private readonly IVsSolutionBuildManager _vsBuildManager;
|
||||||
|
private readonly object mostRecentBuildInfosLock = new object();
|
||||||
|
private readonly Dictionary<string, BuildInfo> mostRecentBuildInfos
|
||||||
|
= new Dictionary<string, BuildInfo>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public BuildEventsWatcher(IVsSolution vsSolution, IVsSolutionBuildManager vsBuildManager)
|
||||||
|
{
|
||||||
|
_vsSolution = vsSolution ?? throw new ArgumentNullException(nameof(vsSolution));
|
||||||
|
_vsBuildManager = vsBuildManager ?? throw new ArgumentNullException(nameof(vsBuildManager));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<bool> PerformBuildAsync(string projectPath, DateTime allowExistingBuildsSince)
|
||||||
|
{
|
||||||
|
BuildInfo newBuildInfo;
|
||||||
|
|
||||||
|
lock (mostRecentBuildInfosLock)
|
||||||
|
{
|
||||||
|
if (mostRecentBuildInfos.TryGetValue(projectPath, out var existingInfo))
|
||||||
|
{
|
||||||
|
// If there's a build in progress, we'll join that even if it was started
|
||||||
|
// before allowExistingBuildsSince, because it's too messy to cancel
|
||||||
|
// in-progress builds. On rare occasions if the user is editing files while
|
||||||
|
// a build is in progress they *might* see a not-latest build when they
|
||||||
|
// reload, but then they just have to reload again.
|
||||||
|
var acceptBuild = !existingInfo.TaskCompletionSource.Task.IsCompleted
|
||||||
|
|| existingInfo.StartTime > allowExistingBuildsSince;
|
||||||
|
if (acceptBuild)
|
||||||
|
{
|
||||||
|
return existingInfo.TaskCompletionSource.Task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're going to start a new build now. Track the BuildInfo for it even
|
||||||
|
// before it starts so other incoming build requests can join it.
|
||||||
|
mostRecentBuildInfos[projectPath] = newBuildInfo = new BuildInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return PerformNewBuildAsync(projectPath, newBuildInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int UpdateSolution_Begin(ref int pfCancelUpdate)
|
||||||
|
=> VSConstants.S_OK;
|
||||||
|
|
||||||
|
public int UpdateSolution_Done(int fSucceeded, int fModified, int fCancelCommand)
|
||||||
|
=> VSConstants.S_OK;
|
||||||
|
|
||||||
|
public int UpdateSolution_StartUpdate(ref int pfCancelUpdate)
|
||||||
|
=> VSConstants.S_OK;
|
||||||
|
|
||||||
|
public int UpdateSolution_Cancel()
|
||||||
|
=> VSConstants.S_OK;
|
||||||
|
|
||||||
|
public int OnActiveProjectCfgChange(IVsHierarchy pIVsHierarchy)
|
||||||
|
=> VSConstants.S_OK;
|
||||||
|
|
||||||
|
public int UpdateProjectCfg_Begin(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, ref int pfCancel)
|
||||||
|
{
|
||||||
|
if (IsBlazorProject(pHierProj))
|
||||||
|
{
|
||||||
|
// This method runs both for manually-invoked builds and for builds triggered automatically
|
||||||
|
// by PerformNewBuildAsync(). In the case where it's a manually-invoked build, make sure
|
||||||
|
// there's an in-progress BuildInfo so that if there are further builds requests while the
|
||||||
|
// build is still in progress we can join them onto this existing build.
|
||||||
|
var ctx = (IVsBrowseObjectContext)pCfgProj;
|
||||||
|
var projectPath = ctx.UnconfiguredProject.FullPath;
|
||||||
|
lock (mostRecentBuildInfosLock)
|
||||||
|
{
|
||||||
|
var hasBuildInProgress =
|
||||||
|
mostRecentBuildInfos.TryGetValue(projectPath, out var existingInfo)
|
||||||
|
&& !existingInfo.TaskCompletionSource.Task.IsCompleted;
|
||||||
|
if (!hasBuildInProgress)
|
||||||
|
{
|
||||||
|
mostRecentBuildInfos[projectPath] = new BuildInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return VSConstants.S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int UpdateProjectCfg_Done(IVsHierarchy pHierProj, IVsCfg pCfgProj, IVsCfg pCfgSln, uint dwAction, int fSuccess, int fCancel)
|
||||||
|
{
|
||||||
|
if (IsBlazorProject(pHierProj))
|
||||||
|
{
|
||||||
|
var buildResult = fSuccess == 1;
|
||||||
|
var ctx = (IVsBrowseObjectContext)pCfgProj;
|
||||||
|
var projectPath = ctx.UnconfiguredProject.FullPath;
|
||||||
|
|
||||||
|
// Mark pending build info as completed
|
||||||
|
BuildInfo foundInfo = null;
|
||||||
|
lock (mostRecentBuildInfosLock)
|
||||||
|
{
|
||||||
|
mostRecentBuildInfos.TryGetValue(projectPath, out foundInfo);
|
||||||
|
}
|
||||||
|
if (foundInfo != null)
|
||||||
|
{
|
||||||
|
foundInfo.TaskCompletionSource.TrySetResult(buildResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return VSConstants.S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> PerformNewBuildAsync(string projectPath, BuildInfo buildInfo)
|
||||||
|
{
|
||||||
|
// Switch to the UI thread and request the build
|
||||||
|
var didStartBuild = await ThreadHelper.JoinableTaskFactory.RunAsync(async delegate
|
||||||
|
{
|
||||||
|
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
|
||||||
|
|
||||||
|
var hr = _vsSolution.GetProjectOfUniqueName(projectPath, out var hierarchy);
|
||||||
|
if (hr != VSConstants.S_OK)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr = _vsBuildManager.StartSimpleUpdateProjectConfiguration(
|
||||||
|
hierarchy,
|
||||||
|
/* not used */ null,
|
||||||
|
/* not used */ null,
|
||||||
|
(uint)VSSOLNBUILDUPDATEFLAGS.SBF_OPERATION_BUILD,
|
||||||
|
/* other flags */ 0,
|
||||||
|
/* suppress dialogs */ 1);
|
||||||
|
if (hr != VSConstants.S_OK)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!didStartBuild)
|
||||||
|
{
|
||||||
|
// Since the build didn't start, make sure nobody's waiting for it
|
||||||
|
buildInfo.TaskCompletionSource.TrySetResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await buildInfo.TaskCompletionSource.Task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsBlazorProject(IVsHierarchy pHierProj)
|
||||||
|
=> pHierProj.IsCapabilityMatch(BlazorProjectCapability);
|
||||||
|
|
||||||
|
class BuildInfo
|
||||||
|
{
|
||||||
|
public DateTime StartTime { get; }
|
||||||
|
public TaskCompletionSource<bool> TaskCompletionSource { get; }
|
||||||
|
|
||||||
|
public BuildInfo()
|
||||||
|
{
|
||||||
|
StartTime = DateTime.Now;
|
||||||
|
TaskCompletionSource = new TaskCompletionSource<bool>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.VisualStudio.BlazorExtension
|
||||||
|
{
|
||||||
|
internal static class StreamProtocolExtensions
|
||||||
|
{
|
||||||
|
public static async Task<string> ReadStringAsync(this Stream stream)
|
||||||
|
{
|
||||||
|
var length = BitConverter.ToInt32(await ReadBytesAsync(stream, 4), 0);
|
||||||
|
var utf8Bytes = await ReadBytesAsync(stream, length);
|
||||||
|
return Encoding.UTF8.GetString(utf8Bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<DateTime> ReadDateTimeAsync(this Stream stream)
|
||||||
|
{
|
||||||
|
var ticksBytes = await ReadBytesAsync(stream, 8);
|
||||||
|
var ticks = BitConverter.ToInt64(ticksBytes, 0);
|
||||||
|
return new DateTime(ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task WriteBoolAsync(this Stream stream, bool value)
|
||||||
|
{
|
||||||
|
var byteVal = value ? (byte)1 : (byte)0;
|
||||||
|
await stream.WriteAsync(new[] { byteVal }, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task WriteIntAsync(this Stream stream, int value)
|
||||||
|
{
|
||||||
|
await stream.WriteAsync(BitConverter.GetBytes(value), 0, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<byte[]> ReadBytesAsync(Stream stream, int exactLength)
|
||||||
|
{
|
||||||
|
var buf = new byte[exactLength];
|
||||||
|
var bytesRead = 0;
|
||||||
|
while (bytesRead < exactLength)
|
||||||
|
{
|
||||||
|
bytesRead += await stream.ReadAsync(buf, bytesRead, exactLength - bytesRead);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Microsoft.VisualStudio.Shell;
|
using Microsoft.VisualStudio.Shell;
|
||||||
|
using Microsoft.VisualStudio.Shell.Interop;
|
||||||
|
|
||||||
namespace Microsoft.VisualStudio.BlazorExtension
|
namespace Microsoft.VisualStudio.BlazorExtension
|
||||||
{
|
{
|
||||||
|
|
@ -11,8 +12,29 @@ namespace Microsoft.VisualStudio.BlazorExtension
|
||||||
[PackageRegistration(UseManagedResourcesOnly = true)]
|
[PackageRegistration(UseManagedResourcesOnly = true)]
|
||||||
[AboutDialogInfo(PackageGuidString, "ASP.NET Core Blazor Language Services", "#110", "112")]
|
[AboutDialogInfo(PackageGuidString, "ASP.NET Core Blazor Language Services", "#110", "112")]
|
||||||
[Guid(BlazorPackage.PackageGuidString)]
|
[Guid(BlazorPackage.PackageGuidString)]
|
||||||
|
[ProvideAutoLoad(UIContextGuids80.SolutionExists)]
|
||||||
public sealed class BlazorPackage : Package
|
public sealed class BlazorPackage : Package
|
||||||
{
|
{
|
||||||
public const string PackageGuidString = "d9fe04bc-57a7-4107-915e-3a5c2f9e19fb";
|
public const string PackageGuidString = "d9fe04bc-57a7-4107-915e-3a5c2f9e19fb";
|
||||||
|
|
||||||
|
protected override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
RegisterAutoRebuildService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterAutoRebuildService()
|
||||||
|
{
|
||||||
|
ThreadHelper.ThrowIfNotOnUIThread();
|
||||||
|
|
||||||
|
// Create build watcher. No need to unadvise, as this only happens once anyway.
|
||||||
|
var solution = (IVsSolution)GetGlobalService(typeof(IVsSolution));
|
||||||
|
var buildManager = (IVsSolutionBuildManager)GetService(typeof(SVsSolutionBuildManager));
|
||||||
|
var buildWatcher = new BuildEventsWatcher(solution, buildManager);
|
||||||
|
var hr = buildManager.AdviseUpdateSolutionEvents(buildWatcher, out var cookie);
|
||||||
|
Marshal.ThrowExceptionForHR(hr);
|
||||||
|
|
||||||
|
new AutoRebuildService(buildWatcher).Listen();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,9 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="AboutDialogInfoAttribute.cs" />
|
<Compile Include="AboutDialogInfoAttribute.cs" />
|
||||||
|
<Compile Include="AutoRebuild\AutoRebuildService.cs" />
|
||||||
|
<Compile Include="AutoRebuild\BuildEventsWatcher.cs" />
|
||||||
|
<Compile Include="AutoRebuild\StreamProtocolExtensions.cs" />
|
||||||
<Compile Include="BlazorPackage.cs" />
|
<Compile Include="BlazorPackage.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue