From f6dba35656c4171e5c830553f6ddf4515120d76f Mon Sep 17 00:00:00 2001 From: Liam Gordon Date: Thu, 7 Dec 2023 12:04:14 -0500 Subject: [PATCH] updater --- src/D2L.Bmx/Program.cs | 8 +++ src/D2L.Bmx/UpdateChecker.cs | 129 +++++++++++++++++++++++------------ src/D2L.Bmx/UpdateHandler.cs | 93 +++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 src/D2L.Bmx/UpdateHandler.cs diff --git a/src/D2L.Bmx/Program.cs b/src/D2L.Bmx/Program.cs index 8c791d16..1d3b07e5 100644 --- a/src/D2L.Bmx/Program.cs +++ b/src/D2L.Bmx/Program.cs @@ -198,6 +198,12 @@ ); } ); +var updateCommand = new Command( "update", "Updates BMX to the latest version" ); +updateCommand.SetHandler( ( InvocationContext context ) => { + var handler = new UpdateHandler(); + return handler.HandleAsync(); +} ); + // root command var rootCommand = new RootCommand( "BMX grants you API access to your AWS accounts!" ) { // put more frequently used commands first, as the order here affects help text @@ -205,6 +211,7 @@ writeCommand, loginCommand, configureCommand, + updateCommand, }; // start bmx @@ -226,6 +233,7 @@ } } + UpdateHandler.Cleanup(); await UpdateChecker.CheckForUpdatesAsync( configProvider.GetConfiguration() ); await next( context ); diff --git a/src/D2L.Bmx/UpdateChecker.cs b/src/D2L.Bmx/UpdateChecker.cs index 54ce9ef6..82898e7c 100644 --- a/src/D2L.Bmx/UpdateChecker.cs +++ b/src/D2L.Bmx/UpdateChecker.cs @@ -5,41 +5,50 @@ namespace D2L.Bmx; -internal static class UpdateChecker { - public static async Task CheckForUpdatesAsync( BmxConfig config ) { - try { +internal static class UpdateChecker +{ + public static async Task CheckForUpdatesAsync(BmxConfig config) + { + try + { var cachedVersion = GetUpdateCheckCache(); var localVersion = Assembly.GetExecutingAssembly().GetName().Version; - var latestVersion = new Version( cachedVersion?.VersionName ?? "0.0.0" ); - if( ShouldFetchLatestVersion( cachedVersion ) ) { - latestVersion = new Version( await GetLatestReleaseVersionAsync() ); + var latestVersion = new Version(cachedVersion?.VersionName ?? "0.0.0"); + if (ShouldFetchLatestVersion(cachedVersion)) + { + latestVersion = new Version(await GetLatestReleaseVersionAsync()); } - string updateLocation = string.Equals( config.Org, "d2l", StringComparison.OrdinalIgnoreCase ) + string updateLocation = string.Equals(config.Org, "d2l", StringComparison.OrdinalIgnoreCase) ? "https://bmx.d2l.dev" : "https://github.com/Brightspace/bmx/releases/latest"; - if( latestVersion > localVersion ) { - DisplayUpdateMessage( $"A new BMX release is available: v{latestVersion} (current: v{localVersion})\n" + - $"Upgrade now at {updateLocation}" ); + if (latestVersion > localVersion) + { + DisplayUpdateMessage($"A new BMX release is available: v{latestVersion} (current: v{localVersion})\n" + + $"Upgrade now at {updateLocation}"); } - } catch( Exception ) { + } + catch (Exception) + { // Give up and don't bother telling the user we couldn't check for updates } } - private static void DisplayUpdateMessage( string message ) { + private static void DisplayUpdateMessage(string message) + { var originalBackgroundColor = Console.BackgroundColor; var originalForegroundColor = Console.ForegroundColor; Console.BackgroundColor = ConsoleColor.Gray; Console.ForegroundColor = ConsoleColor.Black; - string[] lines = message.Split( "\n" ); + string[] lines = message.Split("\n"); int consoleWidth = Console.WindowWidth; - foreach( string line in lines ) { - Console.Error.Write( line.PadRight( consoleWidth ) ); + foreach (string line in lines) + { + Console.Error.Write(line.PadRight(consoleWidth)); Console.Error.WriteLine(); } @@ -49,12 +58,13 @@ private static void DisplayUpdateMessage( string message ) { Console.Error.WriteLine(); } - private static async Task GetLatestReleaseVersionAsync() { + private static async Task GetLatestReleaseVersionAsync() + { using var httpClient = new HttpClient(); - httpClient.BaseAddress = new Uri( "https://api.github.com" ); - httpClient.Timeout = TimeSpan.FromSeconds( 2 ); - httpClient.DefaultRequestHeaders.Add( "User-Agent", "BMX" ); - var response = await httpClient.GetAsync( "repos/Brightspace/bmx/releases/latest" ); + httpClient.BaseAddress = new Uri("https://api.github.com"); + httpClient.Timeout = TimeSpan.FromSeconds(2); + httpClient.DefaultRequestHeaders.Add("User-Agent", "BMX"); + var response = await httpClient.GetAsync("repos/Brightspace/bmx/releases/latest"); response.EnsureSuccessStatusCode(); await using var responseStream = await response.Content.ReadAsStreamAsync(); @@ -62,62 +72,91 @@ private static async Task GetLatestReleaseVersionAsync() { responseStream, SourceGenerationContext.Default.GithubRelease ); - string version = releaseData?.TagName?.TrimStart( 'v' ) ?? string.Empty; - SaveLatestVersion( version ); + string version = releaseData?.TagName?.TrimStart('v') ?? string.Empty; + SaveLatestVersion(version); return version; } - private static void SaveLatestVersion( string version ) { - if( string.IsNullOrWhiteSpace( version ) ) { + private static void SaveLatestVersion(string version) + { + if (string.IsNullOrWhiteSpace(version)) + { return; } - var cache = new UpdateCheckCache { + var cache = new UpdateCheckCache + { VersionName = version, TimeLastChecked = DateTimeOffset.UtcNow }; - string content = JsonSerializer.Serialize( cache, SourceGenerationContext.Default.UpdateCheckCache ); - var op = new FileStreamOptions { + string content = JsonSerializer.Serialize(cache, SourceGenerationContext.Default.UpdateCheckCache); + var op = new FileStreamOptions + { Mode = FileMode.OpenOrCreate, Access = FileAccess.ReadWrite, }; - if( !RuntimeInformation.IsOSPlatform( OSPlatform.Windows ) ) { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { op.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite; } - using var fs = new FileStream( BmxPaths.UPDATE_CHECK_FILE_NAME, op ); - using var writer = new StreamWriter( fs ); - writer.Write( content ); + using var fs = new FileStream(BmxPaths.UPDATE_CHECK_FILE_NAME, op); + using var writer = new StreamWriter(fs); + writer.Write(content); } - private static UpdateCheckCache? GetUpdateCheckCache() { - if( !File.Exists( BmxPaths.UPDATE_CHECK_FILE_NAME ) ) { + private static UpdateCheckCache? GetUpdateCheckCache() + { + if (!File.Exists(BmxPaths.UPDATE_CHECK_FILE_NAME)) + { return null; } - string content = File.ReadAllText( BmxPaths.UPDATE_CHECK_FILE_NAME ); - try { - return JsonSerializer.Deserialize( content, SourceGenerationContext.Default.UpdateCheckCache ); - } catch( JsonException ) { + string content = File.ReadAllText(BmxPaths.UPDATE_CHECK_FILE_NAME); + try + { + return JsonSerializer.Deserialize(content, SourceGenerationContext.Default.UpdateCheckCache); + } + catch (JsonException) + { return null; } } - private static bool ShouldFetchLatestVersion( UpdateCheckCache? cache ) { - if( cache is null || string.IsNullOrWhiteSpace( cache.VersionName ) - || ( DateTimeOffset.UtcNow - cache.TimeLastChecked ) > TimeSpan.FromDays( 1 ) - || ( cache.TimeLastChecked > DateTimeOffset.UtcNow ) - ) { + private static bool ShouldFetchLatestVersion(UpdateCheckCache? cache) + { + if (cache is null || string.IsNullOrWhiteSpace(cache.VersionName) + || (DateTimeOffset.UtcNow - cache.TimeLastChecked) > TimeSpan.FromDays(1) + || (cache.TimeLastChecked > DateTimeOffset.UtcNow) + ) + { return true; } return false; } } -internal record GithubRelease { - [JsonPropertyName( "tag_name" )] +internal record GithubRelease +{ + [JsonPropertyName("tag_name")] public string? TagName { get; set; } + + [JsonPropertyName("assets")] + public List? Assets { get; set; } +} + +internal record Assets +{ + [JsonPropertyName("url")] + public string? Url { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("browser_download_url")] + public string? BrowserDownloadUrl { get; set; } } -internal record UpdateCheckCache { +internal record UpdateCheckCache +{ public string? VersionName { get; set; } public DateTimeOffset? TimeLastChecked { get; set; } } diff --git a/src/D2L.Bmx/UpdateHandler.cs b/src/D2L.Bmx/UpdateHandler.cs new file mode 100644 index 00000000..cbd1ab9a --- /dev/null +++ b/src/D2L.Bmx/UpdateHandler.cs @@ -0,0 +1,93 @@ +using System.IO.Compression; +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace D2L.Bmx; + +internal class UpdateHandler { + + public async Task HandleAsync() { + using var httpClient = new HttpClient(); + httpClient.BaseAddress = new Uri( "https://api.github.com" ); + httpClient.Timeout = TimeSpan.FromSeconds( 2 ); + httpClient.DefaultRequestHeaders.Add( "User-Agent", "BMX" ); + var response = await httpClient.GetAsync( "repos/Brightspace/bmx/releases/latest" ); + response.EnsureSuccessStatusCode(); + + await using var responseStream = await response.Content.ReadAsStreamAsync(); + var releaseData = await JsonSerializer.DeserializeAsync( + responseStream, + SourceGenerationContext.Default.GithubRelease + ); + string archiveName = GetOSFileName(); + string downloadUrl = releaseData?.Assets?.FirstOrDefault( a => a.Name == archiveName )?.BrowserDownloadUrl + ?? string.Empty; + string? currentProcessPath = Environment.ProcessPath; + string backupPath = currentProcessPath + ".old.bak"; // will figure out how to do version later + if( string.IsNullOrWhiteSpace( downloadUrl ) ) { + return; + } + + if( !string.IsNullOrEmpty( currentProcessPath ) ) { + File.Move( currentProcessPath, backupPath ); + } else { + currentProcessPath = "C:/bin"; + } + + var archiveRes = await httpClient.GetAsync( downloadUrl, HttpCompletionOption.ResponseHeadersRead ); + string? downloadPath = Path.GetTempFileName(); + using( var fs = new FileStream( downloadPath, FileMode.Create, FileAccess.Write, FileShare.None ) ) { + await archiveRes.Content.CopyToAsync( fs ); + await fs.FlushAsync(); + fs.Dispose(); + } + Console.WriteLine( "Downloaded!" ); + + string extension = Path.GetExtension( downloadUrl ).ToLowerInvariant(); + string? extractPath = Path.GetDirectoryName( currentProcessPath ); + + if( extension.Equals( ".zip" ) ) { + using( ZipArchive archive = ZipFile.OpenRead( downloadPath ) ) { + foreach( ZipArchiveEntry entry in archive.Entries ) { + string? destinationPath = Path.GetFullPath( Path.Combine( extractPath!, entry.FullName ) ); + if( destinationPath.StartsWith( extractPath!, StringComparison.Ordinal ) ) { + entry.ExtractToFile( destinationPath, overwrite: true ); + } + } + } + } + + string newExecutablePath = Path.Combine( extractPath!, Path.GetFileName( currentProcessPath ) ); + File.Move( newExecutablePath, currentProcessPath, overwrite: true ); + } + + private static string GetOSFileName() { + + if( RuntimeInformation.IsOSPlatform( OSPlatform.Windows ) ) { + return "bmx-win-x64.zip"; + } else if( RuntimeInformation.IsOSPlatform( OSPlatform.OSX ) ) { + return "bmx-osx-x64.tar.gz"; + } else if( RuntimeInformation.IsOSPlatform( OSPlatform.Linux ) ) { + return "bmx-linux-x64.tar.gz"; + } else { + throw new Exception( "Unknown OS" ); + } + } + + public static void Cleanup() { + string processDirectory = Path.GetDirectoryName( Environment.ProcessPath ) ?? string.Empty; + if( string.IsNullOrEmpty( processDirectory ) ) { + return; + } + + Console.WriteLine( $"Cleaning up old binaries in {processDirectory}" ); + foreach( string file in Directory.GetFiles( processDirectory, "*.old.bak" ) ) { + try { + Console.WriteLine( file ); + File.Delete( file ); + } catch( Exception ex ) { + Console.WriteLine( $"Failed to delete old binary {file}: {ex.Message}" ); + } + } + } +}