Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding update command #418

Merged
merged 27 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/D2L.Bmx/GithubRelease.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Text.Json.Serialization;

namespace D2L.Bmx;
internal record GithubRelease {
[JsonPropertyName( "tag_name" )]
public string? TagName { get; set; }

[JsonPropertyName( "assets" )]
public List<Assets>? Assets { get; set; }
}

internal record Assets {
[JsonPropertyName( "url" )]
public string? Url { get; set; }

alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved
[JsonPropertyName( "name" )]
public string? Name { get; set; }

[JsonPropertyName( "browser_download_url" )]
public string? BrowserDownloadUrl { get; set; }
}
8 changes: 8 additions & 0 deletions src/D2L.Bmx/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,20 @@
);
} );

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
printCommand,
writeCommand,
loginCommand,
configureCommand,
updateCommand,
};

// start bmx
Expand All @@ -226,6 +233,7 @@
}
}

UpdateHandler.Cleanup();
await UpdateChecker.CheckForUpdatesAsync( configProvider.GetConfiguration() );

await next( context );
Expand Down
19 changes: 9 additions & 10 deletions src/D2L.Bmx/UpdateChecker.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace D2L.Bmx;

Expand All @@ -12,7 +11,7 @@ public static async Task CheckForUpdatesAsync( BmxConfig config ) {
var localVersion = Assembly.GetExecutingAssembly().GetName().Version;
var latestVersion = new Version( cachedVersion?.VersionName ?? "0.0.0" );
if( ShouldFetchLatestVersion( cachedVersion ) ) {
latestVersion = new Version( await GetLatestReleaseVersionAsync() );
latestVersion = new Version( GetLatestReleaseVersion( await GetLatestReleaseDataAsync() ) );
}

string updateLocation = string.Equals( config.Org, "d2l", StringComparison.OrdinalIgnoreCase )
Expand Down Expand Up @@ -49,7 +48,13 @@ private static void DisplayUpdateMessage( string message ) {
Console.Error.WriteLine();
}

private static async Task<string> GetLatestReleaseVersionAsync() {
internal static string GetLatestReleaseVersion( GithubRelease? releaseData ) {
string version = releaseData?.TagName?.TrimStart( 'v' ) ?? string.Empty;
SaveLatestVersion( version );
return version;
}

internal static async Task<GithubRelease?> GetLatestReleaseDataAsync() {
using var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri( "https://api.github.com" );
httpClient.Timeout = TimeSpan.FromSeconds( 2 );
Expand All @@ -62,9 +67,7 @@ private static async Task<string> GetLatestReleaseVersionAsync() {
responseStream,
SourceGenerationContext.Default.GithubRelease
);
string version = releaseData?.TagName?.TrimStart( 'v' ) ?? string.Empty;
SaveLatestVersion( version );
return version;
return releaseData;
}

private static void SaveLatestVersion( string version ) {
Expand Down Expand Up @@ -112,10 +115,6 @@ private static bool ShouldFetchLatestVersion( UpdateCheckCache? cache ) {
}
}

internal record GithubRelease {
[JsonPropertyName( "tag_name" )]
public string? TagName { get; set; }
}

internal record UpdateCheckCache {
public string? VersionName { get; set; }
Expand Down
136 changes: 136 additions & 0 deletions src/D2L.Bmx/UpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Formats.Tar;
using System.Reflection;

namespace D2L.Bmx;

internal class UpdateHandler {

public async Task HandleAsync() {
using var httpClient = new HttpClient();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably have enough GitHub interaction code to warrant a GitHubClient class. Then, ideally, I'd inject that into dependent classes via constructor parameters, and avoid instantiating new clients directly.
DI is generally nice (for testing and dependency/behaviour tracking), and consistent with our general coding practice.
I'm willing to let this PR through without DI for now, but I would like us to refactor this soon-ish.

var releaseData = await UpdateChecker.GetLatestReleaseDataAsync();
var localVersion = Assembly.GetExecutingAssembly().GetName().Version;
var latestVersion = new Version( UpdateChecker.GetLatestReleaseVersion( releaseData ) );
if ( latestVersion <= localVersion ) {
Console.WriteLine( $"Already own the latest version {latestVersion}" );
cfbao marked this conversation as resolved.
Show resolved Hide resolved
return;
}
string archiveName = GetOSFileName();
string downloadUrl = releaseData?.Assets?.FirstOrDefault( a => a.Name == archiveName )?.BrowserDownloadUrl
?? string.Empty;
cfbao marked this conversation as resolved.
Show resolved Hide resolved
if( string.IsNullOrWhiteSpace( downloadUrl ) ) {
return;
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved
}

string? downloadPath = Path.GetTempFileName();

try {
var archiveRes = await httpClient.GetAsync( downloadUrl, HttpCompletionOption.ResponseHeadersRead );
using( var fs = new FileStream( downloadPath, FileMode.Create, FileAccess.Write, FileShare.None ) ) {
await archiveRes.Content.CopyToAsync( fs );
await fs.FlushAsync();
fs.Dispose();
}
cfbao marked this conversation as resolved.
Show resolved Hide resolved
} catch ( Exception ex ) {
throw new BmxException( "Failed to download the update", ex );
}


string? currentProcessPath = Environment.ProcessPath;
string? currentProcessFileName = Path.GetFileNameWithoutExtension( currentProcessPath );
string? currentProcessExtention = Path.GetExtension( currentProcessPath );
string backupPath = $"{currentProcessFileName}v{localVersion}{currentProcessExtention}";
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved

if( !string.IsNullOrEmpty( currentProcessPath ) ) {
File.Move( currentProcessPath, backupPath );
} else {
currentProcessPath = "C:/bin";
}
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved

try {
string extension = Path.GetExtension( downloadUrl ).ToLowerInvariant();
string? extractPath = Path.GetDirectoryName( currentProcessPath );

if( extension.Equals( ".zip" ) ) {
DecompressZipFile( downloadPath, extractPath! );
} else if( extension.Equals( ".gz" ) ) {
DecompressTarGzipFile( downloadPath, extractPath! );
} else {
Console.WriteLine( extension );
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved
throw new Exception( "Unknown archive type" );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use BmxException with better error message

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already a nested exception (not sure if we can un-nest it without making the block messy) so I don't think the user will see it. I think the "Failed to update with new files" message should capture this tho

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see. Didn't notice the nesting. I'd still like to avoid it, but it's good for now.

}

string newExecutablePath = Path.Combine(
extractPath!,
Path.GetFileName( currentProcessPath )!
);
File.Move( newExecutablePath, currentProcessPath, overwrite: true );
} catch( Exception ex ) {
if (currentProcessPath != "C:/bin") {
File.Move( backupPath, currentProcessPath );
}
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved
throw new BmxException( "Failed to update", ex );
}
}

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" );
cfbao marked this conversation as resolved.
Show resolved Hide resolved
}
}

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}" );
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved
foreach( string file in Directory.GetFiles( processDirectory, "*.old.bak" ) ) {
try {
Console.WriteLine( $"Cleaning up {file}" );
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved
File.Delete( file );
} catch( Exception ex ) {
cfbao marked this conversation as resolved.
Show resolved Hide resolved
Console.WriteLine( $"Failed to delete old binary {file}: {ex.Message}" );
}
}
}

