diff --git a/src/Microsoft.Tye.Core/ProcessSpec.cs b/src/Microsoft.Tye.Core/ProcessSpec.cs index 7952726c..fb05f45f 100644 --- a/src/Microsoft.Tye.Core/ProcessSpec.cs +++ b/src/Microsoft.Tye.Core/ProcessSpec.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Threading.Tasks; namespace Microsoft.Tye { @@ -16,8 +17,10 @@ namespace Microsoft.Tye public IDictionary EnvironmentVariables { get; set; } = new Dictionary(); public string? Arguments { get; set; } public Action? OutputData { get; set; } + public Func>? Build { get; set; } public Action? ErrorData { get; set; } public Action? OnStart { get; set; } + public Action? OnStop { get; set; } public string? ShortDisplayName() => Path.GetFileNameWithoutExtension(Executable); } diff --git a/src/Microsoft.Tye.Core/ProcessUtil.cs b/src/Microsoft.Tye.Core/ProcessUtil.cs index 0c3f6000..4781c8fe 100644 --- a/src/Microsoft.Tye.Core/ProcessUtil.cs +++ b/src/Microsoft.Tye.Core/ProcessUtil.cs @@ -26,6 +26,7 @@ namespace Microsoft.Tye Action? outputDataReceived = null, Action? errorDataReceived = null, Action? onStart = null, + Action? onStop = null, CancellationToken cancellationToken = default) { using var process = new Process() @@ -136,12 +137,14 @@ namespace Microsoft.Tye } } - return await processLifetimeTask.Task; + var processResult = await processLifetimeTask.Task; + onStop?.Invoke(processResult.ExitCode); + return processResult; } public static Task RunAsync(ProcessSpec processSpec, CancellationToken cancellationToken = default, bool throwOnError = true) { - return RunAsync(processSpec.Executable!, processSpec.Arguments!, processSpec.WorkingDirectory, throwOnError: throwOnError, processSpec.EnvironmentVariables, processSpec.OutputData, processSpec.ErrorData, processSpec.OnStart, cancellationToken); + return RunAsync(processSpec.Executable!, processSpec.Arguments!, processSpec.WorkingDirectory, throwOnError: throwOnError, processSpec.EnvironmentVariables, processSpec.OutputData, processSpec.ErrorData, processSpec.OnStart, processSpec.OnStop, cancellationToken); } public static void KillProcess(int pid) diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index d156677f..3fb9b203 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -237,13 +237,16 @@ namespace Microsoft.Tye.Hosting { var replica = serviceName + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); var status = new ProcessStatus(service, replica); - service.Replicas[replica] = status; using var stoppingCts = new CancellationTokenSource(); status.StoppingTokenSource = stoppingCts; await using var _ = processInfo.StoppedTokenSource.Token.Register(() => status.StoppingTokenSource.Cancel()); - service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); + if (!_options.Watch) + { + service.Replicas[replica] = status; + service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); + } // This isn't your host name environment["APP_INSTANCE"] = replica; @@ -297,14 +300,56 @@ namespace Microsoft.Tye.Hosting status.Pid = pid; WriteReplicaToStore(pid.ToString()); + + if (_options.Watch && service.Description.RunInfo is ProjectRunInfo runInfo) + { + // OnStart/OnStop will be called multiple times for watch. + // Watch will constantly be adding and removing from the list, so only add here for watch. + service.Replicas[replica] = status; + service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); + } + service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status)); + }, + OnStop = exitCode => + { + status.ExitCode = exitCode; + + if (status.Pid != null) + { + service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status)); + } + + if (!_options.Watch) + { + // Only increase backoff when not watching project as watch will wait for file changes before rebuild. + backOff *= 2; + } + + service.Restarts++; + + service.Replicas.TryRemove(replica, out var _); + service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); + + if (status.ExitCode != null) + { + _logger.LogInformation("{ServiceName} process exited with exit code {ExitCode}", replica, status.ExitCode); + } + }, + Build = async () => + { + 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; } }; if (_options.Watch && service.Description.RunInfo is ProjectRunInfo runInfo) { var projectFile = runInfo.ProjectFile.FullName; - var fileSetFactory = new MsBuildFileSetFactory(_logger, projectFile, waitOnError: true, @@ -316,16 +361,8 @@ namespace Microsoft.Tye.Hosting } else { - var result = await ProcessUtil.RunAsync(processInfo, status.StoppingTokenSource.Token, throwOnError: false); - - status.ExitCode = result.ExitCode; - - if (status.Pid != null) - { - service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status)); - } + await ProcessUtil.RunAsync(processInfo, status.StoppingTokenSource.Token, throwOnError: false); } - } catch (Exception ex) { @@ -340,19 +377,6 @@ namespace Microsoft.Tye.Hosting // Swallow cancellation exceptions and continue } } - - backOff *= 2; - - service.Restarts++; - - if (status.ExitCode != null) - { - _logger.LogInformation("{ServiceName} process exited with exit code {ExitCode}", replica, status.ExitCode); - } - - // Remove the replica from the set - service.Replicas.TryRemove(replica, out var _); - service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); } } diff --git a/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs b/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs index 77fb42e1..f2c97ca7 100644 --- a/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs +++ b/src/Microsoft.Tye.Hosting/Watch/DotNetWatcher.cs @@ -31,6 +31,11 @@ namespace Microsoft.DotNet.Watcher while (true) { + if (cancellationToken.IsCancellationRequested) + { + return; + } + processSpec.EnvironmentVariables["DOTNET_WATCH_ITERATION"] = iteration.ToString(CultureInfo.InvariantCulture); iteration++; @@ -42,11 +47,6 @@ namespace Microsoft.DotNet.Watcher return; } - if (cancellationToken.IsCancellationRequested) - { - return; - } - using (var currentRunCancellationSource = new CancellationTokenSource()) using (var combinedCancellationSource = CancellationTokenSource.CreateLinkedTokenSource( cancellationToken, @@ -95,6 +95,21 @@ namespace Microsoft.DotNet.Watcher { _logger.LogInformation($"watch: File changed: {fileSetTask.Result}"); } + + if (processSpec.Build != null) + { + while (true) + { + var exitCode = await processSpec.Build(); + if (exitCode == 0) + { + break; + // Build failed, keep retrying builds until successful build. + } + + await fileSetWatcher.GetChangedFileAsync(cancellationToken, () => _logger.LogWarning("Waiting for a file to change before restarting dotnet...")); + } + } } } } diff --git a/test/E2ETest/TyeRunTests.cs b/test/E2ETest/TyeRunTests.cs index 831f7ccf..b227593b 100644 --- a/test/E2ETest/TyeRunTests.cs +++ b/test/E2ETest/TyeRunTests.cs @@ -291,7 +291,7 @@ namespace E2ETest return; } - await Task.Delay(500); + await Task.Delay(5000); } throw new Exception("Failed to relaunch project with dotnet watch");