using System.Diagnostics.CodeAnalysis;
using System.IO.Compression;
using NLog;
using Octokit;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Database;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Core.Models.Packages;
///
/// Base class for packages that are hosted on Github.
/// Author and Name should be the Github username and repository name respectively.
///
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[SuppressMessage("ReSharper", "VirtualMemberNeverOverridden.Global")]
public abstract class BaseGitPackage : BasePackage
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
protected readonly IGithubApiCache GithubApi;
protected readonly ISettingsManager SettingsManager;
protected readonly IDownloadService DownloadService;
protected readonly IPrerequisiteHelper PrerequisiteHelper;
public PyVenvRunner? VenvRunner;
///
/// URL of the hosted web page on launch
///
protected string WebUrl = string.Empty;
public override string GithubUrl => $"https://github.com/{Author}/{Name}";
public override string DownloadLocation =>
Path.Combine(SettingsManager.LibraryDir, "Packages", $"{Name}.zip");
public override string InstallLocation { get; set; }
protected string GetDownloadUrl(string tagName, bool isCommitHash = false)
{
return isCommitHash
? $"https://github.com/{Author}/{Name}/archive/{tagName}.zip"
: $"https://api.github.com/repos/{Author}/{Name}/zipball/{tagName}";
}
protected BaseGitPackage(IGithubApiCache githubApi, ISettingsManager settingsManager,
IDownloadService downloadService, IPrerequisiteHelper prerequisiteHelper)
{
GithubApi = githubApi;
SettingsManager = settingsManager;
DownloadService = downloadService;
PrerequisiteHelper = prerequisiteHelper;
}
protected async Task GetLatestRelease(bool includePrerelease = false)
{
var releases = await GithubApi
.GetAllReleases(Author, Name)
.ConfigureAwait(false);
return includePrerelease ? releases.First() : releases.First(x => !x.Prerelease);
}
public override Task> GetAllBranches()
{
return GithubApi.GetAllBranches(Author, Name);
}
public override Task> GetAllReleases()
{
return GithubApi.GetAllReleases(Author, Name);
}
public override Task?> GetAllCommits(string branch, int page = 1, int perPage = 10)
{
return GithubApi.GetAllCommits(Author, Name, branch, page, perPage);
}
public override async Task> GetAllVersions(bool isReleaseMode = true)
{
// Release mode
if (isReleaseMode)
{
var allReleases = await GetAllReleases().ConfigureAwait(false);
return allReleases.Where(r => r.Prerelease == false).Select(r =>
new PackageVersion
{
TagName = r.TagName!,
ReleaseNotesMarkdown = r.Body
});
}
// Branch mode
var allBranches = await GetAllBranches().ConfigureAwait(false);
return allBranches.Select(b => new PackageVersion
{
TagName = $"{b.Name}",
ReleaseNotesMarkdown = string.Empty
});
}
///
/// Setup the virtual environment for the package.
///
///
///
///
[MemberNotNull(nameof(VenvRunner))]
protected async Task SetupVenv(string installedPackagePath, string venvName = "venv")
{
var venvPath = Path.Combine(installedPackagePath, "venv");
VenvRunner?.Dispose();
VenvRunner = new PyVenvRunner(venvPath);
if (!VenvRunner.Exists())
{
await VenvRunner.Setup().ConfigureAwait(false);
}
return VenvRunner;
}
public override async Task> GetReleaseTags()
{
var allReleases = await GithubApi
.GetAllReleases(Author, Name)
.ConfigureAwait(false);
return allReleases;
}
public override async Task DownloadPackage(string version, bool isCommitHash,
IProgress? progress = null)
{
var downloadUrl = GetDownloadUrl(version, isCommitHash);
if (!Directory.Exists(DownloadLocation.Replace($"{Name}.zip", "")))
{
Directory.CreateDirectory(DownloadLocation.Replace($"{Name}.zip", ""));
}
await DownloadService
.DownloadToFileAsync(downloadUrl, DownloadLocation, progress: progress)
.ConfigureAwait(false);
progress?.Report(new ProgressReport(100, message: "Download Complete"));
return version;
}
public override async Task InstallPackage(IProgress? progress = null)
{
await UnzipPackage(progress).ConfigureAwait(false);
progress?.Report(new ProgressReport(1f, $"{DisplayName} installed successfully"));
File.Delete(DownloadLocation);
}
protected Task UnzipPackage(IProgress? progress = null)
{
using var zip = ZipFile.OpenRead(DownloadLocation);
var zipDirName = string.Empty;
var totalEntries = zip.Entries.Count;
var currentEntry = 0;
foreach (var entry in zip.Entries)
{
currentEntry++;
if (string.IsNullOrWhiteSpace(entry.Name) && entry.FullName.EndsWith("/"))
{
if (string.IsNullOrWhiteSpace(zipDirName))
{
zipDirName = entry.FullName;
}
var folderPath = Path.Combine(InstallLocation,
entry.FullName.Replace(zipDirName, string.Empty));
Directory.CreateDirectory(folderPath);
continue;
}
var destinationPath = Path.GetFullPath(Path.Combine(InstallLocation,
entry.FullName.Replace(zipDirName, string.Empty)));
entry.ExtractToFile(destinationPath, true);
progress?.Report(new ProgressReport(current: Convert.ToUInt64(currentEntry),
total: Convert.ToUInt64(totalEntries)));
}
progress?.Report(new ProgressReport(1f, $"{DisplayName} installed successfully"));
return Task.CompletedTask;
}
public override async Task CheckForUpdates(InstalledPackage package)
{
var currentVersion = package.PackageVersion;
if (string.IsNullOrWhiteSpace(currentVersion))
{
return false;
}
try
{
if (string.IsNullOrWhiteSpace(package.InstalledBranch))
{
var latestVersion = await GetLatestVersion().ConfigureAwait(false);
UpdateAvailable = latestVersion != currentVersion;
return latestVersion != currentVersion;
}
else
{
var allCommits = (await GetAllCommits(package.InstalledBranch)
.ConfigureAwait(false))?.ToList();
if (allCommits == null || !allCommits.Any())
{
Logger.Warn("No commits found for {Package}", package.PackageName);
return false;
}
var latestCommitHash = allCommits.First().Sha;
return latestCommitHash != currentVersion;
}
}
catch (ApiException e)
{
Logger.Warn(e, "Failed to check for package updates");
return false;
}
}
public override async Task Update(InstalledPackage installedPackage,
IProgress? progress = null, bool includePrerelease = false)
{
if (string.IsNullOrWhiteSpace(installedPackage.InstalledBranch))
{
var releases = await GetAllReleases().ConfigureAwait(false);
var latestRelease = releases.First(x => includePrerelease || !x.Prerelease);
await DownloadPackage(latestRelease.TagName, false, progress).ConfigureAwait(false);
await InstallPackage(progress).ConfigureAwait(false);
return latestRelease.TagName;
}
else
{
var allCommits = await GetAllCommits(
installedPackage.InstalledBranch).ConfigureAwait(false);
var latestCommit = allCommits.First();
await DownloadPackage(latestCommit.Sha, true, progress);
await InstallPackage(progress).ConfigureAwait(false);
return latestCommit.Sha;
}
}
// Send input to the running process.
public virtual void SendInput(string input)
{
var process = VenvRunner?.Process;
if (process == null)
{
Logger.Warn("No process running for {Name}", Name);
return;
}
process.StandardInput.WriteLine(input);
}
public virtual async Task SendInputAsync(string input)
{
var process = VenvRunner?.Process;
if (process == null)
{
Logger.Warn("No process running for {Name}", Name);
return;
}
await process.StandardInput.WriteLineAsync(input).ConfigureAwait(false);
}
///
public override void Shutdown()
{
if (VenvRunner is not null)
{
VenvRunner.Dispose();
VenvRunner = null;
}
}
///
public override async Task WaitForShutdown()
{
if (VenvRunner is not null)
{
await VenvRunner.DisposeAsync().ConfigureAwait(false);
VenvRunner = null;
}
}
}