Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Microsoft.Tye.Core/HostOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class HostOptions

public List<string> Debug { get; } = new List<string>();

public List<string> NoStart { get; } = new List<string>();

public string? DistributedTraceProvider { get; set; }

public bool Docker { get; set; }
Expand Down
50 changes: 49 additions & 1 deletion src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<th>Replicas</th>
<th>Restarts</th>
<th>Logs</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
Expand Down Expand Up @@ -92,16 +93,38 @@
<td>@service.Replicas.Count/@service.Description.Replicas</td>
<td>@service.Restarts</td>
<td><NavLink href="@logsPath">View</NavLink></td>
<td>
@if (CanStartStop(service))
{
if (service.Replicas.Count == 0)
{
<button @onclick="async () => await StartServiceAsync(service)" class="btn btn-default btn-xs">
<span class="oi oi-media-play"></span>
</button>
}
else
{
<button @onclick="async () => await StopServiceAsync(service)" class="btn btn-default btn-xs">
<span class="oi oi-media-stop"></span>
</button>
}
}
</td>
}
</tr>
}
</tbody>
</table>

@code {

static readonly ServiceType[] stopables = new[] { ServiceType.Container, ServiceType.Executable, ServiceType.Project, ServiceType.Function };
private List<IDisposable> _subscriptions = new List<IDisposable>();

bool CanStartStop(Service? service)
{
return service != null && stopables.Contains(service.ServiceType);
}

string GetUrl(ServiceBinding b)
{
return $"{(b.Protocol ?? "tcp")}://{b.Host ?? "localhost"}:{b.Port}";
Expand All @@ -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());
Expand Down
55 changes: 47 additions & 8 deletions src/Microsoft.Tye.Hosting/DockerRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -537,15 +539,21 @@ Task DockerRunAsync(CancellationToken cancellationToken)
return Task.WhenAll(tasks);
}

var dockerInfo = new DockerInformation();
async Task BuildAndRunAsync(CancellationToken cancellationToken)
{
await DockerBuildAsync(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;
}
Expand Down Expand Up @@ -587,22 +595,53 @@ 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;
}

private class DockerInformation
{
public Task Task { get; set; } = default!;
public CancellationTokenSource StoppingTokenSource { get; } = new CancellationTokenSource();
private Func<CancellationToken, Task>? _buildAndRunAsync;

public Task Task { get; private set; } = default!;
public CancellationTokenSource StoppingTokenSource { get; private set; } = new CancellationTokenSource();

public void SetBuildAndRunTask(Func<CancellationToken, Task> 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
Expand Down
24 changes: 24 additions & 0 deletions src/Microsoft.Tye.Hosting/DockerRunnerOptions.cs
Original file line number Diff line number Diff line change
@@ -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()
};
}
}
}
104 changes: 70 additions & 34 deletions src/Microsoft.Tye.Hosting/ProcessRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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))
{
Expand All @@ -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)
Expand Down Expand Up @@ -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<string, Service> 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<string, Service> services)
{

var index = 0;
var tasks = new Task[services.Count];
Expand Down Expand Up @@ -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
Expand Down
Loading