forked from dotnet/blazor
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Auto rebuild when reloading after a file change.
- Loading branch information
1 parent
7252443
commit 4d93f6e
Showing
14 changed files
with
758 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
135 changes: 135 additions & 0 deletions
135
src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/AutoRebuildExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
17 changes: 17 additions & 0 deletions
17
src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/IRebuildService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
51 changes: 51 additions & 0 deletions
51
src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/ProcessUtils.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/StreamProtocolExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} | ||
} |
107 changes: 107 additions & 0 deletions
107
src/Microsoft.AspNetCore.Blazor.Server/AutoRebuild/VSForWindowsRebuildService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.