public async Task StartAsync(Tye.Hosting.Model.Application application) { foreach (var processor in _applicationProcessors) { await processor.StartAsync(application); } }
public async Task StopAsync(Tye.Hosting.Model.Application application) { // Shutdown in the opposite order foreach (var processor in _applicationProcessors.Reverse()) { await processor.StopAsync(application); } }
private static WebApplication BuildWebApplication( Tye.Hosting.Model.Application application, string[] args, ILogEventSink?sink) { var builder = WebApplication.CreateBuilder(args); // Logging for this application builder.Host.UseSerilog((context, configuration) => { configuration .MinimumLevel.Verbose() .Filter.ByExcluding(Matching.FromSource("Microsoft.AspNetCore")) .Filter.ByExcluding(Matching.FromSource("Microsoft.Extensions")) .Filter.ByExcluding(Matching.FromSource("Microsoft.Hosting")) .Enrich .FromLogContext() .WriteTo .Console(); if (sink is object) { configuration.WriteTo.Sink(sink, LogEventLevel.Verbose); } }); builder.Services.AddRazorPages(o => o.RootDirectory = "/Dashboard/Pages"); builder.Services.AddServerSideBlazor(); builder.Services.AddOptions <StaticFileOptions>() .PostConfigure(o => { var fileProvider = new ManifestEmbeddedFileProvider(typeof(TyeHost).Assembly, "wwwroot"); // Make sure we don't remove the existing file providers (blazor needs this) o.FileProvider = new CompositeFileProvider(o.FileProvider, fileProvider); }); builder.Services.AddCors( options => { options.AddPolicy( "default", policy => { policy .AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); builder.Services.AddSingleton(application); var app = builder.Build(); return(app); }
public Task StartAsync(Tye.Hosting.Model.Application application) { var tasks = new Task[application.Services.Count]; var index = 0; foreach (var s in application.Services) { tasks[index++] = s.Value.Description.RunInfo is DockerRunInfo docker?StartContainerAsync(application, s.Value, docker) : Task.CompletedTask; } return(Task.WhenAll(tasks)); }
public Task StopAsync(Tye.Hosting.Model.Application application) { foreach (var service in application.Services.Values) { if (service.Items.TryGetValue(typeof(Subscription), out var item) && item is IDisposable disposable) { disposable.Dispose(); } } return(Task.CompletedTask); }
public Task StartAsync(Tye.Hosting.Model.Application application) { foreach (var service in application.Services.Values) { if (service.Description.RunInfo is null) { continue; } service.Items[typeof(Subscription)] = service.ReplicaEvents.Subscribe(OnReplicaChanged); } return(Task.CompletedTask); }
public Task StopAsync(Tye.Hosting.Model.Application application) { var services = application.Services; var index = 0; var tasks = new Task[services.Count]; foreach (var s in services.Values) { var state = s; tasks[index++] = StopContainerAsync(state); } return(Task.WhenAll(tasks)); }
public Task StartAsync(Tye.Hosting.Model.Application application) { var tasks = new Task[application.Services.Count]; var index = 0; foreach (var s in application.Services) { tasks[index++] = s.Value.ServiceType switch { ServiceType.Executable => LaunchService(application, s.Value), ServiceType.Project => LaunchService(application, s.Value), _ => Task.CompletedTask, }; } return(Task.WhenAll(tasks)); }
public Task StartAsync(Tye.Hosting.Model.Application application) { var tasks = new Task[application.Services.Count]; var index = 0; foreach (var s in application.Services) { tasks[index++] = s.Value.ServiceType switch { ServiceType.Container => Task.CompletedTask, ServiceType.External => Task.CompletedTask, ServiceType.Executable => LaunchService(application, s.Value), ServiceType.Project => LaunchService(application, s.Value), _ => throw new InvalidOperationException("Unknown ServiceType."), }; } return(Task.WhenAll(tasks)); }
public async Task StartAsync(Tye.Hosting.Model.Application application) { _host = new HostBuilder() .ConfigureServer(server => { server.UseSockets(sockets => { foreach (var service in application.Services.Values) { if (service.Description.RunInfo == null) { // We eventually want to proxy everything, this is temporary continue; } static int GetNextPort() { // Let the OS assign the next available port. Unless we cycle through all ports // on a test run, the OS will always increment the port number when making these calls. // This prevents races in parallel test runs where a test is already bound to // a given port, and a new test is able to bind to the same port due to port // reuse being enabled by default by the OS. using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Bind(new IPEndPoint(IPAddress.Loopback, 0)); return(((IPEndPoint)socket.LocalEndPoint).Port); } foreach (var binding in service.Description.Bindings) { if (binding.Port == null && !binding.AutoAssignPort) { continue; } if (binding.Port == null) { binding.Port = GetNextPort(); } if (binding.Protocol == "http" || (binding.Protocol == null && service.ServiceType == Model.ServiceType.Project)) { binding.ContainerPort = 80; } else if (binding.Protocol == "https") { binding.ContainerPort = 443; } if (service.Description.Replicas == 1) { // No need to proxy service.PortMap[binding.Port.Value] = new List <int> { binding.Port.Value }; continue; } var ports = new List <int>(); for (var i = 0; i < service.Description.Replicas; i++) { // Reserve a port for each replica var port = GetNextPort(); ports.Add(port); } _logger.LogInformation( "Mapping external port {ExternalPort} to internal port(s) {InternalPorts} for {ServiceName} binding {BindingName}", binding.Port, string.Join(", ", ports.Select(p => p.ToString())), service.Description.Name, binding.Name ?? binding.Protocol); service.PortMap[binding.Port.Value] = ports; sockets.Listen(IPAddress.Loopback, binding.Port.Value, o => { long count = 0; // o.UseConnectionLogging("Tye.Proxy"); o.Run(async connection => { var notificationFeature = connection.Features.Get <IConnectionLifetimeNotificationFeature>(); var next = (int)(Interlocked.Increment(ref count) % ports.Count); NetworkStream?targetStream = null; try { var target = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; var port = ports[next]; _logger.LogDebug("Attempting to connect to {ServiceName} listening on {ExternalPort}:{Port}", service.Description.Name, binding.Port, port); await target.ConnectAsync(IPAddress.Loopback, port); _logger.LogDebug("Successfully connected to {ServiceName} listening on {ExternalPort}:{Port}", service.Description.Name, binding.Port, port); targetStream = new NetworkStream(target, ownsSocket: true); } catch (Exception ex) { _logger.LogDebug(ex, "Proxy error for service {ServiceName}", service.Description.Name); if (targetStream is object) { await targetStream.DisposeAsync(); } connection.Abort(); return; } try { _logger.LogDebug("Proxying traffic to {ServiceName} {ExternalPort}:{InternalPort}", service.Description.Name, binding.Port, ports[next]); // external -> internal var reading = Task.Run(() => connection.Transport.Input.CopyToAsync(targetStream, notificationFeature.ConnectionClosedRequested)); // internal -> external var writing = Task.Run(() => targetStream.CopyToAsync(connection.Transport.Output, notificationFeature.ConnectionClosedRequested)); await Task.WhenAll(reading, writing); } catch (ConnectionResetException) { // Connection was reset } catch (IOException) { // Reset can also appear as an IOException with an inner SocketException } catch (OperationCanceledException ex) { if (!notificationFeature.ConnectionClosedRequested.IsCancellationRequested) { _logger.LogDebug(0, ex, "Proxy error for service {ServiceName}", service.Description.Name); } } catch (Exception ex) { _logger.LogDebug(0, ex, "Proxy error for service {ServiceName}", service.Description.Name); } finally { await targetStream.DisposeAsync(); } // This needs to reconnect to the target port(s) until its bound // it has to stop if the service is no longer running }); }); } }
private async Task LaunchService(Tye.Hosting.Model.Application application, Tye.Hosting.Model.Service service) { var serviceDescription = service.Description; var serviceName = serviceDescription.Name; var path = ""; var workingDirectory = ""; var args = ""; if (serviceDescription.RunInfo is ProjectRunInfo project) { var expandedProject = Environment.ExpandEnvironmentVariables(project.Project); var fullProjectPath = Path.GetFullPath(Path.Combine(application.ContextDirectory, expandedProject)); path = GetExePath(fullProjectPath); workingDirectory = Path.GetDirectoryName(fullProjectPath) !; args = project.Args ?? ""; service.Status.ProjectFilePath = fullProjectPath; } else if (serviceDescription.RunInfo is ExecutableRunInfo executable) { var expandedExecutable = Environment.ExpandEnvironmentVariables(executable.Executable); path = Path.GetExtension(expandedExecutable) == ".dll" ? Path.GetFullPath(Path.Combine(application.ContextDirectory, expandedExecutable)) : expandedExecutable; workingDirectory = executable.WorkingDirectory != null? Path.GetFullPath(Path.Combine(application.ContextDirectory, Environment.ExpandEnvironmentVariables(executable.WorkingDirectory))) : Path.GetDirectoryName(path) !; args = executable.Args ?? ""; } else { throw new InvalidOperationException("Unsupported ServiceType."); } // If this is a dll then use dotnet to run it if (Path.GetExtension(path) == ".dll") { args = $"\"{path}\" {args}".Trim(); path = "dotnet"; } service.Status.ExecutablePath = path; service.Status.WorkingDirectory = workingDirectory; service.Status.Args = args; var processInfo = new ProcessInfo(new Task[service.Description.Replicas]); if (service.Status.ProjectFilePath != null && service.Description.RunInfo is ProjectRunInfo project2 && project2.Build && _options.BuildProjects) { // Sometimes building can fail because of file locking (like files being open in VS) _logger.LogInformation("Building project {ProjectFile}", service.Status.ProjectFilePath); service.Logs.OnNext($"dotnet build \"{service.Status.ProjectFilePath}\" /nologo"); var buildResult = await ProcessUtil.RunAsync("dotnet", $"build \"{service.Status.ProjectFilePath}\" /nologo", throwOnError : false); service.Logs.OnNext(buildResult.StandardOutput); if (buildResult.ExitCode != 0) { _logger.LogInformation("Building {ProjectFile} failed with exit code {ExitCode}: \r\n" + buildResult.StandardOutput, service.Status.ProjectFilePath, buildResult.ExitCode); return; } } async Task RunApplicationAsync(IEnumerable <(int Port, int BindingPort, string?Protocol)> ports) { // Make sure we yield before trying to start the process, this is important so we don't hang startup await Task.Yield(); var hasPorts = ports.Any(); var environment = new Dictionary <string, string> { // Default to development environment ["DOTNET_ENVIRONMENT"] = "Development" }; // Set up environment variables to use the version of dotnet we're using to run // this is important for tests where we're not using a globally-installed dotnet. var dotnetRoot = GetDotnetRoot(); if (dotnetRoot is object) { environment["DOTNET_ROOT"] = dotnetRoot; environment["DOTNET_MULTILEVEL_LOOKUP"] = "0"; environment["PATH"] = $"{dotnetRoot};{Environment.GetEnvironmentVariable("PATH")}"; } application.PopulateEnvironment(service, (k, v) => environment[k] = v); if (_options.DebugMode && (_options.DebugAllServices || _options.ServicesToDebug.Contains(serviceName, StringComparer.OrdinalIgnoreCase))) { environment["DOTNET_STARTUP_HOOKS"] = typeof(Hosting.Runtime.HostingRuntimeHelpers).Assembly.Location; } if (hasPorts) { // These are the ports that the application should use for binding // 1. Configure ASP.NET Core to bind to those same ports environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://localhost:{p.Port}")); // Set the HTTPS port for the redirect middleware foreach (var p in ports) { if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase)) { // We need to set the redirect URL to the exposed port so the redirect works cleanly environment["HTTPS_PORT"] = p.BindingPort.ToString(); } } // 3. For non-ASP.NET Core apps, pass the same information in the PORT env variable as a semicolon separated list. environment["PORT"] = string.Join(";", ports.Select(p => $"{p.Port}")); } while (!processInfo.StoppedTokenSource.IsCancellationRequested) { var replica = serviceName + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); var status = new ProcessStatus(service, replica); service.Replicas[replica] = status; service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); // This isn't your host name environment["APP_INSTANCE"] = replica; status.ExitCode = null; status.Pid = null; status.Environment = environment; if (hasPorts) { status.Ports = ports.Select(p => p.Port); } _logger.LogInformation("Launching service {ServiceName}: {ExePath} {args}", replica, path, args); try { service.Logs.OnNext($"[{replica}]:{path} {args}"); var result = await ProcessUtil.RunAsync(path, args, environmentVariables : environment, workingDirectory : workingDirectory, outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"), onStart : pid => { if (hasPorts) { _logger.LogInformation("{ServiceName} running on process id {PID} bound to {Address}", replica, pid, string.Join(", ", ports.Select(p => $"{p.Protocol ?? "http"}://localhost:{p.Port}"))); } else { _logger.LogInformation("{ServiceName} running on process id {PID}", replica, pid); } status.Pid = pid; service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status)); }, throwOnError : false, cancellationToken : processInfo.StoppedTokenSource.Token); status.ExitCode = result.ExitCode; if (status.Pid != null) { service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status)); } } catch (Exception ex) { _logger.LogError(0, ex, "Failed to launch process for service {ServiceName}", replica); try { await Task.Delay(5000, processInfo.StoppedTokenSource.Token); } catch (OperationCanceledException) { // Swallow cancellation exceptions and continue } } 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 _); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); } } if (serviceDescription.Bindings.Count > 0) { // Each replica is assigned a list of internal ports, one mapped to each external // port for (int i = 0; i < serviceDescription.Replicas; i++) { var ports = new List <(int, int, string?)>(); foreach (var binding in serviceDescription.Bindings) { if (binding.Port == null) { continue; } ports.Add((service.PortMap[binding.Port.Value][i], binding.Port.Value, binding.Protocol)); } processInfo.Tasks[i] = RunApplicationAsync(ports); } } else { for (int i = 0; i < service.Description.Replicas; i++) { processInfo.Tasks[i] = RunApplicationAsync(Enumerable.Empty <(int, int, string?)>()); } } service.Items[typeof(ProcessInfo)] = processInfo; }
public Task StopAsync(Tye.Hosting.Model.Application application) { return(KillRunningProcesses(application.Services)); }
private async Task StartContainerAsync(Tye.Hosting.Model.Application application, Tye.Hosting.Model.Service service, DockerRunInfo docker) { if (!await DockerDetector.Instance.IsDockerInstalled.Value) { _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not installed.", service.Description.Name); service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not installed."); return; } if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value) { _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not running.", service.Description.Name); service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not running."); return; } var serviceDescription = service.Description; var environmentArguments = ""; var volumes = ""; var workingDirectory = docker.WorkingDirectory != null ? $"-w {docker.WorkingDirectory}" : ""; // This is .NET specific var userSecretStore = GetUserSecretsPathFromSecrets(); if (!string.IsNullOrEmpty(userSecretStore)) { // Map the user secrets on this drive to user secrets docker.VolumeMappings[userSecretStore] = "/root/.microsoft/usersecrets:ro"; } var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]); async Task RunDockerContainer(IEnumerable <(int Port, int?InternalPort, int BindingPort, string?Protocol)> ports) { var hasPorts = ports.Any(); var replica = service.Description.Name.ToLower() + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); var status = new DockerStatus(service, replica); service.Replicas[replica] = status; service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); var environment = new Dictionary <string, string> { // Default to development environment ["DOTNET_ENVIRONMENT"] = "Development", // Remove the color codes from the console output ["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"] = "true" }; var portString = ""; if (hasPorts) { status.Ports = ports.Select(p => p.Port); // These are the ports that the application should use for binding // 1. Tell the docker container what port to bind to portString = string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.InternalPort ?? p.Port}")); // 2. Configure ASP.NET Core to bind to those same ports environment["ASPNETCORE_URLS"] = string.Join(";", ports.Select(p => $"{p.Protocol ?? "http"}://*:{p.InternalPort ?? p.Port}")); // Set the HTTPS port for the redirect middleware foreach (var p in ports) { if (string.Equals(p.Protocol, "https", StringComparison.OrdinalIgnoreCase)) { // We need to set the redirect URL to the exposed port so the redirect works cleanly environment["HTTPS_PORT"] = p.BindingPort.ToString(); } } // 3. For non-ASP.NET Core apps, pass the same information in the PORT env variable as a semicolon separated list. environment["PORT"] = string.Join(";", ports.Select(p => $"{p.InternalPort ?? p.Port}")); } // See: https://github.com/docker/for-linux/issues/264 // // The way we do proxying here doesn't really work for multi-container scenarios on linux // without some more setup. application.PopulateEnvironment(service, (key, value) => environment[key] = value, "host.docker.internal"); environment["APP_INSTANCE"] = replica; foreach (var pair in environment) { environmentArguments += $"-e {pair.Key}={pair.Value} "; } foreach (var pair in docker.VolumeMappings) { var sourcePath = Path.GetFullPath(Path.Combine(application.ContextDirectory, pair.Key.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar))); volumes += $"-v {sourcePath}:{pair.Value} "; } var command = $"run -d {workingDirectory} {volumes} {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}"; _logger.LogInformation("Running docker command {Command}", command); service.Logs.OnNext($"[{replica}]: {command}"); status.DockerCommand = command; var result = await ProcessUtil.RunAsync( "docker", command, throwOnError : false, cancellationToken : dockerInfo.StoppingTokenSource.Token, outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}")); if (result.ExitCode != 0) { _logger.LogError("docker run failed for {ServiceName} with exit code {ExitCode}:" + result.StandardError, service.Description.Name, result.ExitCode); service.Replicas.TryRemove(replica, out _); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); PrintStdOutAndErr(service, replica, result); return; } var containerId = (string?)result.StandardOutput.Trim(); // There's a race condition that sometimes makes us miss the output // so keep trying to get the container id while (string.IsNullOrEmpty(containerId)) { // Try to get the ID of the container result = await ProcessUtil.RunAsync("docker", $"ps --no-trunc -f name={replica} --format " + "{{.ID}}"); containerId = result.ExitCode == 0 ? result.StandardOutput.Trim() : null; } var shortContainerId = containerId.Substring(0, Math.Min(12, containerId.Length)); status.ContainerId = shortContainerId; _logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status)); _logger.LogInformation("Collecting docker logs for {ContainerName}.", replica); await ProcessUtil.RunAsync("docker", $"logs -f {containerId}", outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"), onStart : pid => { status.DockerLogsPid = pid; }, throwOnError : false, cancellationToken : dockerInfo.StoppingTokenSource.Token); _logger.LogInformation("docker logs collection for {ContainerName} complete with exit code {ExitCode}", replica, result.ExitCode); // Docker has a tendency to get stuck so we're going to timeout this shutdown process var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); _logger.LogInformation("Stopping container {ContainerName} with ID {ContainerId}", replica, shortContainerId); result = await ProcessUtil.RunAsync("docker", $"stop {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token); PrintStdOutAndErr(service, replica, result); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status)); _logger.LogInformation("Stopped container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode); result = await ProcessUtil.RunAsync("docker", $"rm {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token); PrintStdOutAndErr(service, replica, result); _logger.LogInformation("Removed container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode); service.Replicas.TryRemove(replica, out _); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); }; if (serviceDescription.Bindings.Count > 0) { // Each replica is assigned a list of internal ports, one mapped to each external // port for (var i = 0; i < serviceDescription.Replicas; i++) { var ports = new List <(int, int?, int, string?)>(); foreach (var binding in serviceDescription.Bindings) { if (binding.Port == null) { continue; } ports.Add((service.PortMap[binding.Port.Value][i], binding.InternalPort, binding.Port.Value, binding.Protocol)); } dockerInfo.Tasks[i] = RunDockerContainer(ports); } } else { for (var i = 0; i < service.Description.Replicas; i++) { dockerInfo.Tasks[i] = RunDockerContainer(Enumerable.Empty <(int, int?, int, string?)>()); } } service.Items[typeof(DockerInformation)] = dockerInfo; }
public TyeHost(Tye.Hosting.Model.Application application, string[] args) : this(application, args, new string[0]) { }
public TyeHost(Tye.Hosting.Model.Application application, string[] args, string[] servicesToDebug) { _application = application; _args = args; _servicesToDebug = servicesToDebug; }
private async Task StartContainerAsync(Tye.Hosting.Model.Application application, Tye.Hosting.Model.Service service, DockerRunInfo docker) { if (!await DockerDetector.Instance.IsDockerInstalled.Value) { _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not installed.", service.Description.Name); service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not installed."); return; } if (!await DockerDetector.Instance.IsDockerConnectedToDaemon.Value) { _logger.LogError("Unable to start docker container for service {ServiceName}, Docker is not running.", service.Description.Name); service.Logs.OnNext($"Unable to start docker container for service {service.Description.Name}, Docker is not running."); return; } var serviceDescription = service.Description; var environmentArguments = ""; var dockerInfo = new DockerInformation(new Task[service.Description.Replicas]); async Task RunDockerContainer(IEnumerable <(int Port, int?InternalPort, int BindingPort, string?Protocol)> ports) { var hasPorts = ports.Any(); var replica = service.Description.Name.ToLower() + "_" + Guid.NewGuid().ToString().Substring(0, 10).ToLower(); var status = new DockerStatus(service, replica); service.Replicas[replica] = status; service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Added, status)); var environment = new Dictionary <string, string> { // Default to development environment ["DOTNET_ENVIRONMENT"] = "Development", // Remove the color codes from the console output ["DOTNET_LOGGING__CONSOLE__DISABLECOLORS"] = "true" }; var portString = ""; if (hasPorts) { status.Ports = ports.Select(p => p.Port); portString = string.Join(" ", ports.Select(p => $"-p {p.Port}:{p.InternalPort ?? p.Port}")); foreach (var p in ports) { environment[$"{p.Protocol?.ToUpper() ?? "HTTP"}_PORT"] = p.BindingPort.ToString(); } } application.PopulateEnvironment(service, (key, value) => environment[key] = value, "host.docker.internal"); environment["APP_INSTANCE"] = replica; foreach (var pair in environment) { environmentArguments += $"-e {pair.Key}={pair.Value} "; } var command = $"run -d {environmentArguments} {portString} --name {replica} --restart=unless-stopped {docker.Image} {docker.Args ?? ""}"; _logger.LogInformation("Running docker command {Command}", command); service.Logs.OnNext($"[{replica}]: {command}"); status.DockerCommand = command; var result = await ProcessUtil.RunAsync( "docker", command, throwOnError : false, cancellationToken : dockerInfo.StoppingTokenSource.Token, outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}")); if (result.ExitCode != 0) { _logger.LogError("docker run failed for {ServiceName} with exit code {ExitCode}:" + result.StandardError, service.Description.Name, result.ExitCode); service.Replicas.TryRemove(replica, out _); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); PrintStdOutAndErr(service, replica, result); return; } var containerId = (string?)result.StandardOutput.Trim(); // There's a race condition that sometimes makes us miss the output // so keep trying to get the container id while (string.IsNullOrEmpty(containerId)) { // Try to get the ID of the container result = await ProcessUtil.RunAsync("docker", $"ps --no-trunc -f name={replica} --format " + "{{.ID}}"); containerId = result.ExitCode == 0 ? result.StandardOutput.Trim() : null; } var shortContainerId = containerId.Substring(0, Math.Min(12, containerId.Length)); status.ContainerId = shortContainerId; _logger.LogInformation("Running container {ContainerName} with ID {ContainerId}", replica, shortContainerId); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Started, status)); _logger.LogInformation("Collecting docker logs for {ContainerName}.", replica); await ProcessUtil.RunAsync("docker", $"logs -f {containerId}", outputDataReceived : data => service.Logs.OnNext($"[{replica}]: {data}"), onStart : pid => { status.DockerLogsPid = pid; }, throwOnError : false, cancellationToken : dockerInfo.StoppingTokenSource.Token); _logger.LogInformation("docker logs collection for {ContainerName} complete with exit code {ExitCode}", replica, result.ExitCode); // Docker has a tendency to get stuck so we're going to timeout this shutdown process var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); _logger.LogInformation("Stopping container {ContainerName} with ID {ContainerId}", replica, shortContainerId); result = await ProcessUtil.RunAsync("docker", $"stop {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token); PrintStdOutAndErr(service, replica, result); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Stopped, status)); _logger.LogInformation("Stopped container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode); result = await ProcessUtil.RunAsync("docker", $"rm {containerId}", throwOnError : false, cancellationToken : timeoutCts.Token); PrintStdOutAndErr(service, replica, result); _logger.LogInformation("Removed container {ContainerName} with ID {ContainerId} exited with {ExitCode}", replica, shortContainerId, result.ExitCode); service.Replicas.TryRemove(replica, out _); service.ReplicaEvents.OnNext(new ReplicaEvent(ReplicaState.Removed, status)); }; if (serviceDescription.Bindings.Count > 0) { // Each replica is assigned a list of internal ports, one mapped to each external // port for (var i = 0; i < serviceDescription.Replicas; i++) { var ports = new List <(int, int?, int, string?)>(); foreach (var binding in serviceDescription.Bindings) { if (binding.Port == null) { continue; } ports.Add((service.PortMap[binding.Port.Value][i], binding.InternalPort, binding.Port.Value, binding.Protocol)); } dockerInfo.Tasks[i] = RunDockerContainer(ports); } } else { for (var i = 0; i < service.Description.Replicas; i++) { dockerInfo.Tasks[i] = RunDockerContainer(Enumerable.Empty <(int, int?, int, string?)>()); } } service.Items[typeof(DockerInformation)] = dockerInfo; }
public TyeHost(Tye.Hosting.Model.Application application, string[] args) { _application = application; _args = args; }