diff --git a/src/Microsoft.Tye.Core/HostOptions.cs b/src/Microsoft.Tye.Core/HostOptions.cs index 73135cfd8..f6dc8d227 100644 --- a/src/Microsoft.Tye.Core/HostOptions.cs +++ b/src/Microsoft.Tye.Core/HostOptions.cs @@ -12,6 +12,8 @@ public class HostOptions public List Debug { get; } = new List(); + public List NoStart { get; } = new List(); + public string? DistributedTraceProvider { get; set; } public bool Docker { get; set; } diff --git a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor index cf06ba18b..f3841e00d 100644 --- a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor +++ b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor @@ -19,6 +19,7 @@ Replicas Restarts Logs + Actions @@ -92,6 +93,23 @@ @service.Replicas.Count/@service.Description.Replicas @service.Restarts View + + @if (CanStartStop(service)) + { + if (service.Replicas.Count == 0) + { + + } + else + { + + } + } + } } @@ -99,9 +117,14 @@ @code { - + static readonly ServiceType[] stopables = new[] { ServiceType.Container, ServiceType.Executable, ServiceType.Project, ServiceType.Function }; private List _subscriptions = new List(); + bool CanStartStop(Service? service) + { + return service != null && stopables.Contains(service.ServiceType); + } + string GetUrl(ServiceBinding b) { return $"{(b.Protocol ?? "tcp")}://{b.Host ?? "localhost"}:{b.Port}"; @@ -120,6 +143,31 @@ InvokeAsync(() => StateHasChanged()); } + private async Task StartServiceAsync(Service service) + { + if (service.ServiceType == ServiceType.Container) + { + await DockerRunner.RestartContainerAsync(service); + } + else + { + await ProcessRunner.RestartService(service); + } + } + + private async Task StopServiceAsync(Service service) + { + if (service.ServiceType == ServiceType.Container) + { + await DockerRunner.StopContainerAsync(service); + } + else + { + await ProcessRunner.KillProcessAsync(service); + } + } + + void IDisposable.Dispose() { _subscriptions.ForEach(d => d.Dispose()); diff --git a/src/Microsoft.Tye.Hosting/DockerRunner.cs b/src/Microsoft.Tye.Hosting/DockerRunner.cs index 2284f5517..ac9f14f40 100644 --- a/src/Microsoft.Tye.Hosting/DockerRunner.cs +++ b/src/Microsoft.Tye.Hosting/DockerRunner.cs @@ -24,11 +24,13 @@ public class DockerRunner : IApplicationProcessor private readonly ILogger _logger; private readonly ReplicaRegistry _replicaRegistry; + private readonly DockerRunnerOptions _options; - public DockerRunner(ILogger logger, ReplicaRegistry replicaRegistry) + public DockerRunner(ILogger logger, ReplicaRegistry replicaRegistry, DockerRunnerOptions options) { _logger = logger; _replicaRegistry = replicaRegistry; + _options = options; } public async Task StartAsync(Application application) @@ -537,6 +539,7 @@ Task DockerRunAsync(CancellationToken cancellationToken) return Task.WhenAll(tasks); } + var dockerInfo = new DockerInformation(); async Task BuildAndRunAsync(CancellationToken cancellationToken) { await DockerBuildAsync(cancellationToken); @@ -544,8 +547,13 @@ async Task BuildAndRunAsync(CancellationToken cancellationToken) await DockerRunAsync(cancellationToken); } - var dockerInfo = new DockerInformation(); - dockerInfo.Task = BuildAndRunAsync(dockerInfo.StoppingTokenSource.Token); + dockerInfo.SetBuildAndRunTask(BuildAndRunAsync); + + if (!_options.ManualStartServices && + !(_options.ServicesNotToStart?.Contains(service.Description.Name, StringComparer.OrdinalIgnoreCase) ?? false)) + { + dockerInfo.BuildAndRun(); + } service.Items[typeof(DockerInformation)] = dockerInfo; } @@ -587,13 +595,25 @@ private static void PrintStdOutAndErr(Service service, string replica, ProcessRe } } - private Task StopContainerAsync(Service service) + public static async Task RestartContainerAsync(Service service) + { + if (service.Items.TryGetValue(typeof(DockerInformation), out var value) && value is DockerInformation di) + { + await StopContainerAsync(service); + + di.BuildAndRun(); + service.Restarts++; + await di.Task; + } + } + + public static Task StopContainerAsync(Service service) { if (service.Items.TryGetValue(typeof(DockerInformation), out var value) && value is DockerInformation di) { - di.StoppingTokenSource.Cancel(); + di.CancelAndResetStoppingTokenSource(); + return di.Task ?? Task.CompletedTask; - return di.Task; } return Task.CompletedTask; @@ -601,8 +621,27 @@ private Task StopContainerAsync(Service service) private class DockerInformation { - public Task Task { get; set; } = default!; - public CancellationTokenSource StoppingTokenSource { get; } = new CancellationTokenSource(); + private Func? _buildAndRunAsync; + + public Task Task { get; private set; } = default!; + public CancellationTokenSource StoppingTokenSource { get; private set; } = new CancellationTokenSource(); + + public void SetBuildAndRunTask(Func func) + { + _buildAndRunAsync = func; + } + + public void BuildAndRun() + { + Task = _buildAndRunAsync?.Invoke(StoppingTokenSource.Token) ?? Task.CompletedTask; + } + + internal void CancelAndResetStoppingTokenSource() + { + StoppingTokenSource.Cancel(); + StoppingTokenSource.Dispose(); + StoppingTokenSource = new CancellationTokenSource(); + } } private class DockerApplicationInformation diff --git a/src/Microsoft.Tye.Hosting/DockerRunnerOptions.cs b/src/Microsoft.Tye.Hosting/DockerRunnerOptions.cs new file mode 100644 index 000000000..5c5a6bd39 --- /dev/null +++ b/src/Microsoft.Tye.Hosting/DockerRunnerOptions.cs @@ -0,0 +1,24 @@ +// 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.Linq; + +namespace Microsoft.Tye.Hosting +{ + public class DockerRunnerOptions + { + public bool ManualStartServices { get; set; } + public string[]? ServicesNotToStart { get; set; } + + public static DockerRunnerOptions FromHostOptions(HostOptions options) + { + return new DockerRunnerOptions + { + ManualStartServices = options.NoStart?.Contains("*", StringComparer.OrdinalIgnoreCase) ?? false, + ServicesNotToStart = options.NoStart?.ToArray() + }; + } + } +} diff --git a/src/Microsoft.Tye.Hosting/ProcessRunner.cs b/src/Microsoft.Tye.Hosting/ProcessRunner.cs index 4dc8f1248..de4458719 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunner.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunner.cs @@ -179,10 +179,11 @@ service.Description.RunInfo is ProjectRunInfo project2 && } private void LaunchService(Application application, Service service) + { - var serviceDescription = service.Description; - var processInfo = new ProcessInfo(new Task[service.Description.Replicas]); - var serviceName = serviceDescription.Name; + var processInfo = (service.Items.ContainsKey(typeof(ProcessInfo)) ? (ProcessInfo?)service.Items[typeof(ProcessInfo)] : null) + ?? new ProcessInfo(new Task[service.Description.Replicas]); + var serviceName = service.Description.Name; // Set by BuildAndRunService var args = service.Status.Args!; @@ -258,7 +259,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? var backOff = TimeSpan.FromSeconds(5); - while (!processInfo.StoppedTokenSource.IsCancellationRequested) + while (!processInfo!.StoppedTokenSource.IsCancellationRequested) { var replica = serviceName + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); var status = new ProcessStatus(service, replica); @@ -297,7 +298,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? try { service.Logs.OnNext($"[{replica}]:{path} {copiedArgs}"); - var processInfo = new ProcessSpec + var processSpec = new ProcessSpec { Executable = path, WorkingDirectory = workingDirectory, @@ -351,8 +352,10 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? // Only increase backoff when not watching project as watch will wait for file changes before rebuild. backOff *= 2; } - - service.Restarts++; + if (!processInfo.StoppedTokenSource.IsCancellationRequested) + { + service.Restarts++; + } service.Replicas.TryRemove(replica, out var _); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); @@ -385,7 +388,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? environment["DOTNET_WATCH"] = "1"; await new DotNetWatcher(_logger) - .WatchAsync(processInfo, fileSetFactory, replica, status.StoppingTokenSource.Token); + .WatchAsync(processSpec, fileSetFactory, replica, status.StoppingTokenSource.Token); } else if (_options.Watch && (service.Description.RunInfo is AzureFunctionRunInfo azureFunctionRunInfo) && !string.IsNullOrEmpty(azureFunctionRunInfo.ProjectFile)) { @@ -397,11 +400,11 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? environment["DOTNET_WATCH"] = "1"; await new DotNetWatcher(_logger) - .WatchAsync(processInfo, fileSetFactory, replica, status.StoppingTokenSource.Token); + .WatchAsync(processSpec, fileSetFactory, replica, status.StoppingTokenSource.Token); } else { - await ProcessUtil.RunAsync(processInfo, status.StoppingTokenSource.Token, throwOnError: false); + await ProcessUtil.RunAsync(processSpec, status.StoppingTokenSource.Token, throwOnError: false); } } catch (Exception ex) @@ -429,50 +432,77 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string? } } - if (serviceDescription.Bindings.Count > 0) + void Start() { - // Each replica is assigned a list of internal ports, one mapped to each external - // port - for (int i = 0; i < serviceDescription.Replicas; i++) + if (service.Description!.Bindings.Count > 0) { - var ports = new List<(int, int, string?, string?)>(); - foreach (var binding in serviceDescription.Bindings) + // Each replica is assigned a list of internal ports, one mapped to each external + // port + for (int i = 0; i < service.Description.Replicas; i++) { - if (binding.Port == null) + var ports = new List<(int, int, string?, string?)>(); + foreach (var binding in service.Description.Bindings) { - continue; + if (binding.Port == null) + { + continue; + } + + ports.Add((binding.Port.Value, binding.ReplicaPorts[i], binding.Protocol, binding.Host)); } - ports.Add((binding.Port.Value, binding.ReplicaPorts[i], binding.Protocol, binding.Host)); + processInfo!.Tasks[i] = RunApplicationAsync(ports, args); + } + } + else + { + for (int i = 0; i < service.Description.Replicas; i++) + { + processInfo!.Tasks[i] = RunApplicationAsync(Enumerable.Empty<(int, int, string?, string?)>(), args); } - - processInfo.Tasks[i] = RunApplicationAsync(ports, args); } } + + processInfo.Start = Start; + service.Items[typeof(ProcessInfo)] = processInfo; + if (!_options.ManualStartServices && !(_options.ServicesNotToStart?.Contains(serviceName, StringComparer.OrdinalIgnoreCase) ?? false)) + { + processInfo.Start(); + } else { - for (int i = 0; i < service.Description.Replicas; i++) + for (int i = 0; i < processInfo.Tasks.Length; i++) { - processInfo.Tasks[i] = RunApplicationAsync(Enumerable.Empty<(int, int, string?, string?)>(), args); + processInfo.Tasks[i] = Task.CompletedTask; } } + } - service.Items[typeof(ProcessInfo)] = processInfo; + public static async Task RestartService(Service service) + { + if (service.Items.TryGetValue(typeof(ProcessInfo), out var stateObj) && stateObj is ProcessInfo state) + { + await KillProcessAsync(service); + service.Restarts++; + state.Start?.Invoke(); + await Task.WhenAll(state.Tasks); + } } - private Task KillRunningProcesses(IDictionary services) + public static async Task KillProcessAsync(Service service) { - static Task KillProcessAsync(Service service) + if (service.Items.TryGetValue(typeof(ProcessInfo), out var stateObj) && stateObj is ProcessInfo state) { - if (service.Items.TryGetValue(typeof(ProcessInfo), out var stateObj) && stateObj is ProcessInfo state) - { - // Cancel the token before stopping the process - state.StoppedTokenSource.Cancel(); + // Cancel the token before stopping the process + state.StoppedTokenSource?.Cancel(); - return Task.WhenAll(state.Tasks); - } - return Task.CompletedTask; + await Task.WhenAll(state.Tasks); + state.ResetStoppedTokenSource(); } + } + + private Task KillRunningProcesses(IDictionary services) + { var index = 0; var tasks = new Task[services.Count]; @@ -541,7 +571,13 @@ public ProcessInfo(Task[] tasks) public Task[] Tasks { get; } - public CancellationTokenSource StoppedTokenSource { get; } = new CancellationTokenSource(); + public CancellationTokenSource StoppedTokenSource { get; private set; } = new CancellationTokenSource(); + public Action? Start { get; internal set; } + internal void ResetStoppedTokenSource() + { + StoppedTokenSource.Dispose(); + StoppedTokenSource = new CancellationTokenSource(); + } } private class ProjectGroup diff --git a/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs b/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs index 699814279..43af3cf52 100644 --- a/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs +++ b/src/Microsoft.Tye.Hosting/ProcessRunnerOptions.cs @@ -14,6 +14,8 @@ public class ProcessRunnerOptions public string[]? ServicesToDebug { get; set; } public bool DebugAllServices { get; set; } public bool Watch { get; set; } + public bool ManualStartServices { get; set; } + public string[]? ServicesNotToStart { get; set; } public static ProcessRunnerOptions FromHostOptions(HostOptions options) { @@ -23,6 +25,8 @@ public static ProcessRunnerOptions FromHostOptions(HostOptions options) DebugMode = options.Debug.Any(), ServicesToDebug = options.Debug.ToArray(), DebugAllServices = options.Debug?.Contains("*", StringComparer.OrdinalIgnoreCase) ?? false, + ManualStartServices = options.NoStart?.Contains("*", StringComparer.OrdinalIgnoreCase) ?? false, + ServicesNotToStart = options.NoStart?.ToArray(), Watch = options.Watch }; } diff --git a/src/Microsoft.Tye.Hosting/TyeHost.cs b/src/Microsoft.Tye.Hosting/TyeHost.cs index 86b142a3a..87cf0a0f3 100644 --- a/src/Microsoft.Tye.Hosting/TyeHost.cs +++ b/src/Microsoft.Tye.Hosting/TyeHost.cs @@ -318,7 +318,7 @@ private static AggregateApplicationProcessor CreateApplicationProcessor(ReplicaR new DockerImagePuller(logger), new FuncFinder(logger), new ReplicaMonitor(logger), - new DockerRunner(logger, replicaRegistry), + new DockerRunner(logger, replicaRegistry, DockerRunnerOptions.FromHostOptions(options)), new ProcessRunner(logger, replicaRegistry, ProcessRunnerOptions.FromHostOptions(options)) }; diff --git a/src/tye/Program.RunCommand.cs b/src/tye/Program.RunCommand.cs index dd59e428b..fb2996f11 100644 --- a/src/tye/Program.RunCommand.cs +++ b/src/tye/Program.RunCommand.cs @@ -74,6 +74,15 @@ private static Command CreateRunCommand() Description = "Watches for code changes for all dotnet projects.", Required = false }, + new Option("--no-start") + { + Argument = new Argument("service") + { + Arity = ArgumentArity.ZeroOrMore, + }, + Description = "Skip automatic start for specific service(s). Specify \"*\" to skip start for all services.", + Required = false + }, StandardOptions.Framework, StandardOptions.Tags, StandardOptions.Verbosity, @@ -111,9 +120,10 @@ private static Command CreateRunCommand() LoggingProvider = args.Logs, MetricsProvider = args.Metrics, LogVerbosity = args.Verbosity, - Watch = args.Watch + Watch = args.Watch, }; options.Debug.AddRange(args.Debug); + options.NoStart.AddRange(args.NoStart); await application.ProcessExtensionsAsync(options, output, ExtensionContext.OperationKind.LocalRun); @@ -154,6 +164,8 @@ private class RunCommandArguments public string[] Debug { get; set; } = Array.Empty(); + public string[] NoStart { get; set; } = Array.Empty(); + public string Dtrace { get; set; } = default!; public bool Docker { get; set; } diff --git a/test/Test.Infrastructure/TestHelpers.cs b/test/Test.Infrastructure/TestHelpers.cs index 1f906210c..fc5eabc0a 100644 --- a/test/Test.Infrastructure/TestHelpers.cs +++ b/test/Test.Infrastructure/TestHelpers.cs @@ -249,7 +249,7 @@ static async Task Purge(TyeHost host) var logger = host.Logger; var replicaRegistry = new ReplicaRegistry(host.Application.ContextDirectory, logger); var processRunner = new ProcessRunner(logger, replicaRegistry, new ProcessRunnerOptions()); - var dockerRunner = new DockerRunner(logger, replicaRegistry); + var dockerRunner = new DockerRunner(logger, replicaRegistry, new DockerRunnerOptions()); await processRunner.StartAsync(new Application(host.Application.Name, new FileInfo(host.Application.Source), null, new Dictionary(), ContainerEngine.Default)); await dockerRunner.StartAsync(new Application(host.Application.Name, new FileInfo(host.Application.Source), null, new Dictionary(), ContainerEngine.Default));