edge-install/Program.cs
Robert Morrison f650e6a3c9 HELL COMMIT
Commit sins to add all code into repo
2023-05-28 15:11:41 +01:00

476 lines
17 KiB
C#

//using System.CommandLine;
using Serilog;
using Spectre.Console;
using System.IO.Compression;
using System.Text.Json;
using System.Security.Cryptography;
using System.Text;
namespace EdgeInstaller;
public static class EdgeInstall
{
private static HttpClient _client = new HttpClient();
private static readonly string DataDir =
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"ClosedLess",
"EdgeInstall-Jankmode");
public static async Task<int> Main()
{
// WARN: Hardcoded variables are bad m'kay...
string Arch = "amd64";
// Ensure DataDir exists.
// NOTE: Directory.CreateDirectory will ALWAYS create the entire path
Directory.CreateDirectory(DataDir);
// TODO: wrap this code in foot detection since foot isn't detected properly
// BUG: Report detection error to Spectre.Console (foot == xterm)
// HACK: We have to force this right now for foot to work properly...
AnsiConsole.Console.Profile.Capabilities.Ansi = true;
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(".EdgeInstaller.Log") // Only ever log to the logfile
.CreateLogger();
var Release = AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green"))
.StartAsync(
"Getting Latest Release data",
_ => GetRelease()
);
string ReleaseString = await Release;
Log.Debug("{rs}", ReleaseString);
AnsiConsole.MarkupLine("[green]Attempting to decode release data now[/]");
ReleaseData data = getReleaseData(ReleaseString);
RenderReleaseTable(data);
Arch = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Choose Architecture")
.PageSize(10)
.MoreChoicesText("[grey]Scroll for more...[/]")
.AddChoices(data.Architectures)
);
// create a directory for the chosen architecture
// NOTE: we also grab the path here so we can use it later.
var Archdir = Directory.CreateDirectory(Path.Combine(DataDir, Arch)).FullName;
// Check if the Release data we just grabbed is newer than the one that exists.
// NOTE: we only do this if our chosen architecture hasn't been downloaded
// OOF semaphores..
bool NeedDownload = true;
Log.Information("Reached Package download Stage");
if (File.Exists(Path.Combine(Archdir, "Packages.gz")))
{
Log.Information("Checking for release.json");
// we need to check the release since we have an existing package.gz
string JSONstring = File.ReadAllText(Path.Combine(DataDir, "release.json"));
ReleaseData? oldData = JsonSerializer.Deserialize<ReleaseData>(JSONstring);
if (oldData is not null)
{
// if equal then our data is the newest available
if (DateTime.Compare(oldData.Date, data.Date) == 0)
{
Log.Information("Our packages.gz is up to date");
NeedDownload = false;
}
}
}
if (NeedDownload)
{
PackageFile gzFile =
data.PackageFiles
.Where(
file =>
file.filename.Contains(".gz") &&
file.filename.Contains(Arch)) // ensure we only fetch for the chosen architecture
.First();
if (gzFile is null)
{
throw new NullReferenceException("OOPS gzfile null");
}
string URL = "http://packages.microsoft.com/repos/edge/dists/stable/" + gzFile.filename;
string filename = await AnsiConsole.Progress()
.Columns(new ProgressColumn[]
{
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn(),
})
.StartAsync<string>(async ctx =>
{
var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false });
return await downloadFile(URL, Archdir, task);
});
string JSONdata = JsonSerializer.Serialize<ReleaseData>(data);
File.WriteAllText(Path.Combine(DataDir, "release.json"), JSONdata);
if (!ValidateChecksum(filename, gzFile.Checksum))
{
File.Delete(filename);
Environment.Exit(1);
}
}
string PackagesFile = Path.Combine(Archdir, "Packages.gz");
string PackagesString = GetPackagesString(PackagesFile);
AnsiConsole.MarkupLineInterpolated($"Read {PackagesFile} see log for contents");
Log.Debug("{packagesString}", PackagesString);
List<Package> packages = new();
StringBuilder sb = new();
foreach (var line in PackagesString.Split('\n'))
{
if (line == "")
{
string PackageString = sb.ToString();
sb.Clear(); // ensure we clear the StringBuilder when we are done with it.
try
{ // Just in case we somehow get a broken Package
Package p = new Package(PackageString);
packages.Add(p);
}
catch (Exception e)
{
AnsiConsole.MarkupLine("[red]Error:[/] Could not create package from string");
AnsiConsole.MarkupLine("[yellow i]see log for more details[/]");
Log.Error("Could not create Package from passed string:\n{string}", PackageString);
Log.Error(e, "Exception thrown");
}
}
sb.AppendLine(line);
}
/* INFO: This LINQ query allows sorting.
* Pay special attention to the usage of the version comparer
*/
packages = packages
.OrderBy(package => package.PackageName)
.ThenByDescending(package => package.Version).ToList();
// Print any packages we have
var Variant = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Choose variant")
.PageSize(4)
.AddChoices(new[] {
"stable",
"beta",
"dev",
"all"
})
);
List<Package> VariantPackages = new();
if (Variant == "" || Variant == "all")
{
VariantPackages = packages;
}
else
{
VariantPackages = packages
.Where(package =>
package.PackageName.Contains(Variant))
.ToList();
}
RenderPackagesTable(VariantPackages);
var Package = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Choose Package to Install")
.PageSize(5)
.AddChoices(VariantPackages.ConvertAll(pack => pack.PackageName).Distinct())
);
var ChosenPackages = packages
.Where(package =>
package.PackageName == Package)
.ToList();
Package LatestVersion = ChosenPackages.OrderByDescending(pack => pack.Version).First();
RenderPackageAsTable(LatestVersion);
return 0;
}
static void RenderPackageAsTable(Package package)
{
Table Package = new Table()
.Centered()
.Border(TableBorder.Rounded)
.HideHeaders()
.Title("Package To Install");
AnsiConsole.Live(Package)
.AutoClear(false)
.Start(context =>
{
Package.AddColumns("", "");
context.Refresh();
Thread.Sleep(250);
Package.AddRow("[bold yellow]Package Name : [/]", $"{package.PackageName}");
context.Refresh();
Thread.Sleep(100);
Package.AddRow("[bold yellow]Version : [/]", $"{package.Version.ToString()}");
context.Refresh();
Thread.Sleep(100);
Package.AddRow("[bold yellow]Size : [/]", $"{package.Size.ToString()}B");
context.Refresh();
Thread.Sleep(100);
Package.AddRow("[bold yellow]SHA256 : [/]", $"{package.SHA256}");
context.Refresh();
Thread.Sleep(100);
Package.AddRow("[bold yellow]Filename : [/]", $"{package.Filename}");
context.Refresh();
});
}
static void RenderPackagesTable(List<Package> Packages)
{
Table PackageTable = new Table()
.Centered()
.Expand()
.Border(TableBorder.Rounded)
.Title("Packages");
AnsiConsole.Live(PackageTable)
.AutoClear(false)
.Start(context =>
{
PackageTable
.AddColumn("[bold]PackageName[/]")
.AddColumn("[bold]Version[/]")
.AddColumn("[bold]Size (bytes)[/]")
.AddColumn("[bold]SHA256[/]")
.AddColumn("[bold]Filename[/]");
context.Refresh();
Thread.Sleep(500);
foreach (var pack in Packages)
{
PackageTable.AddRow(FormatPackageName(pack.PackageName), $"[i blue]{pack.Version.ToString()}[/]", $"[grey]{pack.Size.ToString()}[/][b]B[/]", pack.SHA256, pack.Filename);
context.Refresh();
Thread.Sleep(100);
}
});
}
static string FormatPackageName(string name)
{
if (name.Contains("dev"))
{
return $"[red]{name}[/]";
}
if (name.Contains("beta"))
{
return $"[yellow]{name}[/]";
}
if (name.Contains("stable"))
{
return $"[green]{name}[/]";
}
return name;
}
static async Task<string> GetRelease()
{
string releaseURL = "http://packages.microsoft.com/repos/edge/dists/stable/Release";
using HttpResponseMessage response = await _client.GetAsync(releaseURL);
await Task.Delay(5000);
response.EnsureSuccessStatusCode(); // WARNING: this can throw an exception, Please wrap in try/catch or rewrite function
var body = await response.Content.ReadAsStringAsync();
Log.Debug("{body}", body);
return body;
}
static ReleaseData getReleaseData(string release)
{
return new ReleaseData(release);
}
static async Task<string> downloadFile(string URL, string DestinationDirectory)
{
Uri file = new Uri(URL);
string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last());
using HttpResponseMessage response = await _client.GetAsync(URL);
response.EnsureSuccessStatusCode();
await Task.Delay(2000);
var fileData = response.Content.ReadAsStreamAsync();
using var fileStream = new FileStream(fileLocation, FileMode.OpenOrCreate);
await response.Content.CopyToAsync(fileStream);
return fileLocation;
}
static async Task<string> downloadFile(string URL, string DestinationDirectory, ProgressTask task)
{
Uri file = new Uri(URL);
string fileLocation = Path.Combine(DestinationDirectory, file.Segments.Last());
using (HttpResponseMessage response = await _client.GetAsync(URL, HttpCompletionOption.ResponseHeadersRead))
{
response.EnsureSuccessStatusCode();
task.MaxValue = response.Content.Headers.ContentLength ?? 0;
task.StartTask();
await Task.Delay(2000);
AnsiConsole.MarkupLineInterpolated($"Starting download of [u]{URL}[/] ({task.MaxValue} Bytes)");
using (var contentStream = await response.Content.ReadAsStreamAsync())
using (var fileStream = File.Open(fileLocation, FileMode.OpenOrCreate))
{
var buffer = new byte[512];
while (true)
{
var read = await contentStream.ReadAsync(buffer, 0, buffer.Length);
await Task.Delay(500);
if (read == 0)
{
AnsiConsole.MarkupLineInterpolated($"Download of [u]{URL}[/] [green b]Complete![/]");
break;
}
task.Increment(read);
await fileStream.WriteAsync(buffer, 0, read);
}
}
}
return fileLocation;
}
static bool ValidateChecksum(string path, string checksum)
{
using (SHA256 mySHA256 = SHA256.Create())
{
using (FileStream fs = File.Open(path, FileMode.Open))
{
fs.Position = 0;
byte[] hashValue = mySHA256.ComputeHash(fs);
if (hashValue is null)
{
throw new NullReferenceException("Hash be null??");
}
Log.Debug("Computed hash = {hash}", hashValue);
byte[] storedHash = Convert.FromHexString(checksum);
Log.Debug("Stored Hash = {hash}", storedHash);
Log.Debug("Compare result = {result}", hashValue.SequenceEqual(storedHash));
return (storedHash.SequenceEqual(hashValue));
}
}
}
static void RenderReleaseTable(ReleaseData data)
{
// Lets output this ReleaseData now
Table ReleaseTable = new Table()
.Centered()
.Border(TableBorder.Rounded)
.Expand()
.Title("Release Data");
AnsiConsole.Live(ReleaseTable)
.AutoClear(false)
.Start(context =>
{
ReleaseTable.AddColumn("[bold]Field[/]");
ReleaseTable.AddColumn("[bold]Value[/]");
context.Refresh();
Thread.Sleep(500);
ReleaseTable.AddRow("[bold yellow]Date:[/]", $"[blue]{data.Date:R}[/]");
context.Refresh();
Thread.Sleep(500);
Table archs = new Table()
.Centered()
.Border(TableBorder.None)
.HideFooters()
.HideHeaders()
.AddColumn(new TableColumn(Text.Empty));
ReleaseTable.AddRow(new Markup("[bold yellow]Architectures:[/]"), archs);
foreach (var arch in data.Architectures)
{
archs.AddRow(arch);
context.Refresh();
Thread.Sleep(250);
}
Table PackageFilesTable = new Table()
.Centered()
.Border(TableBorder.Square)
.BorderColor(Color.Red)
.AddColumn("[bold]Filename[/]")
.AddColumn("[bold]Checksum[/]")
.AddColumn("[bold]Size[/]");
ReleaseTable.AddRow(
new Markup("[bold yellow]Package Files:[/]"),
PackageFilesTable);
context.Refresh();
foreach (var PackageFile in data.PackageFiles)
{
PackageFilesTable.AddRow(PackageFile.filename, PackageFile.Checksum, PackageFile.size.ToString());
context.Refresh();
Thread.Sleep(250);
}
});
}
private static string GetPackagesString(string filepath)
{
using (var CompressedStream = File.Open(filepath, FileMode.Open))
using (var decompressor = new GZipStream(CompressedStream, CompressionMode.Decompress))
using (var streamReader = new StreamReader(decompressor))
{
return streamReader.ReadToEnd();
}
}
/*TODO:
- Parse package to find latest version
- Check version
- Offer update
- Make SENSIBLE backup
- Do install
= if install fails revert backup
*/
}