mirror of https://github.com/dotnet/tye.git
Browse Source
* Central build queue, w/ build from solution * Fix some warnings * Need to use -target and not -targets Also needed to update msbuild version past 16.10 because of a bug that meant that the default targets of projects couldn't be built. * Remove unnecessary function * Handle failures building solution better * Styling updates. * Scaffold explicit start/stop of builds. * More styling updates. * Cleanup working directory. * Make build result more consistent. * Tweak logging. * Update formatting. * Update more formatting. * Add locks for critical areas. * Update schema and reference doc. * Fix formatting. * Updates per PR feedback. * More updates per PR feedback. Co-authored-by: James Lloyd <james.lloyd@onlineseminar.nl> Co-authored-by: phoff <phoff@microsoft.com>pull/1299/head
committed by
GitHub
11 changed files with 329 additions and 11 deletions
@ -0,0 +1,293 @@ |
|||
// Licensed to the .NET Foundation under one or more agreements.
|
|||
// The .NET Foundation licenses this file to you under the MIT license.
|
|||
// See the LICENSE file in the project root for more information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading; |
|||
using System.Threading.Channels; |
|||
using System.Threading.Tasks; |
|||
using Microsoft.Build.Construction; |
|||
using Microsoft.Extensions.Logging; |
|||
|
|||
namespace Microsoft.Tye.Hosting |
|||
{ |
|||
internal sealed class BuildWatcher : IAsyncDisposable |
|||
{ |
|||
private CancellationTokenSource? _cancellationTokenSource; |
|||
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); |
|||
private readonly ILogger _logger; |
|||
private Task? _processor; |
|||
private Channel<BuildRequest>? _queue; |
|||
|
|||
public BuildWatcher(ILogger logger) |
|||
{ |
|||
_logger = logger; |
|||
} |
|||
|
|||
public Task StartAsync(string? solutionPath, string workingDirectory) |
|||
{ |
|||
return WithLockAsync( |
|||
async () => |
|||
{ |
|||
await ResetAsync(); |
|||
|
|||
_queue = Channel.CreateUnbounded<BuildRequest>(); |
|||
_cancellationTokenSource = new CancellationTokenSource(); |
|||
_processor = Task.Run(() => ProcessTaskQueueAsync(_logger, _queue.Reader, solutionPath, workingDirectory, _cancellationTokenSource.Token)); |
|||
}); |
|||
} |
|||
|
|||
public Task StopAsync() |
|||
{ |
|||
return WithLockAsync(ResetAsync); |
|||
} |
|||
|
|||
public async Task<int> BuildProjectFileAsync(string projectFilePath) |
|||
{ |
|||
var request = new BuildRequest(projectFilePath); |
|||
|
|||
await WithLockAsync( |
|||
async () => |
|||
{ |
|||
if (_queue == null) |
|||
{ |
|||
throw new InvalidOperationException("The worker is not running."); |
|||
} |
|||
|
|||
await _queue.Writer.WriteAsync(request); |
|||
}); |
|||
|
|||
return await request.Task; |
|||
} |
|||
|
|||
#region IAsyncDisposable Members
|
|||
|
|||
public async ValueTask DisposeAsync() |
|||
{ |
|||
await WithLockAsync(ResetAsync); |
|||
|
|||
_lock.Dispose(); |
|||
} |
|||
|
|||
#endregion
|
|||
|
|||
private async Task WithLockAsync(Func<Task> action) |
|||
{ |
|||
await _lock.WaitAsync(); |
|||
|
|||
try |
|||
{ |
|||
await action(); |
|||
} |
|||
finally |
|||
{ |
|||
_lock.Release(); |
|||
} |
|||
} |
|||
|
|||
private async Task ResetAsync() |
|||
{ |
|||
if (_queue != null) |
|||
{ |
|||
_queue.Writer.TryComplete(); |
|||
_queue = null; |
|||
} |
|||
|
|||
if (_cancellationTokenSource != null) |
|||
{ |
|||
_cancellationTokenSource.Cancel(); |
|||
_cancellationTokenSource.Dispose(); |
|||
_cancellationTokenSource = null; |
|||
} |
|||
|
|||
if (_processor != null) |
|||
{ |
|||
await _processor; |
|||
|
|||
_processor = null; |
|||
} |
|||
} |
|||
|
|||
private static string GetProjectName(SolutionFile solution, string projectFile) |
|||
{ |
|||
foreach (var project in solution.ProjectsInOrder) |
|||
{ |
|||
if (project.AbsolutePath == projectFile) |
|||
{ |
|||
return project.ProjectName; |
|||
} |
|||
} |
|||
|
|||
throw new InvalidOperationException($"Could not find project in solution: {projectFile}"); |
|||
} |
|||
|
|||
private static async Task ProcessTaskQueueAsync( |
|||
ILogger logger, |
|||
ChannelReader<BuildRequest> requestReader, |
|||
string? solutionPath, |
|||
string workingDirectory, |
|||
CancellationToken cancellationToken) |
|||
{ |
|||
logger.LogInformation("Build Watcher: Watching for builds..."); |
|||
|
|||
try |
|||
{ |
|||
while (await requestReader.WaitToReadAsync(cancellationToken)) |
|||
{ |
|||
var delay = TimeSpan.FromMilliseconds(250); |
|||
|
|||
logger.LogInformation("Build Watcher: Builds requested; waiting {DelayInMs}ms for more...", delay.TotalMilliseconds); |
|||
|
|||
await Task.Delay(delay); |
|||
|
|||
logger.LogInformation("Build Watcher: Getting requests..."); |
|||
|
|||
var requests = new List<BuildRequest>(); |
|||
|
|||
while (requestReader.TryRead(out var request)) |
|||
{ |
|||
requests.Add(request); |
|||
} |
|||
|
|||
logger.LogInformation("Build Watcher: Processing {Count} requests...", requests.Count); |
|||
|
|||
var solution = (solutionPath != null) ? SolutionFile.Parse(solutionPath) : null; |
|||
|
|||
var solutionBatch = new Dictionary<string, List<BuildRequest>>(); // store the list of promises
|
|||
var projectBatch = new Dictionary<string, List<BuildRequest>>(); |
|||
|
|||
foreach (var request in requests) |
|||
{ |
|||
if (solution?.ProjectShouldBuild(request.ProjectFilePath) == true) |
|||
{ |
|||
if (!solutionBatch.ContainsKey(request.ProjectFilePath)) |
|||
{ |
|||
solutionBatch.Add(request.ProjectFilePath, new List<BuildRequest>()); |
|||
} |
|||
|
|||
solutionBatch[request.ProjectFilePath].Add(request); |
|||
} |
|||
else |
|||
{ |
|||
// this will also prevent us building multiple times if a project is used by multiple services
|
|||
if (!projectBatch.ContainsKey(request.ProjectFilePath)) |
|||
{ |
|||
projectBatch.Add(request.ProjectFilePath, new List<BuildRequest>()); |
|||
} |
|||
|
|||
projectBatch[request.ProjectFilePath].Add(request); |
|||
} |
|||
} |
|||
|
|||
async Task WithRequestCompletion(IEnumerable<BuildRequest> requests, Func<Task<int>> buildFunc) |
|||
{ |
|||
try |
|||
{ |
|||
int exitCode = await buildFunc(); |
|||
|
|||
foreach (var request in requests) |
|||
{ |
|||
request.Complete(exitCode); |
|||
} |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
foreach (var request in requests) |
|||
{ |
|||
request.Complete(ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
var tasks = new List<Task>(); |
|||
|
|||
if (solutionBatch.Any()) |
|||
{ |
|||
var targets = String.Join(",", solutionBatch.Keys.Select(key => GetProjectName(solution!, key))); |
|||
|
|||
tasks.Add( |
|||
WithRequestCompletion( |
|||
solutionBatch.Values.SelectMany(x => x), |
|||
async () => |
|||
{ |
|||
logger.LogInformation("Build Watcher: Building {Targets} of solution {SolutionPath}...", targets, solutionPath); |
|||
|
|||
var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {solutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory, cancellationToken: cancellationToken); |
|||
|
|||
if (buildResult.ExitCode != 0) |
|||
{ |
|||
logger.LogInformation("Build Watcher: Solution build failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); |
|||
} |
|||
|
|||
return buildResult.ExitCode; |
|||
})); |
|||
} |
|||
|
|||
foreach (var project in projectBatch) |
|||
{ |
|||
tasks.Add( |
|||
WithRequestCompletion( |
|||
project.Value, |
|||
async () => |
|||
{ |
|||
logger.LogInformation("Build Watcher: Building project {ProjectPath}...", project.Key); |
|||
|
|||
var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{project.Key}\" /nologo", throwOnError: false, workingDirectory: workingDirectory, cancellationToken: cancellationToken); |
|||
|
|||
if (buildResult.ExitCode != 0) |
|||
{ |
|||
logger.LogInformation("Build Watcher: Project build failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); |
|||
} |
|||
|
|||
return buildResult.ExitCode; |
|||
})); |
|||
} |
|||
|
|||
logger.LogInformation("Build Watcher: Waiting for builds to complete..."); |
|||
|
|||
// NOTE: WithRequestCompletion() will trap exceptions so build errors should not bubble up from WhenAll().
|
|||
|
|||
await Task.WhenAll(tasks); |
|||
|
|||
logger.LogInformation("Build Watcher: Done with requests; waiting for more..."); |
|||
} |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
// NO-OP: Trap exception due to cancellation.
|
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
logger.LogError(ex, "Build Watcher: Error while processing builds."); |
|||
} |
|||
|
|||
logger.LogInformation("Build Watcher: Done watching."); |
|||
} |
|||
|
|||
private class BuildRequest |
|||
{ |
|||
private readonly TaskCompletionSource<int> _result = new TaskCompletionSource<int>(); |
|||
|
|||
public BuildRequest(string projectFilePath) |
|||
{ |
|||
ProjectFilePath = projectFilePath; |
|||
} |
|||
|
|||
public string ProjectFilePath { get; } |
|||
|
|||
public Task<int> Task => _result.Task; |
|||
|
|||
public void Complete(int exitCode) |
|||
{ |
|||
_result.TrySetResult(exitCode); |
|||
} |
|||
|
|||
public void Complete(Exception ex) |
|||
{ |
|||
_result.TrySetException(ex); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue