1
0
mirror of https://github.com/chylex/TweetDuck.git synced 2025-04-14 03:15:49 +02:00

Begin rewriting update checker to run within C#

This commit is contained in:
chylex 2018-04-10 21:18:08 +02:00
parent f99d035621
commit ad45cf8c72
10 changed files with 238 additions and 159 deletions

View File

@ -78,8 +78,9 @@ public FormBrowser(UpdaterSettings updaterSettings){
Disposed += (sender, args) => {
Config.MuteToggled -= Config_MuteToggled;
Config.TrayBehaviorChanged -= Config_TrayBehaviorChanged;
browser.Dispose();
updates.Dispose();
contextMenu.Dispose();
notificationScreenshotManager?.Dispose();
@ -96,6 +97,7 @@ public FormBrowser(UpdaterSettings updaterSettings){
UpdateTrayIcon();
this.updates = new UpdateHandler(browser, updaterSettings);
this.updates.CheckFinished += updates_CheckFinished;
this.updates.UpdateAccepted += updates_UpdateAccepted;
this.updates.UpdateDismissed += updates_UpdateDismissed;
@ -233,6 +235,18 @@ private static void plugins_Executed(object sender, PluginErrorEventArgs e){
}
}
private void updates_CheckFinished(object sender, UpdateCheckEventArgs e){
this.InvokeAsyncSafe(() => {
e.Result.Handle(update => {
if (!update.IsUpdateNew && !update.IsUpdateDismissed){
updates.StartTimer();
}
}, ex => {
// TODO show error and ask if the user wants to temporarily disable checks -- or maybe only do that for the first check of the day
});
});
}
private void updates_UpdateAccepted(object sender, UpdateEventArgs e){
this.InvokeAsyncSafe(() => {
FormManager.CloseAllDialogs();
@ -241,13 +255,21 @@ private void updates_UpdateAccepted(object sender, UpdateEventArgs e){
Config.DismissedUpdate = null;
Config.Save();
}
updates.BeginUpdateDownload(this, e.UpdateInfo, update => {
if (update.DownloadStatus == UpdateDownloadStatus.Done){
UpdateInstallerPath = update.InstallerPath;
}
ForceClose();
updates.BeginUpdateDownload(this, e.UpdateInfo, update => {
UpdateDownloadStatus status = update.DownloadStatus;
if (status == UpdateDownloadStatus.Done){
UpdateInstallerPath = update.InstallerPath;
ForceClose();
}
else if (FormMessage.Error("Update Has Failed", "Could not automatically download the update: "+(update.DownloadError?.Message ?? "unknown error")+"\n\nWould you like to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(Program.Website);
ForceClose();
}
else{
Show();
}
});
});
}

View File

@ -222,9 +222,13 @@ private void updates_CheckFinished(object sender, UpdateCheckEventArgs e){
if (e.EventId == updateCheckEventId){
btnCheckUpdates.Enabled = true;
if (!e.IsUpdateAvailable){
FormMessage.Information("No Updates Available", "Your version of TweetDuck is up to date.", FormMessage.OK);
}
e.Result.Handle(update => {
if (!update.IsUpdateNew){
FormMessage.Information("No Updates Available", "Your version of TweetDuck is up to date.", FormMessage.OK);
}
}, ex => {
Program.Reporter.HandleException("Update Check", "Encountered an error while checking updates.", true, ex);
});
}
});
}

View File

@ -1,33 +1,9 @@
(function($, $TDU){
//
// Variable: Current timeout ID for update checking.
//
var updateCheckTimeoutID;
//
// Constant: Update exe file name.
//
const updateFileName = "TweetDuck.Update.exe";
//
// Constant: Url that returns JSON data about latest version.
//
const updateCheckUrlLatest = "https://api.github.com/repos/chylex/TweetDuck/releases/latest";
//
// Constant: Url that returns JSON data about all versions, including prereleases.
//
const updateCheckUrlAll = "https://api.github.com/repos/chylex/TweetDuck/releases";
//
// Constant: Fallback url in case the update installer file is missing.
//
const updateDownloadFallback = "https://tweetduck.chylex.com";
//
// Function: Creates the update notification element. Removes the old one if already exists.
//
var displayNotification = function(version, download, changelog){
var displayNotification = function(version, changelog){
// styles
var css = $("#tweetduck-update-css");
@ -167,7 +143,7 @@
<div id='tweetduck-changelog'>
<div id='tweetduck-changelog-box'>
<h2>TweetDuck Update ${version}</h2>
${changelog}
${markdown(atob(changelog))}
</div>
</div>
`).appendTo(document.body).css("display", "none");
@ -219,17 +195,11 @@
buttonDiv.children(".tdu-btn-download").click(function(){
hide();
if (download){
$TDU.onUpdateAccepted();
}
else{
$TDU.openBrowser(updateDownloadFallback);
}
$TDU.onUpdateAccepted();
});
buttonDiv.children(".tdu-btn-later").click(function(){
clearTimeout(updateCheckTimeoutID);
$TDU.onUpdateDelayed();
slide();
});
@ -245,15 +215,6 @@
return ele;
};
//
// Function: Returns milliseconds until the start of the next hour, with an extra offset in seconds that can skip an hour if the clock would roll over too soon.
//
var getTimeUntilNextHour = function(extra){
var now = new Date();
var offset = new Date(+now+extra*1000);
return new Date(offset.getFullYear(), offset.getMonth(), offset.getDate(), offset.getHours()+1, 0, 0)-now;
};
//
// Function: Ghetto-converts markdown to HTML.
//
@ -273,33 +234,6 @@
.replace(/\n\r?\n\r?/g, "<br>");
};
//
// Function: Runs an update check and updates all DOM elements appropriately.
//
var runUpdateCheck = function(eventID, versionTag, dismissedVersionTag, allowPre){
clearTimeout(updateCheckTimeoutID);
updateCheckTimeoutID = setTimeout($TDU.triggerUpdateCheck, getTimeUntilNextHour(60*30)); // 30 minute offset
$.getJSON(allowPre ? updateCheckUrlAll : updateCheckUrlLatest, function(response){
var release = allowPre ? response[0] : response;
var tagName = release.tag_name;
var hasUpdate = tagName !== versionTag && tagName !== dismissedVersionTag && release.assets.length > 0;
if (hasUpdate){
var obj = release.assets.find(asset => asset.name === updateFileName) || { browser_download_url: "" };
displayNotification(tagName, obj.browser_download_url, markdown(release.body));
if (eventID){ // ignore undefined and 0
$TDU.onUpdateCheckFinished(eventID, tagName, obj.browser_download_url);
}
}
else if (eventID){ // ignore undefined and 0
$TDU.onUpdateCheckFinished(eventID, null, null);
}
});
};
//
// Block: Check updates on startup.
//
@ -310,5 +244,5 @@
//
// Block: Setup global functions.
//
window.TDUF_runUpdateCheck = runUpdateCheck;
window.TDUF_displayNotification = displayNotification;
})($, $TDU);

View File

@ -69,6 +69,7 @@
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Management" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
@ -316,6 +317,7 @@
<Compile Include="Core\Management\BrowserCache.cs" />
<Compile Include="Core\Utils\BrowserUtils.cs" />
<Compile Include="Core\Utils\NativeMethods.cs" />
<Compile Include="Updates\UpdateCheckClient.cs" />
<Compile Include="Updates\UpdateDownloadStatus.cs" />
<Compile Include="Updates\UpdateHandler.cs" />
<Compile Include="Updates\UpdateInfo.cs" />

View File

@ -1,13 +1,14 @@
using System;
using TweetDuck.Data;
namespace TweetDuck.Updates.Events{
sealed class UpdateCheckEventArgs : EventArgs{
public int EventId { get; }
public bool IsUpdateAvailable { get; }
public Result<UpdateInfo> Result { get; }
public UpdateCheckEventArgs(int eventId, bool isUpdateAvailable){
public UpdateCheckEventArgs(int eventId, Result<UpdateInfo> result){
this.EventId = eventId;
this.IsUpdateAvailable = isUpdateAvailable;
this.Result = result;
}
}
}

View File

@ -1,7 +1,5 @@
using System;
using System.Windows.Forms;
using TweetDuck.Core.Other;
using TweetDuck.Core.Utils;
namespace TweetDuck.Updates{
sealed partial class FormUpdateDownload : Form{
@ -22,19 +20,8 @@ private void btnCancel_Click(object sender, EventArgs e){
}
private void timerDownloadCheck_Tick(object sender, EventArgs e){
if (updateInfo.DownloadStatus == UpdateDownloadStatus.Done){
if (updateInfo.DownloadStatus.IsFinished()){
timerDownloadCheck.Stop();
DialogResult = DialogResult.OK;
Close();
}
else if (updateInfo.DownloadStatus == UpdateDownloadStatus.Failed){
timerDownloadCheck.Stop();
if (FormMessage.Error("Update Has Failed", "Could not download the update: "+(updateInfo.DownloadError?.Message ?? "unknown error")+"\n\nDo you want to open the website and try downloading the update manually?", FormMessage.Yes, FormMessage.No)){
BrowserUtils.OpenExternalBrowser(Program.Website);
DialogResult = DialogResult.OK;
}
Close();
}
}

View File

@ -0,0 +1,63 @@
using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
using TweetDuck.Core.Utils;
using JsonObject = System.Collections.Generic.IDictionary<string, object>;
namespace TweetDuck.Updates{
sealed class UpdateCheckClient{
private const string ApiLatestRelease = "https://api.github.com/repos/chylex/TweetDuck/releases/latest";
private const string UpdaterAssetName = "TweetDuck.Update.exe";
private readonly UpdaterSettings settings;
public UpdateCheckClient(UpdaterSettings settings){
this.settings = settings;
}
public Task<UpdateInfo> Check(){
TaskCompletionSource<UpdateInfo> result = new TaskCompletionSource<UpdateInfo>();
WebClient client = BrowserUtils.CreateWebClient();
client.Headers[HttpRequestHeader.Accept] = "application/vnd.github.v3+json"; // TODO could use .html to avoid custom markdown parsing
client.DownloadStringTaskAsync(ApiLatestRelease).ContinueWith(task => {
if (task.IsCanceled){
result.SetCanceled();
}
else if (task.IsFaulted){
result.SetException(task.Exception.InnerException);
}
else{
try{
result.SetResult(ParseFromJson(task.Result));
}catch(Exception e){
result.SetException(e);
}
}
});
return result.Task;
}
private UpdateInfo ParseFromJson(string json){
bool IsUpdaterAsset(JsonObject obj){
return UpdaterAssetName == (string)obj["name"];
}
string AssetDownloadUrl(JsonObject obj){
return (string)obj["browser_download_url"];
}
JsonObject root = (JsonObject)new JavaScriptSerializer().DeserializeObject(json);
string versionTag = (string)root["tag_name"];
string releaseNotes = (string)root["body"];
string downloadUrl = ((Array)root["assets"]).Cast<JsonObject>().Where(IsUpdaterAsset).Select(AssetDownloadUrl).FirstOrDefault();
return new UpdateInfo(settings, versionTag, releaseNotes, downloadUrl);
}
}
}

View File

@ -2,7 +2,14 @@
public enum UpdateDownloadStatus{
None = 0,
InProgress,
AssetMissing,
Done,
Failed
}
public static class UpdateDownloadStatusExtensions{
public static bool IsFinished(this UpdateDownloadStatus status){
return status == UpdateDownloadStatus.AssetMissing || status == UpdateDownloadStatus.Done || status == UpdateDownloadStatus.Failed;
}
}
}

View File

@ -1,21 +1,26 @@
using CefSharp;
using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using TweetDuck.Core.Controls;
using TweetDuck.Core.Other.Interfaces;
using TweetDuck.Core.Utils;
using TweetDuck.Data;
using TweetDuck.Resources;
using TweetDuck.Updates.Events;
namespace TweetDuck.Updates{
sealed class UpdateHandler{
sealed class UpdateHandler : IDisposable{
public const int CheckCodeUpdatesDisabled = -1;
public const int CheckCodeNotOnTweetDeck = -2;
private readonly ITweetDeckBrowser browser;
private readonly UpdaterSettings settings;
private readonly UpdateCheckClient client;
private readonly ITweetDeckBrowser browser;
private readonly Timer timer;
public event EventHandler<UpdateEventArgs> UpdateAccepted;
public event EventHandler<UpdateEventArgs> UpdateDelayed;
public event EventHandler<UpdateEventArgs> UpdateDismissed;
public event EventHandler<UpdateCheckEventArgs> CheckFinished;
@ -23,11 +28,44 @@ sealed class UpdateHandler{
private UpdateInfo lastUpdateInfo;
public UpdateHandler(ITweetDeckBrowser browser, UpdaterSettings settings){
this.browser = browser;
this.settings = settings;
this.client = new UpdateCheckClient(settings);
this.browser = browser;
this.browser.OnFrameLoaded(OnFrameLoaded);
this.browser.RegisterBridge("$TDU", new Bridge(this));
browser.OnFrameLoaded(OnFrameLoaded);
browser.RegisterBridge("$TDU", new Bridge(this));
this.timer = new Timer();
this.timer.Tick += timer_Tick;
}
public void Dispose(){
timer.Dispose();
}
private void timer_Tick(object sender, EventArgs e){
timer.Stop();
Check(false);
}
public void StartTimer(){
if (timer.Enabled){
return;
}
timer.Stop();
if (Program.UserConfig.EnableUpdateCheck){
DateTime now = DateTime.Now;
TimeSpan nextHour = now.AddSeconds(60*(60-now.Minute)-now.Second)-now;
if (nextHour.TotalMinutes < 15){
nextHour = nextHour.Add(TimeSpan.FromHours(1));
}
timer.Interval = (int)Math.Ceiling(nextHour.TotalMilliseconds);
timer.Start();
}
}
private void OnFrameLoaded(IFrame frame){
@ -43,17 +81,24 @@ public int Check(bool force){
if (!browser.IsTweetDeckWebsite){
return CheckCodeNotOnTweetDeck;
}
int nextEventId = unchecked(++lastEventId);
Task<UpdateInfo> checkTask = client.Check();
browser.ExecuteFunction("TDUF_runUpdateCheck", (int)unchecked(++lastEventId), Program.VersionTag, settings.DismissedUpdate ?? string.Empty, settings.AllowPreReleases);
return lastEventId;
checkTask.ContinueWith(task => HandleUpdateCheckSuccessful(nextEventId, task.Result), TaskContinuationOptions.OnlyOnRanToCompletion);
checkTask.ContinueWith(task => HandleUpdateCheckFailed(nextEventId, task.Exception.InnerException), TaskContinuationOptions.OnlyOnFaulted);
return nextEventId;
}
return CheckCodeUpdatesDisabled;
}
public void BeginUpdateDownload(Form ownerForm, UpdateInfo updateInfo, Action<UpdateInfo> onSuccess){
if (updateInfo.DownloadStatus == UpdateDownloadStatus.Done){
onSuccess(updateInfo);
public void BeginUpdateDownload(Form ownerForm, UpdateInfo updateInfo, Action<UpdateInfo> onFinished){
UpdateDownloadStatus status = updateInfo.DownloadStatus;
if (status == UpdateDownloadStatus.Done || status == UpdateDownloadStatus.AssetMissing){
onFinished(updateInfo);
}
else{
FormUpdateDownload downloadForm = new FormUpdateDownload(updateInfo);
@ -65,13 +110,7 @@ public void BeginUpdateDownload(Form ownerForm, UpdateInfo updateInfo, Action<Up
downloadForm.FormClosed += (sender, args) => {
downloadForm.Dispose();
if (downloadForm.DialogResult == DialogResult.OK){ // success or manual download
onSuccess(updateInfo);
}
else{
ownerForm.Show();
}
onFinished(updateInfo);
};
downloadForm.Show();
@ -85,17 +124,41 @@ public void CleanupDownload(){
}
}
private void TriggerUpdateAcceptedEvent(UpdateEventArgs args){
UpdateAccepted?.Invoke(this, args);
private void HandleUpdateCheckSuccessful(int eventId, UpdateInfo info){
if (info.IsUpdateNew && !info.IsUpdateDismissed){
CleanupDownload();
lastUpdateInfo = info;
lastUpdateInfo.BeginSilentDownload();
browser.ExecuteFunction("TDUF_displayNotification", lastUpdateInfo.VersionTag, Convert.ToBase64String(Encoding.GetEncoding("iso-8859-1").GetBytes(lastUpdateInfo.ReleaseNotes))); // TODO move browser stuff outside
}
CheckFinished?.Invoke(this, new UpdateCheckEventArgs(eventId, new Result<UpdateInfo>(info)));
}
private void TriggerUpdateDismissedEvent(UpdateEventArgs args){
settings.DismissedUpdate = args.UpdateInfo.VersionTag;
UpdateDismissed?.Invoke(this, args);
private void HandleUpdateCheckFailed(int eventId, Exception exception){
CheckFinished?.Invoke(this, new UpdateCheckEventArgs(eventId, new Result<UpdateInfo>(exception)));
}
private void TriggerCheckFinishedEvent(UpdateCheckEventArgs args){
CheckFinished?.Invoke(this, args);
private void TriggerUpdateAcceptedEvent(){
if (lastUpdateInfo != null){
UpdateAccepted?.Invoke(this, new UpdateEventArgs(lastUpdateInfo));
}
}
private void TriggerUpdateDelayedEvent(){
if (lastUpdateInfo != null){
UpdateDelayed?.Invoke(this, new UpdateEventArgs(lastUpdateInfo));
}
}
private void TriggerUpdateDismissedEvent(){
if (lastUpdateInfo != null){
settings.DismissedUpdate = lastUpdateInfo.VersionTag;
UpdateDismissed?.Invoke(this, new UpdateEventArgs(lastUpdateInfo));
CleanupDownload();
}
}
public sealed class Bridge{
@ -109,31 +172,16 @@ public void TriggerUpdateCheck(){
owner.Check(false);
}
public void OnUpdateCheckFinished(int eventId, string versionTag, string downloadUrl){
if (versionTag != null && (owner.lastUpdateInfo == null || owner.lastUpdateInfo.VersionTag != versionTag)){
owner.CleanupDownload();
owner.lastUpdateInfo = new UpdateInfo(owner.settings, eventId, versionTag, downloadUrl);
owner.lastUpdateInfo.BeginSilentDownload();
}
owner.TriggerCheckFinishedEvent(new UpdateCheckEventArgs(eventId, owner.lastUpdateInfo != null));
public void OnUpdateAccepted(){
owner.TriggerUpdateAcceptedEvent();
}
public void OnUpdateAccepted(){
if (owner.lastUpdateInfo != null){
owner.TriggerUpdateAcceptedEvent(new UpdateEventArgs(owner.lastUpdateInfo));
}
public void OnUpdateDelayed(){
owner.TriggerUpdateDelayedEvent();
}
public void OnUpdateDismissed(){
if (owner.lastUpdateInfo != null){
owner.TriggerUpdateDismissedEvent(new UpdateEventArgs(owner.lastUpdateInfo));
owner.CleanupDownload();
}
}
public void OpenBrowser(string url){
BrowserUtils.OpenExternalBrowser(url);
owner.TriggerUpdateDismissedEvent();
}
}
}

View File

@ -5,40 +5,43 @@
namespace TweetDuck.Updates{
sealed class UpdateInfo{
public int EventId { get; }
public string VersionTag { get; }
public string ReleaseNotes { get; }
public string InstallerPath { get; }
public bool IsUpdateNew => VersionTag != Program.VersionTag;
public bool IsUpdateDismissed => VersionTag == settings.DismissedUpdate;
public UpdateDownloadStatus DownloadStatus { get; private set; }
public Exception DownloadError { get; private set; }
private readonly string installerFolder;
private readonly UpdaterSettings settings;
private readonly string downloadUrl;
private WebClient currentDownload;
public UpdateInfo(UpdaterSettings settings, int eventId, string versionTag, string downloadUrl){
this.installerFolder = settings.InstallerDownloadFolder;
public UpdateInfo(UpdaterSettings settings, string versionTag, string releaseNotes, string downloadUrl){
this.settings = settings;
this.downloadUrl = downloadUrl;
this.EventId = eventId;
this.VersionTag = versionTag;
this.InstallerPath = Path.Combine(installerFolder, "TweetDuck."+versionTag+".exe");
this.ReleaseNotes = releaseNotes;
this.InstallerPath = Path.Combine(settings.InstallerDownloadFolder, "TweetDuck."+versionTag+".exe");
}
public void BeginSilentDownload(){
if (DownloadStatus == UpdateDownloadStatus.None || DownloadStatus == UpdateDownloadStatus.Failed){
DownloadStatus = UpdateDownloadStatus.InProgress;
try{
Directory.CreateDirectory(installerFolder);
}catch(Exception e){
DownloadError = e;
DownloadStatus = UpdateDownloadStatus.Failed;
if (string.IsNullOrEmpty(downloadUrl)){
DownloadError = new InvalidDataException("Missing installer asset.");
DownloadStatus = UpdateDownloadStatus.AssetMissing;
return;
}
if (string.IsNullOrEmpty(downloadUrl)){
DownloadError = new UriFormatException("Could not determine URL of the update installer");
try{
Directory.CreateDirectory(settings.InstallerDownloadFolder);
}catch(Exception e){
DownloadError = e;
DownloadStatus = UpdateDownloadStatus.Failed;
return;
}
@ -68,5 +71,13 @@ public void DeleteInstaller(){
// rip
}
}
public override bool Equals(object obj){
return obj is UpdateInfo info && VersionTag == info.VersionTag;
}
public override int GetHashCode(){
return VersionTag.GetHashCode();
}
}
}