- Upgrade from .NET Core 3.1 to .NET 10, SSH.NET 2016.1.0 to 2024.1.0 - Fix ServerData JSON deserialization crash with boolean lsblk fields (#13) - Fix DiskFreeResult.Parse overflow with TryParse and null safety (#20) - Fix SSH connection issues via SSH.NET library upgrade (#22) - Fix bash generation to detect existing btcpayserver-docker dir (#5) - Add disk space and server resources display in Summary page (#6) - Add FastSync support for initial Bitcoin sync (#9) - Add SSH key authentication for remote deployments (#23) - Replace CircleCI with GitHub Actions publishing to GHCR and Docker Hub (#24) - Add unified multi-arch Dockerfile using Docker Buildx (#24) - Make additional services data-driven via ServiceRegistry (#27) - Add CloudInit deployment type for VPS provisioning (#28) - Fix Summary page duplicate Libre Patron entry and typos (#26) - Replace deprecated WebClient with IHttpClientFactory - Remove Startup.cs in favor of minimal hosting in Program.cs - Fix ThunderHub validation targeting wrong model property
181 lines
5.8 KiB
C#
181 lines
5.8 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Text;
|
|
using BTCPayServerDockerConfigurator.Controllers;
|
|
using BTCPayServerDockerConfigurator.Models;
|
|
using Microsoft.Extensions.Options;
|
|
using Renci.SshNet;
|
|
|
|
namespace BTCPayServerDockerConfigurator;
|
|
|
|
public class DeploymentService
|
|
{
|
|
private readonly IOptions<ConfiguratorOptions> _options;
|
|
private readonly ILogger<DeploymentService> _logger;
|
|
|
|
private readonly ConcurrentDictionary<string,
|
|
(Task<UpdateSettings<ConfiguratorSettings, DeployAdditionalData>>, StringBuilder)>
|
|
OngoingDeployments = new();
|
|
|
|
public DeploymentService(IOptions<ConfiguratorOptions> options,
|
|
ILogger<DeploymentService> logger)
|
|
{
|
|
_options = options;
|
|
_logger = logger;
|
|
}
|
|
|
|
public string StartDeployment(ConfiguratorSettings configuratorSettings, bool verified)
|
|
{
|
|
var id = Guid.NewGuid().ToString();
|
|
var sb = new StringBuilder();
|
|
|
|
var tcs =
|
|
new TaskCompletionSource<
|
|
UpdateSettings<ConfiguratorSettings, DeployAdditionalData>>();
|
|
_ = Task.Run(async () =>
|
|
{
|
|
var bash = configuratorSettings.ConstructBashFile();
|
|
|
|
if (configuratorSettings.DeploymentSettings.DeploymentType == DeploymentType.Manual)
|
|
{
|
|
tcs.SetResult(new UpdateSettings<ConfiguratorSettings, DeployAdditionalData>
|
|
{
|
|
Additional = new DeployAdditionalData { Bash = bash },
|
|
Json = configuratorSettings.ToString(),
|
|
Settings = configuratorSettings
|
|
});
|
|
return;
|
|
}
|
|
|
|
var ssh = configuratorSettings.GetSshSettings(_options.Value, verified);
|
|
|
|
try
|
|
{
|
|
using var connection = await ssh.ConnectAsync();
|
|
var shell =
|
|
connection.CreateShellStream("xterm", 50, 50, 640, 480, 17640);
|
|
var commands = bash
|
|
.Replace(Environment.NewLine, "\n")
|
|
.Split("\n", StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
var result = await RunCommandsInShellSequential(commands, shell, sb);
|
|
var exitcode = result.Item1 ? 0 : -1;
|
|
|
|
tcs.SetResult(new UpdateSettings<ConfiguratorSettings, DeployAdditionalData>
|
|
{
|
|
Additional = new DeployAdditionalData
|
|
{
|
|
Bash = bash,
|
|
Error = "",
|
|
ExitStatus = exitcode,
|
|
Output = result.Item2.Replace(Environment.NewLine, "\n"),
|
|
InProgress = false
|
|
},
|
|
Json = configuratorSettings.ToString(),
|
|
Settings = configuratorSettings
|
|
});
|
|
|
|
shell.Dispose();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
tcs.SetResult(new UpdateSettings<ConfiguratorSettings, DeployAdditionalData>
|
|
{
|
|
Additional = new DeployAdditionalData
|
|
{
|
|
Bash = bash,
|
|
Error = e.Message,
|
|
ExitStatus = -1,
|
|
InProgress = false
|
|
},
|
|
Json = configuratorSettings.ToString(),
|
|
Settings = configuratorSettings
|
|
});
|
|
}
|
|
});
|
|
|
|
OngoingDeployments.TryAdd(id, (tcs.Task, sb));
|
|
return id;
|
|
}
|
|
|
|
private async Task<(bool, string)> RunCommandsInShellSequential(string[] commands,
|
|
ShellStream shellStream, StringBuilder result)
|
|
{
|
|
var failed = false;
|
|
var finished = false;
|
|
var cts = new TaskCompletionSource<bool>();
|
|
shellStream.ErrorOccurred += (sender, args) =>
|
|
{
|
|
result.AppendLine(args.Exception.Message);
|
|
failed = true;
|
|
cts.TrySetResult(false);
|
|
};
|
|
shellStream.DataReceived += (sender, args) =>
|
|
{
|
|
var str = Encoding.UTF8.GetString(args.Data);
|
|
if (!str.Contains("end-of-command"))
|
|
{
|
|
result.Append(str);
|
|
}
|
|
|
|
if (str.Contains("end-of-command 2"))
|
|
{
|
|
finished = true;
|
|
}
|
|
};
|
|
shellStream.Read();
|
|
for (var index = 0; index < commands.Length; index++)
|
|
{
|
|
var command = commands[index];
|
|
if (failed)
|
|
break;
|
|
|
|
shellStream.WriteLine(command);
|
|
waiter:
|
|
shellStream.WriteLine("echo \"end-of-command $(expr 1 + 1)\"");
|
|
var counter = 0;
|
|
while (!finished)
|
|
{
|
|
if (counter > 10)
|
|
{
|
|
goto waiter;
|
|
}
|
|
|
|
await Task.Delay(TimeSpan.FromMilliseconds(500));
|
|
counter++;
|
|
}
|
|
|
|
if (commands.Length == index + 1)
|
|
{
|
|
cts.TrySetResult(true);
|
|
}
|
|
|
|
finished = false;
|
|
}
|
|
|
|
failed = !await cts.Task;
|
|
return (!failed, result.ToString());
|
|
}
|
|
|
|
public UpdateSettings<ConfiguratorSettings, DeployAdditionalData> GetDeploymentResult(
|
|
string id)
|
|
{
|
|
if (!OngoingDeployments.ContainsKey(id))
|
|
return null;
|
|
|
|
if (OngoingDeployments[id].Item1.IsCompleted)
|
|
{
|
|
return OngoingDeployments.Remove(id, out var result) ? result.Item1.Result : null;
|
|
}
|
|
|
|
return new UpdateSettings<ConfiguratorSettings, DeployAdditionalData>
|
|
{
|
|
Additional = new DeployAdditionalData
|
|
{
|
|
InProgress = true,
|
|
Output = OngoingDeployments[id].Item2.ToString()
|
|
.Replace(Environment.NewLine, "\n")
|
|
}
|
|
};
|
|
}
|
|
}
|