diff --git a/src/Microsoft.AspNetCore.Blazor.Build/targets/All.props b/src/Microsoft.AspNetCore.Blazor.Build/targets/All.props
index 919d5cba0b..223d79d523 100644
--- a/src/Microsoft.AspNetCore.Blazor.Build/targets/All.props
+++ b/src/Microsoft.AspNetCore.Blazor.Build/targets/All.props
@@ -4,6 +4,9 @@
$(DefaultWebContentItemExcludes);wwwroot\**
+
+ true
+
true
diff --git a/src/Microsoft.AspNetCore.Blazor.Build/targets/All.targets b/src/Microsoft.AspNetCore.Blazor.Build/targets/All.targets
index 07aff3ddaa..2afe433dd0 100644
--- a/src/Microsoft.AspNetCore.Blazor.Build/targets/All.targets
+++ b/src/Microsoft.AspNetCore.Blazor.Build/targets/All.targets
@@ -23,6 +23,7 @@
+
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs
new file mode 100644
index 0000000000..a54e023966
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/IRebuildService.cs b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/IRebuildService.cs
new file mode 100644
index 0000000000..ee48b02548
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/IRebuildService.cs
@@ -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
+{
+ ///
+ /// 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.
+ ///
+ internal interface IRebuildService
+ {
+ Task PerformRebuildAsync(string projectFullPath, DateTime ifNotBuiltSince);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/ProcessUtils.cs b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/ProcessUtils.cs
new file mode 100644
index 0000000000..4ede95e267
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/ProcessUtils.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/StreamProtocolExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/StreamProtocolExtensions.cs
new file mode 100644
index 0000000000..ae7aa96462
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/StreamProtocolExtensions.cs
@@ -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 ReadBoolAsync(this Stream stream)
+ {
+ var responseBuf = new byte[1];
+ await stream.ReadAsync(responseBuf, 0, 1);
+ return responseBuf[0] == 1;
+ }
+
+ public static async Task ReadIntAsync(this Stream stream)
+ {
+ var responseBuf = new byte[4];
+ await stream.ReadAsync(responseBuf, 0, 4);
+ return BitConverter.ToInt32(responseBuf, 0);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/VSForWindowsRebuildService.cs b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/VSForWindowsRebuildService.cs
new file mode 100644
index 0000000000..6ba10ab992
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/VSForWindowsRebuildService.cs
@@ -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
+{
+ ///
+ /// Finds the VS process that launched this app process (if any), and uses
+ /// named pipes to communicate with its AutoRebuild listener (if any).
+ ///
+ 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 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;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs b/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs
index 8142ab2560..3b75e22d7c 100644
--- a/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Server/BlazorAppBuilderExtensions.cs
@@ -49,6 +49,11 @@ namespace Microsoft.AspNetCore.Builder
OnPrepareResponse = SetCacheHeaders
};
+ if (env.IsDevelopment() && config.EnableAutoRebuilding)
+ {
+ applicationBuilder.UseAutoRebuild(config);
+ }
+
// First, match the request against files in the client app dist directory
applicationBuilder.UseStaticFiles(distDirStaticFiles);
diff --git a/src/Microsoft.AspNetCore.Blazor.Server/BlazorConfig.cs b/src/Microsoft.AspNetCore.Blazor.Server/BlazorConfig.cs
index 0884d93eda..765a46cbcc 100644
--- a/src/Microsoft.AspNetCore.Blazor.Server/BlazorConfig.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Server/BlazorConfig.cs
@@ -14,6 +14,7 @@ namespace Microsoft.AspNetCore.Blazor.Server
public string WebRootPath { get; }
public string DistPath
=> Path.Combine(Path.GetDirectoryName(SourceOutputAssemblyPath), "dist");
+ public bool EnableAutoRebuilding { get; }
public static BlazorConfig Read(string assemblyPath)
=> new BlazorConfig(assemblyPath);
@@ -41,6 +42,8 @@ namespace Microsoft.AspNetCore.Blazor.Server
{
WebRootPath = webRootPath;
}
+
+ EnableAutoRebuilding = configLines.Contains("autorebuild:true", StringComparer.Ordinal);
}
}
}
diff --git a/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/AutoRebuildService.cs b/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/AutoRebuildService.cs
new file mode 100644
index 0000000000..49977fccfa
--- /dev/null
+++ b/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/AutoRebuildService.cs
@@ -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
+{
+ ///
+ /// The counterpart to VSForWindowsRebuildService.cs in the Blazor.Server project.
+ /// Listens for named pipe connections and rebuilds projects on request.
+ ///
+ 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;
+ }
+ }
+}
diff --git a/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/BuildEventsWatcher.cs b/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/BuildEventsWatcher.cs
new file mode 100644
index 0000000000..1c56be2254
--- /dev/null
+++ b/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/BuildEventsWatcher.cs
@@ -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
+{
+ ///
+ /// Watches for Blazor project build events, starts new builds, and tracks builds in progress.
+ ///
+ 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 mostRecentBuildInfos
+ = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public BuildEventsWatcher(IVsSolution vsSolution, IVsSolutionBuildManager vsBuildManager)
+ {
+ _vsSolution = vsSolution ?? throw new ArgumentNullException(nameof(vsSolution));
+ _vsBuildManager = vsBuildManager ?? throw new ArgumentNullException(nameof(vsBuildManager));
+ }
+
+ public Task 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 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 TaskCompletionSource { get; }
+
+ public BuildInfo()
+ {
+ StartTime = DateTime.Now;
+ TaskCompletionSource = new TaskCompletionSource();
+ }
+ }
+ }
+}
diff --git a/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/StreamProtocolExtensions.cs b/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/StreamProtocolExtensions.cs
new file mode 100644
index 0000000000..4c13dc9588
--- /dev/null
+++ b/tooling/Microsoft.VisualStudio.BlazorExtension/AutoRebuild/StreamProtocolExtensions.cs
@@ -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 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 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 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;
+ }
+ }
+}
diff --git a/tooling/Microsoft.VisualStudio.BlazorExtension/BlazorPackage.cs b/tooling/Microsoft.VisualStudio.BlazorExtension/BlazorPackage.cs
index 1d93fb5164..f5ea8de8a4 100644
--- a/tooling/Microsoft.VisualStudio.BlazorExtension/BlazorPackage.cs
+++ b/tooling/Microsoft.VisualStudio.BlazorExtension/BlazorPackage.cs
@@ -4,6 +4,7 @@
using System;
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Shell;
+using Microsoft.VisualStudio.Shell.Interop;
namespace Microsoft.VisualStudio.BlazorExtension
{
@@ -11,8 +12,29 @@ namespace Microsoft.VisualStudio.BlazorExtension
[PackageRegistration(UseManagedResourcesOnly = true)]
[AboutDialogInfo(PackageGuidString, "ASP.NET Core Blazor Language Services", "#110", "112")]
[Guid(BlazorPackage.PackageGuidString)]
+ [ProvideAutoLoad(UIContextGuids80.SolutionExists)]
public sealed class BlazorPackage : Package
{
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();
+ }
}
}
diff --git a/tooling/Microsoft.VisualStudio.BlazorExtension/Microsoft.VisualStudio.BlazorExtension.csproj b/tooling/Microsoft.VisualStudio.BlazorExtension/Microsoft.VisualStudio.BlazorExtension.csproj
index 5f9967ee0f..fe0fd5830c 100644
--- a/tooling/Microsoft.VisualStudio.BlazorExtension/Microsoft.VisualStudio.BlazorExtension.csproj
+++ b/tooling/Microsoft.VisualStudio.BlazorExtension/Microsoft.VisualStudio.BlazorExtension.csproj
@@ -250,6 +250,9 @@
+
+
+