Skip to content
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

Auto-update functionality #50

Open
wants to merge 78 commits into
base: main
Choose a base branch
from

Conversation

dbampersand
Copy link
Contributor

This is regarding this issue: #42

The code in this PR first checks if the version number defined in does not match the most recent version on Github. If it doesn't, it downloads the zip, unpacks it and then copies it over the root directory. My linux partition is broken right now so only the Windows sections of the code have been tested.

Let me know if there's anything I can improve on in this PR!

Thanks!

tsa96 and others added 30 commits July 2, 2024 01:47
Omitting LangVersion causes compile to use latest C# version corresponding that that .NET release. So essentially updating .NET version also updates language version.

https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version
Includes fix from Jun 22.
Messy commit, see code review comment
This commit no longer needs some of these formatting since we'll run CSharpier soon, but I'd have to spend ages resolving merge conflicts to get rid of them, just ignore. (Tom 09/07)
Discussed with BIRD and he agrees, simplest if we explicitly use ASCII everywhere.
I was a bit confused why we had some cases of Encoding.Default and others with ASCII, good to be explicit.
…me places

I decided to merge two commits together as they had so much overlap.
I'm getting the use of underscores for private members consistent, then doing some
hard renames like Property -> EntityProperty (I really really hate use C# words like "Property" as classes)
NLog is more flexible then MS's Logger, can be configured via nlog.config, and much nicer to use without DI.
Sorry, hard to split this up well.
Stripper: didn't touch much
RunExternalTool: altered to always use a tmp file, and to log the output of the executable.
ReplaceTexture: rewrote so string/regex replacing is combined. bit more code here, but the UI code is drastically better because of it.
Compression: Deleted, it's just put of the save process now.
Being lazy and doing this all in one commit. Used Rider to delete all the unused, shouldn't need reviewing.
Can't take a ref to a property (cursed), so make the class implement a resize method
- Support for compression pakfile items
- Switches from storing compression state/desired compression on lumps, save message gets passed it
- Adds progress updating
- Adds cancellation support
- Improved debug ouput
Fixes momentum-mod#45.
I wasn't updating compression state properly, screwed everything up.
Gamelumps were a bastard.
@dbampersand dbampersand force-pushed the Auto_update branch 2 times, most recently from b4c5c0c to dcac590 Compare July 16, 2024 14:53
@tsa96
Copy link
Member

tsa96 commented Jul 16, 2024

Oh and for commit messages please see https://github.com/momentum-mod#contribution-guidelines.
Sorry if Git stuff is a bit much, we just like the keep the tree as organised as possible and prefer to fixup changes brought up in review rather than separate commits. Let me know if you have any issues, and really appreciate you working through all these change requests!

@dbampersand
Copy link
Contributor Author

Hey - thanks a ton for the code review, it means a lot! I've rebased and made the requested changes.

There's significant code duplication in the method HttpDownload as I modified that from BspService.cs in order to grab the progress bar code - I think that could be replaced inside a more generic class but I don't want to step on toes while you're refactoring the UI.

Let me know if this works.

Thanks!

Comment on lines 79 to 80
throw new Exception("Could not parse Major/Minor/Patch version.");
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing always halts execution, it's impossible for return null to ever be reached. That's why I prefer inverting the above conditional and doing if (!match.Success) throw new InvalidDataException(...), then you don't need to indent the code block doing the version parsing. Just a bit clearer to the reader, they don't need to read back upwards to the match.success conditional to check why we're throwing here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're still not throwing with if (!match.Success) throw ..., your current approach is a lot harder to read.

src/Lumper.UI/Updater/Updater.cs Outdated Show resolved Hide resolved
src/Lumper.UI/Updater/Updater.cs Outdated Show resolved Hide resolved
}
else
{
throw new Exception("Could not connect - error " + response.StatusCode);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to avoid generic exceptions, there's probably some HttpException or similar provided by the HttpClient library. Usually easy to find in an editor with code completion, just type Http and let it autocomplete.

Comment on lines 145 to 150
ButtonResult result = await MessageBoxManager
.GetMessageBoxStandard(
"Error",
ex.Message, ButtonEnum.Ok)
.ShowWindowDialogAsync(Program.Desktop.MainWindow);
return null;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really don't need to be catching exceptions here. Just put a big try/catch in the Update() function and let these bubble up.

Comment on lines 357 to 362
ButtonResult result = await MessageBoxManager
.GetMessageBoxStandard(
"Error",
"IO error: " + ex.Message, ButtonEnum.Ok)
.ShowWindowDialogAsync(Program.Desktop.MainWindow);
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More message boxes that should be logger errors

try
{
var fileURL = GetPath(assets, OSPlatform.Linux);
var fileName = "linux_" + latest.ToString() + ".zip";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if you do $"linux_{latest}.zip" tostring should get called for you!

Comment on lines 406 to 303
{
ButtonResult result = await MessageBoxManager
.GetMessageBoxStandard(
"Error",
"IO error: " + ex.Message, ButtonEnum.Ok)
.ShowWindowDialogAsync(Program.Desktop.MainWindow);
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Message box grr

GroupCollection currentVersion = match.Groups;


int.TryParse(currentVersion[1].ToString(), out var major);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryParse here returns a boolean whether the parse was successful, which you're not using. TryParse has an evil sibling, Parse, which will throw if it fails. But as I've rambled about below, throwing for a parsing error like that is completely fine. In fact it'd be very weird if the parse failed here, given our regex was successful! So all you need is

return new Version(int.Parse(currentVersion[1].Value, currentVersion[2].Value, currentVersion[3].Value))`

note I'm using .Value on the GroupCollection instead of ToString. If you peek at the source, ToString just returns Value, but Value is already a string and is what you should be accessing.

using Newtonsoft.Json;
using NLog;

internal sealed partial class Updater
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't really think internal is necessary here.

@tsa96
Copy link
Member

tsa96 commented Jul 16, 2024

Hey - thanks a ton for the code review, it means a lot! I've rebased and made the requested changes.

There's significant code duplication in the method HttpDownload as I modified that from BspService.cs in order to grab the progress bar code - I think that could be replaced inside a more generic class but I don't want to step on toes while you're refactoring the UI.

Let me know if this works.

Thanks!

Getting there! Sorry for chucking so much at you, hope the comments are informative. If you have questions about anything or what to discuss I'm happy to talk about this in our Discord in text chat or voice.

I don't think the code duplication of HttpDownload is too bad, since I think your version should be a little different anyway, with the outer method controlling the IoHandler and IoProgressWindow.

Oh, and if you could fixup the changes you've made in review to the original commits that'd be appreciated, see my comment further up. Very happy to help with any Git issues, no idea how much experience you have and rebasing is hard at first! I appreciate the effort to split the commits up but in practice I don't find it makes review easier and I'd rather not squash this branch when merging since the commit changing the AssemblyVersion is worth keeping separate.

Comment on lines +57 to +60
ButtonResult result = await MessageBoxManager
.GetMessageBoxStandard(
"No updates available",
"No updates available", ButtonEnum.Ok)
.ShowWindowDialogAsync(Program.Desktop.MainWindow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to do a Logger.Info here I think. Maybe I'm weird but I don't like random message boxes popping up!

Comment on lines 42 to 49
Lumper.UI.Updater.Updater.Version? updateAvailable = await Lumper.UI.Updater.Updater.CheckForUpdate();
if (updateAvailable != null)
{
string updateNumber = updateAvailable.ToString();
ButtonResult result = await MessageBoxManager
.GetMessageBoxStandard(
"Update Available",
$"An update is available ({updateNumber}), do you want to download and restart?", ButtonEnum.OkCancel)
.ShowWindowDialogAsync(Program.Desktop.MainWindow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually sorry, I suggested returning null here but I think a bool would be way easier to understand. So make method signature would be
bool CheckForUpdate(out Version version)

then in the method have something like

version = latest;
return (current != latest);

then here would be

        var updateAvailable = await Lumper.UI.Updater.Updater.CheckForUpdate(out Version version);
        if (updateAvailable)
        {
             ButtonResult result = await MessageBoxManager
                .GetMessageBoxStandard(
                    "Update Available",
                    $"An update is available ({version), do you want to download and restart?", ButtonEnum.OkCancel)
                .ShowWindowDialogAsync(Program.Desktop.MainWindow);

Also import is quite weird, just do import Lumper.UI.Updater then you can do just e.g. Updater.CheckForUpdate()

Comment on lines 74 to 75
<Separator />
<MenuItem Header="Check for updates" Command="{Binding UpdateCommand}" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's split this into an "About" section that just has this.

@tsa96
Copy link
Member

tsa96 commented Jul 16, 2024

@BIRD311 do you want to take a look at this? Really hate to burden you with every more review work, but if you were up for testing on linux and maybe glancing at the code that'd be great. Up to you!

@dbampersand dbampersand force-pushed the Auto_update branch 2 times, most recently from eb0f36d to 71e4b76 Compare July 17, 2024 03:13
@dbampersand
Copy link
Contributor Author

Hey - thanks a ton for the code review and thanks for your patience. I've made the changes and squashed them. Hoping I did that correctly! The one change I couldn't do was regarding CheckForUpdate - C# doesn't support async with 'out' modifiers, so I changed that to a Tuple that contains the success/fail and the Version. I'll see if I can test the Linux code later.

@tsa96
Copy link
Member

tsa96 commented Jul 17, 2024

On phone so hard to review but I noticed you're using Tuple when you can use the new tuple syntax of e.g. (bool, Updater.Version), so return a Task<(bool, Updater.Version)>. Then you can save the Item1, Item2 crap by destructuring e.g. "(var success, Updater.Version version) = await GetUpdate(); (I forget the rhs method name here)

For Git stuff, probably the easiest approach is to do a mixed reset to unstage everything, then just recommit in two different commits. To clarify what we want the commit tree to look like, we want

  • a commit adding the assembly version: feat(ui): specify AssemblyVersion of Lumper.UI
  • a commit adding all the updater code: feat(ui): add auto updated system

Anything brought up in review should just be fixup/amended to that second commit (just stage those changes then git commit --amend --no-edit)

I noticed dotnet format formatted a bunch of csproj files that it shouldn't, this is because we haven't finished the auto formatting system yet, please just discard any changes made to files you're not interacting with in this PR. For the assemblyversion thing, probably easiest to just discard all changes to that file then add the assemblyversion line back in your editor. Sorry this is so messy, will be sorted soon!

To recommit I'd do

git branch backup // make backup branch but don't checkout, in case you mess up you can reset back to here and start over , can delete at end
git reset --mixed HEAD~4 // or wherever the first commit before your first commit is
// remove csproj formatting stuff from -  lumper ui csproj
git add lumper.ui.csproj
git commit -m <above message I wrote>
git add updater.cs, MainWindowViewModel.cs, any other stuff you modified (try to avoid auto formatting stuff!)
git commit -m <other above message>
git push --force

Check logs to make sure yt makes sense before pushing. This is rushed sorry, on phone in a coffee shop and only have a few minutes. If struggling with any git stuff I highly recommend a graphical client, my preferred client is Fork https://git-fork.com/ (free, just slams asking for money occasionally)

@dbampersand
Copy link
Contributor Author

Thanks for the patience and the explanation - I've fixed the commit history now and it should be clean of the formatting edits.

@tsa96
Copy link
Member

tsa96 commented Jul 17, 2024

Aaaalmost, just you've still got a fixup commit. If you just reorder the commit with the UI stuff and the commit with the assemblyversion stuff, you can easily amend any changes you have to make to the UI stuff using git commit --amend --no-edit, which will amend any staged changes into the most recent commit.

To order do git rebase -i HEAD~3 and just swap the order of the commits in the text editor that Git will open, then save and close the editor, and you'll have switched the order around.

Also, I'd prefer you keep the commit messages shorter, as you can see in the commit messages when shown on Github get trimmed awkwardly. Also "updater" isn't really a valid scope, I'd just use the messages in my above message. You can rename during the interactive rebase above. When you enter the rebase, you'll see an editor looking something like this

pick aaaaaaaa feat(updater): added checking and automatically downloading/applying …
pick bbbbbbbb feat(Updater): added AssemblyVersion tag to Lumper.UI.csproj allowing…
pick cccccccc fixup! feat(updater): added checking and automatically downloading/ap…

to reorder and reword, change these lines to

reword bbbbbbbb feat(ui): specify AssemblyVersion of Lumper.UI
reword aaaaaaaa feat(ui): add auto updater system
fixup cccccccc fixup! feat(updater): added checking and automatically downloading/ap…

that'll reword the existing commits, switch their order, then fixup the commit with the "fixup!" on the front into the commit above it. Sorry if that's a bit overwhelming, but if you can make sense of that, you've just learnt probably the single most powerful feature in Git!

@dbampersand dbampersand force-pushed the Auto_update branch 2 times, most recently from a2c689b to 1c02fff Compare July 17, 2024 22:52
@dbampersand
Copy link
Contributor Author

Got it (I think!). Sorry, I'm not great with git but I appreciate the hand holding.

@tsa96
Copy link
Member

tsa96 commented Jul 17, 2024

Yeah looks good. And no problem, we're pretty exacting with structuring commits so naturally we're happy to help people struggling with it. Git is a hassle to learn at first, with a steep learning curve, but given how ubiquitous it's become, it's well worth putting the time into learning!

I had a quick glance at your commits and mostly looking good though spotted a few small things, one that you're still using Tuple<A, B> over just (A, B), mentioned in my commit above. It's really just a syntactic thing but would prefer using it. However, I'm exhausted and need to head off, so I'll go through stuff fully tomorrow morning,

Really appreciate the work by the way, and sticking through everything brought up in review. We'll soon be encouraging everyone working on map porting to start using Lumper, which is a vital part of getting Momentum in a releasable state. Great to have other devs working on it!

Copy link
Member

@tsa96 tsa96 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay sorry, since this is taking a while and finding it harder to explain some of the larger issues, I'm going to just checkout this branch and clean it up a bit, rather than post code snippets. I'll make a separate commit for it, please take a look through the changes and make sure they make sense to you, then fixup my commit in yours when ready.

/// Checks for possible update on the Github releases page by the tag name
/// </summary>
/// <returns>A tuple where the first value defines if there is an update ready, and the second value is the latest Version</returns>
public static async Task<Tuple<bool, Version>> CheckForUpdate()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task(bool, Version)

Comment on lines 148 to 156
GithubUpdate assets;
assets = await GetGithubUpdates();

//parse tag name to find the current and latest version
//finding the format of xx.yy.zz
Version current;
Version latest;
current = GetVersionFromString(Assembly.GetExecutingAssembly().GetName().Version.ToString());
latest = GetVersionFromString(assets.TagName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        GithubUpdate assets = await GetGithubUpdates();

        // Parse tag name to find the current and latest version
        // Finding the format of xx.yy.zz
        Version current = GetVersionFromString(Assembly.GetExecutingAssembly().GetName().Version.ToString());
        Version latest = GetVersionFromString(assets.TagName);
        return (current != latest, latest);

}
public static async Task<Stream?> HttpDownload(string url, string fileName, IoProgressWindow progressWindow, IoHandler handler, CancellationTokenSource cts)
{
//the amount the progress bar should go up to while downloading - we still have to unzip for the other percentage
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please capitalise comments and put a space after //. I know some comments in the pre-refactor/cleanup looked like this but would rather be consistent from now on. It takes no effort for programmers to capitalise properly, even though it's being very pedantic, I don't see a good excuse for not capitalising!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Further comments below, please can you scan through and upset all of them. Sorry to be so annoying about this!


private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

private const float DownloadProgressPercentage = 80;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird to put this so far from the code actually using it. You can use const for a variable declaration inside a method, doesn't have to be a field, just put it in the http downloading stuff!

}

}
throw new Exception("Could not find download link for " + nameof(OS));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nameof(OS) is just "OS", you want + OS);

Comment on lines 293 to 294
List<Task> tasks = new List<Task>();
List<FileStream> fileStreams = new List<FileStream>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Best to use var on LHS, no need to specific a type of both sides of the assignment.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nvm these were totally unused!

/// Downloads an update for the program, applies it, and then restarts itself with the new version
/// </summary>
/// <returns></returns>
public static async ValueTask Update()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole method can be reorganised to avoid all the dupe code. For sake of time I'm going to just write the whole thing without the branching, please use it!

{
assets = await GetGithubUpdates();

Version? latest;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be Version latest = GetVersionFromString(assets.TagName); note that latest is non-null. Idk if your editor has LSP supported but this should be warning at you about this, don't ignore it!

Comment on lines 227 to 234
var operatingSystem = "";

if (OS == OSPlatform.Windows)
operatingSystem = "win";
else if (OS == OSPlatform.Linux)
operatingSystem = "linux";
else
throw new ApplicationException("Unsupported OS: " + OS);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're picking this same string in Update, so let's just pass that in to this function. I'll post a rewritten version of Update below, you can modify this function to take a string input.

/// Parses a string to find the major/minor/patch versioning
/// </summary>
/// <param name="s">String containing a version number following the format 'xx.yy.zz'</param>
private static Version GetVersionFromString(string s)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better name is ParseVersion

@tsa96
Copy link
Member

tsa96 commented Jul 18, 2024

Okay done, though haven't tested. Please take a look and make sure everything works and makes sense to you, then fixup into your commit!

Spent a while trying to do nice JSON parsing but since we're using Newtonsoft stuff still, we can't use some of the nice new stuff outlined here. Oh well!

@dbampersand
Copy link
Contributor Author

Thanks for that - I've commited a couple of fixes:

    private record Asset
    {
        [JsonProperty("browser_download_url")]
        public string BrowserDownloadUrl { get;}

        [JsonProperty("name")]
        public string Name { get; }
    }
    private record GithubUpdate
    {
        [JsonProperty("tag_name")]
        public string TagName { get; }

        [JsonProperty("assets")]
        public Asset[] Assets { get; }

    }

To work properly JsonConvert.DeserializeObject needs the setter otherwise it won't update the fields.

   osName = "windows";

The file for Windows is along the lines of "Lumper_v0.1.1_win-x64.zip" so searching if the string cotntains "Windows" fails.

@tsa96
Copy link
Member

tsa96 commented Jul 19, 2024

Oh gotcha, both those fixes make sense.

We're probably about ready to merge but going to give BIRD the opportunity to look at this, and not sure if we'll merge this into refactor/cleanup straight away, or wait until that branch is in first.

By the way, I have a big load of Lumper issues I was meaning to make issues for and haven't had a chance to, if you're interested in helping out further keep an eye on the issues board!

@tsa96
Copy link
Member

tsa96 commented Jul 19, 2024

Just thinking about this a little today, might be a better approach to pull the updating logic out to a separate executable so we can copy and replace stuff easier. Though I guess we'd have to do some work on startup of the main app to determine if we've just updated, and if so, replace the old version of the updater executable with the new one.

Not sure we need to do this, want to hear what others think in our team meeting tomorrow.

Copy link
Member

@BIRD311 BIRD311 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only skimmed this, don't personally care much about the feature. Thank for helping.

Comment on lines +285 to +286
var fileName = $"{osName}_{latest}.zip";
var directoryName = $"{fileName}temp";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write the download to /tmp so it will be cleaned up if something goes wrong and we don't make it to the rm in code. There should be a c# function to get the temp folder or a temp file independent of the platform we're on.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't sure how long this is guaranteed to last, turns out on Windows, the OS doesn't clean up the files for you! I was under the impression they were wiped on reboot, which AFAIK is what all unix-like systems with a /tmp do. So, we should still rm the file after.

Comment on lines +334 to +340
os == OSPlatform.Linux
? $"""
sleep 2 &&
yes | cp -rf "{currentDirectory}\{directoryName}" &&
rm "{currentDirectory}\{fileName}" &&
rm -rf "{currentDirectory}\{directoryName}" &&
./Lumper.UI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • copy doesn't have a destination
  • rm -rf is scary, try moving files instead
  • you can't rely on the current directory being where ./Lumper.UI is
  • forward slashes in the path

@tsa96
Copy link
Member

tsa96 commented Jul 21, 2024

Discussed this a bit yesterday and keen to avoid the shell scripts. Most of the code here we'll still use, but we'll probably pull some of it out to separate executable.

Going to wait until refactor/cleanup is in, then I'll start a new branch off there and pull in some of these changes and create a separate little project. That way I finish auto updating and release and QA CI jobs at once - expect I'll need the AssemblyVersion addition for that. So not sure if this PR itself will get merged in the future, but I'll make sure to cherry pick your current commits so you get credit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants