476 lines
17 KiB
C#
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
|
|
*/
|
|
}
|