aspnetcore/tooling/Microsoft.VisualStudio.Blaz.../AutoRebuild/AutoRebuildService.cs

148 lines
6.0 KiB
C#

// 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;
}
}
}