It can install now. Requires pexec to be present This may be due to an upstream issue with spectre.console affecting how stdin/stdout are handled. TODO: - Add backup/restore functionality - Refactor sections for neatness - Improve UX
342 lines
13 KiB
C#
342 lines
13 KiB
C#
//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<int> 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<string>()
|
|
.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<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(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<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(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<Package> 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<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);
|
|
|
|
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<MemoryStream>(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
|
|
*/
|
|
}
|