//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 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() .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(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(async ctx => { var task = ctx.AddTask(URL, new ProgressTaskSettings { AutoStart = false }); return await downloadFile(URL, Archdir, task); }); string JSONdata = JsonSerializer.Serialize(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 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() .Title("Choose variant") .PageSize(4) .AddChoices(new[] { "stable", "beta", "dev", "all" }) ); List 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() .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 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 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 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 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 */ }