diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..f663e77a --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,5 @@ + + + 11.1.3 + + diff --git a/NetSparkleUpdater.sln b/NetSparkleUpdater.sln index 5e1e6d09..7833b806 100644 --- a/NetSparkleUpdater.sln +++ b/NetSparkleUpdater.sln @@ -28,7 +28,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSparkle.Samples.NetCore. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSparkle.Samples.NetCore.WinForms", "src\NetSparkle.Samples.NetCore.WinForms\NetSparkle.Samples.NetCore.WinForms.csproj", "{0088BDC7-D50B-4AD4-8EE6-D2B7DA150778}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSparkle.UI.WinForms.NetFramework", "src\NetSparkle.UI.WinForms.NetFramework\NetSparkle.UI.WinForms.NetFramework.csproj", "{DAB16394-9862-49C3-818B-6B84F22EF5FE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSparkle.UI.WinForms.NetFramework", "src\NetSparkle.UI.WinForms.NetFramework\NetSparkle.UI.WinForms.NetFramework.csproj", "{DAB16394-9862-49C3-818B-6B84F22EF5FE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSparkle.UI.Avalonia", "src\NetSparkle.UI.Avalonia\NetSparkle.UI.Avalonia.csproj", "{A50C27FA-CE8E-4253-BC1E-4B60053B0B7E}" EndProject @@ -42,6 +42,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSparkle.Samples.Avalonia EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F3EF1CB9-F6D1-4D71-87C2-F2E80DD53968}" ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props nuget\winformsnetframework\NetSparkleUpdater.UI.WinForms.NetFramework.nuspec = nuget\winformsnetframework\NetSparkleUpdater.UI.WinForms.NetFramework.nuspec .github\workflows\publish-nuget.yml = .github\workflows\publish-nuget.yml README.md = README.md @@ -51,6 +52,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSparkle.Samples.Avalonia EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetSparkle.Tests.Trimming", "src\NetSparkle.Tests.Trimming\NetSparkle.Tests.Trimming.csproj", "{7AE76C89-60B5-4F0E-A774-6E52F53B59D5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NetSparkle.Samples.Forms.Multithread", "src\NetSparkle.Samples.Forms.Multithread\NetSparkle.Samples.Forms.Multithread.csproj", "{7952F006-5B13-45CF-93A8-46A0A470575F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -417,6 +420,46 @@ Global {314E5B47-24FA-42F5-9975-3E4252853576}.Release|x64.Build.0 = Release|Any CPU {314E5B47-24FA-42F5-9975-3E4252853576}.Release|x86.ActiveCfg = Release|Any CPU {314E5B47-24FA-42F5-9975-3E4252853576}.Release|x86.Build.0 = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|ARM.ActiveCfg = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|ARM.Build.0 = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|x64.ActiveCfg = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|x64.Build.0 = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|x86.ActiveCfg = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Debug|x86.Build.0 = Debug|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|Any CPU.Build.0 = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|ARM.ActiveCfg = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|ARM.Build.0 = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|x64.ActiveCfg = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|x64.Build.0 = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|x86.ActiveCfg = Release|Any CPU + {7AE76C89-60B5-4F0E-A774-6E52F53B59D5}.Release|x86.Build.0 = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|ARM.ActiveCfg = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|ARM.Build.0 = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|x64.ActiveCfg = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|x64.Build.0 = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|x86.ActiveCfg = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Debug|x86.Build.0 = Debug|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|Any CPU.Build.0 = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|ARM.ActiveCfg = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|ARM.Build.0 = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|x64.ActiveCfg = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|x64.Build.0 = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|x86.ActiveCfg = Release|Any CPU + {7952F006-5B13-45CF-93A8-46A0A470575F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index bcf70c9b..c6f3dbb3 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ We are open to contributions that might make the overall install/update process ### Project file -In your project file, make sure you set up a few things so that the library can read in the pertinent details later. +In your project file, make sure you set up a few things so that the library can read in the pertinent details later. _Note: You can use your own `IAssemblyAccessor` to load version information from somewhere else. However, setting things up in your project file is easy, and NetSparkleUpdater can read that in natively!_ ```xml @@ -118,7 +118,7 @@ In your project file, make sure you set up a few things so that the library can ``` -IMPORTANT NOTE: In .NET 8+, a change was made that causes your git/source code commit hash to be included in your app's `` number. This behavior cannot be avoided by NetSparkleUpdater at this time as we rely on `AssemblyInformationalVersionAttribute`, and this attribute's behavior was changed. Your users may be told that they are currently running `1.0.0+commitHashHere` by NetSparkleUpdater (and your native app itself!). We recommend adding the following lines to your project file (in a new `` or an existing one): +IMPORTANT NOTE: In .NET 8+, a change was made that causes your git/source code commit hash to be included in your app's `` number. This behavior cannot be avoided by NetSparkleUpdater at this time as we rely on `AssemblyInformationalVersionAttribute`, and this attribute's behavior was changed. Your users may be told that they are currently running `1.0.0+commitHashHere` by NetSparkleUpdater (and your native app itself!). We also recommend adding the following lines to your project file (in a new `` or an existing one): ```xml false @@ -127,17 +127,20 @@ IMPORTANT NOTE: In .NET 8+, a change was made that causes your git/source code c ### Code ```csharp +// NOTE: Under most, if not all, circumstances, SparkleUpdater should be initialized on your app's main UI thread. +// This way, if you're using a built-in UI with no custom adjustments, all calls to UI objects will automatically go to the UI thread for you. +// Basically, SparkleUpdater's background loop will make calls to the thread that the SparkleUpdater was created on via SyncronizationContext. +// So, if you start SparkleUpdater on the UI thread, the background loop events will auto-call to the UI thread for you. _sparkle = new SparkleUpdater( "http://example.com/appcast.xml", // link to your app cast file new Ed25519Checker(SecurityMode.Strict, // security mode -- use .Unsafe to ignore all signature checking (NOT recommended!!) "base_64_public_key") // your base 64 public key -- generate this with the NetSparkleUpdater.Tools.AppCastGenerator .NET CLI tool on any OS ) { - UIFactory = new NetSparkleUpdater.UI.WPF.UIFactory(icon), // or null or choose some other UI factory or build your own! + UIFactory = new NetSparkleUpdater.UI.WPF.UIFactory(icon), // or null, or choose some other UI factory, or build your own IUIFactory implementation! RelaunchAfterUpdate = false, // default is false; set to true if you want your app to restart after updating (keep as false if your installer will start your app for you) CustomInstallerArguments = "", // set if you want your installer to get some command-line args - ShowsUIOnMainThread = true, // required on Avalonia, preferred on WPF/WinForms }; -_sparkle.StartLoop(true); // `true` to run an initial check online -- only call StartLoop once for a given SparkleUpdater instance! +_sparkle.StartLoop(true); // `true` to run an initial check online -- only call StartLoop **once** for a given SparkleUpdater instance! ``` On the first Application.Idle event, your App Cast XML file will be downloaded, read, and compared to the currently running version. If it has a software update inside, the user will be notified with a little toast notification (if supported by the UI and enabled) or with an update dialog containing your release notes. The user can then ignore the update, ask to be reminded later, or download/install it now. @@ -403,6 +406,16 @@ Please see [UPGRADING.md](UPGRADING.md) for information on breaking changes betw Nope. You can just reference the core library and handle everything yourself, including any custom UI. Check out the code samples for an example of doing that! +### Can I run my UI on another thread besides my main UI thread? + +This isn't a built-in feature, as NetSparkleUpdater assumes that it can safely make calls/events to the UI on the thread that started the `SparkleUpdater` instance. However, if you'd like to do this, we have a sample on how to do this: `NetSparkle.Samples.Forms.Multithread`. Basically, instead of passing in a `UIFactory` to `SparkleUpdater`, you handle `SparkleUpdater`'s events yourself and show the UI however you want to show it - and yes, you can still use the built-in UI objects for this! + +(Note that on Avalonia, the answer is always "No" since they only support one UI thread at this time.) + +### On WinForms, can I let the user close the main window and still keep the updater forms around? + +Yes. You need to start the `NetSparkleUpdater` forms on a new thread(s). See the `NetSparkle.Samples.Forms.Multithread` sample for how to do this by handling events yourself and still using the built-in WinForms `UIFactory`. + ### How do I make my .NET Framework WinForms app high DPI aware? See #238 [and this documentation](https://docs.microsoft.com/en-us/dotnet/desktop/winforms/high-dpi-support-in-windows-forms?view=netframeworkdesktop-4.8#configuring-your-windows-forms-app-for-high-dpi-support) for the fix for making this work on the sample application. Basically, you need to use an app config file and manifest file to let Windows know that your application is DPI-aware. If that doesn't work for you, try some of the tips at [this SO post](https://stackoverflow.com/questions/4075802/creating-a-dpi-aware-application). @@ -490,12 +503,12 @@ Yes! Please help us make this library awesome! ### What's the tagging scheme, here? -* 2.x.y (Core) -* 2.x.y-app-cast-generator -* 2.x.y-dsa-helper -* 2.x.y-UI-Avalonia -* 2.x.y-UI-WinForms -* 2.x.y-UI-WPF +* Major.Minor.Patch (Core) +* Major.Minor.Patch-app-cast-generator +* Major.Minor.Patch-dsa-helper +* Major.Minor.Patch-UI-Avalonia +* Major.Minor.Patch-UI-WinForms +* Major.Minor.Patch-UI-WPF ## Requirements @@ -511,9 +524,8 @@ Contributions are ALWAYS welcome! If you see a new feature you'd like to add, pl ### Areas where we could use help/contributions -* Unit tests for all parts of the project -* Extensive testing on macOS/Linux -* More built-in app cast parsers +* Unit tests for all parts of the project, including UI unit tests, full download tests, etc. +* Extensive testing/upgrades on macOS/Linux * More options in the app cast generator * See the [issues list](https://github.com/NetSparkleUpdater/NetSparkle/issues) for more diff --git a/UPGRADING.md b/UPGRADING.md index 988e0b24..92de4445 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -22,10 +22,10 @@ * Used `SemVerLike` everywhere instead of `System.Version` for semver compatibility * `WebFileDownloader` now deletes files on cancellation of a download like `LocalFileDownloader` did already (1cd2284c41bbe85d41566915965ad2acdb1a61f5) * `WebFileDownloader` does not call `PrepareToDownloadFile()` in its constructor anymore. Now, it is called after the object is fully created. (420f961dfa9c9071332e2e0737b0f287d2cfa5dc) -* NOTE: If you update to .NET 8+, the location of `JSONConfiguration` save data, by default, will CHANGE due to a change in .NET 8 for `Environment.SpecialFolder.ApplicationData`. See: https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/8.0/getfolderpath-unix. This may cause user's "skipped version" or other data to be lost unless you migrate this data yourself. For most users who are using the defaults, this is likely only a minor (if any) inconvenience at all, but it is worth noting. +* NOTE: If you update to .NET 8+ in your own app, the location of `JSONConfiguration` save data, by default, will CHANGE due to a change in .NET 8 for `Environment.SpecialFolder.ApplicationData`. See: https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/8.0/getfolderpath-unix. This may cause user's "skipped version" or other data to be lost unless you migrate this data yourself. For most users who are using the defaults, this is likely only a minor (if any) inconvenience at all, but it is worth noting. SparkleUpdater makes no attempt to account for this change at this time. * `AssemblyReflectionAccessor` has been deprecated. Please use `AsmResolverAccessor` instead. (#587) * `AssemblyDiagnosticAccessor.AssemblyVersion` now returns `FileVersionInfo.GetVersionInfo(assemblyName).ProductVersion` rather than `FileVersionInfo.GetVersionInfo(assemblyName).FileVersion` so we can get semver versioning information -* For ed25519 use, changed from BouncyCastle to Chaos.NaCl (large file size savings and we don't need all of BouncyCastle's features). You can verify its use and code online here: https://github.com/NetSparkleUpdater/Chaos.NaCl. Additionally, this package is available on NuGet for your own projects. +* For ed25519 use, changed from BouncyCastle to Chaos.NaCl (large file size savings and we don't need all of BouncyCastle's features). You can verify its use and code online here: https://github.com/NetSparkleUpdater/Chaos.NaCl. Additionally, this package is available on NuGet for your own projects under `NetSparkleUpdater.Chaos.NaCl`. * `LogWriter` has undergone several changes: * `public static string tag = "netsparkle:";` has changed to `public string Tag { get; set; } = "netsparkle:";` * Instead of a simple `bool` to control printing to `Trace.WriteLine` or `Console.WriteLine`, there is now a new enum, `NetSparkleUpdater.Enums.LogWriterOutputMode`, which you can use to control whether `LogWriter` outputs to `Console`, `Trace`, `Debug`, or to `None` (don't output at all). @@ -53,6 +53,20 @@ * XML app casts write `version`, `shortVersion`, and `criticalUpdate` to the `` tag and the `` (both for backwards/Sparkle compat; we'd rather not write to `` but we don't want to break anyone that updates their app cast gen without updating the main library). * If both the overall `` and the `` have this data, the info from the `` is prioritized. * JSON app casts are not affected. +* `IUpdateDownloader` has a new event `DownloadStarted` of type `DownloadFromPathToPathEvent(object sender, string from, string to)`, which should be called right before any download begins. +* `IUpdateDownloader.StartFileDownload` returns `Task` +* `IUpdateDownloader.StartFileDownload` renamed to `IUpdateDownloader.DownloadFile` +* Background worker loop pauses for a few seconds before starting just in case the loop started with the software -- so the update available window isn't immediately shown to the user (if applicable) +* `ReleaseNotesGrabber.GetReleaseNotes` and `DownloadAllReleaseNotes` no longer take a `Sparkle` instance since that is given in the constructor (TODO: might adjust more later and only send in the `ISignatureVerifier`) +* Major UI refactoring: + * `ShowsUIOnMainThread` is gone and will not come back. It never really worked as intended and was just confusing. If you want to do fancy things with threads, handle `SparkleUpdater` events and handle things in your own way for your own app's needs. + * It's highly recommended to NetSparkle on the main thread. + * The background loop will use `SyncronizationContext` to post events and callbacks to whatever thread/context started the main `SparkleUpdater` instance, which is why starting things on the main UI thread is recommended. + * You can still do your own things with threads by handling events instead of passing a built-in `UIFactory` to `SparkleUpdater`, but this does not stop you from using a built-in `UIFactory` implementation to actually create said GUI elements. See the `NetSparkle.Samples.Forms.Multithread` sample. + * For a sample of running WinForms on multiple UI threads while still using built-in UI objects, see the `NetSparkle.Samples.Forms.Multithread` sample. + * Note: passing your own `UIFactory` that starts windows/things on new threads into `SparkleUpdater` is not a supported configuration. It might work, it might not. Use at your own risk. + * NetSparkle basically makes no attempts to worry about threading now (e.g. calling to the main thread) except for the background loop calling to the main thread that started the `SparkleUpdater` instance. For most apps, this will be fine as they are just using their main UI thread. + * Practically speaking, though, if you call `SparkleUpdater` functions on a background thread, subsequent events and things that are called as a result of that `SparkleUpdater` call will probably come back on the background thread that first called the event. Use at your own risk. When in doubt, for your own UI needs, make sure to check `InvokeRequired` on WinForms, and on WPF/Avalonia, marshal things to the UI thread (unless you're using data binding in which case it's handled for you!). **Changes/Fixes** @@ -83,13 +97,16 @@ * Base language version is now 8.0 (9.0 for Avalonia), but this is only used for nullability compatibility (compile-time), so this shouldn't affect older projects (`.NET 4.6.2`, `netstandard2.0`) and is thus a non-breaking change * Fixed initialization issue in DownloadProgressWindow (WinForms) icon use * Added `JsonAppCastGenerator` to read/write app casts from/to JSON (output json with the app cast generator option `--output-type json` and then set the `AppCastGenerator` on your `SparkleUpdater` object to an instance of `JsonAppCastGenerator`) -* Added `ChannelAppCastFilter` (implements `IAppCastFilter`) for easy way to filter your app cast items by a channel, e.g. `beta` or `alpha`. Use by setting `AppCastHelper.AppCastFilter`. Uses simple `string.Contains` invariant lowercase string check to search for channels in the `AppCastItem`'s version information. - * If you want to allow versions like `2.0.0-beta.1`, set `ChannelSearchNames` to `new List() {"beta"}` +* Added `ChannelAppCastFilter` (implements `IAppCastFilter`) for easy way to filter your app cast items by a channel, e.g. `beta` or `alpha`. Use by setting `AppCastHelper.AppCastFilter`. Uses simple `string.Contains` invariant lowercase string check to search for channels in the `AppCastItem`'s version and/or `Channel` information. + * If you want to allow versions like `2.0.0-beta.1` or channels such as `beta`, set `ChannelSearchNames` to `new List() {"beta"}` * Set `RemoveOlderItems` to `false` if you want to keep old versions when filtering, e.g. for rolling back to an old version * Set `KeepItemsWithNoChannelInfo` to `false` if you want to remove all items that don't match the given channel (doing this will not let people on a beta version update to a non-beta version!) * `AppCast? SparkleUpdater.AppCastCache` holds the most recently deserialized app cast information. -* `AppCastItem` has a new `Channel` property. Use it along with `ChannelAppCastFilter` if you want to use channels that way instead of via your `` property. In the app cast generator, use the `--channel` option to set this (note that using this will set ALL items added to the app cast to that specific channel; to add only new items to your app cast instead of rewriting the whole thing, use the `--reparse-existing` option). +* `AppCastItem` has a new `Channel` property. Use it along with `ChannelAppCastFilter` if you want to use channels that way instead of via your csproj `` property. In the app cast generator, use the `--channel` option to set this (note that using this will set ALL items added to the app cast to that specific channel; to add only new items to your app cast instead of rewriting the whole thing, use the `--reparse-existing` option). * App cast generator has a new `--use-ed25519-signature-attribute` to use `edSignature` in its XML output instead of `signature` (json output unaffected) to match the original Sparkle library +* Added `UpdateDetectedAsync` -- prioritized over `UpdateDetected` if both `UpdateDetectedAsync` and `UpdateDetected` are implemented. +* Fixed calling `CheckForUpdatesAtUserRequest` not showing UI if `UpdateDetected`/`UpdateDetectedAsync` is implemented and the next action is to show the user interface +* `ReleaseNotesGrabber.DownloadReleaseNotes` catches all exceptions instead of just `WebException` ## Updating from 0.X or 1.X to 2.X diff --git a/src/NetSparkle.Samples.Avalonia.MacOS/MainWindow.axaml.cs b/src/NetSparkle.Samples.Avalonia.MacOS/MainWindow.axaml.cs index 28fa9cff..dccf42a7 100644 --- a/src/NetSparkle.Samples.Avalonia.MacOS/MainWindow.axaml.cs +++ b/src/NetSparkle.Samples.Avalonia.MacOS/MainWindow.axaml.cs @@ -6,6 +6,7 @@ using NetSparkleUpdater.Enums; using NetSparkleUpdater.SignatureVerifiers; using System.IO; +using System.Threading.Tasks; namespace NetSparkleUpdater.Samples.Avalonia { @@ -21,17 +22,20 @@ public MainWindow() _sparkle = new CustomSparkleUpdater("https://netsparkleupdater.github.io/NetSparkle/files/sample-app-macos/appcast.xml", new Ed25519Checker(Enums.SecurityMode.Strict, "8zPswEwycU7XQ7OcGQtI/b22pWo1qM2Ual2OhssaDyI=")) { UIFactory = new NetSparkleUpdater.UI.Avalonia.UIFactory(Icon), - // Avalonia version doesn't support separate threads: https://github.com/AvaloniaUI/Avalonia/issues/3434#issuecomment-573446972 - ShowsUIOnMainThread = true, LogWriter = new LogWriter(LogWriterOutputMode.Console) //UseNotificationToast = false // Avalonia version doesn't yet support notification toast messages }; // TLS 1.2 required by GitHub (https://developer.github.com/changes/2018-02-01-weak-crypto-removal-notice/) _sparkle.SecurityProtocolType = System.Net.SecurityProtocolType.Tls12; - _sparkle.StartLoop(true, true); + StartSparkle(); } - public async void ManualUpdateCheck_Click(object sender, RoutedEventArgs e) + private async void StartSparkle() + { + await _sparkle.StartLoop(true, true); + } + + public async Task ManualUpdateCheck_Click(object sender, RoutedEventArgs e) { await _sparkle.CheckForUpdatesAtUserRequest(); } diff --git a/src/NetSparkle.Samples.Avalonia.MacOS/NetSparkle.Samples.Avalonia.MacOS.csproj b/src/NetSparkle.Samples.Avalonia.MacOS/NetSparkle.Samples.Avalonia.MacOS.csproj index 1af3c655..f1e029d4 100644 --- a/src/NetSparkle.Samples.Avalonia.MacOS/NetSparkle.Samples.Avalonia.MacOS.csproj +++ b/src/NetSparkle.Samples.Avalonia.MacOS/NetSparkle.Samples.Avalonia.MacOS.csproj @@ -22,9 +22,9 @@ - - - + + + diff --git a/src/NetSparkle.Samples.Avalonia.MacOSZip/MainWindow.axaml.cs b/src/NetSparkle.Samples.Avalonia.MacOSZip/MainWindow.axaml.cs index 4f78fc36..e4749b01 100644 --- a/src/NetSparkle.Samples.Avalonia.MacOSZip/MainWindow.axaml.cs +++ b/src/NetSparkle.Samples.Avalonia.MacOSZip/MainWindow.axaml.cs @@ -24,8 +24,6 @@ public MainWindow() _sparkle = new CustomSparkleUpdater(appcastToUse, new Ed25519Checker(Enums.SecurityMode.Strict, "8zPswEwycU7XQ7OcGQtI/b22pWo1qM2Ual2OhssaDyI=")) { UIFactory = new NetSparkleUpdater.UI.Avalonia.UIFactory(Icon), - // Avalonia version doesn't support separate threads: https://github.com/AvaloniaUI/Avalonia/issues/3434#issuecomment-573446972 - ShowsUIOnMainThread = true, LogWriter = new LogWriter(), RelaunchAfterUpdate = true, //UseNotificationToast = false // Avalonia version doesn't yet support notification toast messages @@ -41,7 +39,12 @@ public MainWindow() }; // TLS 1.2 required by GitHub (https://developer.github.com/changes/2018-02-01-weak-crypto-removal-notice/) _sparkle.SecurityProtocolType = System.Net.SecurityProtocolType.Tls12; - _sparkle.StartLoop(true, true); + StartSparkle(); + } + + private async void StartSparkle() + { + await _sparkle.StartLoop(true, true); } public async void ManualUpdateCheck_Click(object sender, RoutedEventArgs e) diff --git a/src/NetSparkle.Samples.Avalonia.MacOSZip/NetSparkle.Samples.Avalonia.MacOSZip.csproj b/src/NetSparkle.Samples.Avalonia.MacOSZip/NetSparkle.Samples.Avalonia.MacOSZip.csproj index 2d617580..dca798ed 100644 --- a/src/NetSparkle.Samples.Avalonia.MacOSZip/NetSparkle.Samples.Avalonia.MacOSZip.csproj +++ b/src/NetSparkle.Samples.Avalonia.MacOSZip/NetSparkle.Samples.Avalonia.MacOSZip.csproj @@ -17,9 +17,9 @@ - - - + + + diff --git a/src/NetSparkle.Samples.Avalonia/MainWindow.axaml.cs b/src/NetSparkle.Samples.Avalonia/MainWindow.axaml.cs index 31954688..860a6557 100644 --- a/src/NetSparkle.Samples.Avalonia/MainWindow.axaml.cs +++ b/src/NetSparkle.Samples.Avalonia/MainWindow.axaml.cs @@ -7,6 +7,7 @@ using NetSparkleUpdater.SignatureVerifiers; using NetSparkleUpdater.UI.Avalonia; using System.IO; +using System.Threading.Tasks; namespace NetSparkleUpdater.Samples.Avalonia { @@ -26,16 +27,19 @@ public MainWindow() // use the following property to change the main grid background on the update window. nullable. //UpdateWindowGridBackgroundBrush = new SolidColorBrush(Colors.Purple) }, - // Avalonia version doesn't support separate threads: https://github.com/AvaloniaUI/Avalonia/issues/3434#issuecomment-573446972 - ShowsUIOnMainThread = true, //UseNotificationToast = false // Avalonia version doesn't yet support notification toast messages }; // TLS 1.2 required by GitHub (https://developer.github.com/changes/2018-02-01-weak-crypto-removal-notice/) _sparkle.SecurityProtocolType = System.Net.SecurityProtocolType.Tls12; - _sparkle.StartLoop(true, true); + StartSparkle(); } - public async void ManualUpdateCheck_Click() + private async void StartSparkle() + { + await _sparkle.StartLoop(true, true); + } + + public async Task ManualUpdateCheck_Click() { await _sparkle.CheckForUpdatesAtUserRequest(); } diff --git a/src/NetSparkle.Samples.Avalonia/NetSparkle.Samples.Avalonia.csproj b/src/NetSparkle.Samples.Avalonia/NetSparkle.Samples.Avalonia.csproj index d152db72..294c017b 100644 --- a/src/NetSparkle.Samples.Avalonia/NetSparkle.Samples.Avalonia.csproj +++ b/src/NetSparkle.Samples.Avalonia/NetSparkle.Samples.Avalonia.csproj @@ -23,9 +23,9 @@ - - - + + + diff --git a/src/NetSparkle.Samples.Forms.Multithread/Form1.Designer.cs b/src/NetSparkle.Samples.Forms.Multithread/Form1.Designer.cs new file mode 100644 index 00000000..8fb3be3c --- /dev/null +++ b/src/NetSparkle.Samples.Forms.Multithread/Form1.Designer.cs @@ -0,0 +1,80 @@ +namespace NetSparkle.Samples.Forms.Multithread +{ + partial class Form1 + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1)); + this.checkForUpdatesTimer = new System.Windows.Forms.Timer(this.components); + this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components); + this.ExplicitUserRequestCheckButton = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // checkForUpdatesTimer + // + this.checkForUpdatesTimer.Enabled = true; + // + // notifyIcon1 + // + this.notifyIcon1.BalloonTipIcon = System.Windows.Forms.ToolTipIcon.Info; + this.notifyIcon1.BalloonTipText = "hello"; + this.notifyIcon1.BalloonTipTitle = "hello"; + this.notifyIcon1.Text = "notifyIcon1"; + this.notifyIcon1.Visible = true; + // + // ExplicitUserRequestCheckButton + // + this.ExplicitUserRequestCheckButton.Location = new System.Drawing.Point(12, 40); + this.ExplicitUserRequestCheckButton.Name = "ExplicitUserRequestCheckButton"; + this.ExplicitUserRequestCheckButton.Size = new System.Drawing.Size(212, 23); + this.ExplicitUserRequestCheckButton.TabIndex = 1; + this.ExplicitUserRequestCheckButton.Text = "Check for Update "; + this.ExplicitUserRequestCheckButton.UseVisualStyleBackColor = true; + this.ExplicitUserRequestCheckButton.Click += new System.EventHandler(this.ExplicitUserRequestCheckButton_Click); + // + // Form1 + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(248, 154); + this.Controls.Add(this.ExplicitUserRequestCheckButton); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.Name = "Form1"; + this.Text = "Form1"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Timer checkForUpdatesTimer; + private System.Windows.Forms.NotifyIcon notifyIcon1; + private System.Windows.Forms.Button ExplicitUserRequestCheckButton; + } +} + diff --git a/src/NetSparkle.Samples.Forms.Multithread/Form1.cs b/src/NetSparkle.Samples.Forms.Multithread/Form1.cs new file mode 100644 index 00000000..e09b78ff --- /dev/null +++ b/src/NetSparkle.Samples.Forms.Multithread/Form1.cs @@ -0,0 +1,305 @@ +using System; +using System.Drawing; +using System.IO; +using System.Reflection; +using System.Windows.Forms; +using NetSparkleUpdater; +using NetSparkleUpdater.Enums; +using NetSparkleUpdater.SignatureVerifiers; +using NetSparkleUpdater.UI.WinForms; +using static System.Windows.Forms.VisualStyles.VisualStyleElement; + +namespace NetSparkle.Samples.Forms.Multithread +{ + public partial class Form1 : Form + { + private SparkleUpdater _sparkleUpdateDetector; + private UIFactory _factory; + private UpdateInfo? _updateInfo = null; + private Icon? _icon; + private string? _downloadPath = null; + private bool _hasDownloadFinished = false; + + public Form1() + { + InitializeComponent(); + + var appcastUrl = "https://netsparkleupdater.github.io/NetSparkle/files/sample-app/appcast.xml"; + // set icon in project properties - + string manifestModuleName = System.Reflection.Assembly.GetEntryAssembly()?.ManifestModule.FullyQualifiedName ?? ""; + _icon = System.Drawing.Icon.ExtractAssociatedIcon(manifestModuleName); + _factory = new UIFactory(_icon); + // you can, of course, use your own UIFactory, your own UI objects, etc. This is just a sample! + _sparkleUpdateDetector = new SparkleUpdater(appcastUrl, new DSAChecker(SecurityMode.Strict)) + { + UIFactory = null // so we can handle threads, which is outside the context of SparkleUpdater's use of UIFactory objects + }; + // TLS 1.2 required by GitHub (https://developer.github.com/changes/2018-02-01-weak-crypto-removal-notice/) + _sparkleUpdateDetector.SecurityProtocolType = System.Net.SecurityProtocolType.Tls12; + //_sparkleUpdateDetector.CloseApplication += _sparkleUpdateDetector_CloseApplication; + } + + private void _sparkleUpdateDetector_CloseApplication() + { + Application.Exit(); + } + + private async void AppBackgroundCheckButton_Click(object sender, EventArgs e) + { + // Manually check for updates, this will not show a ui + var result = await _sparkleUpdateDetector.CheckForUpdatesQuietly(); + if (result.Status == UpdateStatus.UpdateAvailable) + { + // if update(s) are found, then we have to trigger the UI to show it gracefully + ShowUpdateWindow(); + } + } + + private CheckingForUpdatesWindow? _checkingForUpdatesWindow; + private UpdateAvailableWindow? _updateAvailableWindow; + private DownloadProgressWindow? _downloadProgressWindow; + + private void ShowCheckingWindow() + { + if (_checkingForUpdatesWindow == null) + { + //var re = new AutoResetEvent(false); // can use this to make an async thread start sync, basically + // overall func won't return until .Set() called. + var t = new Thread(() => + { + var window = _factory.ShowCheckingForUpdates(_sparkleUpdateDetector) as CheckingForUpdatesWindow; + window!.FormClosed += (a, b) => Application.ExitThread(); + window!.Shown += CheckingWindowShown; + _checkingForUpdatesWindow = window; + //re.Set(); + Application.Run(window); + }); + t.SetApartmentState(ApartmentState.STA); // only supported on Windows + t.Start(); + //re.WaitOne(); + } + } + + private void ShowMessage(string title, string message) + { + var t = new Thread(() => + { + var messageWindow = new MessageNotificationWindow(title, message, _icon); + messageWindow.StartPosition = FormStartPosition.CenterScreen; + messageWindow.ShowDialog(); + messageWindow.FormClosed += (a, b) => Application.ExitThread(); + //re.Set(); + Application.Run(messageWindow); + }); + t.SetApartmentState(ApartmentState.STA); + t.Start(); + } + + private async void CheckingWindowShown(object? sender, EventArgs e) + { + _updateInfo = await _sparkleUpdateDetector.CheckForUpdatesAtUserRequest(); + if (_updateInfo != null) + { + switch (_updateInfo.Status) + { + case UpdateStatus.UpdateAvailable: + ShowUpdateWindow(); + break; + case UpdateStatus.UpdateNotAvailable: + ShowMessage("Info", "No update available"); + CloseCheckingForUpdatesWindow(); // could be done once message shown but that's OK for this sample + break; + case UpdateStatus.UserSkipped: + ShowMessage("Info", "User skipped update"); + CloseCheckingForUpdatesWindow(); + break; + case UpdateStatus.CouldNotDetermine: + ShowMessage("Info", "We couldn't tell if there was an update..."); + CloseCheckingForUpdatesWindow(); + break; + } + } + } + + private void CloseCheckingForUpdatesWindow() + { + if (_checkingForUpdatesWindow?.InvokeRequired ?? false) + { + _checkingForUpdatesWindow.Invoke(CloseCheckingForUpdatesWindow); + } + else + { + _checkingForUpdatesWindow?.Close(); + _checkingForUpdatesWindow = null; + } + } + + private void UpdateAvailableWindowShown(object? sender, EventArgs e) + { + CloseCheckingForUpdatesWindow(); + } + + private void ExplicitUserRequestCheckButton_Click(object sender, EventArgs e) + { + ShowCheckingWindow(); + } + + private void ShowUpdateWindow() + { + if (_updateInfo != null && _updateAvailableWindow == null) + { + var t = new Thread(() => + { + var window = _factory.CreateUpdateAvailableWindow(_sparkleUpdateDetector, _updateInfo.Updates, false) as UpdateAvailableWindow; + window!.FormClosed += (a, b) => Application.ExitThread(); + window!.Shown += UpdateAvailableWindowShown; + window.UserResponded += UpdateWindowUserResponded; + _updateAvailableWindow = window; + Application.Run(window); + }); + t.SetApartmentState(ApartmentState.STA); // only supported on Windows + t.Start(); + } + } + + private void ShowDownloadingWindow() + { + if (_updateInfo != null) + { + var t = new Thread(() => + { + var window = _factory.CreateProgressWindow(_sparkleUpdateDetector, _updateInfo.Updates[0]) as DownloadProgressWindow; + window!.FormClosed += (a, b) => Application.ExitThread(); + window!.Shown += ProgressWindowShown; + _downloadProgressWindow = window; + Application.Run(window); + }); + t.SetApartmentState(ApartmentState.STA); // only supported on Windows + t.Start(); + } + } + + private void UpdateWindowUserResponded(object sender, NetSparkleUpdater.Events.UpdateResponseEventArgs e) + { + if (_updateAvailableWindow != null && _updateInfo != null) + { + if (e.Result == UpdateAvailableResult.InstallUpdate) + { + ShowDownloadingWindow(); + } + } + } + + private void CloseUpdateAvailableWindow() + { + if (_updateAvailableWindow?.InvokeRequired ?? false) + { + _updateAvailableWindow.Invoke(CloseUpdateAvailableWindow); + } + else + { + _updateAvailableWindow?.Close(); + _updateAvailableWindow = null; + } + } + + private async void ProgressWindowShown(object? sender, EventArgs e) + { + if (_updateInfo != null && _downloadProgressWindow != null) + { + // we want to download the item + _sparkleUpdateDetector.DownloadFinished -= _sparkle_FinishedDownloading; + _sparkleUpdateDetector.DownloadFinished += _sparkle_FinishedDownloading; + + _sparkleUpdateDetector.DownloadHadError -= _sparkle_DownloadError; + _sparkleUpdateDetector.DownloadHadError += _sparkle_DownloadError; + + _sparkleUpdateDetector.DownloadMadeProgress += _sparkle_DownloadMadeProgress; + _downloadProgressWindow.DownloadProcessCompleted += DownloadProgressWindow_DownloadProcessCompleted; + + _hasDownloadFinished = false; + await _sparkleUpdateDetector.InitAndBeginDownload(_updateInfo.Updates.First()); + } + CloseUpdateAvailableWindow(); + } + + private void CloseDownloadProgressWindow() + { + if (_downloadProgressWindow?.InvokeRequired ?? false) + { + _downloadProgressWindow.Invoke(CloseDownloadProgressWindow); + } + else + { + _downloadProgressWindow?.Close(); + _downloadProgressWindow = null; + } + } + + private void _sparkle_DownloadMadeProgress(object sender, AppCastItem item, NetSparkleUpdater.Events.ItemDownloadProgressEventArgs e) + { + if (_downloadProgressWindow?.InvokeRequired ?? false) + { + _downloadProgressWindow.Invoke(() => _sparkle_DownloadMadeProgress(sender, item, e)); + } + else + { + if (!_hasDownloadFinished) + { + _downloadProgressWindow?.OnDownloadProgressChanged(sender, e); + } + } + } + + private void _sparkle_DownloadError(AppCastItem item, string? path, Exception exception) + { + if (_downloadProgressWindow?.InvokeRequired ?? false) + { + _downloadProgressWindow.Invoke(() => _sparkle_DownloadError(item, path, exception)); + } + else + { + _downloadProgressWindow?.DisplayErrorMessage("We had an error during the download process :( -- " + exception.Message); + } + } + + private void _sparkle_FinishedDownloading(AppCastItem item, string path) + { + if (_downloadProgressWindow?.InvokeRequired ?? false) + { + _downloadProgressWindow.Invoke(() => _sparkle_FinishedDownloading(item, path)); + } + else + { + if (_updateInfo != null) + { + _hasDownloadFinished = true; + var updateSize = _updateInfo.Updates.First().UpdateSize; + var validationRes = _sparkleUpdateDetector.SignatureVerifier.VerifySignatureOfFile( + _updateInfo.Updates.First().DownloadSignature ?? "", path); + bool isSignatureInvalid = validationRes == ValidationResult.Invalid; + _downloadProgressWindow?.FinishedDownloadingFile(isDownloadedFileValid: !isSignatureInvalid); + _downloadPath = path; + } + } + } + + private async void DownloadProgressWindow_DownloadProcessCompleted(object sender, NetSparkleUpdater.Events.DownloadInstallEventArgs args) + { + if (args.ShouldInstall && _updateInfo != null) + { + _sparkleUpdateDetector.CloseApplication += _sparkle_CloseApplication; + await _sparkleUpdateDetector.InstallUpdate(_updateInfo.Updates.First(), _downloadPath); + } + else + { + CloseDownloadProgressWindow(); + } + } + + private void _sparkle_CloseApplication() + { + Application.Exit(); + } + } +} diff --git a/src/NetSparkle.Samples.Forms.Multithread/Form1.resx b/src/NetSparkle.Samples.Forms.Multithread/Form1.resx new file mode 100644 index 00000000..b26887ff --- /dev/null +++ b/src/NetSparkle.Samples.Forms.Multithread/Form1.resx @@ -0,0 +1,312 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 17, 17 + + + 193, 17 + + + + + AAABAAIAEBAAAAAAIABoBAAAJgAAADAwAAAAACAAqCUAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAE + AADXDQAA1w0AAAAAAAAAAAAA////AP///wD///8A////AP///wD///8A////AABczlIAXM5i////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AABczg8BXs/tAV3P9ABc + zhf///8A////AP///wD///8A////AP///wD///8A////AABczmIAXM5rAFzOMwBczgUAXc+fDW3a9xBx + 3fkBXtCwAFzOAwBczjAAXM5nAFzOb////wD///8A////AP///wAAXM50A1/Q/Adi0PoBXs/6A1/Q+h+I + 8v8givP/BWHR+QFez/kGYdD7BGHR/ABczoz///8A////AP///wD///8AAFzOPAVh0fcji/b/I4fw/yCF + 7v8Xhfb/E4P2/yGF7/8ih/D/I4v2/whl1PMAXM5Q////AP///wD///8A////AABczgkAXM78K43y/xWG + 9v8Thfb/a7T6/2q0+v8Thfb/EoT1/y+S9f8BXc7+AFzOFv///wD///8A////AABczhUAXc+nB2LQ+jST + 8f8fjff/bbf7/83q///N6v//bbf7/xmK9v83lvT/CmTS+QFe0LMAXM4a////AABczmMCXs/zIHrf+EWf + 9v80mvj/crr7/83q///N6v//zer//83q//9xuvv/MZj4/0ah9v8lf+H6Al7P9gBczm8AXM5jAV3P8yJ7 + 3/hOpvf/QaL5/x+S+P8bkPj/zer//83q//8bkPj/HZH4/z2h+f9QqPf/JoDi+gJez/YAXM5v////AABc + zhUBXs+nCGPR+lKm9P85ofn/IZb4/83q///N6v//IZb4/zKe+f9Xq/X/C2XT+QJf0LMAXM4a////AP// + /wD///8AAFzOCQBdzvxisvb/Qaj6/0Cn+v+/5P7/v+T+/0Cn+v8+pvr/aLj5/wNez/4AXM4W////AP// + /wD///8A////AABczjwQaNP3dcH8/2y49v9ms/T/Xrf7/1e0+/9otPX/a7f2/3TB+/8cc9j0AFzOUP// + /wD///8A////AP///wAAXM50CmPR/BJp0/oCXs/6CmTR+nG9+f90wPr/EGjS+QFdz/kPZ9L7DWbS/ABc + zoz///8A////AP///wD///8AAFzOYgBczmsAXM4zAFzOBQJfz54xhd/4PI7j+QJf0LAAXM4DAFzOMABc + zmcAXM5v////AP///wD///8A////AP///wD///8A////AP///wAAXM4PAl/P7QRfz/QAXM4X////AP// + /wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AABczlAAXM5g////AP// + /wD///8A////AP///wD///8A////AP5/AAD8PwAAwAMAAMADAADAAwAAwAMAAIABAAAAAAAAAAAAAIAB + AADAAwAAwAMAAMADAADAAwAA/D8AAP5/AAAoAAAAMAAAAGAAAAABACAAAAAAAAAkAAASCwAAEgsAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA + AAEAAAACAAAAAwAAAAMAAAAEAAAABQAAAAYAAAAHAAAABwAAAAcAAAAIAAAACAAAAAkAAAAJAAAACAAA + AAgAAAAHAAAABwAAAAcAAAAGAAAABQAAAAQAAAADAAAAAwAAAAIAAAABAAAAAQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAEAAAABAAAAAgAAAAMAAAAEAAAABQAAAAYAAAAHAAAACQAAAAoAAAALAAAADAAAAA0AAAANAAAADgAA + AA4AAAAOAAAADgAAAA0AAAANAAAADAAAAAsAAAAKAAAACQAAAAcAAAAGAAAABQAAAAQAAAADAAAAAgAA + AAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAQAAAAIAAAADAAAAAwAAAAUAAAAHAAAACAAAAAoAAAAMAAAADQAAAA8AAAARAAAAEgAA + ABQAAAAVAEacRQBOr2IAAAAWAAAAFQAAABUAAAAUAAAAEgAAABEAAAAPAAAADQAAAAwAAAAKAAAACAAA + AAcAAAAFAAAAAwAAAAMAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMAAAAEAAAABgAAAAgAAAAKAAAADAAAAA8AAAARAAAAFAAA + ABYAAAAZAAAAGwAAABwACxkiAVzL3AJezfEAIkwuAAAAHgAAAB4AAAAcAAAAGwAAABkAAAAWAAAAFAAA + ABEAAAAPAAAADAAAAAoAAAAIAAAABgAAAAQAAAADAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAwAAAAQAAAAGAAAACQAAAAsAAAAOAAAAEQAA + ABUAAAAYAAAAGwAAAB4AAAAhAAAAJAAAACYAT7CNDGjU+BJv2PkBVb2wAAAAKAAAACcAAAAmAAAAJAAA + ACEAAAAeAAAAGwAAABgAAAAVAAAAEQAAAA4AAAALAAAACQAAAAYAAAAEAAAAAwAAAAIAAAABAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAACAAAABAAAAAYAAAAJAAAADAAA + ABAAAAATAAAAFwAAABsAAAAfAAAAIwAAACYAAAAqAAAALQArYUwCXc76Jozw/yqR8/8GYc/8ADqCZwAA + ADIAAAAvAAAALQAAACoAAAAmAAAAIwAAAB8AAAAbAAAAFwAAABMAAAAQAAAADAAAAAkAAAAGAAAABAAA + AAIAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAIAAAADAAAABQAA + AAgAAAAMAAAADwAAABQAAAAYAAAAHQAAACEAAAAmAAAAKgAAAC4AAAAyAAAANgFYw9MaeN/8JpT4/yGR + 9/8fgOX+AlzJ6QALGUEAAAA5AAAANgAAADIAAAAuAAAAKgAAACYAAAAhAAAAHQAAABgAAAAUAAAADwAA + AAwAAAAIAAAABQAAAAMAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA + AAMAAAAEAAAABwAAAAoAAAAOAAAAEgAAABcAAAAcAAAAIQAAACYAAAAsAAAAMAAAADUAAAA5AEOWiAlj + 0foqk/X/Fov3/xOJ9/8plPf/EGrU+QBMqKgAAABAAAAAPQAAADkAAAA1AAAAMAAAACwAAAAmAAAAIQAA + ABwAAAAXAAAAEgAAAA4AAAAKAAAABwAAAAQAAAADAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAABAAAAAQAAAAMAAAAFAFO6QABIoSwAAAAQAAAAFQAAABoAAAAfAAAAJQAAACoAAAAvAAAANAAA + ADkAHD5QAV3N9yWH6/8djvf/EIf3/xCH9/8Xi/f/KY3w/wRfz/wAKVxnAAAAQgAAAD4AAAA5AAAANAAA + AC8AAAAqAAAAJQAAAB8AAAAaADZ6LgBOr08AAAALAAAACAAAAAUAAAADAAAAAQAAAAEAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMAAAAFAFrJkQJdzv8DX8/xAVvJvwBSuIcAQpVZABUwLwAA + ACwAAAAyAAAANwAAADwBVLvFFnLb+ieR+P8Ohff/DoX3/w6F9/8Ohff/Io/4/x574v0AWMTfAAMGRgAA + AEAAAAA8AAAANwAMGjgAOoFbAE6uhwBYxLsCXs7tA17P/wBaybYAAAAMAAAACAAAAAUAAAADAAAAAgAA + AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgAAAAMAAAAFAFbCWQZj0vcpi+//I4Hm/xl0 + 2/gQaNL5B2DP/gJcyuAAUretAESXgABEmIoGYc/8KpD0/xKG9v8Lg/b/C4P2/wuD9v8Lg/b/DoT2/ymR + 9f8NZ9L6AEiiogA+i4EATq+rAVrH3AZgz/wNZdH7GHLZ+CF/5f8piu7/DWvY+ABVv34AAAAMAAAACAAA + AAUAAAADAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAAMAAAAFAEihIQBc + zv8oj/X/G4r3/xyL9/8kj/f/Ko3y/yiG6v8feuH8FG3V+A9m0f4jguj/HYv3/wmB9v8Jgfb/CYH2/wmB + 9v8Jgfb/CYH2/xeI9v8miO3/EGbR/xNr1PgdeOD8JoXp/ymM8f8lj/f/HYv3/xiJ9/8sk/f/BGDQ+gBM + q0gAAAALAAAACAAAAAUAAAADAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAA + AAIAAAAEAAAABwBdz+ofhOz/GYj3/wd/9v8Hf/b/CID2/w+D9v8YiPf/H4z3/yWN9f8Ziff/CYD2/wd/ + 9v8Hf/b/B3/2/wd/9v8Hf/b/B3/2/wd/9v8Xh/f/JIz1/yCM9/8ZiPf/EIT2/wiA9v8Hf/b/B3/2/xOF + 9v8ni/H/AFzO/AAiTBYAAAAKAAAABwAAAAQAAAACAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAQAAAAIAAAADAAAABQBczK4WeOL/IIv2/wR99f8EffX/BH31/wR99f8EffX/BH31/wR9 + 9f8EffX/BH31/wR99f8EffX/BH31/wR99f8EffX/BH31/wR99f8EffX/BH31/wR99f8EffX/BH31/wR9 + 9f8EffX/BH31/xmI9/8df+n/AF3O1gAAAAsAAAAIAAAABQAAAAMAAAACAAAAAQAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAACAAAABABZyG8Ladj1KI/2/wJ79f8Ce/X/Anv1/wJ7 + 9f8Ce/X/Anv1/wJ79f8Ce/X/Anv1/wJ79f8Ce/X/Anv1/wJ79f8Ce/X/Anv1/wJ79f8Ce/X/Anv1/wJ7 + 9f8Ce/X/Anv1/wJ79f8Ce/X/Anv1/yGL9v8Uc9/8AFnHlwAAAAkAAAAGAAAABAAAAAIAAAABAAAAAQAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAACAAAAAwBWwjUCXs/8LZD2/wd9 + 9f8BevX/AXn1/wB59f8AefX/AHn1/wB59f8AefX/AHn1/wB59f8AefX/AHn1/xSF9v8AefX/AHn1/wB5 + 9f8AefX/AHn1/wB59f8BefX/AXn1/wF69f8BevX/Anv1/yuQ9/8KZtX1AFfCXAAAAAYAAAAEAAAAAwAA + AAIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAgA1 + dwcAXM/4KIrw/xSE9v8De/X/A3v1/wJ79f8Ce/X/Anv1/wJ79f8Ce/X/Anv1/wJ79f8Ce/X/RaT6/43P + /v9NqPn/A3z1/wJ79f8Ce/X/Anv1/wJ79f8Ce/X/A3v1/wN79f8De/X/C3/1/y+R9P8CXc7/AE6vIwAA + AAQAAAADAAAAAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAQAAAAEAXc/IIH/o/yCL9/8Fffb/BX32/wV99v8Fffb/BX32/wV99v8Fffb/BX32/wuA + 9v9gtPv/jtD+/47Q/v+O0P7/YLT7/wmA9v8Fffb/BX32/wV99v8Fffb/BX32/wV99v8Fffb/GYf3/ymJ + 7v8AXc/tAAAAAwAAAAIAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAXM2XGHTe+iyS9/8Hf/b/B3/2/wd/9v8Hf/b/B3/2/wd/ + 9v8Hf/b/F4j3/3TA/P+O0P7/jtD+/47Q/v+O0P7/jtD+/2+9/P8Rhff/B3/2/wd/9v8Hf/b/B3/2/wd/ + 9v8Hf/b/I473/yJ/5v8AXc7EAAAAAgAAAAEAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzOFABdz6YHYdD8Lojo/iKO9/8Jgfb/CYH2/wmB + 9v8Jgfb/CYH2/wmB9v8plPj/g8n9/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q/v96xf3/HIz3/wmB + 9v8Jgfb/CYH2/wmB9v8Jgfb/GIn3/zWP7f8LZdL6AF3PwQBcziIAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXM5fAV7P8hx02/k7l/P/LJT3/w+E + 9v8Mg/b/DIP2/wyD9v8Mg/b/DIP2/0Ci+v+Lz/7/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q + /v+O0P7/hMr9/ymU+P8Mg/b/DIP2/wyD9v8Mg/b/DYP2/yeR9/89mfX/I3zf+wNf0PoAXM54AFzOBAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzOJQFe0MUMZdL5NI3r/zyc + +f8djfj/DoX3/w6F9/8Ohff/DoX3/w6F9/8Qhvf/WbH7/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q + /v+O0P7/jtD+/47Q/v+O0P7/jtD+/4rN/v85nvr/DoX3/w6F9/8Ohff/DoX3/w6F9/8Xivf/OJr4/zqT + 7/8TbNX4AV7Q2ABczjYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABczgUAXc9+Al7P+yZ+ + 4PxEnvb/Lpb4/xKI9/8Qh/f/EIf3/xCH9/8Qh/f/EIf3/xiM+P9qvPz/is7+/4rO/v+Lz/7/jtD+/47Q + /v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/i8/+/4rO/v+Kzv7/Rqf6/xCH9/8Qh/f/EIf3/xCH + 9/8Qh/f/EIf3/yeT+P9Dn/f/LYXl/QZgz/0AXc+VAFzODQAAAAAAAAAAAAAAAAAAAAAAAAAAAFzOOwFe + 0N0TbNb4Ppjw/z+f+f8dj/f/Eon3/xKJ9/8Siff/Eon3/xKJ9/8Siff/Eon3/xKJ9/8Siff/Eon3/xKJ + 9/89ovr/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/OZ/6/xKJ9/8Siff/Eon3/xKJ + 9/8Siff/Eon3/xKJ9/8Siff/Eon3/xKJ9/8ZjPf/OJz4/0Oc8/8cdNn5AV3P6wBczk4AAAAAAAAAAAAA + AAAAXM4QAFzOzQNez/44k+7/Taf5/y2X+P8Vi/f/FYv3/xWL9/8Vi/f/FYv3/xWL9/8Vi/f/FYv3/xWL + 9/8Vi/f/FYv3/xWL9/8/o/r/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/O6D6/xWL + 9/8Vi/f/FYv3/xWL9/8Vi/f/FYv3/xWL9/8Vi/f/FYv3/xWL9/8Vi/f/JJL4/0ml+f9AmfH/CmTS/QBd + z98AXM4iAAAAAAAAAAAAAAAAAFzOAwBcznIAXc/4Hnnf+kqk9/9Dovn/H5H4/xeN+P8Xjfj/F434/xeN + +P8Xjfj/F434/xeN+P8Xjfj/F434/xeN+P9ApPr/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q + /v+O0P7/PaL6/xeN+P8Xjfj/F434/xeN+P8Xjfj/F434/xeN+P8Xjfj/F434/xyP+P86nvn/Tqf4/yiD + 5PsBXs/7AF3PiABczgkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXM4eAV7PuAZh0fk2j+v/Uqv6/zWd + +f8aj/j/GY/4/xmP+P8Zj/j/GY/4/xmP+P8Zj/j/GY/4/xmP+P9Bpvr/jtD+/47Q/v+O0P7/jtD+/47Q + /v+O0P7/jtD+/47Q/v+O0P7/PqP6/xmP+P8Zj/j/GY/4/xmP+P8Zj/j/GY/4/xmP+P8Zj/j/Lpn5/06p + +v9AmO//DGbT+AFe0M4AXM4sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABc + zlIBXs/rGXPa+E2k9P9Nqfr/KJf4/xyR+P8ckfj/HJH4/xyR+P8ckfj/HJH4/xyR+P9Dp/r/jtD+/47Q + /v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/QKX6/xyR+P8ckfj/HJH4/xyR+P8ckfj/HJH4/yOU + +P9Hpvn/Uqj2/yN83voBXc/1AFzOaQBczgEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAXM4NAV3PlgNf0Pwyi+b9WK75/zGc+f8ek/j/HpP4/x6T+P8ek/j/HpP4/x6T + +P9Ep/r/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q/v+O0P7/QKb6/x6T+P8ek/j/HpP4/x6T + +P8ek/j/J5f4/1iu+v89k+v/CGLQ+wFe0LAAXM4ZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABczjUBXtDcLoLg+0yq+v8glfn/IJX5/yCV + +f8glfn/IJX5/yCV+f8sm/r/i87+/47Q/v+O0P7/jtD+/47Q/v+O0P7/jtD+/47Q/v+Jzf7/KZn5/yCV + +f8glfn/IJX5/yCV+f8glfn/QKT6/z+R5/8BXs/tAFzOSwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABXc+hMYnl/U6s + +v8il/n/Ipf5/yKX+f8il/n/Ipf5/yKX+f8il/n/K5z5/z2l+/89pfv/PaX7/z2l+/89pfv/PaX7/z2l + +v8pm/n/Ipf5/yKX+f8il/n/Ipf5/yKX+f8il/n/Qqb6/0KX7P8CX9DPAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAACX9DhSZ7u/0Sn+v8lmfn/JZn5/yWZ+f8lmfn/JZn5/yWZ+f8lmfn/JZn5/yWZ+f8lmfn/JZn5/yWZ + +f8lmfn/JZn5/yWZ+f8lmfn/JZn5/yWZ+f8lmfn/JZn5/yWZ+f8lmfn/OKL6/1qr9P8AXc77AFzOBgAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABczhIAXM7+YLH3/zij+f8nm/n/J5v5/yeb+f8nm/n/J5v5/yeb+f8nm/n/J5v5/yeb + +f8nm/n/J5v5/yeb+f8nm/n/J5v5/yeb+f8nm/n/J5v5/yeb+f8nm/n/J5v5/yeb+f8nm/n/K535/2e4 + +v8LZNH7AFzONwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAABczksQadP4arv8/yyf+v8pnfr/KZ36/ymd+v8pnfr/KZ36/ymd + +v8pnfr/KZ36/ymd+v8pnfr/KZ36/ymd+v8pnfr/KZ36/ymd+v8pnfr/KZ36/ymd+v8pnfr/KZ36/ymd + +v8pnfr/KZ36/2C2+/8le9z3AFzOcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABczoUsg+D6Yrf7/yyf+v8sn/r/LJ/6/yyf + +v8sn/r/LJ/6/yyf+v8sn/r/LJ/6/yyf+v8sn/r/LJ/6/yyf+v8sn/r/LJ/6/yyf+v8sn/r/LJ/6/yyf + +v8sn/r/LJ/6/yyf+v8sn/r/LJ/6/1Wy+/8+k+f/Al7QrwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJf0MlGm+v/VrP7/y6h + +v8uofr/LqH6/y6h+v8wovr/Paj6/0es+/80pPr/LqH6/y6h+v8uofr/LqH6/y6h+v8uofr/LqH6/y6h + +v8wovr/Rav7/z+p+v8xovr/LqH6/y6h+v8uofr/LqH6/0mt+/9ZqfH/Al7Q6gAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzOBAFd + z/lhsfT/Sa77/zam+v9Grfv/VbT7/2W6+/9xv/r/arT0/1ul7P9vu/j/Q6z6/zCj+v8wo/r/MKP6/zCj + +v8wo/r/MKP6/zmn+v9vvfr/XKXs/2ay8/9xvvr/aLz7/1m1+/9Kr/v/O6j6/z2p+v9wvfn/A17P/wBc + zhsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAFzOMwZg0Px2wvz/csH8/3K9+P9irvH/SJfn/S1+2/YSaNH+A1/P8wFez+M1iOH6br/8/zWm + +/8ypfv/MqX7/zKl+/8ypfv/MqX7/2W7/P9JmOj9Al/P6wJez+4OZdH/J3ra9kWU5vxeqvD/crv4/3TC + /P93w/z/GXDW9wBczlUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAFzObRBo0/w9jOL4H3LW+Qhh0P0DYNDaAV3PmgBczl8AXM4nAFzOAQBc + zhgCX8/1X63x/1q3/P81p/v/Naf7/zWn+/81p/v/TrL7/2249f8IYdD8AFzOMwAAAAAAXM4eAFzOVwBd + zpEDYNDTB2HP+xtv1fo4ieD3HnTX/QBdzo8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzOkwJf0LoAXM56AFzOQQBczgwAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAXM5jFmzU+HfC+v9Er/v/N6n7/zep+/88q/v/dML8/yl82/gBXc+NAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAFzOBwBczjoAXM5yAl/QsgBdzrEAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAl/QxkOV5vxvwvz/Oqv7/zmr+/9jvfz/WKTs/gJf + z+QAXM4KAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFzOJAVg0PltuPX/Wbn8/020 + /P92wPj/DmbR+wBczkQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFd + zngjeNn4e8f8/3fH/f84iOD4Al7PowAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAABczgQCX9DXUqHr/WSv8f8EX8/vAFzOEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAXM4zCmTR+xZr1PkAXM5XAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV7PjwJfz7kAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAD/AAD/wAAAAD8AAP+A + AAAAHwAA/wAAAAAPAAD+AAAAAAcAAPwAAAAAAwAA/AAAAAADAAD8AAAAAAMAAPgAAAAAAQAA+AAAAAAB + AAD4AAAAAAEAAPgAAAAAAQAA/AAAAAADAAD8AAAAAAMAAPwAAAAAAwAA/gAAAAAHAAD/AAAAAA8AAP+A + AAAAHwAA/8AAAAA/AAD/gAAAAH8AAP8AAAAAHwAA/AAAAAAPAADwAAAAAAMAAOAAAAAAAQAAwAAAAAAA + AADgAAAAAAEAAPgAAAAABwAA/gAAAAAPAAD/AAAAAD8AAP/AAAAA/wAA/+AAAAH/AAD/4AAAAP8AAP/A + AAAA/wAA/8AAAAD/AAD/wAAAAP8AAP/AAAAA/wAA/4AAAAB/AAD/gAAAAH8AAP+AAAEAfwAA/4PwA/B/ + AAD///gD//8AAP//+Af//wAA///8D///AAD///wP//8AAP///h///wAA////P///AAD///////8AAP// + /////wAA + + + \ No newline at end of file diff --git a/src/NetSparkle.Samples.Forms.Multithread/NetSparkle.Samples.Forms.Multithread.csproj b/src/NetSparkle.Samples.Forms.Multithread/NetSparkle.Samples.Forms.Multithread.csproj new file mode 100644 index 00000000..328478ef --- /dev/null +++ b/src/NetSparkle.Samples.Forms.Multithread/NetSparkle.Samples.Forms.Multithread.csproj @@ -0,0 +1,36 @@ + + + WinExe + net8.0-windows + enable + true + enable + software-update-available.ico + false + + + + + + + + + + + + + + + + Form + + + Form1.cs + + + + + Form1.cs + + + \ No newline at end of file diff --git a/src/NetSparkle.Samples.Forms.Multithread/NetSparkle.Samples.Forms.Multithread.csproj.user b/src/NetSparkle.Samples.Forms.Multithread/NetSparkle.Samples.Forms.Multithread.csproj.user new file mode 100644 index 00000000..317a8c7b --- /dev/null +++ b/src/NetSparkle.Samples.Forms.Multithread/NetSparkle.Samples.Forms.Multithread.csproj.user @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/NetSparkle.Samples.Forms.Multithread/NetSparkle_DSA.pub b/src/NetSparkle.Samples.Forms.Multithread/NetSparkle_DSA.pub new file mode 100644 index 00000000..84f22b0c --- /dev/null +++ b/src/NetSparkle.Samples.Forms.Multithread/NetSparkle_DSA.pub @@ -0,0 +1 @@ +

2IX45gCGAGjNXBg3/spd03M2oMsmghmul/H5ePY9AN9URjAOYUcwfi3kQ/KM8i2eD1V/cBKkMJX4xx3kpt39n2Vt+7tyWfXjzMb+3qPapPHMMJL5DDS9cw6FgG3H0tT/LSm7Qn7JLA6w8FOiVIjC+x61u5WqFK0I0vFnc8T1YZs=

xXaw/HrlbsRqg7WD5g1xlkVblys=P2bv3DOTGnyjVnDC5j6Pwd4U6VtRzhPsww9COnwlRWmyNFcf0F/3H5S9YJrO7P+CgIvef30eqUYPHUauLbZ2UK0rItwepz45SfysLvVUb6PU6z/j7gEYiGO/lKd13GfhxmL1mS+XasDSw5brRMxePidiom8H/AB7VVQgELcgutQ=mxJD6rwuJe532PpS9HKjlhvcY1ssBEOFf3HmiYot4d670y0+5nICF3YO8vLxndHA4RqgU/Hux9KtKKg/2hi7c/88j5YyTDWX86n8aPIxHWklKsreP24ul87uuktfhDlhltpfd8AlXxn2SpxQ1Fy/Ktid3E1+uDb64HKf9a35CwE=q/C5yP0VC0Y0ZUPzSD5+O5Rt5X8=sg==
\ No newline at end of file diff --git a/src/NetSparkle.Samples.Forms.Multithread/Program.cs b/src/NetSparkle.Samples.Forms.Multithread/Program.cs new file mode 100644 index 00000000..7bb60448 --- /dev/null +++ b/src/NetSparkle.Samples.Forms.Multithread/Program.cs @@ -0,0 +1,17 @@ +namespace NetSparkle.Samples.Forms.Multithread +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + Application.Run(new Form1()); + } + } +} \ No newline at end of file diff --git a/src/NetSparkle.Samples.Forms.Multithread/software-update-available.ico b/src/NetSparkle.Samples.Forms.Multithread/software-update-available.ico new file mode 100644 index 00000000..9d1af8d3 Binary files /dev/null and b/src/NetSparkle.Samples.Forms.Multithread/software-update-available.ico differ diff --git a/src/NetSparkle.Samples.NetCore.WPF/MainWindow.xaml.cs b/src/NetSparkle.Samples.NetCore.WPF/MainWindow.xaml.cs index 8ec10b63..b5fe8fc8 100644 --- a/src/NetSparkle.Samples.NetCore.WPF/MainWindow.xaml.cs +++ b/src/NetSparkle.Samples.NetCore.WPF/MainWindow.xaml.cs @@ -40,13 +40,23 @@ public MainWindow() _sparkle = new SparkleUpdater("https://netsparkleupdater.github.io/NetSparkle/files/sample-app/appcast.xml", new DSAChecker(Enums.SecurityMode.Strict)) { UIFactory = new NetSparkleUpdater.UI.WPF.UIFactory(NetSparkleUpdater.UI.WPF.IconUtilities.ToImageSource(icon)), - ShowsUIOnMainThread = false, //RelaunchAfterUpdate = true, //UseNotificationToast = true }; // TLS 1.2 required by GitHub (https://developer.github.com/changes/2018-02-01-weak-crypto-removal-notice/) _sparkle.SecurityProtocolType = System.Net.SecurityProtocolType.Tls12; - _sparkle.StartLoop(true, true); + StartSparkle(); + } + + private async void StartSparkle() + { + _sparkle.UpdateDetected += _sparkle_UpdateDetected; + await _sparkle.StartLoop(true, true); + } + + private void _sparkle_UpdateDetected(object sender, Events.UpdateDetectedEventArgs e) + { + //e.NextAction = Enums.NextUpdateAction.PerformUpdateUnattended; // uncomment to see how the app will auto-update due to loop } private async void ManualUpdateCheck_Click(object sender, RoutedEventArgs e) diff --git a/src/NetSparkle.Samples.NetCore.WPF/NetSparkle.Samples.NetCore.WPF.csproj b/src/NetSparkle.Samples.NetCore.WPF/NetSparkle.Samples.NetCore.WPF.csproj index e712b1b5..9bcb1938 100644 --- a/src/NetSparkle.Samples.NetCore.WPF/NetSparkle.Samples.NetCore.WPF.csproj +++ b/src/NetSparkle.Samples.NetCore.WPF/NetSparkle.Samples.NetCore.WPF.csproj @@ -2,7 +2,7 @@ WinExe - net7.0-windows + net8.0-windows true software-update-available.ico false diff --git a/src/NetSparkle.Samples.NetCore.WinForms/Form1.cs b/src/NetSparkle.Samples.NetCore.WinForms/Form1.cs index 524f0e6e..da2e5d50 100644 --- a/src/NetSparkle.Samples.NetCore.WinForms/Form1.cs +++ b/src/NetSparkle.Samples.NetCore.WinForms/Form1.cs @@ -31,7 +31,12 @@ public Form1() // TLS 1.2 required by GitHub (https://developer.github.com/changes/2018-02-01-weak-crypto-removal-notice/) _sparkleUpdateDetector.SecurityProtocolType = System.Net.SecurityProtocolType.Tls12; //_sparkleUpdateDetector.CloseApplication += _sparkleUpdateDetector_CloseApplication; - _sparkleUpdateDetector.StartLoop(true, true); + StartSparkle(); + } + + private async void StartSparkle() + { + await _sparkleUpdateDetector.StartLoop(true, true); } private void _sparkleUpdateDetector_CloseApplication() @@ -50,9 +55,9 @@ private async void AppBackgroundCheckButton_Click(object sender, EventArgs e) } } - private void ExplicitUserRequestCheckButton_Click(object sender, EventArgs e) + private async void ExplicitUserRequestCheckButton_Click(object sender, EventArgs e) { - _sparkleUpdateDetector.CheckForUpdatesAtUserRequest(); + await _sparkleUpdateDetector.CheckForUpdatesAtUserRequest(); } } } diff --git a/src/NetSparkle.Samples.NetCore.WinForms/NetSparkle.Samples.NetCore.WinForms.csproj b/src/NetSparkle.Samples.NetCore.WinForms/NetSparkle.Samples.NetCore.WinForms.csproj index 0b1c2ad0..894309ca 100644 --- a/src/NetSparkle.Samples.NetCore.WinForms/NetSparkle.Samples.NetCore.WinForms.csproj +++ b/src/NetSparkle.Samples.NetCore.WinForms/NetSparkle.Samples.NetCore.WinForms.csproj @@ -2,7 +2,7 @@ WinExe - net7.0-windows + net8.0-windows true software-update-available.ico false diff --git a/src/NetSparkle.Samples.NetFramework.WPF/MainWindow.xaml.cs b/src/NetSparkle.Samples.NetFramework.WPF/MainWindow.xaml.cs index 3a246c1d..8fa99198 100644 --- a/src/NetSparkle.Samples.NetFramework.WPF/MainWindow.xaml.cs +++ b/src/NetSparkle.Samples.NetFramework.WPF/MainWindow.xaml.cs @@ -41,7 +41,6 @@ public MainWindow() TextBlock.SetFontStyle(window, FontStyles.Italic); } }, - ShowsUIOnMainThread = false, //RelaunchAfterUpdate = true, //UseNotificationToast = true }; diff --git a/src/NetSparkle.UI.Avalonia/Controls/BaseWindow.cs b/src/NetSparkle.UI.Avalonia/Controls/BaseWindow.cs index a953e72b..98dd6d72 100644 --- a/src/NetSparkle.UI.Avalonia/Controls/BaseWindow.cs +++ b/src/NetSparkle.UI.Avalonia/Controls/BaseWindow.cs @@ -15,27 +15,17 @@ namespace NetSparkleUpdater.UI.Avalonia.Controls /// public class BaseWindow : Window { - /// - /// Whether or not this Window is on the main thread or not - /// - protected bool _isOnMainThread; /// /// Whether or not the close window code has been called yet /// (so that it is not called more than one time). /// protected bool _hasInitiatedShutdown = false; - - /// - /// Cancellation token used when showing this window on the main UI dispatcher - /// - protected CancellationTokenSource _cancellationTokenSource; /// /// Public, default construtor for BaseWindow objects /// public BaseWindow() { - _cancellationTokenSource = new CancellationTokenSource(); _hasInitiatedShutdown = false; } @@ -51,45 +41,24 @@ public BaseWindow(bool useClosingEvent) { Closing += BaseWindow_Closing; } - _cancellationTokenSource = new CancellationTokenSource(); _hasInitiatedShutdown = false; } private void BaseWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) { Closing -= BaseWindow_Closing; - if (!_isOnMainThread && !_hasInitiatedShutdown) + if (!_hasInitiatedShutdown) { _hasInitiatedShutdown = true; - _cancellationTokenSource.Cancel(); } } /// /// Show this window to the user. /// - /// true if the window is being shown while - /// on the main thread; false otherwise - protected void ShowWindow(bool isOnMainThread) + protected void ShowWindow() { - - try - { - Show(); - _isOnMainThread = isOnMainThread; - if (!isOnMainThread) - { - Dispatcher.UIThread.MainLoop(_cancellationTokenSource.Token); - } - } - catch (ThreadAbortException) - { - Close(); - if (!isOnMainThread) - { - _cancellationTokenSource.Cancel(); - } - } + Show(); } /// @@ -101,10 +70,9 @@ protected void CloseWindow() Dispatcher.UIThread.InvokeAsync(() => { Close(); - if (!_isOnMainThread && !_hasInitiatedShutdown) + if (!_hasInitiatedShutdown) { _hasInitiatedShutdown = true; - _cancellationTokenSource.Cancel(); } }); } diff --git a/src/NetSparkle.UI.Avalonia/DownloadProgressWindow.axaml.cs b/src/NetSparkle.UI.Avalonia/DownloadProgressWindow.axaml.cs index 4d8d641b..755f6c61 100644 --- a/src/NetSparkle.UI.Avalonia/DownloadProgressWindow.axaml.cs +++ b/src/NetSparkle.UI.Avalonia/DownloadProgressWindow.axaml.cs @@ -68,10 +68,9 @@ private void DownloadProgressWindow_Closing(object? sender, System.ComponentMode DownloadProcessCompleted?.Invoke(this, new DownloadInstallEventArgs(false)); } Closing -= DownloadProgressWindow_Closing; - if (!_isOnMainThread && !_hasInitiatedShutdown) + if (!_hasInitiatedShutdown) { _hasInitiatedShutdown = true; - _cancellationTokenSource.Cancel(); } } @@ -123,9 +122,9 @@ void IDownloadProgress.SetDownloadAndInstallButtonEnabled(bool shouldBeEnabled) } } - void IDownloadProgress.Show(bool isOnMainThread) + void IDownloadProgress.Show() { - ShowWindow(isOnMainThread); + ShowWindow(); } /// diff --git a/src/NetSparkle.UI.Avalonia/Helpers/RelayCommand.cs b/src/NetSparkle.UI.Avalonia/Helpers/RelayCommand.cs index 2f9a61c6..68ef3f87 100644 --- a/src/NetSparkle.UI.Avalonia/Helpers/RelayCommand.cs +++ b/src/NetSparkle.UI.Avalonia/Helpers/RelayCommand.cs @@ -5,7 +5,6 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Input; -using Microsoft.CodeAnalysis.CSharp.Syntax; namespace NetSparkleUpdater.UI.Avalonia.Helpers { diff --git a/src/NetSparkle.UI.Avalonia/NetSparkle.UI.Avalonia.csproj b/src/NetSparkle.UI.Avalonia/NetSparkle.UI.Avalonia.csproj index 963a703c..d0eaf54d 100644 --- a/src/NetSparkle.UI.Avalonia/NetSparkle.UI.Avalonia.csproj +++ b/src/NetSparkle.UI.Avalonia/NetSparkle.UI.Avalonia.csproj @@ -74,12 +74,12 @@ - - + + - + diff --git a/src/NetSparkle.UI.Avalonia/UIFactory.cs b/src/NetSparkle.UI.Avalonia/UIFactory.cs index 2edf5458..76dc5e2a 100644 --- a/src/NetSparkle.UI.Avalonia/UIFactory.cs +++ b/src/NetSparkle.UI.Avalonia/UIFactory.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Threading; using NetSparkleUpdater.Interfaces; using NetSparkleUpdater.Properties; using NetSparkleUpdater.UI.Avalonia.ViewModels; @@ -10,6 +11,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using System.Threading.Tasks; namespace NetSparkleUpdater.UI.Avalonia { diff --git a/src/NetSparkle.UI.Avalonia/UpdateAvailableWindow.axaml.cs b/src/NetSparkle.UI.Avalonia/UpdateAvailableWindow.axaml.cs index 23164f36..c4acac5c 100644 --- a/src/NetSparkle.UI.Avalonia/UpdateAvailableWindow.axaml.cs +++ b/src/NetSparkle.UI.Avalonia/UpdateAvailableWindow.axaml.cs @@ -158,9 +158,9 @@ void IUpdateAvailable.HideSkipButton() } } - void IUpdateAvailable.Show(bool isOnMainThread) + void IUpdateAvailable.Show() { - ShowWindow(isOnMainThread); + ShowWindow(); } /// diff --git a/src/NetSparkle.UI.WPF/DownloadProgressWindow.xaml.cs b/src/NetSparkle.UI.WPF/DownloadProgressWindow.xaml.cs index d29bfdfb..d7480250 100644 --- a/src/NetSparkle.UI.WPF/DownloadProgressWindow.xaml.cs +++ b/src/NetSparkle.UI.WPF/DownloadProgressWindow.xaml.cs @@ -108,9 +108,9 @@ void IDownloadProgress.SetDownloadAndInstallButtonEnabled(bool shouldBeEnabled) ActionButton.IsEnabled = shouldBeEnabled; } - void IDownloadProgress.Show(bool isOnMainThread) + void IDownloadProgress.Show() { - ShowWindow(isOnMainThread); + ShowWindow(true); } private void ActionButton_Click(object sender, RoutedEventArgs e) diff --git a/src/NetSparkle.UI.WPF/UIFactory.cs b/src/NetSparkle.UI.WPF/UIFactory.cs index d0413540..361a6804 100644 --- a/src/NetSparkle.UI.WPF/UIFactory.cs +++ b/src/NetSparkle.UI.WPF/UIFactory.cs @@ -5,6 +5,7 @@ using System.Windows.Media; using System.Windows; using System.Threading; +using System.Threading.Tasks; using System.Collections.Generic; using NetSparkleUpdater.UI.WPF.ViewModels; diff --git a/src/NetSparkle.UI.WPF/UpdateAvailableWindow.xaml.cs b/src/NetSparkle.UI.WPF/UpdateAvailableWindow.xaml.cs index 377428c5..0b2ccb7e 100644 --- a/src/NetSparkle.UI.WPF/UpdateAvailableWindow.xaml.cs +++ b/src/NetSparkle.UI.WPF/UpdateAvailableWindow.xaml.cs @@ -113,9 +113,9 @@ void IUpdateAvailable.HideSkipButton() } } - void IUpdateAvailable.Show(bool isOnMainThread) + void IUpdateAvailable.Show() { - ShowWindow(isOnMainThread); + ShowWindow(true); } /// diff --git a/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.Designer.cs b/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.Designer.cs index edc09555..6b924088 100644 --- a/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.Designer.cs +++ b/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.Designer.cs @@ -32,82 +32,82 @@ protected override void Dispose(bool disposing) private void InitializeComponent() { System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DownloadProgressWindow)); - this.lblHeader = new System.Windows.Forms.Label(); - this.progressDownload = new System.Windows.Forms.ProgressBar(); - this.btnInstallAndReLaunch = new System.Windows.Forms.Button(); - this.imgAppIcon = new System.Windows.Forms.PictureBox(); - this.downloadProgressLbl = new System.Windows.Forms.Label(); - this.buttonCancel = new System.Windows.Forms.Button(); - this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); - ((System.ComponentModel.ISupportInitialize)(this.imgAppIcon)).BeginInit(); - this.tableLayoutPanel1.SuspendLayout(); - this.SuspendLayout(); + lblHeader = new System.Windows.Forms.Label(); + progressDownload = new System.Windows.Forms.ProgressBar(); + btnInstallAndReLaunch = new System.Windows.Forms.Button(); + imgAppIcon = new System.Windows.Forms.PictureBox(); + downloadProgressLbl = new System.Windows.Forms.Label(); + buttonCancel = new System.Windows.Forms.Button(); + tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel(); + ((System.ComponentModel.ISupportInitialize)imgAppIcon).BeginInit(); + tableLayoutPanel1.SuspendLayout(); + SuspendLayout(); // // lblHeader // - this.lblHeader.AutoEllipsis = true; - resources.ApplyResources(this.lblHeader, "lblHeader"); - this.lblHeader.Name = "lblHeader"; + lblHeader.AutoEllipsis = true; + resources.ApplyResources(lblHeader, "lblHeader"); + lblHeader.Name = "lblHeader"; // // progressDownload // - this.tableLayoutPanel1.SetColumnSpan(this.progressDownload, 2); - resources.ApplyResources(this.progressDownload, "progressDownload"); - this.progressDownload.Name = "progressDownload"; + tableLayoutPanel1.SetColumnSpan(progressDownload, 2); + resources.ApplyResources(progressDownload, "progressDownload"); + progressDownload.Name = "progressDownload"; + progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Marquee; // // btnInstallAndReLaunch // - resources.ApplyResources(this.btnInstallAndReLaunch, "btnInstallAndReLaunch"); - this.tableLayoutPanel1.SetColumnSpan(this.btnInstallAndReLaunch, 2); - this.btnInstallAndReLaunch.Name = "btnInstallAndReLaunch"; - this.btnInstallAndReLaunch.UseVisualStyleBackColor = true; - this.btnInstallAndReLaunch.Click += new System.EventHandler(this.OnInstallAndReLaunchClick); + resources.ApplyResources(btnInstallAndReLaunch, "btnInstallAndReLaunch"); + tableLayoutPanel1.SetColumnSpan(btnInstallAndReLaunch, 2); + btnInstallAndReLaunch.Name = "btnInstallAndReLaunch"; + btnInstallAndReLaunch.UseVisualStyleBackColor = true; + btnInstallAndReLaunch.Click += OnInstallAndReLaunchClick; // // imgAppIcon // - resources.ApplyResources(this.imgAppIcon, "imgAppIcon"); - this.imgAppIcon.Name = "imgAppIcon"; - this.imgAppIcon.TabStop = false; + resources.ApplyResources(imgAppIcon, "imgAppIcon"); + imgAppIcon.Name = "imgAppIcon"; + imgAppIcon.TabStop = false; // // downloadProgressLbl // - resources.ApplyResources(this.downloadProgressLbl, "downloadProgressLbl"); - this.tableLayoutPanel1.SetColumnSpan(this.downloadProgressLbl, 2); - this.downloadProgressLbl.Name = "downloadProgressLbl"; + resources.ApplyResources(downloadProgressLbl, "downloadProgressLbl"); + tableLayoutPanel1.SetColumnSpan(downloadProgressLbl, 2); + downloadProgressLbl.Name = "downloadProgressLbl"; // // buttonCancel // - resources.ApplyResources(this.buttonCancel, "buttonCancel"); - this.tableLayoutPanel1.SetColumnSpan(this.buttonCancel, 2); - this.buttonCancel.Name = "buttonCancel"; - this.buttonCancel.UseVisualStyleBackColor = true; - this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click); + resources.ApplyResources(buttonCancel, "buttonCancel"); + tableLayoutPanel1.SetColumnSpan(buttonCancel, 2); + buttonCancel.Name = "buttonCancel"; + buttonCancel.UseVisualStyleBackColor = true; + buttonCancel.Click += buttonCancel_Click; // // tableLayoutPanel1 // - resources.ApplyResources(this.tableLayoutPanel1, "tableLayoutPanel1"); - this.tableLayoutPanel1.Controls.Add(this.lblHeader, 1, 0); - this.tableLayoutPanel1.Controls.Add(this.buttonCancel, 0, 4); - this.tableLayoutPanel1.Controls.Add(this.imgAppIcon, 0, 0); - this.tableLayoutPanel1.Controls.Add(this.progressDownload, 0, 2); - this.tableLayoutPanel1.Controls.Add(this.btnInstallAndReLaunch, 0, 3); - this.tableLayoutPanel1.Controls.Add(this.downloadProgressLbl, 0, 1); - this.tableLayoutPanel1.Name = "tableLayoutPanel1"; + resources.ApplyResources(tableLayoutPanel1, "tableLayoutPanel1"); + tableLayoutPanel1.Controls.Add(lblHeader, 1, 0); + tableLayoutPanel1.Controls.Add(buttonCancel, 0, 4); + tableLayoutPanel1.Controls.Add(imgAppIcon, 0, 0); + tableLayoutPanel1.Controls.Add(progressDownload, 0, 2); + tableLayoutPanel1.Controls.Add(btnInstallAndReLaunch, 0, 3); + tableLayoutPanel1.Controls.Add(downloadProgressLbl, 0, 1); + tableLayoutPanel1.Name = "tableLayoutPanel1"; // // DownloadProgressWindow // resources.ApplyResources(this, "$this"); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.BackColor = System.Drawing.SystemColors.Control; - this.Controls.Add(this.tableLayoutPanel1); - this.MaximizeBox = false; - this.Name = "DownloadProgressWindow"; - this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show; - ((System.ComponentModel.ISupportInitialize)(this.imgAppIcon)).EndInit(); - this.tableLayoutPanel1.ResumeLayout(false); - this.tableLayoutPanel1.PerformLayout(); - this.ResumeLayout(false); - + AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + BackColor = System.Drawing.SystemColors.Control; + Controls.Add(tableLayoutPanel1); + MaximizeBox = false; + Name = "DownloadProgressWindow"; + SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show; + ((System.ComponentModel.ISupportInitialize)imgAppIcon).EndInit(); + tableLayoutPanel1.ResumeLayout(false); + tableLayoutPanel1.PerformLayout(); + ResumeLayout(false); } #endregion diff --git a/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.cs b/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.cs index 80358bf8..ca01940b 100644 --- a/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.cs +++ b/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.cs @@ -73,13 +73,13 @@ private void DownloadProgressWindow_FormClosing(object? sender, FormClosingEvent /// /// Show the UI and waits /// - void IDownloadProgress.Show(bool isOnMainThread) + void IDownloadProgress.Show() { Show(); - if (!isOnMainThread) - { - Application.Run(this); - } + //if (!isOnMainThread) + //{ + // Application.Run(this); + //} } /// @@ -162,6 +162,7 @@ private void OnDownloadProgressChanged(object sender, long bytesReceived, long t } else { + progressDownload.Style = ProgressBarStyle.Continuous; progressDownload.Value = percentage; downloadProgressLbl.Text = "(" + Utilities.ConvertNumBytesToUserReadableString(bytesReceived) + " / " + Utilities.ConvertNumBytesToUserReadableString(totalBytesToReceive) + ")"; diff --git a/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.resx b/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.resx index 7320bb50..605c7df7 100644 --- a/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.resx +++ b/src/NetSparkle.UI.WinForms.NetCore/DownloadProgressWindow.resx @@ -1,4 +1,64 @@ + + @@ -57,33 +117,32 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - + + True - + Fill - - + + Segoe UI, 9.75pt, style=Bold NoControl - - + 64, 6 - - + + 6, 6, 6, 6 - + 727, 50 - + 8 @@ -93,7 +152,7 @@ lblHeader - System.Windows.Forms.Label, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.Label, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 tableLayoutPanel1 @@ -107,7 +166,7 @@ GrowAndShrink - + 2 @@ -116,16 +175,16 @@ NoControl - + 279, 245 - + 6, 7, 6, 7 - + 238, 57 - + 13 @@ -135,7 +194,7 @@ buttonCancel - System.Windows.Forms.Button, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.Button, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 tableLayoutPanel1 @@ -146,26 +205,26 @@ NoControl - + 5, 7 - + 5, 7, 5, 7 - + 48, 48 Zoom - + 6 imgAppIcon - System.Windows.Forms.PictureBox, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.PictureBox, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 tableLayoutPanel1 @@ -179,16 +238,16 @@ NoControl - + 211, 174 - + 6, 7, 6, 7 - + 375, 57 - + 10 @@ -198,7 +257,7 @@ btnInstallAndReLaunch - System.Windows.Forms.Button, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.Button, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 tableLayoutPanel1 @@ -206,28 +265,28 @@ 4 - + True Fill - + Segoe UI, 10.125pt, style=Bold NoControl - + 6, 68 - + 6, 6, 6, 6 - + 785, 37 - + 12 @@ -237,7 +296,7 @@ downloadProgressLbl - System.Windows.Forms.Label, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.Label, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 tableLayoutPanel1 @@ -245,23 +304,23 @@ 5 - + 0, 0 - + 5 - + 797, 325 - + 14 tableLayoutPanel1 - System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 $this @@ -275,23 +334,23 @@ Fill - + 6, 117 - + 6, 6, 6, 6 - + 785, 44 - + 14 progressDownload - System.Windows.Forms.ProgressBar, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.ProgressBar, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 tableLayoutPanel1 @@ -299,19 +358,19 @@ 3 - + True - + 13, 32 GrowAndShrink - + 797, 325 - + AAABAAIAEBAAAAEAIABoBAAAJgAAADAwAAABACAAqCUAAI4EAAAoAAAAEAAAACAAAAABACAAAAAAAAAE AADXDQAA1w0AAAAAAAAAAAAA////AP///wD///8A////AP///wD///8A////AABczlIAXM5i////AP// @@ -496,10 +555,10 @@ /////wAA - + 6, 7, 6, 7 - + 350, 350 @@ -512,6 +571,6 @@ DownloadProgressWindow - System.Windows.Forms.Form, System.Windows.Forms, Version=5.0.6.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Windows.Forms.Form, System.Windows.Forms, Culture=neutral, PublicKeyToken=b77a5c561934e089 \ No newline at end of file diff --git a/src/NetSparkle.UI.WinForms.NetCore/UIFactory.cs b/src/NetSparkle.UI.WinForms.NetCore/UIFactory.cs index 99eacd47..88a3c160 100644 --- a/src/NetSparkle.UI.WinForms.NetCore/UIFactory.cs +++ b/src/NetSparkle.UI.WinForms.NetCore/UIFactory.cs @@ -7,11 +7,13 @@ using System.Threading; using System.Collections.Generic; using static System.Windows.Forms.VisualStyles.VisualStyleElement; +using System.Threading.Tasks; namespace NetSparkleUpdater.UI.WinForms { /// - /// UI factory for WinForms .NET Core interface + /// UI factory for WinForms .NET Core interface. + /// Note that it expects to be created on your main UI thread. /// public class UIFactory : IUIFactory { @@ -19,6 +21,7 @@ public class UIFactory : IUIFactory /// Icon used on various windows shown by NetSparkleUpdater /// protected Icon? _applicationIcon = null; + private SynchronizationContext _syncContext; /// public UIFactory() @@ -26,6 +29,7 @@ public UIFactory() HideReleaseNotes = false; HideRemindMeLaterButton = false; HideSkipButton = false; + _syncContext = SynchronizationContext.Current ?? new SynchronizationContext(); } /// diff --git a/src/NetSparkle.UI.WinForms.NetCore/UpdateAvailableWindow.cs b/src/NetSparkle.UI.WinForms.NetCore/UpdateAvailableWindow.cs index 56b02369..a0783b33 100644 --- a/src/NetSparkle.UI.WinForms.NetCore/UpdateAvailableWindow.cs +++ b/src/NetSparkle.UI.WinForms.NetCore/UpdateAvailableWindow.cs @@ -80,7 +80,7 @@ public UpdateAvailableWindow(SparkleUpdater sparkle, List items, Ic AppCastItem? item = items.FirstOrDefault(); var downloadInstallText = isUpdateAlreadyDownloaded ? "install" : "download"; - lblHeader.Text = lblHeader.Text.Replace("APP", appNameTitle); + lblHeader.Text = lblHeader.Text.Replace("APP", item != null ? appNameTitle : "the application"); if (item != null) { lblInfoText.Text = string.Format("{0} {1} is now available (you have {2}). Would you like to {3} it now?", appNameTitle, item.Version, installedVersion, downloadInstallText); @@ -190,13 +190,20 @@ void IUpdateAvailable.HideReleaseNotes() /// /// Shows the dialog /// - void IUpdateAvailable.Show(bool IsOnMainThread) + void IUpdateAvailable.Show() { - Show(); - if (!IsOnMainThread) + if (InvokeRequired) { - Application.Run(this); + Invoke(() => Show()); } + else + { + Show(); + } + //if (!IsOnMainThread) + //{ + // Application.Run(this); + //} } void IUpdateAvailable.BringToFront() diff --git a/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.Designer.cs b/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.Designer.cs index 027c0ef5..60549ae2 100644 --- a/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.Designer.cs +++ b/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.Designer.cs @@ -54,6 +54,7 @@ private void InitializeComponent() this.tableLayoutPanel1.SetColumnSpan(this.progressDownload, 2); resources.ApplyResources(this.progressDownload, "progressDownload"); this.progressDownload.Name = "progressDownload"; + this.progressDownload.Style = System.Windows.Forms.ProgressBarStyle.Marquee; // // btnInstallAndReLaunch // diff --git a/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.cs b/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.cs index 80358bf8..ca01940b 100644 --- a/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.cs +++ b/src/NetSparkle.UI.WinForms.NetFramework/DownloadProgressWindow.cs @@ -73,13 +73,13 @@ private void DownloadProgressWindow_FormClosing(object? sender, FormClosingEvent /// /// Show the UI and waits /// - void IDownloadProgress.Show(bool isOnMainThread) + void IDownloadProgress.Show() { Show(); - if (!isOnMainThread) - { - Application.Run(this); - } + //if (!isOnMainThread) + //{ + // Application.Run(this); + //} } /// @@ -162,6 +162,7 @@ private void OnDownloadProgressChanged(object sender, long bytesReceived, long t } else { + progressDownload.Style = ProgressBarStyle.Continuous; progressDownload.Value = percentage; downloadProgressLbl.Text = "(" + Utilities.ConvertNumBytesToUserReadableString(bytesReceived) + " / " + Utilities.ConvertNumBytesToUserReadableString(totalBytesToReceive) + ")"; diff --git a/src/NetSparkle.UI.WinForms.NetFramework/UIFactory.cs b/src/NetSparkle.UI.WinForms.NetFramework/UIFactory.cs index ca3e9c77..7023a929 100644 --- a/src/NetSparkle.UI.WinForms.NetFramework/UIFactory.cs +++ b/src/NetSparkle.UI.WinForms.NetFramework/UIFactory.cs @@ -7,11 +7,13 @@ using System.Threading; using System.Collections.Generic; using static System.Windows.Forms.VisualStyles.VisualStyleElement; +using System.Threading.Tasks; namespace NetSparkleUpdater.UI.WinForms { /// - /// UI factory for WinForms .NET Framework interface + /// UI factory for WinForms .NET Framework interface. + /// Note that it expects to be created on your main UI thread. /// public class UIFactory : IUIFactory { @@ -20,12 +22,15 @@ public class UIFactory : IUIFactory /// protected Icon? _applicationIcon = null; + private SynchronizationContext _syncContext; + /// public UIFactory() { HideReleaseNotes = false; HideRemindMeLaterButton = false; HideSkipButton = false; + _syncContext = SynchronizationContext.Current ?? new SynchronizationContext(); } /// diff --git a/src/NetSparkle.UI.WinForms.NetFramework/UpdateAvailableWindow.cs b/src/NetSparkle.UI.WinForms.NetFramework/UpdateAvailableWindow.cs index e521ab8d..04d07295 100644 --- a/src/NetSparkle.UI.WinForms.NetFramework/UpdateAvailableWindow.cs +++ b/src/NetSparkle.UI.WinForms.NetFramework/UpdateAvailableWindow.cs @@ -80,6 +80,7 @@ public UpdateAvailableWindow(SparkleUpdater sparkle, List items, Ic AppCastItem item = items.FirstOrDefault(); var downloadInstallText = isUpdateAlreadyDownloaded ? "install" : "download"; + lblHeader.Text = lblHeader.Text.Replace("APP", item != null ? appNameTitle : "the application"); if (item != null) { lblInfoText.Text = string.Format("{0} {1} is now available (you have {2}). Would you like to {3} it now?", appNameTitle, item.Version, installedVersion, downloadInstallText); @@ -189,13 +190,13 @@ void IUpdateAvailable.HideReleaseNotes() /// /// Shows the dialog /// - void IUpdateAvailable.Show(bool IsOnMainThread) + void IUpdateAvailable.Show() { Show(); - if (!IsOnMainThread) - { - Application.Run(this); - } + //if (!IsOnMainThread) + //{ + // Application.Run(this); + //} } void IUpdateAvailable.BringToFront() diff --git a/src/NetSparkle/AssemblyAccessors/AsmResolverAccessor.cs b/src/NetSparkle/AssemblyAccessors/AsmResolverAccessor.cs index f3a949b2..76fca316 100644 --- a/src/NetSparkle/AssemblyAccessors/AsmResolverAccessor.cs +++ b/src/NetSparkle/AssemblyAccessors/AsmResolverAccessor.cs @@ -82,7 +82,7 @@ public string AssemblyVersion get { return _assemblyAttributes != null - ? FindAttributeData(_assemblyAttributes, typeof(AssemblyInformationalVersionAttribute)) ?? "" + ? FindAttributeData(_assemblyAttributes, typeof(AssemblyInformationalVersionAttribute)) ?? FileVersion : ""; } } diff --git a/src/NetSparkle/Downloaders/LocalFileDownloader.cs b/src/NetSparkle/Downloaders/LocalFileDownloader.cs index 9bf502c3..3d89e2e1 100644 --- a/src/NetSparkle/Downloaders/LocalFileDownloader.cs +++ b/src/NetSparkle/Downloaders/LocalFileDownloader.cs @@ -8,6 +8,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Security.Policy; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -65,6 +66,9 @@ public ILogger? LogWriter /// public bool UseLocalUriPath { get; set; } = false; + /// + public event DownloadFromPathToPathEvent? DownloadStarted; + /// public event DownloadProgressEvent? DownloadProgressChanged; @@ -100,7 +104,7 @@ public void Dispose() } /// - public async void StartFileDownload(Uri? uri, string downloadFilePath) + public async Task DownloadFile(Uri? uri, string downloadFilePath) { var path = UseLocalUriPath ? uri?.LocalPath : uri?.AbsolutePath; if (path != null) @@ -123,6 +127,7 @@ private async Task CopyFileAsync(string sourceFile, string destinationFile, Canc try { var wasCanceled = false; + DownloadStarted?.Invoke(this, sourceFile, destinationFile); using (var sourceStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize, fileOptions)) using (var destinationStream = diff --git a/src/NetSparkle/Downloaders/WebFileDownloader.cs b/src/NetSparkle/Downloaders/WebFileDownloader.cs index ff6ac9a9..b020b9a1 100644 --- a/src/NetSparkle/Downloaders/WebFileDownloader.cs +++ b/src/NetSparkle/Downloaders/WebFileDownloader.cs @@ -103,6 +103,9 @@ protected virtual HttpClient CreateHttpClient(HttpClientHandler? handler) /// public bool IsDownloading { get; private set; } + /// + public event DownloadFromPathToPathEvent? DownloadStarted; + /// public event DownloadProgressEvent? DownloadProgressChanged; @@ -118,12 +121,11 @@ public void Dispose() } /// - public async void StartFileDownload(Uri? uri, string downloadFilePath) + public async Task DownloadFile(Uri? uri, string downloadFilePath) { if (uri == null) { _logger?.PrintMessage("StartFileDownloadAsync had a null Uri; not going to download anything"); - return; } else { @@ -149,6 +151,7 @@ private async Task StartFileDownloadAsync(Uri? uri, string downloadFilePath) { _cts = new CancellationTokenSource(); } + DownloadStarted?.Invoke(this, uri.ToString(), downloadFilePath); using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, uri)) using (HttpResponseMessage response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, _cts.Token)) { diff --git a/src/NetSparkle/Interfaces/IDownloadProgress.cs b/src/NetSparkle/Interfaces/IDownloadProgress.cs index e5e5b709..ebe52c2d 100644 --- a/src/NetSparkle/Interfaces/IDownloadProgress.cs +++ b/src/NetSparkle/Interfaces/IDownloadProgress.cs @@ -25,7 +25,7 @@ public interface IDownloadProgress /// Show the UI for download progress /// /// True if download was successful; false otherwise - void Show(bool isOnMainThread); + void Show(); /// /// Called when the download progress changes diff --git a/src/NetSparkle/Interfaces/IUIFactory.cs b/src/NetSparkle/Interfaces/IUIFactory.cs index b815f2f7..c277f29b 100644 --- a/src/NetSparkle/Interfaces/IUIFactory.cs +++ b/src/NetSparkle/Interfaces/IUIFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; namespace NetSparkleUpdater.Interfaces { diff --git a/src/NetSparkle/Interfaces/IUpdateAvailable.cs b/src/NetSparkle/Interfaces/IUpdateAvailable.cs index b21171ed..28138513 100644 --- a/src/NetSparkle/Interfaces/IUpdateAvailable.cs +++ b/src/NetSparkle/Interfaces/IUpdateAvailable.cs @@ -17,7 +17,7 @@ public interface IUpdateAvailable /// /// Show the UI that displays release notes, etc. /// - void Show(bool IsOnMainThread); + void Show(); /// /// Hides the release notes diff --git a/src/NetSparkle/Interfaces/IUpdateDownloader.cs b/src/NetSparkle/Interfaces/IUpdateDownloader.cs index 06cf337a..0de451a3 100644 --- a/src/NetSparkle/Interfaces/IUpdateDownloader.cs +++ b/src/NetSparkle/Interfaces/IUpdateDownloader.cs @@ -16,6 +16,11 @@ public interface IUpdateDownloader /// bool IsDownloading { get; } + /// + /// Called when the download of a file is just about to begin + /// + public event DownloadFromPathToPathEvent? DownloadStarted; + /// /// Event to call when some progress has been made on the download /// @@ -31,7 +36,7 @@ public interface IUpdateDownloader /// /// URL for the download /// Where to download the file - void StartFileDownload(Uri? uri, string downloadFilePath); + Task DownloadFile(Uri? uri, string downloadFilePath); /// /// Cancel the download. diff --git a/src/NetSparkle/NetSparkleDelegates.cs b/src/NetSparkle/NetSparkleDelegates.cs index c30b19c5..4e66c563 100644 --- a/src/NetSparkle/NetSparkleDelegates.cs +++ b/src/NetSparkle/NetSparkleDelegates.cs @@ -22,12 +22,21 @@ namespace NetSparkleUpdater public delegate void LoopFinishedOperation(object sender, bool updateRequired); /// - /// An update was detected for the user's currently running software + /// An update was detected for the user's currently running software. + /// If both this and UpdateDetectedAsync are implemented, UpdateDetectedAsync is prioritized. /// /// the object that initiated the call /// Information about the update that was detected public delegate void UpdateDetected(object sender, UpdateDetectedEventArgs e); + /// + /// An update was detected for the user's currently running software. + /// If both this and UpdateDetected are implemented, UpdateDetectedAsync is prioritized. + /// + /// the object that initiated the call + /// Information about the update that was detected + public delegate Task UpdateDetectedAsync(object sender, UpdateDetectedEventArgs e); + /// /// has started checking for updates /// @@ -66,6 +75,13 @@ namespace NetSparkleUpdater /// public delegate void DownloadEvent(AppCastItem item, string path); + /// + /// A delegate for download events (start, canceled) with just a path. + /// Make sure to check that the path is valid in case this is called when the path is unknown/unset + /// for some reason (e.g. failed before download began). + /// + public delegate void DownloadFromPathToPathEvent(object sender, string from, string to); + /// /// Delegate that provides information about some download progress that has been made /// diff --git a/src/NetSparkle/ReleaseNotesGrabber.cs b/src/NetSparkle/ReleaseNotesGrabber.cs index 62aaaf28..8a440f5b 100644 --- a/src/NetSparkle/ReleaseNotesGrabber.cs +++ b/src/NetSparkle/ReleaseNotesGrabber.cs @@ -152,7 +152,7 @@ public virtual async Task DownloadAllReleaseNotes(List item { _logger?.PrintMessage("Initializing release notes for {0}", castItem.Version ?? "[Unknown version]"); // TODO: could we optimize this by doing multiple downloads at once? - var releaseNotes = await GetReleaseNotes(castItem, _sparkle, cancellationToken); + var releaseNotes = await GetReleaseNotes(castItem, cancellationToken); sb.Append(string.Format((hasAddedFirstItem ? "
" : "") + ReleaseNotesTemplate, castItem.Version, castItem.PublicationDate != DateTime.MinValue && @@ -174,12 +174,10 @@ public virtual async Task DownloadAllReleaseNotes(List item /// in HTML format so that they can be displayed to the user. ///
/// item to download the release notes for - /// that can be used for logging information - /// about the release notes grabbing process (or its failures) -- TODO: Remove this param entirely /// token that can be used to cancel a release notes /// grabbing operation /// The release notes, formatted as HTML, for a given release of the software - protected virtual async Task GetReleaseNotes(AppCastItem item, SparkleUpdater? sparkle, CancellationToken cancellationToken) + protected virtual async Task GetReleaseNotes(AppCastItem item, CancellationToken cancellationToken) { string criticalUpdate = item.IsCriticalUpdate ? "Critical Update" : ""; // at first try to use embedded description @@ -216,7 +214,7 @@ public virtual async Task DownloadAllReleaseNotes(List item // download release notes _logger?.PrintMessage("Downloading release notes for {0} at {1}", item.Version ?? "[Unknown version]", item.ReleaseNotesLink ?? "[Unknown release notes link]"); string notes = item.ReleaseNotesLink != null - ? await DownloadReleaseNotes(item.ReleaseNotesLink, cancellationToken, sparkle) + ? await DownloadReleaseNotes(item.ReleaseNotesLink, cancellationToken) : ""; _logger?.PrintMessage("Done downloading release notes for {0}", item.Version ?? "[Unknown version]"); if (string.IsNullOrWhiteSpace(notes)) @@ -266,11 +264,9 @@ public virtual async Task DownloadAllReleaseNotes(List item ///
/// string URL to the release notes to download /// token that can be used to cancel a download operation - /// that can be used for logging information - /// about the download process (or its failures) TODO: remove this param as no longer needed; breaking change /// The release notes data (file data) at the given link as a string. Typically this data /// is formatted as markdown. - protected virtual async Task DownloadReleaseNotes(string link, CancellationToken cancellationToken, SparkleUpdater? sparkle) + protected virtual async Task DownloadReleaseNotes(string link, CancellationToken cancellationToken) { try { @@ -280,7 +276,7 @@ protected virtual async Task DownloadReleaseNotes(string link, Cancellat return await httpClient.GetStringAsync(link); } } - catch (WebException ex) + catch (Exception ex) { _logger?.PrintMessage("Cannot download release notes from {0} because {1}", link, ex.Message); return ""; diff --git a/src/NetSparkle/SparkleUpdater.cs b/src/NetSparkle/SparkleUpdater.cs index 3756a8d4..c5221987 100644 --- a/src/NetSparkle/SparkleUpdater.cs +++ b/src/NetSparkle/SparkleUpdater.cs @@ -48,8 +48,7 @@ public partial class SparkleUpdater : IDisposable private ILogger? _logWriter; private readonly Task _taskWorker; private CancellationToken _cancelToken; - private readonly CancellationTokenSource _cancelTokenSource; - private readonly SynchronizationContext _syncContext; + private CancellationTokenSource _cancelTokenSource; private readonly string? _appReferenceAssembly; private bool _doInitialCheck; @@ -81,6 +80,7 @@ public partial class SparkleUpdater : IDisposable /// suffice as a "fix" for now. /// private Action? _actionToRunOnProgressWindowShown; + private SynchronizationContext _syncContextForWorkerLoop; #endregion @@ -116,12 +116,11 @@ public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, s { _latestDownloadedUpdateInfo = null; _hasAttemptedFileRedownload = false; - + _syncContextForWorkerLoop = SynchronizationContext.Current ?? new SynchronizationContext(); + UIFactory = factory; SignatureVerifier = signatureVerifier; LogWriter = new LogWriter(); - // Syncronization Context - _syncContext = SynchronizationContext.Current ?? new SynchronizationContext(); // init UI UIFactory?.Init(this); _appReferenceAssembly = null; @@ -133,10 +132,7 @@ public SparkleUpdater(string appcastUrl, ISignatureVerifier signatureVerifier, s } // adjust the delegates - _taskWorker = new Task(() => - { - OnWorkerDoWork(); - }); + _taskWorker = new Task(OnWorkerDoWork); _cancelTokenSource = new CancellationTokenSource(); _cancelToken = _cancelTokenSource.Token; @@ -366,13 +362,6 @@ public string RestartExecutableName /// public bool UseNotificationToast { get; set; } - /// - /// This setting is only valid on WinForms and WPF. - /// If true, tries to run UI code on the main thread using . - /// Must be set to true if using NetSparkleUpdater from Avalonia. - /// - public bool ShowsUIOnMainThread { get; set; } - /// /// Object that handles any diagnostic messages for NetSparkle. /// If you want to use your own class for this, you should just @@ -561,9 +550,9 @@ public Process? InstallerProcess /// You should only call this function when your app is initialized and shows its main UI. /// /// whether the first check should happen before or after the first interval - public void StartLoop(bool doInitialCheck) + public async Task StartLoop(bool doInitialCheck) { - StartLoop(doInitialCheck, false); + await StartLoop(doInitialCheck, false); } /// @@ -572,32 +561,36 @@ public void StartLoop(bool doInitialCheck) /// /// whether the first check should happen before or after the first interval /// the interval to wait between update checks - public void StartLoop(bool doInitialCheck, TimeSpan checkFrequency) + public async Task StartLoop(bool doInitialCheck, TimeSpan checkFrequency) { - StartLoop(doInitialCheck, false, checkFrequency); + await StartLoop(doInitialCheck, false, checkFrequency); } /// /// Starts a SparkleUpdater background loop to check for updates every 24 hours. + /// Loop pauses for a few seconds on first run so that if the loop is started with the app, + /// the user isn't immediately shown an update window. /// You should only call this function when your app is initialized and shows its main UI. /// /// whether the first check should happen before or after the first interval /// if is true, whether the first check /// should happen even if the last check was less than 24 hours ago - public void StartLoop(bool doInitialCheck, bool forceInitialCheck) + public async Task StartLoop(bool doInitialCheck, bool forceInitialCheck) { - StartLoop(doInitialCheck, forceInitialCheck, TimeSpan.FromHours(24)); + await StartLoop(doInitialCheck, forceInitialCheck, TimeSpan.FromHours(24)); } /// /// Starts a SparkleUpdater background loop to check for updates on a given interval. + /// Loop pauses for a few seconds on first run so that if the loop is started with the app, + /// the user isn't immediately shown an update window. /// You should only call this function when your app is initialized and shows its main UIw. /// /// whether the first check should happen before or after the first interval /// if is true, whether the first check /// should happen even if the last check was within the last interval /// the interval to wait between update checks - public async void StartLoop(bool doInitialCheck, bool forceInitialCheck, TimeSpan checkFrequency) + public async Task StartLoop(bool doInitialCheck, bool forceInitialCheck, TimeSpan checkFrequency) { if (ClearOldInstallers != null) { @@ -613,18 +606,16 @@ public async void StartLoop(bool doInitialCheck, bool forceInitialCheck, TimeSpa // first set the event handle _loopingHandle.Set(); - // Start the helper thread as a background worker - // store info _doInitialCheck = doInitialCheck; _forceInitialCheck = forceInitialCheck; _checkFrequency = checkFrequency; + // create new cancel token source + _cancelTokenSource = new CancellationTokenSource(); + _cancelToken = _cancelTokenSource.Token; LogWriter?.PrintMessage("Starting background worker"); - // start the work - //var scheduler = TaskScheduler.FromCurrentSynchronizationContext(); - //_taskWorker.Start(scheduler); // don't allow starting the task 2x if (_taskWorker.IsCompleted == false && _taskWorker.Status != TaskStatus.Running && _taskWorker.Status != TaskStatus.WaitingToRun && _taskWorker.Status != TaskStatus.WaitingForActivation) @@ -640,6 +631,7 @@ public void StopLoop() { // ensure the work will finished _exitHandle.Set(); + _cancelTokenSource.Cancel(); } /// @@ -815,65 +807,15 @@ private void OnToastClick(List updates) private void ShowUpdateAvailableWindow(List updates, bool isUpdateAlreadyDownloaded = false) { - if (UpdateAvailableWindow != null) - { - // close old window - if (ShowsUIOnMainThread) - { - _syncContext.Post((state) => - { - UpdateAvailableWindow.Close(); - UpdateAvailableWindow = null; - }, null); - } - else - { - UpdateAvailableWindow.Close(); - UpdateAvailableWindow = null; - } - } + UpdateAvailableWindow?.Close(); + UpdateAvailableWindow = null; + UpdateAvailableWindow = UIFactory?.CreateUpdateAvailableWindow(this, updates, isUpdateAlreadyDownloaded); - // create the form - Thread thread = new Thread(() => - { - try - { - // define action - Action showSparkleUIForAvailableUpdate = () => - { - UpdateAvailableWindow = UIFactory?.CreateUpdateAvailableWindow(this, updates, isUpdateAlreadyDownloaded); - - if (UpdateAvailableWindow != null) - { - UpdateAvailableWindow.UserResponded += OnUpdateWindowUserResponded; - UpdateAvailableWindow.Show(ShowsUIOnMainThread); - } - }; - - // call action - if (ShowsUIOnMainThread) - { - _syncContext.Post((state) => showSparkleUIForAvailableUpdate(), null); - } - else - { - showSparkleUIForAvailableUpdate(); - } - } - catch (Exception e) - { - LogWriter?.PrintMessage("Error showing sparkle form: {0}", e.Message); - } - }); -#if NETFRAMEWORK - thread.SetApartmentState(ApartmentState.STA); -#else - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (UpdateAvailableWindow != null) { - thread.SetApartmentState(ApartmentState.STA); // only supported on Windows + UpdateAvailableWindow.UserResponded += OnUpdateWindowUserResponded; + UpdateAvailableWindow.Show(); } -#endif - thread.Start(); } /// @@ -981,13 +923,11 @@ public async Task InitAndBeginDownload(AppCastItem item) // so that the user can actually perform the install _actionToRunOnProgressWindowShown = () => { - CallFuncConsideringUIThreads(() => { - DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName); - }); + DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName); bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall; if (shouldInstallAndRelaunch) { - CallFuncConsideringUIThreads(() => { ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); }); + ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); } }; CreateAndShowProgressWindow(item, true); @@ -1009,16 +949,13 @@ public async Task InitAndBeginDownload(AppCastItem item) // we won't be able to download anyway since we couldn't delete the file :( we'll try next time the // update loop goes around. needsToDownload = false; - CallFuncConsideringUIThreads(() => - { - DownloadHadError?.Invoke(item, _downloadTempFileName, - new Exception(string.Format("Unable to delete old download at {0}", _downloadTempFileName))); - }); + DownloadHadError?.Invoke(item, _downloadTempFileName, + new Exception(string.Format("Unable to delete old download at {0}", _downloadTempFileName))); } } else { - CallFuncConsideringUIThreads(() => { DownloadedFileIsCorrupt?.Invoke(item, _downloadTempFileName); }); + DownloadedFileIsCorrupt?.Invoke(item, _downloadTempFileName); } } if (item.DownloadLink == null || string.IsNullOrWhiteSpace(item.DownloadLink)) @@ -1034,32 +971,35 @@ public async Task InitAndBeginDownload(AppCastItem item) { UpdateDownloader.DownloadProgressChanged -= OnDownloadProgressChanged; UpdateDownloader.DownloadFileCompleted -= OnDownloadFinished; + UpdateDownloader.DownloadStarted -= OnDownloadStarted; UpdateDownloader.DownloadProgressChanged += OnDownloadProgressChanged; UpdateDownloader.DownloadFileCompleted += OnDownloadFinished; + UpdateDownloader.DownloadStarted += OnDownloadStarted; } _actionToRunOnProgressWindowShown = () => { Uri url = Utilities.GetAbsoluteURL(item.DownloadLink, AppCastUrl); LogWriter?.PrintMessage("Starting to download {0} to {1}", item.DownloadLink, _downloadTempFileName); - UpdateDownloader?.StartFileDownload(url, _downloadTempFileName); - CallFuncConsideringUIThreads(() => - { - DownloadStarted?.Invoke(item, _downloadTempFileName); - }); + UpdateDownloader?.DownloadFile(url, _downloadTempFileName); }; CreateAndShowProgressWindow(item, false); } } } + private void OnDownloadStarted(object sender, string from, string to) + { + if (_itemBeingDownloaded != null && to != null) + { + DownloadStarted?.Invoke(_itemBeingDownloaded, to); + } + } + private void OnDownloadProgressChanged(object sender, ItemDownloadProgressEventArgs args) { if (_itemBeingDownloaded != null) // just a null safety check, shouldn't be null in this case { - CallFuncConsideringUIThreads(() => - { - DownloadMadeProgress?.Invoke(sender, _itemBeingDownloaded, args); - }); + DownloadMadeProgress?.Invoke(sender, _itemBeingDownloaded, args); } } @@ -1089,63 +1029,31 @@ private void CreateAndShowProgressWindow(AppCastItem castItem, bool shouldShowAs } ProgressWindow = null; } - Action showSparkleDownloadUI = () => {}; if (ProgressWindow == null && UIFactory != null && !IsDownloadingSilently()) { - // create the form - showSparkleDownloadUI = () => + ProgressWindow = UIFactory?.CreateProgressWindow(this, castItem); + if (ProgressWindow != null) { - ProgressWindow = UIFactory?.CreateProgressWindow(this, castItem); - if (ProgressWindow != null) + ProgressWindow.DownloadProcessCompleted += ProgressWindowCompleted; + if (UpdateDownloader != null) { - ProgressWindow.DownloadProcessCompleted += ProgressWindowCompleted; - if (UpdateDownloader != null) - { - UpdateDownloader.DownloadProgressChanged += ProgressWindow.OnDownloadProgressChanged; - } - if (shouldShowAsDownloadedAlready) - { - ProgressWindow?.FinishedDownloadingFile(true); - _syncContext.Post((state2) => - { - OnDownloadFinished(this, new AsyncCompletedEventArgs(null, false, null)); - _actionToRunOnProgressWindowShown?.Invoke(); - _actionToRunOnProgressWindowShown = null; - }, null); - } + UpdateDownloader.DownloadProgressChanged += ProgressWindow.OnDownloadProgressChanged; } - }; - } - Thread thread = new Thread(() => - { - // call action - if (ShowsUIOnMainThread) - { - _syncContext.Post((state) => + if (shouldShowAsDownloadedAlready) { - showSparkleDownloadUI(); - _actionToRunOnProgressWindowShown?.Invoke(); - _actionToRunOnProgressWindowShown = null; - ProgressWindow?.Show(ShowsUIOnMainThread); - }, null); - } - else - { - showSparkleDownloadUI(); - _actionToRunOnProgressWindowShown?.Invoke(); - _actionToRunOnProgressWindowShown = null; - ProgressWindow?.Show(ShowsUIOnMainThread); + ProgressWindow?.FinishedDownloadingFile(true); + OnDownloadFinished(this, new AsyncCompletedEventArgs(null, false, null)); + } } - }); -#if NETFRAMEWORK - thread.SetApartmentState(ApartmentState.STA); -#else - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + ProgressWindow?.Show(); + _actionToRunOnProgressWindowShown?.Invoke(); + _actionToRunOnProgressWindowShown = null; + } + else { - thread.SetApartmentState(ApartmentState.STA); // only supported on Windows + _actionToRunOnProgressWindowShown?.Invoke(); + _actionToRunOnProgressWindowShown = null; } -#endif - thread.Start(); } private async void ProgressWindowCompleted(object sender, DownloadInstallEventArgs args) @@ -1203,27 +1111,20 @@ private void OnDownloadFinished(object? sender, AsyncCompletedEventArgs e) } catch (Exception deleteEx) { - CallFuncConsideringUIThreads(() => + string cleanUpErrorMessage = "Download canceled (Cleanup error): " + deleteEx.Message; + if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(cleanUpErrorMessage)) { - string cleanUpErrorMessage = "Download canceled (Cleanup error): " + deleteEx.Message; - if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(cleanUpErrorMessage)) - { - UIFactory?.ShowDownloadErrorMessage(this, cleanUpErrorMessage, AppCastUrl); - } - }); + UIFactory?.ShowDownloadErrorMessage(this, cleanUpErrorMessage, AppCastUrl); + } } - } LogWriter?.PrintMessage("Download was canceled"); string errorMessage = "Download canceled"; - CallFuncConsideringUIThreads(() => + if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage)) { - if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage)) - { - UIFactory?.ShowDownloadErrorMessage(this, errorMessage, AppCastUrl); - } - DownloadCanceled?.Invoke(_itemBeingDownloaded, _downloadTempFileName); - }); + UIFactory?.ShowDownloadErrorMessage(this, errorMessage, AppCastUrl); + } + DownloadCanceled?.Invoke(_itemBeingDownloaded, _downloadTempFileName); return; } if (e.Error != null) @@ -1238,24 +1139,18 @@ private void OnDownloadFinished(object? sender, AsyncCompletedEventArgs e) catch (Exception deleteEx) { string cleanUpErrorMessage = "Error while downloading (Cleanup error): " + deleteEx.Message; - CallFuncConsideringUIThreads(() => + if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(cleanUpErrorMessage)) { - if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(cleanUpErrorMessage)) - { - UIFactory?.ShowDownloadErrorMessage(this, cleanUpErrorMessage, AppCastUrl); - } - }); + UIFactory?.ShowDownloadErrorMessage(this, cleanUpErrorMessage, AppCastUrl); + } } } LogWriter?.PrintMessage("Error on download finished: {0}", e.Error.Message); - CallFuncConsideringUIThreads(() => + if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(e.Error.Message)) { - if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(e.Error.Message)) - { - UIFactory?.ShowDownloadErrorMessage(this, e.Error.Message, AppCastUrl); - } - DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, e.Error); - }); + UIFactory?.ShowDownloadErrorMessage(this, e.Error.Message, AppCastUrl); + } + DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, e.Error); return; } // test the item for signature @@ -1275,10 +1170,7 @@ private void OnDownloadFinished(object? sender, AsyncCompletedEventArgs e) { var message = "File not found even though it was reported as downloading successfully!"; LogWriter?.PrintMessage(message); - CallFuncConsideringUIThreads(() => - { - DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(message)); - }); + DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(message)); } // check the signature @@ -1290,10 +1182,7 @@ private void OnDownloadFinished(object? sender, AsyncCompletedEventArgs e) { LogWriter?.PrintMessage("Error validating signature of file: {0}; {1}", exc.Message, exc.StackTrace ?? "[No stack trace available]"); validationRes = ValidationResult.Invalid; - CallFuncConsideringUIThreads(() => - { - DownloadedFileThrewWhileCheckingSignature?.Invoke(_itemBeingDownloaded, _downloadTempFileName); - }); + DownloadedFileThrewWhileCheckingSignature?.Invoke(_itemBeingDownloaded, _downloadTempFileName); } } } @@ -1301,7 +1190,7 @@ private void OnDownloadFinished(object? sender, AsyncCompletedEventArgs e) bool isSignatureInvalid = validationRes == ValidationResult.Invalid; // if Unchecked, we accept download as valid if (shouldShowUIItems) { - CallFuncConsideringUIThreads(() => { ProgressWindow?.FinishedDownloadingFile(!isSignatureInvalid); }); + ProgressWindow?.FinishedDownloadingFile(!isSignatureInvalid); } // signature of file isn't valid so exit with error if (isSignatureInvalid) @@ -1309,28 +1198,22 @@ private void OnDownloadFinished(object? sender, AsyncCompletedEventArgs e) LogWriter?.PrintMessage("Invalid signature for downloaded file for app cast: {0}", _downloadTempFileName); string errorMessage = "Downloaded file has invalid signature!"; // Default to showing errors in the progress window. Only go to the UIFactory to show errors if necessary. - CallFuncConsideringUIThreads(() => + DownloadedFileIsCorrupt?.Invoke(_itemBeingDownloaded, _downloadTempFileName); + if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage)) { - DownloadedFileIsCorrupt?.Invoke(_itemBeingDownloaded, _downloadTempFileName); - if (shouldShowUIItems && ProgressWindow != null && !ProgressWindow.DisplayErrorMessage(errorMessage)) - { - UIFactory?.ShowDownloadErrorMessage(this, errorMessage, AppCastUrl); - } - DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(errorMessage)); - }); + UIFactory?.ShowDownloadErrorMessage(this, errorMessage, AppCastUrl); + } + DownloadHadError?.Invoke(_itemBeingDownloaded, _downloadTempFileName, new NetSparkleException(errorMessage)); } else { LogWriter?.PrintMessage("Signature is valid. File successfully downloaded!"); - CallFuncConsideringUIThreads(() => - { - DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName); - bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall; - if (shouldInstallAndRelaunch) - { - ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); - } - }); + DownloadFinished?.Invoke(_itemBeingDownloaded, _downloadTempFileName); + bool shouldInstallAndRelaunch = UserInteractionMode == UserInteractionMode.DownloadAndInstall; + if (shouldInstallAndRelaunch) + { + ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); + } } _itemBeingDownloaded = null; // done downloading, so we can clear the download status, basically } @@ -1773,27 +1656,24 @@ public async Task QuitApplication() // If you have better ideas on how to figure out if they've shut all other windows, let me know... try { - await CallFuncConsideringUIThreadsAsync(new Func(async () => + if (CloseApplicationAsync != null) { - if (CloseApplicationAsync != null) - { - LogWriter?.PrintMessage("Shutting down application via CloseApplicationAsync..."); - await CloseApplicationAsync.Invoke(); - } - else if (CloseApplication != null) - { - LogWriter?.PrintMessage("Shutting down application via CloseApplication..."); - CloseApplication.Invoke(); - } - else - { - // Because the download/install window is usually on a separate thread, - // send dual shutdown messages via both the sync context (kills "main" app) - // and the current thread (kills current thread) - LogWriter?.PrintMessage("Shutting down application via UIFactory..."); - UIFactory?.Shutdown(this); - } - })); + LogWriter?.PrintMessage("Shutting down application via CloseApplicationAsync..."); + await CloseApplicationAsync.Invoke(); + } + else if (CloseApplication != null) + { + LogWriter?.PrintMessage("Shutting down application via CloseApplication..."); + CloseApplication.Invoke(); + } + else + { + // Because the download/install window is usually on a separate thread, + // send dual shutdown messages via both the sync context (kills "main" app) + // and the current thread (kills current thread) + LogWriter?.PrintMessage("Shutting down application via UIFactory..."); + UIFactory?.Shutdown(this); + } } catch (Exception e) { @@ -1855,21 +1735,18 @@ public async Task CheckForUpdatesAtUserRequest(bool ignoreSkippedVer if (CheckingForUpdatesWindow != null) // if null, user closed 'Checking for Updates...' window or the UIFactory was null { CheckingForUpdatesWindow?.Close(); - CallFuncConsideringUIThreads(() => + switch (updateData.Status) { - switch (updateData.Status) - { - case UpdateStatus.UpdateNotAvailable: - UIFactory?.ShowVersionIsUpToDate(this); - break; - case UpdateStatus.UserSkipped: - UIFactory?.ShowVersionIsSkippedByUserRequest(this); // they can get skipped version from Configuration - break; - case UpdateStatus.CouldNotDetermine: - UIFactory?.ShowCannotDownloadAppcast(this, AppCastUrl); - break; - } - }); + case UpdateStatus.UpdateNotAvailable: + UIFactory?.ShowVersionIsUpToDate(this); + break; + case UpdateStatus.UserSkipped: + UIFactory?.ShowVersionIsSkippedByUserRequest(this); // they can get skipped version from Configuration + break; + case UpdateStatus.CouldNotDetermine: + UIFactory?.ShowCannotDownloadAppcast(this, AppCastUrl); + break; + } } return updateData; // in this case, we've already shown UI talking about the new version @@ -1921,12 +1798,16 @@ private async Task CheckForUpdates(bool isUserManuallyCheckingForUpd updates ); - // UpdateDetected allows for catching and overriding the update handling, - // so if the user has implemented it, tell them there is an update and stop - // handling everything. - if (UpdateDetected != null) + if (UpdateDetected != null || UpdateDetectedAsync != null) { - UpdateDetected(this, ev); // event's next action can change, here + if (UpdateDetectedAsync != null) + { + await UpdateDetectedAsync(this, ev); // event's next action can change, here + } + else if (UpdateDetected != null) + { + UpdateDetected(this, ev); // event's next action can change, here + } switch (ev.NextAction) { case NextUpdateAction.PerformUpdateUnattended: @@ -1949,6 +1830,10 @@ private async Task CheckForUpdates(bool isUserManuallyCheckingForUpd { UpdatesHaveBeenDownloaded(updates); } + else + { + ShowUpdateNeededUI(updates); + } break; } } @@ -1975,99 +1860,52 @@ public void CancelFileDownload() } } - /// - /// Events should always be fired on the thread that started the Sparkle object. - /// Used for events that are fired after coming from an update available window - /// or the download progress window. - /// Basically, if is true, just invokes the action. Otherwise, - /// uses the to call the action. Ensures that the action - /// is always on the main thread. - /// - /// - private void CallFuncConsideringUIThreads(Action action) - { - if (ShowsUIOnMainThread) - { - action?.Invoke(); - } - else - { - _syncContext.Post((state) => action?.Invoke(), null); - } - } - - /// - /// Events should always be fired on the thread that started the Sparkle object. - /// Used for events that are fired after coming from an update available window - /// or the download progress window. - /// Basically, if is true, just invokes the action. Otherwise, - /// uses the to call the action. Ensures that the action - /// is always on the main thread. - /// - /// - private async Task CallFuncConsideringUIThreadsAsync(Func action) - { - if (ShowsUIOnMainThread) - { - await action.Invoke(); - } - else - { - _syncContext.Post(async (state) => await action.Invoke(), null); - } - } - private async void OnUpdateWindowUserResponded(object sender, UpdateResponseEventArgs args) { LogWriter?.PrintMessage("Update window response: {0}", args.Result); var currentItem = args.UpdateItem; var result = args.Result; - if (result != UpdateAvailableResult.None && string.IsNullOrWhiteSpace(_downloadTempFileName)) - { - // we need the download file name in order to tell the user the skipped version - // file path and/or to run the installer - _downloadTempFileName = await GetDownloadPathForAppCastItem(currentItem); - } if (result == UpdateAvailableResult.SkipUpdate) { - // skip this version - Configuration.SetVersionToSkip(currentItem.Version ?? ""); - CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); }); + if (currentItem.Version != null) + { + Configuration.SetVersionToSkip(currentItem.Version); + } + UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); } else if (result == UpdateAvailableResult.InstallUpdate) { - await CallFuncConsideringUIThreadsAsync(async () => + if (UserInteractionMode == UserInteractionMode.DownloadNoInstall && string.IsNullOrWhiteSpace(_downloadTempFileName)) { - UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); - if (UserInteractionMode == UserInteractionMode.DownloadNoInstall && File.Exists(_downloadTempFileName)) - { - // Binary should already be downloaded. Run it! - ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); - } - else - { - // download the binaries - await InitAndBeginDownload(currentItem); - } - }); + // we need the download file name in order to run the installer + _downloadTempFileName = await GetDownloadPathForAppCastItem(currentItem); + } + UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); + if (UserInteractionMode == UserInteractionMode.DownloadNoInstall && File.Exists(_downloadTempFileName)) + { + // Binary should already be downloaded. Run it! + ProgressWindowCompleted(this, new DownloadInstallEventArgs(true)); + } + else + { + // download the binaries + await InitAndBeginDownload(currentItem); + } } else { - CallFuncConsideringUIThreads(() => { UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); }); + UserRespondedToUpdate?.Invoke(this, new UpdateResponseEventArgs(result, currentItem)); } - CallFuncConsideringUIThreads(() => + if (result != UpdateAvailableResult.None) { - if (result != UpdateAvailableResult.None) - { - // if result is None, then user closed the window some other way to ignore things so we don't need - // to close it - UpdateAvailableWindow?.Close(); - UpdateAvailableWindow = null; // done using the window so don't hold onto reference - } - CheckingForUpdatesWindow?.Close(); - CheckingForUpdatesWindow = null; - }); + // if result is None, then user closed the window some other way to ignore things so we don't need + // to close it + UpdateAvailableWindow?.Close(); + UpdateAvailableWindow = null; // done using the window so don't hold onto reference + } + CheckingForUpdatesWindow?.Close(); + CheckingForUpdatesWindow = null; } /// @@ -2088,53 +1926,55 @@ private async void OnWorkerDoWork() { break; } - // set state + LogWriter?.PrintMessage("Before starting update loop, pausing for 3 seconds so we don't pop " + + "up update window immediately..."); + Thread.Sleep(3000); bool isUpdateAvailable = false; - // notify - LoopStarted?.Invoke(this); + LogWriter?.PrintMessage("Starting update loop..."); + _syncContextForWorkerLoop.Post((state) => LoopStarted?.Invoke(this), null); // report status if (doInitialCheck) { - // report status - LogWriter?.PrintMessage("Starting update loop..."); - - // read the config LogWriter?.PrintMessage("Reading config..."); Configuration config = Configuration; - // calc CheckTasp bool checkTSPInternal = checkTSP; - if (isInitialCheck && checkTSPInternal) { checkTSPInternal = !_forceInitialCheck; } - - // check if it's ok the recheck to software state TimeSpan csp = DateTime.Now - config.LastCheckTime; + LogWriter?.PrintMessage("Done reading config. !CheckTSPInternal = {0}; csp >= _checkFrequency = {1}", + !checkTSPInternal, csp >= _checkFrequency); if (!checkTSPInternal || csp >= _checkFrequency) { checkTSP = true; // when sparkle will be deactivated wait another cycle + LogWriter?.PrintMessage("Config has check for update true? {0}", config.CheckForUpdate == true); if (config.CheckForUpdate == true) { - // update the runonce feature goIntoLoop = !config.IsFirstRun; // check if update is required if (_cancelToken.IsCancellationRequested || !goIntoLoop) { + LogWriter?.PrintMessage("Cancellation token had cancellation requested " + + "and/or goIntoLoop was false (will happen first time you start the app); stopping loop"); + LogWriter?.PrintMessage("_cancelToken.IsCancellationRequested = {0}; goIntoLoop = {1}", + _cancelToken.IsCancellationRequested, goIntoLoop); break; } + LogWriter?.PrintMessage("In worker thread loop, getting update status..."); _latestDownloadedUpdateInfo = await GetUpdateStatus(config); if (_cancelToken.IsCancellationRequested) { break; } isUpdateAvailable = _latestDownloadedUpdateInfo.Status == UpdateStatus.UpdateAvailable; + LogWriter?.PrintMessage("In worker thread loop, is update available? {0}", isUpdateAvailable); if (isUpdateAvailable) { List updates = _latestDownloadedUpdateInfo.Updates; @@ -2148,7 +1988,23 @@ private async void OnWorkerDoWork() updates[0], updates ); - UpdateDetected?.Invoke(this, ev); + if (UpdateDetected != null || UpdateDetectedAsync != null) + { + var re = new AutoResetEvent(false); + _syncContextForWorkerLoop.Post(async (state) => + { + if (UpdateDetectedAsync != null) + { + await UpdateDetectedAsync(this, ev); + } + else if (UpdateDetected != null) + { + UpdateDetected.Invoke(this, ev); + } + re.Set(); + }, null); + re.WaitOne(); // wait for UpdateDetected to finish even though we called back to UI thread + } if (_cancelToken.IsCancellationRequested) { break; @@ -2161,18 +2017,18 @@ private async void OnWorkerDoWork() { LogWriter?.PrintMessage("Unattended update desired from consumer"); UserInteractionMode = UserInteractionMode.DownloadAndInstall; - UpdatesHaveBeenDownloaded(updates); + _syncContextForWorkerLoop.Post((state) => UpdatesHaveBeenDownloaded(updates), null); break; } case NextUpdateAction.ProhibitUpdate: { - LogWriter?.PrintMessage("Update prohibited from consumer"); + LogWriter?.PrintMessage("Update prohibited by consumer"); break; } default: { LogWriter?.PrintMessage("Preparing to show standard update UI"); - UpdatesHaveBeenDownloaded(updates); + _syncContextForWorkerLoop.Post((state) => UpdatesHaveBeenDownloaded(updates), null); break; } } @@ -2199,8 +2055,7 @@ private async void OnWorkerDoWork() // reset initial check isInitialCheck = false; - // notify - LoopFinished?.Invoke(this, isUpdateAvailable); + _syncContextForWorkerLoop.Post((state) => LoopFinished?.Invoke(this, isUpdateAvailable), null); // report wait statement LogWriter?.PrintMessage("Sleeping for another {0} minutes, exit event or force update check event", _checkFrequency.TotalMinutes); @@ -2250,7 +2105,7 @@ private async void OnWorkerDoWork() } } while (goIntoLoop); - // reset the islooping handle + // reset the looping handle if (!_disposed) { _loopingHandle?.Reset(); diff --git a/src/NetSparkle/SparkleUpdaterEvents.cs b/src/NetSparkle/SparkleUpdaterEvents.cs index 609291a0..47a898e2 100644 --- a/src/NetSparkle/SparkleUpdaterEvents.cs +++ b/src/NetSparkle/SparkleUpdaterEvents.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Diagnostics; +using NetSparkleUpdater.Events; namespace NetSparkleUpdater { @@ -21,10 +22,23 @@ public partial class SparkleUpdater : IDisposable public event UpdateCheckStarted? UpdateCheckStarted; /// /// This event can be used to override the standard user interface - /// process when an update is detected + /// process when an update is detected. To modify the next action, + /// change the value of the + /// property. + /// If both this and UpdateDetectedAsync are implemented, UpdateDetectedAsync is + /// used, and UpdateDetected is not called. /// public event UpdateDetected? UpdateDetected; /// + /// This event can be used to override the standard user interface + /// process when an update is detected. To modify the next action, + /// change the value of the + /// property. + /// If both this and UpdateDetected are implemented, UpdateDetectedAsync is + /// used, and UpdateDetected is not called. + /// + public event UpdateDetectedAsync? UpdateDetectedAsync; + /// /// Called when update check is all done. may have been /// called between the start and end of the update check. ///