Skip to content

Commit 2db09a7

Browse files
authored
Merge pull request #10 from Pen-Link/start-stop
Start stop
2 parents f5c4956 + 2a55897 commit 2db09a7

9 files changed

Lines changed: 211 additions & 46 deletions

File tree

src/Microsoft.Tye.Core/HostOptions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public class HostOptions
1212

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

15+
public List<string> NoStart { get; } = new List<string>();
16+
1517
public string? DistributedTraceProvider { get; set; }
1618

1719
public bool Docker { get; set; }

src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<th>Replicas</th>
2020
<th>Restarts</th>
2121
<th>Logs</th>
22+
<th>Actions</th>
2223
</tr>
2324
</thead>
2425
<tbody>
@@ -92,16 +93,38 @@
9293
<td>@service.Replicas.Count/@service.Description.Replicas</td>
9394
<td>@service.Restarts</td>
9495
<td><NavLink href="@logsPath">View</NavLink></td>
96+
<td>
97+
@if (CanStartStop(service))
98+
{
99+
if (service.Replicas.Count == 0)
100+
{
101+
<button @onclick="async () => await StartServiceAsync(service)" class="btn btn-default btn-xs">
102+
<span class="oi oi-media-play"></span>
103+
</button>
104+
}
105+
else
106+
{
107+
<button @onclick="async () => await StopServiceAsync(service)" class="btn btn-default btn-xs">
108+
<span class="oi oi-media-stop"></span>
109+
</button>
110+
}
111+
}
112+
</td>
95113
}
96114
</tr>
97115
}
98116
</tbody>
99117
</table>
100118

101119
@code {
102-
120+
static readonly ServiceType[] stopables = new[] { ServiceType.Container, ServiceType.Executable, ServiceType.Project, ServiceType.Function };
103121
private List<IDisposable> _subscriptions = new List<IDisposable>();
104122

123+
bool CanStartStop(Service? service)
124+
{
125+
return service != null && stopables.Contains(service.ServiceType);
126+
}
127+
105128
string GetUrl(ServiceBinding b)
106129
{
107130
return $"{(b.Protocol ?? "tcp")}://{b.Host ?? "localhost"}:{b.Port}";
@@ -120,6 +143,31 @@
120143
InvokeAsync(() => StateHasChanged());
121144
}
122145

146+
private async Task StartServiceAsync(Service service)
147+
{
148+
if (service.ServiceType == ServiceType.Container)
149+
{
150+
await DockerRunner.RestartContainerAsync(service);
151+
}
152+
else
153+
{
154+
await ProcessRunner.RestartService(service);
155+
}
156+
}
157+
158+
private async Task StopServiceAsync(Service service)
159+
{
160+
if (service.ServiceType == ServiceType.Container)
161+
{
162+
await DockerRunner.StopContainerAsync(service);
163+
}
164+
else
165+
{
166+
await ProcessRunner.KillProcessAsync(service);
167+
}
168+
}
169+
170+
123171
void IDisposable.Dispose()
124172
{
125173
_subscriptions.ForEach(d => d.Dispose());

src/Microsoft.Tye.Hosting/DockerRunner.cs

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,13 @@ public class DockerRunner : IApplicationProcessor
2424
private readonly ILogger _logger;
2525

2626
private readonly ReplicaRegistry _replicaRegistry;
27+
private readonly DockerRunnerOptions _options;
2728

28-
public DockerRunner(ILogger logger, ReplicaRegistry replicaRegistry)
29+
public DockerRunner(ILogger logger, ReplicaRegistry replicaRegistry, DockerRunnerOptions options)
2930
{
3031
_logger = logger;
3132
_replicaRegistry = replicaRegistry;
33+
_options = options;
3234
}
3335

3436
public async Task StartAsync(Application application)
@@ -537,15 +539,21 @@ Task DockerRunAsync(CancellationToken cancellationToken)
537539
return Task.WhenAll(tasks);
538540
}
539541

542+
var dockerInfo = new DockerInformation();
540543
async Task BuildAndRunAsync(CancellationToken cancellationToken)
541544
{
542545
await DockerBuildAsync(cancellationToken);
543546

544547
await DockerRunAsync(cancellationToken);
545548
}
546549

547-
var dockerInfo = new DockerInformation();
548-
dockerInfo.Task = BuildAndRunAsync(dockerInfo.StoppingTokenSource.Token);
550+
dockerInfo.SetBuildAndRunTask(BuildAndRunAsync);
551+
552+
if (!_options.ManualStartServices &&
553+
!(_options.ServicesNotToStart?.Contains(service.Description.Name, StringComparer.OrdinalIgnoreCase) ?? false))
554+
{
555+
dockerInfo.BuildAndRun();
556+
}
549557

550558
service.Items[typeof(DockerInformation)] = dockerInfo;
551559
}
@@ -587,22 +595,53 @@ private static void PrintStdOutAndErr(Service service, string replica, ProcessRe
587595
}
588596
}
589597

