From cebf82337578244241c91dd1ab9765d7268cc4a1 Mon Sep 17 00:00:00 2001 From: James Lloyd Date: Tue, 28 Sep 2021 11:04:26 +0200 Subject: [PATCH 01/18] Central build queue, w/ build from solution --- src/Microsoft.Tye.Core/ApplicationBuilder.cs | 1 + src/Microsoft.Tye.Core/ApplicationFactory.cs | 3 +- .../ConfigModel/ConfigApplication.cs | 2 + .../MsBuild/SolutionFile.cs | 18 +- .../Serialization/ConfigApplicationParser.cs | 3 + .../Model/Application.cs | 1 + src/Microsoft.Tye.Hosting/ProcessRunner.cs | 13 +- .../WatchBuilderWorker.cs | 180 ++++++++++++++++++ src/tye/ApplicationBuilderExtensions.cs | 6 +- 9 files changed, 218 insertions(+), 9 deletions(-) create mode 100644 src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs diff --git a/src/Microsoft.Tye.Core/ApplicationBuilder.cs b/src/Microsoft.Tye.Core/ApplicationBuilder.cs index 5123067c5..23d0bdd65 100644 --- a/src/Microsoft.Tye.Core/ApplicationBuilder.cs +++ b/src/Microsoft.Tye.Core/ApplicationBuilder.cs @@ -36,5 +36,6 @@ public ApplicationBuilder(FileInfo source, string name, ContainerEngine containe public List Ingress { get; } = new List(); public string? Network { get; set; } + public string? BuildSolution { get; internal set; } } } diff --git a/src/Microsoft.Tye.Core/ApplicationFactory.cs b/src/Microsoft.Tye.Core/ApplicationFactory.cs index 37377495c..da367bd2f 100644 --- a/src/Microsoft.Tye.Core/ApplicationFactory.cs +++ b/src/Microsoft.Tye.Core/ApplicationFactory.cs @@ -32,7 +32,8 @@ public static async Task CreateAsync(OutputContext output, F var root = new ApplicationBuilder(source, rootConfig.Name!, new ContainerEngine(rootConfig.ContainerEngineType), rootConfig.DashboardPort) { - Namespace = rootConfig.Namespace + Namespace = rootConfig.Namespace, + BuildSolution = rootConfig.BuildSolution, }; queue.Enqueue((rootConfig, new HashSet())); diff --git a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs index b98bd1310..d79371a6d 100644 --- a/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs +++ b/src/Microsoft.Tye.Core/ConfigModel/ConfigApplication.cs @@ -26,6 +26,8 @@ public class ConfigApplication public int? DashboardPort { get; set; } + public string? BuildSolution { get; set; } + public string? Namespace { get; set; } public string? Registry { get; set; } diff --git a/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs b/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs index 1986ed762..f7117bd7a 100644 --- a/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs +++ b/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs @@ -239,7 +239,7 @@ internal string SolutionFileDirectory #region Methods - internal bool ProjectShouldBuild(string projectFile) + public bool ProjectShouldBuild(string projectFile) { return _solutionFilter?.Contains(FileUtilities.FixFilePath(projectFile)) != false; } @@ -825,6 +825,22 @@ private void AddProjectToSolution(ProjectInSolution proj) _projectsInOrder.Add(proj); } + /// + /// Get the target name for the given project file in this solution, or null if the project file is not referenced by + /// this solution. + /// + /// projectFile + public string GetProjectName(string projectFile) + { + foreach(var project in _projects) { + if(project.Value.AbsolutePath == FileUtilities.FixFilePath(projectFile)) + { + return project.Value.GetOriginalProjectName(); + } + } + return null; + } + /// /// Validate relative path of a project /// diff --git a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs index 68a1225b7..a7ccc6ae9 100644 --- a/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs +++ b/src/Microsoft.Tye.Core/Serialization/ConfigApplicationParser.cs @@ -21,6 +21,9 @@ public static void HandleConfigApplication(YamlMappingNode yamlMappingNode, Conf case "name": app.Name = YamlParser.GetScalarValue(key, child.Value); break; + case "solution": + app.BuildSolution = YamlParser.GetScalarValue(key, child.Value); + break; case "namespace": app.Namespace = YamlParser.GetScalarValue(key, child.Value); break; diff --git a/src/Microsoft.Tye.Hosting/Model/Application.cs b/src/Microsoft.Tye.Hosting/Model/Application.cs index 2a394a136..97f36e65d 100644 --- a/src/Microsoft.Tye.Hosting/Model/Application.cs +++ b/src/Microsoft.Tye.Hosting/Model/Application.cs @@ -39,6 +39,7 @@ public Application(string name, FileInfo source, int? dashboardPort, Dictionary< public Dictionary Items { get; } = new Dictionary(); public string? Network { get; set; } + public string? BuildSolution { get; set; } public void PopulateEnvironment(Service service, Action set, string defaultHost = "localhost") { diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 57920dad0..2e28e2fef 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -26,15 +26,19 @@ public class ProcessRunner : IApplicationProcessor private readonly ProcessRunnerOptions _options; private readonly ReplicaRegistry _replicaRegistry; + private WatchBuilderWorker _watchBuilderWorker; + public ProcessRunner(ILogger logger, ReplicaRegistry replicaRegistry, ProcessRunnerOptions options) { _logger = logger; _replicaRegistry = replicaRegistry; _options = options; + _watchBuilderWorker = new WatchBuilderWorker(logger); } public async Task StartAsync(Application application) { + _watchBuilderWorker.SolutionPath = application.BuildSolution; await PurgeFromPreviousRun(); await BuildAndRunProjects(application); @@ -356,12 +360,9 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? { if (service.Description.RunInfo is ProjectRunInfo) { - var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError: false, workingDirectory: application.ContextDirectory); - if (buildResult.ExitCode != 0) - { - _logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); - } - return buildResult.ExitCode; + var exitCode = await _watchBuilderWorker.buildProjectFileAsync(service.Status.ProjectFilePath, application.ContextDirectory); + _logger.LogInformation($"Built {service.Status.ProjectFilePath} with exit code {exitCode}"); + return exitCode; } return 0; diff --git a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs new file mode 100644 index 000000000..ecc1b47de --- /dev/null +++ b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Threading.Channels; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Build.Construction; + +namespace Microsoft.Tye.Hosting +{ + class WatchBuilderWorker + { + private readonly ILogger _logger; + private readonly Channel _queue; + private Task _processor; + private string? _solutionPath; + + public string? SolutionPath { get => _solutionPath; set => _solutionPath = value; } + + public WatchBuilderWorker(ILogger logger) + { + _logger = logger; + _queue = Channel.CreateUnbounded(); + _processor = Task.Run(ProcessTaskQueueAsync); + } + + class BuildRequest + { + public BuildRequest(string projectFilePath, string workingDirectory) + { + this.projectFilePath = projectFilePath; + this.workingDirectory = workingDirectory; + } + + public string projectFilePath { get; set; } + + public string workingDirectory { get; set; } + + private TaskCompletionSource _result = new TaskCompletionSource(); + + public Task task() + { + return _result.Task; + } + + public void complete(int exitCode) + { + if(!_result.TrySetResult(exitCode)) + throw new Exception("failed to set result"); + } + } + + public Task buildProjectFileAsync(string projectFilePath, string workingDirectory) { + var buildRequest = new BuildRequest(projectFilePath, workingDirectory); + _queue.Writer.WriteAsync(buildRequest); + return buildRequest.task(); + } + + public Task buildProjectFileAsyncImpl(string projectFilePath, string workingDirectory) { + _logger.LogInformation($"Building project ${projectFilePath}..."); + return ProcessUtil.RunAsync("dotnet", $"build \"{projectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory) + .ContinueWith((processTask) => { + var buildResult = processTask.Result; + if (buildResult.ExitCode != 0) + { + _logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + } + return buildResult.ExitCode; + }); + } + + private string GetProjectName(SolutionFile solution, string projectFile) + { + foreach(var project in solution.ProjectsInOrder) { + if(project.AbsolutePath == projectFile) + { + return project.ProjectName; + } + } + // TODO: error + return ""; + } + + private async Task ProcessTaskQueueAsync() + { + try + { + while (await _queue.Reader.WaitToReadAsync()) + { + var solutionBatch = new Dictionary>(); // store the list of promises + var projectBatch = new Dictionary>(); + // TODO: quiet time... maybe wait both...? + await Task.Delay(100); + + var solution = (SolutionPath != null) ? SolutionFile.Parse(SolutionPath) : null; + string targets = ""; + string workingDirectory = ""; // FIXME: should be set in the worker constructor + while (_queue.Reader.TryRead(out BuildRequest item)) + { + try { + if(workingDirectory.Length == 0) + { + workingDirectory = item.workingDirectory; + } + if(solution != null && solution.ProjectShouldBuild(item.projectFilePath)) + { + if(!solutionBatch.ContainsKey(item.projectFilePath)) + { + if(targets.Length > 0) + { + targets += ","; + } + targets += GetProjectName(solution, item.projectFilePath); + solutionBatch.Add(item.projectFilePath, new List()); + } + solutionBatch[item.projectFilePath].Add(item); + } + else + { + // this will also prevent us building multiple times if a project is used by multiple services + if(!projectBatch.ContainsKey(item.projectFilePath)) + { + projectBatch.Add(item.projectFilePath, new List()); + } + projectBatch[item.projectFilePath].Add(item); + } + } + catch (Exception e) + { + item.complete(-1); + } + } + + var tasks = new List(); + if(solutionBatch.Count > 0) + { + tasks.Add(Task.Run(async () => { + _logger.LogInformation("Building projects from solution: " + targets); + var buildResult = await ProcessUtil.RunAsync("dotnet", "msbuild -targets:" + targets, throwOnError: false, workingDirectory: workingDirectory); + if (buildResult.ExitCode != 0) + { + _logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + } + foreach(var project in solutionBatch) + { + foreach(var buildRequest in project.Value) + { + buildRequest.complete(buildResult.ExitCode); + } + } + })); + } + else + { + foreach(var project in projectBatch) + { + // FIXME: this is serial + tasks.Add(Task.Run(async () => { + var exitCode = await buildProjectFileAsyncImpl(project.Key, workingDirectory); + foreach(var buildRequest in project.Value) + { + buildRequest.complete(exitCode); + } + })); + } + } + + Task.WaitAll(tasks.ToArray()); + } + } + catch (OperationCanceledException) + { + // Prevent throwing if stoppingToken was signaled + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred executing task work item."); + } + } + } +} \ No newline at end of file diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 342564712..064028663 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -218,7 +218,11 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati services.Add(ingress.Name, new Service(description, ServiceSource.Host)); } - return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine) { Network = application.Network }; + return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine) + { + Network = application.Network, + BuildSolution = application.BuildSolution + }; } public static Tye.Hosting.Model.EnvironmentVariable ToHostingEnvironmentVariable(this EnvironmentVariableBuilder builder) From 06bdc5d8ab95ae82ed7163a34f039d24b873d374 Mon Sep 17 00:00:00 2001 From: James Lloyd Date: Tue, 28 Sep 2021 14:40:55 +0200 Subject: [PATCH 02/18] Fix some warnings --- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 2 +- src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 2e28e2fef..9534f7cb4 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -360,7 +360,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? { if (service.Description.RunInfo is ProjectRunInfo) { - var exitCode = await _watchBuilderWorker.buildProjectFileAsync(service.Status.ProjectFilePath, application.ContextDirectory); + var exitCode = await _watchBuilderWorker.buildProjectFileAsync(service.Status.ProjectFilePath!, application.ContextDirectory); _logger.LogInformation($"Built {service.Status.ProjectFilePath} with exit code {exitCode}"); return exitCode; } diff --git a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs index ecc1b47de..194094faa 100644 --- a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs +++ b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs @@ -124,7 +124,7 @@ private async Task ProcessTaskQueueAsync() projectBatch[item.projectFilePath].Add(item); } } - catch (Exception e) + catch (Exception) { item.complete(-1); } From 5c47ab41e39db792a05d51c0c4e4fa09f206a5a2 Mon Sep 17 00:00:00 2001 From: James Lloyd Date: Wed, 29 Sep 2021 08:58:40 +0200 Subject: [PATCH 03/18] 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. --- src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs index 194094faa..bf1e8738a 100644 --- a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs +++ b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs @@ -109,7 +109,7 @@ private async Task ProcessTaskQueueAsync() { targets += ","; } - targets += GetProjectName(solution, item.projectFilePath); + targets += GetProjectName(solution, item.projectFilePath); // note, assuming the default target is Build solutionBatch.Add(item.projectFilePath, new List()); } solutionBatch[item.projectFilePath].Add(item); @@ -135,7 +135,7 @@ private async Task ProcessTaskQueueAsync() { tasks.Add(Task.Run(async () => { _logger.LogInformation("Building projects from solution: " + targets); - var buildResult = await ProcessUtil.RunAsync("dotnet", "msbuild -targets:" + targets, throwOnError: false, workingDirectory: workingDirectory); + var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {SolutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); if (buildResult.ExitCode != 0) { _logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); From 7f518b517fe692c3142d73ae906c9f3739d8afae Mon Sep 17 00:00:00 2001 From: James Lloyd Date: Thu, 30 Sep 2021 08:50:47 +0200 Subject: [PATCH 04/18] Remove unnecessary function --- src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs b/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs index f7117bd7a..999a091e6 100644 --- a/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs +++ b/src/Microsoft.Tye.Core/MsBuild/SolutionFile.cs @@ -825,22 +825,6 @@ private void AddProjectToSolution(ProjectInSolution proj) _projectsInOrder.Add(proj); } - /// - /// Get the target name for the given project file in this solution, or null if the project file is not referenced by - /// this solution. - /// - /// projectFile - public string GetProjectName(string projectFile) - { - foreach(var project in _projects) { - if(project.Value.AbsolutePath == FileUtilities.FixFilePath(projectFile)) - { - return project.Value.GetOriginalProjectName(); - } - } - return null; - } - /// /// Validate relative path of a project /// From 298a24c56c4c0030ee214d7c052d870ab3a1465c Mon Sep 17 00:00:00 2001 From: James Lloyd Date: Thu, 14 Oct 2021 07:54:17 +0200 Subject: [PATCH 05/18] Handle failures building solution better --- .../WatchBuilderWorker.cs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs index bf1e8738a..340728d70 100644 --- a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs +++ b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs @@ -16,10 +16,10 @@ class WatchBuilderWorker public string? SolutionPath { get => _solutionPath; set => _solutionPath = value; } - public WatchBuilderWorker(ILogger logger) + public WatchBuilderWorker(ILogger logger) { - _logger = logger; - _queue = Channel.CreateUnbounded(); + _logger = logger; + _queue = Channel.CreateUnbounded(); _processor = Task.Run(ProcessTaskQueueAsync); } @@ -92,7 +92,7 @@ private async Task ProcessTaskQueueAsync() await Task.Delay(100); var solution = (SolutionPath != null) ? SolutionFile.Parse(SolutionPath) : null; - string targets = ""; + string targets = ""; string workingDirectory = ""; // FIXME: should be set in the worker constructor while (_queue.Reader.TryRead(out BuildRequest item)) { @@ -135,16 +135,23 @@ private async Task ProcessTaskQueueAsync() { tasks.Add(Task.Run(async () => { _logger.LogInformation("Building projects from solution: " + targets); - var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {SolutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); - if (buildResult.ExitCode != 0) + int exitCode = -1; + try { - _logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {SolutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); + if (buildResult.ExitCode != 0) + { + _logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + } + exitCode = buildResult.ExitCode; } - foreach(var project in solutionBatch) - { - foreach(var buildRequest in project.Value) + finally { + foreach(var project in solutionBatch) { - buildRequest.complete(buildResult.ExitCode); + foreach(var buildRequest in project.Value) + { + buildRequest.complete(exitCode); + } } } })); From 1616660efad58fc4dd91ce398a3cac4558f5a8af Mon Sep 17 00:00:00 2001 From: phoff Date: Tue, 1 Feb 2022 12:21:30 -0800 Subject: [PATCH 06/18] Styling updates. --- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 2 +- .../WatchBuilderWorker.cs | 91 ++++++++++--------- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 64a4b8d5f..856f0d410 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -362,7 +362,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? { if (service.Description.RunInfo is ProjectRunInfo) { - var exitCode = await _watchBuilderWorker.buildProjectFileAsync(service.Status.ProjectFilePath!, application.ContextDirectory); + var exitCode = await _watchBuilderWorker.BuildProjectFileAsync(service.Status.ProjectFilePath!, application.ContextDirectory); _logger.LogInformation($"Built {service.Status.ProjectFilePath} with exit code {exitCode}"); return exitCode; } diff --git a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs index 340728d70..300c047d2 100644 --- a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs +++ b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs @@ -1,3 +1,7 @@ +// 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.Threading.Channels; @@ -7,7 +11,7 @@ namespace Microsoft.Tye.Hosting { - class WatchBuilderWorker + internal class WatchBuilderWorker { private readonly ILogger _logger; private readonly Channel _queue; @@ -23,39 +27,13 @@ public WatchBuilderWorker(ILogger logger) _processor = Task.Run(ProcessTaskQueueAsync); } - class BuildRequest - { - public BuildRequest(string projectFilePath, string workingDirectory) - { - this.projectFilePath = projectFilePath; - this.workingDirectory = workingDirectory; - } - - public string projectFilePath { get; set; } - - public string workingDirectory { get; set; } - - private TaskCompletionSource _result = new TaskCompletionSource(); - - public Task task() - { - return _result.Task; - } - - public void complete(int exitCode) - { - if(!_result.TrySetResult(exitCode)) - throw new Exception("failed to set result"); - } - } - - public Task buildProjectFileAsync(string projectFilePath, string workingDirectory) { + public Task BuildProjectFileAsync(string projectFilePath, string workingDirectory) { var buildRequest = new BuildRequest(projectFilePath, workingDirectory); _queue.Writer.WriteAsync(buildRequest); - return buildRequest.task(); + return buildRequest.Task; } - public Task buildProjectFileAsyncImpl(string projectFilePath, string workingDirectory) { + private Task BuildProjectFileAsyncImpl(string projectFilePath, string workingDirectory) { _logger.LogInformation($"Building project ${projectFilePath}..."); return ProcessUtil.RunAsync("dotnet", $"build \"{projectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory) .ContinueWith((processTask) => { @@ -94,39 +72,39 @@ private async Task ProcessTaskQueueAsync() var solution = (SolutionPath != null) ? SolutionFile.Parse(SolutionPath) : null; string targets = ""; string workingDirectory = ""; // FIXME: should be set in the worker constructor - while (_queue.Reader.TryRead(out BuildRequest item)) + while (_queue.Reader.TryRead(out BuildRequest? item)) { try { if(workingDirectory.Length == 0) { - workingDirectory = item.workingDirectory; + workingDirectory = item.WorkingDirectory; } - if(solution != null && solution.ProjectShouldBuild(item.projectFilePath)) + if(solution != null && solution.ProjectShouldBuild(item.ProjectFilePath)) { - if(!solutionBatch.ContainsKey(item.projectFilePath)) + if(!solutionBatch.ContainsKey(item.ProjectFilePath)) { if(targets.Length > 0) { targets += ","; } - targets += GetProjectName(solution, item.projectFilePath); // note, assuming the default target is Build - solutionBatch.Add(item.projectFilePath, new List()); + targets += GetProjectName(solution, item.ProjectFilePath); // note, assuming the default target is Build + solutionBatch.Add(item.ProjectFilePath, new List()); } - solutionBatch[item.projectFilePath].Add(item); + solutionBatch[item.ProjectFilePath].Add(item); } else { // this will also prevent us building multiple times if a project is used by multiple services - if(!projectBatch.ContainsKey(item.projectFilePath)) + if(!projectBatch.ContainsKey(item.ProjectFilePath)) { - projectBatch.Add(item.projectFilePath, new List()); + projectBatch.Add(item.ProjectFilePath, new List()); } - projectBatch[item.projectFilePath].Add(item); + projectBatch[item.ProjectFilePath].Add(item); } } catch (Exception) { - item.complete(-1); + item.Complete(-1); } } @@ -150,7 +128,7 @@ private async Task ProcessTaskQueueAsync() { foreach(var buildRequest in project.Value) { - buildRequest.complete(exitCode); + buildRequest.Complete(exitCode); } } } @@ -162,10 +140,10 @@ private async Task ProcessTaskQueueAsync() { // FIXME: this is serial tasks.Add(Task.Run(async () => { - var exitCode = await buildProjectFileAsyncImpl(project.Key, workingDirectory); + var exitCode = await BuildProjectFileAsyncImpl(project.Key, workingDirectory); foreach(var buildRequest in project.Value) { - buildRequest.complete(exitCode); + buildRequest.Complete(exitCode); } })); } @@ -183,5 +161,30 @@ private async Task ProcessTaskQueueAsync() _logger.LogError(ex, "Error occurred executing task work item."); } } + + private class BuildRequest + { + private readonly TaskCompletionSource _result = new TaskCompletionSource(); + + public BuildRequest(string projectFilePath, string workingDirectory) + { + ProjectFilePath = projectFilePath; + WorkingDirectory = workingDirectory; + } + + public string ProjectFilePath { get; } + + public string WorkingDirectory { get; } + + public Task Task => _result.Task; + + public void Complete(int exitCode) + { + if(!_result.TrySetResult(exitCode)) + { + throw new Exception("failed to set result"); + } + } + } } } \ No newline at end of file From 373676561b2164491d03bbc53327e1681277dea1 Mon Sep 17 00:00:00 2001 From: phoff Date: Tue, 1 Feb 2022 15:07:04 -0800 Subject: [PATCH 07/18] Scaffold explicit start/stop of builds. --- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 14 ++- .../WatchBuilderWorker.cs | 93 ++++++++++++++----- 2 files changed, 79 insertions(+), 28 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 856f0d410..a3efefff7 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -26,27 +26,31 @@ public class ProcessRunner : IApplicationProcessor private readonly ProcessRunnerOptions _options; private readonly ReplicaRegistry _replicaRegistry; - private WatchBuilderWorker _watchBuilderWorker; + private readonly WatchBuilderWorker _watchBuilderWorker; public ProcessRunner(ILogger logger, ReplicaRegistry replicaRegistry, ProcessRunnerOptions options) { _logger = logger; _replicaRegistry = replicaRegistry; _options = options; + _watchBuilderWorker = new WatchBuilderWorker(logger); } public async Task StartAsync(Application application) { - _watchBuilderWorker.SolutionPath = application.BuildSolution; await PurgeFromPreviousRun(); + await _watchBuilderWorker.StartAsync(application.BuildSolution); + await BuildAndRunProjects(application); } - public Task StopAsync(Application application) + public async Task StopAsync(Application application) { - return KillRunningProcesses(application.Services); + await _watchBuilderWorker.StopAsync(); + + await KillRunningProcesses(application.Services); } private async Task BuildAndRunProjects(Application application) @@ -362,7 +366,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? { if (service.Description.RunInfo is ProjectRunInfo) { - var exitCode = await _watchBuilderWorker.BuildProjectFileAsync(service.Status.ProjectFilePath!, application.ContextDirectory); + var exitCode = await _watchBuilderWorker!.BuildProjectFileAsync(service.Status.ProjectFilePath!, application.ContextDirectory); _logger.LogInformation($"Built {service.Status.ProjectFilePath} with exit code {exitCode}"); return exitCode; } diff --git a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs index 300c047d2..837f8290a 100644 --- a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs +++ b/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs @@ -8,45 +8,84 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Build.Construction; +using System.Threading; namespace Microsoft.Tye.Hosting { - internal class WatchBuilderWorker + internal sealed class WatchBuilderWorker : IAsyncDisposable { + private CancellationTokenSource? _cancellationTokenSource; private readonly ILogger _logger; - private readonly Channel _queue; - private Task _processor; - private string? _solutionPath; - - public string? SolutionPath { get => _solutionPath; set => _solutionPath = value; } + private Task? _processor; + private Channel? _queue; public WatchBuilderWorker(ILogger logger) { _logger = logger; + } + + public async Task StartAsync(string? solutionPath) + { + await StopAsync(); + _queue = Channel.CreateUnbounded(); - _processor = Task.Run(ProcessTaskQueueAsync); + _cancellationTokenSource = new CancellationTokenSource(); + _processor = Task.Run(() => ProcessTaskQueueAsync(_logger, _queue.Reader, solutionPath, _cancellationTokenSource.Token)); + } + + public async Task StopAsync() + { + _queue?.Writer.TryComplete(); + _queue = null; + + _cancellationTokenSource?.Cancel(); + _cancellationTokenSource?.Dispose(); + _cancellationTokenSource = null; + + if (_processor != null) + { + await _processor; + + _processor = null; + } } - public Task BuildProjectFileAsync(string projectFilePath, string workingDirectory) { + public async Task BuildProjectFileAsync(string projectFilePath, string workingDirectory) { + if (_queue == null) + { + throw new InvalidOperationException("The worker is not running."); + } + var buildRequest = new BuildRequest(projectFilePath, workingDirectory); - _queue.Writer.WriteAsync(buildRequest); - return buildRequest.Task; + + await _queue.Writer.WriteAsync(buildRequest); + + return await buildRequest.Task; + } + + #region IAsyncDisposable Members + + public async ValueTask DisposeAsync() + { + await StopAsync(); } - private Task BuildProjectFileAsyncImpl(string projectFilePath, string workingDirectory) { - _logger.LogInformation($"Building project ${projectFilePath}..."); + #endregion + + private static Task BuildProjectFileAsyncImpl(ILogger logger, string projectFilePath, string workingDirectory) { + logger.LogInformation($"Building project ${projectFilePath}..."); return ProcessUtil.RunAsync("dotnet", $"build \"{projectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory) .ContinueWith((processTask) => { var buildResult = processTask.Result; if (buildResult.ExitCode != 0) { - _logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); } return buildResult.ExitCode; }); } - private string GetProjectName(SolutionFile solution, string projectFile) + private static string GetProjectName(SolutionFile solution, string projectFile) { foreach(var project in solution.ProjectsInOrder) { if(project.AbsolutePath == projectFile) @@ -58,21 +97,27 @@ private string GetProjectName(SolutionFile solution, string projectFile) return ""; } - private async Task ProcessTaskQueueAsync() + private static async Task ProcessTaskQueueAsync( + ILogger logger, + ChannelReader requestReader, + string? solutionPath, + CancellationToken cancellationToken) { + logger.LogInformation("Build Watcher: Watching for builds..."); + try { - while (await _queue.Reader.WaitToReadAsync()) + while (await requestReader.WaitToReadAsync(cancellationToken)) { var solutionBatch = new Dictionary>(); // store the list of promises var projectBatch = new Dictionary>(); // TODO: quiet time... maybe wait both...? await Task.Delay(100); - var solution = (SolutionPath != null) ? SolutionFile.Parse(SolutionPath) : null; + var solution = (solutionPath != null) ? SolutionFile.Parse(solutionPath) : null; string targets = ""; string workingDirectory = ""; // FIXME: should be set in the worker constructor - while (_queue.Reader.TryRead(out BuildRequest? item)) + while (requestReader.TryRead(out BuildRequest? item)) { try { if(workingDirectory.Length == 0) @@ -112,14 +157,14 @@ private async Task ProcessTaskQueueAsync() if(solutionBatch.Count > 0) { tasks.Add(Task.Run(async () => { - _logger.LogInformation("Building projects from solution: " + targets); + logger.LogInformation("Building projects from solution: " + targets); int exitCode = -1; try { - var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {SolutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); + var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {solutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); if (buildResult.ExitCode != 0) { - _logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); } exitCode = buildResult.ExitCode; } @@ -140,7 +185,7 @@ private async Task ProcessTaskQueueAsync() { // FIXME: this is serial tasks.Add(Task.Run(async () => { - var exitCode = await BuildProjectFileAsyncImpl(project.Key, workingDirectory); + var exitCode = await BuildProjectFileAsyncImpl(logger, project.Key, workingDirectory); foreach(var buildRequest in project.Value) { buildRequest.Complete(exitCode); @@ -158,8 +203,10 @@ private async Task ProcessTaskQueueAsync() } catch (Exception ex) { - _logger.LogError(ex, "Error occurred executing task work item."); + logger.LogError(ex, "Error occurred executing task work item."); } + + logger.LogInformation("Build Watcher: Done watching."); } private class BuildRequest From e825a0e38883cdd9cf5f0406a64d68e2937197c0 Mon Sep 17 00:00:00 2001 From: phoff Date: Wed, 2 Feb 2022 12:31:35 -0800 Subject: [PATCH 08/18] More styling updates. --- ...{WatchBuilderWorker.cs => BuildWatcher.cs} | 61 ++++++++++++------- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 4 +- 2 files changed, 41 insertions(+), 24 deletions(-) rename src/Microsoft.Tye.Hosting/{WatchBuilderWorker.cs => BuildWatcher.cs} (79%) diff --git a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs similarity index 79% rename from src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs rename to src/Microsoft.Tye.Hosting/BuildWatcher.cs index 837f8290a..331ae9ef4 100644 --- a/src/Microsoft.Tye.Hosting/WatchBuilderWorker.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -12,14 +12,14 @@ namespace Microsoft.Tye.Hosting { - internal sealed class WatchBuilderWorker : IAsyncDisposable + internal sealed class BuildWatcher : IAsyncDisposable { private CancellationTokenSource? _cancellationTokenSource; private readonly ILogger _logger; private Task? _processor; private Channel? _queue; - public WatchBuilderWorker(ILogger logger) + public BuildWatcher(ILogger logger) { _logger = logger; } @@ -109,47 +109,64 @@ private static async Task ProcessTaskQueueAsync( { while (await requestReader.WaitToReadAsync(cancellationToken)) { - var solutionBatch = new Dictionary>(); // store the list of promises - var projectBatch = new Dictionary>(); - // TODO: quiet time... maybe wait both...? - await Task.Delay(100); + logger.LogInformation("Build Watcher: Builds requested; waiting for more..."); + + // TODO: Consider using a quiet time. + await Task.Delay(TimeSpan.FromMilliseconds(250)); + + logger.LogInformation("Build Watcher: Getting requests..."); + + var requests = new List(); + + while (requestReader.TryRead(out var request)) + { + requests.Add(request); + } + + logger.LogInformation("Build Watcher: Processing {0} requests...", requests.Count); var solution = (solutionPath != null) ? SolutionFile.Parse(solutionPath) : null; string targets = ""; string workingDirectory = ""; // FIXME: should be set in the worker constructor - while (requestReader.TryRead(out BuildRequest? item)) + + var solutionBatch = new Dictionary>(); // store the list of promises + var projectBatch = new Dictionary>(); + + foreach (var request in requests) { - try { - if(workingDirectory.Length == 0) + try + { + if (workingDirectory.Length == 0) { - workingDirectory = item.WorkingDirectory; + workingDirectory = request.WorkingDirectory; } - if(solution != null && solution.ProjectShouldBuild(item.ProjectFilePath)) + + if (solution != null && solution.ProjectShouldBuild(request.ProjectFilePath)) { - if(!solutionBatch.ContainsKey(item.ProjectFilePath)) + if(!solutionBatch.ContainsKey(request.ProjectFilePath)) { if(targets.Length > 0) { targets += ","; } - targets += GetProjectName(solution, item.ProjectFilePath); // note, assuming the default target is Build - solutionBatch.Add(item.ProjectFilePath, new List()); + targets += GetProjectName(solution, request.ProjectFilePath); // note, assuming the default target is Build + solutionBatch.Add(request.ProjectFilePath, new List()); } - solutionBatch[item.ProjectFilePath].Add(item); + 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(item.ProjectFilePath)) + if(!projectBatch.ContainsKey(request.ProjectFilePath)) { - projectBatch.Add(item.ProjectFilePath, new List()); + projectBatch.Add(request.ProjectFilePath, new List()); } - projectBatch[item.ProjectFilePath].Add(item); + projectBatch[request.ProjectFilePath].Add(request); } } catch (Exception) { - item.Complete(-1); + request.Complete(-1); } } @@ -157,14 +174,14 @@ private static async Task ProcessTaskQueueAsync( if(solutionBatch.Count > 0) { tasks.Add(Task.Run(async () => { - logger.LogInformation("Building projects from solution: " + targets); + logger.LogInformation("Build Watcher: Building projects from solution: " + targets); int exitCode = -1; try { var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {solutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); if (buildResult.ExitCode != 0) { - logger.LogInformation("Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + logger.LogInformation("Build Watcher: Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); } exitCode = buildResult.ExitCode; } @@ -203,7 +220,7 @@ private static async Task ProcessTaskQueueAsync( } catch (Exception ex) { - logger.LogError(ex, "Error occurred executing task work item."); + logger.LogError(ex, "Build Watcher: Error occurred executing task work item."); } logger.LogInformation("Build Watcher: Done watching."); diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index a3efefff7..0e699cdb5 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -26,7 +26,7 @@ public class ProcessRunner : IApplicationProcessor private readonly ProcessRunnerOptions _options; private readonly ReplicaRegistry _replicaRegistry; - private readonly WatchBuilderWorker _watchBuilderWorker; + private readonly BuildWatcher _watchBuilderWorker; public ProcessRunner(ILogger logger, ReplicaRegistry replicaRegistry, ProcessRunnerOptions options) { @@ -34,7 +34,7 @@ public ProcessRunner(ILogger logger, ReplicaRegistry replicaRegistry, ProcessRun _replicaRegistry = replicaRegistry; _options = options; - _watchBuilderWorker = new WatchBuilderWorker(logger); + _watchBuilderWorker = new BuildWatcher(logger); } public async Task StartAsync(Application application) From 8ff450caca56efedeaa25f076a2cc065e9572347 Mon Sep 17 00:00:00 2001 From: phoff Date: Wed, 2 Feb 2022 13:58:43 -0800 Subject: [PATCH 09/18] Cleanup working directory. --- src/Microsoft.Tye.Hosting/BuildWatcher.cs | 70 ++++++++++------------ src/Microsoft.Tye.Hosting/ProcessRunner.cs | 4 +- 2 files changed, 34 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/BuildWatcher.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs index 331ae9ef4..c898c6387 100644 --- a/src/Microsoft.Tye.Hosting/BuildWatcher.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Build.Construction; using System.Threading; +using System.Linq; namespace Microsoft.Tye.Hosting { @@ -24,13 +25,13 @@ public BuildWatcher(ILogger logger) _logger = logger; } - public async Task StartAsync(string? solutionPath) + public async Task StartAsync(string? solutionPath, string workingDirectory) { await StopAsync(); _queue = Channel.CreateUnbounded(); _cancellationTokenSource = new CancellationTokenSource(); - _processor = Task.Run(() => ProcessTaskQueueAsync(_logger, _queue.Reader, solutionPath, _cancellationTokenSource.Token)); + _processor = Task.Run(() => ProcessTaskQueueAsync(_logger, _queue.Reader, solutionPath, workingDirectory, _cancellationTokenSource.Token)); } public async Task StopAsync() @@ -50,13 +51,13 @@ public async Task StopAsync() } } - public async Task BuildProjectFileAsync(string projectFilePath, string workingDirectory) { + public async Task BuildProjectFileAsync(string projectFilePath) { if (_queue == null) { throw new InvalidOperationException("The worker is not running."); } - var buildRequest = new BuildRequest(projectFilePath, workingDirectory); + var buildRequest = new BuildRequest(projectFilePath); await _queue.Writer.WriteAsync(buildRequest); @@ -101,6 +102,7 @@ private static async Task ProcessTaskQueueAsync( ILogger logger, ChannelReader requestReader, string? solutionPath, + string workingDirectory, CancellationToken cancellationToken) { logger.LogInformation("Build Watcher: Watching for builds..."); @@ -126,8 +128,6 @@ private static async Task ProcessTaskQueueAsync( logger.LogInformation("Build Watcher: Processing {0} requests...", requests.Count); var solution = (solutionPath != null) ? SolutionFile.Parse(solutionPath) : null; - string targets = ""; - string workingDirectory = ""; // FIXME: should be set in the worker constructor var solutionBatch = new Dictionary>(); // store the list of promises var projectBatch = new Dictionary>(); @@ -136,31 +136,23 @@ private static async Task ProcessTaskQueueAsync( { try { - if (workingDirectory.Length == 0) + if (solution?.ProjectShouldBuild(request.ProjectFilePath) == true) { - workingDirectory = request.WorkingDirectory; - } - - if (solution != null && solution.ProjectShouldBuild(request.ProjectFilePath)) - { - if(!solutionBatch.ContainsKey(request.ProjectFilePath)) + if (!solutionBatch.ContainsKey(request.ProjectFilePath)) { - if(targets.Length > 0) - { - targets += ","; - } - targets += GetProjectName(solution, request.ProjectFilePath); // note, assuming the default target is Build solutionBatch.Add(request.ProjectFilePath, new List()); } + 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)) + if (!projectBatch.ContainsKey(request.ProjectFilePath)) { projectBatch.Add(request.ProjectFilePath, new List()); } + projectBatch[request.ProjectFilePath].Add(request); } } @@ -171,8 +163,11 @@ private static async Task ProcessTaskQueueAsync( } var tasks = new List(); - if(solutionBatch.Count > 0) + + if (solutionBatch.Any()) { + var targets = String.Concat(",", solutionBatch.Keys.Select(key => GetProjectName(solution!, key))); + tasks.Add(Task.Run(async () => { logger.LogInformation("Build Watcher: Building projects from solution: " + targets); int exitCode = -1; @@ -186,9 +181,9 @@ private static async Task ProcessTaskQueueAsync( exitCode = buildResult.ExitCode; } finally { - foreach(var project in solutionBatch) + foreach (var project in solutionBatch) { - foreach(var buildRequest in project.Value) + foreach (var buildRequest in project.Value) { buildRequest.Complete(exitCode); } @@ -196,22 +191,24 @@ private static async Task ProcessTaskQueueAsync( } })); } - else + + foreach (var project in projectBatch) { - foreach(var project in projectBatch) - { - // FIXME: this is serial - tasks.Add(Task.Run(async () => { - var exitCode = await BuildProjectFileAsyncImpl(logger, project.Key, workingDirectory); - foreach(var buildRequest in project.Value) + // FIXME: this is serial + tasks.Add( + Task.Run( + async () => { - buildRequest.Complete(exitCode); - } - })); - } + var exitCode = await BuildProjectFileAsyncImpl(logger, project.Key, workingDirectory); + + foreach (var buildRequest in project.Value) + { + buildRequest.Complete(exitCode); + } + })); } - Task.WaitAll(tasks.ToArray()); + await Task.WhenAll(tasks); } } catch (OperationCanceledException) @@ -230,16 +227,13 @@ private class BuildRequest { private readonly TaskCompletionSource _result = new TaskCompletionSource(); - public BuildRequest(string projectFilePath, string workingDirectory) + public BuildRequest(string projectFilePath) { ProjectFilePath = projectFilePath; - WorkingDirectory = workingDirectory; } public string ProjectFilePath { get; } - public string WorkingDirectory { get; } - public Task Task => _result.Task; public void Complete(int exitCode) diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 0e699cdb5..4dc8f1248 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -41,7 +41,7 @@ public async Task StartAsync(Application application) { await PurgeFromPreviousRun(); - await _watchBuilderWorker.StartAsync(application.BuildSolution); + await _watchBuilderWorker.StartAsync(application.BuildSolution, application.ContextDirectory); await BuildAndRunProjects(application); } @@ -366,7 +366,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? { if (service.Description.RunInfo is ProjectRunInfo) { - var exitCode = await _watchBuilderWorker!.BuildProjectFileAsync(service.Status.ProjectFilePath!, application.ContextDirectory); + var exitCode = await _watchBuilderWorker!.BuildProjectFileAsync(service.Status.ProjectFilePath!); _logger.LogInformation($"Built {service.Status.ProjectFilePath} with exit code {exitCode}"); return exitCode; } From 181a239ae67748477d9feac64e3df582b9fb4e22 Mon Sep 17 00:00:00 2001 From: phoff Date: Wed, 2 Feb 2022 14:27:56 -0800 Subject: [PATCH 10/18] Make build result more consistent. --- src/Microsoft.Tye.Hosting/BuildWatcher.cs | 73 ++++++++++++----------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/BuildWatcher.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs index c898c6387..354eaf9ab 100644 --- a/src/Microsoft.Tye.Hosting/BuildWatcher.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -162,52 +162,55 @@ private static async Task ProcessTaskQueueAsync( } } - var tasks = new List(); - - if (solutionBatch.Any()) + async Task WithRequestCompletion(IEnumerable requests, Func> buildFunc) { - var targets = String.Concat(",", solutionBatch.Keys.Select(key => GetProjectName(solution!, key))); + try + { + int exitCode = await buildFunc(); - tasks.Add(Task.Run(async () => { - logger.LogInformation("Build Watcher: Building projects from solution: " + targets); - int exitCode = -1; - try + foreach (var request in requests) { - var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {solutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); - if (buildResult.ExitCode != 0) - { - logger.LogInformation("Build Watcher: Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); - } - exitCode = buildResult.ExitCode; + request.Complete(exitCode); } - finally { - foreach (var project in solutionBatch) - { - foreach (var buildRequest in project.Value) - { - buildRequest.Complete(exitCode); - } - } + } + catch (Exception ex) + { + foreach (var request in requests) + { + request.Complete(ex); } - })); + } } - foreach (var project in projectBatch) + var tasks = new List(); + + if (solutionBatch.Any()) { - // FIXME: this is serial + var targets = String.Concat(",", solutionBatch.Keys.Select(key => GetProjectName(solution!, key))); + tasks.Add( - Task.Run( - async () => + WithRequestCompletion( + solutionBatch.Values.SelectMany(x => x), + async () => { - var exitCode = await BuildProjectFileAsyncImpl(logger, project.Key, workingDirectory); + logger.LogInformation("Build Watcher: Building projects from solution: " + targets); + + var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {solutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory); - foreach (var buildRequest in project.Value) + if (buildResult.ExitCode != 0) { - buildRequest.Complete(exitCode); + logger.LogInformation("Build Watcher: Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); } + + return buildResult.ExitCode; })); } + foreach (var project in projectBatch) + { + tasks.Add(WithRequestCompletion(project.Value, () => BuildProjectFileAsyncImpl(logger, project.Key, workingDirectory))); + } + await Task.WhenAll(tasks); } } @@ -238,10 +241,12 @@ public BuildRequest(string projectFilePath) public void Complete(int exitCode) { - if(!_result.TrySetResult(exitCode)) - { - throw new Exception("failed to set result"); - } + _result.TrySetResult(exitCode); + } + + public void Complete(Exception ex) + { + _result.TrySetException(ex); } } } From b56236210be0bf58c1962f86c5c4c4aa08394906 Mon Sep 17 00:00:00 2001 From: phoff Date: Wed, 2 Feb 2022 15:41:09 -0800 Subject: [PATCH 11/18] Tweak logging. --- src/Microsoft.Tye.Hosting/BuildWatcher.cs | 79 ++++++++++++++--------- 1 file changed, 47 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/BuildWatcher.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs index 354eaf9ab..e1e071d1e 100644 --- a/src/Microsoft.Tye.Hosting/BuildWatcher.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -73,9 +73,9 @@ public async ValueTask DisposeAsync() #endregion - private static Task BuildProjectFileAsyncImpl(ILogger logger, string projectFilePath, string workingDirectory) { + private static Task BuildProjectFileAsyncImpl(ILogger logger, string projectFilePath, string workingDirectory, CancellationToken cancellationToken) { logger.LogInformation($"Building project ${projectFilePath}..."); - return ProcessUtil.RunAsync("dotnet", $"build \"{projectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory) + return ProcessUtil.RunAsync("dotnet", $"build \"{projectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory, cancellationToken: cancellationToken) .ContinueWith((processTask) => { var buildResult = processTask.Result; if (buildResult.ExitCode != 0) @@ -111,10 +111,11 @@ private static async Task ProcessTaskQueueAsync( { while (await requestReader.WaitToReadAsync(cancellationToken)) { - logger.LogInformation("Build Watcher: Builds requested; waiting for more..."); + var delay = TimeSpan.FromMilliseconds(250); - // TODO: Consider using a quiet time. - await Task.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..."); @@ -125,7 +126,7 @@ private static async Task ProcessTaskQueueAsync( requests.Add(request); } - logger.LogInformation("Build Watcher: Processing {0} requests...", requests.Count); + logger.LogInformation("Build Watcher: Processing {Count} requests...", requests.Count); var solution = (solutionPath != null) ? SolutionFile.Parse(solutionPath) : null; @@ -134,31 +135,24 @@ private static async Task ProcessTaskQueueAsync( foreach (var request in requests) { - try + if (solution?.ProjectShouldBuild(request.ProjectFilePath) == true) { - if (solution?.ProjectShouldBuild(request.ProjectFilePath) == true) + if (!solutionBatch.ContainsKey(request.ProjectFilePath)) { - if (!solutionBatch.ContainsKey(request.ProjectFilePath)) - { - solutionBatch.Add(request.ProjectFilePath, new List()); - } - - solutionBatch[request.ProjectFilePath].Add(request); + solutionBatch.Add(request.ProjectFilePath, new List()); } - 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()); - } - projectBatch[request.ProjectFilePath].Add(request); - } + solutionBatch[request.ProjectFilePath].Add(request); } - catch (Exception) + else { - request.Complete(-1); + // 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()); + } + + projectBatch[request.ProjectFilePath].Add(request); } } @@ -186,20 +180,20 @@ async Task WithRequestCompletion(IEnumerable requests, Func GetProjectName(solution!, key))); + 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 projects from solution: " + targets); + 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); + var buildResult = await ProcessUtil.RunAsync("dotnet", $"msbuild {solutionPath} -target:{targets}", throwOnError: false, workingDirectory: workingDirectory, cancellationToken: cancellationToken); if (buildResult.ExitCode != 0) { - logger.LogInformation("Build Watcher: Building solution failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); + logger.LogInformation("Build Watcher: Solution build failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); } return buildResult.ExitCode; @@ -208,19 +202,40 @@ async Task WithRequestCompletion(IEnumerable requests, Func BuildProjectFileAsyncImpl(logger, project.Key, workingDirectory))); + 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) { - // Prevent throwing if stoppingToken was signaled + // NO-OP: Trap exception due to cancellation. } catch (Exception ex) { - logger.LogError(ex, "Build Watcher: Error occurred executing task work item."); + logger.LogError(ex, "Build Watcher: Error while processing builds."); } logger.LogInformation("Build Watcher: Done watching."); From 1711ea5e7225fe9a0ba891585583bd2a686f2e03 Mon Sep 17 00:00:00 2001 From: phoff Date: Wed, 2 Feb 2022 15:49:19 -0800 Subject: [PATCH 12/18] Update formatting. --- src/Microsoft.Tye.Hosting/BuildWatcher.cs | 43 +++++++++-------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/BuildWatcher.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs index e1e071d1e..20ef595fe 100644 --- a/src/Microsoft.Tye.Hosting/BuildWatcher.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -1,15 +1,15 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Extensions.Logging; using Microsoft.Build.Construction; -using System.Threading; -using System.Linq; +using Microsoft.Extensions.Logging; namespace Microsoft.Tye.Hosting { @@ -51,7 +51,8 @@ public async Task StopAsync() } } - public async Task BuildProjectFileAsync(string projectFilePath) { + public async Task BuildProjectFileAsync(string projectFilePath) + { if (_queue == null) { throw new InvalidOperationException("The worker is not running."); @@ -73,29 +74,17 @@ public async ValueTask DisposeAsync() #endregion - private static Task BuildProjectFileAsyncImpl(ILogger logger, string projectFilePath, string workingDirectory, CancellationToken cancellationToken) { - logger.LogInformation($"Building project ${projectFilePath}..."); - return ProcessUtil.RunAsync("dotnet", $"build \"{projectFilePath}\" /nologo", throwOnError: false, workingDirectory: workingDirectory, cancellationToken: cancellationToken) - .ContinueWith((processTask) => { - var buildResult = processTask.Result; - if (buildResult.ExitCode != 0) - { - logger.LogInformation("Building projects failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, buildResult.ExitCode); - } - return buildResult.ExitCode; - }); - } - private static string GetProjectName(SolutionFile solution, string projectFile) { - foreach(var project in solution.ProjectsInOrder) { - if(project.AbsolutePath == projectFile) + foreach (var project in solution.ProjectsInOrder) + { + if (project.AbsolutePath == projectFile) { return project.ProjectName; } } - // TODO: error - return ""; + + throw new InvalidOperationException($"Could not find project in solution: {projectFile}"); } private static async Task ProcessTaskQueueAsync( @@ -185,10 +174,10 @@ async Task WithRequestCompletion(IEnumerable requests, Func x), - async () => + 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) @@ -208,14 +197,14 @@ async Task WithRequestCompletion(IEnumerable requests, Func { 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; })); } @@ -265,4 +254,4 @@ public void Complete(Exception ex) } } } -} \ No newline at end of file +} From 85440d55731cb31c3a13de1e07834c222621db14 Mon Sep 17 00:00:00 2001 From: phoff Date: Wed, 2 Feb 2022 15:53:42 -0800 Subject: [PATCH 13/18] Update more formatting. --- src/tye/ApplicationBuilderExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tye/ApplicationBuilderExtensions.cs b/src/tye/ApplicationBuilderExtensions.cs index 0d5182664..6f1ff5c45 100644 --- a/src/tye/ApplicationBuilderExtensions.cs +++ b/src/tye/ApplicationBuilderExtensions.cs @@ -221,8 +221,8 @@ public static Application ToHostingApplication(this ApplicationBuilder applicati services.Add(ingress.Name, new Service(description, ServiceSource.Host)); } - return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine) - { + return new Application(application.Name, application.Source, application.DashboardPort, services, application.ContainerEngine) + { Network = application.Network, BuildSolution = application.BuildSolution }; From bb1ae725b8e011dd986c240d55190178f3acab38 Mon Sep 17 00:00:00 2001 From: phoff Date: Thu, 3 Feb 2022 09:41:46 -0800 Subject: [PATCH 14/18] Add locks for critical areas. --- src/Microsoft.Tye.Hosting/BuildWatcher.cs | 84 +++++++++++++++-------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/BuildWatcher.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs index 20ef595fe..e958d5000 100644 --- a/src/Microsoft.Tye.Hosting/BuildWatcher.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -16,6 +16,7 @@ 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? _queue; @@ -25,55 +26,84 @@ public BuildWatcher(ILogger logger) _logger = logger; } - public async Task StartAsync(string? solutionPath, string workingDirectory) + public Task StartAsync(string? solutionPath, string workingDirectory) { - await StopAsync(); + return WithLockAsync( + async () => + { + await ResetAsync(); - _queue = Channel.CreateUnbounded(); - _cancellationTokenSource = new CancellationTokenSource(); - _processor = Task.Run(() => ProcessTaskQueueAsync(_logger, _queue.Reader, solutionPath, workingDirectory, _cancellationTokenSource.Token)); + _queue = Channel.CreateUnbounded(); + _cancellationTokenSource = new CancellationTokenSource(); + _processor = Task.Run(() => ProcessTaskQueueAsync(_logger, _queue.Reader, solutionPath, workingDirectory, _cancellationTokenSource.Token)); + }); } - public async Task StopAsync() + public Task StopAsync() { - _queue?.Writer.TryComplete(); - _queue = null; - - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; - - if (_processor != null) - { - await _processor; - - _processor = null; - } + return WithLockAsync(ResetAsync); } public async Task BuildProjectFileAsync(string projectFilePath) { - if (_queue == null) - { - throw new InvalidOperationException("The worker is not running."); - } + var request = new BuildRequest(projectFilePath); - var buildRequest = new BuildRequest(projectFilePath); + await WithLockAsync( + async () => + { + if (_queue == null) + { + throw new InvalidOperationException("The worker is not running."); + } - await _queue.Writer.WriteAsync(buildRequest); + await _queue.Writer.WriteAsync(request); + }); - return await buildRequest.Task; + return await request.Task; } #region IAsyncDisposable Members public async ValueTask DisposeAsync() { - await StopAsync(); + await WithLockAsync(ResetAsync); + + _lock.Dispose(); } #endregion + private async Task WithLockAsync(Func action) + { + await _lock.WaitAsync(); + + try + { + await action(); + } + finally + { + _lock.Release(); + } + } + + private async Task ResetAsync() + { + _queue?.Writer.TryComplete(); + _queue = 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) From b901e2de5802114685eef748a10ead82b0aabb97 Mon Sep 17 00:00:00 2001 From: phoff Date: Thu, 3 Feb 2022 10:01:05 -0800 Subject: [PATCH 15/18] Update schema and reference doc. --- docs/reference/schema.md | 4 ++++ src/schema/tye-schema.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/reference/schema.md b/docs/reference/schema.md index 0b1584443..0c6dce47e 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -101,6 +101,10 @@ Specifies the list of ingresses. Specifies the list of services. Applications must have at least one service. +#### `solution` (string) + +Indicates the solution to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution can help reduce repeated builds of shared libraries when in watch mode. + ## Service `Service` elements appear in a list within the `services` root property. diff --git a/src/schema/tye-schema.json b/src/schema/tye-schema.json index f7fcf9da1..2c7066a33 100644 --- a/src/schema/tye-schema.json +++ b/src/schema/tye-schema.json @@ -45,6 +45,10 @@ "$ref": "#/definitions/extension" } }, + "solution": { + "description": "Indicates the solution to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution can help reduce repeated builds of shared libraries when in watch mode.", + "type": "string" + }, "services": { "description": "The application's services.", "type": "array", From 3acd282c565ba1068663023428d90acb5132c9f2 Mon Sep 17 00:00:00 2001 From: phoff Date: Thu, 3 Feb 2022 11:05:31 -0800 Subject: [PATCH 16/18] Fix formatting. --- src/Microsoft.Tye.Hosting/BuildWatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.Tye.Hosting/BuildWatcher.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs index e958d5000..60c1e99a0 100644 --- a/src/Microsoft.Tye.Hosting/BuildWatcher.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -76,7 +76,7 @@ public async ValueTask DisposeAsync() private async Task WithLockAsync(Func action) { await _lock.WaitAsync(); - + try { await action(); From 5a7b5cdcae79bf22f61bbeaf04084632c785097d Mon Sep 17 00:00:00 2001 From: phoff Date: Mon, 7 Feb 2022 14:15:54 -0800 Subject: [PATCH 17/18] Updates per PR feedback. --- src/Microsoft.Tye.Hosting/BuildWatcher.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Tye.Hosting/BuildWatcher.cs b/src/Microsoft.Tye.Hosting/BuildWatcher.cs index 60c1e99a0..efae0d57a 100644 --- a/src/Microsoft.Tye.Hosting/BuildWatcher.cs +++ b/src/Microsoft.Tye.Hosting/BuildWatcher.cs @@ -89,12 +89,18 @@ private async Task WithLockAsync(Func action) private async Task ResetAsync() { - _queue?.Writer.TryComplete(); - _queue = null; + if (_queue != null) + { + _queue.Writer.TryComplete(); + _queue = null; + } - _cancellationTokenSource?.Cancel(); - _cancellationTokenSource?.Dispose(); - _cancellationTokenSource = null; + if (_cancellationTokenSource != null) + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } if (_processor != null) { From 931558a99a68ad0ce654e90f5bdfe7af1ecbbd08 Mon Sep 17 00:00:00 2001 From: phoff Date: Tue, 8 Feb 2022 11:53:30 -0800 Subject: [PATCH 18/18] More updates per PR feedback. --- docs/reference/schema.md | 2 +- src/schema/tye-schema.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference/schema.md b/docs/reference/schema.md index 0c6dce47e..66b73e4eb 100644 --- a/docs/reference/schema.md +++ b/docs/reference/schema.md @@ -103,7 +103,7 @@ Specifies the list of services. Applications must have at least one service. #### `solution` (string) -Indicates the solution to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution can help reduce repeated builds of shared libraries when in watch mode. +Indicates the solution file (.sln) or filter (.slnf) to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution [filter] can help reduce repeated builds of shared libraries when in watch mode. ## Service diff --git a/src/schema/tye-schema.json b/src/schema/tye-schema.json index 2c7066a33..23dc0a895 100644 --- a/src/schema/tye-schema.json +++ b/src/schema/tye-schema.json @@ -46,7 +46,7 @@ } }, "solution": { - "description": "Indicates the solution to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution can help reduce repeated builds of shared libraries when in watch mode.", + "description": "Indicates the solution file (.sln) or filter (.slnf) to use when building project-based services in watch mode. If omitted, those services will be built individually. Specifying the solution [filter] can help reduce repeated builds of shared libraries when in watch mode.", "type": "string" }, "services": {