-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
2.x UserDriver API Redesign #1551
Comments
And, obviously, for folks that don't want to create a custom UI or manage the presentation themselves, there would be a default fallback that shows UI just as Sparkle currently does. But the core "update checking" element of Sparkle would be separate from that "presentation layer" of Sparkle so that the presentation layer could be easily replaced at any time. |
Also, I realize this is a super-opinionated post. It is in no way my intent to disparage the work that's been done or insult the choices made—I've shipped millions of lines of code; I understand how things evolve and why choices from long ago result in complex interdependencies today. I'm simply suggesting that a 2.x release is an excellent chance to do the hard work of separating the core logic and presentation logic and paying down some of the technical debt accumulated since the early 2000s. |
I used the term "delegate" in my original post, but I realize that the existing updater has both a "driver" and a "delegate" and that those are separate things. "Presentation Driver" may be a less-confusing description than "delegate" in light of this. Basically, if I |
The more work I do implementing a custom UI with the current Sparkle 2 API, the more I think a redesign is warranted. For example, consider this method:
I gather this method means "Show the little window that says 'checking for updates' with a cancel button and if the user clicks the cancel button, call the reply block with a 'cancel' value. Otherwise, call the reply block at some point in the future after I call a few other methods on you." But then the next method is:
And the docs for that method tell me this is a good time to call the reply block that was passed with the FORMER method. I gather this method gets called when Sparkle is done checking for updates and either has one to show or there isn't an update available. But now I have to save the reply block from the first method somewhere AND keep track of whether I've called it or not. This sort of pattern plays out over and over as far as I can tell. It's not good. There shouldn't be blocks and complex callbacks like this. The process should go:
I don't have to tell Maybe I'm missing something, but the current approach seems VASTLY more complicated and interwoven than it needs to be. |
I also think it's a bad idea to allow all of these protocol methods to be called from any thread. The documentation comments are misleading because, in general, the comment "This method can be called from any thread" means: "YOU can call this method on ME from any thread; I'm prepared to handle that." But in Sparkle's case, I think what the comments are trying to say is: "Sparkle will call this method on YOU from any thread, and you better handle that." Folks aren't going to realize that and they're going to forget to wrap each method's implementation in a dispatch to the main queue, so you're going to get a lot of people who update UI from background threads, which will produce crashes—but not always reliably, which makes it tough to debug. And, the "any thread" thing violates the spirit of this API. I mean, the whole reason we're using it is to do custom UI work, right? That means our implementations are going to need to be on the main thread, so Sparkle should take care of that for us—not force the users of the API to wrap a thousand things in dispatch blocks to the main thread. |
I think the general issue is that Sparkle was originally designed to be a completely automatic, one-stop-shop for updating apps. The update-checking, update-presenting, and update-installing stages were all tightly coupled and interdependent. If we want to expand Sparkle to allow more flexibility and customization, we need to divorce those three elements from each other and make them loosely-coupled. Doing that now, before 10,000 apps adopt this new API, is a good idea. It's a ton of work, but it'll be worth it when the new API is super clean and super simple. |
That makes sense. But there's one detail that makes Sparkle dependent on what the UI does: it needs to know if an update is aborted, so it can delete temp files and reset state. So you can't completely ignore the UI, you would need to reply when it's irrelevant/rejected. I agree these methods should be merged. It could be |
That’s pretty straightforward: if the app gets to termination and “updater” has not received a “install the update” command from me, it simply deletes temp files and clears state. I see it as Updater having an update “staged” and ready to go. If I then tell Updater to re-check for updates, it can clear that staged one out and redo the whole process. And, of course, I can still tell it to abort at any time, which will also trigger the clearing and state reset. But that shouldn’t be REQUIRED to keep Sparkle in a consistent state. |
To be clear, though: I’m proposing a large restructuring of Sparkle’s core architecture and flow, not a few minor changes to this existing API. I’m happy to help do that, but it would take more than just me. The good news is that all of the really “hard” stuff Sparkle does would not be touched and can continue as-is. The downloading, extracting, verification, and installing code wouldn’t be modified. What WOULD be modified is the way Sparkle reports each step to the “UI Presenter” layer and waits to take action. |
I agree. That's a useful refactoring. Sparkle started with a very coupled architecture where update state is driven by the UI, and it decoupling them more would make both processes easier to follow. |
Is there anyone else that needs to be on board? You look like the primary maintainer for the last five years. I'm currently implementing the 2.x custom API in one of my apps and doing that will give me a good feel for the process Sparkle is currently using and how it can be improved. Also, I did find one bit of future-proofing that made me chuckle: Sparkle uses an unsigned 64-bit integer for the download length of an update. This means it's prepared to handle an update that's 18.4 exabytes in size. For context, ALL OF GOOGLE is about 15 exabytes (https://what-if.xkcd.com/63/). |
Yes, I'm mostly keeping lights on on the project. I'm happy for you to lead the design. As for 64-bit sizes: I wouldn't be surprised if someone tried to send a 4GB update sooner or later :) |
Sounds good. It’s going to take me a while to get that going, but I’m happy to take a crack at it. Should we consider using Swift? I’m fine either way, but I get the feeling that Apple is going to start releasing Swift-Only stuff at a more rapid pace in the future. Looking down the road 10 years, getting started on the migration to Swift might be a good idea. If not, I’m happy to use ObjC. Swift ticks me off on a daily basis, anyway. |
I don't mind adopting Swift in the long term, but I suggest doing one thing at a time. I'm worried that it could snowball into a major rewrite, and take too long to ship. |
Got it. I basically envision refactoring the core “updater” to have a “presentationDelegate” property. It will send the delegate a series of messages (much like the current 2.x API) but won’t be reliant on the reply blocks, etc. In other words, Updater will just inform the presentationDelegate of its current status and wait to be told what to do. I plan to guarantee that the delegate methods are called only on the main thread, since that’s convenient for the API consumer. I would not rewrite existing Sparkle classes in Swift, but I would consider using Swift for the new classes and protocols I’d be adding, if that makes sense. (Including the new, default “presentationDelegate” class.) I’d avoid using structs and swift-only features so that ObjC apps can still use the new 2.x API. One less thing to move to Swift down the road. But, if it’s an issue I can write it in ObjC instead. |
Hi all, just stopping by and this caught my interest a bit. I agree greatly that the API design and architecture should be reviewed on that branch for anyone using it. Just for more context: The branch went through several major iterations and the threading concerns in the user driver are a relic of supporting a case where the user driver & updater/scheduler could be in separate processes (via NSXPC). This worked out mostly badly and should be dropped/forgotten IMO, should be trivial to verify/ensure it's "all main thread only". It's also why all the methods return void, reply blocks only have one parameter, all outstanding replies need to be responded, see user driver header docs for more details. For simplifying pain points which I agree there are, one should be aware of the different update states Sparkle (and this branch) can be in. There are different sort of questions and some different sort of responses presented to the user depending if the update has already been downloaded or not, or if the update has already started installing and is not cancelable, or if update is just informational and so on. It would be incorrect to present "download the update" if the update has already been downloaded or "skip update" if the update has automatically been waiting to install when the app quits without possible cancellation, and so on. On topic of review other than UI (but of intermingled relevance), there was a great deal of architectural changing between the updater and the installer (agent/daemon) as well which involved multiple stages of message passing. I created a couple of documents in Documentation/ (namely Installation.md and Security.md) for things I've forgotten by now.. It's been a few years since I've looked back at this :). [edit]: I also am not sure if I was too harsh to deprecate the SUUpdater shim for adoption despite its coupling and instantiation problems.. [edit 2]: Since I don't follow too closely here, feel free to email me if wondering why a particular thing is the way it was designed/written and if I can recall why, would probably be glad to respond. |
Reading a bit more here, There may be pain points with user driver managing changing state however across different methods and I think that may be interesting to review over. There is also UI-linking decoupling consideration. For example, the sparkle updater is UI-free so it should not rely on a dependency on the standard user driver/presenter if it has a UI dependency, thus should not have a way to "use default presenter if supplier passes nil"; moreover, nullability types should be minimized when possible from API design point. I had also wrote a SPUStandardUpdaterController class that composed of the updater and user driver with sensible defaults analogous to 1.x SUUpdater. |
Having the [edit]: Well maybe it makes sense if one would ever envision an app to have a different UI based on whether an update was user-initiated or not. Not so much in the case for the absence of a UI though. |
I spent some time looking at the Sparkle codebase and thinking about the re-architecture. I also completed an integration using the existing API for a custom UI, which helped me grok the flow. Here are my thoughts:
|
Be sure to carefully visit the 2.x branch before another rewrite (it was a rewrite architecturally). It already decouples the UI and business logic of the project. That is also a small part; architecturally it's otherwise significantly changed internally and needed to decouple more than just that. I'm not sure how many people that are using it are aware of the internal architecture and all the decisions made that they aren't exposed to directly, which is why I advised the above documents. Some decisions in how the installer works, while decoupled, for example is still related to how the UI API was designed. Swift is a so-so choice and doesn't inherently solve any threading aspect I'd think. (Again the threading concern brought up here was misunderstood; they are called from the main thread and ought to consider it a 'doc' bug now). |
I love Obj-C, but the writing is on the wall: Swift is the future of Apple platforms. Swift annoys the hell out of me in many ways, but there isn’t really a choice to he made here, unless we want to rewrite everything again in five years or so. (I see it like the choice between using Carbon or Cocoa 17 years ago.) I did plan to use the 2.x branch as the starting point. I do see that it enforces the main thread for almost all those API calls where the docs suggest they might be called on any thread. Swift’s value types would help us solve shared mutable state issues. |
For clarity it should all be main thread unless the user driver one passes in is a remote XPC object, which nobody should / want to do. I agree with Kornel's take on Swift and prioritization here. |
I get your feeling of "this is a mess, let's start over clean". This feels like an easy fix, but for the project it's dangerous:
This may turn out to be a Python 2/3 situation. v2 is 90% ready. It needs EdDSA PR's finished, and we could start moving people over to it. Then we could kill off 1.x. If you make API changes in v2 in roughly the direction you want, then you can polish and reimplement them in Swift in v3 later, offering more gradual migration path. |
@kornelski I couldn't agree more. The problem is that I don't think I can make those changes without a large restructuring. That should absolutely not hold up 2.x, because having used this branch extensively now in one of my apps, it's a solid improvement over 1.x. The API may not be ideal, but it is functional. I'd recommend that we verify that the SPUUserDriver methods are all guaranteed to be called on the main thread, then remove those "this may be called on any thread" comments (@zorgiepoo indicated they are no longer correct). A Swift port of Sparkle would be a good V3 and that would be a great time to drop legacy stuff, simplify and rearchitect the project. |
There are good points brought up here about this particular API.
In my experience of touching most of Sparkle's architecture, it's not all that bad (although it's been a while). I think rewriting things should generally be done a small modular level, piece by piece, where there are understood benefits, and where the old code and implications are well understood. Dropping legacy support of things is not likely hard from an engineering perspective. Tough problems can often have necessary complexity and be language agnostic. Compatibility is an important concern as well. |
All the concerns I see here are cleaned up recently, and not any significant overhauling changes were needed:
I still think this approach with the showable protocol is right over an optional delegation one. |
I have immense respect for Sparkle and I am positive there are implementation details and issues of which I am not aware. That being said, I think the 2.x Custom UserDriver API should be re-thought.
It looks like what happened is that Sparkle's own internal flow for presenting UI elements was exposed, as-is, as a protocol. This isn't really ideal. For example, we have these three nearly-duplicate methods:
All three are required to be implemented by anyone adopting the protocol, but I frankly don't care if an update is already downloaded or resumable—it has no effect on the UI I'm going to present to the user. If I DID care about this information, there would be a much better, cleaner way to get it: have the core Sparkle update-checker inform me that an update is available and allow me to inspect a property of that update to see if it's already downloaded or resumable, etc.
Proposal:
Completely divorce "Sparkle the Update Checker" from "Sparkle the Update Presenter".
Here's how Sparkle should really work:
I
init
the Sparkle Updater and assign a delegate. During this process, I set the bundles, tell Sparkle the feed URL, etc. In other words, I configure the updater.I tell the updater to go check for updates and notify my delegate when it's done.
The updater calls my delegate and says:
A) I found an update. Here's the
SUAppcastItem
for it.B) I didn't find any updates.
C) I hit an error finding updates: there's no internet connection
My delegate decides when and if I'm going to present any UI in response to these events. Sparkle should never care if I do or don't take action; it just tells me what's available.
When I decide to present a UI for an update, I pull properties off the
SUAppcastItem
such as release notes, whether the update is already downloaded, whether the user previously delayed installing this item, etc.I call a method on the updater:
A) Install this AppCast item.
B) Skip this AppCast item permanently.
C) Mark this AppCast item to remind the user later.
The updater sends its delegate a message: "Hey, I'm ready to install and relaunch now". But it doesn't just wait for a reply to that. Instead, it just stops and sits there until I call something like
-installAndRelaunch
.In Short
The existing protocol is like this:
"It's time to show UI element X"
"It's time to show UI element Y, let me know what the reply is"
"It's time to show UI element Z, let me know what the reply is"
"It's time to close UI element X."
"It's time to close UI element Y."
I think a better structure is for Sparkle (the core updater) to simply check for updates, notify its delegate of the progress/results, and then wait to be told to do something.
This mixing of the core update-checker logic and the UI presentation logic isn't clean. It makes using Sparkle 2.x very tedious. And it's going to be a nightmare to transition to SwiftUI or Swift when the time comes.
Rather than ship the 2.x branch and then support it for a decade, I recommend we take the time to rearchitect the "separation of powers" now so that Sparkle is set up for decades to come.
The text was updated successfully, but these errors were encountered: