using Docker.DotNet; using Docker.DotNet.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace AnsonBot.Core; public interface IRconService { Task SendAsync(string command, CancellationToken ct = default); } public class DockerRconService : IRconService, IDisposable { private readonly DockerOptions _options; private readonly ILogger _logger; private readonly DockerClient _client; public DockerRconService( IOptions options, ILogger logger) { _options = options.Value; _logger = logger; var config = string.IsNullOrWhiteSpace(_options.DaemonUri) ? new DockerClientConfiguration() : new DockerClientConfiguration(new Uri(_options.DaemonUri)); _client = config.CreateClient(); } public async Task SendAsync(string command, CancellationToken ct = default) { using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(_options.TimeoutSeconds)); _logger.LogInformation("Exec in {Container}: {Cli} {Command}", _options.ContainerName, _options.RconCliPath, command); var exec = await _client.Exec.ExecCreateContainerAsync( _options.ContainerName, new ContainerExecCreateParameters { // Passed as argv, so no shell quoting concerns. Cmd = new List { _options.RconCliPath, command }, AttachStdout = true, AttachStderr = true }, cts.Token); using var stream = await _client.Exec.StartAndAttachContainerExecAsync( exec.ID, tty: false, cts.Token); var (stdout, stderr) = await stream.ReadOutputToEndAsync(cts.Token); var inspect = await _client.Exec.InspectContainerExecAsync(exec.ID, cts.Token); if (inspect.ExitCode != 0) { _logger.LogError("rcon exec failed (exit {Code}): {Error}", inspect.ExitCode, stderr); throw new InvalidOperationException( $"'{_options.RconCliPath} {command}' exited with code {inspect.ExitCode}: {stderr}"); } return stdout; } public void Dispose() => _client.Dispose(); }