Skip to content

Commit

Permalink
updater
Browse files Browse the repository at this point in the history
  • Loading branch information
gord5500 committed Dec 7, 2023
1 parent 1ce974d commit f6dba35
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 45 deletions.
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
129 changes: 84 additions & 45 deletions src/D2L.Bmx/UpdateChecker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand All @@ -49,75 +58,105 @@ private static void DisplayUpdateMessage( string message ) {
Console.Error.WriteLine();
}

private static async Task<string> GetLatestReleaseVersionAsync() {
private static async Task<string> 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();
var releaseData = await JsonSerializer.DeserializeAsync(
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>? 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; }
}
93 changes: 93 additions & 0 deletions src/D2L.Bmx/UpdateHandler.cs
Original file line number Diff line number Diff line change
@@ -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}" );
}
}
}
}

0 comments on commit f6dba35

Please sign in to comment.