JT
1 year ago
15 changed files with 422 additions and 257 deletions
@ -0,0 +1,266 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Diagnostics; |
||||||
|
using System.Diagnostics.CodeAnalysis; |
||||||
|
using System.IO; |
||||||
|
using System.IO.Compression; |
||||||
|
using System.Linq; |
||||||
|
using System.Net.Http; |
||||||
|
using System.Net.Http.Headers; |
||||||
|
using System.Threading; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using NLog; |
||||||
|
using Refit; |
||||||
|
using StabilityMatrix.Api; |
||||||
|
using StabilityMatrix.Helper; |
||||||
|
using StabilityMatrix.Models.Api; |
||||||
|
using StabilityMatrix.Python; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models.Packages; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Base class for packages that are hosted on Github. |
||||||
|
/// Author and Name should be the Github username and repository name respectively. |
||||||
|
/// </summary> |
||||||
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] |
||||||
|
public abstract class BaseGitPackage : BasePackage |
||||||
|
{ |
||||||
|
protected static readonly Logger Logger = LogManager.GetCurrentClassLogger(); |
||||||
|
protected readonly IGithubApi GithubApi; |
||||||
|
protected readonly ISettingsManager SettingsManager; |
||||||
|
protected PyVenvRunner? VenvRunner; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// URL of the hosted web page on launch |
||||||
|
/// </summary> |
||||||
|
protected string WebUrl = string.Empty; |
||||||
|
|
||||||
|
public override string GithubUrl => $"https://github.com/{Author}/{Name}"; |
||||||
|
|
||||||
|
public override string DownloadLocation => |
||||||
|
$"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\StabilityMatrix\\Packages\\{Name}.zip"; |
||||||
|
|
||||||
|
public override string InstallLocation { get; set; } = |
||||||
|
$"{Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)}\\StabilityMatrix\\Packages"; |
||||||
|
|
||||||
|
protected string GetDownloadUrl(string tagName) => $"https://api.github.com/repos/{Author}/{Name}/zipball/{tagName}"; |
||||||
|
|
||||||
|
protected BaseGitPackage(IGithubApi githubApi, ISettingsManager settingsManager) |
||||||
|
{ |
||||||
|
this.GithubApi = githubApi; |
||||||
|
this.SettingsManager = settingsManager; |
||||||
|
} |
||||||
|
|
||||||
|
protected Task<GithubRelease> GetLatestRelease() |
||||||
|
{ |
||||||
|
return GithubApi.GetLatestRelease(Author, Name); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Setup the virtual environment for the package. |
||||||
|
/// </summary> |
||||||
|
/// <param name="installedPackagePath"></param> |
||||||
|
/// <param name="venvName"></param> |
||||||
|
/// <returns></returns> |
||||||
|
protected async Task<PyVenvRunner> SetupVenv(string installedPackagePath, string venvName = "venv") |
||||||
|
{ |
||||||
|
var venvPath = Path.Combine(installedPackagePath, "venv"); |
||||||
|
VenvRunner?.Dispose(); |
||||||
|
VenvRunner = new PyVenvRunner(venvPath); |
||||||
|
if (!VenvRunner.Exists()) |
||||||
|
{ |
||||||
|
await VenvRunner.Setup(); |
||||||
|
} |
||||||
|
return VenvRunner; |
||||||
|
} |
||||||
|
|
||||||
|
public override async Task<IEnumerable<string>> GetVersions() |
||||||
|
{ |
||||||
|
var allReleases = await GithubApi.GetAllReleases(Author, Name); |
||||||
|
return allReleases.Select(release => release.TagName!); |
||||||
|
} |
||||||
|
|
||||||
|
public override async Task<string?> DownloadPackage(bool isUpdate = false, string? version = null) |
||||||
|
{ |
||||||
|
var latestRelease = await GetLatestRelease(); |
||||||
|
var latestTagName = latestRelease.TagName; |
||||||
|
if (string.IsNullOrWhiteSpace(latestTagName) && string.IsNullOrWhiteSpace(version)) |
||||||
|
{ |
||||||
|
throw new Exception("Could not find latest release. Both latest release and version are null or empty."); |
||||||
|
} |
||||||
|
var tagName = version ?? latestTagName!; |
||||||
|
var downloadUrl = GetDownloadUrl(tagName); |
||||||
|
|
||||||
|
if (!Directory.Exists(DownloadLocation.Replace($"{Name}.zip", ""))) |
||||||
|
{ |
||||||
|
Directory.CreateDirectory(DownloadLocation.Replace($"{Name}.zip", "")); |
||||||
|
} |
||||||
|
|
||||||
|
using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(5) }; |
||||||
|
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("StabilityMatrix", "1.0")); |
||||||
|
await using var file = new FileStream(DownloadLocation, FileMode.Create, FileAccess.Write, FileShare.None); |
||||||
|
|
||||||
|
|
||||||
|
long contentLength = 0; |
||||||
|
var retryCount = 0; |
||||||
|
var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead); |
||||||
|
while (contentLength == 0 && retryCount++ < 5) |
||||||
|
{ |
||||||
|
response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead); |
||||||
|
contentLength = response.Content.Headers.ContentLength ?? 0; |
||||||
|
Logger.Debug("Retrying get-headers for content-length"); |
||||||
|
Thread.Sleep(50); |
||||||
|
} |
||||||
|
|
||||||
|
var isIndeterminate = contentLength == 0; |
||||||
|
|
||||||
|
await using var stream = await response.Content.ReadAsStreamAsync(); |
||||||
|
var totalBytesRead = 0; |
||||||
|
while (true) |
||||||
|
{ |
||||||
|
var buffer = new byte[1024]; |
||||||
|
var bytesRead = await stream.ReadAsync(buffer); |
||||||
|
if (bytesRead == 0) break; |
||||||
|
await file.WriteAsync(buffer.AsMemory(0, bytesRead)); |
||||||
|
|
||||||
|
totalBytesRead += bytesRead; |
||||||
|
|
||||||
|
if (isIndeterminate) |
||||||
|
{ |
||||||
|
if (isUpdate) |
||||||
|
{ |
||||||
|
OnUpdateProgressChanged(-1); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
OnDownloadProgressChanged(-1); |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
var progress = (int)(totalBytesRead * 100d / contentLength); |
||||||
|
Logger.Debug($"Progress; {progress}"); |
||||||
|
|
||||||
|
if (isUpdate) |
||||||
|
{ |
||||||
|
OnUpdateProgressChanged(progress); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
OnDownloadProgressChanged(progress); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
await file.FlushAsync(); |
||||||
|
OnDownloadComplete(DownloadLocation); |
||||||
|
|
||||||
|
return tagName; |
||||||
|
} |
||||||
|
|
||||||
|
private void UnzipPackage(bool isUpdate = false) |
||||||
|
{ |
||||||
|
if (isUpdate) |
||||||
|
{ |
||||||
|
OnInstallProgressChanged(0); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
OnUpdateProgressChanged(0); |
||||||
|
} |
||||||
|
|
||||||
|
Directory.CreateDirectory(InstallLocation); |
||||||
|
|
||||||
|
using var zip = ZipFile.OpenRead(DownloadLocation); |
||||||
|
var zipDirName = string.Empty; |
||||||
|
var totalEntries = zip.Entries.Count; |
||||||
|
var currentEntry = 0; |
||||||
|
|
||||||
|
foreach (var entry in zip.Entries) |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(entry.Name) && entry.FullName.EndsWith("/")) |
||||||
|
{ |
||||||
|
if (string.IsNullOrWhiteSpace(zipDirName)) |
||||||
|
{ |
||||||
|
zipDirName = entry.FullName; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
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); |
||||||
|
currentEntry++; |
||||||
|
|
||||||
|
var progressValue = (int)((double)currentEntry / totalEntries * 100); |
||||||
|
|
||||||
|
if (isUpdate) |
||||||
|
{ |
||||||
|
OnUpdateProgressChanged(progressValue); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
OnInstallProgressChanged(progressValue); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public override Task InstallPackage(bool isUpdate = false) |
||||||
|
{ |
||||||
|
UnzipPackage(isUpdate); |
||||||
|
|
||||||
|
if (isUpdate) |
||||||
|
{ |
||||||
|
OnUpdateComplete("Update complete"); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
OnInstallComplete("Installation complete"); |
||||||
|
} |
||||||
|
|
||||||
|
return Task.CompletedTask; |
||||||
|
} |
||||||
|
|
||||||
|
public override async Task<bool> CheckForUpdates(string installedPackageName) |
||||||
|
{ |
||||||
|
var currentVersion = SettingsManager.Settings.InstalledPackages.FirstOrDefault(x => x.Name == installedPackageName) |
||||||
|
?.PackageVersion; |
||||||
|
if (string.IsNullOrWhiteSpace(currentVersion)) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
var latestRelease = await GetLatestRelease(); |
||||||
|
UpdateAvailable = latestRelease.TagName != currentVersion; |
||||||
|
return latestRelease.TagName != currentVersion; |
||||||
|
} |
||||||
|
catch (ApiException e) |
||||||
|
{ |
||||||
|
Logger.Error(e, "Failed to check for updates"); |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public override async Task<string?> Update() |
||||||
|
{ |
||||||
|
var version = await DownloadPackage(true); |
||||||
|
await InstallPackage(true); |
||||||
|
return version; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public override Task Shutdown() |
||||||
|
{ |
||||||
|
VenvRunner?.Dispose(); |
||||||
|
VenvRunner?.Process?.WaitForExitAsync(); |
||||||
|
return Task.CompletedTask; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Diagnostics; |
||||||
|
using System.IO; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using StabilityMatrix.Api; |
||||||
|
using StabilityMatrix.Helper; |
||||||
|
using StabilityMatrix.Python; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Models.Packages; |
||||||
|
|
||||||
|
public class VladAutomatic : BaseGitPackage |
||||||
|
{ |
||||||
|
public override string Name => "automatic"; |
||||||
|
public override string DisplayName { get; set; } = "SD.Next Web UI"; |
||||||
|
public override string Author => "vladmandic"; |
||||||
|
public override string LaunchCommand => "launch.py"; |
||||||
|
public override string DefaultLaunchArguments => $"{GetVramOption()} {GetXformersOption()}"; |
||||||
|
|
||||||
|
public VladAutomatic(IGithubApi githubApi, ISettingsManager settingsManager) : base(githubApi, settingsManager) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public override List<LaunchOptionDefinition> LaunchOptions => new() |
||||||
|
{ |
||||||
|
new() |
||||||
|
{ |
||||||
|
Name = "API", |
||||||
|
Options = new() { "--api" } |
||||||
|
}, |
||||||
|
new() |
||||||
|
{ |
||||||
|
Name = "VRAM", |
||||||
|
Options = new() { "--lowvram", "--medvram" } |
||||||
|
}, |
||||||
|
new() |
||||||
|
{ |
||||||
|
Name = "Xformers", |
||||||
|
Options = new() { "--xformers" } |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
public override async Task RunPackage(string installedPackagePath, string arguments) |
||||||
|
{ |
||||||
|
await SetupVenv(installedPackagePath); |
||||||
|
|
||||||
|
void HandleConsoleOutput(string? s) |
||||||
|
{ |
||||||
|
if (s == null) return; |
||||||
|
if (s.Contains("model loaded", StringComparison.OrdinalIgnoreCase)) |
||||||
|
{ |
||||||
|
OnStartupComplete(WebUrl); |
||||||
|
} |
||||||
|
if (s.Contains("Running on", StringComparison.OrdinalIgnoreCase)) |
||||||
|
{ |
||||||
|
WebUrl = s.Split(" ")[5]; |
||||||
|
} |
||||||
|
Debug.WriteLine($"process stdout: {s}"); |
||||||
|
OnConsoleOutput($"{s}\n"); |
||||||
|
} |
||||||
|
|
||||||
|
void HandleExit(int i) |
||||||
|
{ |
||||||
|
Debug.WriteLine($"Venv process exited with code {i}"); |
||||||
|
OnConsoleOutput($"Venv process exited with code {i}"); |
||||||
|
} |
||||||
|
|
||||||
|
var args = $"\"{Path.Combine(installedPackagePath, LaunchCommand)}\" {arguments}"; |
||||||
|
|
||||||
|
VenvRunner.RunDetached(args.TrimEnd(), HandleConsoleOutput, HandleExit, workingDirectory: installedPackagePath); |
||||||
|
} |
||||||
|
|
||||||
|
private static string GetVramOption() |
||||||
|
{ |
||||||
|
var vramGb = HardwareHelper.GetGpuMemoryBytes() / 1024 / 1024 / 1024; |
||||||
|
|
||||||
|
return vramGb switch |
||||||
|
{ |
||||||
|
< 4 => "--lowvram", |
||||||
|
< 8 => "--medvram", |
||||||
|
_ => string.Empty |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private static string GetXformersOption() |
||||||
|
{ |
||||||
|
var gpuName = HardwareHelper.GetGpuChipName(); |
||||||
|
return gpuName.Contains("NVIDIA", StringComparison.OrdinalIgnoreCase) ? "--xformers" : string.Empty; |
||||||
|
} |
||||||
|
} |
@ -1,7 +1,7 @@ |
|||||||
using System.IO; |
using System.IO; |
||||||
using System.Threading.Tasks; |
using System.Threading.Tasks; |
||||||
|
|
||||||
namespace StabilityMatrix; |
namespace StabilityMatrix.Python; |
||||||
|
|
||||||
public interface IPyRunner |
public interface IPyRunner |
||||||
{ |
{ |
Loading…
Reference in new issue