diff --git a/Nebula.Launcher/Nebula.Launcher.csproj b/Nebula.Launcher/Nebula.Launcher.csproj index da3683d..36d40e9 100644 --- a/Nebula.Launcher/Nebula.Launcher.csproj +++ b/Nebula.Launcher/Nebula.Launcher.csproj @@ -62,5 +62,7 @@ + diff --git a/Nebula.Packager/CommandLineParser.cs b/Nebula.Packager/CommandLineParser.cs new file mode 100644 index 0000000..15320b9 --- /dev/null +++ b/Nebula.Packager/CommandLineParser.cs @@ -0,0 +1,37 @@ +namespace Nebula.Packager; + +public class CommandLineParser +{ + public string Configuration { get; set; } = "Release"; + public string RootPath { get; set; } = string.Empty; + + public static CommandLineParser Parse(IReadOnlyList args) + { + using var enumerator = args.GetEnumerator(); + + var parsed = new CommandLineParser(); + + while (enumerator.MoveNext()) + { + var arg = enumerator.Current; + + if (arg == "--configuration") + { + if (!enumerator.MoveNext()) + throw new InvalidOperationException("Missing args for --configuration"); + + parsed.Configuration = enumerator.Current; + } + + if (arg == "--root-path") + { + if(!enumerator.MoveNext()) + throw new InvalidOperationException("Missing args for --root-path"); + + parsed.RootPath = enumerator.Current; + } + } + + return parsed; + } +} \ No newline at end of file diff --git a/Nebula.Packager/Nebula.Packager.csproj b/Nebula.Packager/Nebula.Packager.csproj index 54ee940..b21b111 100644 --- a/Nebula.Packager/Nebula.Packager.csproj +++ b/Nebula.Packager/Nebula.Packager.csproj @@ -4,4 +4,11 @@ enable enable + + + + + + + diff --git a/Nebula.Packager/Program.cs b/Nebula.Packager/Program.cs index 772f32d..9f532fa 100644 --- a/Nebula.Packager/Program.cs +++ b/Nebula.Packager/Program.cs @@ -2,24 +2,36 @@ using System.Security.Cryptography; using System.Text.Json; using System.Text.Json.Serialization; +using Nebula.Shared; +using Nebula.SharedModels; namespace Nebula.Packager; public static class Program { public static void Main(string[] args) { - Pack("","Release"); + var parsedArgs = CommandLineParser.Parse(args); + + Pack(parsedArgs.RootPath, parsedArgs.Configuration); } - private static void Pack(string rootPath,string configuration) + private static string ShowEmptyOrValue(string? value) { + if(string.IsNullOrWhiteSpace(value)) return ""; + return value; + } + + private static void Pack(string rootPath, string configuration) + { + Console.WriteLine($"Packaging with arguments: RootPath {ShowEmptyOrValue(rootPath)} and Configuration {configuration}"); + var processInfo = new ProcessStartInfo { FileName = "dotnet", ArgumentList = { "publish", - Path.Combine(rootPath,"Nebula.Launcher", "Nebula.Launcher.csproj"), + Path.Combine(rootPath, "Nebula.Launcher", "Nebula.Launcher.csproj"), "-c", configuration, } }; @@ -57,18 +69,14 @@ public static class Program entries.Add(new LauncherManifestEntry(hashStr, fileNameCut)); Console.WriteLine($"Added {hashStr} file name {fileNameCut}"); } + + var manifestRuntimeInfo = new LauncherRuntimeInfo( + CurrentConVar.DotnetVersion.DefaultValue!, + CurrentConVar.DotnetUrl.DefaultValue! + ); using var manifest = File.CreateText(Path.Combine(destinationDirectory, "manifest.json")); manifest.AutoFlush = true; - manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries))); + manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries, manifestRuntimeInfo))); } } - -public record struct LauncherManifest( - [property: JsonPropertyName("entries")] HashSet Entries -); - -public record struct LauncherManifestEntry( - [property: JsonPropertyName("hash")] string Hash, - [property: JsonPropertyName("path")] string Path -); \ No newline at end of file diff --git a/Nebula.Shared/Nebula.Shared.csproj b/Nebula.Shared/Nebula.Shared.csproj index d0a205d..da508e1 100644 --- a/Nebula.Shared/Nebula.Shared.csproj +++ b/Nebula.Shared/Nebula.Shared.csproj @@ -16,6 +16,7 @@ + diff --git a/Nebula.Shared/Services/ContentService.Download.cs b/Nebula.Shared/Services/ContentService.Download.cs index 991cb44..075af34 100644 --- a/Nebula.Shared/Services/ContentService.Download.cs +++ b/Nebula.Shared/Services/ContentService.Download.cs @@ -81,7 +81,7 @@ public partial class ContentService var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater()); - var response = await _http.GetAsync(info.DownloadUri, cancellationToken); + var response = await _http.GetAsync(info.DownloadUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken); response.EnsureSuccessStatusCode(); loadingHandler.SetLoadingMessage("Downloading zip content"); diff --git a/Nebula.SharedModels/LauncherManifest.cs b/Nebula.SharedModels/LauncherManifest.cs new file mode 100644 index 0000000..393534b --- /dev/null +++ b/Nebula.SharedModels/LauncherManifest.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace Nebula.SharedModels; + +public record struct LauncherManifest( + [property: JsonPropertyName("entries")] HashSet Entries, + [property: JsonPropertyName("runtime_info")] LauncherRuntimeInfo RuntimeInfo +); \ No newline at end of file diff --git a/Nebula.SharedModels/LauncherManifestEntry.cs b/Nebula.SharedModels/LauncherManifestEntry.cs new file mode 100644 index 0000000..43a5d16 --- /dev/null +++ b/Nebula.SharedModels/LauncherManifestEntry.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace Nebula.SharedModels; + +public record struct LauncherManifestEntry( + [property: JsonPropertyName("hash")] string Hash, + [property: JsonPropertyName("path")] string Path +); \ No newline at end of file diff --git a/Nebula.SharedModels/LauncherRuntimeInfo.cs b/Nebula.SharedModels/LauncherRuntimeInfo.cs new file mode 100644 index 0000000..8397a40 --- /dev/null +++ b/Nebula.SharedModels/LauncherRuntimeInfo.cs @@ -0,0 +1,7 @@ +using System.Text.Json.Serialization; + +namespace Nebula.SharedModels; + +public record struct LauncherRuntimeInfo( + [property: JsonPropertyName("version")] string RuntimeVersion, + [property: JsonPropertyName("runtimes")] Dictionary DotnetRuntimes); \ No newline at end of file diff --git a/Nebula.SharedModels/Nebula.SharedModels.csproj b/Nebula.SharedModels/Nebula.SharedModels.csproj new file mode 100644 index 0000000..237d661 --- /dev/null +++ b/Nebula.SharedModels/Nebula.SharedModels.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/Nebula.UpdateResolver/DotnetStandalone.cs b/Nebula.UpdateResolver/DotnetStandalone.cs index 8d73a62..e803913 100644 --- a/Nebula.UpdateResolver/DotnetStandalone.cs +++ b/Nebula.UpdateResolver/DotnetStandalone.cs @@ -7,7 +7,8 @@ using System.Net.Http; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -using Nebula.UpdateResolver.Configuration; +using Nebula.SharedModels; +using Nebula.UpdateResolver.Rest; namespace Nebula.UpdateResolver; @@ -15,19 +16,21 @@ public static class DotnetStandalone { private static readonly HttpClient HttpClient = new(); - private static readonly string FullPath = - Path.Join(MainWindow.RootPath, $"dotnet.{ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetVersion)}", - DotnetUrlHelper.GetRuntimeIdentifier()); - - private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension()); - - public static async Task Run(string dllPath) + private static string GetExecutePath(LauncherRuntimeInfo runtimeInfo) { - await EnsureDotnet(); + return Path.Join(MainWindow.RootPath, + $"dotnet.{runtimeInfo.RuntimeVersion}", + DotnetUrlHelper.GetRuntimeIdentifier(), + $"dotnet{DotnetUrlHelper.GetExtension()}"); + } + + public static async Task Run(LauncherRuntimeInfo runtimeInfo, string dllPath) + { + await EnsureDotnet(runtimeInfo); return Process.Start(new ProcessStartInfo { - FileName = ExecutePath, + FileName = GetExecutePath(runtimeInfo), Arguments = dllPath, CreateNoWindow = true, UseShellExecute = false, @@ -37,35 +40,38 @@ public static class DotnetStandalone }); } - private static async Task EnsureDotnet() + private static async Task EnsureDotnet(LauncherRuntimeInfo runtimeInfo) { - if (!Directory.Exists(FullPath)) - await Download(); + if (!Directory.Exists(GetExecutePath(runtimeInfo))) + await Download(runtimeInfo); } - private static async Task Download() + private static async Task Download(LauncherRuntimeInfo runtimeInfo) { LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}..."); - var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl( - ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetUrl)! - ); + var fullPath = GetExecutePath(runtimeInfo); - using var response = await HttpClient.GetAsync(url); + var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(runtimeInfo.DotnetRuntimes); + + using var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(); + var stream = await response.Content.ReadAsStreamAsync(); + await using var tempStream = new MemoryStream(); + stream.CopyTo(tempStream,"dotnet", response.Content.Headers.ContentLength ?? 0); + await stream.DisposeAsync(); - Directory.CreateDirectory(FullPath); + Directory.CreateDirectory(fullPath); if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { - using var zipArchive = new ZipArchive(stream); - zipArchive.ExtractToDirectory(FullPath, true); + await using var zipArchive = new ZipArchive(tempStream); + await zipArchive.ExtractToDirectoryAsync(fullPath, true); } else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) { - TarUtils.ExtractTarGz(stream, FullPath); + TarUtils.ExtractTarGz(tempStream, fullPath); } else { @@ -80,10 +86,8 @@ public static class DotnetUrlHelper { public static string GetExtension() { - if (OperatingSystem.IsWindows()) return ".exe"; - return ""; + return OperatingSystem.IsWindows() ? ".exe" : string.Empty; } - public static string GetCurrentPlatformDotnetUrl(Dictionary dotnetUrl) { var rid = GetRuntimeIdentifier(); @@ -96,10 +100,38 @@ public static class DotnetUrlHelper public static string GetRuntimeIdentifier() { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - return Environment.Is64BitProcess ? "win-x64" : "win-x86"; + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "win-x64", + Architecture.X86 => "win-x86", + Architecture.Arm64 => "win-arm64", + _ => throw new PlatformNotSupportedException($"Unsupported Windows architecture: {RuntimeInformation.ProcessArchitecture}") + }; + } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux-x64"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "linux-x64", + Architecture.X86 => "linux-x86", + Architecture.Arm => "linux-arm", + Architecture.Arm64 => "linux-arm64", + _ => throw new PlatformNotSupportedException($"Unsupported Linux architecture: {RuntimeInformation.ProcessArchitecture}") + }; + } - throw new PlatformNotSupportedException("Unsupported operating system"); + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "osx-x64", + Architecture.Arm64 => "osx-arm64", + _ => throw new PlatformNotSupportedException($"Unsupported macOS architecture: {RuntimeInformation.ProcessArchitecture}") + }; + } + + throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}"); } } \ No newline at end of file diff --git a/Nebula.UpdateResolver/LauncherManifest.cs b/Nebula.UpdateResolver/LauncherManifest.cs deleted file mode 100644 index ad9711e..0000000 --- a/Nebula.UpdateResolver/LauncherManifest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace Nebula.UpdateResolver; - -public record struct LauncherManifest( - [property: JsonPropertyName("entries")] HashSet Entries -); - -public record struct LauncherManifestEntry( - [property: JsonPropertyName("hash")] string Hash, - [property: JsonPropertyName("path")] string Path - ); \ No newline at end of file diff --git a/Nebula.UpdateResolver/MainWindow.axaml.cs b/Nebula.UpdateResolver/MainWindow.axaml.cs index 5d24cb1..3620a4f 100644 --- a/Nebula.UpdateResolver/MainWindow.axaml.cs +++ b/Nebula.UpdateResolver/MainWindow.axaml.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Net.Http; -using System.Text; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; +using Nebula.SharedModels; using Nebula.UpdateResolver.Configuration; using Nebula.UpdateResolver.Rest; @@ -19,7 +19,8 @@ public partial class MainWindow : Window private readonly HttpClient _httpClient = new HttpClient(); public readonly FileApi FileApi = new FileApi(Path.Join(RootPath,"app")); - private string LogStr = ""; + private string _logStr = ""; + public MainWindow() { InitializeComponent(); @@ -34,7 +35,7 @@ public partial class MainWindow : Window var messageOut = $"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {PercentLabel.Content}"; Console.WriteLine(messageOut); - LogStr += messageOut + "\n"; + _logStr += messageOut + "\n"; }; LogStandalone.Log("Starting up"); if (!Design.IsDesignMode) @@ -47,7 +48,11 @@ public partial class MainWindow : Window { try { - var info = await EnsureFiles(); + var manifest = await RestStandalone.GetAsync( + new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None); + + var info = EnsureFiles(FilterEntries(manifest.Entries)); + LogStandalone.Log("Downloading files..."); foreach (var file in info.ToDelete) @@ -57,7 +62,7 @@ public partial class MainWindow : Window } var loadedManifest = info.FilesExist; - Save(loadedManifest); + Save(loadedManifest, manifest.RuntimeInfo); var count = info.ToDownload.Count; var resolved = 0; @@ -75,18 +80,18 @@ public partial class MainWindow : Window LogStandalone.Log("Saving " + file.Path, (int)(resolved / (float)count * 100f)); loadedManifest.Add(file); - Save(loadedManifest); + Save(loadedManifest, manifest.RuntimeInfo); } LogStandalone.Log("Download finished. Running launcher..."); - await DotnetStandalone.Run(Path.Join(FileApi.RootPath, "Nebula.Launcher.dll")); + await DotnetStandalone.Run(manifest.RuntimeInfo, Path.Join(FileApi.RootPath, "Nebula.Launcher.dll")); } catch(HttpRequestException e){ LogStandalone.LogError(e); LogStandalone.Log("Network connection error..."); var logPath = Path.Join(RootPath,"updateResloverError.txt"); - await File.WriteAllTextAsync(logPath, LogStr); + await File.WriteAllTextAsync(logPath, _logStr); Process.Start(new ProcessStartInfo(){ FileName = "notepad", Arguments = logPath @@ -96,7 +101,7 @@ public partial class MainWindow : Window { LogStandalone.LogError(e); var logPath = Path.Join(RootPath,"updateResloverError.txt"); - await File.WriteAllTextAsync(logPath, LogStr); + await File.WriteAllTextAsync(logPath, _logStr); Process.Start(new ProcessStartInfo(){ FileName = "notepad", Arguments = logPath @@ -108,11 +113,9 @@ public partial class MainWindow : Window Environment.Exit(0); } - private async Task EnsureFiles() + private ManifestEnsureInfo EnsureFiles(HashSet entries) { LogStandalone.Log("Ensuring launcher manifest..."); - var manifest = await RestStandalone.GetAsync( - new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None); var toDownload = new HashSet(); var toDelete = new HashSet(); @@ -124,13 +127,13 @@ public partial class MainWindow : Window LogStandalone.Log("Delta manifest loaded!"); foreach (var file in currentManifest.Entries) { - if (!manifest.Entries.Contains(file)) + if (!entries.Contains(file)) toDelete.Add(EnsurePath(file)); else filesExist.Add(EnsurePath(file)); } - foreach (var file in manifest.Entries) + foreach (var file in entries) { if(!currentManifest.Entries.Contains(file)) toDownload.Add(EnsurePath(file)); @@ -138,18 +141,37 @@ public partial class MainWindow : Window } else { - toDownload = manifest.Entries; + toDownload = entries; } LogStandalone.Log("Saving launcher manifest..."); return new ManifestEnsureInfo(toDownload, toDelete, filesExist); } - - private void Save(HashSet entries) + private HashSet FilterEntries(IEnumerable entries) { - ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries)); + var filtered = new HashSet(); + var runtimeIdentifier = DotnetUrlHelper.GetRuntimeIdentifier(); + + foreach (var entry in entries) + { + var splited = entry.Path.Split("/"); + + if(splited.Length < 2 || + splited[0] != "runtimes" || + splited[1] == runtimeIdentifier) + { + filtered.Add(entry); + } + } + + return filtered; + } + + private void Save(HashSet entries, LauncherRuntimeInfo info) + { + ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries, info)); } private LauncherManifestEntry EnsurePath(LauncherManifestEntry entry) diff --git a/Nebula.UpdateResolver/ManifestEnsureInfo.cs b/Nebula.UpdateResolver/ManifestEnsureInfo.cs index cae871b..8c40754 100644 --- a/Nebula.UpdateResolver/ManifestEnsureInfo.cs +++ b/Nebula.UpdateResolver/ManifestEnsureInfo.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Nebula.SharedModels; namespace Nebula.UpdateResolver; diff --git a/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj b/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj index 575ab14..7196292 100644 --- a/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj +++ b/Nebula.UpdateResolver/Nebula.UpdateResolver.csproj @@ -26,4 +26,8 @@ All + + + + diff --git a/Nebula.UpdateResolver/Rest/Helper.cs b/Nebula.UpdateResolver/Rest/Helper.cs index ae8f370..f619a43 100644 --- a/Nebula.UpdateResolver/Rest/Helper.cs +++ b/Nebula.UpdateResolver/Rest/Helper.cs @@ -1,5 +1,4 @@ -using System; -using System.Diagnostics; +using System.IO; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; @@ -9,31 +8,25 @@ namespace Nebula.UpdateResolver.Rest; public static class Helper { public static readonly JsonSerializerOptions JsonWebOptions = new(JsonSerializerDefaults.Web); - public static void SafeOpenBrowser(string uri) - { - if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri)) - { - Console.WriteLine("Unable to parse URI in server-provided link: {Link}", uri); - return; - } - - if (parsedUri.Scheme is not ("http" or "https")) - { - Console.WriteLine("Refusing to open server-provided link {Link}, only http/https are allowed", parsedUri); - return; - } - - OpenBrowser(parsedUri.ToString()); - } - public static void OpenBrowser(string url) - { - Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); - } - public static async Task AsJson(this HttpContent content) where T : notnull { var str = await content.ReadAsStringAsync(); return JsonSerializer.Deserialize(str, JsonWebOptions) ?? throw new JsonException("AsJson: did not expect null response"); } + + public static void CopyTo(this Stream input, Stream output, string fileName, long totalLength) + { + const int bufferSize = 81920; + var buffer = new byte[bufferSize]; + + long totalRead = 0; + int bytesRead; + while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0) + { + output.Write(buffer, 0, bytesRead); + totalRead += bytesRead; + LogStandalone.Log($"Saving {fileName}", (int)(((float)totalLength / totalRead) * 100)); + } + } } \ No newline at end of file diff --git a/Nebula.UpdateResolver/UpdateCVars.cs b/Nebula.UpdateResolver/UpdateCVars.cs index e5e76af..f101a85 100644 --- a/Nebula.UpdateResolver/UpdateCVars.cs +++ b/Nebula.UpdateResolver/UpdateCVars.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using Nebula.SharedModels; using Nebula.UpdateResolver.Configuration; namespace Nebula.UpdateResolver; @@ -9,13 +9,4 @@ public static class UpdateConVars ConVarBuilder.Build("update.url","https://durenko.tatar/nebula/manifest/"); public static readonly ConVar CurrentLauncherManifest = ConVarBuilder.Build("update.manifest"); - - public static readonly ConVar> DotnetUrl = ConVarBuilder.Build>("dotnet.url", - new(){ - {"win-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x64.zip"}, - {"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x86.zip"}, - {"linux-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-linux-x64.tar.gz"} - }); - - public static readonly ConVar DotnetVersion = ConVarBuilder.Build("dotnet.version", "10.0.2"); } \ No newline at end of file diff --git a/Nebula.sln.DotSettings.user b/Nebula.sln.DotSettings.user index e478e5e..bc97dff 100644 --- a/Nebula.sln.DotSettings.user +++ b/Nebula.sln.DotSettings.user @@ -1,5 +1,6 @@  ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -29,6 +30,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -40,6 +42,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/Nebula.slnx b/Nebula.slnx index 7a03d11..6b5d969 100644 --- a/Nebula.slnx +++ b/Nebula.slnx @@ -2,6 +2,7 @@ +