//using System.CommandLine; using Serilog; using Spectre.Console; using System.Text.Json; using System.Text; using SharpCompress.Compressors.Xz; using System.Formats.Tar; using System.Diagnostics; namespace EdgeInstaller; public static partial class EdgeInstall { // NOTE: tweaking these values can be used to make downloads look more "fancy" // A.K.A make downloads take longer so the progress bar appears for a little longer private static readonly int downloadDelayMS = 0; private static readonly int downloadBufferBYTES = 10240; // The default architecture to get // likely will go unused until proper cli interface is added private static readonly string _DefaultArch = "amd64"; 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() { _client.BaseAddress = new Uri("https://packages.microsoft.com/repos/edge/"); // Ensure DataDir exists. // NOTE: Directory.CreateDirectory will ALWAYS create the entire path Directory.CreateDirectory(DataDir); BUGFIX_ansiDetection(); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.File(Path.Combine(DataDir, ".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 = new(ReleaseString); RenderReleaseTable(data); // only select arch if needed string TargetArch = data.Architectures.Count > 1 ? AnsiConsole.Prompt( new SelectionPrompt() .Title("Choose Architecture") .PageSize(5) .MoreChoicesText("[grey]Scroll for more...[/]") .AddChoices(data.Architectures) ) : data.Architectures.First(); // 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, TargetArch)).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 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(TargetArch)) // ensure we only fetch for the chosen architecture .First(); if (gzFile is null) { throw new NullReferenceException("OOPS gzfile null"); } string URL = "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(new FileInfo(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(); if (PackageString == "") { continue; // The last "package" will be empty } 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); var versionsDir = Path.Join(Archdir, "Versions"); var versionDir = Path.Join(versionsDir, LatestVersion.PackageName, LatestVersion.Version.ToString()); Directory.CreateDirectory(versionDir); // Let's get and unpack the latest version // for the moment we will always just grab using (MemoryStream debStream = await AnsiConsole.Progress() .Columns(new ProgressColumn[] { new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn(), new RemainingTimeColumn(), new SpinnerColumn(), }) .StartAsync(async ctx => { var task = ctx.AddTask(LatestVersion.Filename, new ProgressTaskSettings { AutoStart = false }); return await downloadStream(LatestVersion.Filename, task); })) { if (!ValidateChecksum(debStream, LatestVersion.SHA256)) { AnsiConsole.MarkupLine("[red b]ERROR:[/] checksum did not validate"); Environment.Exit(1); } using (DebFile debfile = new DebFile(debStream)) { // NOTE: deb entries have trailing / DebEntry? dataEntry = debfile.fileEntries.Find(file => file.name == "data.tar.xz/"); if (dataEntry is null) { AnsiConsole.MarkupLine("[red b]ERROR:[/] debfile does not contain data.tar.xz"); Environment.Exit(1); } // Looks like we have a hit. Lets try and extract it if (!debfile.getFile(dataEntry, out Stream dataStream)) { AnsiConsole.MarkupLine("[red b]ERROR:[/] couldn't get stream for data.tar.xz"); Environment.Exit(1); } using (XZStream dataXZStream = new(dataStream)) { using (TarReader reader = new(dataXZStream)) { while (true) { var tarEntry = reader.GetNextEntry(); if (tarEntry is null) { break; // TarReader returns null when we hit the end of the stream } AnsiConsole.MarkupLineInterpolated($"TarEntry {tarEntry.Name}"); // work out where we need to put the extracted file var filepath = Path.Combine(versionDir, tarEntry.Name); if (tarEntry.EntryType == TarEntryType.Directory) { // createDirectory doesn't fail if already exists Directory.CreateDirectory(filepath); continue; } if (tarEntry.LinkName != "") { // Handle links manually if (File.Exists(filepath)) { File.Delete(filepath); } File.CreateSymbolicLink(filepath, tarEntry.LinkName); continue; // we are done with this file so keep going } tarEntry.ExtractToFile(filepath, overwrite: true); } } } // Now assuming nothing broke before now we can copy the files into the system directories. // WARN: This is dangerous stuff and involves making system calls. // NOTE: be sure when calling to other programs to wait until they exit. var Mover = new ProcessStartInfo(); Mover.FileName = "pkexec"; Mover.Arguments = $"cp -dri {versionDir}/opt {versionDir}/usr /"; Mover.UseShellExecute = true; var pr = Process.Start(Mover); pr?.WaitForExit(); } } return 0; } /*TODO: - Check version - Offer update - Make SENSIBLE backup - Do install = if install fails revert backup */ }