This repository has been archived by the owner on Nov 20, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 520
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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>
- Loading branch information
1 parent
3ea9e9e
commit f677a15
Showing
11 changed files
with
329 additions
and
11 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
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
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
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,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); | ||
} | ||
} | ||
} | ||
} |
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.