< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 168
Coverable lines: 168
Total lines: 305
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 34
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/dotnet-snap/dotnet-snap/src/Dotnet.Installer.Core/Services/Implementations/SnapService.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2using Dotnet.Installer.Core.Services.Contracts;
 3using Dotnet.Installer.Core.Types;
 4
 5namespace Dotnet.Installer.Core.Services.Implementations;
 6
 7public partial class SnapService : ISnapService
 8{
 9    public bool IsSnapInstalled(string name, CancellationToken cancellationToken = default)
 010    {
 011        return Directory.Exists(Path.Combine("/", "snap", name));
 012    }
 13
 14    public Task<Terminal.InvocationResult> Install(string name, bool edge = false, CancellationToken cancellationToken =
 015    {
 016        var arguments = new List<string>
 017        {
 018            "install"
 019        };
 20
 021        if (edge) arguments.Add("--edge");
 022        arguments.Add(name);
 23
 024        return Terminal.Invoke("snap", arguments.ToArray());
 025    }
 26
 27    public Task<Terminal.InvocationResult> Remove(string name, bool purge = false, CancellationToken cancellationToken =
 028    {
 029        var arguments = new List<string>
 030        {
 031            "remove"
 032        };
 33
 034        if (purge) arguments.Add("--purge");
 035        arguments.Add(name);
 36
 037        return Terminal.Invoke("snap", arguments.ToArray());
 038    }
 39
 40    public Task<IImmutableList<SnapInfo>> GetInstalledSnaps(CancellationToken cancellationToken = default)
 041    {
 42        SnapdRestClient snapdRestClient;
 43
 44        try
 045        {
 046            snapdRestClient = GetSnapdRestClient();
 047        }
 048        catch (Exception exception)
 049        {
 050            return Task.FromException<IImmutableList<SnapInfo>>(exception);
 51        }
 52
 053        return snapdRestClient.GetInstalledSnapsAsync(cancellationToken);
 054    }
 55
 56    public Task<SnapInfo?> GetInstalledSnap(string name, CancellationToken cancellationToken = default)
 057    {
 58        SnapdRestClient snapdRestClient;
 59
 60        try
 061        {
 062            snapdRestClient = GetSnapdRestClient();
 063        }
 064        catch (Exception exception)
 065        {
 066            return Task.FromException<SnapInfo?>(exception);
 67        }
 68
 069        return snapdRestClient.GetInstalledSnapAsync(name, cancellationToken);
 070    }
 71
 72    public Task<SnapInfo?> FindSnap(string name, CancellationToken cancellationToken = default)
 073    {
 74        SnapdRestClient snapdRestClient;
 75
 76        try
 077        {
 078            snapdRestClient = GetSnapdRestClient();
 079        }
 080        catch (Exception exception)
 081        {
 082            return Task.FromException<SnapInfo?>(exception);
 83        }
 84
 085        return snapdRestClient.FindSnapAsync(name, cancellationToken);
 086    }
 87
 88    public void Dispose()
 089    {
 090        _snapdRestClient?.Dispose();
 091    }
 92}

/home/runner/work/dotnet-snap/dotnet-snap/src/Dotnet.Installer.Core/Services/Implementations/SnapService.Private.cs

#LineLine coverage
 1using System.Collections.Immutable;
 2using System.Diagnostics.CodeAnalysis;
 3using System.Net;
 4using System.Net.Http.Json;
 5using System.Net.Sockets;
 6using System.Text.Json;
 7using Dotnet.Installer.Core.Types;
 8
 9namespace Dotnet.Installer.Core.Services.Implementations;
 10
 11public partial class SnapService
 12{
 13    private SnapdRestClient? _snapdRestClient;
 14
 15    private SnapdRestClient GetSnapdRestClient()
 016    {
 017        if (_snapdRestClient is null)
 018        {
 019            _snapdRestClient = new SnapdRestClient();
 020        }
 21
 022        return _snapdRestClient;
 023    }
 24
 25    /// <summary>
 26    /// HTTP client that interacts with the local snapd REST API via a Unix socket.
 27    /// </summary>
 28    /// <see href="https://snapcraft.io/docs/snapd-api"/>
 29    private class SnapdRestClient : IDisposable
 30    {
 31        private const string SnapdUnixSocketPath = "/run/snapd.socket";
 32
 33        private readonly HttpClient _httpClient;
 34        private readonly JsonSerializerOptions _jsonSerializerOptions;
 35
 036        public SnapdRestClient()
 037        {
 038            if (!File.Exists(SnapdUnixSocketPath))
 039            {
 040                throw new ApplicationException($"Could not find the snapd unix-socket {SnapdUnixSocketPath}");
 41            }
 42
 043            var httpMessageHandler = new SocketsHttpHandler
 044            {
 045                ConnectCallback = async (_, cancellationToken) =>
 046                {
 047                    var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
 048                    var endpoint = new UnixDomainSocketEndPoint(SnapdUnixSocketPath);
 049                    await socket.ConnectAsync(endpoint, cancellationToken).ConfigureAwait(false);
 050                    return new NetworkStream(socket, ownsSocket: false);
 051                }
 052            };
 53
 054            _httpClient = new HttpClient(httpMessageHandler);
 055            _httpClient.BaseAddress = new Uri("http://localhost");
 56
 057            _jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
 058            {
 059                PropertyNamingPolicy = JsonNamingPolicy.KebabCaseLower,
 060            };
 061        }
 62
 63        public async Task<IImmutableList<SnapInfo>> GetInstalledSnapsAsync(CancellationToken cancellationToken)
 064        {
 65            try
 066            {
 067                var snapdResponse = await RequestAsync(requestUri: "/v2/snaps", cancellationToken);
 68
 069                if (snapdResponse is { StatusCode: HttpStatusCode.OK })
 070                {
 071                    return snapdResponse.Deserialize<IImmutableList<SnapInfo>>(_jsonSerializerOptions);
 72                }
 73
 074                var error = snapdResponse.Deserialize<SnapdError>(_jsonSerializerOptions);
 75
 076                throw new ApplicationException(message: $"Snapd REST API responded with status code " +
 077                                                        $"{(int)snapdResponse.StatusCode} ({snapdResponse.Status}): " +
 078                                                        error.Message);
 79            }
 080            catch (Exception exception) when (exception is not OperationCanceledException)
 081            {
 082                throw new ApplicationException(
 083                    message: "An unexpected failure occured while requesting information about the local snaps",
 084                    innerException: exception);
 85            }
 086        }
 87
 88        public async Task<SnapInfo?> GetInstalledSnapAsync(string snapName, CancellationToken cancellationToken = defaul
 089        {
 90            try
 091            {
 092                var snapdResponse = await RequestAsync(
 093                    requestUri: $"/v2/snaps/{Uri.EscapeDataString(snapName)}",
 094                    cancellationToken);
 95
 096                if (snapdResponse is { StatusCode: HttpStatusCode.OK })
 097                {
 098                    return snapdResponse.Deserialize<SnapInfo>(_jsonSerializerOptions);
 99                }
 100
 0101                var error = snapdResponse.Deserialize<SnapdError>(_jsonSerializerOptions);
 102
 0103                if (error is { Kind: "snap-not-found" })
 0104                {
 0105                    return null;
 106                }
 107
 0108                throw new ApplicationException(message: $"Snapd REST API responded with status code " +
 0109                                                        $"{(int)snapdResponse.StatusCode} ({snapdResponse.Status}): " +
 0110                                                        error.Message);
 111            }
 0112            catch (Exception exception) when (exception is not OperationCanceledException)
 0113            {
 0114                throw new ApplicationException(
 0115                    message: $"An unexpected failure occured while requesting information about the local snap \"{snapNa
 0116                    innerException: exception);
 117            }
 0118        }
 119
 120        /// <summary>
 121        /// Search for snaps whose name matches the given string.
 122        /// </summary>
 123        /// <param name="snapName">
 124        /// The name of the snap to search for. The match is exact
 125        /// (i.e. <see cref="FindSnapAsync"/> would return 0 or 1 results) unless the string ends in <c>*</c>
 126        /// </param>
 127        /// <param name="cancellationToken">The cancellation token to cancel the operation.</param>
 128        /// <returns>A list of snaps that matches the specified <paramref name="snapName"/></returns>
 129        /// <exception cref="ObjectDisposedException"></exception>
 130        /// <exception cref="ApplicationException"></exception>
 131        /// <exception cref="OperationCanceledException"></exception>
 132        /// <seealso href="https://snapcraft.io/docs/snapd-api#heading--find"/>
 133        public async Task<SnapInfo?> FindSnapAsync(string snapName, CancellationToken cancellationToken = default)
 0134        {
 135            try
 0136            {
 0137                var snapdResponse = await RequestAsync(
 0138                    requestUri: $"/v2/find?name={Uri.EscapeDataString(snapName)}",
 0139                    cancellationToken);
 140
 0141                if (snapdResponse is { StatusCode: HttpStatusCode.OK })
 0142                {
 0143                    return snapdResponse.Deserialize<SnapInfo[]>(_jsonSerializerOptions).Single();
 144                }
 145
 0146                var error = snapdResponse.Deserialize<SnapdError>(_jsonSerializerOptions);
 147
 0148                if (error is { Kind: "snap-not-found" })
 0149                {
 0150                    return null;
 151                }
 152
 0153                throw new ApplicationException(message: $"Snapd REST API responded with status code " +
 0154                                               $"{(int)snapdResponse.StatusCode} ({snapdResponse.Status}): " +
 0155                                               error.Message);
 156            }
 0157            catch (Exception exception) when (exception is not OperationCanceledException)
 0158            {
 0159                throw new ApplicationException(
 0160                    message: $"An unexpected failure occured while requesting information about the snap \"{snapName}\""
 0161                    innerException: exception);
 162            }
 0163        }
 164
 165        private async Task<SnapdResponse> RequestAsync(string requestUri, CancellationToken cancellationToken)
 0166        {
 0167            cancellationToken.ThrowIfCancellationRequested();
 168
 0169            var snapdResponse = await _httpClient
 0170                .GetFromJsonAsync<SnapdResponse>(requestUri, _jsonSerializerOptions, cancellationToken)
 0171                .ConfigureAwait(false);
 172
 0173            return snapdResponse ?? throw new ApplicationException("Snapd REST API response is null");
 0174        }
 175
 176        public void Dispose()
 0177        {
 0178            _httpClient.Dispose();
 0179        }
 180
 0181        private record SnapdResponse(
 0182            string Type,
 0183            HttpStatusCode StatusCode,
 0184            string Status,
 0185            JsonElement Result)
 186        {
 187            public TResult Deserialize<TResult>(JsonSerializerOptions jsonDeserializerOptions)
 0188            {
 0189                return Result.Deserialize<TResult>(jsonDeserializerOptions)
 0190                       ?? throw new ApplicationException("Snapd REST API result is null");
 0191            }
 192
 193            public bool TryGetResultAs<TResult>(
 194                JsonSerializerOptions jsonDeserializerOptions,
 195                [NotNullWhen(returnValue: true)]
 196                out TResult? result)
 0197            {
 198                try
 0199                {
 0200                    result = Result.Deserialize<TResult>(jsonDeserializerOptions);
 0201                }
 0202                catch
 0203                {
 0204                    result = default;
 0205                }
 206
 0207                return result is not null;
 0208            }
 209        }
 210
 0211        private record SnapdError(string Message, string? Kind);
 212    }
 213}