590-
private Task StopContainerAsync(Service service)
598+
public static async Task RestartContainerAsync(Service service)
599+
{
600+
if (service.Items.TryGetValue(typeof(DockerInformation), out var value) && value is DockerInformation di)
601+
{
602+
await StopContainerAsync(service);
603+
604+
di.BuildAndRun();
605+
service.Restarts++;
606+
await di.Task;
607+
}
608+
}
609+
610+
public static Task StopContainerAsync(Service service)
591611
{
592612
if (service.Items.TryGetValue(typeof(DockerInformation), out var value) && value is DockerInformation di)
593613
{
594-
di.StoppingTokenSource.Cancel();
614+
di.CancelAndResetStoppingTokenSource();
615+
return di.Task ?? Task.CompletedTask;
595616

596-
return di.Task;
597617
}
598618

599619
return Task.CompletedTask;
600620
}
601621

602622
private class DockerInformation
603623
{
604-
public Task Task { get; set; } = default!;
605-
public CancellationTokenSource StoppingTokenSource { get; } = new CancellationTokenSource();
624+
private Func<CancellationToken, Task>? _buildAndRunAsync;
625+
626+
public Task Task { get; private set; } = default!;
627+
public CancellationTokenSource StoppingTokenSource { get; private set; } = new CancellationTokenSource();
628+
629+
public void SetBuildAndRunTask(Func<CancellationToken, Task> func)
630+
{
631+
_buildAndRunAsync = func;
632+
}
633+
634+
public void BuildAndRun()
635+
{
636+
Task = _buildAndRunAsync?.Invoke(StoppingTokenSource.Token) ?? Task.CompletedTask;
637+
}
638+
639+
internal void CancelAndResetStoppingTokenSource()
640+
{
641+
StoppingTokenSource.Cancel();
642+
StoppingTokenSource.Dispose();
643+
StoppingTokenSource = new CancellationTokenSource();
644+
}
606645
}
607646

608647
private class DockerApplicationInformation
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Linq;
7+
8+
namespace Microsoft.Tye.Hosting
9+
{
10+
public class DockerRunnerOptions
11+
{
12+
public bool ManualStartServices { get; set; }
13+
public string[]? ServicesNotToStart { get; set; }
14+
15+
public static DockerRunnerOptions FromHostOptions(HostOptions options)
16+
{
17+
return new DockerRunnerOptions
18+
{
19+
ManualStartServices = options.NoStart?.Contains("*", StringComparer.OrdinalIgnoreCase) ?? false,
20+
ServicesNotToStart = options.NoStart?.ToArray()
21+
};
22+
}
23+
}
24+
}

src/Microsoft.Tye.Hosting/ProcessRunner.cs

Lines changed: 70 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,11 @@ service.Description.RunInfo is ProjectRunInfo project2 &&
179179
}
180180

