diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c115693af7..84f26ffa56 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,18 +30,18 @@ jobs: image: mono:${{ matrix.mono }} steps: - - uses: actions/checkout@v3 - - name: Adding HTTPS support to APT for old Mono images run: | apt-get update || true apt-get install -y apt-transport-https + - name: Installing checkout/build dependencies + run: apt-get update && apt-get install -y git + - uses: actions/checkout@v3 + - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: dotnet-version: '7' - - name: Installing build dependencies - run: apt-get update && apt-get install -y git - name: Install runtime dependencies run: apt-get install -y xvfb - name: Restore cache for _build/tools diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9b6e93b8c4..8a3fbfe129 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,6 +19,8 @@ jobs: image: mono:latest steps: + - name: Installing checkout/build dependencies + run: apt-get update && apt-get install -y git make sed gzip fakeroot lintian dpkg-dev gpg createrepo - uses: actions/checkout@v3 - name: Check version @@ -35,8 +37,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: '7' - - name: Installing build dependencies - run: apt-get update && apt-get install -y git make sed gzip fakeroot lintian dpkg-dev gpg createrepo - name: Installing runtime dependencies run: apt-get install -y xvfb - name: Install Docker @@ -119,6 +119,9 @@ jobs: echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin ./build docker-metadata --exclusive + - name: Create a version.json file for S3 + shell: bash + run: python bin/version_info.py > _build/repack/Release/version.json - name: Push ckan.exe and netkan.exe to S3 # Send ckan.exe and netkan.exe to https://ksp-ckan.s3-us-west-2.amazonaws.com/ uses: jakejarvis/s3-sync-action@master diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 719ae366bf..a283ad2dbb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,14 +13,14 @@ jobs: image: mono:latest steps: + - name: Installing checkout/build dependencies + run: apt-get update && apt-get install -y git make sed libplist-utils xorriso gzip fakeroot lintian rpm wget jq dpkg-dev gpg createrepo - uses: actions/checkout@v3 - name: Setup .NET Core uses: actions/setup-dotnet@v3 with: dotnet-version: '7' - - name: Installing build dependencies - run: apt-get update && apt-get install -y git make sed libplist-utils xorriso gzip fakeroot lintian rpm wget jq dpkg-dev gpg createrepo - name: Installing runtime dependencies run: apt-get install -y xvfb diff --git a/AutoUpdate/Main.cs b/AutoUpdate/Main.cs index 2d32697b02..df9d915005 100644 --- a/AutoUpdate/Main.cs +++ b/AutoUpdate/Main.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Runtime.InteropServices; using System.Threading; using System.Windows.Forms; @@ -46,7 +47,7 @@ public static int Main(string[] args) // Wait for CKAN to close try { - if (IsOnWindows()) + if (IsOnWindows) { // On Unix you can only wait for CHILD processes to exit var process = Process.GetProcessById(Math.Abs(pid)); @@ -121,19 +122,19 @@ public static int Main(string[] args) private static void StartCKAN(string path) { // Start CKAN - if (IsOnMono()) + if (IsOnMono) { Process.Start("mono", string.Format("\"{0}\"", path)); } else { - Process.Start(path); + Process.Start(path, "--asroot"); } } private static void MakeExecutable(string path) { - if (!IsOnWindows()) + if (!IsOnWindows) { // TODO: It would be really lovely (and safer!) to use the native system // call here: http://docs.go-mono.com/index.aspx?link=M:Mono.Unix.Native.Syscall.chmod @@ -151,20 +152,14 @@ private static void MakeExecutable(string path) /// /// Are we on Mono? /// - private static bool IsOnMono() - { - return Type.GetType("Mono.Runtime") != null; - } + private static bool IsOnMono + => Type.GetType("Mono.Runtime") != null; /// /// Are we on Windows? /// - private static bool IsOnWindows() - { - PlatformID platform = Environment.OSVersion.Platform; - return platform != PlatformID.MacOSX && - platform != PlatformID.Unix && platform != PlatformID.Xbox; - } + private static bool IsOnWindows + => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); /// /// Display unexpected exceptions to user diff --git a/Cmdline/Action/Filter.cs b/Cmdline/Action/Filter.cs index 3c8ed92688..0c21b97e07 100644 --- a/Cmdline/Action/Filter.cs +++ b/Cmdline/Action/Filter.cs @@ -5,6 +5,8 @@ using CommandLine; using CommandLine.Text; +using CKAN.Configuration; + namespace CKAN.CmdLine { /// @@ -78,7 +80,7 @@ private int ListFilters(FilterListOptions opts) return exitCode; } - var cfg = ServiceLocator.Container.Resolve(); + var cfg = ServiceLocator.Container.Resolve(); user.RaiseMessage(Properties.Resources.FilterListGlobalHeader); foreach (string filter in cfg.GlobalInstallFilters) { @@ -111,7 +113,7 @@ private int AddFilters(FilterAddOptions opts, string verb) if (opts.global) { - var cfg = ServiceLocator.Container.Resolve(); + var cfg = ServiceLocator.Container.Resolve(); var duplicates = cfg.GlobalInstallFilters .Intersect(opts.filters) .ToArray(); @@ -172,7 +174,7 @@ private int RemoveFilters(FilterRemoveOptions opts, string verb) if (opts.global) { - var cfg = ServiceLocator.Container.Resolve(); + var cfg = ServiceLocator.Container.Resolve(); var notFound = opts.filters .Except(cfg.GlobalInstallFilters) .ToArray(); diff --git a/Cmdline/Action/Upgrade.cs b/Cmdline/Action/Upgrade.cs index 46b631900d..ba5069c2ec 100644 --- a/Cmdline/Action/Upgrade.cs +++ b/Cmdline/Action/Upgrade.cs @@ -3,9 +3,11 @@ using System.Collections.Generic; using System.Transactions; +using Autofac; using log4net; using CKAN.Versioning; +using CKAN.Configuration; namespace CKAN.CmdLine { @@ -47,38 +49,63 @@ public int RunCommand(CKAN.GameInstance instance, object raw_options) user.RaiseMessage(" or ckan upgrade --all"); if (AutoUpdate.CanUpdate) { - user.RaiseMessage(" or ckan upgrade ckan"); + user.RaiseMessage(" or ckan upgrade ckan [--stable-release|--dev-build]"); } return Exit.BADOPT; } if (!options.upgrade_all && options.modules[0] == "ckan" && AutoUpdate.CanUpdate) { - user.RaiseMessage(Properties.Resources.UpgradeQueryingCKAN); - AutoUpdate.Instance.FetchLatestReleaseInfo(); - var latestVersion = AutoUpdate.Instance.latestUpdate.Version; - var currentVersion = new ModuleVersion(Meta.GetVersion(VersionFormat.Short)); + if (options.dev_build && options.stable_release) + { + user.RaiseMessage(Properties.Resources.UpgradeCannotCombineFlags); + return Exit.BADOPT; + } + var config = ServiceLocator.Container.Resolve(); + var devBuild = options.dev_build + || (!options.stable_release && config.DevBuilds); + if (devBuild != config.DevBuilds) + { + config.DevBuilds = devBuild; + user.RaiseMessage( + config.DevBuilds + ? Properties.Resources.UpgradeSwitchingToDevBuilds + : Properties.Resources.UpgradeSwitchingToStableReleases); + } - if (latestVersion.IsGreaterThan(currentVersion)) + user.RaiseMessage(Properties.Resources.UpgradeQueryingCKAN); + try { - user.RaiseMessage(Properties.Resources.UpgradeNewCKANAvailable, latestVersion); - var releaseNotes = AutoUpdate.Instance.latestUpdate.ReleaseNotes; - user.RaiseMessage(releaseNotes); - user.RaiseMessage(""); - user.RaiseMessage(""); + var upd = new AutoUpdate(); + var update = upd.GetUpdate(config.DevBuilds); + var latestVersion = update.Version; + var currentVersion = new ModuleVersion(Meta.GetVersion()); - if (user.RaiseYesNoDialog(Properties.Resources.UpgradeProceed)) + if (!latestVersion.Equals(currentVersion)) { - user.RaiseMessage(Properties.Resources.UpgradePleaseWait); - AutoUpdate.Instance.StartUpdateProcess(false); + user.RaiseMessage(Properties.Resources.UpgradeNewCKANAvailable, latestVersion); + var releaseNotes = update.ReleaseNotes; + user.RaiseMessage(releaseNotes); + user.RaiseMessage(""); + user.RaiseMessage(""); + + if (user.RaiseYesNoDialog(Properties.Resources.UpgradeProceed)) + { + user.RaiseMessage(Properties.Resources.UpgradePleaseWait); + upd.StartUpdateProcess(false, config.DevBuilds, user); + } + } + else + { + user.RaiseMessage(Properties.Resources.UpgradeAlreadyHaveLatest); } + return Exit.OK; } - else + catch (Exception exc) { - user.RaiseMessage(Properties.Resources.UpgradeAlreadyHaveLatest); + user.RaiseError("Upgrade failed: {0}", exc.Message); + return Exit.ERROR; } - - return Exit.OK; } try diff --git a/Cmdline/Main.cs b/Cmdline/Main.cs index 1e2afa16b2..63a7435d37 100644 --- a/Cmdline/Main.cs +++ b/Cmdline/Main.cs @@ -113,27 +113,20 @@ public static int Execute(GameInstanceManager manager, CommonOptions opts, strin } } catch (NoGameInstanceKraken) - { - return printMissingInstanceError(new ConsoleUser(false)); - } - finally { log.Info("CKAN exiting."); + return printMissingInstanceError(new ConsoleUser(false)); } - // Why do we print "CKAN exiting" twice??? Options cmdline; try { cmdline = new Options(args); } catch (BadCommandKraken) - { - return AfterHelp(); - } - finally { log.Info("CKAN exiting."); + return AfterHelp(); } // Process commandline options. @@ -301,7 +294,7 @@ private static int Gui(GameInstanceManager manager, GuiOptions options, string[] // but trying to catch it here doesn't seem to help. Dunno why. // GUI expects its first param to be an identifier, don't confuse it - GUI.GUI.Main_(args.Except(new string[] {"--verbose", "--debug", "--show-console"}) + GUI.GUI.Main_(args.Except(new string[] {"--verbose", "--debug", "--show-console", "--asroot"}) .ToArray(), manager, options.ShowConsole); diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index 4ea9019b33..13499591e4 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -405,6 +405,14 @@ internal class UpgradeOptions : InstanceSpecificOptions [Option("all", DefaultValue = false, HelpText = "Upgrade all available updated modules")] public bool upgrade_all { get; set; } + [Option("dev-build", DefaultValue = false, + HelpText = "For `ckan` option only, use dev builds")] + public bool dev_build { get; set; } + + [Option("stable-release", DefaultValue = false, + HelpText = "For `ckan` option only, use stable releases")] + public bool stable_release { get; set; } + [ValueList(typeof (List))] [InstalledIdentifiers] public List modules { get; set; } diff --git a/Cmdline/Properties/Resources.resx b/Cmdline/Properties/Resources.resx index f1e19db9dd..1dcf548b05 100644 --- a/Cmdline/Properties/Resources.resx +++ b/Cmdline/Properties/Resources.resx @@ -362,6 +362,9 @@ Try `ckan list` for a list of installed mods. Removed modules [Name (CKAN identifier)]: Updated modules [Name (CKAN identifier)]: Updated information on {0} compatible modules + The `--dev-build` and `--stable-release` flags cannot be combined! + Switching to dev builds + Switching to stable releases Querying the latest CKAN version New CKAN version available - {0} Proceed with install? diff --git a/ConsoleUI/ModListScreen.cs b/ConsoleUI/ModListScreen.cs index 60bcac5bd9..39c2708086 100644 --- a/ConsoleUI/ModListScreen.cs +++ b/ConsoleUI/ModListScreen.cs @@ -9,6 +9,7 @@ using CKAN.ConsoleUI.Toolkit; using CKAN.Extensions; using CKAN.Games; +using CKAN.Configuration; namespace CKAN.ConsoleUI { @@ -540,7 +541,7 @@ private bool EditAuthTokens(ConsoleTheme theme) private bool EditInstallFilters(ConsoleTheme theme) { LaunchSubScreen(theme, new InstallFiltersScreen( - ServiceLocator.Container.Resolve(), + ServiceLocator.Container.Resolve(), manager.CurrentInstance )); return true; diff --git a/Core/AutoUpdate/AutoUpdate.cs b/Core/AutoUpdate/AutoUpdate.cs new file mode 100644 index 0000000000..bcbc44155b --- /dev/null +++ b/Core/AutoUpdate/AutoUpdate.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.IO; +using System.Diagnostics; +using System.Reflection; + +namespace CKAN +{ + /// + /// CKAN client auto-updating routines. This works in conjunction with the + /// auto-update helper to allow users to upgrade. + /// + public class AutoUpdate + { + public AutoUpdate() + { + } + + public CkanUpdate GetUpdate(bool devBuild) + { + if (updates.TryGetValue(devBuild, out CkanUpdate update)) + { + return update; + } + var newUpdate = devBuild + ? new S3BuildCkanUpdate() as CkanUpdate + : new GitHubReleaseCkanUpdate(); + updates.Add(devBuild, newUpdate); + return newUpdate; + } + + private readonly Dictionary updates = new Dictionary(); + + /// + /// Report whether it's possible to run the auto-updater. + /// Checks whether we can overwrite the running ckan.exe. + /// Windows doesn't let us check this because it locks the EXE + /// for a running process, so assume we can always overwrite on Windows. + /// + public static readonly bool CanUpdate = Platform.IsWindows || CanWrite(exePath); + + /// + /// Downloads the new ckan.exe version, as well as the updater helper, + /// and then launches the helper allowing us to upgrade. + /// + /// If set to true launch CKAN after update. + public void StartUpdateProcess(bool launchCKANAfterUpdate, bool devBuild, IUser user = null) + { + var pid = Process.GetCurrentProcess().Id; + + var update = GetUpdate(devBuild); + + // download updater app and new ckan.exe + NetAsyncDownloader.DownloadWithProgress(update.Targets, user); + + // run updater + SetExecutable(update.updaterFilename); + Process.Start(new ProcessStartInfo + { + Verb = "runas", + FileName = update.updaterFilename, + Arguments = string.Format(@"{0} ""{1}"" ""{2}"" {3}", + -pid, exePath, + update.ckanFilename, + launchCKANAfterUpdate ? "launch" : "nolaunch"), + // .NET ignores Verb without this + UseShellExecute = true, + CreateNoWindow = true, + }); + + // Caller should now exit. Let them do it safely. + } + + public static void SetExecutable(string fileName) + { + // mark as executable if on Linux or Mac + if (Platform.IsUnix || Platform.IsMac) + { + // TODO: It would be really lovely (and safer!) to use the native system + // call here: http://docs.go-mono.com/index.aspx?link=M:Mono.Unix.Native.Syscall.chmod + + string command = string.Format("+x \"{0}\"", fileName); + + ProcessStartInfo permsinfo = new ProcessStartInfo("chmod", command) + { + UseShellExecute = false + }; + Process permsprocess = Process.Start(permsinfo); + permsprocess.WaitForExit(); + } + } + + private static bool CanWrite(string path) + { + try + { + // Try to open the file for writing. + // We won't actually write, but we expect the OS to stop us if we don't have permissions. + using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite)) { } + return true; + } + catch + { + return false; + } + } + + // This is null when running tests, seemingly. + private static readonly string exePath = Assembly.GetEntryAssembly()?.Location ?? ""; + } +} diff --git a/Core/AutoUpdate/CkanUpdate.cs b/Core/AutoUpdate/CkanUpdate.cs new file mode 100644 index 0000000000..8ef171b033 --- /dev/null +++ b/Core/AutoUpdate/CkanUpdate.cs @@ -0,0 +1,26 @@ +using System; +using System.IO; +using System.Collections.Generic; + +using CKAN.Versioning; + +namespace CKAN +{ + /// + /// Object representing a CKAN release + /// + public abstract class CkanUpdate + { + public CkanModuleVersion Version { get; protected set; } + public Uri ReleaseDownload { get; protected set; } + public long ReleaseSize { get; protected set; } + public Uri UpdaterDownload { get; protected set; } + public long UpdaterSize { get; protected set; } + public string ReleaseNotes { get; protected set; } + + public string updaterFilename = $"{Path.GetTempPath()}{Guid.NewGuid()}.exe"; + public string ckanFilename = $"{Path.GetTempPath()}{Guid.NewGuid()}.exe"; + + public abstract IList Targets { get; } + } +} diff --git a/Core/AutoUpdate/GithubReleaseCkanUpdate.cs b/Core/AutoUpdate/GithubReleaseCkanUpdate.cs new file mode 100644 index 0000000000..eb0b76dbc0 --- /dev/null +++ b/Core/AutoUpdate/GithubReleaseCkanUpdate.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; + +using Autofac; +using Newtonsoft.Json; + +using CKAN.Configuration; +using CKAN.Versioning; + +namespace CKAN +{ + /// + /// Represents a CKAN release on GitHub + /// + public class GitHubReleaseCkanUpdate : CkanUpdate + { + /// + /// Initialize the Object + /// + /// JSON representation of release + public GitHubReleaseCkanUpdate(GitHubReleaseInfo releaseJson = null) + { + if (releaseJson == null) + { + var coreConfig = ServiceLocator.Container.Resolve(); + var token = coreConfig.TryGetAuthToken(latestCKANReleaseApiUrl.Host, out string t) + ? t : null; + releaseJson = JsonConvert.DeserializeObject( + Net.DownloadText(latestCKANReleaseApiUrl, token)); + if (releaseJson == null) + { + throw new Kraken(Properties.Resources.AutoUpdateNotFetched); + } + } + + Version = new CkanModuleVersion(releaseJson.tag_name.ToString(), + releaseJson.name.ToString()); + ReleaseNotes = ExtractReleaseNotes(releaseJson.body.ToString()); + foreach (var asset in releaseJson.assets) + { + string url = asset.browser_download_url.ToString(); + if (url.EndsWith("ckan.exe")) + { + ReleaseDownload = asset.browser_download_url; + ReleaseSize = asset.size; + } + else if (url.EndsWith("AutoUpdater.exe")) + { + UpdaterDownload = asset.browser_download_url; + UpdaterSize = asset.size; + } + } + } + + public override IList Targets => new[] + { + new NetAsyncDownloader.DownloadTarget( + UpdaterDownload, updaterFilename, UpdaterSize), + new NetAsyncDownloader.DownloadTarget( + ReleaseDownload, ckanFilename, ReleaseSize), + }; + + /// + /// Extracts release notes from the body of text provided by the github API. + /// By default this is everything after the first three dashes on a line by + /// itself, but as a fallback we'll use the whole body if not found. + /// + /// The release notes. + internal static string ExtractReleaseNotes(string releaseBody) + { + const string divider = "\r\n---\r\n"; + // Get at most two pieces, the first is the image, the second is the release notes + string[] notesArray = releaseBody.Split(new string[] { divider }, 2, StringSplitOptions.None); + return notesArray.Length > 1 ? notesArray[1] : notesArray[0]; + } + + private static readonly Uri latestCKANReleaseApiUrl = + new Uri("https://api.github.com/repos/KSP-CKAN/CKAN/releases/latest"); + } +} diff --git a/Core/AutoUpdate/GithubReleaseInfo.cs b/Core/AutoUpdate/GithubReleaseInfo.cs new file mode 100644 index 0000000000..8216ca892e --- /dev/null +++ b/Core/AutoUpdate/GithubReleaseInfo.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace CKAN +{ + public class GitHubReleaseInfo + { + public string tag_name; + public string name; + public string body; + public List assets; + + public sealed class Asset + { + public Uri browser_download_url; + public long size; + } + } +} diff --git a/Core/AutoUpdate/S3BuildCkanUpdate.cs b/Core/AutoUpdate/S3BuildCkanUpdate.cs new file mode 100644 index 0000000000..19def760e3 --- /dev/null +++ b/Core/AutoUpdate/S3BuildCkanUpdate.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +using Newtonsoft.Json; + +using CKAN.Versioning; + +namespace CKAN +{ + public class S3BuildCkanUpdate : CkanUpdate + { + public S3BuildCkanUpdate(S3BuildVersionInfo versionJson = null) + { + if (versionJson == null) + { + versionJson = JsonConvert.DeserializeObject( + Net.DownloadText(new Uri(S3BaseUrl, VersionJsonUrlPiece))); + if (versionJson == null) + { + throw new Kraken(Properties.Resources.AutoUpdateNotFetched); + } + } + + Version = new CkanModuleVersion(versionJson.version.ToString(), "dev"); + ReleaseNotes = versionJson.changelog; + UpdaterDownload = new Uri(S3BaseUrl, AutoUpdaterUrlPiece); + ReleaseDownload = new Uri(S3BaseUrl, CkanUrlPiece); + } + + public override IList Targets => new[] + { + new NetAsyncDownloader.DownloadTarget( + UpdaterDownload, updaterFilename), + new NetAsyncDownloader.DownloadTarget( + ReleaseDownload, ckanFilename), + }; + + private static readonly Uri S3BaseUrl = + new Uri("https://ksp-ckan.s3-us-west-2.amazonaws.com/"); + private const string VersionJsonUrlPiece = "version.json"; + private const string AutoUpdaterUrlPiece = "AutoUpdater.exe"; + private const string CkanUrlPiece = "ckan.exe"; + } +} diff --git a/Core/AutoUpdate/S3BuildVersionInfo.cs b/Core/AutoUpdate/S3BuildVersionInfo.cs new file mode 100644 index 0000000000..0b5e504f7b --- /dev/null +++ b/Core/AutoUpdate/S3BuildVersionInfo.cs @@ -0,0 +1,10 @@ +using CKAN.Versioning; + +namespace CKAN +{ + public class S3BuildVersionInfo + { + public ModuleVersion version; + public string changelog; + } +} diff --git a/Core/CKANPathUtils.cs b/Core/CKANPathUtils.cs index 018005eb05..1d18a32c68 100644 --- a/Core/CKANPathUtils.cs +++ b/Core/CKANPathUtils.cs @@ -60,7 +60,7 @@ private static string[] SteamPaths Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "Library", "Application Support", "Steam"), } - : new string[] {}; + : Array.Empty(); /// /// Normalizes the path by replacing all \ with / and removing any trailing slash. diff --git a/Core/Configuration/IConfiguration.cs b/Core/Configuration/IConfiguration.cs index 7637fb1d2e..1537ac0fad 100644 --- a/Core/Configuration/IConfiguration.cs +++ b/Core/Configuration/IConfiguration.cs @@ -63,7 +63,11 @@ public interface IConfiguration /// List of hosts in order of priority when there are multiple URLs to choose from. /// The first null value represents where all other hosts should go. /// - /// string[] PreferredHosts { get; set; } + + /// + /// true if user wants to use nightly builds from S3, false to use releases from GitHub + /// + bool DevBuilds { get; set; } } } diff --git a/Core/Configuration/JsonConfiguration.cs b/Core/Configuration/JsonConfiguration.cs index cf47850d74..7ca31938ba 100644 --- a/Core/Configuration/JsonConfiguration.cs +++ b/Core/Configuration/JsonConfiguration.cs @@ -27,8 +27,9 @@ private class Config public string Language { get; set; } public IList GameInstances { get; set; } = new List(); public IDictionary AuthTokens { get; set; } = new Dictionary(); - public string[] GlobalInstallFilters { get; set; } = new string[] { }; - public string[] PreferredHosts { get; set; } = new string[] { }; + public string[] GlobalInstallFilters { get; set; } = Array.Empty(); + public string[] PreferredHosts { get; set; } = Array.Empty(); + public bool DevBuilds { get; set; } } public class ConfigConverter : JsonPropertyNamesChangedConverter @@ -323,6 +324,26 @@ public string[] PreferredHosts } } + public bool DevBuilds + { + get + { + lock (_lock) + { + return config.DevBuilds; + } + } + + set + { + lock (_lock) + { + config.DevBuilds = value; + SaveConfig(); + } + } + } + // // Save the JSON configuration file. // diff --git a/Core/Configuration/Win32RegistryConfiguration.cs b/Core/Configuration/Win32RegistryConfiguration.cs index 2b2cf6fff9..dab16876ab 100644 --- a/Core/Configuration/Win32RegistryConfiguration.cs +++ b/Core/Configuration/Win32RegistryConfiguration.cs @@ -183,6 +183,11 @@ public void SetAuthToken(string host, string token) /// public string[] PreferredHosts { get; set; } + /// + /// Not implemented because the Windows registry is deprecated + /// + public bool DevBuilds { get; set; } + public static bool DoesRegistryConfigurationExist() { RegistryKey key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(CKAN_KEY_NO_PREFIX); diff --git a/Core/GameInstance.cs b/Core/GameInstance.cs index 01f8e191bc..9cea2f3622 100644 --- a/Core/GameInstance.cs +++ b/Core/GameInstance.cs @@ -181,7 +181,7 @@ public string[] InstallFilters { get => File.Exists(InstallFiltersFile) ? JsonConvert.DeserializeObject(File.ReadAllText(InstallFiltersFile)) - : new string[] { }; + : Array.Empty(); #pragma warning disable IDE0027 set diff --git a/Core/GameInstanceManager.cs b/Core/GameInstanceManager.cs index 6488582636..6e800ff35b 100644 --- a/Core/GameInstanceManager.cs +++ b/Core/GameInstanceManager.cs @@ -503,7 +503,7 @@ public void ClearAutoStart() private void LoadInstances() { - log.Info("Loading KSP instances"); + log.Info("Loading game instances"); instances.Clear(); diff --git a/Core/Games/KerbalSpaceProgram.cs b/Core/Games/KerbalSpaceProgram.cs index e0a41d4e67..beaf56edd0 100644 --- a/Core/Games/KerbalSpaceProgram.cs +++ b/Core/Games/KerbalSpaceProgram.cs @@ -101,7 +101,7 @@ public string MacPath() } public string PrimaryModDirectoryRelative => "GameData"; - public string[] AlternateModDirectoriesRelative => new string[] { }; + public string[] AlternateModDirectoriesRelative => Array.Empty(); public string PrimaryModDirectory(GameInstance inst) => CKANPathUtils.NormalizePath( diff --git a/Core/Games/KerbalSpaceProgram2.cs b/Core/Games/KerbalSpaceProgram2.cs index 1e841cc72b..c85c535318 100644 --- a/Core/Games/KerbalSpaceProgram2.cs +++ b/Core/Games/KerbalSpaceProgram2.cs @@ -112,9 +112,7 @@ public string PrimaryModDirectory(GameInstance inst) "PDLauncher", }; - public string[] ReservedPaths => new string[] - { - }; + public string[] ReservedPaths => Array.Empty(); public string[] CreateableDirs => new string[] { @@ -124,7 +122,7 @@ public string PrimaryModDirectory(GameInstance inst) "BepInEx/plugins", }; - public string[] AutoRemovableDirs => new string[] { }; + public string[] AutoRemovableDirs => Array.Empty(); /// /// Checks the path against a list of reserved game directories @@ -160,7 +158,7 @@ public string DefaultCommandLine(string path) public string[] AdjustCommandLine(string[] args, GameVersion installedVersion) => args; - public IDlcDetector[] DlcDetectors => new IDlcDetector[] { }; + public IDlcDetector[] DlcDetectors => Array.Empty(); private static readonly Uri BuildMapUri = new Uri("https://raw.githubusercontent.com/KSP-CKAN/KSP2-CKAN-meta/main/builds.json"); diff --git a/Core/Meta.cs b/Core/Meta.cs index 396144c3d1..599302f28b 100644 --- a/Core/Meta.cs +++ b/Core/Meta.cs @@ -28,7 +28,9 @@ public static string GetVersion(VersionFormat format = VersionFormat.Normal) case VersionFormat.Short: return $"v{version.UpToCharacters(shortDelimiters)}"; case VersionFormat.Normal: - return $"v{version.UpToCharacter('+')}"; + return "v" + Assembly.GetExecutingAssembly() + .GetAssemblyAttribute() + .Version; case VersionFormat.Full: return $"v{version}"; default: @@ -38,9 +40,6 @@ public static string GetVersion(VersionFormat format = VersionFormat.Normal) private static readonly char[] shortDelimiters = new char[] { '-', '+' }; - private static string UpToCharacter(this string orig, char what) - => orig.UpToIndex(orig.IndexOf(what)); - private static string UpToCharacters(this string orig, char[] what) => orig.UpToIndex(orig.IndexOfAny(what)); diff --git a/Core/ModuleInstaller.cs b/Core/ModuleInstaller.cs index 921619f0f7..ead7186a85 100644 --- a/Core/ModuleInstaller.cs +++ b/Core/ModuleInstaller.cs @@ -659,7 +659,7 @@ public void UninstallList( // Find all the things which need uninstalling. var revdep = mods .Union(registry_manager.registry.FindReverseDependencies( - mods.Except(installing?.Select(m => m.identifier) ?? new string[] {}) + mods.Except(installing?.Select(m => m.identifier) ?? Array.Empty()) .ToList(), installing)) .ToList(); diff --git a/Core/Net/AutoUpdate.cs b/Core/Net/AutoUpdate.cs deleted file mode 100644 index 1eef7b8c79..0000000000 --- a/Core/Net/AutoUpdate.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Diagnostics; -using System.Reflection; - -using Newtonsoft.Json; - -using CKAN.Versioning; - -namespace CKAN -{ - /// - /// Object representing a CKAN release - /// - public class CkanUpdate - { - /// - /// Initialize the Object - /// - /// JSON representation of release - public CkanUpdate(string json) - { - dynamic response = JsonConvert.DeserializeObject(json); - - Version = new CkanModuleVersion( - response.tag_name.ToString(), - response.name.ToString() - ); - ReleaseNotes = ExtractReleaseNotes(response.body.ToString()); - - foreach (var asset in response.assets) - { - string url = asset.browser_download_url.ToString(); - if (url.EndsWith("ckan.exe")) - { - ReleaseDownload = asset.browser_download_url; - ReleaseSize = (long)asset.size; - } - else if (url.EndsWith("AutoUpdater.exe")) - { - UpdaterDownload = asset.browser_download_url; - UpdaterSize = (long)asset.size; - } - } - } - - /// - /// Extracts release notes from the body of text provided by the github API. - /// By default this is everything after the first three dashes on a line by - /// itself, but as a fallback we'll use the whole body if not found. - /// - /// The release notes. - public static string ExtractReleaseNotes(string releaseBody) - { - const string divider = "\r\n---\r\n"; - // Get at most two pieces, the first is the image, the second is the release notes - string[] notesArray = releaseBody.Split(new string[] { divider }, 2, StringSplitOptions.None); - return notesArray.Length > 1 ? notesArray[1] : notesArray[0]; - } - - public readonly CkanModuleVersion Version; - public readonly Uri ReleaseDownload; - public readonly long ReleaseSize; - public readonly Uri UpdaterDownload; - public readonly long UpdaterSize; - public readonly string ReleaseNotes; - } - - /// - /// CKAN client auto-updating routines. This works in conjunction with the - /// auto-update helper to allow users to upgrade. - /// - public class AutoUpdate - { - /// - /// The list of releases containing ckan.exe and AutoUpdater.exe - /// - private static readonly Uri latestCKANReleaseApiUrl = new Uri("https://api.github.com/repos/KSP-CKAN/CKAN/releases/latest"); - - public static readonly AutoUpdate Instance = new AutoUpdate(); - - public CkanUpdate latestUpdate; - - // This is private so we can enforce our class being a singleton. - private AutoUpdate() { } - - private static bool CanWrite(string path) - { - try - { - // Try to open the file for writing. - // We won't actually write, but we expect the OS to stop us if we don't have permissions. - using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite)) { } - return true; - } - catch - { - return false; - } - } - - // This is null when running tests, seemingly. - private static readonly string exePath = Assembly.GetEntryAssembly()?.Location ?? ""; - - /// - /// Report whether it's possible to run the auto-updater. - /// Checks whether we can overwrite the running ckan.exe. - /// Windows doesn't let us check this because it locks the EXE - /// for a running process, so assume we can always overwrite on Windows. - /// - public static readonly bool CanUpdate = Platform.IsWindows || CanWrite(exePath); - - /// - /// Our metadata is considered fetched if we have a latest version, release notes, - /// and download URLs for the ckan executable and helper. - /// - public bool IsFetched() - { - return latestUpdate != null; - } - - /// - /// Fetches all the latest release info, populating our attributes in - /// the process. - /// - public void FetchLatestReleaseInfo() - { - latestUpdate = new CkanUpdate(Net.DownloadText(latestCKANReleaseApiUrl)); - } - - /// - /// Downloads the new ckan.exe version, as well as the updater helper, - /// and then launches the helper allowing us to upgrade. - /// - /// If set to true launch CKAN after update. - public void StartUpdateProcess(bool launchCKANAfterUpdate, IUser user = null) - { - if (!IsFetched()) - { - throw new Kraken(Properties.Resources.AutoUpdateNotFetched); - } - - var pid = Process.GetCurrentProcess().Id; - - // download updater app and new ckan.exe - string updaterFilename = Path.GetTempPath() + Guid.NewGuid().ToString() + ".exe"; - string ckanFilename = Path.GetTempPath() + Guid.NewGuid().ToString() + ".exe"; - Net.DownloadWithProgress( - new[] - { - new Net.DownloadTarget( - new List { latestUpdate.UpdaterDownload }, - updaterFilename, - latestUpdate.UpdaterSize), - new Net.DownloadTarget( - new List { latestUpdate.ReleaseDownload }, - ckanFilename, - latestUpdate.ReleaseSize), - }, - user - ); - - // run updater - SetExecutable(updaterFilename); - Process.Start(new ProcessStartInfo - { - Verb = "runas", - FileName = updaterFilename, - Arguments = string.Format(@"{0} ""{1}"" ""{2}"" {3}", -pid, exePath, ckanFilename, launchCKANAfterUpdate ? "launch" : "nolaunch"), - UseShellExecute = false, - // Make child's stdin a pipe so it can tell when we exit - RedirectStandardInput = true, - CreateNoWindow = true, - }); - - // Caller should now exit. Let them do it safely. - } - - public static void SetExecutable(string fileName) - { - // mark as executable if on Linux or Mac - if (Platform.IsUnix || Platform.IsMac) - { - // TODO: It would be really lovely (and safer!) to use the native system - // call here: http://docs.go-mono.com/index.aspx?link=M:Mono.Unix.Native.Syscall.chmod - - string command = string.Format("+x \"{0}\"", fileName); - - ProcessStartInfo permsinfo = new ProcessStartInfo("chmod", command) - { - UseShellExecute = false - }; - Process permsprocess = Process.Start(permsinfo); - permsprocess.WaitForExit(); - } - } - } -} diff --git a/Core/Net/Net.cs b/Core/Net/Net.cs index 4c0608f487..88f6698efa 100644 --- a/Core/Net/Net.cs +++ b/Core/Net/Net.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net; using System.Text.RegularExpressions; @@ -21,6 +20,7 @@ namespace CKAN public static class Net { // The user agent that we report to web sites + // Maybe overwritten by command line args public static string UserAgentString = "Mozilla/4.0 (compatible; CKAN)"; private const int MaxRetries = 3; @@ -76,14 +76,14 @@ public static string Download(string url, string filename = null, IUser user = n public static string Download(string url, out string etag, string filename = null, IUser user = null) { - TxFileManager FileTransaction = new TxFileManager(); - user = user ?? new NullUser(); user.RaiseMessage(Properties.Resources.NetDownloading, url); + var FileTransaction = new TxFileManager(); // Generate a temporary file if none is provided. if (filename == null) { + filename = FileTransaction.GetTempFileName(); } @@ -141,55 +141,6 @@ public static string Download(string url, out string etag, string filename = nul return filename; } - public class DownloadTarget - { - public List urls { get; private set; } - public string filename { get; private set; } - public long size { get; set; } - public string mimeType { get; private set; } - - public DownloadTarget(List urls, string filename = null, long size = 0, string mimeType = "") - { - TxFileManager FileTransaction = new TxFileManager(); - - this.urls = urls; - this.filename = string.IsNullOrEmpty(filename) - ? FileTransaction.GetTempFileName() - : filename; - this.size = size; - this.mimeType = mimeType; - } - } - - public static string DownloadWithProgress(string url, string filename = null, IUser user = null) - => DownloadWithProgress(new Uri(url), filename, user); - - public static string DownloadWithProgress(Uri url, string filename = null, IUser user = null) - { - var targets = new[] { - new DownloadTarget(new List { url }, filename) - }; - DownloadWithProgress(targets, user); - return targets.First().filename; - } - - public static void DownloadWithProgress(IList downloadTargets, IUser user = null) - { - var downloader = new NetAsyncDownloader(user ?? new NullUser()); - downloader.onOneCompleted += (url, filename, error, etag) => - { - if (error != null) - { - user?.RaiseError(error.ToString()); - } - else - { - File.Move(filename, downloadTargets.First(p => p.urls.Contains(url)).filename); - } - }; - downloader.DownloadAndWait(downloadTargets); - } - /// /// Download a string from a URL /// diff --git a/Core/Net/NetAsyncDownloader.DownloadPart.cs b/Core/Net/NetAsyncDownloader.DownloadPart.cs new file mode 100644 index 0000000000..b49f6434e7 --- /dev/null +++ b/Core/Net/NetAsyncDownloader.DownloadPart.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.ComponentModel; + +using Autofac; + +using CKAN.Configuration; + +namespace CKAN +{ + public partial class NetAsyncDownloader + { + // Private utility class for tracking downloads + private class DownloadPart + { + public readonly DownloadTarget target; + public readonly string path; + + public DateTime lastProgressUpdateTime; + public long lastProgressUpdateSize; + public long bytesLeft; + public long size; + public long bytesPerSecond; + public Exception error; + + // Number of target URLs already tried and failed + private int triedDownloads; + + /// + /// Percentage, bytes received, total bytes to receive + /// + public event Action Progress; + public event Action Done; + + private string mimeType => target.mimeType; + private ResumingWebClient agent; + + public DownloadPart(DownloadTarget target) + { + this.target = target; + path = target.filename ?? Path.GetTempFileName(); + size = bytesLeft = target.size; + lastProgressUpdateTime = DateTime.Now; + triedDownloads = 0; + } + + public void Download(Uri url, string path) + { + ResetAgent(); + // Check whether to use an auth token for this host + if (url.IsAbsoluteUri + && ServiceLocator.Container.Resolve().TryGetAuthToken(url.Host, out string token) + && !string.IsNullOrEmpty(token)) + { + log.InfoFormat("Using auth token for {0}", url.Host); + // Send our auth token to the GitHub API (or whoever else needs one) + agent.Headers.Add("Authorization", $"token {token}"); + } + agent.DownloadFileAsyncWithResume(url, path); + } + + public Uri CurrentUri => target.urls[triedDownloads]; + + public bool HaveMoreUris => triedDownloads + 1 < target.urls.Count; + + public void NextUri() + { + if (HaveMoreUris) + { + ++triedDownloads; + } + } + + public void Abort() + { + agent?.CancelAsyncOverridden(); + } + + private void ResetAgent() + { + // This WebClient child class does some complicated stuff, let's keep using it for now + #pragma warning disable SYSLIB0014 + agent = new ResumingWebClient(); + #pragma warning restore SYSLIB0014 + + agent.Headers.Add("User-Agent", Net.UserAgentString); + + // Tell the server what kind of files we want + if (!string.IsNullOrEmpty(mimeType)) + { + log.InfoFormat("Setting MIME type {0}", mimeType); + agent.Headers.Add("Accept", mimeType); + } + + // Forward progress and completion events to our listeners + agent.DownloadProgressChanged += (sender, args) => + { + Progress?.Invoke(args.ProgressPercentage, args.BytesReceived, args.TotalBytesToReceive); + }; + agent.DownloadProgress += (percent, bytesReceived, totalBytesToReceive) => + { + Progress?.Invoke(percent, bytesReceived, totalBytesToReceive); + }; + agent.DownloadFileCompleted += (sender, args) => + { + Done?.Invoke(sender, args, + args.Cancelled || args.Error != null + ? null + : agent.ResponseHeaders?.Get("ETag")?.Replace("\"", "")); + }; + } + } + } +} diff --git a/Core/Net/NetAsyncDownloader.DownloadTarget.cs b/Core/Net/NetAsyncDownloader.DownloadTarget.cs new file mode 100644 index 0000000000..b00787be30 --- /dev/null +++ b/Core/Net/NetAsyncDownloader.DownloadTarget.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; + +using ChinhDo.Transactions.FileManager; + +namespace CKAN +{ + public partial class NetAsyncDownloader + { + public class DownloadTarget + { + public List urls { get; private set; } + public string filename { get; private set; } + public long size { get; set; } + public string mimeType { get; private set; } + + public DownloadTarget(List urls, + string filename = null, + long size = 0, + string mimeType = "") + { + var FileTransaction = new TxFileManager(); + + this.urls = urls; + this.filename = string.IsNullOrEmpty(filename) + ? FileTransaction.GetTempFileName() + : filename; + this.size = size; + this.mimeType = mimeType; + } + + public DownloadTarget(Uri url, + string filename = null, + long size = 0, + string mimeType = "") + : this(new List { url }, + filename, size, mimeType) + { + } + } + } +} diff --git a/Core/Net/NetAsyncDownloader.cs b/Core/Net/NetAsyncDownloader.cs index ebaa513c54..88c3d107fd 100644 --- a/Core/Net/NetAsyncDownloader.cs +++ b/Core/Net/NetAsyncDownloader.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Linq; using System.Net; @@ -10,115 +9,13 @@ using Autofac; using log4net; -using CKAN.Configuration; - namespace CKAN { /// /// Download lots of files at once! /// - public class NetAsyncDownloader + public partial class NetAsyncDownloader { - // Private utility class for tracking downloads - private class NetAsyncDownloaderDownloadPart - { - public readonly Net.DownloadTarget target; - public DateTime lastProgressUpdateTime; - public long lastProgressUpdateSize; - public readonly string path; - public long bytesLeft; - public long size; - public long bytesPerSecond; - public Exception error; - - // Number of target URLs already tried and failed - private int triedDownloads; - - /// - /// Percentage, bytes received, total bytes to receive - /// - public event Action Progress; - public event Action Done; - - private string mimeType => target.mimeType; - private ResumingWebClient agent; - - public NetAsyncDownloaderDownloadPart(Net.DownloadTarget target) - { - this.target = target; - path = target.filename ?? Path.GetTempFileName(); - size = bytesLeft = target.size; - lastProgressUpdateTime = DateTime.Now; - triedDownloads = 0; - } - - public void Download(Uri url, string path) - { - ResetAgent(); - // Check whether to use an auth token for this host - if (url.IsAbsoluteUri - && ServiceLocator.Container.Resolve().TryGetAuthToken(url.Host, out string token) - && !string.IsNullOrEmpty(token)) - { - log.InfoFormat("Using auth token for {0}", url.Host); - // Send our auth token to the GitHub API (or whoever else needs one) - agent.Headers.Add("Authorization", $"token {token}"); - } - agent.DownloadFileAsyncWithResume(url, path); - } - - public Uri CurrentUri => target.urls[triedDownloads]; - - public bool HaveMoreUris => triedDownloads + 1 < target.urls.Count; - - public void NextUri() - { - if (HaveMoreUris) - { - ++triedDownloads; - } - } - - public void Abort() - { - agent?.CancelAsyncOverridden(); - } - - private void ResetAgent() - { - // This WebClient child class does some complicated stuff, let's keep using it for now - #pragma warning disable SYSLIB0014 - agent = new ResumingWebClient(); - #pragma warning restore SYSLIB0014 - - agent.Headers.Add("User-Agent", Net.UserAgentString); - - // Tell the server what kind of files we want - if (!string.IsNullOrEmpty(mimeType)) - { - log.InfoFormat("Setting MIME type {0}", mimeType); - agent.Headers.Add("Accept", mimeType); - } - - // Forward progress and completion events to our listeners - agent.DownloadProgressChanged += (sender, args) => - { - Progress?.Invoke(args.ProgressPercentage, args.BytesReceived, args.TotalBytesToReceive); - }; - agent.DownloadProgress += (percent, bytesReceived, totalBytesToReceive) => - { - Progress?.Invoke(percent, bytesReceived, totalBytesToReceive); - }; - agent.DownloadFileCompleted += (sender, args) => - { - Done?.Invoke(sender, args, - args.Cancelled || args.Error != null - ? null - : agent.ResponseHeaders?.Get("ETag")?.Replace("\"", "")); - }; - } - } - private static readonly ILog log = LogManager.GetLogger(typeof(NetAsyncDownloader)); public readonly IUser User; @@ -126,13 +23,13 @@ private void ResetAgent() /// /// Raised when data arrives for a download /// - public event Action Progress; + public event Action Progress; private readonly object dlMutex = new object(); // NOTE: Never remove anything from this, because closures have indexes into it! // (Clearing completely after completion is OK) - private readonly List downloads = new List(); - private readonly List queuedDownloads = new List(); + private readonly List downloads = new List(); + private readonly List queuedDownloads = new List(); private int completed_downloads; // For inter-thread communication @@ -150,20 +47,50 @@ public NetAsyncDownloader(IUser user) complete_or_canceled = new ManualResetEvent(false); } + public static string DownloadWithProgress(string url, string filename = null, IUser user = null) + => DownloadWithProgress(new Uri(url), filename, user); + + public static string DownloadWithProgress(Uri url, string filename = null, IUser user = null) + { + var targets = new[] + { + new DownloadTarget(url, filename) + }; + DownloadWithProgress(targets, user); + return targets.First().filename; + } + + public static void DownloadWithProgress(IList downloadTargets, IUser user = null) + { + var downloader = new NetAsyncDownloader(user ?? new NullUser()); + downloader.onOneCompleted += (url, filename, error, etag) => + { + if (error != null) + { + user?.RaiseError(error.ToString()); + } + else + { + File.Move(filename, downloadTargets.First(p => p.urls.Contains(url)).filename); + } + }; + downloader.DownloadAndWait(downloadTargets); + } + /// /// Start a new batch of downloads /// /// The downloads to begin - public void DownloadAndWait(IList targets) + public void DownloadAndWait(IList targets) { lock (dlMutex) { if (downloads.Count + queuedDownloads.Count > completed_downloads) { // Some downloads are still in progress, add to the current batch - foreach (Net.DownloadTarget target in targets) + foreach (DownloadTarget target in targets) { - DownloadModule(new NetAsyncDownloaderDownloadPart(target)); + DownloadModule(new DownloadPart(target)); } // Wait for completion along with original caller // so we can handle completion tasks for the added mods @@ -282,17 +209,17 @@ public void CancelDownload() /// Downloads our files. /// /// A collection of DownloadTargets - private void Download(ICollection targets) + private void Download(ICollection targets) { downloads.Clear(); queuedDownloads.Clear(); foreach (var t in targets) { - DownloadModule(new NetAsyncDownloaderDownloadPart(t)); + DownloadModule(new DownloadPart(t)); } } - private void DownloadModule(NetAsyncDownloaderDownloadPart dl) + private void DownloadModule(DownloadPart dl) { if (shouldQueue(dl.CurrentUri)) { @@ -372,7 +299,7 @@ private void triggerCompleted() /// The total amount of bytes we expect to download private void FileProgressReport(int index, long bytesDownloaded, long bytesToDownload) { - NetAsyncDownloaderDownloadPart download = downloads[index]; + DownloadPart download = downloads[index]; DateTime now = DateTime.Now; TimeSpan timeSpan = now - download.lastProgressUpdateTime; diff --git a/Core/Net/NetAsyncModulesDownloader.cs b/Core/Net/NetAsyncModulesDownloader.cs index 0d79fee14e..0093876b2a 100644 --- a/Core/Net/NetAsyncModulesDownloader.cs +++ b/Core/Net/NetAsyncModulesDownloader.cs @@ -42,14 +42,16 @@ public NetAsyncModulesDownloader(IUser user, NetModuleCache cache) this.cache = cache; } - internal Net.DownloadTarget TargetFromModuleGroup(HashSet group, - string[] preferredHosts) + internal NetAsyncDownloader.DownloadTarget TargetFromModuleGroup( + HashSet group, + string[] preferredHosts) => TargetFromModuleGroup(group, group.OrderBy(m => m.identifier).First(), preferredHosts); - private Net.DownloadTarget TargetFromModuleGroup(HashSet group, - CkanModule first, - string[] preferredHosts) - => new Net.DownloadTarget( + private NetAsyncDownloader.DownloadTarget TargetFromModuleGroup( + HashSet group, + CkanModule first, + string[] preferredHosts) + => new NetAsyncDownloader.DownloadTarget( group.SelectMany(mod => mod.download) .Concat(group.Select(mod => mod.InternetArchiveDownload) .Where(uri => uri != null) diff --git a/Core/Relationships/RelationshipResolver.cs b/Core/Relationships/RelationshipResolver.cs index 827e837ddc..ba0ed23f66 100644 --- a/Core/Relationships/RelationshipResolver.cs +++ b/Core/Relationships/RelationshipResolver.cs @@ -628,7 +628,6 @@ public Dictionary ConflictList /// Avoids duplicates and explains why dependencies are in the list. /// Use for reporting the conflicts to the user, use ConflictsList for coloring rows. /// - /// public IEnumerable ConflictDescriptions => conflicts.Where(kvp => kvp.Value == null // Pick the pair with the least distantly selected one first diff --git a/Core/Repositories/RepositoryDataManager.cs b/Core/Repositories/RepositoryDataManager.cs index 2b33d75755..3cf20d9c35 100644 --- a/Core/Repositories/RepositoryDataManager.cs +++ b/Core/Repositories/RepositoryDataManager.cs @@ -7,9 +7,7 @@ using ChinhDo.Transactions.FileManager; using log4net; -#if NETFRAMEWORK using CKAN.Extensions; -#endif using CKAN.Games; namespace CKAN @@ -160,7 +158,7 @@ public UpdateResult Update(Repository[] repos, try { // Download metadata - var targets = toUpdate.Select(r => new Net.DownloadTarget(new List() { r.uri })) + var targets = toUpdate.Select(r => new NetAsyncDownloader.DownloadTarget(r.uri)) .ToArray(); downloader.DownloadAndWait(targets); @@ -170,17 +168,20 @@ public UpdateResult Update(Repository[] repos, var progress = new ProgressFilesOffsetsToPercent( new Progress(p => user.RaiseProgress(msg, p)), targets.Select(t => new FileInfo(t.filename).Length)); - foreach ((Repository repo, Net.DownloadTarget target) in toUpdate.Zip(targets)) + foreach ((Repository repo, NetAsyncDownloader.DownloadTarget target) in toUpdate.Zip(targets)) { var file = target.filename; msg = string.Format(Properties.Resources.NetRepoLoadingModulesFromRepo, repo.name); // Load the file, save to in memory cache + log.InfoFormat("Loading {0}...", file); var repoData = repositoriesData[repo] = RepositoryData.FromDownload(file, game, progress); // Save parsed data to disk + log.DebugFormat("Saving data for {0} repo...", repo.name); repoData.SaveTo(GetRepoDataPath(repo)); // Delete downloaded archive + log.DebugFormat("Deleting {0}...", file); File.Delete(file); progress.NextFile(); } @@ -200,8 +201,13 @@ public UpdateResult Update(Repository[] repos, kvp.Value)) .ToList()); } - catch + catch (Exception exc) { + foreach (var e in exc.TraverseNodes(ex => ex.InnerException) + .Reverse()) + { + log.Error("Repository update failed", e); + } // Reset etags on errors loadETags(); throw; diff --git a/Core/Types/Kraken.cs b/Core/Types/Kraken.cs index 90a9f60978..3b9cc4479e 100644 --- a/Core/Types/Kraken.cs +++ b/Core/Types/Kraken.cs @@ -251,10 +251,10 @@ public BadRelationshipsKraken( modRelList conflicts ) : base( (depends?.Select(dep => string.Format(Properties.Resources.KrakenMissingDependency, dep.Item1, dep.Item2)) - ?? new string[] {} + ?? Array.Empty() ).Concat( conflicts?.Select(conf => string.Format(Properties.Resources.KrakenConflictsWith, conf.Item1, conf.Item2)) - ?? new string[] {} + ?? Array.Empty() ).ToArray() ) { diff --git a/Core/Versioning/ModuleVersion.cs b/Core/Versioning/ModuleVersion.cs index e09abfde8a..dde88bbfa2 100644 --- a/Core/Versioning/ModuleVersion.cs +++ b/Core/Versioning/ModuleVersion.cs @@ -65,17 +65,13 @@ public ModuleVersion(string version) /// true if versions have the same epoch, false if different /// public bool EpochEquals(ModuleVersion other) - { - return _epoch == other._epoch; - } + => _epoch == other._epoch; /// /// New module version with same version as 'this' but with one greater epoch /// public ModuleVersion IncrementEpoch() - { - return new ModuleVersion($"{_epoch + 1}:{_version}"); - } + => new ModuleVersion($"{_epoch + 1}:{_version}"); /// /// Converts the value of the current object to its equivalent diff --git a/GUI/Controls/Changeset.Designer.cs b/GUI/Controls/Changeset.Designer.cs index 87e767fdda..ace396195c 100644 --- a/GUI/Controls/Changeset.Designer.cs +++ b/GUI/Controls/Changeset.Designer.cs @@ -35,7 +35,7 @@ private void InitializeComponent() this.ChangeType = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.Reason = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.CloseTheGameLabel = new System.Windows.Forms.Label(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.ConfirmChangesButton = new System.Windows.Forms.Button(); this.BackButton = new System.Windows.Forms.Button(); this.CancelChangesButton = new System.Windows.Forms.Button(); @@ -157,7 +157,7 @@ private void InitializeComponent() private System.Windows.Forms.ColumnHeader ChangeType; private System.Windows.Forms.ColumnHeader Reason; private System.Windows.Forms.Label CloseTheGameLabel; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Button BackButton; private System.Windows.Forms.Button CancelChangesButton; private System.Windows.Forms.Button ConfirmChangesButton; diff --git a/GUI/Controls/ChooseProvidedMods.Designer.cs b/GUI/Controls/ChooseProvidedMods.Designer.cs index 7f4a268df6..f0da91877b 100644 --- a/GUI/Controls/ChooseProvidedMods.Designer.cs +++ b/GUI/Controls/ChooseProvidedMods.Designer.cs @@ -34,7 +34,7 @@ private void InitializeComponent() this.ChooseProvidedModsListView = new ThemedListView(); this.modNameColumnHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.modDescriptionColumnHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.ChooseProvidedModsCancelButton = new System.Windows.Forms.Button(); this.ChooseProvidedModsContinueButton = new System.Windows.Forms.Button(); this.BottomButtonPanel.SuspendLayout(); @@ -136,7 +136,7 @@ private void InitializeComponent() private System.Windows.Forms.ListView ChooseProvidedModsListView; private System.Windows.Forms.ColumnHeader modNameColumnHeader; private System.Windows.Forms.ColumnHeader modDescriptionColumnHeader; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Button ChooseProvidedModsCancelButton; private System.Windows.Forms.Button ChooseProvidedModsContinueButton; } diff --git a/GUI/Controls/ChooseRecommendedMods.Designer.cs b/GUI/Controls/ChooseRecommendedMods.Designer.cs index ddaec8ff7f..0395f2600f 100644 --- a/GUI/Controls/ChooseRecommendedMods.Designer.cs +++ b/GUI/Controls/ChooseRecommendedMods.Designer.cs @@ -38,7 +38,7 @@ private void InitializeComponent() this.ModNameHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.SourceModulesHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.DescriptionHeader = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.RecommendedModsToggleCheckbox = new System.Windows.Forms.CheckBox(); this.RecommendedModsCancelButton = new System.Windows.Forms.Button(); this.RecommendedModsContinueButton = new System.Windows.Forms.Button(); @@ -180,7 +180,7 @@ private void InitializeComponent() private System.Windows.Forms.ColumnHeader ModNameHeader; private System.Windows.Forms.ColumnHeader SourceModulesHeader; private System.Windows.Forms.ColumnHeader DescriptionHeader; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.CheckBox RecommendedModsToggleCheckbox; private System.Windows.Forms.Button RecommendedModsCancelButton; private System.Windows.Forms.Button RecommendedModsContinueButton; diff --git a/GUI/Controls/DeleteDirectories.Designer.cs b/GUI/Controls/DeleteDirectories.Designer.cs index 6a59309080..3aa3977545 100644 --- a/GUI/Controls/DeleteDirectories.Designer.cs +++ b/GUI/Controls/DeleteDirectories.Designer.cs @@ -37,7 +37,7 @@ private void InitializeComponent() this.ContentsListView = new ThemedListView(); this.FileColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.SelectDirPrompt = new System.Windows.Forms.ListViewItem(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.OpenDirectoryButton = new System.Windows.Forms.Button(); this.KeepAllButton = new System.Windows.Forms.Button(); this.DeleteButton = new System.Windows.Forms.Button(); @@ -198,7 +198,7 @@ private void InitializeComponent() private System.Windows.Forms.ListView ContentsListView; private System.Windows.Forms.ColumnHeader FileColumn; private System.Windows.Forms.ListViewItem SelectDirPrompt; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Button OpenDirectoryButton; private System.Windows.Forms.Button KeepAllButton; private System.Windows.Forms.Button DeleteButton; diff --git a/GUI/Controls/EditModpack.Designer.cs b/GUI/Controls/EditModpack.Designer.cs index 717a61221c..a80dac22f0 100644 --- a/GUI/Controls/EditModpack.Designer.cs +++ b/GUI/Controls/EditModpack.Designer.cs @@ -48,7 +48,7 @@ private void InitializeComponent() this.LicenseLabel = new System.Windows.Forms.Label(); this.LicenseComboBox = new System.Windows.Forms.ComboBox(); this.IncludeVersionsCheckbox = new System.Windows.Forms.CheckBox(); - this.RelationshipsListView = new ThemedListView(); + this.RelationshipsListView = new CKAN.GUI.ThemedListView(); this.ModNameColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.ModVersionColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.ModAbstractColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); @@ -56,7 +56,7 @@ private void InitializeComponent() this.RecommendationsGroup = new System.Windows.Forms.ListViewGroup(); this.SuggestionsGroup = new System.Windows.Forms.ListViewGroup(); this.IgnoredGroup = new System.Windows.Forms.ListViewGroup(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.DependsRadioButton = new System.Windows.Forms.RadioButton(); this.RecommendsRadioButton = new System.Windows.Forms.RadioButton(); this.SuggestsRadioButton = new System.Windows.Forms.RadioButton(); @@ -450,7 +450,7 @@ private void InitializeComponent() private System.Windows.Forms.ListViewGroup RecommendationsGroup; private System.Windows.Forms.ListViewGroup SuggestionsGroup; private System.Windows.Forms.ListViewGroup IgnoredGroup; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.RadioButton DependsRadioButton; private System.Windows.Forms.RadioButton RecommendsRadioButton; private System.Windows.Forms.RadioButton SuggestsRadioButton; diff --git a/GUI/Controls/InstallationHistory.Designer.cs b/GUI/Controls/InstallationHistory.Designer.cs index 77a177e069..48be3020a4 100644 --- a/GUI/Controls/InstallationHistory.Designer.cs +++ b/GUI/Controls/InstallationHistory.Designer.cs @@ -45,7 +45,7 @@ private void InitializeComponent() this.DescriptionColumn = ((System.Windows.Forms.ColumnHeader)(new System.Windows.Forms.ColumnHeader())); this.SelectInstallMessage = new System.Windows.Forms.ListViewItem(); this.NoModsMessage = new System.Windows.Forms.ListViewItem(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.OKButton = new System.Windows.Forms.Button(); this.BottomButtonPanel.SuspendLayout(); this.SuspendLayout(); @@ -232,7 +232,7 @@ private void InitializeComponent() private System.Windows.Forms.ColumnHeader DescriptionColumn; private System.Windows.Forms.ListViewItem SelectInstallMessage; private System.Windows.Forms.ListViewItem NoModsMessage; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Button OKButton; } } diff --git a/GUI/Controls/LeftRightRowPanel.cs b/GUI/Controls/LeftRightRowPanel.cs index 95c210d323..12bdfd5596 100644 --- a/GUI/Controls/LeftRightRowPanel.cs +++ b/GUI/Controls/LeftRightRowPanel.cs @@ -1,6 +1,6 @@ using System.Windows.Forms; -namespace CKAN +namespace CKAN.GUI { /// /// A panel containing two groups of controls in flow layouts, diff --git a/GUI/Controls/PlayTime.Designer.cs b/GUI/Controls/PlayTime.Designer.cs index 19828b50e3..47015dd913 100755 --- a/GUI/Controls/PlayTime.Designer.cs +++ b/GUI/Controls/PlayTime.Designer.cs @@ -2,12 +2,12 @@ namespace CKAN.GUI { partial class PlayTime { - /// + /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; - /// + /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. @@ -22,8 +22,8 @@ protected override void Dispose(bool disposing) #region Component Designer generated code - /// - /// Required method for Designer support - do not modify + /// + /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() @@ -34,7 +34,7 @@ private void InitializeComponent() this.PlayTimeGrid = new System.Windows.Forms.DataGridView(); this.NameColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.TimeColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.TotalLabel = new System.Windows.Forms.Label(); this.OKButton = new System.Windows.Forms.Button(); this.BottomButtonPanel.SuspendLayout(); @@ -149,7 +149,7 @@ private void InitializeComponent() private System.Windows.Forms.DataGridView PlayTimeGrid; private System.Windows.Forms.DataGridViewTextBoxColumn NameColumn; private System.Windows.Forms.DataGridViewTextBoxColumn TimeColumn; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Label TotalLabel; private System.Windows.Forms.Button OKButton; } diff --git a/GUI/Controls/UnmanagedFiles.Designer.cs b/GUI/Controls/UnmanagedFiles.Designer.cs index ee0779b189..81b67ee212 100644 --- a/GUI/Controls/UnmanagedFiles.Designer.cs +++ b/GUI/Controls/UnmanagedFiles.Designer.cs @@ -42,7 +42,7 @@ private void InitializeComponent() this.DeleteButton = new System.Windows.Forms.ToolStripMenuItem(); this.GameFolderTree = new System.Windows.Forms.TreeView(); this.OKButton = new System.Windows.Forms.Button(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.BottomButtonPanel.SuspendLayout(); this.SuspendLayout(); // @@ -192,7 +192,7 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripMenuItem ShowInFolderButton; private System.Windows.Forms.ToolStripMenuItem DeleteButton; private System.Windows.Forms.TreeView GameFolderTree; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Button OKButton; } } diff --git a/GUI/Controls/UnmanagedFiles.cs b/GUI/Controls/UnmanagedFiles.cs index 887af5dcaf..992a9535a0 100644 --- a/GUI/Controls/UnmanagedFiles.cs +++ b/GUI/Controls/UnmanagedFiles.cs @@ -63,7 +63,7 @@ private void _UpdateGameFolderTree() Task.Factory.StartNew(() => { var paths = inst?.UnmanagedFiles(registry).ToArray() - ?? new string[] { }; + ?? Array.Empty(); Util.Invoke(this, () => { GameFolderTree.BeginUpdate(); diff --git a/GUI/Controls/Wait.Designer.cs b/GUI/Controls/Wait.Designer.cs index 4baca22724..f1dc3a608d 100644 --- a/GUI/Controls/Wait.Designer.cs +++ b/GUI/Controls/Wait.Designer.cs @@ -35,7 +35,7 @@ private void InitializeComponent() this.DialogProgressBar = new System.Windows.Forms.ProgressBar(); this.ProgressBarTable = new System.Windows.Forms.TableLayoutPanel(); this.LogTextBox = new System.Windows.Forms.TextBox(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.CancelCurrentActionButton = new System.Windows.Forms.Button(); this.RetryCurrentActionButton = new System.Windows.Forms.Button(); this.OkButton = new System.Windows.Forms.Button(); @@ -183,7 +183,7 @@ private void InitializeComponent() private System.Windows.Forms.ProgressBar DialogProgressBar; private System.Windows.Forms.TableLayoutPanel ProgressBarTable; private System.Windows.Forms.TextBox LogTextBox; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Button RetryCurrentActionButton; private System.Windows.Forms.Button CancelCurrentActionButton; private System.Windows.Forms.Button OkButton; diff --git a/GUI/Dialogs/DownloadsFailedDialog.Designer.cs b/GUI/Dialogs/DownloadsFailedDialog.Designer.cs index 02ebe26c55..dd180e2198 100644 --- a/GUI/Dialogs/DownloadsFailedDialog.Designer.cs +++ b/GUI/Dialogs/DownloadsFailedDialog.Designer.cs @@ -36,7 +36,7 @@ private void InitializeComponent() this.SkipColumn = new System.Windows.Forms.DataGridViewCheckBoxColumn(); this.ModColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); this.ErrorColumn = new System.Windows.Forms.DataGridViewTextBoxColumn(); - this.BottomButtonPanel = new LeftRightRowPanel(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.RetryButton = new System.Windows.Forms.Button(); this.AbortButton = new System.Windows.Forms.Button(); ((System.ComponentModel.ISupportInitialize)(this.DownloadsGrid)).BeginInit(); @@ -186,7 +186,7 @@ private void InitializeComponent() private System.Windows.Forms.DataGridViewCheckBoxColumn SkipColumn; private System.Windows.Forms.DataGridViewTextBoxColumn ModColumn; private System.Windows.Forms.DataGridViewTextBoxColumn ErrorColumn; - private LeftRightRowPanel BottomButtonPanel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.Button RetryButton; private System.Windows.Forms.Button AbortButton; } diff --git a/GUI/Dialogs/DownloadsFailedDialog.cs b/GUI/Dialogs/DownloadsFailedDialog.cs index f5bb21cb03..9b604f2a59 100644 --- a/GUI/Dialogs/DownloadsFailedDialog.cs +++ b/GUI/Dialogs/DownloadsFailedDialog.cs @@ -195,7 +195,6 @@ public DownloadRow(object data, Exception exc) /// /// True if Skip column has a checkmark /// - /// #pragma warning disable IDE0027 public bool Skip { get => !Retry; set { Retry = !value; } } #pragma warning restore IDE0027 diff --git a/GUI/Dialogs/SettingsDialog.Designer.cs b/GUI/Dialogs/SettingsDialog.Designer.cs index 80c4f4c412..505687706a 100644 --- a/GUI/Dialogs/SettingsDialog.Designer.cs +++ b/GUI/Dialogs/SettingsDialog.Designer.cs @@ -63,6 +63,7 @@ private void InitializeComponent() this.LatestVersionPreLabel = new System.Windows.Forms.Label(); this.LatestVersionLabel = new System.Windows.Forms.Label(); this.CheckUpdateOnLaunchCheckbox = new System.Windows.Forms.CheckBox(); + this.DevBuildsCheckbox = new System.Windows.Forms.CheckBox(); this.CheckForUpdatesButton = new System.Windows.Forms.Button(); this.InstallUpdateButton = new System.Windows.Forms.Button(); this.BehaviourGroupBox = new System.Windows.Forms.GroupBox(); @@ -100,7 +101,7 @@ private void InitializeComponent() this.RepositoryGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.RepositoryGroupBox.Location = new System.Drawing.Point(12, 6); this.RepositoryGroupBox.Name = "RepositoryGroupBox"; - this.RepositoryGroupBox.Size = new System.Drawing.Size(476, 128); + this.RepositoryGroupBox.Size = new System.Drawing.Size(488, 128); this.RepositoryGroupBox.TabIndex = 0; this.RepositoryGroupBox.TabStop = false; resources.ApplyResources(this.RepositoryGroupBox, "RepositoryGroupBox"); @@ -115,7 +116,7 @@ private void InitializeComponent() this.ReposListBox.FullRowSelect = true; this.ReposListBox.MultiSelect = false; this.ReposListBox.Name = "ReposListBox"; - this.ReposListBox.Size = new System.Drawing.Size(452, 67); + this.ReposListBox.Size = new System.Drawing.Size(464, 67); this.ReposListBox.TabIndex = 0; this.ReposListBox.View = System.Windows.Forms.View.Details; this.ReposListBox.SelectedIndexChanged += new System.EventHandler(this.ReposListBox_SelectedIndexChanged); @@ -166,7 +167,7 @@ private void InitializeComponent() // this.DeleteRepoButton.Enabled = false; this.DeleteRepoButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.DeleteRepoButton.Location = new System.Drawing.Point(394, 93); + this.DeleteRepoButton.Location = new System.Drawing.Point(406, 93); this.DeleteRepoButton.Name = "DeleteRepoButton"; this.DeleteRepoButton.Size = new System.Drawing.Size(70, 25); this.DeleteRepoButton.TabIndex = 4; @@ -180,9 +181,9 @@ private void InitializeComponent() this.AuthTokensGroupBox.Controls.Add(this.DeleteAuthTokenButton); this.AuthTokensGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; this.AuthTokensGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.AuthTokensGroupBox.Location = new System.Drawing.Point(12, 140); + this.AuthTokensGroupBox.Location = new System.Drawing.Point(512, 6); this.AuthTokensGroupBox.Name = "AuthTokensGroupBox"; - this.AuthTokensGroupBox.Size = new System.Drawing.Size(476, 128); + this.AuthTokensGroupBox.Size = new System.Drawing.Size(244, 128); this.AuthTokensGroupBox.TabIndex = 1; this.AuthTokensGroupBox.TabStop = false; resources.ApplyResources(this.AuthTokensGroupBox, "AuthTokensGroupBox"); @@ -198,13 +199,13 @@ private void InitializeComponent() this.AuthTokensListBox.View = System.Windows.Forms.View.Details; this.AuthTokensListBox.Location = new System.Drawing.Point(12, 18); this.AuthTokensListBox.Name = "AuthTokensListBox"; - this.AuthTokensListBox.Size = new System.Drawing.Size(452, 67); + this.AuthTokensListBox.Size = new System.Drawing.Size(220, 67); this.AuthTokensListBox.TabIndex = 0; this.AuthTokensListBox.SelectedIndexChanged += new System.EventHandler(this.AuthTokensListBox_SelectedIndexChanged); // // AuthHostHeader // - this.AuthHostHeader.Width = 120; + this.AuthHostHeader.Width = 100; resources.ApplyResources(this.AuthHostHeader, "AuthHostHeader"); // // AuthTokenHeader @@ -226,13 +227,111 @@ private void InitializeComponent() // this.DeleteAuthTokenButton.Enabled = false; this.DeleteAuthTokenButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.DeleteAuthTokenButton.Location = new System.Drawing.Point(394, 93); + this.DeleteAuthTokenButton.Location = new System.Drawing.Point(164, 93); this.DeleteAuthTokenButton.Name = "DeleteAuthTokenButton"; this.DeleteAuthTokenButton.Size = new System.Drawing.Size(70, 25); this.DeleteAuthTokenButton.TabIndex = 2; this.DeleteAuthTokenButton.Click += new System.EventHandler(this.DeleteAuthTokenButton_Click); resources.ApplyResources(this.DeleteAuthTokenButton, "DeleteAuthTokenButton"); // + // AutoUpdateGroupBox + // + this.AutoUpdateGroupBox.Controls.Add(this.LocalVersionPreLabel); + this.AutoUpdateGroupBox.Controls.Add(this.LocalVersionLabel); + this.AutoUpdateGroupBox.Controls.Add(this.LatestVersionPreLabel); + this.AutoUpdateGroupBox.Controls.Add(this.LatestVersionLabel); + this.AutoUpdateGroupBox.Controls.Add(this.CheckUpdateOnLaunchCheckbox); + this.AutoUpdateGroupBox.Controls.Add(this.DevBuildsCheckbox); + this.AutoUpdateGroupBox.Controls.Add(this.CheckForUpdatesButton); + this.AutoUpdateGroupBox.Controls.Add(this.InstallUpdateButton); + this.AutoUpdateGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; + this.AutoUpdateGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.AutoUpdateGroupBox.Location = new System.Drawing.Point(12, 144); + this.AutoUpdateGroupBox.Name = "AutoUpdateGroupBox"; + this.AutoUpdateGroupBox.Size = new System.Drawing.Size(254, 156); + this.AutoUpdateGroupBox.TabIndex = 2; + this.AutoUpdateGroupBox.TabStop = false; + resources.ApplyResources(this.AutoUpdateGroupBox, "AutoUpdateGroupBox"); + // + // LocalVersionPreLabel + // + this.LocalVersionPreLabel.AutoSize = true; + this.LocalVersionPreLabel.Location = new System.Drawing.Point(9, 18); + this.LocalVersionPreLabel.Name = "LocalVersionPreLabel"; + this.LocalVersionPreLabel.Size = new System.Drawing.Size(73, 13); + this.LocalVersionPreLabel.TabIndex = 0; + resources.ApplyResources(this.LocalVersionPreLabel, "LocalVersionPreLabel"); + // + // LocalVersionLabel + // + this.LocalVersionLabel.AutoSize = true; + this.LocalVersionLabel.Location = new System.Drawing.Point(95, 18); + this.LocalVersionLabel.Name = "LocalVersionLabel"; + this.LocalVersionLabel.Size = new System.Drawing.Size(37, 13); + this.LocalVersionLabel.TabIndex = 1; + resources.ApplyResources(this.LocalVersionLabel, "LocalVersionLabel"); + // + // LatestVersionPreLabel + // + this.LatestVersionPreLabel.AutoSize = true; + this.LatestVersionPreLabel.Location = new System.Drawing.Point(9, 39); + this.LatestVersionPreLabel.Name = "LatestVersionPreLabel"; + this.LatestVersionPreLabel.Size = new System.Drawing.Size(76, 13); + this.LatestVersionPreLabel.TabIndex = 2; + resources.ApplyResources(this.LatestVersionPreLabel, "LatestVersionPreLabel"); + // + // LatestVersionLabel + // + this.LatestVersionLabel.AutoSize = true; + this.LatestVersionLabel.Location = new System.Drawing.Point(95, 39); + this.LatestVersionLabel.Name = "LatestVersionLabel"; + this.LatestVersionLabel.Size = new System.Drawing.Size(25, 13); + this.LatestVersionLabel.TabIndex = 3; + resources.ApplyResources(this.LatestVersionLabel, "LatestVersionLabel"); + // + // CheckUpdateOnLaunchCheckbox + // + this.CheckUpdateOnLaunchCheckbox.AutoSize = true; + this.CheckUpdateOnLaunchCheckbox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.CheckUpdateOnLaunchCheckbox.Location = new System.Drawing.Point(12, 61); + this.CheckUpdateOnLaunchCheckbox.Name = "CheckUpdateOnLaunchCheckbox"; + this.CheckUpdateOnLaunchCheckbox.Size = new System.Drawing.Size(195, 17); + this.CheckUpdateOnLaunchCheckbox.TabIndex = 4; + this.CheckUpdateOnLaunchCheckbox.CheckedChanged += new System.EventHandler(this.CheckUpdateOnLaunchCheckbox_CheckedChanged); + resources.ApplyResources(this.CheckUpdateOnLaunchCheckbox, "CheckUpdateOnLaunchCheckbox"); + // + // DevBuildsCheckbox + // + this.DevBuildsCheckbox.AutoSize = true; + this.DevBuildsCheckbox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.DevBuildsCheckbox.Location = new System.Drawing.Point(12, 85); + this.DevBuildsCheckbox.Name = "DevBuildsCheckbox"; + this.DevBuildsCheckbox.Size = new System.Drawing.Size(195, 17); + this.DevBuildsCheckbox.TabIndex = 4; + this.DevBuildsCheckbox.CheckedChanged += new System.EventHandler(this.DevBuildsCheckbox_CheckedChanged); + resources.ApplyResources(this.DevBuildsCheckbox, "DevBuildsCheckbox"); + // + // CheckForUpdatesButton + // + this.CheckForUpdatesButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.CheckForUpdatesButton.Location = new System.Drawing.Point(12, 108); + this.CheckForUpdatesButton.Name = "CheckForUpdatesButton"; + this.CheckForUpdatesButton.Size = new System.Drawing.Size(112, 38); + this.CheckForUpdatesButton.TabIndex = 5; + this.CheckForUpdatesButton.Click += new System.EventHandler(this.CheckForUpdatesButton_Click); + resources.ApplyResources(this.CheckForUpdatesButton, "CheckForUpdatesButton"); + // + // InstallUpdateButton + // + this.InstallUpdateButton.Enabled = false; + this.InstallUpdateButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; + this.InstallUpdateButton.Location = new System.Drawing.Point(130, 108); + this.InstallUpdateButton.Name = "InstallUpdateButton"; + this.InstallUpdateButton.Size = new System.Drawing.Size(112, 38); + this.InstallUpdateButton.TabIndex = 6; + this.InstallUpdateButton.Click += new System.EventHandler(this.InstallUpdateButton_Click); + resources.ApplyResources(this.InstallUpdateButton, "InstallUpdateButton"); + // // CacheGroupBox // this.CacheGroupBox.Controls.Add(this.CachePath); @@ -246,10 +345,10 @@ private void InitializeComponent() this.CacheGroupBox.Controls.Add(this.OpenCacheButton); this.CacheGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; this.CacheGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.CacheGroupBox.Location = new System.Drawing.Point(12, 274); + this.CacheGroupBox.Location = new System.Drawing.Point(280, 144); this.CacheGroupBox.Name = "CacheGroupBox"; - this.CacheGroupBox.Size = new System.Drawing.Size(476, 124); - this.CacheGroupBox.TabIndex = 2; + this.CacheGroupBox.Size = new System.Drawing.Size(476, 156); + this.CacheGroupBox.TabIndex = 3; this.CacheGroupBox.TabStop = false; resources.ApplyResources(this.CacheGroupBox, "CacheGroupBox"); // @@ -367,92 +466,6 @@ private void InitializeComponent() this.OpenCacheButton.Click += new System.EventHandler(this.OpenCacheButton_Click); resources.ApplyResources(this.OpenCacheButton, "OpenCacheButton"); // - // AutoUpdateGroupBox - // - this.AutoUpdateGroupBox.Controls.Add(this.LocalVersionPreLabel); - this.AutoUpdateGroupBox.Controls.Add(this.LocalVersionLabel); - this.AutoUpdateGroupBox.Controls.Add(this.LatestVersionPreLabel); - this.AutoUpdateGroupBox.Controls.Add(this.LatestVersionLabel); - this.AutoUpdateGroupBox.Controls.Add(this.CheckUpdateOnLaunchCheckbox); - this.AutoUpdateGroupBox.Controls.Add(this.CheckForUpdatesButton); - this.AutoUpdateGroupBox.Controls.Add(this.InstallUpdateButton); - this.AutoUpdateGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; - this.AutoUpdateGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.AutoUpdateGroupBox.Location = new System.Drawing.Point(12, 404); - this.AutoUpdateGroupBox.Name = "AutoUpdateGroupBox"; - this.AutoUpdateGroupBox.Size = new System.Drawing.Size(254, 132); - this.AutoUpdateGroupBox.TabIndex = 3; - this.AutoUpdateGroupBox.TabStop = false; - resources.ApplyResources(this.AutoUpdateGroupBox, "AutoUpdateGroupBox"); - // - // LocalVersionPreLabel - // - this.LocalVersionPreLabel.AutoSize = true; - this.LocalVersionPreLabel.Location = new System.Drawing.Point(9, 18); - this.LocalVersionPreLabel.Name = "LocalVersionPreLabel"; - this.LocalVersionPreLabel.Size = new System.Drawing.Size(73, 13); - this.LocalVersionPreLabel.TabIndex = 0; - resources.ApplyResources(this.LocalVersionPreLabel, "LocalVersionPreLabel"); - // - // LocalVersionLabel - // - this.LocalVersionLabel.AutoSize = true; - this.LocalVersionLabel.Location = new System.Drawing.Point(95, 18); - this.LocalVersionLabel.Name = "LocalVersionLabel"; - this.LocalVersionLabel.Size = new System.Drawing.Size(37, 13); - this.LocalVersionLabel.TabIndex = 1; - resources.ApplyResources(this.LocalVersionLabel, "LocalVersionLabel"); - // - // LatestVersionPreLabel - // - this.LatestVersionPreLabel.AutoSize = true; - this.LatestVersionPreLabel.Location = new System.Drawing.Point(9, 39); - this.LatestVersionPreLabel.Name = "LatestVersionPreLabel"; - this.LatestVersionPreLabel.Size = new System.Drawing.Size(76, 13); - this.LatestVersionPreLabel.TabIndex = 2; - resources.ApplyResources(this.LatestVersionPreLabel, "LatestVersionPreLabel"); - // - // LatestVersionLabel - // - this.LatestVersionLabel.AutoSize = true; - this.LatestVersionLabel.Location = new System.Drawing.Point(95, 39); - this.LatestVersionLabel.Name = "LatestVersionLabel"; - this.LatestVersionLabel.Size = new System.Drawing.Size(25, 13); - this.LatestVersionLabel.TabIndex = 3; - resources.ApplyResources(this.LatestVersionLabel, "LatestVersionLabel"); - // - // CheckUpdateOnLaunchCheckbox - // - this.CheckUpdateOnLaunchCheckbox.AutoSize = true; - this.CheckUpdateOnLaunchCheckbox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.CheckUpdateOnLaunchCheckbox.Location = new System.Drawing.Point(12, 61); - this.CheckUpdateOnLaunchCheckbox.Name = "CheckUpdateOnLaunchCheckbox"; - this.CheckUpdateOnLaunchCheckbox.Size = new System.Drawing.Size(195, 17); - this.CheckUpdateOnLaunchCheckbox.TabIndex = 4; - this.CheckUpdateOnLaunchCheckbox.CheckedChanged += new System.EventHandler(this.CheckUpdateOnLaunchCheckbox_CheckedChanged); - resources.ApplyResources(this.CheckUpdateOnLaunchCheckbox, "CheckUpdateOnLaunchCheckbox"); - // - // CheckForUpdatesButton - // - this.CheckForUpdatesButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.CheckForUpdatesButton.Location = new System.Drawing.Point(12, 84); - this.CheckForUpdatesButton.Name = "CheckForUpdatesButton"; - this.CheckForUpdatesButton.Size = new System.Drawing.Size(112, 38); - this.CheckForUpdatesButton.TabIndex = 5; - this.CheckForUpdatesButton.Click += new System.EventHandler(this.CheckForUpdatesButton_Click); - resources.ApplyResources(this.CheckForUpdatesButton, "CheckForUpdatesButton"); - // - // InstallUpdateButton - // - this.InstallUpdateButton.Enabled = false; - this.InstallUpdateButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.InstallUpdateButton.Location = new System.Drawing.Point(130, 84); - this.InstallUpdateButton.Name = "InstallUpdateButton"; - this.InstallUpdateButton.Size = new System.Drawing.Size(112, 38); - this.InstallUpdateButton.TabIndex = 6; - this.InstallUpdateButton.Click += new System.EventHandler(this.InstallUpdateButton_Click); - resources.ApplyResources(this.InstallUpdateButton, "InstallUpdateButton"); - // // BehaviourGroupBox // this.BehaviourGroupBox.Controls.Add(this.EnableTrayIconCheckBox); @@ -463,9 +476,9 @@ private void InitializeComponent() this.BehaviourGroupBox.Controls.Add(this.PauseRefreshCheckBox); this.BehaviourGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; this.BehaviourGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.BehaviourGroupBox.Location = new System.Drawing.Point(278, 404); + this.BehaviourGroupBox.Location = new System.Drawing.Point(12, 310); this.BehaviourGroupBox.Name = "BehaviourGroupBox"; - this.BehaviourGroupBox.Size = new System.Drawing.Size(210, 132); + this.BehaviourGroupBox.Size = new System.Drawing.Size(254, 150); this.BehaviourGroupBox.TabIndex = 4; this.BehaviourGroupBox.TabStop = false; resources.ApplyResources(this.BehaviourGroupBox, "BehaviourGroupBox"); @@ -544,7 +557,7 @@ private void InitializeComponent() this.MoreSettingsGroupBox.Controls.Add(this.HideVCheckbox); this.MoreSettingsGroupBox.ForeColor = System.Drawing.SystemColors.ControlText; this.MoreSettingsGroupBox.FlatStyle = System.Windows.Forms.FlatStyle.Flat; - this.MoreSettingsGroupBox.Location = new System.Drawing.Point(12, 542); + this.MoreSettingsGroupBox.Location = new System.Drawing.Point(280, 310); this.MoreSettingsGroupBox.Name = "MoreSettingsGroupBox"; this.MoreSettingsGroupBox.Size = new System.Drawing.Size(476, 150); this.MoreSettingsGroupBox.TabIndex = 5; @@ -622,7 +635,7 @@ private void InitializeComponent() // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(500, 703); + this.ClientSize = new System.Drawing.Size(768, 470); this.Controls.Add(this.RepositoryGroupBox); this.Controls.Add(this.AuthTokensGroupBox); this.Controls.Add(this.CacheGroupBox); @@ -687,6 +700,7 @@ private void InitializeComponent() private System.Windows.Forms.Label LatestVersionPreLabel; private System.Windows.Forms.Label LatestVersionLabel; private System.Windows.Forms.CheckBox CheckUpdateOnLaunchCheckbox; + private System.Windows.Forms.CheckBox DevBuildsCheckbox; private System.Windows.Forms.Button CheckForUpdatesButton; private System.Windows.Forms.Button InstallUpdateButton; private System.Windows.Forms.GroupBox BehaviourGroupBox; diff --git a/GUI/Dialogs/SettingsDialog.cs b/GUI/Dialogs/SettingsDialog.cs index d3b7f5ce4f..8939f0860c 100644 --- a/GUI/Dialogs/SettingsDialog.cs +++ b/GUI/Dialogs/SettingsDialog.cs @@ -8,7 +8,6 @@ #endif using log4net; -using Autofac; using CKAN.Versioning; using CKAN.Configuration; @@ -26,23 +25,31 @@ public partial class SettingsDialog : Form public bool RepositoryRemoved { get; private set; } = false; public bool RepositoryMoved { get; private set; } = false; - private readonly IUser m_user; - private readonly IConfiguration config; - private readonly RegistryManager regMgr; + private readonly IConfiguration coreConfig; + private readonly GUIConfiguration guiConfig; + private readonly RegistryManager regMgr; + private readonly AutoUpdate updater; + private readonly IUser user; /// /// Initialize a settings window /// - public SettingsDialog(RegistryManager regMgr, IUser user) + public SettingsDialog(IConfiguration coreConfig, + GUIConfiguration guiConfig, + RegistryManager regMgr, + AutoUpdate updater, + IUser user) { InitializeComponent(); - m_user = user; - this.regMgr = regMgr; + this.coreConfig = coreConfig; + this.guiConfig = guiConfig; + this.regMgr = regMgr; + this.updater = updater; + this.user = user; if (Platform.IsMono) { ClearCacheMenu.Renderer = new FlatToolStripRenderer(); } - config = ServiceLocator.Container.Resolve(); } private void SettingsDialog_Load(object sender, EventArgs e) @@ -55,29 +62,50 @@ public void UpdateDialog() RefreshReposListBox(false); RefreshAuthTokensListBox(); UpdateLanguageSelectionComboBox(); - - LocalVersionLabel.Text = Meta.GetVersion(); - - CheckUpdateOnLaunchCheckbox.Checked = Main.Instance.configuration.CheckForUpdatesOnLaunch; - RefreshOnStartupCheckbox.Checked = Main.Instance.configuration.RefreshOnStartup; - HideEpochsCheckbox.Checked = Main.Instance.configuration.HideEpochs; - HideVCheckbox.Checked = Main.Instance.configuration.HideV; - AutoSortUpdateCheckBox.Checked = Main.Instance.configuration.AutoSortByUpdate; - EnableTrayIconCheckBox.Checked = MinimizeToTrayCheckBox.Enabled = Main.Instance.configuration.EnableTrayIcon; - MinimizeToTrayCheckBox.Checked = Main.Instance.configuration.MinimizeToTray; - PauseRefreshCheckBox.Checked = Main.Instance.configuration.RefreshPaused; + UpdateAutoUpdate(); + + CheckUpdateOnLaunchCheckbox.Checked = guiConfig.CheckForUpdatesOnLaunch; + DevBuildsCheckbox.Checked = coreConfig.DevBuilds; + RefreshOnStartupCheckbox.Checked = guiConfig.RefreshOnStartup; + HideEpochsCheckbox.Checked = guiConfig.HideEpochs; + HideVCheckbox.Checked = guiConfig.HideV; + AutoSortUpdateCheckBox.Checked = guiConfig.AutoSortByUpdate; + EnableTrayIconCheckBox.Checked = MinimizeToTrayCheckBox.Enabled = guiConfig.EnableTrayIcon; + MinimizeToTrayCheckBox.Checked = guiConfig.MinimizeToTray; + PauseRefreshCheckBox.Checked = guiConfig.RefreshPaused; UpdateRefreshRate(); - UpdateCacheInfo(config.DownloadCacheDir); + UpdateCacheInfo(coreConfig.DownloadCacheDir); + } + + private void UpdateAutoUpdate() + { + LocalVersionLabel.Text = Meta.GetVersion(); + try + { + var latestVersion = updater.GetUpdate(coreConfig.DevBuilds) + .Version; + LatestVersionLabel.Text = latestVersion.ToString(); + // Allow downgrading in case they want to stop using dev builds + InstallUpdateButton.Enabled = !latestVersion.Equals(new ModuleVersion(Meta.GetVersion())); + } + catch + { + // Can't get the version, reset the label + var resources = new SingleAssemblyComponentResourceManager(typeof(SettingsDialog)); + resources.ApplyResources(LatestVersionLabel, + LatestVersionLabel.Name); + InstallUpdateButton.Enabled = false; + } } protected override void OnFormClosing(FormClosingEventArgs e) { - if (CachePath.Text != config.DownloadCacheDir + if (CachePath.Text != coreConfig.DownloadCacheDir && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) { - m_user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); e.Cancel = true; } else @@ -88,10 +116,10 @@ protected override void OnFormClosing(FormClosingEventArgs e) private void UpdateRefreshRate() { - int rate = config.RefreshRate; + int rate = coreConfig.RefreshRate; RefreshTextBox.Text = rate.ToString(); PauseRefreshCheckBox.Enabled = rate != 0; - Main.Instance.pauseToolStripMenuItem.Enabled = config.RefreshRate != 0; + Main.Instance.pauseToolStripMenuItem.Enabled = coreConfig.RefreshRate != 0; Main.Instance.UpdateRefreshTimer(); } @@ -135,7 +163,7 @@ private void UpdateLanguageSelectionComboBox() LanguageSelectionComboBox.Items.AddRange(Utilities.AvailableLanguages); // If the current language is supported by CKAN, set is as selected. // Else display a blank field. - LanguageSelectionComboBox.SelectedIndex = LanguageSelectionComboBox.FindStringExact(config.Language); + LanguageSelectionComboBox.SelectedIndex = LanguageSelectionComboBox.FindStringExact(coreConfig.Language); } private void UpdateCacheInfo(string newPath) @@ -152,10 +180,10 @@ private void UpdateCacheInfo(string newPath) Util.Invoke(this, () => { - if (config.CacheSizeLimit.HasValue) + if (coreConfig.CacheSizeLimit.HasValue) { // Show setting in MiB - CacheLimit.Text = (config.CacheSizeLimit.Value / 1024 / 1024).ToString(); + CacheLimit.Text = (coreConfig.CacheSizeLimit.Value / 1024 / 1024).ToString(); } CacheSummary.Text = string.Format( Properties.Resources.SettingsDialogSummmary, @@ -163,8 +191,8 @@ private void UpdateCacheInfo(string newPath) CacheSummary.ForeColor = SystemColors.ControlText; OpenCacheButton.Enabled = true; ClearCacheButton.Enabled = (cacheSize > 0); - PurgeToLimitMenuItem.Enabled = (config.CacheSizeLimit.HasValue - && cacheSize > config.CacheSizeLimit.Value); + PurgeToLimitMenuItem.Enabled = (coreConfig.CacheSizeLimit.HasValue + && cacheSize > coreConfig.CacheSizeLimit.Value); }); } @@ -191,12 +219,12 @@ private void CacheLimit_TextChanged(object sender, EventArgs e) { if (string.IsNullOrEmpty(CacheLimit.Text)) { - config.CacheSizeLimit = null; + coreConfig.CacheSizeLimit = null; } else { // Translate from MB to bytes - config.CacheSizeLimit = Convert.ToInt64(CacheLimit.Text) * 1024 * 1024; + coreConfig.CacheSizeLimit = Convert.ToInt64(CacheLimit.Text) * 1024 * 1024; } UpdateCacheInfo(CachePath.Text); } @@ -215,7 +243,7 @@ private void ChangeCacheButton_Click(object sender, EventArgs e) { Description = Properties.Resources.SettingsDialogCacheDescrip, RootFolder = Environment.SpecialFolder.MyComputer, - SelectedPath = config.DownloadCacheDir, + SelectedPath = coreConfig.DownloadCacheDir, ShowNewFolderButton = true }; DialogResult result = cacheChooser.ShowDialog(this); @@ -228,30 +256,30 @@ private void ChangeCacheButton_Click(object sender, EventArgs e) private void PurgeToLimitMenuItem_Click(object sender, EventArgs e) { // Purge old downloads if we're over the limit - if (config.CacheSizeLimit.HasValue) + if (coreConfig.CacheSizeLimit.HasValue) { // Switch main cache since user seems committed to this path - if (CachePath.Text != config.DownloadCacheDir + if (CachePath.Text != coreConfig.DownloadCacheDir && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) { - m_user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); return; } Main.Instance.Manager.Cache.EnforceSizeLimit( - config.CacheSizeLimit.Value, + coreConfig.CacheSizeLimit.Value, regMgr.registry); - UpdateCacheInfo(config.DownloadCacheDir); + UpdateCacheInfo(coreConfig.DownloadCacheDir); } } private void PurgeAllMenuItem_Click(object sender, EventArgs e) { // Switch main cache since user seems committed to this path - if (CachePath.Text != config.DownloadCacheDir + if (CachePath.Text != coreConfig.DownloadCacheDir && !Main.Instance.Manager.TrySetupCache(CachePath.Text, out string failReason)) { - m_user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); + user.RaiseError(Properties.Resources.SettingsDialogSummaryInvalid, failReason); return; } @@ -279,7 +307,7 @@ private void PurgeAllMenuItem_Click(object sender, EventArgs e) // finally, clear the preview contents list Main.Instance.RefreshModContentsTree(); - UpdateCacheInfo(config.DownloadCacheDir); + UpdateCacheInfo(coreConfig.DownloadCacheDir); } } @@ -291,7 +319,7 @@ private void ResetCacheButton_Click(object sender, EventArgs e) private void OpenCacheButton_Click(object sender, EventArgs e) { - Utilities.ProcessStartURL(config.DownloadCacheDir); + Utilities.ProcessStartURL(coreConfig.DownloadCacheDir); } private void ReposListBox_SelectedIndexChanged(object sender, EventArgs e) @@ -347,7 +375,7 @@ private void NewRepoButton_Click(object sender, EventArgs e) var registry = regMgr.registry; if (registry.Repositories.Values.Any(other => other.uri == repo.uri)) { - m_user.RaiseError(Properties.Resources.SettingsDialogRepoAddDuplicateURL, repo.uri); + user.RaiseError(Properties.Resources.SettingsDialogRepoAddDuplicateURL, repo.uri); return; } if (registry.Repositories.TryGetValue(repo.name, out Repository existing)) @@ -411,9 +439,9 @@ private void DownRepoButton_Click(object sender, EventArgs e) private void RefreshAuthTokensListBox() { AuthTokensListBox.Items.Clear(); - foreach (string host in config.GetAuthTokenHosts()) + foreach (string host in coreConfig.GetAuthTokenHosts()) { - if (config.TryGetAuthToken(host, out string token)) + if (coreConfig.TryGetAuthToken(host, out string token)) { AuthTokensListBox.Items.Add(new ListViewItem( new string[] { host, token }) @@ -508,7 +536,7 @@ private void NewAuthTokenButton_Click(object sender, EventArgs e) case DialogResult.OK: case DialogResult.Yes: - config.SetAuthToken(hostTextBox.Text, tokenTextBox.Text); + coreConfig.SetAuthToken(hostTextBox.Text, tokenTextBox.Text); RefreshAuthTokensListBox(); break; } @@ -518,22 +546,22 @@ private bool validNewAuthToken(string host, string token) { if (host.Length <= 0) { - m_user.RaiseError(Properties.Resources.AddAuthTokenHostRequired); + user.RaiseError(Properties.Resources.AddAuthTokenHostRequired); return false; } if (token.Length <= 0) { - m_user.RaiseError(Properties.Resources.AddAuthTokenTokenRequired); + user.RaiseError(Properties.Resources.AddAuthTokenTokenRequired); return false; } if (Uri.CheckHostName(host) == UriHostNameType.Unknown) { - m_user.RaiseError(Properties.Resources.AddAuthTokenInvalidHost, host); + user.RaiseError(Properties.Resources.AddAuthTokenInvalidHost, host); return false; } - if (ServiceLocator.Container.Resolve().TryGetAuthToken(host, out _)) + if (coreConfig.TryGetAuthToken(host, out _)) { - m_user.RaiseError(Properties.Resources.AddAuthTokenDupHost, host); + user.RaiseError(Properties.Resources.AddAuthTokenDupHost, host); return false; } @@ -547,7 +575,7 @@ private void DeleteAuthTokenButton_Click(object sender, EventArgs e) string item = AuthTokensListBox.SelectedItems[0].Tag as string; string host = item?.Split('|')[0].Trim(); - config.SetAuthToken(host, null); + coreConfig.SetAuthToken(host, null); RefreshAuthTokensListBox(); DeleteAuthTokenButton.Enabled = false; } @@ -557,16 +585,11 @@ private void CheckForUpdatesButton_Click(object sender, EventArgs e) { try { - AutoUpdate.Instance.FetchLatestReleaseInfo(); - var latestVersion = AutoUpdate.Instance.latestUpdate.Version; - InstallUpdateButton.Enabled = latestVersion.IsGreaterThan(new ModuleVersion(Meta.GetVersion(VersionFormat.Short))) - && AutoUpdate.Instance.IsFetched(); - - LatestVersionLabel.Text = latestVersion.ToString(); + UpdateAutoUpdate(); } - catch (Exception ex) + catch (Exception exc) { - log.Warn("Exception caught in CheckForUpdates:\r\n" + ex); + log.Warn("Exception caught in CheckForUpdates:\r\n" + exc); } } @@ -579,29 +602,34 @@ private void InstallUpdateButton_Click(object sender, EventArgs e) } else { - m_user.RaiseError(Properties.Resources.SettingsDialogUpdateFailed); + user.RaiseError(Properties.Resources.SettingsDialogUpdateFailed); } - } private void CheckUpdateOnLaunchCheckbox_CheckedChanged(object sender, EventArgs e) { - Main.Instance.configuration.CheckForUpdatesOnLaunch = CheckUpdateOnLaunchCheckbox.Checked; + guiConfig.CheckForUpdatesOnLaunch = CheckUpdateOnLaunchCheckbox.Checked; + } + + private void DevBuildsCheckbox_CheckedChanged(object sender, EventArgs e) + { + coreConfig.DevBuilds = DevBuildsCheckbox.Checked; + UpdateAutoUpdate(); } private void RefreshOnStartupCheckbox_CheckedChanged(object sender, EventArgs e) { - Main.Instance.configuration.RefreshOnStartup = RefreshOnStartupCheckbox.Checked; + guiConfig.RefreshOnStartup = RefreshOnStartupCheckbox.Checked; } private void HideEpochsCheckbox_CheckedChanged(object sender, EventArgs e) { - Main.Instance.configuration.HideEpochs = HideEpochsCheckbox.Checked; + guiConfig.HideEpochs = HideEpochsCheckbox.Checked; } private void HideVCheckbox_CheckedChanged(object sender, EventArgs e) { - Main.Instance.configuration.HideV = HideVCheckbox.Checked; + guiConfig.HideV = HideVCheckbox.Checked; } private void LanguageSelectionComboBox_MouseWheel(object sender, MouseEventArgs e) @@ -615,29 +643,29 @@ private void LanguageSelectionComboBox_MouseWheel(object sender, MouseEventArgs private void LanguageSelectionComboBox_SelectionChanged(object sender, EventArgs e) { - config.Language = LanguageSelectionComboBox.SelectedItem.ToString(); + coreConfig.Language = LanguageSelectionComboBox.SelectedItem.ToString(); } private void AutoSortUpdateCheckBox_CheckedChanged(object sender, EventArgs e) { - Main.Instance.configuration.AutoSortByUpdate = AutoSortUpdateCheckBox.Checked; + guiConfig.AutoSortByUpdate = AutoSortUpdateCheckBox.Checked; } private void EnableTrayIconCheckBox_CheckedChanged(object sender, EventArgs e) { - MinimizeToTrayCheckBox.Enabled = Main.Instance.configuration.EnableTrayIcon = EnableTrayIconCheckBox.Checked; + MinimizeToTrayCheckBox.Enabled = guiConfig.EnableTrayIcon = EnableTrayIconCheckBox.Checked; Main.Instance.CheckTrayState(); } private void MinimizeToTrayCheckBox_CheckedChanged(object sender, EventArgs e) { - Main.Instance.configuration.MinimizeToTray = MinimizeToTrayCheckBox.Checked; + guiConfig.MinimizeToTray = MinimizeToTrayCheckBox.Checked; Main.Instance.CheckTrayState(); } private void RefreshTextBox_TextChanged(object sender, EventArgs e) { - config.RefreshRate = string.IsNullOrEmpty(RefreshTextBox.Text) ? 0 : int.Parse(RefreshTextBox.Text); + coreConfig.RefreshRate = string.IsNullOrEmpty(RefreshTextBox.Text) ? 0 : int.Parse(RefreshTextBox.Text); UpdateRefreshRate(); } @@ -651,9 +679,9 @@ private void RefreshTextBox_KeyPress(object sender, KeyPressEventArgs e) private void PauseRefreshCheckBox_CheckedChanged(object sender, EventArgs e) { - Main.Instance.configuration.RefreshPaused = PauseRefreshCheckBox.Checked; + guiConfig.RefreshPaused = PauseRefreshCheckBox.Checked; - if (Main.Instance.configuration.RefreshPaused) + if (guiConfig.RefreshPaused) { Main.Instance.refreshTimer.Stop(); } diff --git a/GUI/Dialogs/SettingsDialog.resx b/GUI/Dialogs/SettingsDialog.resx index e57622fd9b..72e7c02d00 100644 --- a/GUI/Dialogs/SettingsDialog.resx +++ b/GUI/Dialogs/SettingsDialog.resx @@ -144,6 +144,7 @@ Latest version: ??? Check for CKAN updates on launch + Use dev builds Check for updates Install update Behaviour diff --git a/GUI/Dialogs/YesNoDialog.Designer.cs b/GUI/Dialogs/YesNoDialog.Designer.cs index 190f0470cc..6040b0d53e 100644 --- a/GUI/Dialogs/YesNoDialog.Designer.cs +++ b/GUI/Dialogs/YesNoDialog.Designer.cs @@ -31,11 +31,13 @@ private void InitializeComponent() this.components = new System.ComponentModel.Container(); System.ComponentModel.ComponentResourceManager resources = new SingleAssemblyComponentResourceManager(typeof(YesNoDialog)); this.panel1 = new System.Windows.Forms.Panel(); - this.DescriptionLabel = new TransparentTextBox(); + this.DescriptionLabel = new CKAN.GUI.TransparentTextBox(); + this.BottomButtonPanel = new CKAN.GUI.LeftRightRowPanel(); this.SuppressCheckbox = new System.Windows.Forms.CheckBox(); this.YesButton = new System.Windows.Forms.Button(); this.NoButton = new System.Windows.Forms.Button(); this.panel1.SuspendLayout(); + this.BottomButtonPanel.SuspendLayout(); this.SuspendLayout(); // // panel1 @@ -64,6 +66,14 @@ private void InitializeComponent() this.DescriptionLabel.BorderStyle = System.Windows.Forms.BorderStyle.None; resources.ApplyResources(this.DescriptionLabel, "DescriptionLabel"); // + // BottomButtonPanel + // + this.BottomButtonPanel.LeftControls.Add(this.SuppressCheckbox); + this.BottomButtonPanel.RightControls.Add(this.NoButton); + this.BottomButtonPanel.RightControls.Add(this.YesButton); + this.BottomButtonPanel.Dock = System.Windows.Forms.DockStyle.Bottom; + this.BottomButtonPanel.Name = "BottomButtonPanel"; + // // SuppressCheckbox // this.SuppressCheckbox.AutoSize = false; @@ -80,6 +90,8 @@ private void InitializeComponent() // this.YesButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)); + this.YesButton.AutoSize = true; + this.YesButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.YesButton.DialogResult = System.Windows.Forms.DialogResult.Yes; this.YesButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.YesButton.Location = new System.Drawing.Point(250, 92); @@ -93,6 +105,8 @@ private void InitializeComponent() // this.NoButton.Anchor = ((System.Windows.Forms.AnchorStyles)(System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)); + this.NoButton.AutoSize = true; + this.NoButton.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowOnly; this.NoButton.DialogResult = System.Windows.Forms.DialogResult.No; this.NoButton.FlatStyle = System.Windows.Forms.FlatStyle.Flat; this.NoButton.Location = new System.Drawing.Point(331, 92); @@ -107,9 +121,7 @@ private void InitializeComponent() this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(418, 127); - this.Controls.Add(this.SuppressCheckbox); - this.Controls.Add(this.NoButton); - this.Controls.Add(this.YesButton); + this.Controls.Add(this.BottomButtonPanel); this.Controls.Add(this.panel1); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.Icon = EmbeddedImages.AppIcon; @@ -117,14 +129,17 @@ private void InitializeComponent() this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; resources.ApplyResources(this, "$this"); this.panel1.ResumeLayout(false); - this.ResumeLayout(false); + this.BottomButtonPanel.ResumeLayout(false); + this.BottomButtonPanel.PerformLayout(); + this.ResumeLayout(false); this.PerformLayout(); } #endregion private System.Windows.Forms.Panel panel1; - private TransparentTextBox DescriptionLabel; + private CKAN.GUI.TransparentTextBox DescriptionLabel; + private CKAN.GUI.LeftRightRowPanel BottomButtonPanel; private System.Windows.Forms.CheckBox SuppressCheckbox; private System.Windows.Forms.Button YesButton; private System.Windows.Forms.Button NoButton; diff --git a/GUI/Labels/ModuleLabelList.cs b/GUI/Labels/ModuleLabelList.cs index 1275f90c7b..279e6cac6c 100644 --- a/GUI/Labels/ModuleLabelList.cs +++ b/GUI/Labels/ModuleLabelList.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using System.Collections.Generic; using System.IO; @@ -12,7 +13,7 @@ namespace CKAN.GUI public class ModuleLabelList { [JsonProperty("labels", NullValueHandling = NullValueHandling.Ignore)] - public ModuleLabel[] Labels = new ModuleLabel[] {}; + public ModuleLabel[] Labels = Array.Empty(); public IEnumerable LabelsFor(string instanceName) => Labels.Where(l => l.AppliesTo(instanceName)); diff --git a/GUI/Main/Main.cs b/GUI/Main/Main.cs index f64e5c2aad..9b0a6eac31 100644 --- a/GUI/Main/Main.cs +++ b/GUI/Main/Main.cs @@ -18,6 +18,7 @@ using CKAN.Extensions; using CKAN.Versioning; using CKAN.GUI.Attributes; +using CKAN.Configuration; // Don't warn if we use our own obsolete properties #pragma warning disable 0618 @@ -36,6 +37,7 @@ public partial class Main : Form, IMessageFilter public readonly GameInstanceManager Manager; public GameInstance CurrentInstance => Manager.CurrentInstance; private readonly RepositoryDataManager repoData; + private readonly AutoUpdate updater = new AutoUpdate(); // Stuff we set when the game instance changes public GUIConfiguration configuration; @@ -78,7 +80,7 @@ public Main(string[] cmdlineArgs, GameInstanceManager mgr) } } - Configuration.IConfiguration mainConfig = ServiceLocator.Container.Resolve(); + var mainConfig = ServiceLocator.Container.Resolve(); // If the language is not set yet in the config, try to save the current language. // If it isn't supported, it'll still be null afterwards. Doesn't matter, .NET handles the resource selection. @@ -292,14 +294,13 @@ protected override void OnShown(EventArgs e) { HideWaitDialog(); CheckTrayState(); - bool autoUpdating = CheckForCKANUpdate(); Console.CancelKeyPress += (sender2, evt2) => { // Hide tray icon on Ctrl-C minimizeNotifyIcon.Visible = false; }; InitRefreshTimer(); - CurrentInstanceUpdated(!autoUpdating); + CurrentInstanceUpdated(); } else { @@ -334,7 +335,7 @@ private void manageGameInstancesMenuItem_Click(object sender, EventArgs e) try { ManageMods.ModGrid.ClearSelection(); - CurrentInstanceUpdated(true); + CurrentInstanceUpdated(); done = true; } catch (RegistryInUseKraken kraken) @@ -351,7 +352,7 @@ private void manageGameInstancesMenuItem_Click(object sender, EventArgs e) { // Couldn't get the lock, revert to previous instance Manager.CurrentInstance = old_instance; - CurrentInstanceUpdated(false); + CurrentInstanceUpdated(); done = true; } } @@ -375,7 +376,7 @@ private void UpdateStatusBar() /// React to switching to a new game instance /// /// true if a repo update is allowed if needed (e.g. on initial load), false otherwise - private void CurrentInstanceUpdated(bool allowRepoUpdate) + private void CurrentInstanceUpdated() { // This will throw RegistryInUseKraken if locked by another process var regMgr = RegistryManager.Instance(CurrentInstance, repoData); @@ -407,16 +408,11 @@ private void CurrentInstanceUpdated(bool allowRepoUpdate) configuration?.Save(); configuration = GUIConfigForInstance(CurrentInstance); - if (!configuration.CheckForUpdatesOnLaunchNoNag && AutoUpdate.CanUpdate) - { - log.Debug("Asking user if they wish for auto-updates"); - if (new AskUserForAutoUpdatesDialog().ShowDialog(this) == DialogResult.OK) - { - configuration.CheckForUpdatesOnLaunch = true; - } + AutoUpdatePrompts(ServiceLocator.Container + .Resolve(), + configuration); - configuration.CheckForUpdatesOnLaunchNoNag = true; - } + bool autoUpdating = CheckForCKANUpdate(); var pluginsPath = Path.Combine(CurrentInstance.CkanDir(), "Plugins"); if (!Directory.Exists(pluginsPath)) @@ -429,7 +425,7 @@ private void CurrentInstanceUpdated(bool allowRepoUpdate) CurrentInstance.game.RebuildSubdirectories(CurrentInstance.GameDir()); bool repoUpdateNeeded = configuration.RefreshOnStartup; - if (allowRepoUpdate) + if (!autoUpdating) { // If not allowing, don't do anything if (repoUpdateNeeded) @@ -625,7 +621,11 @@ private void CKANSettingsToolStripMenuItem_Click(object sender, EventArgs e) { // Flipping enabled here hides the main form itself. Enabled = false; - var dialog = new SettingsDialog(RegistryManager.Instance(CurrentInstance, repoData), currentUser); + var dialog = new SettingsDialog(ServiceLocator.Container.Resolve(), + configuration, + RegistryManager.Instance(CurrentInstance, repoData), + updater, + currentUser); dialog.ShowDialog(this); Enabled = true; if (dialog.RepositoryAdded) @@ -649,7 +649,7 @@ private void preferredHostsToolStripMenuItem_Click(object sender, EventArgs e) { Enabled = false; var dlg = new PreferredHostsDialog( - ServiceLocator.Container.Resolve(), + ServiceLocator.Container.Resolve(), RegistryManager.Instance(CurrentInstance, repoData).registry); dlg.ShowDialog(this); Enabled = true; @@ -658,7 +658,7 @@ private void preferredHostsToolStripMenuItem_Click(object sender, EventArgs e) private void installFiltersToolStripMenuItem_Click(object sender, EventArgs e) { Enabled = false; - var dlg = new InstallFiltersDialog(ServiceLocator.Container.Resolve(), CurrentInstance); + var dlg = new InstallFiltersDialog(ServiceLocator.Container.Resolve(), CurrentInstance); dlg.ShowDialog(this); Enabled = true; } diff --git a/GUI/Main/MainAutoUpdate.cs b/GUI/Main/MainAutoUpdate.cs index 64f6a8984d..d3089d3b28 100644 --- a/GUI/Main/MainAutoUpdate.cs +++ b/GUI/Main/MainAutoUpdate.cs @@ -2,6 +2,9 @@ using System.ComponentModel; using System.Windows.Forms; +using Autofac; + +using CKAN.Configuration; using CKAN.Versioning; // Don't warn if we use our own obsolete properties @@ -11,6 +14,28 @@ namespace CKAN.GUI { public partial class Main { + private void AutoUpdatePrompts(IConfiguration coreConfig, + GUIConfiguration guiConfig) + { + if (!guiConfig.CheckForUpdatesOnLaunchNoNag && AutoUpdate.CanUpdate) + { + log.Debug("Asking user if they wish for auto-updates"); + if (new AskUserForAutoUpdatesDialog().ShowDialog(this) == DialogResult.OK) + { + guiConfig.CheckForUpdatesOnLaunch = true; + } + guiConfig.CheckForUpdatesOnLaunchNoNag = true; + } + + if (!guiConfig.DevBuildsNoNag && guiConfig.CheckForUpdatesOnLaunch) + { + coreConfig.DevBuilds = !YesNoDialog(Properties.Resources.MainReleasesOrDevBuildsPrompt, + Properties.Resources.MainReleasesOrDevBuildsYes, + Properties.Resources.MainReleasesOrDevBuildsNo); + guiConfig.DevBuildsNoNag = true; + } + } + /// /// Look for a CKAN update and start installing it if found. /// Note that this will happen on a background thread! @@ -25,15 +50,16 @@ private bool CheckForCKANUpdate() try { log.Info("Making auto-update call"); - AutoUpdate.Instance.FetchLatestReleaseInfo(); - var latest_version = AutoUpdate.Instance.latestUpdate.Version; - var current_version = new ModuleVersion(Meta.GetVersion()); + var mainConfig = ServiceLocator.Container.Resolve(); + var update = updater.GetUpdate(mainConfig.DevBuilds); + var latestVersion = update.Version; + var currentVersion = new ModuleVersion(Meta.GetVersion()); - if (AutoUpdate.Instance.IsFetched() && latest_version.IsGreaterThan(current_version)) + if (latestVersion.IsGreaterThan(currentVersion)) { log.Debug("Found higher ckan version"); - var release_notes = AutoUpdate.Instance.latestUpdate.ReleaseNotes; - var dialog = new NewUpdateDialog(latest_version.ToString(), release_notes); + var releaseNotes = update.ReleaseNotes; + var dialog = new NewUpdateDialog(latestVersion.ToString(), releaseNotes); if (dialog.ShowDialog(this) == DialogResult.OK) { UpdateCKAN(); @@ -43,7 +69,8 @@ private bool CheckForCKANUpdate() } catch (Exception exception) { - currentUser.RaiseError(Properties.Resources.MainAutoUpdateFailed, exception.Message); + currentUser.RaiseError(Properties.Resources.MainAutoUpdateFailed, + exception.Message); log.Error("Error in auto-update", exception); } } @@ -59,15 +86,16 @@ public void UpdateCKAN() ShowWaitDialog(); DisableMainWindow(); tabController.RenameTab("WaitTabPage", Properties.Resources.MainUpgradingWaitTitle); - Wait.SetDescription(string.Format(Properties.Resources.MainUpgradingTo, AutoUpdate.Instance.latestUpdate.Version)); + var mainConfig = ServiceLocator.Container.Resolve(); + var update = updater.GetUpdate(mainConfig.DevBuilds); + Wait.SetDescription(string.Format(Properties.Resources.MainUpgradingTo, + update.Version)); log.Info("Start ckan update"); - Wait.StartWaiting( - (sender, args) => AutoUpdate.Instance.StartUpdateProcess(true, currentUser), - UpdateReady, - false, - null - ); + Wait.StartWaiting((sender, args) => updater.StartUpdateProcess(true, mainConfig.DevBuilds, currentUser), + UpdateReady, + false, + null); } private void UpdateReady(object sender, RunWorkerCompletedEventArgs e) diff --git a/GUI/Main/MainInstall.cs b/GUI/Main/MainInstall.cs index 796fa35d89..84c5e2feeb 100644 --- a/GUI/Main/MainInstall.cs +++ b/GUI/Main/MainInstall.cs @@ -4,14 +4,15 @@ using System.ComponentModel; using System.Linq; using System.Transactions; +#if NET5_0_OR_GREATER +using System.Runtime.Versioning; +#endif using Autofac; using CKAN.Extensions; using CKAN.GUI.Attributes; -#if NET5_0_OR_GREATER -using System.Runtime.Versioning; -#endif +using CKAN.Configuration; // Don't warn if we use our own obsolete properties #pragma warning disable 0618 @@ -429,7 +430,10 @@ private void PostInstallMods(object sender, RunWorkerCompletedEventArgs e) } // Now pretend they clicked the menu option for the settings Enabled = false; - new SettingsDialog(RegistryManager.Instance(CurrentInstance, repoData), + new SettingsDialog(ServiceLocator.Container.Resolve(), + configuration, + RegistryManager.Instance(CurrentInstance, repoData), + updater, currentUser) .ShowDialog(this); Enabled = true; diff --git a/GUI/Main/MainTrayIcon.cs b/GUI/Main/MainTrayIcon.cs index bfe2871b85..a97cbab168 100644 --- a/GUI/Main/MainTrayIcon.cs +++ b/GUI/Main/MainTrayIcon.cs @@ -117,7 +117,12 @@ private void openCKANToolStripMenuItem_Click(object sender, EventArgs e) private void cKANSettingsToolStripMenuItem1_Click(object sender, EventArgs e) { OpenWindow(); - new SettingsDialog(RegistryManager.Instance(CurrentInstance, repoData), currentUser).ShowDialog(this); + new SettingsDialog(ServiceLocator.Container.Resolve(), + configuration, + RegistryManager.Instance(CurrentInstance, repoData), + updater, + currentUser) + .ShowDialog(this); } private void minimizedContextMenuStrip_Opening(object sender, CancelEventArgs e) diff --git a/GUI/Model/GUIConfiguration.cs b/GUI/Model/GUIConfiguration.cs index 1fd31c4233..394024d570 100644 --- a/GUI/Model/GUIConfiguration.cs +++ b/GUI/Model/GUIConfiguration.cs @@ -18,6 +18,7 @@ public class GUIConfiguration public bool CheckForUpdatesOnLaunch = false; public bool CheckForUpdatesOnLaunchNoNag = false; + public bool DevBuildsNoNag = false; public bool EnableTrayIcon = false; public bool MinimizeToTray = false; diff --git a/GUI/Properties/Resources.resx b/GUI/Properties/Resources.resx index 8881e2e2a7..e8fde203ac 100644 --- a/GUI/Properties/Resources.resx +++ b/GUI/Properties/Resources.resx @@ -144,6 +144,13 @@ Try to move {2} out of {3} and restart CKAN. Unknown Method can not be called unless IsCKAN {0} (using mod version {1}) + CKAN is configured to check for updates at launch. In addition to the regular stable releases, dev builds are also available. Using dev builds will give you early access to new features and bugfixes, but with a higher chance of bugs. Please consider using the dev builds to help us catch and fix bugs before they make it into a release. + +Which build would you like to use? + +(This setting can be changed later in the CKAN settings.) + Use stable releases only + Use dev builds Error in auto-update: {0} There are conflicts. Really quit? {0} Quit diff --git a/GUI/URLHandlers.cs b/GUI/URLHandlers.cs index e1bc0896c2..698c8408ca 100644 --- a/GUI/URLHandlers.cs +++ b/GUI/URLHandlers.cs @@ -73,14 +73,13 @@ public static void RegisterURLHandler(GUIConfiguration config, IUser user) { // trigger a UAC prompt (if UAC is enabled) Verb = "runas", + // .NET ignores Verb without this + UseShellExecute = true, Arguments = $"gui --asroot {UrlRegistrationArgument}" }); } - else - { - config.URLHandlerNoNag = true; - config.Save(); - } + config.URLHandlerNoNag = true; + config.Save(); // Don't re-throw the exception because we just dealt with it } } diff --git a/Netkan/Model/Metadata.cs b/Netkan/Model/Metadata.cs index b66e516b57..42d2f2ccde 100644 --- a/Netkan/Model/Metadata.cs +++ b/Netkan/Model/Metadata.cs @@ -180,7 +180,7 @@ public string[] Licenses case JTokenType.String: return new string[] { (string)lic }; } - return new string[] { }; + return Array.Empty(); } } diff --git a/Tests/Core/Configuration/FakeConfiguration.cs b/Tests/Core/Configuration/FakeConfiguration.cs index aab6748509..eb00c5fbc5 100644 --- a/Tests/Core/Configuration/FakeConfiguration.cs +++ b/Tests/Core/Configuration/FakeConfiguration.cs @@ -147,9 +147,11 @@ public string Language } } - public string[] GlobalInstallFilters { get; set; } = new string[] { }; + public string[] GlobalInstallFilters { get; set; } = Array.Empty(); - public string[] PreferredHosts { get; set; } = new string[] { }; + public string[] PreferredHosts { get; set; } = Array.Empty(); + + public bool DevBuilds { get; set; } public void Dispose() { diff --git a/Tests/Core/ModuleInstallerTests.cs b/Tests/Core/ModuleInstallerTests.cs index 9d2b875c7d..62baffd60b 100644 --- a/Tests/Core/ModuleInstallerTests.cs +++ b/Tests/Core/ModuleInstallerTests.cs @@ -680,7 +680,7 @@ public void GroupFilesByRemovable_WithFiles_CorrectOutput(string relRoot, // Act ModuleInstaller.GroupFilesByRemovable(relRoot, registry, - new string[] {}, + Array.Empty(), game, relPaths, out string[] removable, diff --git a/Tests/Core/Net/AutoUpdateTests.cs b/Tests/Core/Net/AutoUpdateTests.cs index faf3e00c2c..a6588358af 100644 --- a/Tests/Core/Net/AutoUpdateTests.cs +++ b/Tests/Core/Net/AutoUpdateTests.cs @@ -1,7 +1,11 @@ +using System.IO; using System.Net; + using NUnit.Framework; +using Newtonsoft.Json; + using CKAN; -using CKAN.Versioning; +using Tests.Data; namespace Tests.Core.AutoUpdateTests { @@ -9,23 +13,24 @@ namespace Tests.Core.AutoUpdateTests public class AutoUpdateTests { [Test] + [TestCase(true)] + [TestCase(false)] [Category("Online")] // This could fail if run during a release, so it's marked as Flaky. [Category("FlakyNetwork")] - public void FetchLatestReleaseInfo() + public void GetUpdate_DevBuildOrStable_Works(bool devBuild) { // Force-allow TLS 1.2 for HTTPS URLs, because GitHub requires it. // This is on by default in .NET 4.6, but not in 4.5. ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - var updater = AutoUpdate.Instance; + var updater = new AutoUpdate(); + var update = updater.GetUpdate(devBuild); // Is is a *really* basic test to just make sure we get release info // if we ask for it. - updater.FetchLatestReleaseInfo(); - Assert.IsNotNull(updater.latestUpdate.ReleaseNotes); - Assert.IsNotNull(updater.latestUpdate.Version); - Assert.IsTrue(updater.IsFetched()); + Assert.IsNotNull(update.ReleaseNotes); + Assert.IsNotNull(update.Version); } [Test] @@ -34,15 +39,13 @@ public void FetchLatestReleaseInfo() [TestCase("aaa\r\n---\r\nbbb\r\n---\r\nccc", "bbb\r\n---\r\nccc", "Multi release notes markers")] public void ExtractReleaseNotes(string body, string expected, string comment) { - Assert.AreEqual( - expected, - CkanUpdate.ExtractReleaseNotes(body), - comment - ); + Assert.AreEqual(expected, + GitHubReleaseCkanUpdate.ExtractReleaseNotes(body), + comment); } [Test] - public void CkanUpdate_NormalUpdate_ParsedCorrectly() + public void GitHubReleaseCkanUpdate_NormalUpdate_ParsedCorrectly() { // Arrange const string releaseJSON = @"{ @@ -65,15 +68,39 @@ public void CkanUpdate_NormalUpdate_ParsedCorrectly() }"; // Act - CkanUpdate cu = new CkanUpdate(releaseJSON); + var relInfo = JsonConvert.DeserializeObject(releaseJSON); + var upd = new GitHubReleaseCkanUpdate(relInfo); // Assert - Assert.AreEqual(new CkanModuleVersion("v1.25.0", "Wallops"), cu.Version); - Assert.AreEqual("https://github.com/KSP-CKAN/CKAN/releases/download/v1.25.0/ckan.exe", cu.ReleaseDownload.ToString()); - Assert.AreEqual(6651392, cu.ReleaseSize); - Assert.AreEqual("https://github.com/KSP-CKAN/CKAN/releases/download/v1.25.0/AutoUpdater.exe", cu.UpdaterDownload.ToString()); - Assert.AreEqual(414208, cu.UpdaterSize); - Assert.AreEqual("Greatest release notes of all time", cu.ReleaseNotes); + Assert.AreEqual("v1.25.0", relInfo.tag_name); + Assert.AreEqual("Wallops", relInfo.name); + Assert.AreEqual("https://github.com/KSP-CKAN/CKAN/releases/download/v1.25.0/ckan.exe", + upd.ReleaseDownload.ToString()); + Assert.AreEqual(6651392, upd.ReleaseSize); + Assert.AreEqual("https://github.com/KSP-CKAN/CKAN/releases/download/v1.25.0/AutoUpdater.exe", + upd.UpdaterDownload.ToString()); + Assert.AreEqual(414208, upd.UpdaterSize); + Assert.AreEqual("Greatest release notes of all time", upd.ReleaseNotes); } + + [Test] + public void S3BuildCkanUpdate_Constructor_ParsedCorrectly() + { + // Arrange / Act + var upd = new S3BuildCkanUpdate( + JsonConvert.DeserializeObject( + File.ReadAllText(TestData.DataDir("version.json")))); + + // Assert + Assert.AreEqual("v1.34.5.24015 aka dev", + upd.Version.ToString()); + Assert.AreEqual("### Internal\n\n- [Policy] Fix #3518 rewrite de-indexing policy (#3993 by: JonnyOThan; reviewed: HebaruSan)", + upd.ReleaseNotes); + Assert.AreEqual("https://ksp-ckan.s3-us-west-2.amazonaws.com/ckan.exe", + upd.ReleaseDownload.ToString()); + Assert.AreEqual("https://ksp-ckan.s3-us-west-2.amazonaws.com/AutoUpdater.exe", + upd.UpdaterDownload.ToString()); + } + } } diff --git a/Tests/Core/Net/NetAsyncDownloaderTests.cs b/Tests/Core/Net/NetAsyncDownloaderTests.cs index 9e1f8795b3..fab3e2d76a 100644 --- a/Tests/Core/Net/NetAsyncDownloaderTests.cs +++ b/Tests/Core/Net/NetAsyncDownloaderTests.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Collections.Generic; using System.Linq; using NUnit.Framework; @@ -25,9 +24,9 @@ public void DownloadAndWait_WithValidFileUrl_SetsTargetSize(string pathWithinTes // Arrange var downloader = new NetAsyncDownloader(new NullUser()); var fromPath = TestData.DataDir(pathWithinTestData); - var target = new CKAN.Net.DownloadTarget(new List { new Uri(fromPath) }, + var target = new NetAsyncDownloader.DownloadTarget(new Uri(fromPath), Path.GetTempFileName()); - var targets = new CKAN.Net.DownloadTarget[] { target }; + var targets = new NetAsyncDownloader.DownloadTarget[] { target }; var origSize = new FileInfo(fromPath).Length; // Act @@ -71,7 +70,7 @@ public void DownloadAndWait_WithValidFileUrls_SetsTargetsSize(params string[] pa // Arrange var downloader = new NetAsyncDownloader(new NullUser()); var fromPaths = pathsWithinTestData.Select(p => TestData.DataDir(p)).ToArray(); - var targets = fromPaths.Select(p => new CKAN.Net.DownloadTarget(new List { new Uri(p) }, + var targets = fromPaths.Select(p => new NetAsyncDownloader.DownloadTarget(new Uri(p), Path.GetTempFileName())) .ToArray(); var origSizes = fromPaths.Select(p => new FileInfo(p).Length).ToArray(); @@ -187,7 +186,7 @@ public void DownloadAndWait_WithSomeInvalidUrls_ThrowsDownloadErrorsKraken( // Arrange var downloader = new NetAsyncDownloader(new NullUser()); var fromPaths = pathsWithinTestData.Select(p => Path.GetFullPath(TestData.DataDir(p))).ToArray(); - var targets = fromPaths.Select(p => new CKAN.Net.DownloadTarget(new List { new Uri(p) }, + var targets = fromPaths.Select(p => new NetAsyncDownloader.DownloadTarget(new Uri(p), Path.GetTempFileName())) .ToArray(); var badIndices = fromPaths.Select((p, i) => new Tuple(i, File.Exists(p))) diff --git a/Tests/Core/Relationships/RelationshipResolver.cs b/Tests/Core/Relationships/RelationshipResolver.cs index 6770f1076e..fedab95380 100644 --- a/Tests/Core/Relationships/RelationshipResolver.cs +++ b/Tests/Core/Relationships/RelationshipResolver.cs @@ -312,7 +312,7 @@ public void ModList_WithInstalledModules_ContainsThemWithReasonInstalled() var registry = new CKAN.Registry(repoData.Manager, repo.repo); var list = new List { mod_a }; - registry.RegisterModule(mod_a, new string[] { }, null, false); + registry.RegisterModule(mod_a, Array.Empty(), null, false); var relationship_resolver = new RelationshipResolver( list, null, options, registry, null); diff --git a/Tests/Data/version.json b/Tests/Data/version.json new file mode 100644 index 0000000000..23892ee999 --- /dev/null +++ b/Tests/Data/version.json @@ -0,0 +1,4 @@ +{ + "version": "v1.34.5.24015", + "changelog": "### Internal\n\n- [Policy] Fix #3518 rewrite de-indexing policy (#3993 by: JonnyOThan; reviewed: HebaruSan)" +} diff --git a/Tests/GUI/Model/ModList.cs b/Tests/GUI/Model/ModList.cs index 3c17213adc..800f926bb0 100644 --- a/Tests/GUI/Model/ModList.cs +++ b/Tests/GUI/Model/ModList.cs @@ -161,7 +161,7 @@ public void InstallAndSortByCompat_WithAnyCompat_NoCrash() // Install module and set it as pre-installed manager.Cache.Store(TestData.DogeCoinFlag_101_module(), TestData.DogeCoinFlagZip(), new Progress(bytes => {})); - registry.RegisterModule(anyVersionModule, new string[] { }, instance.KSP, false); + registry.RegisterModule(anyVersionModule, Array.Empty(), instance.KSP, false); HashSet possibleConfigOnlyDirs = null; installer.InstallList( diff --git a/bin/version_info.py b/bin/version_info.py new file mode 100644 index 0000000000..25316f9d13 --- /dev/null +++ b/bin/version_info.py @@ -0,0 +1,32 @@ +""" +Generate a JSON file containing the version and the latest changelog +""" + +from pathlib import Path +import re +import json +from git import Repo + +# Get the year and day of year from the latest commit +repo = Repo('.', search_parent_directories=True) +yyddd = repo.head.commit.committed_datetime.strftime(r'%g%j') +version = '' +changes = '' + +with open(Path(repo.working_dir) / 'CHANGELOG.md', + 'rt', encoding='utf-8') as changelog: + header_pattern = re.compile(r'^\s*\#\#\s+(v[0-9.]+)') + # First header contains the current version + for line in changelog: + match = header_pattern.match(line) + if match: + version = match.group(1) + break + # Second header marks the end of the current changelog + for line in changelog: + if header_pattern.match(line): + break + changes += line + +print(json.dumps({'version': f'{version}.{yyddd}', + 'changelog': changes.strip('\n')}, indent=4)) diff --git a/build.cake b/build.cake index 1c349f709f..33ea649416 100644 --- a/build.cake +++ b/build.cake @@ -1,6 +1,7 @@ #addin "nuget:?package=Cake.SemVer&version=4.0.0" #addin "nuget:?package=semver&version=2.3.0" #addin "nuget:?package=Cake.Docker&version=0.11.1" +#addin nuget:?package=Cake.Git&version=3.0.0 #tool "nuget:?package=ILRepack&version=2.0.18" #tool "nuget:?package=NUnit.ConsoleRunner&version=3.16.3" @@ -258,18 +259,20 @@ Task("Generate-GlobalAssemblyVersionInfo") .Description("Intermediate - Calculate the version strings for the assembly.") .Does(() => { - var version = GetVersion(); - var versionStr2 = string.Format("{0}.{1}", version.Major, version.Minor); - var versionStr3 = string.Format("{0}.{1}.{2}", version.Major, version.Minor, version.Patch); - var metaDirectory = buildDirectory.Combine("meta"); - CreateDirectory(metaDirectory); + var version = GetVersion(); + CreateAssemblyInfo(metaDirectory.CombineWithFilePath("GlobalAssemblyVersionInfo.cs"), new AssemblyInfoSettings { - Version = versionStr2, - FileVersion = versionStr3, + Version = string.Format("{0}.{1}", version.Major, version.Minor), + FileVersion = string.IsNullOrEmpty(version.Metadata) + ? string.Format("{0}.{1}.{2}", + version.Major, version.Minor, version.Patch) + : string.Format("{0}.{1}.{2}.{3}", + version.Major, version.Minor, version.Patch, + version.Metadata), InformationalVersion = version.ToString() }); }); @@ -531,39 +534,17 @@ private Semver.SemVersion GetVersion() if (DirectoryExists(rootDirectory.Combine(".git"))) { - var hash = GetGitCommitHash(); - - version = CreateSemVer( - version.Major, - version.Minor, - version.Patch, - version.Prerelease, - hash == null ? null : hash.Substring(0, 12) - ); + var commitDate = GitLogTip(rootDirectory).Committer.When; + version = CreateSemVer(version.Major, + version.Minor, + version.Patch, + version.Prerelease, + commitDate.ToString("yy") + commitDate.DayOfYear.ToString("000")); } return version; } -private string GetGitCommitHash() -{ - IEnumerable output; - try - { - var exitCode = StartProcess( - "git", - new ProcessSettings { Arguments = "rev-parse HEAD", RedirectStandardOutput = true }, - out output - ); - - return exitCode == 0 ? output.FirstOrDefault() : null; - } - catch(Exception) - { - return null; - } -} - private IEnumerable RunExecutable(FilePath executable, string arguments) { IEnumerable output;