private static void DecompressTarGzipFile( string compressedFilePath, string decompressedFilePath ) {
string? tarPath = Path.Combine(
Path.GetDirectoryName( decompressedFilePath )!,
Path.GetFileNameWithoutExtension( decompressedFilePath )! + ".tar" );

using( FileStream compressedFileStream = File.Open(
compressedFilePath,
FileMode.Open,
FileAccess.Read,
FileShare.Read )
) {
using FileStream outputFileStream = File.Create( tarPath );
using var decompressor = new GZipStream( compressedFileStream, CompressionMode.Decompress );
decompressor.CopyTo( outputFileStream );
}

TarFile.ExtractToDirectory( tarPath, decompressedFilePath, true );
}

private static void DecompressZipFile( string compressedFilePath, string decompressedFilePath ) {
alex-fang0 marked this conversation as resolved.
Show resolved Hide resolved
using( ZipArchive archive = ZipFile.OpenRead( compressedFilePath ) ) {
foreach( ZipArchiveEntry entry in archive.Entries ) {
string? destinationPath = Path.GetFullPath( Path.Combine( decompressedFilePath!, entry.FullName ) );
if( destinationPath.StartsWith( decompressedFilePath!, StringComparison.Ordinal ) ) {
entry.ExtractToFile( destinationPath, overwrite: true );
}
}
}
}
}
Loading