181181
private void LaunchService(Application application, Service service)
182+
182183
{
183-
var serviceDescription = service.Description;
184-
var processInfo = new ProcessInfo(new Task[service.Description.Replicas]);
185-
var serviceName = serviceDescription.Name;
184+
var processInfo = (service.Items.ContainsKey(typeof(ProcessInfo)) ? (ProcessInfo?)service.Items[typeof(ProcessInfo)] : null)
185+
?? new ProcessInfo(new Task[service.Description.Replicas]);
186+
var serviceName = service.Description.Name;
186187

187188
// Set by BuildAndRunService
188189
var args = service.Status.Args!;
@@ -258,7 +259,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
258259

259260
var backOff = TimeSpan.FromSeconds(5);
260261

261-
while (!processInfo.StoppedTokenSource.IsCancellationRequested)
262+
while (!processInfo!.StoppedTokenSource.IsCancellationRequested)
262263
{
263264
var replica = serviceName + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower();
264265
var status = new ProcessStatus(service, replica);
@@ -297,7 +298,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
297298
try
298299
{
299300
service.Logs.OnNext($"[{replica}]:{path} {copiedArgs}");
300-
var processInfo = new ProcessSpec
301+
var processSpec = new ProcessSpec
301302
{
302303
Executable = path,
303304
WorkingDirectory = workingDirectory,
@@ -351,8 +352,10 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
351352
// Only increase backoff when not watching project as watch will wait for file changes before rebuild.
352353
backOff *= 2;
353354
}
354-
355-
service.Restarts++;
355+
if (!processInfo.StoppedTokenSource.IsCancellationRequested)
356+
{
357+
service.Restarts++;
358+
}
356359

357360
service.Replicas.TryRemove(replica, out var _);
358361
service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status));
@@ -385,7 +388,7 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
385388
environment["DOTNET_WATCH"] = "1";
386389

387390
await new DotNetWatcher(_logger)
388-
.WatchAsync(processInfo, fileSetFactory, replica, status.StoppingTokenSource.Token);
391+
.WatchAsync(processSpec, fileSetFactory, replica, status.StoppingTokenSource.Token);
389392
}
390393
else if (_options.Watch && (service.Description.RunInfo is AzureFunctionRunInfo azureFunctionRunInfo) && !string.IsNullOrEmpty(azureFunctionRunInfo.ProjectFile))
391394
{
@@ -397,11 +400,11 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
397400
environment["DOTNET_WATCH"] = "1";
398401

399402
await new DotNetWatcher(_logger)
400-
.WatchAsync(processInfo, fileSetFactory, replica, status.StoppingTokenSource.Token);
403+
.WatchAsync(processSpec, fileSetFactory, replica, status.StoppingTokenSource.Token);
401404
}
402405
else
403406
{
404-
await ProcessUtil.RunAsync(processInfo, status.StoppingTokenSource.Token, throwOnError: false);
407+
await ProcessUtil.RunAsync(processSpec, status.StoppingTokenSource.Token, throwOnError: false);
405408
}
406409
}
407410
catch (Exception ex)
@@ -429,50 +432,77 @@ async Task RunApplicationAsync(IEnumerable<(int ExternalPort, int Port, string?
429432
}
430433
}
431434

