-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement auto-update and UpdateChecker module
- Loading branch information
Showing
16 changed files
with
489 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<TargetFramework>$(TargetFramework)</TargetFramework> | ||
<UseWPF>True</UseWPF> | ||
</PropertyGroup> | ||
<ItemGroup> | ||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" /> | ||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1587.40" /> | ||
<PackageReference Include="Octokit" Version="5.0.0" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<ProjectReference Include="..\Calame\Calame.csproj" /> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<Compile Update="WebBrowserDialog.xaml.cs"> | ||
<SubType>Code</SubType> | ||
</Compile> | ||
</ItemGroup> | ||
<ItemGroup> | ||
<Page Update="WebBrowserDialog.xaml"> | ||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime> | ||
<SubType>Designer</SubType> | ||
</Page> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Specialized; | ||
using System.Linq; | ||
using System.Net.Sockets; | ||
using System.Net; | ||
using System.Text; | ||
using System.Threading.Tasks; | ||
using Octokit; | ||
|
||
namespace Calame.AutoUpdate | ||
{ | ||
// https://octokitnet.readthedocs.io/en/latest/oauth-flow/ | ||
// https://github.com/googlesamples/oauth-apps-for-windows/blob/master/OAuthDesktopApp/OAuthDesktopApp/MainWindow.xaml.cs | ||
// https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/blob/main/HttpSysConsoleClient/ConsoleSystemBrowser/Program.cs | ||
static public class GitHubAutoUpdateApi | ||
{ | ||
static public (Uri loginUri, string requestState, HttpListener httpListener) GetLoginUri(GitHubClient client, string oauthId) | ||
{ | ||
var requestStateBuilder = new StringBuilder(32); | ||
var random = new Random(); | ||
for (int i = 0; i < 32; i++) | ||
requestStateBuilder.Append((char)('a' + random.Next(0, 26))); | ||
|
||
var listener = new TcpListener(IPAddress.Loopback, 0); | ||
listener.Start(); | ||
int port = ((IPEndPoint)listener.LocalEndpoint).Port; | ||
listener.Stop(); | ||
|
||
string requestState = requestStateBuilder.ToString(); | ||
string redirectUri = $"http://{IPAddress.Loopback}:{port}/"; | ||
|
||
var loginRequest = new OauthLoginRequest(oauthId) | ||
{ | ||
Scopes = { "repo" }, | ||
State = requestState, | ||
RedirectUri = new Uri(redirectUri) | ||
}; | ||
|
||
var httpListener = new HttpListener(); | ||
httpListener.Prefixes.Add(redirectUri); | ||
httpListener.Start(); | ||
|
||
return (client.Oauth.GetGitHubLoginUrl(loginRequest), requestState, httpListener); | ||
} | ||
|
||
static public async Task GetAccessTokenAsync(GitHubClient client, string oauthId, string oauthSecret, string requestState, HttpListenerContext httpListenerContext) | ||
{ | ||
NameValueCollection queryStringCollection = httpListenerContext.Request.QueryString; | ||
|
||
string loginError = queryStringCollection.Get("error"); | ||
if (loginError != null) | ||
throw new InvalidOperationException($"OAuth login error: {loginError}"); | ||
|
||
string returnedState = queryStringCollection.Get("state"); | ||
string returnedCode = queryStringCollection.Get("code"); | ||
|
||
if (returnedCode is null || returnedState is null) | ||
throw new InvalidOperationException($"Malformed authorization response. ({queryStringCollection})"); | ||
if (returnedState != requestState) | ||
throw new InvalidOperationException("Invalid authorization response state."); | ||
|
||
var tokenRequest = new OauthTokenRequest(oauthId, oauthSecret, returnedCode); | ||
OauthToken token = await client.Oauth.CreateAccessToken(tokenRequest); | ||
|
||
if (!string.IsNullOrWhiteSpace(token.ErrorDescription)) | ||
throw new InvalidOperationException($"OAuth access token error: {token.ErrorDescription}"); | ||
|
||
client.Credentials = new Credentials(token.AccessToken); | ||
} | ||
|
||
static public async Task<Release> GetLatestRelease(GitHubClient client, string repositoryOwner, string repositoryName) | ||
{ | ||
return await client.Repository.Release.GetLatest(repositoryOwner, repositoryName); | ||
} | ||
|
||
static public async Task<byte[]> DownloadAsset(GitHubClient client, Release release, string assetName) | ||
{ | ||
string assetUrl = release.Assets.FirstOrDefault(x => x.Name == assetName)?.Url; | ||
if (assetUrl is null) | ||
return null; | ||
|
||
IApiResponse<object> response = await client.Connection.Get<object>(new Uri(assetUrl), new Dictionary<string, string>(), "application/octet-stream"); | ||
byte[] bytes = Encoding.ASCII.GetBytes(response.HttpResponse.Body.ToString()); | ||
|
||
return bytes; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
using Octokit; | ||
using System.IO; | ||
using System.Net; | ||
using System.Threading.Tasks; | ||
using System.Windows; | ||
using System; | ||
using System.Linq; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Calame.AutoUpdate | ||
{ | ||
public class GitHubAutoUpdater | ||
{ | ||
private const string MessageCaption = "Auto-Update"; | ||
|
||
static public async Task<string> CheckUpdatesAndAskUserToDownload(IAutoUpdateConfiguration configuration, ILogger logger) | ||
{ | ||
GitHubClient gitHubClient = await GetGitHubClientAsync(configuration, logger); | ||
if (gitHubClient is null) | ||
return null; | ||
|
||
Release latestRelease = await CheckUpdatesAsync(gitHubClient, configuration, logger); | ||
if (latestRelease is null) | ||
return null; | ||
|
||
return await DownloadInstallerAsset(gitHubClient, configuration, latestRelease); | ||
} | ||
|
||
static private async Task<GitHubClient> GetGitHubClientAsync(IAutoUpdateConfiguration configuration, ILogger logger) | ||
{ | ||
if (configuration?.ProductId is null) | ||
return null; | ||
|
||
string message; | ||
|
||
var gitHubClient = new GitHubClient(new ProductHeaderValue(configuration.ProductId)); | ||
|
||
string applicationOAuthId = configuration.ApplicationOAuthId; | ||
string applicationOAuthSecret = configuration.ApplicationOAuthSecret; | ||
|
||
if (applicationOAuthId == null || applicationOAuthSecret == null) | ||
return gitHubClient; | ||
|
||
Uri loginUri; | ||
string requestState; | ||
HttpListener httpListener; | ||
try | ||
{ | ||
(loginUri, requestState, httpListener) = GitHubAutoUpdateApi.GetLoginUri(gitHubClient, applicationOAuthId); | ||
} | ||
catch (Exception ex) | ||
{ | ||
message = "Failed to get the login URL on GitHub."; | ||
|
||
logger.LogError(ex, message); | ||
MessageBox.Show(message, MessageCaption, MessageBoxButton.OK, MessageBoxImage.Error); | ||
return null; | ||
} | ||
|
||
var webBrowserDialog = new WebBrowserDialog(loginUri) | ||
{ | ||
Owner = System.Windows.Application.Current.MainWindow, | ||
WindowStartupLocation = WindowStartupLocation.CenterScreen, | ||
Topmost = true, | ||
Width = 600, | ||
Height = 800, | ||
Title = "Connect to GitHub..." | ||
}; | ||
webBrowserDialog.Closed += OnClosed; | ||
webBrowserDialog.Show(); | ||
|
||
void OnClosed(object sender, EventArgs e) => httpListener.Close(); | ||
|
||
HttpListenerContext httpListenerContext; | ||
try | ||
{ | ||
httpListenerContext = await httpListener.GetContextAsync(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
webBrowserDialog.Close(); | ||
|
||
message = "Authentication to GitHub failed."; | ||
|
||
logger.LogError(ex, message); | ||
MessageBox.Show(message, MessageCaption, MessageBoxButton.OK, MessageBoxImage.Error); | ||
return null; | ||
} | ||
|
||
webBrowserDialog.Close(); | ||
|
||
try | ||
{ | ||
await GitHubAutoUpdateApi.GetAccessTokenAsync(gitHubClient, applicationOAuthId, applicationOAuthSecret, requestState, httpListenerContext); | ||
} | ||
catch (Exception ex) | ||
{ | ||
message = "Failed to connect to GitHub."; | ||
|
||
logger.LogError(ex, message); | ||
MessageBox.Show(message, MessageCaption, MessageBoxButton.OK, MessageBoxImage.Error); | ||
return null; | ||
} | ||
|
||
return gitHubClient; | ||
} | ||
|
||
static private async Task<Release> CheckUpdatesAsync(GitHubClient gitHubClient, IAutoUpdateConfiguration configuration, ILogger logger) | ||
{ | ||
string repositoryOwner = configuration.RepositoryOwner; | ||
string repositoryName = configuration.RepositoryName; | ||
string message; | ||
|
||
Release latestRelease; | ||
try | ||
{ | ||
latestRelease = await GitHubAutoUpdateApi.GetLatestRelease(gitHubClient, repositoryOwner, repositoryName); | ||
} | ||
catch (Exception ex) | ||
{ | ||
message = "Failed to get the latest release tag."; | ||
|
||
logger.LogError(ex, message); | ||
MessageBox.Show(message, MessageCaption, MessageBoxButton.OK, MessageBoxImage.Error); | ||
return null; | ||
} | ||
|
||
if (CalameUtils.IsDevelopmentBuild()) | ||
{ | ||
message = $"Latest version is: {latestRelease.TagName}.\n\n(This development build will not be updated.)"; | ||
|
||
MessageBox.Show(message, MessageCaption, MessageBoxButton.OK, MessageBoxImage.Information); | ||
return null; | ||
} | ||
|
||
if (latestRelease.TagName == CalameUtils.GetVersion()) | ||
{ | ||
message = $"You are using the latest version. ({latestRelease.TagName})"; | ||
|
||
MessageBox.Show(message, MessageCaption, MessageBoxButton.OK, MessageBoxImage.Information); | ||
return null; | ||
} | ||
|
||
if (latestRelease.Assets.All(x => x.Name != configuration.InstallerAssetName)) | ||
{ | ||
message = $"Latest version is {latestRelease.TagName} but no installer is available yet. Retry to update in a few minutes."; | ||
|
||
MessageBox.Show(message, MessageCaption, MessageBoxButton.OK, MessageBoxImage.Information); | ||
return null; | ||
} | ||
|
||
MessageBoxResult messageBoxResult = MessageBox.Show($"Do you want to download the last version ({latestRelease.TagName}) ?", MessageCaption, | ||
MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes); | ||
|
||
return messageBoxResult == MessageBoxResult.Yes ? latestRelease : null; | ||
} | ||
|
||
static private async Task<string> DownloadInstallerAsset(GitHubClient gitHubClient, IAutoUpdateConfiguration configuration, Release release) | ||
{ | ||
string downloadFolderPath = GetRandomFolderPath(Path.GetTempPath()); | ||
string installerAssetName = configuration.InstallerAssetName; | ||
byte[] assetBytes = await GitHubAutoUpdateApi.DownloadAsset(gitHubClient, release, installerAssetName); | ||
|
||
string assetFilePath = Path.Combine(downloadFolderPath, installerAssetName); | ||
using (FileStream assetFileStream = File.Create(assetFilePath)) | ||
{ | ||
await assetFileStream.WriteAsync(assetBytes); | ||
} | ||
|
||
return assetFilePath; | ||
} | ||
|
||
static private string GetRandomFolderPath(string parentFolderPath) | ||
{ | ||
string folderPath; | ||
do | ||
{ | ||
folderPath = Path.Combine(parentFolderPath, Path.GetFileNameWithoutExtension(Path.GetRandomFileName())); | ||
} | ||
while (Directory.Exists(folderPath)); | ||
|
||
return folderPath; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
namespace Calame.AutoUpdate | ||
{ | ||
public interface IAutoUpdateConfiguration | ||
{ | ||
string ProductId { get; } | ||
string ApplicationOAuthId { get; } | ||
string ApplicationOAuthSecret { get; } | ||
string RepositoryOwner { get; } | ||
string RepositoryName { get; } | ||
string InstallerAssetName { get; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<Window x:Class="Calame.AutoUpdate.WebBrowserDialog" | ||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | ||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | ||
xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" | ||
mc:Ignorable="d" | ||
Title="WebBrowserDialog" Height="450" Width="800"> | ||
<Grid> | ||
<wpf:WebView2 x:Name="WebView" /> | ||
</Grid> | ||
</Window> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
using System; | ||
using System.Windows; | ||
|
||
namespace Calame.AutoUpdate | ||
{ | ||
/// <summary> | ||
/// Interaction logic for WebBrowserDialog.xaml | ||
/// </summary> | ||
public partial class WebBrowserDialog : Window | ||
{ | ||
public WebBrowserDialog(Uri uri) | ||
{ | ||
InitializeComponent(); | ||
WebView.Source = uri; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.