432-
if (serviceDescription.Bindings.Count > 0)
435+
void Start()
433436
{
434-
// Each replica is assigned a list of internal ports, one mapped to each external
435-
// port
436-
for (int i = 0; i < serviceDescription.Replicas; i++)
437+
if (service.Description!.Bindings.Count > 0)
437438
{
438-
var ports = new List<(int, int, string?, string?)>();
439-
foreach (var binding in serviceDescription.Bindings)
439+
// Each replica is assigned a list of internal ports, one mapped to each external
440+
// port
441+
for (int i = 0; i < service.Description.Replicas; i++)
440442
{
441-
if (binding.Port == null)
443+
var ports = new List<(int, int, string?, string?)>();
444+
foreach (var binding in service.Description.Bindings)
442445
{
443-
continue;
446+
if (binding.Port == null)
447+
{
448+
continue;
449+
}
450+
451+
ports.Add((binding.Port.Value, binding.ReplicaPorts[i], binding.Protocol, binding.Host));
444452
}
445453

446-
ports.Add((binding.Port.Value, binding.ReplicaPorts[i], binding.Protocol, binding.Host));
454+
processInfo!.Tasks[i] = RunApplicationAsync(ports, args);
455+
}
456+
}
457+
else
458+
{
459+
for (int i = 0; i < service.Description.Replicas; i++)
460+
{
461+
processInfo!.Tasks[i] = RunApplicationAsync(Enumerable.Empty<(int, int, string?, string?)>(), args);
447462
}
448-
449-
processInfo.Tasks[i] = RunApplicationAsync(ports, args);
450463
}
451464
}
465+
466+
processInfo.Start = Start;
467+
service.Items[typeof(ProcessInfo)] = processInfo;
468+
if (!_options.ManualStartServices && !(_options.ServicesNotToStart?.Contains(serviceName, StringComparer.OrdinalIgnoreCase) ?? false))
469+
{
470+
processInfo.Start();
471+
}
452472
else
453473
{
454-
for (int i = 0; i < service.Description.Replicas; i++)
474+
for (int i = 0; i < processInfo.Tasks.Length; i++)
455475
{
456-
processInfo.Tasks[i] = RunApplicationAsync(Enumerable.Empty<(int, int, string?, string?)>(), args);
476+
processInfo.Tasks[i] = Task.CompletedTask;
457477
}
458478
}
479+
}
459480

460-
service.Items[typeof(ProcessInfo)] = processInfo;
481+
public static async Task RestartService(Service service)
482+
{
483+
if (service.Items.TryGetValue(typeof(ProcessInfo), out var stateObj) && stateObj is ProcessInfo state)
484+
{
485+
await KillProcessAsync(service);
486+
service.Restarts++;
487+
state.Start?.Invoke();
488+
await Task.WhenAll(state.Tasks);
489+
}
461490
}
462491

463-
private Task KillRunningProcesses(IDictionary<string, Service> services)
492+
public static async Task KillProcessAsync(Service service)
464493
{
465-
static Task KillProcessAsync(Service service)
494+
if (service.Items.TryGetValue(typeof(ProcessInfo), out var stateObj) && stateObj is ProcessInfo state)
466495
{
467-
if (service.Items.TryGetValue(typeof(ProcessInfo), out var stateObj) && stateObj is ProcessInfo state)
468-
{
469-
// Cancel the token before stopping the process
470-
state.StoppedTokenSource.Cancel();
496+
// Cancel the token before stopping the process
497+
state.StoppedTokenSource?.Cancel();
471498

472-
return Task.WhenAll(state.Tasks);
473-
}
474-
return Task.CompletedTask;
499+
await Task.WhenAll(state.Tasks);
500+
state.ResetStoppedTokenSource();
475501
}
502+
}
503+
504+
private Task KillRunningProcesses(IDictionary<string, Service> services)
505+
{
476506

477507
var index = 0;
478508
var tasks = new Task[services.Count];
@@ -541,7 +571,13 @@ public ProcessInfo(Task[] tasks)
541571

542572
public Task[] Tasks { get; }
543573

544-
public CancellationTokenSource StoppedTokenSource { get; } = new CancellationTokenSource();
574+
public CancellationTokenSource StoppedTokenSource { get; private set; } = new CancellationTokenSource();
575+
public Action? Start { get; internal set; }
576+
internal void ResetStoppedTokenSource()
577+
{
578+
StoppedTokenSource.Dispose();
579+
StoppedTokenSource = new CancellationTokenSource();
580+
}
545581
}
546582

547583
private class ProjectGroup

0 commit comments

Comments
 (0)