From 30f4f33eb3e93a45c6f2ff0bff7e31e9af03f124 Mon Sep 17 00:00:00 2001 From: Jarl Gullberg Date: Wed, 22 Jul 2020 19:07:02 +0200 Subject: [PATCH] Replace tabs with spaces. --- Launchpad.Common/Enums/EManifestType.cs | 36 +- Launchpad.Common/Enums/ESystemTarget.cs | 16 +- Launchpad.Common/ExtensionMethods.cs | 78 +- Launchpad.Common/Handlers/MD5Handler.cs | 40 +- .../Handlers/Manifest/ManifestEntry.cs | 292 +-- .../Handlers/Manifest/ManifestHandler.cs | 412 ++--- Launchpad.Common/PlatformHelpers.cs | 70 +- .../Configuration/ILaunchpadConfiguration.cs | 114 +- .../Extensions/ManifestEntryExtensions.cs | 62 +- Launchpad.Launcher/Handlers/ChecksHandler.cs | 222 +-- Launchpad.Launcher/Handlers/ConfigHandler.cs | 84 +- Launchpad.Launcher/Handlers/GameHandler.cs | 530 +++--- .../Handlers/LauncherHandler.cs | 416 ++--- .../Handlers/Protocols/EModule.cs | 28 +- .../Protocols/Manifest/FTPProtocolHandler.cs | 742 ++++---- .../Protocols/Manifest/HTTPProtocolHandler.cs | 654 +++---- .../Manifest/ManifestBasedProtocolHandler.cs | 1562 ++++++++--------- .../Protocols/ModuleProgressChangedArgs.cs | 76 +- .../Protocols/PatchProtocolHandler.cs | 424 ++--- Launchpad.Launcher/Interface/MainWindow.UI.cs | 122 +- Launchpad.Launcher/Interface/MainWindow.cs | 1276 +++++++------- Launchpad.Launcher/Program.cs | 240 +-- .../Services/GameArgumentService.cs | 94 +- .../Services/LocalVersionService.cs | 80 +- Launchpad.Launcher/Services/TagfileService.cs | 56 +- .../Utility/DirectoryHelpers.cs | 224 +-- .../Utility/Enums/ELauncherMode.cs | 52 +- .../Utility/PatchProtocolProvider.cs | 64 +- Launchpad.Launcher/Utility/ResourceManager.cs | 28 +- Launchpad.Tests/Common/MD5HandlerTests.cs | 34 +- .../Common/StringExtensionsTexts.cs | 74 +- .../Handlers/ManifestGenerationHandler.cs | 214 +-- .../Interface/MainWindow.UI.cs | 74 +- Launchpad.Utilities/Interface/MainWindow.cs | 228 +-- Launchpad.Utilities/Options/CLIOptions.cs | 54 +- Launchpad.Utilities/Program.cs | 130 +- .../Utility/DirectoryHelpers.cs | 30 +- ...ifestGenerationProgressChangedEventArgs.cs | 56 +- Scripts/launchpad-publish.sh | 68 +- Scripts/update-translations.sh | 28 +- 40 files changed, 4542 insertions(+), 4542 deletions(-) diff --git a/Launchpad.Common/Enums/EManifestType.cs b/Launchpad.Common/Enums/EManifestType.cs index a33556b3..1c8e7f35 100644 --- a/Launchpad.Common/Enums/EManifestType.cs +++ b/Launchpad.Common/Enums/EManifestType.cs @@ -22,24 +22,24 @@ namespace Launchpad.Common.Enums { - /// - /// Enum defining the type of manifest. - /// - public enum EManifestType - { - /// - /// An unknown manifest. - /// - Unknown = 0, + /// + /// Enum defining the type of manifest. + /// + public enum EManifestType + { + /// + /// An unknown manifest. + /// + Unknown = 0, - /// - /// A launcher manifest. - /// - Launchpad = 1, + /// + /// A launcher manifest. + /// + Launchpad = 1, - /// - /// A game manifest. - /// - Game = 2 - } + /// + /// A game manifest. + /// + Game = 2 + } } diff --git a/Launchpad.Common/Enums/ESystemTarget.cs b/Launchpad.Common/Enums/ESystemTarget.cs index c0725d32..24de3808 100644 --- a/Launchpad.Common/Enums/ESystemTarget.cs +++ b/Launchpad.Common/Enums/ESystemTarget.cs @@ -24,12 +24,12 @@ namespace Launchpad.Common.Enums { - public enum ESystemTarget - { - Linux, - Mac, - Win64, - Win32, - Unknown - } + public enum ESystemTarget + { + Linux, + Mac, + Win64, + Win32, + Unknown + } } diff --git a/Launchpad.Common/ExtensionMethods.cs b/Launchpad.Common/ExtensionMethods.cs index 97d15f2b..43c9abaa 100644 --- a/Launchpad.Common/ExtensionMethods.cs +++ b/Launchpad.Common/ExtensionMethods.cs @@ -25,45 +25,45 @@ namespace Launchpad.Common { - /// - /// Various extension methods. - /// - public static class ExtensionMethods - { - /// - /// Sanitizes the input string, removing any \n, \r, or \0 characters. - /// - /// Input string. - /// The string, without the illegal characters. - public static string RemoveLineSeparatorsAndNulls(this string input) - { - return input?.Replace("\n", string.Empty).Replace("\0", string.Empty).Replace("\r", string.Empty); - } + /// + /// Various extension methods. + /// + public static class ExtensionMethods + { + /// + /// Sanitizes the input string, removing any \n, \r, or \0 characters. + /// + /// Input string. + /// The string, without the illegal characters. + public static string RemoveLineSeparatorsAndNulls(this string input) + { + return input?.Replace("\n", string.Empty).Replace("\0", string.Empty).Replace("\r", string.Empty); + } - /// - /// Adds a new value to an existing IDictionary, or if the dictionary already contains a value for the given key, - /// updates the existing key with the new value. - /// - /// The dictionary to update. - /// The key of the provided value. - /// The value to add or update. - /// The type of the key. - /// The type of the value. - public static void AddOrUpdate(this IDictionary dictionary, TKey key, TValue value) - { - if (dictionary == null) - { - throw new ArgumentNullException(nameof(dictionary)); - } + /// + /// Adds a new value to an existing IDictionary, or if the dictionary already contains a value for the given key, + /// updates the existing key with the new value. + /// + /// The dictionary to update. + /// The key of the provided value. + /// The value to add or update. + /// The type of the key. + /// The type of the value. + public static void AddOrUpdate(this IDictionary dictionary, TKey key, TValue value) + { + if (dictionary == null) + { + throw new ArgumentNullException(nameof(dictionary)); + } - if (dictionary.ContainsKey(key)) - { - dictionary[key] = value; - } - else - { - dictionary.Add(key, value); - } - } - } + if (dictionary.ContainsKey(key)) + { + dictionary[key] = value; + } + else + { + dictionary.Add(key, value); + } + } + } } diff --git a/Launchpad.Common/Handlers/MD5Handler.cs b/Launchpad.Common/Handlers/MD5Handler.cs index 2ba49fb0..bef540a8 100644 --- a/Launchpad.Common/Handlers/MD5Handler.cs +++ b/Launchpad.Common/Handlers/MD5Handler.cs @@ -26,25 +26,25 @@ namespace Launchpad.Common.Handlers { - /// - /// MD5 hashing handler. Used to ensure file integrity. - /// - public static class MD5Handler - { - /// - /// Gets the file hash from a data stream. - /// - /// The hash. - /// File stream. - public static string GetStreamHash(Stream dataStream) - { - using (var md5 = MD5.Create()) - { - // Calculate the hash of the stream. - var resultString = BitConverter.ToString(md5.ComputeHash(dataStream)).Replace("-", string.Empty); + /// + /// MD5 hashing handler. Used to ensure file integrity. + /// + public static class MD5Handler + { + /// + /// Gets the file hash from a data stream. + /// + /// The hash. + /// File stream. + public static string GetStreamHash(Stream dataStream) + { + using (var md5 = MD5.Create()) + { + // Calculate the hash of the stream. + var resultString = BitConverter.ToString(md5.ComputeHash(dataStream)).Replace("-", string.Empty); - return resultString; - } - } - } + return resultString; + } + } + } } diff --git a/Launchpad.Common/Handlers/Manifest/ManifestEntry.cs b/Launchpad.Common/Handlers/Manifest/ManifestEntry.cs index 139eee87..ddc4c75e 100644 --- a/Launchpad.Common/Handlers/Manifest/ManifestEntry.cs +++ b/Launchpad.Common/Handlers/Manifest/ManifestEntry.cs @@ -24,150 +24,150 @@ namespace Launchpad.Common.Handlers.Manifest { - /// - /// A manifest entry derived from the raw unformatted string. - /// Contains the relative path of the referenced file, as well as - /// its MD5 hash and size in bytes. - /// - public sealed class ManifestEntry : IEquatable - { - /// - /// Gets the path of the file, relative to the game directory. - /// - public string RelativePath { get; } - - /// - /// Gets the MD5 hash of the file. - /// - public string Hash { get; } - - /// - /// Gets the size in bytes of the file. - /// - public long Size { get; } - - /// - /// Initializes a new instance of the class. - /// - /// The relative path to the file. - /// The hash of the file. - /// The size in bytes of the file. - public ManifestEntry(string relativePath, string hash, long size) - { - this.RelativePath = relativePath; - this.Hash = hash; - this.Size = size; - } - - /// - /// Attempts to parse an entry from a raw input. - /// The input is expected to be in [path]:[hash]:[size] format. Note that the file path is case sensitive, - /// but the hash is not. - /// - /// true, if the input was successfully parse, false otherwise. - /// Raw input. - /// The resulting entry. - public static bool TryParse(string rawInput, out ManifestEntry inEntry) - { - inEntry = null; - - if (string.IsNullOrEmpty(rawInput)) - { - return false; - } - - var cleanInput = rawInput.RemoveLineSeparatorsAndNulls(); - - // Split the string into its three components - file, hash and size - var entryElements = cleanInput.Split(':'); - - // If we have three elements (which we should always have), set them in the provided entry - if (entryElements.Length != 3) - { - return false; - } - - // Sanitize the manifest path, converting \ to / on unix and / to \ on Windows. - var relativePath = entryElements[0]; - if (PlatformHelpers.IsRunningOnUnix()) - { - relativePath = relativePath.Replace('\\', '/').TrimStart('/'); - } - else - { - relativePath = relativePath.Replace('/', '\\').TrimStart('\\'); - } - - // Hashes must be exactly 32 characters - if (entryElements[1].Length != 32) - { - return false; - } - - // Set the hash to the second element - var hash = entryElements[1]; - - // Attempt to parse the final element as a long-type byte count. - if (!long.TryParse(entryElements[2], out var parsedSize)) - { - // Oops. The parsing failed, so this entry is invalid. - return false; - } - - // Negative sizes are not allowed - if (parsedSize < 0) - { - return false; - } - - var size = parsedSize; - - inEntry = new ManifestEntry(relativePath, hash, size); - return true; - } - - /// - /// Returns a that represents the current . - /// The returned value matches a raw in-manifest representation of the entry, in the form of - /// [path]:[hash]:[size]. - /// - /// A that represents the current . - public override string ToString() - { - return $"{this.RelativePath}:{this.Hash}:{this.Size}"; - } - - /// - /// Determines whether the specified is equal to the current . - /// - /// The to compare with the current . - /// true if the specified is equal to the current - /// ; otherwise, false. - public override bool Equals(object obj) - { - return Equals(obj as ManifestEntry); - } - - /// - public bool Equals(ManifestEntry other) - { - if (other == null) - { - return false; - } - - return this.RelativePath == other.RelativePath && - string.Equals(this.Hash, other.Hash, StringComparison.InvariantCultureIgnoreCase) && - this.Size == other.Size; - } - - /// - /// Serves as a hash function for a object. - /// - /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. - public override int GetHashCode() - { - return ToString().GetHashCode(); - } - } + /// + /// A manifest entry derived from the raw unformatted string. + /// Contains the relative path of the referenced file, as well as + /// its MD5 hash and size in bytes. + /// + public sealed class ManifestEntry : IEquatable + { + /// + /// Gets the path of the file, relative to the game directory. + /// + public string RelativePath { get; } + + /// + /// Gets the MD5 hash of the file. + /// + public string Hash { get; } + + /// + /// Gets the size in bytes of the file. + /// + public long Size { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The relative path to the file. + /// The hash of the file. + /// The size in bytes of the file. + public ManifestEntry(string relativePath, string hash, long size) + { + this.RelativePath = relativePath; + this.Hash = hash; + this.Size = size; + } + + /// + /// Attempts to parse an entry from a raw input. + /// The input is expected to be in [path]:[hash]:[size] format. Note that the file path is case sensitive, + /// but the hash is not. + /// + /// true, if the input was successfully parse, false otherwise. + /// Raw input. + /// The resulting entry. + public static bool TryParse(string rawInput, out ManifestEntry inEntry) + { + inEntry = null; + + if (string.IsNullOrEmpty(rawInput)) + { + return false; + } + + var cleanInput = rawInput.RemoveLineSeparatorsAndNulls(); + + // Split the string into its three components - file, hash and size + var entryElements = cleanInput.Split(':'); + + // If we have three elements (which we should always have), set them in the provided entry + if (entryElements.Length != 3) + { + return false; + } + + // Sanitize the manifest path, converting \ to / on unix and / to \ on Windows. + var relativePath = entryElements[0]; + if (PlatformHelpers.IsRunningOnUnix()) + { + relativePath = relativePath.Replace('\\', '/').TrimStart('/'); + } + else + { + relativePath = relativePath.Replace('/', '\\').TrimStart('\\'); + } + + // Hashes must be exactly 32 characters + if (entryElements[1].Length != 32) + { + return false; + } + + // Set the hash to the second element + var hash = entryElements[1]; + + // Attempt to parse the final element as a long-type byte count. + if (!long.TryParse(entryElements[2], out var parsedSize)) + { + // Oops. The parsing failed, so this entry is invalid. + return false; + } + + // Negative sizes are not allowed + if (parsedSize < 0) + { + return false; + } + + var size = parsedSize; + + inEntry = new ManifestEntry(relativePath, hash, size); + return true; + } + + /// + /// Returns a that represents the current . + /// The returned value matches a raw in-manifest representation of the entry, in the form of + /// [path]:[hash]:[size]. + /// + /// A that represents the current . + public override string ToString() + { + return $"{this.RelativePath}:{this.Hash}:{this.Size}"; + } + + /// + /// Determines whether the specified is equal to the current . + /// + /// The to compare with the current . + /// true if the specified is equal to the current + /// ; otherwise, false. + public override bool Equals(object obj) + { + return Equals(obj as ManifestEntry); + } + + /// + public bool Equals(ManifestEntry other) + { + if (other == null) + { + return false; + } + + return this.RelativePath == other.RelativePath && + string.Equals(this.Hash, other.Hash, StringComparison.InvariantCultureIgnoreCase) && + this.Size == other.Size; + } + + /// + /// Serves as a hash function for a object. + /// + /// A hash code for this instance that is suitable for use in hashing algorithms and data structures such as a hash table. + public override int GetHashCode() + { + return ToString().GetHashCode(); + } + } } diff --git a/Launchpad.Common/Handlers/Manifest/ManifestHandler.cs b/Launchpad.Common/Handlers/Manifest/ManifestHandler.cs index 629dac7b..b43bbceb 100644 --- a/Launchpad.Common/Handlers/Manifest/ManifestHandler.cs +++ b/Launchpad.Common/Handlers/Manifest/ManifestHandler.cs @@ -28,210 +28,210 @@ namespace Launchpad.Common.Handlers.Manifest { - /// - /// Handler class for the game manifest. - /// - public sealed class ManifestHandler - { - private readonly object ManifestsLock = new object(); - - /// - /// The local base directory of the running assembly. Used to produce relative paths for manifest-related - /// files and folders. - /// - private readonly string LocalBaseDirectory; - - /// - /// The remote where the manifest files are expected to be. - /// - private readonly Uri RemoteURL; - - /// - /// The target system for which the handler should retrieve files. - /// - private readonly ESystemTarget SystemTarget; - - private readonly Dictionary> Manifests = new Dictionary>(); - private readonly Dictionary> OldManifests = new Dictionary>(); - - /// - /// Initializes a new instance of the class. - /// This constructor also serves to updated outdated file paths for the manifests. - /// - /// The local base directory of the launcher installation. - /// The remote where the manifest files are expected to be. - /// The target system for which the handler should retrieve files. - public ManifestHandler(string localBaseDirectory, Uri remoteURL, ESystemTarget systemTarget) - { - this.LocalBaseDirectory = localBaseDirectory; - this.RemoteURL = remoteURL; - this.SystemTarget = systemTarget; - } - - /// - /// Gets the specified manifest currently held by the launcher. The return value of this method may be null if no - /// manifest could be retrieved. - /// - /// The type of manifest to retrieve, that is, the manifest for a specific component. - /// Whether or not the old manifest or the new manifest should be retrieved. - /// A list of objects. - /// Thrown if the is not a known value. - public IReadOnlyList GetManifest(EManifestType manifestType, bool getOldManifest) - { - switch (manifestType) - { - case EManifestType.Game: - case EManifestType.Launchpad: - { - lock (this.ManifestsLock) - { - if (getOldManifest) - { - if (this.OldManifests.ContainsKey(manifestType)) - { - return this.OldManifests[manifestType]; - } - } - else - { - if (this.Manifests.ContainsKey(manifestType)) - { - return this.Manifests[manifestType]; - } - } - } - - return null; - } - default: - { - throw new ArgumentOutOfRangeException(nameof(manifestType), "An unknown manifest type was requested."); - } - } - } - - /// - /// Reloads all manifests of the specifed type from disk. - /// - /// The type of manifest to reload. - public void ReloadManifests(EManifestType manifestType) - { - lock (this.ManifestsLock) - { - var newManifestPath = GetManifestPath(manifestType, false); - var oldManifestPath = GetManifestPath(manifestType, true); - - // Reload new manifests - if (!File.Exists(newManifestPath)) - { - this.Manifests.AddOrUpdate(manifestType, null); - } - else - { - this.Manifests.AddOrUpdate(manifestType, LoadManifest(newManifestPath)); - } - - // Reload old manifests - if (!File.Exists(oldManifestPath)) - { - this.OldManifests.AddOrUpdate(manifestType, null); - } - else - { - this.OldManifests.AddOrUpdate(manifestType, LoadManifest(oldManifestPath)); - } - } - } - - /// - /// Loads a manifest from a file on disk. - /// - /// The path to a manifest file. - /// A list of objects. - public static IReadOnlyList LoadManifest(string manifestPath) - { - using (var fileStream = File.OpenRead(manifestPath)) - { - return LoadManifest(fileStream); - } - } - - /// - /// Loads a manifest from a . - /// - /// A stream containing a manifest. - /// A read-only list of objects. - public static IReadOnlyList LoadManifest(Stream manifestStream) - { - var rawManifest = new List(); - using (var sr = new StreamReader(manifestStream)) - { - string line; - while ((line = sr.ReadLine()) != null) - { - rawManifest.Add(line); - } - } - - var manifest = new List(); - foreach (var rawEntry in rawManifest) - { - if (ManifestEntry.TryParse(rawEntry, out var newEntry)) - { - manifest.Add(newEntry); - } - } - - return manifest; - } - - /// - /// Gets the specified manifest's path on disk. The presence of the manifest is not guaranteed at - /// this point. - /// - /// The type of manifest to get the path to. - /// Whether or not the path should specify an old manifest. - /// A fully qualified path to where a manifest should be. - public string GetManifestPath(EManifestType manifestType, bool getOldManifestPath) - { - var manifestPath = Path.Combine(this.LocalBaseDirectory, $"{manifestType}Manifest.txt"); - - if (getOldManifestPath) - { - manifestPath += ".old"; - } - - return manifestPath; - } - - /// - /// Gets the manifest URL for the specified manifest type. - /// - /// The type of manifest to get the URL of. - /// The game manifest URL. - public string GetManifestURL(EManifestType manifestType) - { - if (manifestType == EManifestType.Launchpad) - { - return $"{this.RemoteURL}/launcher/{manifestType}Manifest.txt"; - } - - return $"{this.RemoteURL}/game/{this.SystemTarget}/{manifestType}Manifest.txt"; - } - - /// - /// Gets the manifest URL for the specified manifest type. - /// - /// The type of manifest to get the URL of. - /// The game manifest URL. - public string GetManifestChecksumURL(EManifestType manifestType) - { - if (manifestType == EManifestType.Launchpad) - { - return $"{this.RemoteURL.LocalPath}/launcher/{manifestType}Manifest.checksum"; - } - - return $"{this.RemoteURL.LocalPath}/game/{this.SystemTarget}/{manifestType}Manifest.checksum"; - } - } + /// + /// Handler class for the game manifest. + /// + public sealed class ManifestHandler + { + private readonly object ManifestsLock = new object(); + + /// + /// The local base directory of the running assembly. Used to produce relative paths for manifest-related + /// files and folders. + /// + private readonly string LocalBaseDirectory; + + /// + /// The remote where the manifest files are expected to be. + /// + private readonly Uri RemoteURL; + + /// + /// The target system for which the handler should retrieve files. + /// + private readonly ESystemTarget SystemTarget; + + private readonly Dictionary> Manifests = new Dictionary>(); + private readonly Dictionary> OldManifests = new Dictionary>(); + + /// + /// Initializes a new instance of the class. + /// This constructor also serves to updated outdated file paths for the manifests. + /// + /// The local base directory of the launcher installation. + /// The remote where the manifest files are expected to be. + /// The target system for which the handler should retrieve files. + public ManifestHandler(string localBaseDirectory, Uri remoteURL, ESystemTarget systemTarget) + { + this.LocalBaseDirectory = localBaseDirectory; + this.RemoteURL = remoteURL; + this.SystemTarget = systemTarget; + } + + /// + /// Gets the specified manifest currently held by the launcher. The return value of this method may be null if no + /// manifest could be retrieved. + /// + /// The type of manifest to retrieve, that is, the manifest for a specific component. + /// Whether or not the old manifest or the new manifest should be retrieved. + /// A list of objects. + /// Thrown if the is not a known value. + public IReadOnlyList GetManifest(EManifestType manifestType, bool getOldManifest) + { + switch (manifestType) + { + case EManifestType.Game: + case EManifestType.Launchpad: + { + lock (this.ManifestsLock) + { + if (getOldManifest) + { + if (this.OldManifests.ContainsKey(manifestType)) + { + return this.OldManifests[manifestType]; + } + } + else + { + if (this.Manifests.ContainsKey(manifestType)) + { + return this.Manifests[manifestType]; + } + } + } + + return null; + } + default: + { + throw new ArgumentOutOfRangeException(nameof(manifestType), "An unknown manifest type was requested."); + } + } + } + + /// + /// Reloads all manifests of the specifed type from disk. + /// + /// The type of manifest to reload. + public void ReloadManifests(EManifestType manifestType) + { + lock (this.ManifestsLock) + { + var newManifestPath = GetManifestPath(manifestType, false); + var oldManifestPath = GetManifestPath(manifestType, true); + + // Reload new manifests + if (!File.Exists(newManifestPath)) + { + this.Manifests.AddOrUpdate(manifestType, null); + } + else + { + this.Manifests.AddOrUpdate(manifestType, LoadManifest(newManifestPath)); + } + + // Reload old manifests + if (!File.Exists(oldManifestPath)) + { + this.OldManifests.AddOrUpdate(manifestType, null); + } + else + { + this.OldManifests.AddOrUpdate(manifestType, LoadManifest(oldManifestPath)); + } + } + } + + /// + /// Loads a manifest from a file on disk. + /// + /// The path to a manifest file. + /// A list of objects. + public static IReadOnlyList LoadManifest(string manifestPath) + { + using (var fileStream = File.OpenRead(manifestPath)) + { + return LoadManifest(fileStream); + } + } + + /// + /// Loads a manifest from a . + /// + /// A stream containing a manifest. + /// A read-only list of objects. + public static IReadOnlyList LoadManifest(Stream manifestStream) + { + var rawManifest = new List(); + using (var sr = new StreamReader(manifestStream)) + { + string line; + while ((line = sr.ReadLine()) != null) + { + rawManifest.Add(line); + } + } + + var manifest = new List(); + foreach (var rawEntry in rawManifest) + { + if (ManifestEntry.TryParse(rawEntry, out var newEntry)) + { + manifest.Add(newEntry); + } + } + + return manifest; + } + + /// + /// Gets the specified manifest's path on disk. The presence of the manifest is not guaranteed at + /// this point. + /// + /// The type of manifest to get the path to. + /// Whether or not the path should specify an old manifest. + /// A fully qualified path to where a manifest should be. + public string GetManifestPath(EManifestType manifestType, bool getOldManifestPath) + { + var manifestPath = Path.Combine(this.LocalBaseDirectory, $"{manifestType}Manifest.txt"); + + if (getOldManifestPath) + { + manifestPath += ".old"; + } + + return manifestPath; + } + + /// + /// Gets the manifest URL for the specified manifest type. + /// + /// The type of manifest to get the URL of. + /// The game manifest URL. + public string GetManifestURL(EManifestType manifestType) + { + if (manifestType == EManifestType.Launchpad) + { + return $"{this.RemoteURL}/launcher/{manifestType}Manifest.txt"; + } + + return $"{this.RemoteURL}/game/{this.SystemTarget}/{manifestType}Manifest.txt"; + } + + /// + /// Gets the manifest URL for the specified manifest type. + /// + /// The type of manifest to get the URL of. + /// The game manifest URL. + public string GetManifestChecksumURL(EManifestType manifestType) + { + if (manifestType == EManifestType.Launchpad) + { + return $"{this.RemoteURL.LocalPath}/launcher/{manifestType}Manifest.checksum"; + } + + return $"{this.RemoteURL.LocalPath}/game/{this.SystemTarget}/{manifestType}Manifest.checksum"; + } + } } diff --git a/Launchpad.Common/PlatformHelpers.cs b/Launchpad.Common/PlatformHelpers.cs index b9d52702..6c13126f 100644 --- a/Launchpad.Common/PlatformHelpers.cs +++ b/Launchpad.Common/PlatformHelpers.cs @@ -26,43 +26,43 @@ namespace Launchpad.Common { - /// - /// Helper methods for determining the current platform. - /// - public static class PlatformHelpers - { - /// - /// Determines whether this instance is running on Unix. - /// - /// true if this instance is running on unix; otherwise, false. - public static bool IsRunningOnUnix() - { - var currentPlatform = GetCurrentPlatform(); - return currentPlatform == ESystemTarget.Linux || currentPlatform == ESystemTarget.Mac; - } + /// + /// Helper methods for determining the current platform. + /// + public static class PlatformHelpers + { + /// + /// Determines whether this instance is running on Unix. + /// + /// true if this instance is running on unix; otherwise, false. + public static bool IsRunningOnUnix() + { + var currentPlatform = GetCurrentPlatform(); + return currentPlatform == ESystemTarget.Linux || currentPlatform == ESystemTarget.Mac; + } - /// - /// Gets the current platform the launcher is running on. - /// - /// The current platform. - public static ESystemTarget GetCurrentPlatform() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) - { - return ESystemTarget.Linux; - } + /// + /// Gets the current platform the launcher is running on. + /// + /// The current platform. + public static ESystemTarget GetCurrentPlatform() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return ESystemTarget.Linux; + } - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return ESystemTarget.Mac; - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return ESystemTarget.Mac; + } - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return Environment.Is64BitOperatingSystem ? ESystemTarget.Win64 : ESystemTarget.Win32; - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Environment.Is64BitOperatingSystem ? ESystemTarget.Win64 : ESystemTarget.Win32; + } - throw new PlatformNotSupportedException(); - } - } + throw new PlatformNotSupportedException(); + } + } } diff --git a/Launchpad.Launcher/Configuration/ILaunchpadConfiguration.cs b/Launchpad.Launcher/Configuration/ILaunchpadConfiguration.cs index 4de5b0fd..fc5bf247 100644 --- a/Launchpad.Launcher/Configuration/ILaunchpadConfiguration.cs +++ b/Launchpad.Launcher/Configuration/ILaunchpadConfiguration.cs @@ -26,72 +26,72 @@ namespace Launchpad.Launcher.Configuration { - /// - /// Configuration file interface. - /// - public interface ILaunchpadConfiguration - { - // Launcher section - // ... + /// + /// Configuration file interface. + /// + public interface ILaunchpadConfiguration + { + // Launcher section + // ... - /// - /// Gets or sets the address where the changelog is hosted. - /// - [Option(Alias = "Launcher.ChangelogAddress", DefaultValue = "http://www.example.com/launchpad/changelog/changelog.html")] - Uri ChangelogAddress { get; set; } + /// + /// Gets or sets the address where the changelog is hosted. + /// + [Option(Alias = "Launcher.ChangelogAddress", DefaultValue = "http://www.example.com/launchpad/changelog/changelog.html")] + Uri ChangelogAddress { get; set; } - /// - /// Gets or sets the system target of the launcher. - /// - [Option(Alias = "Launcher.SystemTarget", DefaultValue = "Linux")] - ESystemTarget SystemTarget { get; set; } + /// + /// Gets or sets the system target of the launcher. + /// + [Option(Alias = "Launcher.SystemTarget", DefaultValue = "Linux")] + ESystemTarget SystemTarget { get; set; } - // Game section - // ... + // Game section + // ... - /// - /// Gets or sets the name of the game. - /// - [Option(Alias = "Game.Name", DefaultValue = "LaunchpadExample")] - string GameName { get; set; } + /// + /// Gets or sets the name of the game. + /// + [Option(Alias = "Game.Name", DefaultValue = "LaunchpadExample")] + string GameName { get; set; } - /// - /// Gets or sets the path to the game's executable, relative to the launcher. - /// - [Option(Alias = "Game.ExecutablePath", DefaultValue = "LaunchpadExample/Binaries/Linux/LaunchpadExample")] - string ExecutablePath { get; set; } + /// + /// Gets or sets the path to the game's executable, relative to the launcher. + /// + [Option(Alias = "Game.ExecutablePath", DefaultValue = "LaunchpadExample/Binaries/Linux/LaunchpadExample")] + string ExecutablePath { get; set; } - // Remote section - // ... + // Remote section + // ... - /// - /// Gets or sets the address of the remote server. - /// - [Option(Alias = "Remote.Address", DefaultValue = "ftp://ftp.example.com")] - Uri RemoteAddress { get; set; } + /// + /// Gets or sets the address of the remote server. + /// + [Option(Alias = "Remote.Address", DefaultValue = "ftp://ftp.example.com")] + Uri RemoteAddress { get; set; } - /// - /// Gets or sets the username to use when authenticating with the remote server. - /// - [Option(Alias = "Remote.Username", DefaultValue = "anonymous")] - string RemoteUsername { get; set; } + /// + /// Gets or sets the username to use when authenticating with the remote server. + /// + [Option(Alias = "Remote.Username", DefaultValue = "anonymous")] + string RemoteUsername { get; set; } - /// - /// Gets or sets the password to use when authenticating with the remote server. - /// - [Option(Alias = "Remote.Password", DefaultValue = "anonymous")] - string RemotePassword { get; set; } + /// + /// Gets or sets the password to use when authenticating with the remote server. + /// + [Option(Alias = "Remote.Password", DefaultValue = "anonymous")] + string RemotePassword { get; set; } - /// - /// Gets or sets the number of times to retry file downloads. - /// - [Option(Alias = "Remote.FileDownloadRetries", DefaultValue = 2)] - int RemoteFileDownloadRetries { get; set; } + /// + /// Gets or sets the number of times to retry file downloads. + /// + [Option(Alias = "Remote.FileDownloadRetries", DefaultValue = 2)] + int RemoteFileDownloadRetries { get; set; } - /// - /// Gets or sets the buffer size to use when downloading files. - /// - [Option(Alias = "Remote.FileDownloadBufferSize", DefaultValue = 8192)] - int RemoteFileDownloadBufferSize { get; set; } - } + /// + /// Gets or sets the buffer size to use when downloading files. + /// + [Option(Alias = "Remote.FileDownloadBufferSize", DefaultValue = 8192)] + int RemoteFileDownloadBufferSize { get; set; } + } } diff --git a/Launchpad.Launcher/Extensions/ManifestEntryExtensions.cs b/Launchpad.Launcher/Extensions/ManifestEntryExtensions.cs index b997cad6..d07edc57 100644 --- a/Launchpad.Launcher/Extensions/ManifestEntryExtensions.cs +++ b/Launchpad.Launcher/Extensions/ManifestEntryExtensions.cs @@ -27,38 +27,38 @@ namespace Launchpad.Launcher { - /// - /// Extension methods for the class. - /// - public static class ManifestEntryExtensions - { - /// - /// Verifies the integrity of the file in the manifest entry. - /// - /// The manifest entry to test. - /// true, if file was complete and undamaged, false otherwise. - public static bool IsFileIntegrityIntact(this ManifestEntry entry) - { - var localPath = Path.Combine(DirectoryHelpers.GetLocalGameDirectory(), entry.RelativePath); - if (!File.Exists(localPath)) - { - return false; - } + /// + /// Extension methods for the class. + /// + public static class ManifestEntryExtensions + { + /// + /// Verifies the integrity of the file in the manifest entry. + /// + /// The manifest entry to test. + /// true, if file was complete and undamaged, false otherwise. + public static bool IsFileIntegrityIntact(this ManifestEntry entry) + { + var localPath = Path.Combine(DirectoryHelpers.GetLocalGameDirectory(), entry.RelativePath); + if (!File.Exists(localPath)) + { + return false; + } - var fileInfo = new FileInfo(localPath); - if (fileInfo.Length != entry.Size) - { - return false; - } + var fileInfo = new FileInfo(localPath); + if (fileInfo.Length != entry.Size) + { + return false; + } - using Stream file = File.OpenRead(localPath); - var localHash = MD5Handler.GetStreamHash(file); - if (localHash != entry.Hash) - { - return false; - } + using Stream file = File.OpenRead(localPath); + var localHash = MD5Handler.GetStreamHash(file); + if (localHash != entry.Hash) + { + return false; + } - return true; - } - } + return true; + } + } } diff --git a/Launchpad.Launcher/Handlers/ChecksHandler.cs b/Launchpad.Launcher/Handlers/ChecksHandler.cs index 3a052a8c..08ec20e7 100644 --- a/Launchpad.Launcher/Handlers/ChecksHandler.cs +++ b/Launchpad.Launcher/Handlers/ChecksHandler.cs @@ -30,115 +30,115 @@ namespace Launchpad.Launcher.Handlers { - /// - /// This class handles all the launcher's checks, returning bools for each function. - /// - internal sealed class ChecksHandler - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - private readonly PatchProtocolHandler Patch; - - /// - /// Initializes a new instance of the class. - /// - public ChecksHandler() - { - this.Patch = PatchProtocolProvider.GetHandler(); - } - - /// - /// Determines whether this instance can connect to a patching service. - /// - /// true if this instance can connect to a patching service; otherwise, false. - public bool CanPatch() - { - return this.Patch != null && this.Patch.CanPatch(); - } - - /// - /// Determines whether this is the first time the launcher starts. - /// - /// true if this is the first time; otherwise, false. - public static bool IsInitialStartup() - { - // We use an empty file to determine if this is the first launch or not - return !File.Exists(DirectoryHelpers.GetLauncherTagfilePath()); - } - - /// - /// Determines whether the game is installed. - /// - /// true if the game is installed; otherwise, false. - public bool IsGameInstalled() - { - // Criteria for considering the game 'installed' - var hasGameDirectory = Directory.Exists(DirectoryHelpers.GetLocalGameDirectory()); - var hasInstallCookie = File.Exists(DirectoryHelpers.GetGameTagfilePath()); - var hasGameVersionFile = File.Exists(DirectoryHelpers.GetLocalGameVersionPath()); - - if (!hasGameVersionFile && hasGameDirectory) - { - Log.Warn - ( - "No GameVersion.txt file was found in the installation directory.\n" + - "This may be due to a download error, or the developer may not have included one.\n" + - "Without it, the game cannot be considered fully installed.\n" + - "If you are the developer of this game, add one to your game files with your desired version in it." - ); - } - - // If any of these criteria are false, the game is not considered fully installed. - return hasGameDirectory && hasInstallCookie && IsInstallCookieEmpty() && hasGameVersionFile; - } - - /// - /// Determines whether the game is outdated. - /// - /// true if the game is outdated; otherwise, false. - public bool IsGameOutdated() - { - return this.Patch.IsModuleOutdated(EModule.Game); - } - - /// - /// Determines whether the launcher is outdated. - /// - /// true if the launcher is outdated; otherwise, false. - public bool IsLauncherOutdated() - { - return this.Patch.IsModuleOutdated(EModule.Launcher); - } - - /// - /// Determines whether the install cookie is empty. - /// - /// true if the install cookie is empty, otherwise, false. - private static bool IsInstallCookieEmpty() - { - // Is there an .install file in the directory? - var hasInstallCookie = File.Exists(DirectoryHelpers.GetGameTagfilePath()); - var isInstallCookieEmpty = false; - - if (hasInstallCookie) - { - isInstallCookieEmpty = string.IsNullOrEmpty(File.ReadAllText(DirectoryHelpers.GetGameTagfilePath())); - } - - return isInstallCookieEmpty; - } - - /// - /// Checks whether or not the server provides binaries and patches for the specified platform. - /// - /// true, if the server does provide files for the platform, false otherwise. - /// platform. - public bool IsPlatformAvailable(ESystemTarget platform) - { - return this.Patch.IsPlatformAvailable(platform); - } - } + /// + /// This class handles all the launcher's checks, returning bools for each function. + /// + internal sealed class ChecksHandler + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + private readonly PatchProtocolHandler Patch; + + /// + /// Initializes a new instance of the class. + /// + public ChecksHandler() + { + this.Patch = PatchProtocolProvider.GetHandler(); + } + + /// + /// Determines whether this instance can connect to a patching service. + /// + /// true if this instance can connect to a patching service; otherwise, false. + public bool CanPatch() + { + return this.Patch != null && this.Patch.CanPatch(); + } + + /// + /// Determines whether this is the first time the launcher starts. + /// + /// true if this is the first time; otherwise, false. + public static bool IsInitialStartup() + { + // We use an empty file to determine if this is the first launch or not + return !File.Exists(DirectoryHelpers.GetLauncherTagfilePath()); + } + + /// + /// Determines whether the game is installed. + /// + /// true if the game is installed; otherwise, false. + public bool IsGameInstalled() + { + // Criteria for considering the game 'installed' + var hasGameDirectory = Directory.Exists(DirectoryHelpers.GetLocalGameDirectory()); + var hasInstallCookie = File.Exists(DirectoryHelpers.GetGameTagfilePath()); + var hasGameVersionFile = File.Exists(DirectoryHelpers.GetLocalGameVersionPath()); + + if (!hasGameVersionFile && hasGameDirectory) + { + Log.Warn + ( + "No GameVersion.txt file was found in the installation directory.\n" + + "This may be due to a download error, or the developer may not have included one.\n" + + "Without it, the game cannot be considered fully installed.\n" + + "If you are the developer of this game, add one to your game files with your desired version in it." + ); + } + + // If any of these criteria are false, the game is not considered fully installed. + return hasGameDirectory && hasInstallCookie && IsInstallCookieEmpty() && hasGameVersionFile; + } + + /// + /// Determines whether the game is outdated. + /// + /// true if the game is outdated; otherwise, false. + public bool IsGameOutdated() + { + return this.Patch.IsModuleOutdated(EModule.Game); + } + + /// + /// Determines whether the launcher is outdated. + /// + /// true if the launcher is outdated; otherwise, false. + public bool IsLauncherOutdated() + { + return this.Patch.IsModuleOutdated(EModule.Launcher); + } + + /// + /// Determines whether the install cookie is empty. + /// + /// true if the install cookie is empty, otherwise, false. + private static bool IsInstallCookieEmpty() + { + // Is there an .install file in the directory? + var hasInstallCookie = File.Exists(DirectoryHelpers.GetGameTagfilePath()); + var isInstallCookieEmpty = false; + + if (hasInstallCookie) + { + isInstallCookieEmpty = string.IsNullOrEmpty(File.ReadAllText(DirectoryHelpers.GetGameTagfilePath())); + } + + return isInstallCookieEmpty; + } + + /// + /// Checks whether or not the server provides binaries and patches for the specified platform. + /// + /// true, if the server does provide files for the platform, false otherwise. + /// platform. + public bool IsPlatformAvailable(ESystemTarget platform) + { + return this.Patch.IsPlatformAvailable(platform); + } + } } diff --git a/Launchpad.Launcher/Handlers/ConfigHandler.cs b/Launchpad.Launcher/Handlers/ConfigHandler.cs index 9c437f7a..91739af0 100644 --- a/Launchpad.Launcher/Handlers/ConfigHandler.cs +++ b/Launchpad.Launcher/Handlers/ConfigHandler.cs @@ -27,51 +27,51 @@ namespace Launchpad.Launcher.Handlers { - /// - /// Config handler. - /// This is a singleton class, and it should always be accessed through . - /// - public sealed class ConfigHandler - { - /// - /// The singleton Instance. Will always point to one shared object. - /// - public static readonly ConfigHandler Instance = new ConfigHandler(); + /// + /// Config handler. + /// This is a singleton class, and it should always be accessed through . + /// + public sealed class ConfigHandler + { + /// + /// The singleton Instance. Will always point to one shared object. + /// + public static readonly ConfigHandler Instance = new ConfigHandler(); - /// - /// Gets the configuration instance. - /// - public ILaunchpadConfiguration Configuration { get; } + /// + /// Gets the configuration instance. + /// + public ILaunchpadConfiguration Configuration { get; } - /// - /// Initializes a new instance of the class and initalizes it. - /// - private ConfigHandler() - { - this.Configuration = new ConfigurationBuilder() - .UseIniFile(DirectoryHelpers.GetConfigPath()) - .Build(); + /// + /// Initializes a new instance of the class and initalizes it. + /// + private ConfigHandler() + { + this.Configuration = new ConfigurationBuilder() + .UseIniFile(DirectoryHelpers.GetConfigPath()) + .Build(); - InitializeConfigurationFile(); - } + InitializeConfigurationFile(); + } - /// - /// Initializes the config by checking for bad values or files. - /// Run once when the launcher starts, then avoid unless absolutely neccesary. - /// - private void InitializeConfigurationFile() - { - if (File.Exists(DirectoryHelpers.GetConfigPath())) - { - return; - } + /// + /// Initializes the config by checking for bad values or files. + /// Run once when the launcher starts, then avoid unless absolutely neccesary. + /// + private void InitializeConfigurationFile() + { + if (File.Exists(DirectoryHelpers.GetConfigPath())) + { + return; + } - // Get the default values and write them back to the file, forcing it to be written to disk - foreach (var property in typeof(ILaunchpadConfiguration).GetProperties()) - { - var value = property.GetValue(this.Configuration); - property.SetValue(this.Configuration, value); - } - } - } + // Get the default values and write them back to the file, forcing it to be written to disk + foreach (var property in typeof(ILaunchpadConfiguration).GetProperties()) + { + var value = property.GetValue(this.Configuration); + property.SetValue(this.Configuration, value); + } + } + } } diff --git a/Launchpad.Launcher/Handlers/GameHandler.cs b/Launchpad.Launcher/Handlers/GameHandler.cs index 193e344b..58cd15e4 100644 --- a/Launchpad.Launcher/Handlers/GameHandler.cs +++ b/Launchpad.Launcher/Handlers/GameHandler.cs @@ -34,269 +34,269 @@ namespace Launchpad.Launcher.Handlers { - /// - /// This class has a lot of async stuff going on. It handles installing the game - /// and updating it when it needs to. - /// - /// The download protocol is selected based on the configuration each time this is - /// instantiated, and control is then handed over to whatever the protocol needs - /// to do. - /// - /// Since this class starts new threads in which it does the larger computations, - /// there must be no usage of UI code in this class. Keep it clean. - /// - internal sealed class GameHandler - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - /// - /// Event raised whenever the progress of installing or updating the game changes. - /// - public event EventHandler ProgressChanged; - - /// - /// Event raised whenever the game finishes downloading, regardless of whether or not it's updating - /// or installing. - /// - public event EventHandler DownloadFinished; - - /// - /// Event raised whenever the game fails to download, regardless of whether or not it's updating - /// or installing. - /// - public event EventHandler DownloadFailed; - - /// - /// Event raised whenever the game fails to launch. - /// - public event EventHandler LaunchFailed; - - /// - /// Event raised whenever the game exits. - /// - public event EventHandler GameExited; - - // ... - private static readonly ILaunchpadConfiguration Configuration = ConfigHandler.Instance.Configuration; - - private readonly PatchProtocolHandler Patch; - - private readonly GameArgumentService GameArgumentService = new GameArgumentService(); - - /// - /// Initializes a new instance of the class. - /// - public GameHandler() - { - this.Patch = PatchProtocolProvider.GetHandler(); - if (this.Patch == null) - { - return; - } - - this.Patch.ModuleDownloadProgressChanged += OnModuleInstallProgressChanged; - this.Patch.ModuleVerifyProgressChanged += OnModuleInstallProgressChanged; - this.Patch.ModuleUpdateProgressChanged += OnModuleInstallProgressChanged; - - this.Patch.ModuleInstallationFinished += OnModuleInstallationFinished; - this.Patch.ModuleInstallationFailed += OnModuleInstallationFailed; - } - - /// - /// Starts an asynchronous game installation task. - /// - public void InstallGame() - { - Log.Info($"Starting installation of game files using protocol \"{this.Patch.GetType().Name}\""); - var t = new Thread(this.Patch.InstallGame) - { - Name = "InstallGame", - IsBackground = true - }; - - t.Start(); - } - - /// - /// Starts an asynchronous game update task. - /// - public void UpdateGame() - { - Log.Info($"Starting update of game files using protocol \"{this.Patch.GetType().Name}\""); - var t = new Thread(() => this.Patch.UpdateModule(EModule.Game)) - { - Name = "UpdateGame", - IsBackground = true - }; - - t.Start(); - } - - /// - /// Starts an asynchronous game verification task. - /// - public void VerifyGame() - { - Log.Info("Beginning verification of game files."); - var t = new Thread(() => this.Patch.VerifyModule(EModule.Game)) - { - Name = "VerifyGame", - IsBackground = true - }; - - t.Start(); - } - - /// - /// Deletes all local data and installs the game again. - /// - public void ReinstallGame() - { - Log.Info("Beginning full reinstall of game files."); - if (Directory.Exists(DirectoryHelpers.GetLocalGameDirectory())) - { - Log.Info("Deleting existing game files."); - Directory.Delete(DirectoryHelpers.GetLocalGameDirectory(), true); - } - - if (File.Exists(DirectoryHelpers.GetGameTagfilePath())) - { - Log.Info("Deleting install progress cookie."); - File.Delete(DirectoryHelpers.GetGameTagfilePath()); - } - - var t = new Thread(() => this.Patch.InstallGame()) - { - Name = "ReinstallGame", - IsBackground = true - }; - - t.Start(); - } - - /// - /// Launches the game. - /// - public void LaunchGame() - { - try - { - var executable = Path.Combine(DirectoryHelpers.GetLocalGameDirectory(), Configuration.ExecutablePath); - if (!File.Exists(executable)) - { - throw new FileNotFoundException($"Game executable at path (\"{executable}\") not found."); - } - - var executableDir = Path.GetDirectoryName(executable) ?? DirectoryHelpers.GetLocalLauncherDirectory(); - - // Do not move the argument assignment inside the gameStartInfo initializer. - // It causes a TargetInvocationException crash through black magic. - var gameArguments = string.Join(" ", this.GameArgumentService.GetGameArguments()); - var gameStartInfo = new ProcessStartInfo - { - FileName = executable, - Arguments = gameArguments, - WorkingDirectory = executableDir - }; - - Log.Info($"Launching game. \n\tExecutable path: {gameStartInfo.FileName}"); - - var gameProcess = new Process - { - StartInfo = gameStartInfo, - EnableRaisingEvents = true - }; - - gameProcess.Exited += (sender, args) => - { - if (gameProcess.ExitCode != 0) - { - Log.Info - ( - $"The game exited with an exit code of {gameProcess.ExitCode}. " + - "There may have been issues during runtime, or the game may not have started at all." - ); - } - - OnGameExited(gameProcess.ExitCode); - - // Manual disposing - gameProcess.Dispose(); - }; - - // Make sure the game executable is flagged as such on Unix - if (PlatformHelpers.IsRunningOnUnix()) - { - Process.Start("chmod", $"+x {gameStartInfo.FileName}"); - } - - gameProcess.Start(); - } - catch (FileNotFoundException fex) - { - Log.Warn($"Game launch failed (FileNotFoundException): {fex.Message}"); - Log.Warn("If the game executable is there, try overriding the executable name in the configuration file."); - - OnGameLaunchFailed(); - } - catch (IOException ioex) - { - Log.Warn($"Game launch failed (IOException): {ioex.Message}"); - - OnGameLaunchFailed(); - } - } - - /// - /// Passes the internal event in the protocol handler to the outward-facing - /// event. - /// - /// Sender. - /// E. - private void OnModuleInstallProgressChanged(object sender, ModuleProgressChangedArgs e) - { - this.ProgressChanged?.Invoke(sender, e); - } - - /// - /// Passes the internal event in the protocol handler to the outward-facing - /// event. - /// - /// Sender. - /// E. - private void OnModuleInstallationFinished(object sender, EModule e) - { - this.DownloadFinished?.Invoke(sender, EventArgs.Empty); - } - - /// - /// Passes the internal event in the protocol handler to the outward-facing - /// event. - /// - /// Sender. - /// E. - private void OnModuleInstallationFailed(object sender, EModule e) - { - this.DownloadFailed?.Invoke(sender, EventArgs.Empty); - } - - /// - /// Raises the Game Launch Failed event. - /// - private void OnGameLaunchFailed() - { - this.LaunchFailed?.Invoke(this, EventArgs.Empty); - } - - /// - /// Raises the Game Exited event. - /// - private void OnGameExited(int exitCode) - { - this.GameExited?.Invoke(this, exitCode); - } - } + /// + /// This class has a lot of async stuff going on. It handles installing the game + /// and updating it when it needs to. + /// + /// The download protocol is selected based on the configuration each time this is + /// instantiated, and control is then handed over to whatever the protocol needs + /// to do. + /// + /// Since this class starts new threads in which it does the larger computations, + /// there must be no usage of UI code in this class. Keep it clean. + /// + internal sealed class GameHandler + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + /// + /// Event raised whenever the progress of installing or updating the game changes. + /// + public event EventHandler ProgressChanged; + + /// + /// Event raised whenever the game finishes downloading, regardless of whether or not it's updating + /// or installing. + /// + public event EventHandler DownloadFinished; + + /// + /// Event raised whenever the game fails to download, regardless of whether or not it's updating + /// or installing. + /// + public event EventHandler DownloadFailed; + + /// + /// Event raised whenever the game fails to launch. + /// + public event EventHandler LaunchFailed; + + /// + /// Event raised whenever the game exits. + /// + public event EventHandler GameExited; + + // ... + private static readonly ILaunchpadConfiguration Configuration = ConfigHandler.Instance.Configuration; + + private readonly PatchProtocolHandler Patch; + + private readonly GameArgumentService GameArgumentService = new GameArgumentService(); + + /// + /// Initializes a new instance of the class. + /// + public GameHandler() + { + this.Patch = PatchProtocolProvider.GetHandler(); + if (this.Patch == null) + { + return; + } + + this.Patch.ModuleDownloadProgressChanged += OnModuleInstallProgressChanged; + this.Patch.ModuleVerifyProgressChanged += OnModuleInstallProgressChanged; + this.Patch.ModuleUpdateProgressChanged += OnModuleInstallProgressChanged; + + this.Patch.ModuleInstallationFinished += OnModuleInstallationFinished; + this.Patch.ModuleInstallationFailed += OnModuleInstallationFailed; + } + + /// + /// Starts an asynchronous game installation task. + /// + public void InstallGame() + { + Log.Info($"Starting installation of game files using protocol \"{this.Patch.GetType().Name}\""); + var t = new Thread(this.Patch.InstallGame) + { + Name = "InstallGame", + IsBackground = true + }; + + t.Start(); + } + + /// + /// Starts an asynchronous game update task. + /// + public void UpdateGame() + { + Log.Info($"Starting update of game files using protocol \"{this.Patch.GetType().Name}\""); + var t = new Thread(() => this.Patch.UpdateModule(EModule.Game)) + { + Name = "UpdateGame", + IsBackground = true + }; + + t.Start(); + } + + /// + /// Starts an asynchronous game verification task. + /// + public void VerifyGame() + { + Log.Info("Beginning verification of game files."); + var t = new Thread(() => this.Patch.VerifyModule(EModule.Game)) + { + Name = "VerifyGame", + IsBackground = true + }; + + t.Start(); + } + + /// + /// Deletes all local data and installs the game again. + /// + public void ReinstallGame() + { + Log.Info("Beginning full reinstall of game files."); + if (Directory.Exists(DirectoryHelpers.GetLocalGameDirectory())) + { + Log.Info("Deleting existing game files."); + Directory.Delete(DirectoryHelpers.GetLocalGameDirectory(), true); + } + + if (File.Exists(DirectoryHelpers.GetGameTagfilePath())) + { + Log.Info("Deleting install progress cookie."); + File.Delete(DirectoryHelpers.GetGameTagfilePath()); + } + + var t = new Thread(() => this.Patch.InstallGame()) + { + Name = "ReinstallGame", + IsBackground = true + }; + + t.Start(); + } + + /// + /// Launches the game. + /// + public void LaunchGame() + { + try + { + var executable = Path.Combine(DirectoryHelpers.GetLocalGameDirectory(), Configuration.ExecutablePath); + if (!File.Exists(executable)) + { + throw new FileNotFoundException($"Game executable at path (\"{executable}\") not found."); + } + + var executableDir = Path.GetDirectoryName(executable) ?? DirectoryHelpers.GetLocalLauncherDirectory(); + + // Do not move the argument assignment inside the gameStartInfo initializer. + // It causes a TargetInvocationException crash through black magic. + var gameArguments = string.Join(" ", this.GameArgumentService.GetGameArguments()); + var gameStartInfo = new ProcessStartInfo + { + FileName = executable, + Arguments = gameArguments, + WorkingDirectory = executableDir + }; + + Log.Info($"Launching game. \n\tExecutable path: {gameStartInfo.FileName}"); + + var gameProcess = new Process + { + StartInfo = gameStartInfo, + EnableRaisingEvents = true + }; + + gameProcess.Exited += (sender, args) => + { + if (gameProcess.ExitCode != 0) + { + Log.Info + ( + $"The game exited with an exit code of {gameProcess.ExitCode}. " + + "There may have been issues during runtime, or the game may not have started at all." + ); + } + + OnGameExited(gameProcess.ExitCode); + + // Manual disposing + gameProcess.Dispose(); + }; + + // Make sure the game executable is flagged as such on Unix + if (PlatformHelpers.IsRunningOnUnix()) + { + Process.Start("chmod", $"+x {gameStartInfo.FileName}"); + } + + gameProcess.Start(); + } + catch (FileNotFoundException fex) + { + Log.Warn($"Game launch failed (FileNotFoundException): {fex.Message}"); + Log.Warn("If the game executable is there, try overriding the executable name in the configuration file."); + + OnGameLaunchFailed(); + } + catch (IOException ioex) + { + Log.Warn($"Game launch failed (IOException): {ioex.Message}"); + + OnGameLaunchFailed(); + } + } + + /// + /// Passes the internal event in the protocol handler to the outward-facing + /// event. + /// + /// Sender. + /// E. + private void OnModuleInstallProgressChanged(object sender, ModuleProgressChangedArgs e) + { + this.ProgressChanged?.Invoke(sender, e); + } + + /// + /// Passes the internal event in the protocol handler to the outward-facing + /// event. + /// + /// Sender. + /// E. + private void OnModuleInstallationFinished(object sender, EModule e) + { + this.DownloadFinished?.Invoke(sender, EventArgs.Empty); + } + + /// + /// Passes the internal event in the protocol handler to the outward-facing + /// event. + /// + /// Sender. + /// E. + private void OnModuleInstallationFailed(object sender, EModule e) + { + this.DownloadFailed?.Invoke(sender, EventArgs.Empty); + } + + /// + /// Raises the Game Launch Failed event. + /// + private void OnGameLaunchFailed() + { + this.LaunchFailed?.Invoke(this, EventArgs.Empty); + } + + /// + /// Raises the Game Exited event. + /// + private void OnGameExited(int exitCode) + { + this.GameExited?.Invoke(this, exitCode); + } + } } diff --git a/Launchpad.Launcher/Handlers/LauncherHandler.cs b/Launchpad.Launcher/Handlers/LauncherHandler.cs index 0ffbcd52..71fc9fd3 100644 --- a/Launchpad.Launcher/Handlers/LauncherHandler.cs +++ b/Launchpad.Launcher/Handlers/LauncherHandler.cs @@ -35,212 +35,212 @@ namespace Launchpad.Launcher.Handlers { - /// - /// This class has a lot of async stuff going on. It handles updating the launcher - /// and loading the changelog from the server. - /// Since this class starts new threads in which it does the larger computations, - /// there must be no usage of UI code in this class. Keep it clean. - /// - internal sealed class LauncherHandler - { - // Replace the variables in the script with actual data - private const string TempDirectoryVariable = "%temp%"; - private const string LocalInstallDirectoryVariable = "%localDir%"; - private const string LocalExecutableName = "%launchpadExecutable%"; - - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - /// - /// Raised whenever the launcher finishes downloading. - /// - public event EventHandler LauncherDownloadFinished; - - /// - /// Raised whenever the launcher download progress changes. - /// - public event EventHandler LauncherDownloadProgressChanged; - - private readonly PatchProtocolHandler Patch; - - /// - /// The config handler reference. - /// - private static readonly ILaunchpadConfiguration Configuration = ConfigHandler.Instance.Configuration; - - /// - /// Initializes a new instance of the class. - /// - public LauncherHandler() - { - this.Patch = PatchProtocolProvider.GetHandler(); - - this.Patch.ModuleDownloadProgressChanged += OnLauncherDownloadProgressChanged; - this.Patch.ModuleInstallationFinished += OnLauncherDownloadFinished; - } - - /// - /// Updates the launcher asynchronously. - /// - public void UpdateLauncher() - { - try - { - Log.Info($"Starting update of lancher files using protocol \"{this.Patch.GetType().Name}\""); - - var t = new Thread(() => this.Patch.UpdateModule(EModule.Launcher)) - { - Name = "UpdateLauncher", - IsBackground = true - }; - - t.Start(); - } - catch (IOException ioex) - { - Log.Warn("The launcher update failed (IOException): " + ioex.Message); - } - } - - /// - /// Checks if the launcher can access the standard HTTP changelog. - /// - /// true if the changelog can be accessed; otherwise, false. - public static bool CanAccessStandardChangelog() - { - if (string.IsNullOrEmpty(Configuration.ChangelogAddress.AbsoluteUri)) - { - return false; - } - - var address = Configuration.ChangelogAddress; - - // Only allow HTTP URIs - if (!(address.Scheme == "http" || address.Scheme == "https")) - { - return false; - } - - var headRequest = (HttpWebRequest)WebRequest.Create(address); - headRequest.Method = "HEAD"; - - try - { - using var headResponse = (HttpWebResponse)headRequest.GetResponse(); - return headResponse.StatusCode == HttpStatusCode.OK; - } - catch (WebException wex) - { - Log.Warn("Could not access standard changelog (WebException): " + wex.Message); - return false; - } - } - - /// - /// Creates the update script on disk. - /// - /// ProcessStartInfo for the update script. - public static ProcessStartInfo CreateUpdateScript() - { - try - { - var updateScriptPath = GetUpdateScriptPath(); - var updateScriptSource = GetUpdateScriptSource(); - - File.WriteAllText(updateScriptPath, updateScriptSource); - - if (PlatformHelpers.IsRunningOnUnix()) - { - var chmod = Process.Start("chmod", $"+x {updateScriptPath}"); - chmod?.WaitForExit(); - } - - var updateShellProcess = new ProcessStartInfo - { - FileName = updateScriptPath, - UseShellExecute = false, - RedirectStandardOutput = false, - CreateNoWindow = true, - WindowStyle = ProcessWindowStyle.Normal - }; - - return updateShellProcess; - } - catch (IOException ioex) - { - Log.Warn("Failed to create update script (IOException): " + ioex.Message); - - return null; - } - } - - /// - /// Extracts the bundled update script and populates the variables in it - /// with the data needed for the update procedure. - /// - private static string GetUpdateScriptSource() - { - // Load the script from the embedded resources - var localAssembly = Assembly.GetExecutingAssembly(); - - var scriptSource = string.Empty; - var resourceName = GetUpdateScriptResourceName(); - using (var resourceStream = localAssembly.GetManifestResourceStream(resourceName)) - { - if (resourceStream != null) - { - using var reader = new StreamReader(resourceStream); - scriptSource = reader.ReadToEnd(); - } - } - - var transientScriptSource = scriptSource; - - transientScriptSource = transientScriptSource.Replace(TempDirectoryVariable, Path.GetTempPath()); - transientScriptSource = transientScriptSource.Replace(LocalInstallDirectoryVariable, DirectoryHelpers.GetLocalLauncherDirectory()); - transientScriptSource = transientScriptSource.Replace(LocalExecutableName, Path.GetFileName(localAssembly.Location)); - - return transientScriptSource; - } - - /// - /// Gets the name of the embedded update script. - /// - private static string GetUpdateScriptResourceName() - { - if (PlatformHelpers.IsRunningOnUnix()) - { - return "Launchpad.Launcher.Resources.launchpad_update.sh"; - } - else - { - return "Launchpad.Launcher.Resources.launchpad_update.bat"; - } - } - - /// - /// Gets the name of the embedded update script. - /// - private static string GetUpdateScriptPath() - { - if (PlatformHelpers.IsRunningOnUnix()) - { - return Path.Combine(Path.GetTempPath(), "launchpad_update.sh"); - } - - return Path.Combine(Path.GetTempPath(), "launchpad_update.bat"); - } - - private void OnLauncherDownloadProgressChanged(object sender, ModuleProgressChangedArgs e) - { - this.LauncherDownloadProgressChanged?.Invoke(sender, e); - } - - private void OnLauncherDownloadFinished(object sender, EModule e) - { - this.LauncherDownloadFinished?.Invoke(sender, EventArgs.Empty); - } - } + /// + /// This class has a lot of async stuff going on. It handles updating the launcher + /// and loading the changelog from the server. + /// Since this class starts new threads in which it does the larger computations, + /// there must be no usage of UI code in this class. Keep it clean. + /// + internal sealed class LauncherHandler + { + // Replace the variables in the script with actual data + private const string TempDirectoryVariable = "%temp%"; + private const string LocalInstallDirectoryVariable = "%localDir%"; + private const string LocalExecutableName = "%launchpadExecutable%"; + + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + /// + /// Raised whenever the launcher finishes downloading. + /// + public event EventHandler LauncherDownloadFinished; + + /// + /// Raised whenever the launcher download progress changes. + /// + public event EventHandler LauncherDownloadProgressChanged; + + private readonly PatchProtocolHandler Patch; + + /// + /// The config handler reference. + /// + private static readonly ILaunchpadConfiguration Configuration = ConfigHandler.Instance.Configuration; + + /// + /// Initializes a new instance of the class. + /// + public LauncherHandler() + { + this.Patch = PatchProtocolProvider.GetHandler(); + + this.Patch.ModuleDownloadProgressChanged += OnLauncherDownloadProgressChanged; + this.Patch.ModuleInstallationFinished += OnLauncherDownloadFinished; + } + + /// + /// Updates the launcher asynchronously. + /// + public void UpdateLauncher() + { + try + { + Log.Info($"Starting update of lancher files using protocol \"{this.Patch.GetType().Name}\""); + + var t = new Thread(() => this.Patch.UpdateModule(EModule.Launcher)) + { + Name = "UpdateLauncher", + IsBackground = true + }; + + t.Start(); + } + catch (IOException ioex) + { + Log.Warn("The launcher update failed (IOException): " + ioex.Message); + } + } + + /// + /// Checks if the launcher can access the standard HTTP changelog. + /// + /// true if the changelog can be accessed; otherwise, false. + public static bool CanAccessStandardChangelog() + { + if (string.IsNullOrEmpty(Configuration.ChangelogAddress.AbsoluteUri)) + { + return false; + } + + var address = Configuration.ChangelogAddress; + + // Only allow HTTP URIs + if (!(address.Scheme == "http" || address.Scheme == "https")) + { + return false; + } + + var headRequest = (HttpWebRequest)WebRequest.Create(address); + headRequest.Method = "HEAD"; + + try + { + using var headResponse = (HttpWebResponse)headRequest.GetResponse(); + return headResponse.StatusCode == HttpStatusCode.OK; + } + catch (WebException wex) + { + Log.Warn("Could not access standard changelog (WebException): " + wex.Message); + return false; + } + } + + /// + /// Creates the update script on disk. + /// + /// ProcessStartInfo for the update script. + public static ProcessStartInfo CreateUpdateScript() + { + try + { + var updateScriptPath = GetUpdateScriptPath(); + var updateScriptSource = GetUpdateScriptSource(); + + File.WriteAllText(updateScriptPath, updateScriptSource); + + if (PlatformHelpers.IsRunningOnUnix()) + { + var chmod = Process.Start("chmod", $"+x {updateScriptPath}"); + chmod?.WaitForExit(); + } + + var updateShellProcess = new ProcessStartInfo + { + FileName = updateScriptPath, + UseShellExecute = false, + RedirectStandardOutput = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Normal + }; + + return updateShellProcess; + } + catch (IOException ioex) + { + Log.Warn("Failed to create update script (IOException): " + ioex.Message); + + return null; + } + } + + /// + /// Extracts the bundled update script and populates the variables in it + /// with the data needed for the update procedure. + /// + private static string GetUpdateScriptSource() + { + // Load the script from the embedded resources + var localAssembly = Assembly.GetExecutingAssembly(); + + var scriptSource = string.Empty; + var resourceName = GetUpdateScriptResourceName(); + using (var resourceStream = localAssembly.GetManifestResourceStream(resourceName)) + { + if (resourceStream != null) + { + using var reader = new StreamReader(resourceStream); + scriptSource = reader.ReadToEnd(); + } + } + + var transientScriptSource = scriptSource; + + transientScriptSource = transientScriptSource.Replace(TempDirectoryVariable, Path.GetTempPath()); + transientScriptSource = transientScriptSource.Replace(LocalInstallDirectoryVariable, DirectoryHelpers.GetLocalLauncherDirectory()); + transientScriptSource = transientScriptSource.Replace(LocalExecutableName, Path.GetFileName(localAssembly.Location)); + + return transientScriptSource; + } + + /// + /// Gets the name of the embedded update script. + /// + private static string GetUpdateScriptResourceName() + { + if (PlatformHelpers.IsRunningOnUnix()) + { + return "Launchpad.Launcher.Resources.launchpad_update.sh"; + } + else + { + return "Launchpad.Launcher.Resources.launchpad_update.bat"; + } + } + + /// + /// Gets the name of the embedded update script. + /// + private static string GetUpdateScriptPath() + { + if (PlatformHelpers.IsRunningOnUnix()) + { + return Path.Combine(Path.GetTempPath(), "launchpad_update.sh"); + } + + return Path.Combine(Path.GetTempPath(), "launchpad_update.bat"); + } + + private void OnLauncherDownloadProgressChanged(object sender, ModuleProgressChangedArgs e) + { + this.LauncherDownloadProgressChanged?.Invoke(sender, e); + } + + private void OnLauncherDownloadFinished(object sender, EModule e) + { + this.LauncherDownloadFinished?.Invoke(sender, EventArgs.Empty); + } + } } diff --git a/Launchpad.Launcher/Handlers/Protocols/EModule.cs b/Launchpad.Launcher/Handlers/Protocols/EModule.cs index eddeadaf..e69c011b 100644 --- a/Launchpad.Launcher/Handlers/Protocols/EModule.cs +++ b/Launchpad.Launcher/Handlers/Protocols/EModule.cs @@ -22,19 +22,19 @@ namespace Launchpad.Launcher.Handlers.Protocols { - /// - /// A list of modules that can be downloaded and reported on. - /// - public enum EModule : byte - { - /// - /// The launcher itself. - /// - Launcher = 1, + /// + /// A list of modules that can be downloaded and reported on. + /// + public enum EModule : byte + { + /// + /// The launcher itself. + /// + Launcher = 1, - /// - /// The managed game. - /// - Game = 2 - } + /// + /// The managed game. + /// + Game = 2 + } } diff --git a/Launchpad.Launcher/Handlers/Protocols/Manifest/FTPProtocolHandler.cs b/Launchpad.Launcher/Handlers/Protocols/Manifest/FTPProtocolHandler.cs index addbd939..424385c8 100644 --- a/Launchpad.Launcher/Handlers/Protocols/Manifest/FTPProtocolHandler.cs +++ b/Launchpad.Launcher/Handlers/Protocols/Manifest/FTPProtocolHandler.cs @@ -31,375 +31,375 @@ namespace Launchpad.Launcher.Handlers.Protocols.Manifest { - /// - /// FTP handler. Handles downloading and reading files on a remote FTP server. - /// There are also functions for retrieving remote version information of the game and the launcher. - /// - /// This protocol uses a manifest. - /// - internal sealed class FTPProtocolHandler : ManifestBasedProtocolHandler - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - /// - public override bool CanPatch() - { - Log.Info("Pinging remote patching server to determine if we can connect to it."); - - var canConnect = false; - - var url = this.Configuration.RemoteAddress.AbsoluteUri; - var username = this.Configuration.RemoteUsername; - var password = this.Configuration.RemotePassword; - - try - { - var plainRequest = CreateFtpWebRequest(url, username, password); - - if (plainRequest == null) - { - return false; - } - - plainRequest.Method = WebRequestMethods.Ftp.ListDirectoryDetails; - plainRequest.Timeout = 4000; - - try - { - using var response = (FtpWebResponse)plainRequest.GetResponse(); - switch (response.StatusCode) - { - case FtpStatusCode.OpeningData: - case FtpStatusCode.DataAlreadyOpen: - { - canConnect = true; - break; - } - } - } - catch (WebException wex) - { - Log.Warn("Unable to connect to remote patch server (WebException): " + wex.Message); - canConnect = false; - } - } - catch (WebException wex) - { - Log.Warn("Unable to connect due a malformed url in the configuration (WebException): " + wex.Message); - canConnect = false; - } - - return canConnect; - } - - /// - public override bool IsPlatformAvailable(ESystemTarget platform) - { - var remote = $"{this.Configuration.RemoteAddress}/game/{platform}/.provides"; - - return DoesRemoteFileExist(remote); - } - - /// - public override string GetChangelogMarkup() - { - var changelogURL = $"{this.Configuration.RemoteAddress}/launcher/changelog.pango"; - return ReadRemoteFile(changelogURL); - } - - /// - public override bool CanProvideBanner() - { - var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; - - return DoesRemoteFileExist(bannerURL); - } - - /// - public override Image GetBanner() - { - var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; - - var localBannerPath = Path.Combine(Path.GetTempPath(), "banner.png"); - - DownloadRemoteFile(bannerURL, localBannerPath); - var bytes = File.ReadAllBytes(localBannerPath); - return Image.Load(bytes); - } - - /// - protected override string ReadRemoteFile(string url, bool useAnonymousLogin = false) - { - // Clean the input url first - var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); - - string username; - string password; - if (useAnonymousLogin) - { - username = "anonymous"; - password = "anonymous"; - } - else - { - username = this.Configuration.RemoteUsername; - password = this.Configuration.RemotePassword; - } - - try - { - var request = CreateFtpWebRequest(remoteURL, username, password); - var sizerequest = CreateFtpWebRequest(remoteURL, username, password); - - request.Method = WebRequestMethods.Ftp.DownloadFile; - sizerequest.Method = WebRequestMethods.Ftp.GetFileSize; - - var data = string.Empty; - using var remoteStream = request.GetResponse().GetResponseStream(); - if (remoteStream == null) - { - return string.Empty; - } - - long fileSize; - using (var sizeResponse = (FtpWebResponse)sizerequest.GetResponse()) - { - fileSize = sizeResponse.ContentLength; - } - - var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; - if (fileSize < bufferSize) - { - var smallBuffer = new byte[fileSize]; - remoteStream.Read(smallBuffer, 0, smallBuffer.Length); - - data = Encoding.UTF8.GetString(smallBuffer, 0, smallBuffer.Length); - } - else - { - var buffer = new byte[bufferSize]; - - while (true) - { - var bytesRead = remoteStream.Read(buffer, 0, buffer.Length); - - if (bytesRead == 0) - { - break; - } - - data += Encoding.UTF8.GetString(buffer, 0, bytesRead); - } - } - - return data; - } - catch (WebException wex) - { - Log.Error($"Failed to read the contents of remote file \"{remoteURL}\" (WebException): {wex.Message}"); - return string.Empty; - } - } - - /// - protected override void DownloadRemoteFile(string url, string localPath, long totalSize = 0, long contentOffset = 0, bool useAnonymousLogin = false) - { - // Make sure we're not passing in any backslashes in the url - var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); - - string username; - string password; - if (useAnonymousLogin) - { - username = "anonymous"; - password = "anonymous"; - } - else - { - username = this.Configuration.RemoteUsername; - password = this.Configuration.RemotePassword; - } - - try - { - var request = CreateFtpWebRequest(remoteURL, username, password); - var sizerequest = CreateFtpWebRequest(remoteURL, username, password); - - request.Method = WebRequestMethods.Ftp.DownloadFile; - request.ContentOffset = contentOffset; - - sizerequest.Method = WebRequestMethods.Ftp.GetFileSize; - - using var contentStream = request.GetResponse().GetResponseStream(); - if (contentStream == null) - { - Log.Error - ( - $"Failed to download the remote file at \"{remoteURL}\" (NullReferenceException from the content stream). " + - "Check your internet connection." - ); - - return; - } - - var fileSize = contentOffset; - using (var sizereader = (FtpWebResponse)sizerequest.GetResponse()) - { - fileSize += sizereader.ContentLength; - } - - using - ( - var fileStream = contentOffset > 0 - ? new FileStream(localPath, FileMode.Append) - : new FileStream(localPath, FileMode.Create) - ) - { - fileStream.Position = contentOffset; - var totalBytesDownloaded = contentOffset; - - var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; - if (fileSize < bufferSize) - { - var smallBuffer = new byte[fileSize]; - contentStream.Read(smallBuffer, 0, smallBuffer.Length); - - fileStream.Write(smallBuffer, 0, smallBuffer.Length); - - totalBytesDownloaded += smallBuffer.Length; - - // Report download progress - this.ModuleDownloadProgressArgs.ProgressBarMessage = GetDownloadProgressBarMessage - ( - Path.GetFileName(remoteURL), - totalBytesDownloaded, - fileSize - ); - - this.ModuleDownloadProgressArgs.ProgressFraction = (double)totalBytesDownloaded / fileSize; - OnModuleDownloadProgressChanged(); - } - else - { - var buffer = new byte[bufferSize]; - - while (true) - { - var bytesRead = contentStream.Read(buffer, 0, buffer.Length); - - if (bytesRead == 0) - { - break; - } - - fileStream.Write(buffer, 0, bytesRead); - - totalBytesDownloaded += bytesRead; - - // Report download progress - this.ModuleDownloadProgressArgs.ProgressBarMessage = GetDownloadProgressBarMessage - ( - Path.GetFileName(remoteURL), - totalBytesDownloaded, - fileSize - ); - this.ModuleDownloadProgressArgs.ProgressFraction = (double)totalBytesDownloaded / fileSize; - OnModuleDownloadProgressChanged(); - } - } - } - } - catch (WebException wex) - { - Log.Error($"Failed to download the remote file at \"{remoteURL}\" (WebException): {wex.Message}"); - } - catch (IOException ioex) - { - Log.Error($"Failed to download the remote file at \"{remoteURL}\" (IOException): {ioex.Message}"); - } - } - - /// - /// Creates an ftp web request. - /// - /// The ftp web request. - /// Ftp directory path. - /// Remote FTP username. - /// Remote FTP password. - private FtpWebRequest CreateFtpWebRequest(string remotePath, string username, string password) - { - if (!remotePath.StartsWith(this.Configuration.RemoteAddress.AbsoluteUri)) - { - remotePath = $"{this.Configuration.RemoteAddress}/{remotePath}"; - } - - try - { - var request = (FtpWebRequest)WebRequest.Create(new Uri(remotePath)); - - // Set proxy to null. Under current configuration if this option is not set then the proxy - // that is used will get an html response from the web content gateway (firewall monitoring system) - request.Proxy = null; - - request.UsePassive = true; - request.UseBinary = true; - - request.Credentials = new NetworkCredential(username, password); - - return request; - } - catch (WebException wex) - { - Log.Warn("Unable to create a WebRequest for the specified file (WebException): " + wex.Message); - return null; - } - catch (ArgumentException aex) - { - Log.Warn("Unable to create a WebRequest for the specified file (ArgumentException): " + aex.Message); - return null; - } - catch (UriFormatException uex) - { - Log.Warn - ( - "Unable to create a WebRequest for the specified file (UriFormatException): " + uex.Message + "\n" + - "You may need to add \"ftp://\" before the url in the config." - ); - return null; - } - } - - /// - /// Checks if a given file exists on the remote FTP server. - /// - /// true, if the file exists, false otherwise. - /// Remote path. - private bool DoesRemoteFileExist(string remotePath) - { - var request = CreateFtpWebRequest(remotePath, this.Configuration.RemoteUsername, this.Configuration.RemotePassword); - FtpWebResponse response = null; - - try - { - response = (FtpWebResponse)request.GetResponse(); - } - catch (WebException ex) - { - response = (FtpWebResponse)ex.Response; - if (response.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable) - { - return false; - } - } - finally - { - response?.Dispose(); - } - - return true; - } - } + /// + /// FTP handler. Handles downloading and reading files on a remote FTP server. + /// There are also functions for retrieving remote version information of the game and the launcher. + /// + /// This protocol uses a manifest. + /// + internal sealed class FTPProtocolHandler : ManifestBasedProtocolHandler + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + /// + public override bool CanPatch() + { + Log.Info("Pinging remote patching server to determine if we can connect to it."); + + var canConnect = false; + + var url = this.Configuration.RemoteAddress.AbsoluteUri; + var username = this.Configuration.RemoteUsername; + var password = this.Configuration.RemotePassword; + + try + { + var plainRequest = CreateFtpWebRequest(url, username, password); + + if (plainRequest == null) + { + return false; + } + + plainRequest.Method = WebRequestMethods.Ftp.ListDirectoryDetails; + plainRequest.Timeout = 4000; + + try + { + using var response = (FtpWebResponse)plainRequest.GetResponse(); + switch (response.StatusCode) + { + case FtpStatusCode.OpeningData: + case FtpStatusCode.DataAlreadyOpen: + { + canConnect = true; + break; + } + } + } + catch (WebException wex) + { + Log.Warn("Unable to connect to remote patch server (WebException): " + wex.Message); + canConnect = false; + } + } + catch (WebException wex) + { + Log.Warn("Unable to connect due a malformed url in the configuration (WebException): " + wex.Message); + canConnect = false; + } + + return canConnect; + } + + /// + public override bool IsPlatformAvailable(ESystemTarget platform) + { + var remote = $"{this.Configuration.RemoteAddress}/game/{platform}/.provides"; + + return DoesRemoteFileExist(remote); + } + + /// + public override string GetChangelogMarkup() + { + var changelogURL = $"{this.Configuration.RemoteAddress}/launcher/changelog.pango"; + return ReadRemoteFile(changelogURL); + } + + /// + public override bool CanProvideBanner() + { + var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; + + return DoesRemoteFileExist(bannerURL); + } + + /// + public override Image GetBanner() + { + var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; + + var localBannerPath = Path.Combine(Path.GetTempPath(), "banner.png"); + + DownloadRemoteFile(bannerURL, localBannerPath); + var bytes = File.ReadAllBytes(localBannerPath); + return Image.Load(bytes); + } + + /// + protected override string ReadRemoteFile(string url, bool useAnonymousLogin = false) + { + // Clean the input url first + var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); + + string username; + string password; + if (useAnonymousLogin) + { + username = "anonymous"; + password = "anonymous"; + } + else + { + username = this.Configuration.RemoteUsername; + password = this.Configuration.RemotePassword; + } + + try + { + var request = CreateFtpWebRequest(remoteURL, username, password); + var sizerequest = CreateFtpWebRequest(remoteURL, username, password); + + request.Method = WebRequestMethods.Ftp.DownloadFile; + sizerequest.Method = WebRequestMethods.Ftp.GetFileSize; + + var data = string.Empty; + using var remoteStream = request.GetResponse().GetResponseStream(); + if (remoteStream == null) + { + return string.Empty; + } + + long fileSize; + using (var sizeResponse = (FtpWebResponse)sizerequest.GetResponse()) + { + fileSize = sizeResponse.ContentLength; + } + + var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; + if (fileSize < bufferSize) + { + var smallBuffer = new byte[fileSize]; + remoteStream.Read(smallBuffer, 0, smallBuffer.Length); + + data = Encoding.UTF8.GetString(smallBuffer, 0, smallBuffer.Length); + } + else + { + var buffer = new byte[bufferSize]; + + while (true) + { + var bytesRead = remoteStream.Read(buffer, 0, buffer.Length); + + if (bytesRead == 0) + { + break; + } + + data += Encoding.UTF8.GetString(buffer, 0, bytesRead); + } + } + + return data; + } + catch (WebException wex) + { + Log.Error($"Failed to read the contents of remote file \"{remoteURL}\" (WebException): {wex.Message}"); + return string.Empty; + } + } + + /// + protected override void DownloadRemoteFile(string url, string localPath, long totalSize = 0, long contentOffset = 0, bool useAnonymousLogin = false) + { + // Make sure we're not passing in any backslashes in the url + var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); + + string username; + string password; + if (useAnonymousLogin) + { + username = "anonymous"; + password = "anonymous"; + } + else + { + username = this.Configuration.RemoteUsername; + password = this.Configuration.RemotePassword; + } + + try + { + var request = CreateFtpWebRequest(remoteURL, username, password); + var sizerequest = CreateFtpWebRequest(remoteURL, username, password); + + request.Method = WebRequestMethods.Ftp.DownloadFile; + request.ContentOffset = contentOffset; + + sizerequest.Method = WebRequestMethods.Ftp.GetFileSize; + + using var contentStream = request.GetResponse().GetResponseStream(); + if (contentStream == null) + { + Log.Error + ( + $"Failed to download the remote file at \"{remoteURL}\" (NullReferenceException from the content stream). " + + "Check your internet connection." + ); + + return; + } + + var fileSize = contentOffset; + using (var sizereader = (FtpWebResponse)sizerequest.GetResponse()) + { + fileSize += sizereader.ContentLength; + } + + using + ( + var fileStream = contentOffset > 0 + ? new FileStream(localPath, FileMode.Append) + : new FileStream(localPath, FileMode.Create) + ) + { + fileStream.Position = contentOffset; + var totalBytesDownloaded = contentOffset; + + var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; + if (fileSize < bufferSize) + { + var smallBuffer = new byte[fileSize]; + contentStream.Read(smallBuffer, 0, smallBuffer.Length); + + fileStream.Write(smallBuffer, 0, smallBuffer.Length); + + totalBytesDownloaded += smallBuffer.Length; + + // Report download progress + this.ModuleDownloadProgressArgs.ProgressBarMessage = GetDownloadProgressBarMessage + ( + Path.GetFileName(remoteURL), + totalBytesDownloaded, + fileSize + ); + + this.ModuleDownloadProgressArgs.ProgressFraction = (double)totalBytesDownloaded / fileSize; + OnModuleDownloadProgressChanged(); + } + else + { + var buffer = new byte[bufferSize]; + + while (true) + { + var bytesRead = contentStream.Read(buffer, 0, buffer.Length); + + if (bytesRead == 0) + { + break; + } + + fileStream.Write(buffer, 0, bytesRead); + + totalBytesDownloaded += bytesRead; + + // Report download progress + this.ModuleDownloadProgressArgs.ProgressBarMessage = GetDownloadProgressBarMessage + ( + Path.GetFileName(remoteURL), + totalBytesDownloaded, + fileSize + ); + this.ModuleDownloadProgressArgs.ProgressFraction = (double)totalBytesDownloaded / fileSize; + OnModuleDownloadProgressChanged(); + } + } + } + } + catch (WebException wex) + { + Log.Error($"Failed to download the remote file at \"{remoteURL}\" (WebException): {wex.Message}"); + } + catch (IOException ioex) + { + Log.Error($"Failed to download the remote file at \"{remoteURL}\" (IOException): {ioex.Message}"); + } + } + + /// + /// Creates an ftp web request. + /// + /// The ftp web request. + /// Ftp directory path. + /// Remote FTP username. + /// Remote FTP password. + private FtpWebRequest CreateFtpWebRequest(string remotePath, string username, string password) + { + if (!remotePath.StartsWith(this.Configuration.RemoteAddress.AbsoluteUri)) + { + remotePath = $"{this.Configuration.RemoteAddress}/{remotePath}"; + } + + try + { + var request = (FtpWebRequest)WebRequest.Create(new Uri(remotePath)); + + // Set proxy to null. Under current configuration if this option is not set then the proxy + // that is used will get an html response from the web content gateway (firewall monitoring system) + request.Proxy = null; + + request.UsePassive = true; + request.UseBinary = true; + + request.Credentials = new NetworkCredential(username, password); + + return request; + } + catch (WebException wex) + { + Log.Warn("Unable to create a WebRequest for the specified file (WebException): " + wex.Message); + return null; + } + catch (ArgumentException aex) + { + Log.Warn("Unable to create a WebRequest for the specified file (ArgumentException): " + aex.Message); + return null; + } + catch (UriFormatException uex) + { + Log.Warn + ( + "Unable to create a WebRequest for the specified file (UriFormatException): " + uex.Message + "\n" + + "You may need to add \"ftp://\" before the url in the config." + ); + return null; + } + } + + /// + /// Checks if a given file exists on the remote FTP server. + /// + /// true, if the file exists, false otherwise. + /// Remote path. + private bool DoesRemoteFileExist(string remotePath) + { + var request = CreateFtpWebRequest(remotePath, this.Configuration.RemoteUsername, this.Configuration.RemotePassword); + FtpWebResponse response = null; + + try + { + response = (FtpWebResponse)request.GetResponse(); + } + catch (WebException ex) + { + response = (FtpWebResponse)ex.Response; + if (response.StatusCode == FtpStatusCode.ActionNotTakenFileUnavailable) + { + return false; + } + } + finally + { + response?.Dispose(); + } + + return true; + } + } } diff --git a/Launchpad.Launcher/Handlers/Protocols/Manifest/HTTPProtocolHandler.cs b/Launchpad.Launcher/Handlers/Protocols/Manifest/HTTPProtocolHandler.cs index 27506825..becb54ed 100644 --- a/Launchpad.Launcher/Handlers/Protocols/Manifest/HTTPProtocolHandler.cs +++ b/Launchpad.Launcher/Handlers/Protocols/Manifest/HTTPProtocolHandler.cs @@ -31,331 +31,331 @@ namespace Launchpad.Launcher.Handlers.Protocols.Manifest { - /// - /// HTTP protocol handler. Patches the launcher and game using the - /// HTTP/HTTPS protocol. - /// - internal sealed class HTTPProtocolHandler : ManifestBasedProtocolHandler - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - /// - public override bool CanPatch() - { - Log.Info("Pinging remote patching server to determine if we can connect to it."); - - var canConnect = false; - - try - { - var plainRequest = CreateHttpWebRequest - ( - this.Configuration.RemoteAddress.AbsoluteUri, - this.Configuration.RemoteUsername, - this.Configuration.RemotePassword - ); - - if (plainRequest == null) - { - return false; - } - - plainRequest.Method = WebRequestMethods.Http.Head; - plainRequest.Timeout = 4000; - - try - { - using var response = (HttpWebResponse)plainRequest.GetResponse(); - if (response.StatusCode == HttpStatusCode.OK) - { - canConnect = true; - } - } - catch (WebException wex) - { - Log.Warn("Unable to connect to remote patch server (WebException): " + wex.Message); - canConnect = false; - } - } - catch (WebException wex) - { - Log.Warn("Unable to connect due a malformed url in the configuration (WebException): " + wex.Message); - canConnect = false; - } - - return canConnect; - } - - /// - public override bool IsPlatformAvailable(ESystemTarget platform) - { - var remote = $"{this.Configuration.RemoteAddress}/game/{platform}/.provides"; - - return DoesRemoteDirectoryOrFileExist(remote); - } - - /// - public override string GetChangelogMarkup() - { - var changelogURL = $"{this.Configuration.RemoteAddress}/launcher/changelog.pango"; - return ReadRemoteFile(changelogURL); - } - - /// - public override bool CanProvideBanner() - { - var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; - - return DoesRemoteDirectoryOrFileExist(bannerURL); - } - - /// - public override Image GetBanner() - { - var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; - var localBannerPath = Path.Combine(Path.GetTempPath(), "banner.png"); - - DownloadRemoteFile(bannerURL, localBannerPath); - return Image.Load(localBannerPath); - } - - /// - protected override void DownloadRemoteFile(string url, string localPath, long totalSize = 0, long contentOffset = 0, bool useAnonymousLogin = false) - { - // Clean the url string - var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); - - string username; - string password; - if (useAnonymousLogin) - { - username = "anonymous"; - password = "anonymous"; - } - else - { - username = this.Configuration.RemoteUsername; - password = this.Configuration.RemotePassword; - } - - try - { - var request = CreateHttpWebRequest(remoteURL, username, password); - - request.Method = WebRequestMethods.Http.Get; - request.AddRange(contentOffset); - - using var contentStream = request.GetResponse().GetResponseStream(); - if (contentStream == null) - { - Log.Error - ( - $"Failed to download the remote file at \"{remoteURL}\" (NullReferenceException from the " + - $"content stream). Check your internet connection." - ); - - return; - } - - using var fileStream = contentOffset > 0 ? new FileStream(localPath, FileMode.Append) : - new FileStream(localPath, FileMode.Create); - fileStream.Position = contentOffset; - var totalBytesDownloaded = contentOffset; - - long totalFileSize; - if (contentStream.CanSeek) - { - totalFileSize = contentOffset + contentStream.Length; - } - else - { - totalFileSize = totalSize; - } - - var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; - var buffer = new byte[bufferSize]; - - while (true) - { - var bytesRead = contentStream.Read(buffer, 0, buffer.Length); - - if (bytesRead == 0) - { - break; - } - - fileStream.Write(buffer, 0, bytesRead); - - totalBytesDownloaded += bytesRead; - - // Report download progress - this.ModuleDownloadProgressArgs.ProgressBarMessage = GetDownloadProgressBarMessage - ( - Path.GetFileName(remoteURL), - totalBytesDownloaded, - totalFileSize - ); - this.ModuleDownloadProgressArgs.ProgressFraction = totalBytesDownloaded / (double)totalFileSize; - OnModuleDownloadProgressChanged(); - } - - fileStream.Flush(); - } - catch (WebException wex) - { - Log.Error($"Failed to download the remote file at \"{remoteURL}\" (WebException): {wex.Message}"); - } - catch (IOException ioex) - { - Log.Error($"Failed to download the remote file at \"{remoteURL}\" (IOException): {ioex.Message}"); - } - } - - /// - protected override string ReadRemoteFile(string url, bool useAnonymousLogin = false) - { - var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); - - string username; - string password; - if (useAnonymousLogin) - { - username = "anonymous"; - password = "anonymous"; - } - else - { - username = this.Configuration.RemoteUsername; - password = this.Configuration.RemotePassword; - } - - try - { - var request = CreateHttpWebRequest(remoteURL, username, password); - - request.Method = WebRequestMethods.Http.Get; - - var data = string.Empty; - using var remoteStream = request.GetResponse().GetResponseStream(); - - // Drop out early if the stream wasn't present - if (remoteStream == null) - { - Log.Error - ( - $"Failed to read the contents of remote file \"{remoteURL}\": " + - "Remote stream was null. This could be due to a network interruption " + - "or issues with the remote file." - ); - - return string.Empty; - } - - var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; - var buffer = new byte[bufferSize]; - - while (true) - { - var bytesRead = remoteStream.Read(buffer, 0, buffer.Length); - - if (bytesRead == 0) - { - break; - } - - data += Encoding.UTF8.GetString(buffer, 0, bytesRead); - } - - return data; - } - catch (WebException wex) - { - Log.Error($"Failed to read the contents of remote file \"{remoteURL}\" (WebException): {wex.Message}"); - return string.Empty; - } - catch (NullReferenceException nex) - { - Log.Error("Failed to establish a network connection, or the connection was interrupted during the download (NullReferenceException): " + nex.Message); - return string.Empty; - } - } - - /// - /// Creates a HTTP web request. - /// - /// The HTTP web request. - /// url of the desired remote object. - /// The username used for authentication. - /// The password used for authentication. - private HttpWebRequest CreateHttpWebRequest(string remotePath, string username, string password) - { - if (!remotePath.StartsWith(this.Configuration.RemoteAddress.AbsoluteUri)) - { - remotePath = $"{this.Configuration.RemoteAddress}/{remotePath}"; - } - - try - { - var request = (HttpWebRequest)WebRequest.Create(new Uri(remotePath)); - request.Credentials = new NetworkCredential(username, password); - - return request; - } - catch (WebException wex) - { - Log.Warn("Unable to create a WebRequest for the specified file (WebException): " + wex.Message); - return null; - } - catch (ArgumentException aex) - { - Log.Warn("Unable to create a WebRequest for the specified file (ArgumentException): " + aex.Message); - return null; - } - catch (UriFormatException uex) - { - Log.Warn("Unable to create a WebRequest for the specified file (UriFormatException): " + uex.Message + "\n" + - "You may need to add \"http://\" before the url in the config."); - return null; - } - } - - /// - /// Checks if the provided path points to a valid directory or file. - /// - /// true, if the directory or file exists, false otherwise. - /// The remote url of the directory or file. - private bool DoesRemoteDirectoryOrFileExist(string url) - { - var cleanURL = url.Replace(Path.DirectorySeparatorChar, '/'); - var request = CreateHttpWebRequest(cleanURL, this.Configuration.RemoteUsername, this.Configuration.RemotePassword); - - request.Method = WebRequestMethods.Http.Head; - HttpWebResponse response = null; - try - { - response = (HttpWebResponse)request.GetResponse(); - if (response.StatusCode != HttpStatusCode.OK) - { - return false; - } - } - catch (WebException wex) - { - response = (HttpWebResponse)wex.Response; - if (response.StatusCode == HttpStatusCode.NotFound) - { - return false; - } - } - finally - { - response?.Dispose(); - } - - return true; - } - } + /// + /// HTTP protocol handler. Patches the launcher and game using the + /// HTTP/HTTPS protocol. + /// + internal sealed class HTTPProtocolHandler : ManifestBasedProtocolHandler + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + /// + public override bool CanPatch() + { + Log.Info("Pinging remote patching server to determine if we can connect to it."); + + var canConnect = false; + + try + { + var plainRequest = CreateHttpWebRequest + ( + this.Configuration.RemoteAddress.AbsoluteUri, + this.Configuration.RemoteUsername, + this.Configuration.RemotePassword + ); + + if (plainRequest == null) + { + return false; + } + + plainRequest.Method = WebRequestMethods.Http.Head; + plainRequest.Timeout = 4000; + + try + { + using var response = (HttpWebResponse)plainRequest.GetResponse(); + if (response.StatusCode == HttpStatusCode.OK) + { + canConnect = true; + } + } + catch (WebException wex) + { + Log.Warn("Unable to connect to remote patch server (WebException): " + wex.Message); + canConnect = false; + } + } + catch (WebException wex) + { + Log.Warn("Unable to connect due a malformed url in the configuration (WebException): " + wex.Message); + canConnect = false; + } + + return canConnect; + } + + /// + public override bool IsPlatformAvailable(ESystemTarget platform) + { + var remote = $"{this.Configuration.RemoteAddress}/game/{platform}/.provides"; + + return DoesRemoteDirectoryOrFileExist(remote); + } + + /// + public override string GetChangelogMarkup() + { + var changelogURL = $"{this.Configuration.RemoteAddress}/launcher/changelog.pango"; + return ReadRemoteFile(changelogURL); + } + + /// + public override bool CanProvideBanner() + { + var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; + + return DoesRemoteDirectoryOrFileExist(bannerURL); + } + + /// + public override Image GetBanner() + { + var bannerURL = $"{this.Configuration.RemoteAddress}/launcher/banner.png"; + var localBannerPath = Path.Combine(Path.GetTempPath(), "banner.png"); + + DownloadRemoteFile(bannerURL, localBannerPath); + return Image.Load(localBannerPath); + } + + /// + protected override void DownloadRemoteFile(string url, string localPath, long totalSize = 0, long contentOffset = 0, bool useAnonymousLogin = false) + { + // Clean the url string + var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); + + string username; + string password; + if (useAnonymousLogin) + { + username = "anonymous"; + password = "anonymous"; + } + else + { + username = this.Configuration.RemoteUsername; + password = this.Configuration.RemotePassword; + } + + try + { + var request = CreateHttpWebRequest(remoteURL, username, password); + + request.Method = WebRequestMethods.Http.Get; + request.AddRange(contentOffset); + + using var contentStream = request.GetResponse().GetResponseStream(); + if (contentStream == null) + { + Log.Error + ( + $"Failed to download the remote file at \"{remoteURL}\" (NullReferenceException from the " + + $"content stream). Check your internet connection." + ); + + return; + } + + using var fileStream = contentOffset > 0 ? new FileStream(localPath, FileMode.Append) : + new FileStream(localPath, FileMode.Create); + fileStream.Position = contentOffset; + var totalBytesDownloaded = contentOffset; + + long totalFileSize; + if (contentStream.CanSeek) + { + totalFileSize = contentOffset + contentStream.Length; + } + else + { + totalFileSize = totalSize; + } + + var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; + var buffer = new byte[bufferSize]; + + while (true) + { + var bytesRead = contentStream.Read(buffer, 0, buffer.Length); + + if (bytesRead == 0) + { + break; + } + + fileStream.Write(buffer, 0, bytesRead); + + totalBytesDownloaded += bytesRead; + + // Report download progress + this.ModuleDownloadProgressArgs.ProgressBarMessage = GetDownloadProgressBarMessage + ( + Path.GetFileName(remoteURL), + totalBytesDownloaded, + totalFileSize + ); + this.ModuleDownloadProgressArgs.ProgressFraction = totalBytesDownloaded / (double)totalFileSize; + OnModuleDownloadProgressChanged(); + } + + fileStream.Flush(); + } + catch (WebException wex) + { + Log.Error($"Failed to download the remote file at \"{remoteURL}\" (WebException): {wex.Message}"); + } + catch (IOException ioex) + { + Log.Error($"Failed to download the remote file at \"{remoteURL}\" (IOException): {ioex.Message}"); + } + } + + /// + protected override string ReadRemoteFile(string url, bool useAnonymousLogin = false) + { + var remoteURL = url.Replace(Path.DirectorySeparatorChar, '/'); + + string username; + string password; + if (useAnonymousLogin) + { + username = "anonymous"; + password = "anonymous"; + } + else + { + username = this.Configuration.RemoteUsername; + password = this.Configuration.RemotePassword; + } + + try + { + var request = CreateHttpWebRequest(remoteURL, username, password); + + request.Method = WebRequestMethods.Http.Get; + + var data = string.Empty; + using var remoteStream = request.GetResponse().GetResponseStream(); + + // Drop out early if the stream wasn't present + if (remoteStream == null) + { + Log.Error + ( + $"Failed to read the contents of remote file \"{remoteURL}\": " + + "Remote stream was null. This could be due to a network interruption " + + "or issues with the remote file." + ); + + return string.Empty; + } + + var bufferSize = this.Configuration.RemoteFileDownloadBufferSize; + var buffer = new byte[bufferSize]; + + while (true) + { + var bytesRead = remoteStream.Read(buffer, 0, buffer.Length); + + if (bytesRead == 0) + { + break; + } + + data += Encoding.UTF8.GetString(buffer, 0, bytesRead); + } + + return data; + } + catch (WebException wex) + { + Log.Error($"Failed to read the contents of remote file \"{remoteURL}\" (WebException): {wex.Message}"); + return string.Empty; + } + catch (NullReferenceException nex) + { + Log.Error("Failed to establish a network connection, or the connection was interrupted during the download (NullReferenceException): " + nex.Message); + return string.Empty; + } + } + + /// + /// Creates a HTTP web request. + /// + /// The HTTP web request. + /// url of the desired remote object. + /// The username used for authentication. + /// The password used for authentication. + private HttpWebRequest CreateHttpWebRequest(string remotePath, string username, string password) + { + if (!remotePath.StartsWith(this.Configuration.RemoteAddress.AbsoluteUri)) + { + remotePath = $"{this.Configuration.RemoteAddress}/{remotePath}"; + } + + try + { + var request = (HttpWebRequest)WebRequest.Create(new Uri(remotePath)); + request.Credentials = new NetworkCredential(username, password); + + return request; + } + catch (WebException wex) + { + Log.Warn("Unable to create a WebRequest for the specified file (WebException): " + wex.Message); + return null; + } + catch (ArgumentException aex) + { + Log.Warn("Unable to create a WebRequest for the specified file (ArgumentException): " + aex.Message); + return null; + } + catch (UriFormatException uex) + { + Log.Warn("Unable to create a WebRequest for the specified file (UriFormatException): " + uex.Message + "\n" + + "You may need to add \"http://\" before the url in the config."); + return null; + } + } + + /// + /// Checks if the provided path points to a valid directory or file. + /// + /// true, if the directory or file exists, false otherwise. + /// The remote url of the directory or file. + private bool DoesRemoteDirectoryOrFileExist(string url) + { + var cleanURL = url.Replace(Path.DirectorySeparatorChar, '/'); + var request = CreateHttpWebRequest(cleanURL, this.Configuration.RemoteUsername, this.Configuration.RemotePassword); + + request.Method = WebRequestMethods.Http.Head; + HttpWebResponse response = null; + try + { + response = (HttpWebResponse)request.GetResponse(); + if (response.StatusCode != HttpStatusCode.OK) + { + return false; + } + } + catch (WebException wex) + { + response = (HttpWebResponse)wex.Response; + if (response.StatusCode == HttpStatusCode.NotFound) + { + return false; + } + } + finally + { + response?.Dispose(); + } + + return true; + } + } } diff --git a/Launchpad.Launcher/Handlers/Protocols/Manifest/ManifestBasedProtocolHandler.cs b/Launchpad.Launcher/Handlers/Protocols/Manifest/ManifestBasedProtocolHandler.cs index 1ad6bfd0..842b28cf 100644 --- a/Launchpad.Launcher/Handlers/Protocols/Manifest/ManifestBasedProtocolHandler.cs +++ b/Launchpad.Launcher/Handlers/Protocols/Manifest/ManifestBasedProtocolHandler.cs @@ -36,785 +36,785 @@ namespace Launchpad.Launcher.Handlers.Protocols.Manifest { - /// - /// Base underlying class for protocols using a manifest. - /// - public abstract class ManifestBasedProtocolHandler : PatchProtocolHandler - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - /// - /// The localization catalog. - /// - private static readonly ICatalog LocalizationCatalog = new Catalog("Launchpad", "./Content/locale"); - - private readonly LocalVersionService LocalVersionService = new LocalVersionService(); - - /// - /// The file manifest handler. This allows access to the launcher and game file lists. - /// - private readonly ManifestHandler FileManifestHandler; - - /// - /// Initializes a new instance of the class. - /// - protected ManifestBasedProtocolHandler() - { - this.FileManifestHandler = new ManifestHandler - ( - DirectoryHelpers.GetLocalLauncherDirectory(), - this.Configuration.RemoteAddress, - this.Configuration.SystemTarget - ); - } - - /// - public override void InstallGame() - { - try - { - // Create the .install file to mark that an installation has begun. - // If it exists, do nothing. - this.TagfileService.CreateGameTagfile(); - - // Make sure the manifest is up to date - RefreshModuleManifest(EModule.Game); - - // Download Game - DownloadModule(EModule.Game); - - // Verify Game - VerifyModule(EModule.Game); - } - catch (IOException ioex) - { - Log.Warn("Game installation failed (IOException): " + ioex.Message); - } - - // OnModuleInstallationFinished and OnModuleInstallationFailed is in VerifyGame - // in order to allow it to run as a standalone action, while still keeping this functional. - - // As a side effect, it is required that it is the last action to run in Install and Update, - // which happens to coincide with the general design. - } - - /// - public override void UpdateModule(EModule module) - { - IReadOnlyList manifest; - IReadOnlyList oldManifest; - switch (module) - { - case EModule.Launcher: - { - RefreshModuleManifest(EModule.Launcher); - - manifest = this.FileManifestHandler.GetManifest(EManifestType.Launchpad, false); - oldManifest = this.FileManifestHandler.GetManifest(EManifestType.Launchpad, true); - break; - } - case EModule.Game: - { - RefreshModuleManifest(EModule.Game); - - manifest = this.FileManifestHandler.GetManifest(EManifestType.Game, false); - oldManifest = this.FileManifestHandler.GetManifest(EManifestType.Game, true); - break; - } - default: - { - throw new ArgumentOutOfRangeException(nameof(module), module, "An invalid module value was passed to UpdateModule."); - } - } - - // Check to see if we have valid manifests - if (manifest == null) - { - Log.Error($"No manifest was found when updating the module \"{module}\". The server files may be inaccessible or missing."); - OnModuleInstallationFailed(module); - return; - } - - // This dictionary holds a list of new entries and their equivalents from the old manifest. It is used - // to determine whether or not a file is partial, or merely old yet smaller. - var oldEntriesBeingReplaced = new Dictionary(); - var filesRequiringUpdate = new List(); - foreach (var fileEntry in manifest) - { - filesRequiringUpdate.Add(fileEntry); - if (oldManifest == null) - { - continue; - } - - if (oldManifest.Contains(fileEntry)) - { - continue; - } - - // See if there is an old entry which matches the new one. - var matchingOldEntry = - oldManifest.FirstOrDefault(oldEntry => oldEntry.RelativePath == fileEntry.RelativePath); - - if (matchingOldEntry != null) - { - oldEntriesBeingReplaced.Add(fileEntry, matchingOldEntry); - } - } - - try - { - var updatedFiles = 0; - foreach (var fileEntry in filesRequiringUpdate) - { - ++updatedFiles; - - this.ModuleUpdateProgressArgs.IndicatorLabelMessage = GetUpdateIndicatorLabelMessage - ( - Path.GetFileName(fileEntry.RelativePath), - updatedFiles, - filesRequiringUpdate.Count - ); - OnModuleUpdateProgressChanged(); - - // If we're updating an existing file, make sure to let the downloader know - if (oldEntriesBeingReplaced.ContainsKey(fileEntry)) - { - DownloadManifestEntry(fileEntry, module, oldEntriesBeingReplaced[fileEntry]); - } - else - { - DownloadManifestEntry(fileEntry, module); - } - } - } - catch (IOException ioex) - { - Log.Warn($"Updating of {module} files failed (IOException): " + ioex.Message); - OnModuleInstallationFailed(module); - return; - } - - OnModuleInstallationFinished(module); - } - - /// - public override void VerifyModule(EModule module) - { - var manifest = this.FileManifestHandler.GetManifest((EManifestType)module, false); - var brokenFiles = new List(); - - if (manifest == null) - { - Log.Error($"No manifest was found when verifying the module \"{module}\". The server files may be inaccessible or missing."); - OnModuleInstallationFailed(module); - return; - } - - try - { - var verifiedFiles = 0; - foreach (var fileEntry in manifest) - { - ++verifiedFiles; - - // Prepare the progress event contents - this.ModuleVerifyProgressArgs.IndicatorLabelMessage = GetVerifyIndicatorLabelMessage - ( - Path.GetFileName(fileEntry.RelativePath), - verifiedFiles, - manifest.Count - ); - OnModuleVerifyProgressChanged(); - - if (fileEntry.IsFileIntegrityIntact()) - { - continue; - } - - brokenFiles.Add(fileEntry); - Log.Info($"File \"{Path.GetFileName(fileEntry.RelativePath)}\" failed its integrity check and was queued for redownload."); - } - - var downloadedFiles = 0; - foreach (var fileEntry in brokenFiles) - { - ++downloadedFiles; - - // Prepare the progress event contents - this.ModuleDownloadProgressArgs.IndicatorLabelMessage = GetDownloadIndicatorLabelMessage - ( - Path.GetFileName(fileEntry.RelativePath), - downloadedFiles, - brokenFiles.Count - ); - OnModuleDownloadProgressChanged(); - - for (var i = 0; i < this.Configuration.RemoteFileDownloadRetries; ++i) - { - if (!fileEntry.IsFileIntegrityIntact()) - { - DownloadManifestEntry(fileEntry, module); - Log.Info($"File \"{Path.GetFileName(fileEntry.RelativePath)}\" failed its integrity check again after redownloading. ({i} retries)"); - } - else - { - break; - } - } - } - } - catch (IOException ioex) - { - Log.Warn($"Verification of {module} files failed (IOException): " + ioex.Message); - OnModuleInstallationFailed(module); - } - - OnModuleInstallationFinished(module); - } - - /// - protected override void DownloadModule(EModule module) - { - IReadOnlyList moduleManifest; - switch (module) - { - case EModule.Launcher: - { - RefreshModuleManifest(EModule.Launcher); - - moduleManifest = this.FileManifestHandler.GetManifest(EManifestType.Launchpad, false); - break; - } - case EModule.Game: - { - RefreshModuleManifest(EModule.Game); - - moduleManifest = this.FileManifestHandler.GetManifest(EManifestType.Game, false); - break; - } - default: - { - throw new ArgumentOutOfRangeException - ( - nameof(module), - module, - "An invalid module value was passed to DownloadModule." - ); - } - } - - if (moduleManifest == null) - { - Log.Error($"No manifest was found when installing the module \"{module}\". The server files may be inaccessible or missing."); - OnModuleInstallationFailed(module); - return; - } - - // In order to be able to resume downloading, we check if there is an entry - // stored in the install cookie. - - // Attempt to parse whatever is inside the install cookie - if (ManifestEntry.TryParse(File.ReadAllText(DirectoryHelpers.GetGameTagfilePath()), out var lastDownloadedFile)) - { - // Loop through all the entries in the manifest until we encounter - // an entry which matches the one in the install cookie - foreach (var fileEntry in moduleManifest) - { - if (lastDownloadedFile.Equals(fileEntry)) - { - // Skip all entries before the one we were last at. - moduleManifest = moduleManifest.Skip(moduleManifest.ToList().IndexOf(fileEntry)).ToList(); - } - } - } - - var downloadedFiles = 0; - foreach (var fileEntry in moduleManifest) - { - ++downloadedFiles; - - // Prepare the progress event contents - this.ModuleDownloadProgressArgs.IndicatorLabelMessage = GetDownloadIndicatorLabelMessage - ( - Path.GetFileName(fileEntry.RelativePath), - downloadedFiles, - moduleManifest.Count - ); - OnModuleDownloadProgressChanged(); - - DownloadManifestEntry(fileEntry, module); - } - } - - /// - /// Reads the contents of a remote file as a string. - /// - /// The URL to read. - /// Whether or not to use anonymous credentials. - /// The contents of the file. - protected abstract string ReadRemoteFile(string url, bool useAnonymousLogin = false); - - /// - /// Downloads the contents of the file at the specified url to the specified local path. - /// This method supported resuming a partial file. - /// - /// The URL to download. - /// The local path where the file should be saved. - /// The expected total size of the file. - /// The offset into the file where reading and writing should start. - /// Whether or not to use anonymous credentials. - protected abstract void DownloadRemoteFile - ( - string url, - string localPath, - long totalSize = 0, - long contentOffset = 0, - bool useAnonymousLogin = false - ); - - /// - public override bool IsModuleOutdated(EModule module) - { - try - { - Version local; - Version remote; - - switch (module) - { - case EModule.Launcher: - { - local = this.LocalVersionService.GetLocalLauncherVersion(); - remote = GetRemoteLauncherVersion(); - break; - } - case EModule.Game: - { - local = this.LocalVersionService.GetLocalGameVersion(); - remote = GetRemoteGameVersion(); - break; - } - default: - { - throw new ArgumentOutOfRangeException - ( - nameof(module), - module, - "An invalid module value was passed to IsModuleOutdated." - ); - } - } - - return local < remote; - } - catch (WebException wex) - { - Log.Warn("Unable to determine whether or not the launcher was outdated (WebException): " + wex.Message); - return false; - } - } - - /// - /// Downloads the file referred to by the specifed manifest entry. - /// - /// The entry to download. - /// The module that the entry belongs to. - /// The old entry, if one exists. - /// - /// Will be thrown if the passed to the function is not a valid value. - /// - /// - /// Will be thrown if the local path set in the passed to the function is not a valid value. - /// - protected virtual void DownloadManifestEntry(ManifestEntry fileEntry, EModule module, ManifestEntry oldFileEntry = null) - { - this.ModuleDownloadProgressArgs.Module = module; - - string baseRemoteURL; - string baseLocalPath; - switch (module) - { - case EModule.Launcher: - { - baseRemoteURL = DirectoryHelpers.GetRemoteLauncherBinariesPath(); - baseLocalPath = DirectoryHelpers.GetTempLauncherDownloadPath(); - break; - } - case EModule.Game: - { - baseRemoteURL = DirectoryHelpers.GetRemoteGamePath(); - baseLocalPath = DirectoryHelpers.GetLocalGameDirectory(); - break; - } - default: - { - throw new ArgumentOutOfRangeException - ( - nameof(module), - module, - "An invalid module value was passed to DownloadManifestEntry." - ); - } - } - - // Build the access strings - var remoteURL = $"{baseRemoteURL}/{fileEntry.RelativePath}"; - var localPath = Path.Combine(baseLocalPath, fileEntry.RelativePath); - - // Make sure we have a directory to put the file in - if (!string.IsNullOrEmpty(localPath)) - { - var localPathParentDir = Path.GetDirectoryName(localPath); - if (!Directory.Exists(localPathParentDir)) - { - var parentDir = Path.GetDirectoryName(localPath); - if (!(parentDir is null)) - { - Directory.CreateDirectory(parentDir); - } - } - } - else - { - throw new ArgumentNullException(nameof(localPath), "The local path was null or empty."); - } - - // Reset the cookie - File.WriteAllText(DirectoryHelpers.GetGameTagfilePath(), string.Empty); - - // Write the current file progress to the install cookie - using (TextWriter textWriterProgress = new StreamWriter(DirectoryHelpers.GetGameTagfilePath())) - { - textWriterProgress.WriteLine(fileEntry); - textWriterProgress.Flush(); - } - - // First, let's see if an old file exists, and is valid. - if (oldFileEntry != null) - { - // Check if the file is present, the correct size, and the correct hash - if (oldFileEntry.IsFileIntegrityIntact()) - { - // If it is, delete it. - File.Delete(localPath); - } - } - - if (File.Exists(localPath)) - { - var fileInfo = new FileInfo(localPath); - if (fileInfo.Length != fileEntry.Size) - { - // If the file is partial, resume the download. - if (fileInfo.Length < fileEntry.Size) - { - Log.Info($"Resuming interrupted file \"{Path.GetFileNameWithoutExtension(fileEntry.RelativePath)}\" at byte {fileInfo.Length}."); - DownloadRemoteFile(remoteURL, localPath, fileEntry.Size, fileInfo.Length); - } - else - { - // If it's larger than expected, toss it in the bin and try again. - Log.Info($"Restarting interrupted file \"{Path.GetFileNameWithoutExtension(fileEntry.RelativePath)}\": File bigger than expected."); - - File.Delete(localPath); - DownloadRemoteFile(remoteURL, localPath, fileEntry.Size); - } - } - else - { - string localHash; - using (var fs = File.OpenRead(localPath)) - { - localHash = MD5Handler.GetStreamHash(fs); - } - - if (localHash != fileEntry.Hash) - { - // If the hash doesn't match, toss it in the bin and try again. - Log.Info - ( - $"Redownloading file \"{Path.GetFileNameWithoutExtension(fileEntry.RelativePath)}\": " + - $"Hash sum mismatch. Local: {localHash}, Expected: {fileEntry.Hash}" - ); - - File.Delete(localPath); - DownloadRemoteFile(remoteURL, localPath, fileEntry.Size); - } - } - } - else - { - // No file, download it - DownloadRemoteFile(remoteURL, localPath, fileEntry.Size); - } - - // We've finished the download, so empty the cookie - File.WriteAllText(DirectoryHelpers.GetGameTagfilePath(), string.Empty); - } - - /// - /// Determines whether or not the local copy of the manifest for the specifed module is outdated. - /// - /// The module. - /// true if the manifest is outdated; otherwise, false. - /// - /// Will be thrown if the passed to the function is not a valid value. - /// - protected virtual bool IsModuleManifestOutdated(EModule module) - { - string manifestPath; - switch (module) - { - case EModule.Launcher: - case EModule.Game: - { - manifestPath = this.FileManifestHandler.GetManifestPath((EManifestType)module, false); - break; - } - default: - { - throw new ArgumentOutOfRangeException - ( - nameof(module), - module, - "An invalid module value was passed to RefreshModuleManifest." - ); - } - } - - if (!File.Exists(manifestPath)) - { - return true; - } - - var remoteHash = GetRemoteModuleManifestChecksum(module); - using var file = File.OpenRead(manifestPath); - var localHash = MD5Handler.GetStreamHash(file); - - return remoteHash != localHash; - } - - /// - /// Gets the checksum of the manifest for the specified module. - /// - /// The module. - /// The checksum. - /// - /// Will be thrown if the passed to the function is not a valid value. - /// - protected virtual string GetRemoteModuleManifestChecksum(EModule module) - { - string checksum; - switch (module) - { - case EModule.Launcher: - case EModule.Game: - { - checksum = ReadRemoteFile(this.FileManifestHandler.GetManifestChecksumURL((EManifestType)module)).RemoveLineSeparatorsAndNulls(); - break; - } - default: - { - throw new ArgumentOutOfRangeException - ( - nameof(module), - module, - "An invalid module value was passed to GetRemoteModuleManifestChecksum." - ); - } - } - - return checksum.RemoveLineSeparatorsAndNulls(); - } - - /// - /// Refreshes the current manifest by redownloading it, if required. - /// - /// The module. - /// - /// Will be thrown if the passed to the function is not a valid value. - /// - protected virtual void RefreshModuleManifest(EModule module) - { - bool manifestExists; - switch (module) - { - case EModule.Launcher: - case EModule.Game: - { - manifestExists = File.Exists(this.FileManifestHandler.GetManifestPath((EManifestType)module, false)); - break; - } - default: - { - throw new ArgumentOutOfRangeException - ( - nameof(module), - module, - "An invalid module value was passed to RefreshModuleManifest" - ); - } - } - - if (manifestExists) - { - if (IsModuleManifestOutdated(module)) - { - DownloadModuleManifest(module); - } - } - else - { - DownloadModuleManifest(module); - } - - // Now update the handler instance - this.FileManifestHandler.ReloadManifests((EManifestType)module); - } - - /// - /// Downloads the manifest for the specified module, and backs up the old copy of the manifest. - /// - /// The module. - /// - /// Will be thrown if the passed to the function is not a valid value. - /// - protected virtual void DownloadModuleManifest(EModule module) - { - string remoteURL; - string localPath; - string oldLocalPath; - switch (module) - { - case EModule.Launcher: - case EModule.Game: - { - remoteURL = this.FileManifestHandler.GetManifestURL((EManifestType)module); - localPath = this.FileManifestHandler.GetManifestPath((EManifestType)module, false); - oldLocalPath = this.FileManifestHandler.GetManifestPath((EManifestType)module, true); - - break; - } - default: - { - throw new ArgumentOutOfRangeException - ( - nameof(module), - module, - "An invalid module value was passed to DownloadModuleManifest" - ); - } - } - - try - { - // Delete the old backup (if there is one) - if (File.Exists(oldLocalPath)) - { - File.Delete(oldLocalPath); - } - - // Create a backup of the old manifest so that we can compare them when updating the game - if (File.Exists(localPath)) - { - File.Move(localPath, oldLocalPath); - } - } - catch (IOException ioex) - { - Log.Warn("Failed to back up the old launcher manifest (IOException): " + ioex.Message); - } - - DownloadRemoteFile(remoteURL, localPath); - } - - /// - /// Gets the remote launcher version. - /// - /// The remote launcher version. - /// If the version could not be retrieved from the server, a version of 0.0.0 is returned. - protected virtual Version GetRemoteLauncherVersion() - { - var remoteVersionPath = DirectoryHelpers.GetRemoteLauncherVersionPath(); - var remoteVersion = ReadRemoteFile(remoteVersionPath).RemoveLineSeparatorsAndNulls(); - - if (Version.TryParse(remoteVersion, out var version)) - { - return version; - } - - Log.Warn("Failed to parse the remote launcher version. Using the default of 0.0.0 instead."); - return new Version("0.0.0"); - } - - /// - /// Gets the remote game version. - /// - /// The remote game version. - protected virtual Version GetRemoteGameVersion() - { - var remoteVersionPath = $"{this.Configuration.RemoteAddress}/game/{this.Configuration.SystemTarget}/bin/GameVersion.txt"; - var remoteVersion = ReadRemoteFile(remoteVersionPath).RemoveLineSeparatorsAndNulls(); - - if (Version.TryParse(remoteVersion, out var version)) - { - return version; - } - - Log.Warn("Failed to parse the remote game version. Using the default of 0.0.0 instead."); - return new Version("0.0.0"); - } - - /// - /// Gets the indicator label message to display to the user while repairing. - /// - /// The indicator label message. - /// Current filename. - /// N files downloaded. - /// Total files to download. - protected virtual string GetVerifyIndicatorLabelMessage(string currentFilename, int verifiedFiles, int totalFiles) - { - return LocalizationCatalog.GetString("Verifying file {0} ({1} of {2})", currentFilename, verifiedFiles, totalFiles); - } - - /// - /// Gets the indicator label message to display to the user while repairing. - /// - /// The indicator label message. - /// Current filename. - /// Number of files that have been updated. - /// Total files that are to be updated. - protected virtual string GetUpdateIndicatorLabelMessage(string currentFilename, int updatedFiles, int totalFiles) - { - return LocalizationCatalog.GetString("Updating file {0} ({1} of {2})", currentFilename, updatedFiles, totalFiles); - } - - /// - /// Gets the indicator label message to display to the user while installing. - /// - /// The indicator label message. - /// Current filename. - /// N files downloaded. - /// Total files to download. - protected virtual string GetDownloadIndicatorLabelMessage(string currentFilename, int downloadedFiles, int totalFiles) - { - return LocalizationCatalog.GetString("Downloading file {0} ({1} of {2})", currentFilename, downloadedFiles, totalFiles); - } - - /// - /// Gets the progress bar message. - /// - /// The progress bar message. - /// Filename. - /// Downloaded bytes. - /// Total bytes. - protected virtual string GetDownloadProgressBarMessage(string filename, long downloadedBytes, long totalBytes) - { - return LocalizationCatalog.GetString("Downloading {0}: {1} out of {2}", filename, downloadedBytes, totalBytes); - } - } + /// + /// Base underlying class for protocols using a manifest. + /// + public abstract class ManifestBasedProtocolHandler : PatchProtocolHandler + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + /// + /// The localization catalog. + /// + private static readonly ICatalog LocalizationCatalog = new Catalog("Launchpad", "./Content/locale"); + + private readonly LocalVersionService LocalVersionService = new LocalVersionService(); + + /// + /// The file manifest handler. This allows access to the launcher and game file lists. + /// + private readonly ManifestHandler FileManifestHandler; + + /// + /// Initializes a new instance of the class. + /// + protected ManifestBasedProtocolHandler() + { + this.FileManifestHandler = new ManifestHandler + ( + DirectoryHelpers.GetLocalLauncherDirectory(), + this.Configuration.RemoteAddress, + this.Configuration.SystemTarget + ); + } + + /// + public override void InstallGame() + { + try + { + // Create the .install file to mark that an installation has begun. + // If it exists, do nothing. + this.TagfileService.CreateGameTagfile(); + + // Make sure the manifest is up to date + RefreshModuleManifest(EModule.Game); + + // Download Game + DownloadModule(EModule.Game); + + // Verify Game + VerifyModule(EModule.Game); + } + catch (IOException ioex) + { + Log.Warn("Game installation failed (IOException): " + ioex.Message); + } + + // OnModuleInstallationFinished and OnModuleInstallationFailed is in VerifyGame + // in order to allow it to run as a standalone action, while still keeping this functional. + + // As a side effect, it is required that it is the last action to run in Install and Update, + // which happens to coincide with the general design. + } + + /// + public override void UpdateModule(EModule module) + { + IReadOnlyList manifest; + IReadOnlyList oldManifest; + switch (module) + { + case EModule.Launcher: + { + RefreshModuleManifest(EModule.Launcher); + + manifest = this.FileManifestHandler.GetManifest(EManifestType.Launchpad, false); + oldManifest = this.FileManifestHandler.GetManifest(EManifestType.Launchpad, true); + break; + } + case EModule.Game: + { + RefreshModuleManifest(EModule.Game); + + manifest = this.FileManifestHandler.GetManifest(EManifestType.Game, false); + oldManifest = this.FileManifestHandler.GetManifest(EManifestType.Game, true); + break; + } + default: + { + throw new ArgumentOutOfRangeException(nameof(module), module, "An invalid module value was passed to UpdateModule."); + } + } + + // Check to see if we have valid manifests + if (manifest == null) + { + Log.Error($"No manifest was found when updating the module \"{module}\". The server files may be inaccessible or missing."); + OnModuleInstallationFailed(module); + return; + } + + // This dictionary holds a list of new entries and their equivalents from the old manifest. It is used + // to determine whether or not a file is partial, or merely old yet smaller. + var oldEntriesBeingReplaced = new Dictionary(); + var filesRequiringUpdate = new List(); + foreach (var fileEntry in manifest) + { + filesRequiringUpdate.Add(fileEntry); + if (oldManifest == null) + { + continue; + } + + if (oldManifest.Contains(fileEntry)) + { + continue; + } + + // See if there is an old entry which matches the new one. + var matchingOldEntry = + oldManifest.FirstOrDefault(oldEntry => oldEntry.RelativePath == fileEntry.RelativePath); + + if (matchingOldEntry != null) + { + oldEntriesBeingReplaced.Add(fileEntry, matchingOldEntry); + } + } + + try + { + var updatedFiles = 0; + foreach (var fileEntry in filesRequiringUpdate) + { + ++updatedFiles; + + this.ModuleUpdateProgressArgs.IndicatorLabelMessage = GetUpdateIndicatorLabelMessage + ( + Path.GetFileName(fileEntry.RelativePath), + updatedFiles, + filesRequiringUpdate.Count + ); + OnModuleUpdateProgressChanged(); + + // If we're updating an existing file, make sure to let the downloader know + if (oldEntriesBeingReplaced.ContainsKey(fileEntry)) + { + DownloadManifestEntry(fileEntry, module, oldEntriesBeingReplaced[fileEntry]); + } + else + { + DownloadManifestEntry(fileEntry, module); + } + } + } + catch (IOException ioex) + { + Log.Warn($"Updating of {module} files failed (IOException): " + ioex.Message); + OnModuleInstallationFailed(module); + return; + } + + OnModuleInstallationFinished(module); + } + + /// + public override void VerifyModule(EModule module) + { + var manifest = this.FileManifestHandler.GetManifest((EManifestType)module, false); + var brokenFiles = new List(); + + if (manifest == null) + { + Log.Error($"No manifest was found when verifying the module \"{module}\". The server files may be inaccessible or missing."); + OnModuleInstallationFailed(module); + return; + } + + try + { + var verifiedFiles = 0; + foreach (var fileEntry in manifest) + { + ++verifiedFiles; + + // Prepare the progress event contents + this.ModuleVerifyProgressArgs.IndicatorLabelMessage = GetVerifyIndicatorLabelMessage + ( + Path.GetFileName(fileEntry.RelativePath), + verifiedFiles, + manifest.Count + ); + OnModuleVerifyProgressChanged(); + + if (fileEntry.IsFileIntegrityIntact()) + { + continue; + } + + brokenFiles.Add(fileEntry); + Log.Info($"File \"{Path.GetFileName(fileEntry.RelativePath)}\" failed its integrity check and was queued for redownload."); + } + + var downloadedFiles = 0; + foreach (var fileEntry in brokenFiles) + { + ++downloadedFiles; + + // Prepare the progress event contents + this.ModuleDownloadProgressArgs.IndicatorLabelMessage = GetDownloadIndicatorLabelMessage + ( + Path.GetFileName(fileEntry.RelativePath), + downloadedFiles, + brokenFiles.Count + ); + OnModuleDownloadProgressChanged(); + + for (var i = 0; i < this.Configuration.RemoteFileDownloadRetries; ++i) + { + if (!fileEntry.IsFileIntegrityIntact()) + { + DownloadManifestEntry(fileEntry, module); + Log.Info($"File \"{Path.GetFileName(fileEntry.RelativePath)}\" failed its integrity check again after redownloading. ({i} retries)"); + } + else + { + break; + } + } + } + } + catch (IOException ioex) + { + Log.Warn($"Verification of {module} files failed (IOException): " + ioex.Message); + OnModuleInstallationFailed(module); + } + + OnModuleInstallationFinished(module); + } + + /// + protected override void DownloadModule(EModule module) + { + IReadOnlyList moduleManifest; + switch (module) + { + case EModule.Launcher: + { + RefreshModuleManifest(EModule.Launcher); + + moduleManifest = this.FileManifestHandler.GetManifest(EManifestType.Launchpad, false); + break; + } + case EModule.Game: + { + RefreshModuleManifest(EModule.Game); + + moduleManifest = this.FileManifestHandler.GetManifest(EManifestType.Game, false); + break; + } + default: + { + throw new ArgumentOutOfRangeException + ( + nameof(module), + module, + "An invalid module value was passed to DownloadModule." + ); + } + } + + if (moduleManifest == null) + { + Log.Error($"No manifest was found when installing the module \"{module}\". The server files may be inaccessible or missing."); + OnModuleInstallationFailed(module); + return; + } + + // In order to be able to resume downloading, we check if there is an entry + // stored in the install cookie. + + // Attempt to parse whatever is inside the install cookie + if (ManifestEntry.TryParse(File.ReadAllText(DirectoryHelpers.GetGameTagfilePath()), out var lastDownloadedFile)) + { + // Loop through all the entries in the manifest until we encounter + // an entry which matches the one in the install cookie + foreach (var fileEntry in moduleManifest) + { + if (lastDownloadedFile.Equals(fileEntry)) + { + // Skip all entries before the one we were last at. + moduleManifest = moduleManifest.Skip(moduleManifest.ToList().IndexOf(fileEntry)).ToList(); + } + } + } + + var downloadedFiles = 0; + foreach (var fileEntry in moduleManifest) + { + ++downloadedFiles; + + // Prepare the progress event contents + this.ModuleDownloadProgressArgs.IndicatorLabelMessage = GetDownloadIndicatorLabelMessage + ( + Path.GetFileName(fileEntry.RelativePath), + downloadedFiles, + moduleManifest.Count + ); + OnModuleDownloadProgressChanged(); + + DownloadManifestEntry(fileEntry, module); + } + } + + /// + /// Reads the contents of a remote file as a string. + /// + /// The URL to read. + /// Whether or not to use anonymous credentials. + /// The contents of the file. + protected abstract string ReadRemoteFile(string url, bool useAnonymousLogin = false); + + /// + /// Downloads the contents of the file at the specified url to the specified local path. + /// This method supported resuming a partial file. + /// + /// The URL to download. + /// The local path where the file should be saved. + /// The expected total size of the file. + /// The offset into the file where reading and writing should start. + /// Whether or not to use anonymous credentials. + protected abstract void DownloadRemoteFile + ( + string url, + string localPath, + long totalSize = 0, + long contentOffset = 0, + bool useAnonymousLogin = false + ); + + /// + public override bool IsModuleOutdated(EModule module) + { + try + { + Version local; + Version remote; + + switch (module) + { + case EModule.Launcher: + { + local = this.LocalVersionService.GetLocalLauncherVersion(); + remote = GetRemoteLauncherVersion(); + break; + } + case EModule.Game: + { + local = this.LocalVersionService.GetLocalGameVersion(); + remote = GetRemoteGameVersion(); + break; + } + default: + { + throw new ArgumentOutOfRangeException + ( + nameof(module), + module, + "An invalid module value was passed to IsModuleOutdated." + ); + } + } + + return local < remote; + } + catch (WebException wex) + { + Log.Warn("Unable to determine whether or not the launcher was outdated (WebException): " + wex.Message); + return false; + } + } + + /// + /// Downloads the file referred to by the specifed manifest entry. + /// + /// The entry to download. + /// The module that the entry belongs to. + /// The old entry, if one exists. + /// + /// Will be thrown if the passed to the function is not a valid value. + /// + /// + /// Will be thrown if the local path set in the passed to the function is not a valid value. + /// + protected virtual void DownloadManifestEntry(ManifestEntry fileEntry, EModule module, ManifestEntry oldFileEntry = null) + { + this.ModuleDownloadProgressArgs.Module = module; + + string baseRemoteURL; + string baseLocalPath; + switch (module) + { + case EModule.Launcher: + { + baseRemoteURL = DirectoryHelpers.GetRemoteLauncherBinariesPath(); + baseLocalPath = DirectoryHelpers.GetTempLauncherDownloadPath(); + break; + } + case EModule.Game: + { + baseRemoteURL = DirectoryHelpers.GetRemoteGamePath(); + baseLocalPath = DirectoryHelpers.GetLocalGameDirectory(); + break; + } + default: + { + throw new ArgumentOutOfRangeException + ( + nameof(module), + module, + "An invalid module value was passed to DownloadManifestEntry." + ); + } + } + + // Build the access strings + var remoteURL = $"{baseRemoteURL}/{fileEntry.RelativePath}"; + var localPath = Path.Combine(baseLocalPath, fileEntry.RelativePath); + + // Make sure we have a directory to put the file in + if (!string.IsNullOrEmpty(localPath)) + { + var localPathParentDir = Path.GetDirectoryName(localPath); + if (!Directory.Exists(localPathParentDir)) + { + var parentDir = Path.GetDirectoryName(localPath); + if (!(parentDir is null)) + { + Directory.CreateDirectory(parentDir); + } + } + } + else + { + throw new ArgumentNullException(nameof(localPath), "The local path was null or empty."); + } + + // Reset the cookie + File.WriteAllText(DirectoryHelpers.GetGameTagfilePath(), string.Empty); + + // Write the current file progress to the install cookie + using (TextWriter textWriterProgress = new StreamWriter(DirectoryHelpers.GetGameTagfilePath())) + { + textWriterProgress.WriteLine(fileEntry); + textWriterProgress.Flush(); + } + + // First, let's see if an old file exists, and is valid. + if (oldFileEntry != null) + { + // Check if the file is present, the correct size, and the correct hash + if (oldFileEntry.IsFileIntegrityIntact()) + { + // If it is, delete it. + File.Delete(localPath); + } + } + + if (File.Exists(localPath)) + { + var fileInfo = new FileInfo(localPath); + if (fileInfo.Length != fileEntry.Size) + { + // If the file is partial, resume the download. + if (fileInfo.Length < fileEntry.Size) + { + Log.Info($"Resuming interrupted file \"{Path.GetFileNameWithoutExtension(fileEntry.RelativePath)}\" at byte {fileInfo.Length}."); + DownloadRemoteFile(remoteURL, localPath, fileEntry.Size, fileInfo.Length); + } + else + { + // If it's larger than expected, toss it in the bin and try again. + Log.Info($"Restarting interrupted file \"{Path.GetFileNameWithoutExtension(fileEntry.RelativePath)}\": File bigger than expected."); + + File.Delete(localPath); + DownloadRemoteFile(remoteURL, localPath, fileEntry.Size); + } + } + else + { + string localHash; + using (var fs = File.OpenRead(localPath)) + { + localHash = MD5Handler.GetStreamHash(fs); + } + + if (localHash != fileEntry.Hash) + { + // If the hash doesn't match, toss it in the bin and try again. + Log.Info + ( + $"Redownloading file \"{Path.GetFileNameWithoutExtension(fileEntry.RelativePath)}\": " + + $"Hash sum mismatch. Local: {localHash}, Expected: {fileEntry.Hash}" + ); + + File.Delete(localPath); + DownloadRemoteFile(remoteURL, localPath, fileEntry.Size); + } + } + } + else + { + // No file, download it + DownloadRemoteFile(remoteURL, localPath, fileEntry.Size); + } + + // We've finished the download, so empty the cookie + File.WriteAllText(DirectoryHelpers.GetGameTagfilePath(), string.Empty); + } + + /// + /// Determines whether or not the local copy of the manifest for the specifed module is outdated. + /// + /// The module. + /// true if the manifest is outdated; otherwise, false. + /// + /// Will be thrown if the passed to the function is not a valid value. + /// + protected virtual bool IsModuleManifestOutdated(EModule module) + { + string manifestPath; + switch (module) + { + case EModule.Launcher: + case EModule.Game: + { + manifestPath = this.FileManifestHandler.GetManifestPath((EManifestType)module, false); + break; + } + default: + { + throw new ArgumentOutOfRangeException + ( + nameof(module), + module, + "An invalid module value was passed to RefreshModuleManifest." + ); + } + } + + if (!File.Exists(manifestPath)) + { + return true; + } + + var remoteHash = GetRemoteModuleManifestChecksum(module); + using var file = File.OpenRead(manifestPath); + var localHash = MD5Handler.GetStreamHash(file); + + return remoteHash != localHash; + } + + /// + /// Gets the checksum of the manifest for the specified module. + /// + /// The module. + /// The checksum. + /// + /// Will be thrown if the passed to the function is not a valid value. + /// + protected virtual string GetRemoteModuleManifestChecksum(EModule module) + { + string checksum; + switch (module) + { + case EModule.Launcher: + case EModule.Game: + { + checksum = ReadRemoteFile(this.FileManifestHandler.GetManifestChecksumURL((EManifestType)module)).RemoveLineSeparatorsAndNulls(); + break; + } + default: + { + throw new ArgumentOutOfRangeException + ( + nameof(module), + module, + "An invalid module value was passed to GetRemoteModuleManifestChecksum." + ); + } + } + + return checksum.RemoveLineSeparatorsAndNulls(); + } + + /// + /// Refreshes the current manifest by redownloading it, if required. + /// + /// The module. + /// + /// Will be thrown if the passed to the function is not a valid value. + /// + protected virtual void RefreshModuleManifest(EModule module) + { + bool manifestExists; + switch (module) + { + case EModule.Launcher: + case EModule.Game: + { + manifestExists = File.Exists(this.FileManifestHandler.GetManifestPath((EManifestType)module, false)); + break; + } + default: + { + throw new ArgumentOutOfRangeException + ( + nameof(module), + module, + "An invalid module value was passed to RefreshModuleManifest" + ); + } + } + + if (manifestExists) + { + if (IsModuleManifestOutdated(module)) + { + DownloadModuleManifest(module); + } + } + else + { + DownloadModuleManifest(module); + } + + // Now update the handler instance + this.FileManifestHandler.ReloadManifests((EManifestType)module); + } + + /// + /// Downloads the manifest for the specified module, and backs up the old copy of the manifest. + /// + /// The module. + /// + /// Will be thrown if the passed to the function is not a valid value. + /// + protected virtual void DownloadModuleManifest(EModule module) + { + string remoteURL; + string localPath; + string oldLocalPath; + switch (module) + { + case EModule.Launcher: + case EModule.Game: + { + remoteURL = this.FileManifestHandler.GetManifestURL((EManifestType)module); + localPath = this.FileManifestHandler.GetManifestPath((EManifestType)module, false); + oldLocalPath = this.FileManifestHandler.GetManifestPath((EManifestType)module, true); + + break; + } + default: + { + throw new ArgumentOutOfRangeException + ( + nameof(module), + module, + "An invalid module value was passed to DownloadModuleManifest" + ); + } + } + + try + { + // Delete the old backup (if there is one) + if (File.Exists(oldLocalPath)) + { + File.Delete(oldLocalPath); + } + + // Create a backup of the old manifest so that we can compare them when updating the game + if (File.Exists(localPath)) + { + File.Move(localPath, oldLocalPath); + } + } + catch (IOException ioex) + { + Log.Warn("Failed to back up the old launcher manifest (IOException): " + ioex.Message); + } + + DownloadRemoteFile(remoteURL, localPath); + } + + /// + /// Gets the remote launcher version. + /// + /// The remote launcher version. + /// If the version could not be retrieved from the server, a version of 0.0.0 is returned. + protected virtual Version GetRemoteLauncherVersion() + { + var remoteVersionPath = DirectoryHelpers.GetRemoteLauncherVersionPath(); + var remoteVersion = ReadRemoteFile(remoteVersionPath).RemoveLineSeparatorsAndNulls(); + + if (Version.TryParse(remoteVersion, out var version)) + { + return version; + } + + Log.Warn("Failed to parse the remote launcher version. Using the default of 0.0.0 instead."); + return new Version("0.0.0"); + } + + /// + /// Gets the remote game version. + /// + /// The remote game version. + protected virtual Version GetRemoteGameVersion() + { + var remoteVersionPath = $"{this.Configuration.RemoteAddress}/game/{this.Configuration.SystemTarget}/bin/GameVersion.txt"; + var remoteVersion = ReadRemoteFile(remoteVersionPath).RemoveLineSeparatorsAndNulls(); + + if (Version.TryParse(remoteVersion, out var version)) + { + return version; + } + + Log.Warn("Failed to parse the remote game version. Using the default of 0.0.0 instead."); + return new Version("0.0.0"); + } + + /// + /// Gets the indicator label message to display to the user while repairing. + /// + /// The indicator label message. + /// Current filename. + /// N files downloaded. + /// Total files to download. + protected virtual string GetVerifyIndicatorLabelMessage(string currentFilename, int verifiedFiles, int totalFiles) + { + return LocalizationCatalog.GetString("Verifying file {0} ({1} of {2})", currentFilename, verifiedFiles, totalFiles); + } + + /// + /// Gets the indicator label message to display to the user while repairing. + /// + /// The indicator label message. + /// Current filename. + /// Number of files that have been updated. + /// Total files that are to be updated. + protected virtual string GetUpdateIndicatorLabelMessage(string currentFilename, int updatedFiles, int totalFiles) + { + return LocalizationCatalog.GetString("Updating file {0} ({1} of {2})", currentFilename, updatedFiles, totalFiles); + } + + /// + /// Gets the indicator label message to display to the user while installing. + /// + /// The indicator label message. + /// Current filename. + /// N files downloaded. + /// Total files to download. + protected virtual string GetDownloadIndicatorLabelMessage(string currentFilename, int downloadedFiles, int totalFiles) + { + return LocalizationCatalog.GetString("Downloading file {0} ({1} of {2})", currentFilename, downloadedFiles, totalFiles); + } + + /// + /// Gets the progress bar message. + /// + /// The progress bar message. + /// Filename. + /// Downloaded bytes. + /// Total bytes. + protected virtual string GetDownloadProgressBarMessage(string filename, long downloadedBytes, long totalBytes) + { + return LocalizationCatalog.GetString("Downloading {0}: {1} out of {2}", filename, downloadedBytes, totalBytes); + } + } } diff --git a/Launchpad.Launcher/Handlers/Protocols/ModuleProgressChangedArgs.cs b/Launchpad.Launcher/Handlers/Protocols/ModuleProgressChangedArgs.cs index e24ca3e7..444d7e80 100644 --- a/Launchpad.Launcher/Handlers/Protocols/ModuleProgressChangedArgs.cs +++ b/Launchpad.Launcher/Handlers/Protocols/ModuleProgressChangedArgs.cs @@ -24,45 +24,45 @@ namespace Launchpad.Launcher.Handlers.Protocols { - /// - /// Event arguments for changing modules. - /// - public sealed class ModuleProgressChangedArgs : EventArgs - { - /// - /// Gets or sets the module that is being changed. - /// - public EModule Module - { - get; - set; - } + /// + /// Event arguments for changing modules. + /// + public sealed class ModuleProgressChangedArgs : EventArgs + { + /// + /// Gets or sets the module that is being changed. + /// + public EModule Module + { + get; + set; + } - /// - /// Gets or sets the message that should be displayed on the progress bar. - /// - public string ProgressBarMessage - { - get; - set; - } + /// + /// Gets or sets the message that should be displayed on the progress bar. + /// + public string ProgressBarMessage + { + get; + set; + } - /// - /// Gets or sets the message that should be displayed on the indicator label. - /// - public string IndicatorLabelMessage - { - get; - set; - } + /// + /// Gets or sets the message that should be displayed on the indicator label. + /// + public string IndicatorLabelMessage + { + get; + set; + } - /// - /// Gets or sets the fractional progress (in the range 0 to 1). - /// - public double ProgressFraction - { - get; - set; - } - } + /// + /// Gets or sets the fractional progress (in the range 0 to 1). + /// + public double ProgressFraction + { + get; + set; + } + } } diff --git a/Launchpad.Launcher/Handlers/Protocols/PatchProtocolHandler.cs b/Launchpad.Launcher/Handlers/Protocols/PatchProtocolHandler.cs index fd47299b..fb9f45e2 100644 --- a/Launchpad.Launcher/Handlers/Protocols/PatchProtocolHandler.cs +++ b/Launchpad.Launcher/Handlers/Protocols/PatchProtocolHandler.cs @@ -31,216 +31,216 @@ namespace Launchpad.Launcher.Handlers.Protocols { - /// - /// Patch protocol handler. - /// This class is the base class for all file transfer protocols, providing - /// a common framework for protocols to adhere to. It abstracts away the actual - /// functionality, and reduces the communication with other parts of the launcher - /// down to requests in, files out. - /// - /// By default, the patch protocol handler does not know anything specific about - /// the actual workings of the protocol. - /// - public abstract class PatchProtocolHandler - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - /// - /// Gets the config handler reference. - /// - protected ConfigHandler Config { get; } = ConfigHandler.Instance; - - /// - /// Gets the configuration instance. - /// - protected ILaunchpadConfiguration Configuration { get; } = ConfigHandler.Instance.Configuration; - - /// - /// Raised whenever the download progress of a module changes. - /// - public event EventHandler ModuleDownloadProgressChanged; - - /// - /// Raised whenever the verification progress of a module changes. - /// - public event EventHandler ModuleVerifyProgressChanged; - - /// - /// Raised whenever the update progress of a module changes. - /// - public event EventHandler ModuleUpdateProgressChanged; - - /// - /// Raised whenever the installation of a module finishes. - /// - public event EventHandler ModuleInstallationFinished; - - /// - /// Raised whenver the installation of a module fails. - /// - public event EventHandler ModuleInstallationFailed; - - /// - /// Gets the download progress arguments. - /// - protected ModuleProgressChangedArgs ModuleDownloadProgressArgs { get; } - - /// - /// Gets the verification progress arguments. - /// - protected ModuleProgressChangedArgs ModuleVerifyProgressArgs { get; } - - /// - /// Gets the update progress arguments. - /// - protected ModuleProgressChangedArgs ModuleUpdateProgressArgs { get; } - - /// - /// Gets the tagfile service. - /// - protected TagfileService TagfileService { get; } - - /// - /// Initializes a new instance of the class. - /// - protected PatchProtocolHandler() - { - this.ModuleDownloadProgressArgs = new ModuleProgressChangedArgs(); - this.ModuleVerifyProgressArgs = new ModuleProgressChangedArgs(); - this.ModuleUpdateProgressArgs = new ModuleProgressChangedArgs(); - - this.TagfileService = new TagfileService(); - } - - /// - /// Determines whether this instance can provide patches. Checks for an active connection to the - /// patch provider (file server, distributed hash tables, hyperspace compression waves etc.) - /// - /// true if this instance can provide patches; otherwise, false. - public abstract bool CanPatch(); - - /// - /// Determines whether the protocol can provide patches and updates for the provided platform. - /// - /// The platform to check. - /// true if the platform is available; otherwise, false. - public abstract bool IsPlatformAvailable(ESystemTarget platform); - - /// - /// Determines whether this protocol can provide access to a banner for the game. - /// - /// true if this instance can provide banner; otherwise, false. - public abstract bool CanProvideBanner(); - - /// - /// Gets the changelog. - /// - /// The changelog. - public abstract string GetChangelogMarkup(); - - /// - /// Gets the banner. - /// - /// The banner. - public abstract Image GetBanner(); - - /// - /// Determines whether or not the specified module is outdated. - /// - /// The module. - /// true if the module is outdated; otherwise, false. - public abstract bool IsModuleOutdated(EModule module); - - /// - /// Installs the game. - /// - public virtual void InstallGame() - { - try - { - // Create the .install file to mark that an installation has begun. - // If it exists, do nothing. - this.TagfileService.CreateGameTagfile(); - - // Download Game - DownloadModule(EModule.Game); - - // Verify Game - VerifyModule(EModule.Game); - } - catch (IOException ioex) - { - Log.Warn("Game installation failed (IOException): " + ioex.Message); - } - - // OnModuleInstallationFinished and OnModuleInstallationFailed is in VerifyGame - // in order to allow it to run as a standalone action, while still keeping this functional. - - // As a side effect, it is required that it is the last action to run in Install and Update, - // which happens to coincide with the general design. - } - - /// - /// Downloads the latest version of the specified module. - /// - /// The module. - protected abstract void DownloadModule(EModule module); - - /// - /// Updates the specified module to the latest version. - /// - /// The module to update. - public abstract void UpdateModule(EModule module); - - /// - /// Verifies and repairs the files of the specified module. - /// - /// The module. - public abstract void VerifyModule(EModule module); - - /// - /// Invoke the event. - /// - protected void OnModuleDownloadProgressChanged() - { - this.ModuleDownloadProgressChanged?.Invoke(this, this.ModuleDownloadProgressArgs); - } - - /// - /// Invoke the event. - /// - protected void OnModuleVerifyProgressChanged() - { - this.ModuleVerifyProgressChanged?.Invoke(this, this.ModuleVerifyProgressArgs); - } - - /// - /// Invoke the event. - /// - protected void OnModuleUpdateProgressChanged() - { - this.ModuleUpdateProgressChanged?.Invoke(this, this.ModuleUpdateProgressArgs); - } - - /// - /// Invoke the event. - /// - /// The module that finished. - protected void OnModuleInstallationFinished(EModule module) - { - this.ModuleInstallationFinished?.Invoke(this, module); - } - - /// - /// Invoke the event. - /// - /// The module that failed. - protected void OnModuleInstallationFailed(EModule module) - { - this.ModuleInstallationFailed?.Invoke(this, module); - } - } + /// + /// Patch protocol handler. + /// This class is the base class for all file transfer protocols, providing + /// a common framework for protocols to adhere to. It abstracts away the actual + /// functionality, and reduces the communication with other parts of the launcher + /// down to requests in, files out. + /// + /// By default, the patch protocol handler does not know anything specific about + /// the actual workings of the protocol. + /// + public abstract class PatchProtocolHandler + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + /// + /// Gets the config handler reference. + /// + protected ConfigHandler Config { get; } = ConfigHandler.Instance; + + /// + /// Gets the configuration instance. + /// + protected ILaunchpadConfiguration Configuration { get; } = ConfigHandler.Instance.Configuration; + + /// + /// Raised whenever the download progress of a module changes. + /// + public event EventHandler ModuleDownloadProgressChanged; + + /// + /// Raised whenever the verification progress of a module changes. + /// + public event EventHandler ModuleVerifyProgressChanged; + + /// + /// Raised whenever the update progress of a module changes. + /// + public event EventHandler ModuleUpdateProgressChanged; + + /// + /// Raised whenever the installation of a module finishes. + /// + public event EventHandler ModuleInstallationFinished; + + /// + /// Raised whenver the installation of a module fails. + /// + public event EventHandler ModuleInstallationFailed; + + /// + /// Gets the download progress arguments. + /// + protected ModuleProgressChangedArgs ModuleDownloadProgressArgs { get; } + + /// + /// Gets the verification progress arguments. + /// + protected ModuleProgressChangedArgs ModuleVerifyProgressArgs { get; } + + /// + /// Gets the update progress arguments. + /// + protected ModuleProgressChangedArgs ModuleUpdateProgressArgs { get; } + + /// + /// Gets the tagfile service. + /// + protected TagfileService TagfileService { get; } + + /// + /// Initializes a new instance of the class. + /// + protected PatchProtocolHandler() + { + this.ModuleDownloadProgressArgs = new ModuleProgressChangedArgs(); + this.ModuleVerifyProgressArgs = new ModuleProgressChangedArgs(); + this.ModuleUpdateProgressArgs = new ModuleProgressChangedArgs(); + + this.TagfileService = new TagfileService(); + } + + /// + /// Determines whether this instance can provide patches. Checks for an active connection to the + /// patch provider (file server, distributed hash tables, hyperspace compression waves etc.) + /// + /// true if this instance can provide patches; otherwise, false. + public abstract bool CanPatch(); + + /// + /// Determines whether the protocol can provide patches and updates for the provided platform. + /// + /// The platform to check. + /// true if the platform is available; otherwise, false. + public abstract bool IsPlatformAvailable(ESystemTarget platform); + + /// + /// Determines whether this protocol can provide access to a banner for the game. + /// + /// true if this instance can provide banner; otherwise, false. + public abstract bool CanProvideBanner(); + + /// + /// Gets the changelog. + /// + /// The changelog. + public abstract string GetChangelogMarkup(); + + /// + /// Gets the banner. + /// + /// The banner. + public abstract Image GetBanner(); + + /// + /// Determines whether or not the specified module is outdated. + /// + /// The module. + /// true if the module is outdated; otherwise, false. + public abstract bool IsModuleOutdated(EModule module); + + /// + /// Installs the game. + /// + public virtual void InstallGame() + { + try + { + // Create the .install file to mark that an installation has begun. + // If it exists, do nothing. + this.TagfileService.CreateGameTagfile(); + + // Download Game + DownloadModule(EModule.Game); + + // Verify Game + VerifyModule(EModule.Game); + } + catch (IOException ioex) + { + Log.Warn("Game installation failed (IOException): " + ioex.Message); + } + + // OnModuleInstallationFinished and OnModuleInstallationFailed is in VerifyGame + // in order to allow it to run as a standalone action, while still keeping this functional. + + // As a side effect, it is required that it is the last action to run in Install and Update, + // which happens to coincide with the general design. + } + + /// + /// Downloads the latest version of the specified module. + /// + /// The module. + protected abstract void DownloadModule(EModule module); + + /// + /// Updates the specified module to the latest version. + /// + /// The module to update. + public abstract void UpdateModule(EModule module); + + /// + /// Verifies and repairs the files of the specified module. + /// + /// The module. + public abstract void VerifyModule(EModule module); + + /// + /// Invoke the event. + /// + protected void OnModuleDownloadProgressChanged() + { + this.ModuleDownloadProgressChanged?.Invoke(this, this.ModuleDownloadProgressArgs); + } + + /// + /// Invoke the event. + /// + protected void OnModuleVerifyProgressChanged() + { + this.ModuleVerifyProgressChanged?.Invoke(this, this.ModuleVerifyProgressArgs); + } + + /// + /// Invoke the event. + /// + protected void OnModuleUpdateProgressChanged() + { + this.ModuleUpdateProgressChanged?.Invoke(this, this.ModuleUpdateProgressArgs); + } + + /// + /// Invoke the event. + /// + /// The module that finished. + protected void OnModuleInstallationFinished(EModule module) + { + this.ModuleInstallationFinished?.Invoke(this, module); + } + + /// + /// Invoke the event. + /// + /// The module that failed. + protected void OnModuleInstallationFailed(EModule module) + { + this.ModuleInstallationFailed?.Invoke(this, module); + } + } } diff --git a/Launchpad.Launcher/Interface/MainWindow.UI.cs b/Launchpad.Launcher/Interface/MainWindow.UI.cs index a68f9bb3..1578647d 100644 --- a/Launchpad.Launcher/Interface/MainWindow.UI.cs +++ b/Launchpad.Launcher/Interface/MainWindow.UI.cs @@ -35,74 +35,74 @@ namespace Launchpad.Launcher.Interface { - /// - /// Interface elements for the widget. - /// - public partial class MainWindow - { - [UIElement] private readonly ImageMenuItem MenuRepairItem; - [UIElement] private readonly ImageMenuItem MenuReinstallItem; - [UIElement] private readonly ImageMenuItem MenuAboutItem; + /// + /// Interface elements for the widget. + /// + public partial class MainWindow + { + [UIElement] private readonly ImageMenuItem MenuRepairItem; + [UIElement] private readonly ImageMenuItem MenuReinstallItem; + [UIElement] private readonly ImageMenuItem MenuAboutItem; - [UIElement] private readonly TextView ChangelogTextView; - [UIElement] private readonly Image BannerImage; + [UIElement] private readonly TextView ChangelogTextView; + [UIElement] private readonly Image BannerImage; - [UIElement] private readonly Label StatusLabel; + [UIElement] private readonly Label StatusLabel; - [UIElement] private readonly ProgressBar MainProgressBar; - [UIElement] private readonly Button MainButton; + [UIElement] private readonly ProgressBar MainProgressBar; + [UIElement] private readonly Button MainButton; - /// - /// Creates a new instance of the class, loading its interface definition from file. - /// - /// An instance of the main window widget. - public static MainWindow Create() - { - using var builder = new Builder(Assembly.GetExecutingAssembly(), "Launchpad.Launcher.Interface.Launchpad.glade", null); - var window = new MainWindow(builder, builder.GetObject(nameof(MainWindow)).Handle) - { - Icon = ResourceManager.ApplicationIcon - }; + /// + /// Creates a new instance of the class, loading its interface definition from file. + /// + /// An instance of the main window widget. + public static MainWindow Create() + { + using var builder = new Builder(Assembly.GetExecutingAssembly(), "Launchpad.Launcher.Interface.Launchpad.glade", null); + var window = new MainWindow(builder, builder.GetObject(nameof(MainWindow)).Handle) + { + Icon = ResourceManager.ApplicationIcon + }; - return window; - } + return window; + } - /// - /// Binds UI-related events. - /// - private void BindUIEvents() - { - this.DeleteEvent += OnDeleteEvent; - this.MenuReinstallItem.Activated += OnReinstallGameActionActivated; - this.MenuRepairItem.Activated += OnMenuRepairItemActivated; - this.MenuAboutItem.Activated += OnMenuAboutItemActivated; + /// + /// Binds UI-related events. + /// + private void BindUIEvents() + { + this.DeleteEvent += OnDeleteEvent; + this.MenuReinstallItem.Activated += OnReinstallGameActionActivated; + this.MenuRepairItem.Activated += OnMenuRepairItemActivated; + this.MenuAboutItem.Activated += OnMenuAboutItemActivated; - this.MainButton.Clicked += OnMainButtonClicked; - } + this.MainButton.Clicked += OnMainButtonClicked; + } - /// - /// Displays the about window to the user. - /// - /// The sending object. - /// The event args. - private void OnMenuAboutItemActivated(object sender, EventArgs e) - { - using var builder = new Builder(Assembly.GetExecutingAssembly(), "Launchpad.Launcher.Interface.Launchpad.glade", null); - using var dialog = new AboutDialog(builder.GetObject("MainAboutDialog").Handle); - dialog.Icon = ResourceManager.ApplicationIcon; - dialog.Logo = ResourceManager.ApplicationIcon; - dialog.Run(); - } + /// + /// Displays the about window to the user. + /// + /// The sending object. + /// The event args. + private void OnMenuAboutItemActivated(object sender, EventArgs e) + { + using var builder = new Builder(Assembly.GetExecutingAssembly(), "Launchpad.Launcher.Interface.Launchpad.glade", null); + using var dialog = new AboutDialog(builder.GetObject("MainAboutDialog").Handle); + dialog.Icon = ResourceManager.ApplicationIcon; + dialog.Logo = ResourceManager.ApplicationIcon; + dialog.Run(); + } - /// - /// Exits the application properly when the window is deleted. - /// - /// Sender. - /// The alpha component. - private static void OnDeleteEvent(object sender, DeleteEventArgs a) - { - Application.Quit(); - a.RetVal = true; - } - } + /// + /// Exits the application properly when the window is deleted. + /// + /// Sender. + /// The alpha component. + private static void OnDeleteEvent(object sender, DeleteEventArgs a) + { + Application.Quit(); + a.RetVal = true; + } + } } diff --git a/Launchpad.Launcher/Interface/MainWindow.cs b/Launchpad.Launcher/Interface/MainWindow.cs index 3be500f8..77866971 100644 --- a/Launchpad.Launcher/Interface/MainWindow.cs +++ b/Launchpad.Launcher/Interface/MainWindow.cs @@ -43,642 +43,642 @@ namespace Launchpad.Launcher.Interface { - /// - /// The main UI class for Launchpad. This class acts as a manager for all threaded - /// actions, such as installing, updating or repairing the game. - /// - public sealed partial class MainWindow : Gtk.Window - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - private readonly LocalVersionService LocalVersionService = new LocalVersionService(); - - /// - /// The configuration instance reference. - /// - private readonly ILaunchpadConfiguration Configuration = ConfigHandler.Instance.Configuration; - - /// - /// The checks handler reference. - /// - private readonly ChecksHandler Checks = new ChecksHandler(); - - /// - /// The launcher handler. Allows updating the launcher and loading the changelog. - /// - private readonly LauncherHandler Launcher = new LauncherHandler(); - - /// - /// The game handler. Allows updating, installing and repairing the game. - /// - private readonly GameHandler Game = new GameHandler(); - - private readonly TagfileService TagfileService = new TagfileService(); - - /// - /// The current mode that the launcher is in. Determines what the primary button does when pressed. - /// - private ELauncherMode Mode = ELauncherMode.Inactive; - - /// - /// The localization catalog. - /// - private static readonly ICatalog LocalizationCatalog = new Catalog("Launchpad", "./Content/locale"); - - /// - /// Whether or not the launcher UI has been initialized. - /// - private bool IsInitialized; - - /// - /// Initializes a new instance of the class. - /// - /// The UI builder. - /// The native handle of the window. - private MainWindow(Builder builder, IntPtr handle) - : base(handle) - { - builder.Autoconnect(this); - - BindUIEvents(); - - // Bind the handler events - this.Game.ProgressChanged += OnModuleInstallationProgressChanged; - this.Game.DownloadFinished += OnGameDownloadFinished; - this.Game.DownloadFailed += OnGameDownloadFailed; - this.Game.LaunchFailed += OnGameLaunchFailed; - this.Game.GameExited += OnGameExited; - - this.Launcher.LauncherDownloadProgressChanged += OnModuleInstallationProgressChanged; - this.Launcher.LauncherDownloadFinished += OnLauncherDownloadFinished; - - // Set the initial launcher mode - SetLauncherMode(ELauncherMode.Inactive, false); - - // Set the window title - this.Title = LocalizationCatalog.GetString("Launchpad - {0}", this.Configuration.GameName); - this.StatusLabel.Text = LocalizationCatalog.GetString("Idle"); - } - - /// - /// Initializes the UI of the launcher, performing varying checks against the patching server. - /// - /// A task that must be awaited. - public Task InitializeAsync() - { - if (this.IsInitialized) - { - return Task.CompletedTask; - } - - // First of all, check if we can connect to the patching service. - if (!this.Checks.CanPatch()) - { - using (var dialog = new MessageDialog - ( - this, - DialogFlags.Modal, - MessageType.Warning, - ButtonsType.Ok, - LocalizationCatalog.GetString("Failed to connect to the patch server. Please check your settings.") - )) - { - dialog.Run(); - } - - this.StatusLabel.Text = LocalizationCatalog.GetString("Could not connect to server."); - this.MenuRepairItem.Sensitive = false; - } - else - { - LoadBanner(); - - LoadChangelog(); - - // If we can connect, proceed with the rest of our checks. - if (ChecksHandler.IsInitialStartup()) - { - DisplayInitialStartupDialog(); - } - - // If the launcher does not need an update at this point, we can continue checks for the game - if (!this.Checks.IsLauncherOutdated()) - { - if (!this.Checks.IsPlatformAvailable(this.Configuration.SystemTarget)) - { - Log.Info - ( - $"The server does not provide files for platform \"{PlatformHelpers.GetCurrentPlatform()}\". " + - "A .provides file must be present in the platforms' root directory." - ); - - SetLauncherMode(ELauncherMode.Inactive, false); - } - else - { - if (!this.Checks.IsGameInstalled()) - { - // If the game is not installed, offer to install it - Log.Info("The game has not yet been installed."); - SetLauncherMode(ELauncherMode.Install, false); - - // Since the game has not yet been installed, disallow manual repairs - this.MenuRepairItem.Sensitive = false; - - // and reinstalls - this.MenuReinstallItem.Sensitive = false; - } - else - { - // If the game is installed (which it should be at this point), check if it needs to be updated - if (this.Checks.IsGameOutdated()) - { - // If it does, offer to update it - Log.Info("The game is outdated."); - SetLauncherMode(ELauncherMode.Update, false); - } - else - { - // All checks passed, so we can offer to launch the game. - Log.Info("All checks passed. Game can be launched."); - SetLauncherMode(ELauncherMode.Launch, false); - } - } - } - } - else - { - // The launcher was outdated. - Log.Info($"The launcher is outdated. \n\tLocal version: {this.LocalVersionService.GetLocalLauncherVersion()}"); - SetLauncherMode(ELauncherMode.Update, false); - } - } - - this.IsInitialized = true; - return Task.CompletedTask; - } - - private void LoadChangelog() - { - var protocol = PatchProtocolProvider.GetHandler(); - var markup = protocol.GetChangelogMarkup(); - - // Preprocess dot lists - var dotRegex = new Regex("(?<=^\\s+)\\*", RegexOptions.Multiline); - markup = dotRegex.Replace(markup, "•"); - - // Preprocess line breaks - var regex = new Regex("(? - { - // Fetch the banner from the server - var bannerImage = patchHandler.GetBanner(); - - bannerImage.Mutate(i => i.Resize(this.BannerImage.AllocatedWidth, 0)); - - // Load the image into a pixel buffer - return new Pixbuf - ( - Bytes.NewStatic(bannerImage.SavePixelData()), - Colorspace.Rgb, - true, - 8, - bannerImage.Width, - bannerImage.Height, - 4 * bannerImage.Width - ); - } - ) - .ContinueWith - ( - async bannerTask => this.BannerImage.Pixbuf = await bannerTask - ); - } - - /// - /// Sets the launcher mode and updates UI elements to match. - /// - /// The new mode. - /// If set to true, the selected mode is in progress. - /// - /// Will be thrown if the passed to the function is not a valid value. - /// - private void SetLauncherMode(ELauncherMode newMode, bool isInProgress) - { - // Set the global launcher mode - this.Mode = newMode; - - // Set the UI elements to match - switch (newMode) - { - case ELauncherMode.Install: - { - if (isInProgress) - { - this.MainButton.Sensitive = false; - this.MainButton.Label = LocalizationCatalog.GetString("Installing..."); - } - else - { - this.MainButton.Sensitive = true; - this.MainButton.Label = LocalizationCatalog.GetString("Install"); - } - break; - } - case ELauncherMode.Update: - { - if (isInProgress) - { - this.MainButton.Sensitive = false; - this.MainButton.Label = LocalizationCatalog.GetString("Updating..."); - } - else - { - this.MainButton.Sensitive = true; - this.MainButton.Label = LocalizationCatalog.GetString("Update"); - } - break; - } - case ELauncherMode.Repair: - { - if (isInProgress) - { - this.MainButton.Sensitive = false; - this.MainButton.Label = LocalizationCatalog.GetString("Repairing..."); - } - else - { - this.MainButton.Sensitive = true; - this.MainButton.Label = LocalizationCatalog.GetString("Repair"); - } - break; - } - case ELauncherMode.Launch: - { - if (isInProgress) - { - this.MainButton.Sensitive = false; - this.MainButton.Label = LocalizationCatalog.GetString("Launching..."); - } - else - { - this.MainButton.Sensitive = true; - this.MainButton.Label = LocalizationCatalog.GetString("Launch"); - } - break; - } - case ELauncherMode.Inactive: - { - this.MenuRepairItem.Sensitive = false; - - this.MainButton.Sensitive = false; - this.MainButton.Label = LocalizationCatalog.GetString("Inactive"); - break; - } - default: - { - throw new ArgumentOutOfRangeException(nameof(newMode), "An invalid launcher mode was passed to SetLauncherMode."); - } - } - - if (isInProgress) - { - this.MenuRepairItem.Sensitive = false; - this.MenuReinstallItem.Sensitive = false; - } - else - { - this.MenuRepairItem.Sensitive = true; - this.MenuReinstallItem.Sensitive = true; - } - } - - /// - /// Runs a game repair, no matter what the state the installation is in. - /// - /// Sender. - /// E. - private void OnMenuRepairItemActivated(object sender, EventArgs e) - { - SetLauncherMode(ELauncherMode.Repair, false); - - // Simulate a button press from the user. - this.MainButton.Click(); - } - - /// - /// Handles switching between different functionality depending on what is visible on the button to the user, - /// such as installing, updating, repairing, and launching. - /// - /// The sender. - /// Empty arguments. - private void OnMainButtonClicked(object sender, EventArgs e) - { - // Drop out if the current platform isn't available on the server - if (!this.Checks.IsPlatformAvailable(this.Configuration.SystemTarget)) - { - this.StatusLabel.Text = - LocalizationCatalog.GetString("The server does not provide the game for the selected platform."); - this.MainProgressBar.Text = string.Empty; - - Log.Info - ( - $"The server does not provide files for platform \"{PlatformHelpers.GetCurrentPlatform()}\". " + - "A .provides file must be present in the platforms' root directory." - ); - - SetLauncherMode(ELauncherMode.Inactive, false); - - return; - } - - // else, run the relevant function - switch (this.Mode) - { - case ELauncherMode.Repair: - { - // Repair the game asynchronously - SetLauncherMode(ELauncherMode.Repair, true); - this.Game.VerifyGame(); - - break; - } - case ELauncherMode.Install: - { - // Install the game asynchronously - SetLauncherMode(ELauncherMode.Install, true); - this.Game.InstallGame(); - - break; - } - case ELauncherMode.Update: - { - if (this.Checks.IsLauncherOutdated()) - { - // Update the launcher asynchronously - SetLauncherMode(ELauncherMode.Update, true); - this.Launcher.UpdateLauncher(); - } - else - { - // Update the game asynchronously - SetLauncherMode(ELauncherMode.Update, true); - this.Game.UpdateGame(); - } - - break; - } - case ELauncherMode.Launch: - { - this.StatusLabel.Text = LocalizationCatalog.GetString("Idle"); - this.MainProgressBar.Text = string.Empty; - - SetLauncherMode(ELauncherMode.Launch, true); - this.Game.LaunchGame(); - - break; - } - default: - { - Log.Warn("The main button was pressed with an invalid active mode. No functionality has been defined for this mode."); - break; - } - } - } - - /// - /// Starts the launcher update process when its files have finished downloading. - /// - private static void OnLauncherDownloadFinished(object sender, EventArgs e) - { - Application.Invoke((o, args) => - { - var script = LauncherHandler.CreateUpdateScript(); - Process.Start(script); - - Application.Quit(); - }); - } - - /// - /// Warns the user when the game fails to launch, and offers to attempt a repair. - /// - /// The sender. - /// Empty event args. - private void OnGameLaunchFailed(object sender, EventArgs e) - { - Application.Invoke((o, args) => - { - this.StatusLabel.Text = LocalizationCatalog.GetString("The game failed to launch. Try repairing the installation."); - this.MainProgressBar.Text = string.Empty; - - SetLauncherMode(ELauncherMode.Repair, false); - }); - } - - /// - /// Provides alternatives when the game fails to download, either through an update or through an installation. - /// - /// The sender. - /// Contains the type of failure that occurred. - private void OnGameDownloadFailed(object sender, EventArgs e) - { - Application.Invoke((o, args) => - { - switch (this.Mode) - { - case ELauncherMode.Install: - case ELauncherMode.Update: - case ELauncherMode.Repair: - { - // Set the mode to the same as it was, but no longer in progress. - // The modes which fall to this case are all capable of repairing an incomplete or - // broken install on their own. - SetLauncherMode(this.Mode, false); - break; - } - default: - { - // Other cases (such as Launch) will go to the default mode of Repair. - SetLauncherMode(ELauncherMode.Repair, false); - break; - } - } - }); - } - - /// - /// Updates the progress bar and progress label during installations, repairs and updates. - /// - /// The sender. - /// Contains the progress values and current filename. - private void OnModuleInstallationProgressChanged(object sender, ModuleProgressChangedArgs e) - { - Application.Invoke((o, args) => - { - this.MainProgressBar.Text = e.ProgressBarMessage; - this.StatusLabel.Text = e.IndicatorLabelMessage; - this.MainProgressBar.Fraction = e.ProgressFraction; - }); - } - - /// - /// Allows the user to launch or repair the game once installation finishes. - /// - /// The sender. - /// Contains the result of the download. - private void OnGameDownloadFinished(object sender, EventArgs e) - { - Application.Invoke((o, args) => - { - this.StatusLabel.Text = LocalizationCatalog.GetString("Idle"); - - switch (this.Mode) - { - case ELauncherMode.Install: - { - this.MainProgressBar.Text = LocalizationCatalog.GetString("Installation finished"); - break; - } - case ELauncherMode.Update: - { - this.MainProgressBar.Text = LocalizationCatalog.GetString("Update finished"); - break; - } - case ELauncherMode.Repair: - { - this.MainProgressBar.Text = LocalizationCatalog.GetString("Repair finished"); - break; - } - default: - { - this.MainProgressBar.Text = string.Empty; - break; - } - } - - SetLauncherMode(ELauncherMode.Launch, false); - }); - } - - /// - /// Handles offering of repairing the game to the user should the game exit - /// with a bad exit code. - /// - private void OnGameExited(object sender, int exitCode) - { - Application.Invoke((o, args) => - { - if (exitCode != 0) - { - using var crashDialog = new MessageDialog - ( - this, - DialogFlags.Modal, - MessageType.Question, - ButtonsType.YesNo, - LocalizationCatalog.GetString - ( - "Whoops! The game appears to have crashed.\n" + - "Would you like the launcher to verify the installation?" - ) - ); - if (crashDialog.Run() == (int)ResponseType.Yes) - { - SetLauncherMode(ELauncherMode.Repair, false); - this.MainButton.Click(); - } - else - { - SetLauncherMode(ELauncherMode.Launch, false); - } - } - else - { - SetLauncherMode(ELauncherMode.Launch, false); - } - }); - } - - /// - /// Handles starting of a reinstallation procedure as requested by the user. - /// - private void OnReinstallGameActionActivated(object sender, EventArgs e) - { - using var reinstallConfirmDialog = new MessageDialog - ( - this, - DialogFlags.Modal, - MessageType.Question, - ButtonsType.YesNo, - LocalizationCatalog.GetString - ( - "Reinstalling the game will delete all local files and download the entire game again.\n" + - "Are you sure you want to reinstall the game?" - ) - ); - - if (reinstallConfirmDialog.Run() != (int)ResponseType.Yes) - { - return; - } - - SetLauncherMode(ELauncherMode.Install, true); - this.Game.ReinstallGame(); - } - } + /// + /// The main UI class for Launchpad. This class acts as a manager for all threaded + /// actions, such as installing, updating or repairing the game. + /// + public sealed partial class MainWindow : Gtk.Window + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + private readonly LocalVersionService LocalVersionService = new LocalVersionService(); + + /// + /// The configuration instance reference. + /// + private readonly ILaunchpadConfiguration Configuration = ConfigHandler.Instance.Configuration; + + /// + /// The checks handler reference. + /// + private readonly ChecksHandler Checks = new ChecksHandler(); + + /// + /// The launcher handler. Allows updating the launcher and loading the changelog. + /// + private readonly LauncherHandler Launcher = new LauncherHandler(); + + /// + /// The game handler. Allows updating, installing and repairing the game. + /// + private readonly GameHandler Game = new GameHandler(); + + private readonly TagfileService TagfileService = new TagfileService(); + + /// + /// The current mode that the launcher is in. Determines what the primary button does when pressed. + /// + private ELauncherMode Mode = ELauncherMode.Inactive; + + /// + /// The localization catalog. + /// + private static readonly ICatalog LocalizationCatalog = new Catalog("Launchpad", "./Content/locale"); + + /// + /// Whether or not the launcher UI has been initialized. + /// + private bool IsInitialized; + + /// + /// Initializes a new instance of the class. + /// + /// The UI builder. + /// The native handle of the window. + private MainWindow(Builder builder, IntPtr handle) + : base(handle) + { + builder.Autoconnect(this); + + BindUIEvents(); + + // Bind the handler events + this.Game.ProgressChanged += OnModuleInstallationProgressChanged; + this.Game.DownloadFinished += OnGameDownloadFinished; + this.Game.DownloadFailed += OnGameDownloadFailed; + this.Game.LaunchFailed += OnGameLaunchFailed; + this.Game.GameExited += OnGameExited; + + this.Launcher.LauncherDownloadProgressChanged += OnModuleInstallationProgressChanged; + this.Launcher.LauncherDownloadFinished += OnLauncherDownloadFinished; + + // Set the initial launcher mode + SetLauncherMode(ELauncherMode.Inactive, false); + + // Set the window title + this.Title = LocalizationCatalog.GetString("Launchpad - {0}", this.Configuration.GameName); + this.StatusLabel.Text = LocalizationCatalog.GetString("Idle"); + } + + /// + /// Initializes the UI of the launcher, performing varying checks against the patching server. + /// + /// A task that must be awaited. + public Task InitializeAsync() + { + if (this.IsInitialized) + { + return Task.CompletedTask; + } + + // First of all, check if we can connect to the patching service. + if (!this.Checks.CanPatch()) + { + using (var dialog = new MessageDialog + ( + this, + DialogFlags.Modal, + MessageType.Warning, + ButtonsType.Ok, + LocalizationCatalog.GetString("Failed to connect to the patch server. Please check your settings.") + )) + { + dialog.Run(); + } + + this.StatusLabel.Text = LocalizationCatalog.GetString("Could not connect to server."); + this.MenuRepairItem.Sensitive = false; + } + else + { + LoadBanner(); + + LoadChangelog(); + + // If we can connect, proceed with the rest of our checks. + if (ChecksHandler.IsInitialStartup()) + { + DisplayInitialStartupDialog(); + } + + // If the launcher does not need an update at this point, we can continue checks for the game + if (!this.Checks.IsLauncherOutdated()) + { + if (!this.Checks.IsPlatformAvailable(this.Configuration.SystemTarget)) + { + Log.Info + ( + $"The server does not provide files for platform \"{PlatformHelpers.GetCurrentPlatform()}\". " + + "A .provides file must be present in the platforms' root directory." + ); + + SetLauncherMode(ELauncherMode.Inactive, false); + } + else + { + if (!this.Checks.IsGameInstalled()) + { + // If the game is not installed, offer to install it + Log.Info("The game has not yet been installed."); + SetLauncherMode(ELauncherMode.Install, false); + + // Since the game has not yet been installed, disallow manual repairs + this.MenuRepairItem.Sensitive = false; + + // and reinstalls + this.MenuReinstallItem.Sensitive = false; + } + else + { + // If the game is installed (which it should be at this point), check if it needs to be updated + if (this.Checks.IsGameOutdated()) + { + // If it does, offer to update it + Log.Info("The game is outdated."); + SetLauncherMode(ELauncherMode.Update, false); + } + else + { + // All checks passed, so we can offer to launch the game. + Log.Info("All checks passed. Game can be launched."); + SetLauncherMode(ELauncherMode.Launch, false); + } + } + } + } + else + { + // The launcher was outdated. + Log.Info($"The launcher is outdated. \n\tLocal version: {this.LocalVersionService.GetLocalLauncherVersion()}"); + SetLauncherMode(ELauncherMode.Update, false); + } + } + + this.IsInitialized = true; + return Task.CompletedTask; + } + + private void LoadChangelog() + { + var protocol = PatchProtocolProvider.GetHandler(); + var markup = protocol.GetChangelogMarkup(); + + // Preprocess dot lists + var dotRegex = new Regex("(?<=^\\s+)\\*", RegexOptions.Multiline); + markup = dotRegex.Replace(markup, "•"); + + // Preprocess line breaks + var regex = new Regex("(? + { + // Fetch the banner from the server + var bannerImage = patchHandler.GetBanner(); + + bannerImage.Mutate(i => i.Resize(this.BannerImage.AllocatedWidth, 0)); + + // Load the image into a pixel buffer + return new Pixbuf + ( + Bytes.NewStatic(bannerImage.SavePixelData()), + Colorspace.Rgb, + true, + 8, + bannerImage.Width, + bannerImage.Height, + 4 * bannerImage.Width + ); + } + ) + .ContinueWith + ( + async bannerTask => this.BannerImage.Pixbuf = await bannerTask + ); + } + + /// + /// Sets the launcher mode and updates UI elements to match. + /// + /// The new mode. + /// If set to true, the selected mode is in progress. + /// + /// Will be thrown if the passed to the function is not a valid value. + /// + private void SetLauncherMode(ELauncherMode newMode, bool isInProgress) + { + // Set the global launcher mode + this.Mode = newMode; + + // Set the UI elements to match + switch (newMode) + { + case ELauncherMode.Install: + { + if (isInProgress) + { + this.MainButton.Sensitive = false; + this.MainButton.Label = LocalizationCatalog.GetString("Installing..."); + } + else + { + this.MainButton.Sensitive = true; + this.MainButton.Label = LocalizationCatalog.GetString("Install"); + } + break; + } + case ELauncherMode.Update: + { + if (isInProgress) + { + this.MainButton.Sensitive = false; + this.MainButton.Label = LocalizationCatalog.GetString("Updating..."); + } + else + { + this.MainButton.Sensitive = true; + this.MainButton.Label = LocalizationCatalog.GetString("Update"); + } + break; + } + case ELauncherMode.Repair: + { + if (isInProgress) + { + this.MainButton.Sensitive = false; + this.MainButton.Label = LocalizationCatalog.GetString("Repairing..."); + } + else + { + this.MainButton.Sensitive = true; + this.MainButton.Label = LocalizationCatalog.GetString("Repair"); + } + break; + } + case ELauncherMode.Launch: + { + if (isInProgress) + { + this.MainButton.Sensitive = false; + this.MainButton.Label = LocalizationCatalog.GetString("Launching..."); + } + else + { + this.MainButton.Sensitive = true; + this.MainButton.Label = LocalizationCatalog.GetString("Launch"); + } + break; + } + case ELauncherMode.Inactive: + { + this.MenuRepairItem.Sensitive = false; + + this.MainButton.Sensitive = false; + this.MainButton.Label = LocalizationCatalog.GetString("Inactive"); + break; + } + default: + { + throw new ArgumentOutOfRangeException(nameof(newMode), "An invalid launcher mode was passed to SetLauncherMode."); + } + } + + if (isInProgress) + { + this.MenuRepairItem.Sensitive = false; + this.MenuReinstallItem.Sensitive = false; + } + else + { + this.MenuRepairItem.Sensitive = true; + this.MenuReinstallItem.Sensitive = true; + } + } + + /// + /// Runs a game repair, no matter what the state the installation is in. + /// + /// Sender. + /// E. + private void OnMenuRepairItemActivated(object sender, EventArgs e) + { + SetLauncherMode(ELauncherMode.Repair, false); + + // Simulate a button press from the user. + this.MainButton.Click(); + } + + /// + /// Handles switching between different functionality depending on what is visible on the button to the user, + /// such as installing, updating, repairing, and launching. + /// + /// The sender. + /// Empty arguments. + private void OnMainButtonClicked(object sender, EventArgs e) + { + // Drop out if the current platform isn't available on the server + if (!this.Checks.IsPlatformAvailable(this.Configuration.SystemTarget)) + { + this.StatusLabel.Text = + LocalizationCatalog.GetString("The server does not provide the game for the selected platform."); + this.MainProgressBar.Text = string.Empty; + + Log.Info + ( + $"The server does not provide files for platform \"{PlatformHelpers.GetCurrentPlatform()}\". " + + "A .provides file must be present in the platforms' root directory." + ); + + SetLauncherMode(ELauncherMode.Inactive, false); + + return; + } + + // else, run the relevant function + switch (this.Mode) + { + case ELauncherMode.Repair: + { + // Repair the game asynchronously + SetLauncherMode(ELauncherMode.Repair, true); + this.Game.VerifyGame(); + + break; + } + case ELauncherMode.Install: + { + // Install the game asynchronously + SetLauncherMode(ELauncherMode.Install, true); + this.Game.InstallGame(); + + break; + } + case ELauncherMode.Update: + { + if (this.Checks.IsLauncherOutdated()) + { + // Update the launcher asynchronously + SetLauncherMode(ELauncherMode.Update, true); + this.Launcher.UpdateLauncher(); + } + else + { + // Update the game asynchronously + SetLauncherMode(ELauncherMode.Update, true); + this.Game.UpdateGame(); + } + + break; + } + case ELauncherMode.Launch: + { + this.StatusLabel.Text = LocalizationCatalog.GetString("Idle"); + this.MainProgressBar.Text = string.Empty; + + SetLauncherMode(ELauncherMode.Launch, true); + this.Game.LaunchGame(); + + break; + } + default: + { + Log.Warn("The main button was pressed with an invalid active mode. No functionality has been defined for this mode."); + break; + } + } + } + + /// + /// Starts the launcher update process when its files have finished downloading. + /// + private static void OnLauncherDownloadFinished(object sender, EventArgs e) + { + Application.Invoke((o, args) => + { + var script = LauncherHandler.CreateUpdateScript(); + Process.Start(script); + + Application.Quit(); + }); + } + + /// + /// Warns the user when the game fails to launch, and offers to attempt a repair. + /// + /// The sender. + /// Empty event args. + private void OnGameLaunchFailed(object sender, EventArgs e) + { + Application.Invoke((o, args) => + { + this.StatusLabel.Text = LocalizationCatalog.GetString("The game failed to launch. Try repairing the installation."); + this.MainProgressBar.Text = string.Empty; + + SetLauncherMode(ELauncherMode.Repair, false); + }); + } + + /// + /// Provides alternatives when the game fails to download, either through an update or through an installation. + /// + /// The sender. + /// Contains the type of failure that occurred. + private void OnGameDownloadFailed(object sender, EventArgs e) + { + Application.Invoke((o, args) => + { + switch (this.Mode) + { + case ELauncherMode.Install: + case ELauncherMode.Update: + case ELauncherMode.Repair: + { + // Set the mode to the same as it was, but no longer in progress. + // The modes which fall to this case are all capable of repairing an incomplete or + // broken install on their own. + SetLauncherMode(this.Mode, false); + break; + } + default: + { + // Other cases (such as Launch) will go to the default mode of Repair. + SetLauncherMode(ELauncherMode.Repair, false); + break; + } + } + }); + } + + /// + /// Updates the progress bar and progress label during installations, repairs and updates. + /// + /// The sender. + /// Contains the progress values and current filename. + private void OnModuleInstallationProgressChanged(object sender, ModuleProgressChangedArgs e) + { + Application.Invoke((o, args) => + { + this.MainProgressBar.Text = e.ProgressBarMessage; + this.StatusLabel.Text = e.IndicatorLabelMessage; + this.MainProgressBar.Fraction = e.ProgressFraction; + }); + } + + /// + /// Allows the user to launch or repair the game once installation finishes. + /// + /// The sender. + /// Contains the result of the download. + private void OnGameDownloadFinished(object sender, EventArgs e) + { + Application.Invoke((o, args) => + { + this.StatusLabel.Text = LocalizationCatalog.GetString("Idle"); + + switch (this.Mode) + { + case ELauncherMode.Install: + { + this.MainProgressBar.Text = LocalizationCatalog.GetString("Installation finished"); + break; + } + case ELauncherMode.Update: + { + this.MainProgressBar.Text = LocalizationCatalog.GetString("Update finished"); + break; + } + case ELauncherMode.Repair: + { + this.MainProgressBar.Text = LocalizationCatalog.GetString("Repair finished"); + break; + } + default: + { + this.MainProgressBar.Text = string.Empty; + break; + } + } + + SetLauncherMode(ELauncherMode.Launch, false); + }); + } + + /// + /// Handles offering of repairing the game to the user should the game exit + /// with a bad exit code. + /// + private void OnGameExited(object sender, int exitCode) + { + Application.Invoke((o, args) => + { + if (exitCode != 0) + { + using var crashDialog = new MessageDialog + ( + this, + DialogFlags.Modal, + MessageType.Question, + ButtonsType.YesNo, + LocalizationCatalog.GetString + ( + "Whoops! The game appears to have crashed.\n" + + "Would you like the launcher to verify the installation?" + ) + ); + if (crashDialog.Run() == (int)ResponseType.Yes) + { + SetLauncherMode(ELauncherMode.Repair, false); + this.MainButton.Click(); + } + else + { + SetLauncherMode(ELauncherMode.Launch, false); + } + } + else + { + SetLauncherMode(ELauncherMode.Launch, false); + } + }); + } + + /// + /// Handles starting of a reinstallation procedure as requested by the user. + /// + private void OnReinstallGameActionActivated(object sender, EventArgs e) + { + using var reinstallConfirmDialog = new MessageDialog + ( + this, + DialogFlags.Modal, + MessageType.Question, + ButtonsType.YesNo, + LocalizationCatalog.GetString + ( + "Reinstalling the game will delete all local files and download the entire game again.\n" + + "Are you sure you want to reinstall the game?" + ) + ); + + if (reinstallConfirmDialog.Run() != (int)ResponseType.Yes) + { + return; + } + + SetLauncherMode(ELauncherMode.Install, true); + this.Game.ReinstallGame(); + } + } } diff --git a/Launchpad.Launcher/Program.cs b/Launchpad.Launcher/Program.cs index 5d811df2..119a010b 100644 --- a/Launchpad.Launcher/Program.cs +++ b/Launchpad.Launcher/Program.cs @@ -36,124 +36,124 @@ namespace Launchpad.Launcher { - /// - /// The main program class. - /// - public static class Program - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - - private static readonly LocalVersionService LocalVersionService = new LocalVersionService(); - - /// - /// The main entry point for the application. - /// - private static void Main() - { - // Bind any unhandled exceptions in the main thread so that they are logged. - AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Environment.SetEnvironmentVariable("GTK_EXE_PREFIX", Directory.GetCurrentDirectory()); - Environment.SetEnvironmentVariable("GTK_DATA_PREFIX", Directory.GetCurrentDirectory()); - Environment.SetEnvironmentVariable("GSETTINGS_SCHEMA_DIR", "share\\glib-2.0\\schemas\\"); - } - - Log.Info($"Launchpad v{LocalVersionService.GetLocalLauncherVersion()} starting..."); - - var systemBitness = Environment.Is64BitOperatingSystem ? "x64" : "x86"; - var processBitness = Environment.Is64BitProcess ? "64-bit" : "32-bit"; - Log.Info($"Current platform: {PlatformHelpers.GetCurrentPlatform()} ({systemBitness} platform, {processBitness} process)"); - - Log.Info("Initializing UI..."); - - // Bind any unhandled exceptions in the GTK UI so that they are logged. - ExceptionManager.UnhandledException += OnGLibUnhandledException; - - // Run the GTK UI - Gtk.Application.Init(); - - var win = MainWindow.Create(); - win.Show(); - - Timeout.Add - ( - 50, - () => - { - Task.Factory.StartNew - ( - () => win.InitializeAsync(), - CancellationToken.None, - TaskCreationOptions.DenyChildAttach, - TaskScheduler.FromCurrentSynchronizationContext() - ); - return false; - } - ); - - Gtk.Application.Run(); - } - - /// - /// Passes any unhandled exceptions from the GTK UI to the generic handler. - /// - /// The event object containing the information about the exception. - private static void OnGLibUnhandledException(UnhandledExceptionArgs args) - { - OnUnhandledException(null, args); - } - - /// - /// Event handler for all unhandled exceptions that may be encountered during runtime. While there should never - /// be any unhandled exceptions in an ideal program, unexpected issues can and will arise. This handler logs - /// the exception and all relevant information to a logfile and prints it to the console for debugging purposes. - /// - /// The sending object. - /// The event object containing the information about the exception. - private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs unhandledExceptionEventArgs) - { - Log.Fatal("----------------"); - Log.Fatal("FATAL UNHANDLED EXCEPTION!"); - Log.Fatal("Something has gone terribly, terribly wrong during runtime."); - Log.Fatal("The following is what information could be gathered by the program before crashing."); - Log.Fatal - ( - "Please report this to or via GitHub. Include the full log and a " + - "description of what you were doing when it happened." - ); - - if (!(unhandledExceptionEventArgs.ExceptionObject is Exception unhandledException)) - { - return; - } - - // Unwrap TargetInvocationExceptions - if (unhandledException is TargetInvocationException) - { - unhandledException = unhandledException.InnerException ?? unhandledException; - } - - if (unhandledException is DllNotFoundException) - { - Log.Fatal - ( - "This exception is typical of instances where the GTK# runtime has not been installed.\n" + - "If you haven't installed it, download it at \'http://www.mono-project.com/download/#download-win\'.\n" + - "If you have installed it, reboot your computer and try again." - ); - - // Send the user to the common problems page. - System.Diagnostics.Process.Start("https://github.com/Nihlus/Launchpad/wiki/Common-problems"); - } - - Log.Fatal("Exception type: " + unhandledException.GetType().FullName); - Log.Fatal("Exception Message: " + unhandledException.Message); - Log.Fatal("Exception Stacktrace: " + unhandledException.StackTrace); - } - } + /// + /// The main program class. + /// + public static class Program + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + + private static readonly LocalVersionService LocalVersionService = new LocalVersionService(); + + /// + /// The main entry point for the application. + /// + private static void Main() + { + // Bind any unhandled exceptions in the main thread so that they are logged. + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Environment.SetEnvironmentVariable("GTK_EXE_PREFIX", Directory.GetCurrentDirectory()); + Environment.SetEnvironmentVariable("GTK_DATA_PREFIX", Directory.GetCurrentDirectory()); + Environment.SetEnvironmentVariable("GSETTINGS_SCHEMA_DIR", "share\\glib-2.0\\schemas\\"); + } + + Log.Info($"Launchpad v{LocalVersionService.GetLocalLauncherVersion()} starting..."); + + var systemBitness = Environment.Is64BitOperatingSystem ? "x64" : "x86"; + var processBitness = Environment.Is64BitProcess ? "64-bit" : "32-bit"; + Log.Info($"Current platform: {PlatformHelpers.GetCurrentPlatform()} ({systemBitness} platform, {processBitness} process)"); + + Log.Info("Initializing UI..."); + + // Bind any unhandled exceptions in the GTK UI so that they are logged. + ExceptionManager.UnhandledException += OnGLibUnhandledException; + + // Run the GTK UI + Gtk.Application.Init(); + + var win = MainWindow.Create(); + win.Show(); + + Timeout.Add + ( + 50, + () => + { + Task.Factory.StartNew + ( + () => win.InitializeAsync(), + CancellationToken.None, + TaskCreationOptions.DenyChildAttach, + TaskScheduler.FromCurrentSynchronizationContext() + ); + return false; + } + ); + + Gtk.Application.Run(); + } + + /// + /// Passes any unhandled exceptions from the GTK UI to the generic handler. + /// + /// The event object containing the information about the exception. + private static void OnGLibUnhandledException(UnhandledExceptionArgs args) + { + OnUnhandledException(null, args); + } + + /// + /// Event handler for all unhandled exceptions that may be encountered during runtime. While there should never + /// be any unhandled exceptions in an ideal program, unexpected issues can and will arise. This handler logs + /// the exception and all relevant information to a logfile and prints it to the console for debugging purposes. + /// + /// The sending object. + /// The event object containing the information about the exception. + private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs unhandledExceptionEventArgs) + { + Log.Fatal("----------------"); + Log.Fatal("FATAL UNHANDLED EXCEPTION!"); + Log.Fatal("Something has gone terribly, terribly wrong during runtime."); + Log.Fatal("The following is what information could be gathered by the program before crashing."); + Log.Fatal + ( + "Please report this to or via GitHub. Include the full log and a " + + "description of what you were doing when it happened." + ); + + if (!(unhandledExceptionEventArgs.ExceptionObject is Exception unhandledException)) + { + return; + } + + // Unwrap TargetInvocationExceptions + if (unhandledException is TargetInvocationException) + { + unhandledException = unhandledException.InnerException ?? unhandledException; + } + + if (unhandledException is DllNotFoundException) + { + Log.Fatal + ( + "This exception is typical of instances where the GTK# runtime has not been installed.\n" + + "If you haven't installed it, download it at \'http://www.mono-project.com/download/#download-win\'.\n" + + "If you have installed it, reboot your computer and try again." + ); + + // Send the user to the common problems page. + System.Diagnostics.Process.Start("https://github.com/Nihlus/Launchpad/wiki/Common-problems"); + } + + Log.Fatal("Exception type: " + unhandledException.GetType().FullName); + Log.Fatal("Exception Message: " + unhandledException.Message); + Log.Fatal("Exception Stacktrace: " + unhandledException.StackTrace); + } + } } diff --git a/Launchpad.Launcher/Services/GameArgumentService.cs b/Launchpad.Launcher/Services/GameArgumentService.cs index 192b6998..a75e0d24 100644 --- a/Launchpad.Launcher/Services/GameArgumentService.cs +++ b/Launchpad.Launcher/Services/GameArgumentService.cs @@ -27,56 +27,56 @@ namespace Launchpad.Launcher.Services { - /// - /// A service providing access to arguments that should be passed to the game. - /// - public class GameArgumentService - { - /// - /// Initializes a new instance of the class. - /// - public GameArgumentService() - { - InitializeGameArgumentsFile(); - } + /// + /// A service providing access to arguments that should be passed to the game. + /// + public class GameArgumentService + { + /// + /// Initializes a new instance of the class. + /// + public GameArgumentService() + { + InitializeGameArgumentsFile(); + } - /// - /// Creates a configuration file where the user or developer can add runtime switches for the installed game. - /// If the file already exists, this method does nothing. - /// - private static void InitializeGameArgumentsFile() - { - // Initialize the game arguments file, if needed - if (File.Exists(DirectoryHelpers.GetGameArgumentsPath())) - { - return; - } + /// + /// Creates a configuration file where the user or developer can add runtime switches for the installed game. + /// If the file already exists, this method does nothing. + /// + private static void InitializeGameArgumentsFile() + { + // Initialize the game arguments file, if needed + if (File.Exists(DirectoryHelpers.GetGameArgumentsPath())) + { + return; + } - using var fs = File.Create(DirectoryHelpers.GetGameArgumentsPath()); - using var sw = new StreamWriter(fs); - sw.WriteLine("# This file contains all the arguments passed to the game executable on startup."); - sw.WriteLine("# Lines beginning with a hash character (#) are ignored and considered comments."); - sw.WriteLine("# Everything else is passed line-by-line to the game executable on startup."); - sw.WriteLine("# Multiple arguments can be on the same line in this file."); - sw.WriteLine("# Each line will have a space appended at the end when passed to the game executable."); - sw.WriteLine(string.Empty); - } + using var fs = File.Create(DirectoryHelpers.GetGameArgumentsPath()); + using var sw = new StreamWriter(fs); + sw.WriteLine("# This file contains all the arguments passed to the game executable on startup."); + sw.WriteLine("# Lines beginning with a hash character (#) are ignored and considered comments."); + sw.WriteLine("# Everything else is passed line-by-line to the game executable on startup."); + sw.WriteLine("# Multiple arguments can be on the same line in this file."); + sw.WriteLine("# Each line will have a space appended at the end when passed to the game executable."); + sw.WriteLine(string.Empty); + } - /// - /// Gets a list of command-line arguments that are passed to the game when it starts. - /// - /// The arguments. - public IEnumerable GetGameArguments() - { - if (!File.Exists(DirectoryHelpers.GetGameArgumentsPath())) - { - return new List(); - } + /// + /// Gets a list of command-line arguments that are passed to the game when it starts. + /// + /// The arguments. + public IEnumerable GetGameArguments() + { + if (!File.Exists(DirectoryHelpers.GetGameArgumentsPath())) + { + return new List(); + } - var gameArguments = new List(File.ReadAllLines(DirectoryHelpers.GetGameArgumentsPath())); + var gameArguments = new List(File.ReadAllLines(DirectoryHelpers.GetGameArgumentsPath())); - // Return the list of lines in the argument file, except the ones starting with a hash or empty lines - return gameArguments.Where(s => !s.StartsWith("#") && !string.IsNullOrEmpty(s)).ToList(); - } - } + // Return the list of lines in the argument file, except the ones starting with a hash or empty lines + return gameArguments.Where(s => !s.StartsWith("#") && !string.IsNullOrEmpty(s)).ToList(); + } + } } diff --git a/Launchpad.Launcher/Services/LocalVersionService.cs b/Launchpad.Launcher/Services/LocalVersionService.cs index 4bcfce1a..0947a943 100644 --- a/Launchpad.Launcher/Services/LocalVersionService.cs +++ b/Launchpad.Launcher/Services/LocalVersionService.cs @@ -28,48 +28,48 @@ namespace Launchpad.Launcher.Services { - /// - /// A service which handles local version discovery. - /// - public class LocalVersionService - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + /// + /// A service which handles local version discovery. + /// + public class LocalVersionService + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - /// - /// Gets the local game version. - /// - /// The local game version. - public Version GetLocalGameVersion() - { - try - { - var rawGameVersion = File.ReadAllText(DirectoryHelpers.GetLocalGameVersionPath()); + /// + /// Gets the local game version. + /// + /// The local game version. + public Version GetLocalGameVersion() + { + try + { + var rawGameVersion = File.ReadAllText(DirectoryHelpers.GetLocalGameVersionPath()); - if (Version.TryParse(rawGameVersion, out var gameVersion)) - { - return gameVersion; - } + if (Version.TryParse(rawGameVersion, out var gameVersion)) + { + return gameVersion; + } - Log.Warn("Could not parse local game version. Contents: " + rawGameVersion); - return new Version("0.0.0"); - } - catch (IOException ioex) - { - Log.Warn("Could not read local game version (IOException): " + ioex.Message); - return null; - } - } + Log.Warn("Could not parse local game version. Contents: " + rawGameVersion); + return new Version("0.0.0"); + } + catch (IOException ioex) + { + Log.Warn("Could not read local game version (IOException): " + ioex.Message); + return null; + } + } - /// - /// Gets the local launcher version. - /// - /// The version. - public Version GetLocalLauncherVersion() - { - return GetType().Assembly.GetName().Version; - } - } + /// + /// Gets the local launcher version. + /// + /// The version. + public Version GetLocalLauncherVersion() + { + return GetType().Assembly.GetName().Version; + } + } } diff --git a/Launchpad.Launcher/Services/TagfileService.cs b/Launchpad.Launcher/Services/TagfileService.cs index 56133070..4783450a 100644 --- a/Launchpad.Launcher/Services/TagfileService.cs +++ b/Launchpad.Launcher/Services/TagfileService.cs @@ -25,33 +25,33 @@ namespace Launchpad.Launcher.Services { - /// - /// Service for creating and managing tagfiles. - /// - public class TagfileService - { - /// - /// Creates the launcher cookie. - /// - public void CreateLauncherTagfile() - { - var doesCookieExist = File.Exists(DirectoryHelpers.GetLauncherTagfilePath()); - if (!doesCookieExist) - { - File.Create(DirectoryHelpers.GetLauncherTagfilePath()); - } - } + /// + /// Service for creating and managing tagfiles. + /// + public class TagfileService + { + /// + /// Creates the launcher cookie. + /// + public void CreateLauncherTagfile() + { + var doesCookieExist = File.Exists(DirectoryHelpers.GetLauncherTagfilePath()); + if (!doesCookieExist) + { + File.Create(DirectoryHelpers.GetLauncherTagfilePath()); + } + } - /// - /// Creates the install cookie. - /// - public void CreateGameTagfile() - { - var doesCookieExist = File.Exists(DirectoryHelpers.GetGameTagfilePath()); - if (!doesCookieExist) - { - File.Create(DirectoryHelpers.GetGameTagfilePath()).Close(); - } - } - } + /// + /// Creates the install cookie. + /// + public void CreateGameTagfile() + { + var doesCookieExist = File.Exists(DirectoryHelpers.GetGameTagfilePath()); + if (!doesCookieExist) + { + File.Create(DirectoryHelpers.GetGameTagfilePath()).Close(); + } + } + } } diff --git a/Launchpad.Launcher/Utility/DirectoryHelpers.cs b/Launchpad.Launcher/Utility/DirectoryHelpers.cs index 058fbdbd..97e8dacf 100644 --- a/Launchpad.Launcher/Utility/DirectoryHelpers.cs +++ b/Launchpad.Launcher/Utility/DirectoryHelpers.cs @@ -26,129 +26,129 @@ namespace Launchpad.Launcher.Utility { - /// - /// Helper methods for common paths and directories. - /// - public static class DirectoryHelpers - { - private const string ConfigurationFolderName = "Config"; - private const string ConfigurationFileName = "LauncherConfig"; - private const string GameArgumentsFileName = "GameArguments"; + /// + /// Helper methods for common paths and directories. + /// + public static class DirectoryHelpers + { + private const string ConfigurationFolderName = "Config"; + private const string ConfigurationFileName = "LauncherConfig"; + private const string GameArgumentsFileName = "GameArguments"; - /// - /// Gets the expected path to the config file on disk. - /// - /// The config path. - public static string GetConfigPath() - { - return Path.Combine(GetConfigDirectory(), $"{ConfigurationFileName}.ini"); - } + /// + /// Gets the expected path to the config file on disk. + /// + /// The config path. + public static string GetConfigPath() + { + return Path.Combine(GetConfigDirectory(), $"{ConfigurationFileName}.ini"); + } - /// - /// Gets the path to the config directory. - /// - /// The path. - public static string GetConfigDirectory() - { - return Path.Combine(GetLocalLauncherDirectory(), ConfigurationFolderName); - } + /// + /// Gets the path to the config directory. + /// + /// The path. + public static string GetConfigDirectory() + { + return Path.Combine(GetLocalLauncherDirectory(), ConfigurationFolderName); + } - /// - /// Gets the path to the launcher cookie on disk. - /// - /// The launcher cookie. - public static string GetLauncherTagfilePath() - { - return Path.Combine(GetLocalLauncherDirectory(), ".launcher"); - } + /// + /// Gets the path to the launcher cookie on disk. + /// + /// The launcher cookie. + public static string GetLauncherTagfilePath() + { + return Path.Combine(GetLocalLauncherDirectory(), ".launcher"); + } - /// - /// Gets the install cookie. - /// - /// The install cookie. - public static string GetGameTagfilePath() - { - return Path.Combine(GetLocalLauncherDirectory(), ".game"); - } + /// + /// Gets the install cookie. + /// + /// The install cookie. + public static string GetGameTagfilePath() + { + return Path.Combine(GetLocalLauncherDirectory(), ".game"); + } - /// - /// Gets the local directory where the launcher is stored. - /// - /// The local directory. - public static string GetLocalLauncherDirectory() - { - var executingLocation = Assembly.GetExecutingAssembly().Location; - return Path.GetDirectoryName(executingLocation); - } + /// + /// Gets the local directory where the launcher is stored. + /// + /// The local directory. + public static string GetLocalLauncherDirectory() + { + var executingLocation = Assembly.GetExecutingAssembly().Location; + return Path.GetDirectoryName(executingLocation); + } - /// - /// Gets the temporary launcher download directory. - /// - /// A full path to the directory. - public static string GetTempLauncherDownloadPath() - { - return Path.Combine(Path.GetTempPath(), "launchpad", "launcher"); - } + /// + /// Gets the temporary launcher download directory. + /// + /// A full path to the directory. + public static string GetTempLauncherDownloadPath() + { + return Path.Combine(Path.GetTempPath(), "launchpad", "launcher"); + } - /// - /// Gets the expected path to the argument file on disk. - /// - /// The path. - public static string GetGameArgumentsPath() - { - return Path.Combine(GetConfigDirectory(), $"{GameArgumentsFileName}.txt"); - } + /// + /// Gets the expected path to the argument file on disk. + /// + /// The path. + public static string GetGameArgumentsPath() + { + return Path.Combine(GetConfigDirectory(), $"{GameArgumentsFileName}.txt"); + } - /// - /// Gets the game directory. - /// - /// The directory. - public static string GetLocalGameDirectory() - { - var config = ConfigHandler.Instance.Configuration; - return Path.Combine(GetLocalLauncherDirectory(), "Game", config.SystemTarget.ToString()); - } + /// + /// Gets the game directory. + /// + /// The directory. + public static string GetLocalGameDirectory() + { + var config = ConfigHandler.Instance.Configuration; + return Path.Combine(GetLocalLauncherDirectory(), "Game", config.SystemTarget.ToString()); + } - /// - /// Gets the game version path. - /// - /// The game version path. - public static string GetLocalGameVersionPath() - { - return Path.Combine(GetLocalGameDirectory(), "GameVersion.txt"); - } + /// + /// Gets the game version path. + /// + /// The game version path. + public static string GetLocalGameVersionPath() + { + return Path.Combine(GetLocalGameDirectory(), "GameVersion.txt"); + } - /// - /// Gets the remote path to where launcher binaries are stored. - /// - /// The path. - public static string GetRemoteLauncherBinariesPath() - { - var config = ConfigHandler.Instance.Configuration; - return $"{config.RemoteAddress}/launcher/bin/"; - } + /// + /// Gets the remote path to where launcher binaries are stored. + /// + /// The path. + public static string GetRemoteLauncherBinariesPath() + { + var config = ConfigHandler.Instance.Configuration; + return $"{config.RemoteAddress}/launcher/bin/"; + } - /// - /// Gets the remote path of the launcher version. - /// - /// - /// The path to either the official launchpad binaries or a custom launcher, depending on the settings. - /// - public static string GetRemoteLauncherVersionPath() - { - var config = ConfigHandler.Instance.Configuration; + /// + /// Gets the remote path of the launcher version. + /// + /// + /// The path to either the official launchpad binaries or a custom launcher, depending on the settings. + /// + public static string GetRemoteLauncherVersionPath() + { + var config = ConfigHandler.Instance.Configuration; - return $"{config.RemoteAddress}/launcher/LauncherVersion.txt"; - } + return $"{config.RemoteAddress}/launcher/LauncherVersion.txt"; + } - /// - /// Gets the remote path where the game is stored.. - /// - /// The path. - public static string GetRemoteGamePath() - { - var config = ConfigHandler.Instance.Configuration; - return $"{config.RemoteAddress}/game/{config.SystemTarget}/bin/"; - } - } + /// + /// Gets the remote path where the game is stored.. + /// + /// The path. + public static string GetRemoteGamePath() + { + var config = ConfigHandler.Instance.Configuration; + return $"{config.RemoteAddress}/game/{config.SystemTarget}/bin/"; + } + } } diff --git a/Launchpad.Launcher/Utility/Enums/ELauncherMode.cs b/Launchpad.Launcher/Utility/Enums/ELauncherMode.cs index bf9cf93b..8f69e734 100644 --- a/Launchpad.Launcher/Utility/Enums/ELauncherMode.cs +++ b/Launchpad.Launcher/Utility/Enums/ELauncherMode.cs @@ -22,34 +22,34 @@ namespace Launchpad.Launcher.Utility.Enums { - /// - /// The mode the launcher is in. - /// - internal enum ELauncherMode - { - /// - /// The launcher can install or is installing a game. - /// - Install, + /// + /// The mode the launcher is in. + /// + internal enum ELauncherMode + { + /// + /// The launcher can install or is installing a game. + /// + Install, - /// - /// The launcher can update or is updating a game. - /// - Update, + /// + /// The launcher can update or is updating a game. + /// + Update, - /// - /// The launcher can repair or is repairing a game. - /// - Repair, + /// + /// The launcher can repair or is repairing a game. + /// + Repair, - /// - /// The launcher can launch or is launching a game. - /// - Launch, + /// + /// The launcher can launch or is launching a game. + /// + Launch, - /// - /// The launcher can't do or isn't doing anything. - /// - Inactive - } + /// + /// The launcher can't do or isn't doing anything. + /// + Inactive + } } diff --git a/Launchpad.Launcher/Utility/PatchProtocolProvider.cs b/Launchpad.Launcher/Utility/PatchProtocolProvider.cs index e81fb3cf..53ec9911 100644 --- a/Launchpad.Launcher/Utility/PatchProtocolProvider.cs +++ b/Launchpad.Launcher/Utility/PatchProtocolProvider.cs @@ -27,37 +27,37 @@ namespace Launchpad.Launcher.Utility { - /// - /// TODO: Temporary hack class. This is going away. - /// - public static class PatchProtocolProvider - { - /// - /// Gets an instance of the patch protocol handler which supports the URI set in the configuration. - /// - /// A handler instance. - /// Thrown if no compatible handler is available. - public static PatchProtocolHandler GetHandler() - { - var config = ConfigHandler.Instance.Configuration; - var remoteAddress = config.RemoteAddress; + /// + /// TODO: Temporary hack class. This is going away. + /// + public static class PatchProtocolProvider + { + /// + /// Gets an instance of the patch protocol handler which supports the URI set in the configuration. + /// + /// A handler instance. + /// Thrown if no compatible handler is available. + public static PatchProtocolHandler GetHandler() + { + var config = ConfigHandler.Instance.Configuration; + var remoteAddress = config.RemoteAddress; - switch (remoteAddress.Scheme.ToLowerInvariant()) - { - case "ftp": - { - return new FTPProtocolHandler(); - } - case "http": - case "https": - { - return new HTTPProtocolHandler(); - } - default: - { - throw new ArgumentException($"No compatible protocol handler found for a URI of the form \"{remoteAddress}\"."); - } - } - } - } + switch (remoteAddress.Scheme.ToLowerInvariant()) + { + case "ftp": + { + return new FTPProtocolHandler(); + } + case "http": + case "https": + { + return new HTTPProtocolHandler(); + } + default: + { + throw new ArgumentException($"No compatible protocol handler found for a URI of the form \"{remoteAddress}\"."); + } + } + } + } } diff --git a/Launchpad.Launcher/Utility/ResourceManager.cs b/Launchpad.Launcher/Utility/ResourceManager.cs index 28d4c5bd..3d0db8fd 100644 --- a/Launchpad.Launcher/Utility/ResourceManager.cs +++ b/Launchpad.Launcher/Utility/ResourceManager.cs @@ -24,19 +24,19 @@ namespace Launchpad.Launcher.Utility { - /// - /// Manages embedded resources of the application. - /// - public static class ResourceManager - { - /// - /// Gets the application icon as a pixel buffer. - /// - public static Pixbuf ApplicationIcon { get; } + /// + /// Manages embedded resources of the application. + /// + public static class ResourceManager + { + /// + /// Gets the application icon as a pixel buffer. + /// + public static Pixbuf ApplicationIcon { get; } - static ResourceManager() - { - ApplicationIcon = Pixbuf.LoadFromResource("Launchpad.Launcher.Resources.Icon.png"); - } - } + static ResourceManager() + { + ApplicationIcon = Pixbuf.LoadFromResource("Launchpad.Launcher.Resources.Icon.png"); + } + } } diff --git a/Launchpad.Tests/Common/MD5HandlerTests.cs b/Launchpad.Tests/Common/MD5HandlerTests.cs index 1fd1ed8b..9e0827ad 100644 --- a/Launchpad.Tests/Common/MD5HandlerTests.cs +++ b/Launchpad.Tests/Common/MD5HandlerTests.cs @@ -25,22 +25,22 @@ namespace Launchpad.Tests.Common { - public class MD5HandlerTests - { - /// - /// Holds the string "placeholder". - /// - private readonly MemoryStream DataStream = new MemoryStream(new byte[] - { - 112, 108, 97, 99, 101, 104, 111, 108, 100, 101, 114 - }); + public class MD5HandlerTests + { + /// + /// Holds the string "placeholder". + /// + private readonly MemoryStream DataStream = new MemoryStream(new byte[] + { + 112, 108, 97, 99, 101, 104, 111, 108, 100, 101, 114 + }); - private const string ExpectedHash = "6A99C575AB87F8C7D1ED1E52E7E349CE"; + private const string ExpectedHash = "6A99C575AB87F8C7D1ED1E52E7E349CE"; - [Fact] - public void HashesCorrectly() - { - Assert.Equal(ExpectedHash, MD5Handler.GetStreamHash(this.DataStream)); - } - } -} \ No newline at end of file + [Fact] + public void HashesCorrectly() + { + Assert.Equal(ExpectedHash, MD5Handler.GetStreamHash(this.DataStream)); + } + } +} diff --git a/Launchpad.Tests/Common/StringExtensionsTexts.cs b/Launchpad.Tests/Common/StringExtensionsTexts.cs index d3dfb8c8..c7db5600 100644 --- a/Launchpad.Tests/Common/StringExtensionsTexts.cs +++ b/Launchpad.Tests/Common/StringExtensionsTexts.cs @@ -24,45 +24,45 @@ namespace Launchpad.Tests.Common { - public class StringExtensionsTexts - { - public class RemoveLineSeparatorsAndNulls - { - private const string Expected = "data"; - private const string StringThatContainsNulls = "data\0\0"; - private const string StringThatContainsCarriageReturns = "data\r\r"; - private const string StringThatContainsLinefeeds = "data\n\n"; - private const string StringThatContainsEverything = "data\0\r\n"; + public class StringExtensionsTexts + { + public class RemoveLineSeparatorsAndNulls + { + private const string Expected = "data"; + private const string StringThatContainsNulls = "data\0\0"; + private const string StringThatContainsCarriageReturns = "data\r\r"; + private const string StringThatContainsLinefeeds = "data\n\n"; + private const string StringThatContainsEverything = "data\0\r\n"; - [Fact] - public void DoesNotChangeStringThatDoesNotContainNullsCarriageReturnsOrLineFeeds() - { - Assert.Equal(Expected, Expected.RemoveLineSeparatorsAndNulls()); - } + [Fact] + public void DoesNotChangeStringThatDoesNotContainNullsCarriageReturnsOrLineFeeds() + { + Assert.Equal(Expected, Expected.RemoveLineSeparatorsAndNulls()); + } - [Fact] - public void RemovesNullCharacters() - { - Assert.Equal(Expected, StringThatContainsNulls.RemoveLineSeparatorsAndNulls()); - } + [Fact] + public void RemovesNullCharacters() + { + Assert.Equal(Expected, StringThatContainsNulls.RemoveLineSeparatorsAndNulls()); + } - [Fact] - public void RemovesCarriageReturns() - { - Assert.Equal(Expected, StringThatContainsCarriageReturns.RemoveLineSeparatorsAndNulls()); - } + [Fact] + public void RemovesCarriageReturns() + { + Assert.Equal(Expected, StringThatContainsCarriageReturns.RemoveLineSeparatorsAndNulls()); + } - [Fact] - public void RemovesLineFeeds() - { - Assert.Equal(Expected, StringThatContainsLinefeeds.RemoveLineSeparatorsAndNulls()); - } + [Fact] + public void RemovesLineFeeds() + { + Assert.Equal(Expected, StringThatContainsLinefeeds.RemoveLineSeparatorsAndNulls()); + } - [Fact] - public void RemovesNullsCarriageReturnsAndLineFeeds() - { - Assert.Equal(Expected, StringThatContainsEverything.RemoveLineSeparatorsAndNulls()); - } - } - } -} \ No newline at end of file + [Fact] + public void RemovesNullsCarriageReturnsAndLineFeeds() + { + Assert.Equal(Expected, StringThatContainsEverything.RemoveLineSeparatorsAndNulls()); + } + } + } +} diff --git a/Launchpad.Utilities/Handlers/ManifestGenerationHandler.cs b/Launchpad.Utilities/Handlers/ManifestGenerationHandler.cs index 352f0ec4..66a38a64 100644 --- a/Launchpad.Utilities/Handlers/ManifestGenerationHandler.cs +++ b/Launchpad.Utilities/Handlers/ManifestGenerationHandler.cs @@ -32,112 +32,112 @@ namespace Launchpad.Utilities.Handlers { - public class ManifestGenerationHandler - { - private readonly ManifestGenerationProgressChangedEventArgs GenerationProgressArgs = new ManifestGenerationProgressChangedEventArgs(); - - /// - /// Generates a manifest containing the relative path, MD5 hash and file size from - /// all files in the provided root path. - /// - /// The root path of the directory the manifest should represent. - /// The type of manifest that should be generated. - /// The progress reporter to use. - /// The cancellation token to use. - public Task GenerateManifestAsync - ( - string targetPath, - EManifestType manifestType, - IProgress progressReporter, - CancellationToken ct - ) - { - var parentDirectory = Directory.GetParent(targetPath).ToString(); - - var manifestPath = Path.Combine(parentDirectory, $"{manifestType}Manifest.txt"); - var manifestChecksumPath = Path.Combine(parentDirectory, $"{manifestType}Manifest.checksum"); - - return Task.Run - ( - async () => - { - var manifestFilePaths = new List(Directory - .EnumerateFiles(targetPath, "*", SearchOption.AllDirectories) - .Where(s => !IsPathABlacklistedFile(s))); - - this.GenerationProgressArgs.TotalFiles = manifestFilePaths.Count; - - await using (var tw = new StreamWriter(File.Create(manifestPath, 4096, FileOptions.Asynchronous))) - { - var completedFiles = 0; - foreach (var filePath in manifestFilePaths) - { - ct.ThrowIfCancellationRequested(); - - var newEntry = CreateEntryForFile(targetPath, filePath); - - await tw.WriteLineAsync(newEntry.ToString()); - await tw.FlushAsync(); - - completedFiles++; - - this.GenerationProgressArgs.CompletedFiles = completedFiles; - this.GenerationProgressArgs.Filepath = newEntry.RelativePath; - this.GenerationProgressArgs.Hash = newEntry.Hash; - this.GenerationProgressArgs.Filesize = newEntry.Size; - - progressReporter.Report(this.GenerationProgressArgs); - } - } - - await CreateManifestChecksumAsync(manifestPath, manifestChecksumPath); - }, - ct - ); - } - - private async Task CreateManifestChecksumAsync(string manifestPath, string manifestChecksumPath) - { - // Create a checksum file for the manifest. - await using var manifestStream = File.OpenRead(manifestPath); - var manifestHash = MD5Handler.GetStreamHash(manifestStream); - - await using var checksumStream = File.Create(manifestChecksumPath, 4096, FileOptions.Asynchronous); - await using var tw = new StreamWriter(checksumStream); - await tw.WriteLineAsync(manifestHash); - await tw.FlushAsync(); - tw.Close(); - } - - private ManifestEntry CreateEntryForFile(string parentDirectory, string filePath) - { - string hash; - long fileSize; - using (var fileStream = File.OpenRead(filePath)) - { - hash = MD5Handler.GetStreamHash(fileStream); - fileSize = fileStream.Length; - } - - var relativeFilePath = filePath.Substring(parentDirectory.Length).TrimStart(Path.DirectorySeparatorChar); - var newEntry = new ManifestEntry(relativeFilePath, hash, fileSize); - - return newEntry; - } - - /// - /// Determines whether or not the specified path is blacklisted and should not be included in the manifest. - /// - /// The path to test. - /// true if the path is blackliste; otherwise, false. - private bool IsPathABlacklistedFile(string filePath) - { - return - filePath.EndsWith(".install") || - filePath.EndsWith(".update") || - filePath.EndsWith("GameManifest.txt") || - filePath.EndsWith("GameManifest.checksum"); - } - } + public class ManifestGenerationHandler + { + private readonly ManifestGenerationProgressChangedEventArgs GenerationProgressArgs = new ManifestGenerationProgressChangedEventArgs(); + + /// + /// Generates a manifest containing the relative path, MD5 hash and file size from + /// all files in the provided root path. + /// + /// The root path of the directory the manifest should represent. + /// The type of manifest that should be generated. + /// The progress reporter to use. + /// The cancellation token to use. + public Task GenerateManifestAsync + ( + string targetPath, + EManifestType manifestType, + IProgress progressReporter, + CancellationToken ct + ) + { + var parentDirectory = Directory.GetParent(targetPath).ToString(); + + var manifestPath = Path.Combine(parentDirectory, $"{manifestType}Manifest.txt"); + var manifestChecksumPath = Path.Combine(parentDirectory, $"{manifestType}Manifest.checksum"); + + return Task.Run + ( + async () => + { + var manifestFilePaths = new List(Directory + .EnumerateFiles(targetPath, "*", SearchOption.AllDirectories) + .Where(s => !IsPathABlacklistedFile(s))); + + this.GenerationProgressArgs.TotalFiles = manifestFilePaths.Count; + + await using (var tw = new StreamWriter(File.Create(manifestPath, 4096, FileOptions.Asynchronous))) + { + var completedFiles = 0; + foreach (var filePath in manifestFilePaths) + { + ct.ThrowIfCancellationRequested(); + + var newEntry = CreateEntryForFile(targetPath, filePath); + + await tw.WriteLineAsync(newEntry.ToString()); + await tw.FlushAsync(); + + completedFiles++; + + this.GenerationProgressArgs.CompletedFiles = completedFiles; + this.GenerationProgressArgs.Filepath = newEntry.RelativePath; + this.GenerationProgressArgs.Hash = newEntry.Hash; + this.GenerationProgressArgs.Filesize = newEntry.Size; + + progressReporter.Report(this.GenerationProgressArgs); + } + } + + await CreateManifestChecksumAsync(manifestPath, manifestChecksumPath); + }, + ct + ); + } + + private async Task CreateManifestChecksumAsync(string manifestPath, string manifestChecksumPath) + { + // Create a checksum file for the manifest. + await using var manifestStream = File.OpenRead(manifestPath); + var manifestHash = MD5Handler.GetStreamHash(manifestStream); + + await using var checksumStream = File.Create(manifestChecksumPath, 4096, FileOptions.Asynchronous); + await using var tw = new StreamWriter(checksumStream); + await tw.WriteLineAsync(manifestHash); + await tw.FlushAsync(); + tw.Close(); + } + + private ManifestEntry CreateEntryForFile(string parentDirectory, string filePath) + { + string hash; + long fileSize; + using (var fileStream = File.OpenRead(filePath)) + { + hash = MD5Handler.GetStreamHash(fileStream); + fileSize = fileStream.Length; + } + + var relativeFilePath = filePath.Substring(parentDirectory.Length).TrimStart(Path.DirectorySeparatorChar); + var newEntry = new ManifestEntry(relativeFilePath, hash, fileSize); + + return newEntry; + } + + /// + /// Determines whether or not the specified path is blacklisted and should not be included in the manifest. + /// + /// The path to test. + /// true if the path is blackliste; otherwise, false. + private bool IsPathABlacklistedFile(string filePath) + { + return + filePath.EndsWith(".install") || + filePath.EndsWith(".update") || + filePath.EndsWith("GameManifest.txt") || + filePath.EndsWith("GameManifest.checksum"); + } + } } diff --git a/Launchpad.Utilities/Interface/MainWindow.UI.cs b/Launchpad.Utilities/Interface/MainWindow.UI.cs index 05622387..8e124d87 100644 --- a/Launchpad.Utilities/Interface/MainWindow.UI.cs +++ b/Launchpad.Utilities/Interface/MainWindow.UI.cs @@ -33,49 +33,49 @@ namespace Launchpad.Utilities.Interface { - public partial class MainWindow - { - [UIElement] private readonly FileChooserWidget FolderChooser; + public partial class MainWindow + { + [UIElement] private readonly FileChooserWidget FolderChooser; - [UIElement] private readonly Label StatusLabel; + [UIElement] private readonly Label StatusLabel; - [UIElement] private readonly ProgressBar MainProgressBar; + [UIElement] private readonly ProgressBar MainProgressBar; - [UIElement] private readonly Button GenerateLaunchpadManifestButton; - [UIElement] private readonly Button GenerateGameManifestButton; + [UIElement] private readonly Button GenerateLaunchpadManifestButton; + [UIElement] private readonly Button GenerateGameManifestButton; - /// - /// Creates a new instance of the class, loading its interface definition from file. - /// - /// An instance of the main window widget. - public static MainWindow Create() - { - using var builder = new Builder(Assembly.GetExecutingAssembly(), "Launchpad.Utilities.Interface.Launchpad.Utilities.glade", null); - return new MainWindow(builder, builder.GetObject(nameof(MainWindow)).Handle); - } + /// + /// Creates a new instance of the class, loading its interface definition from file. + /// + /// An instance of the main window widget. + public static MainWindow Create() + { + using var builder = new Builder(Assembly.GetExecutingAssembly(), "Launchpad.Utilities.Interface.Launchpad.Utilities.glade", null); + return new MainWindow(builder, builder.GetObject(nameof(MainWindow)).Handle); + } - /// - /// Binds UI-related events. - /// - private void BindUIEvents() - { - this.DeleteEvent += OnDeleteEvent; + /// + /// Binds UI-related events. + /// + private void BindUIEvents() + { + this.DeleteEvent += OnDeleteEvent; - this.GenerateLaunchpadManifestButton.Clicked += OnGenerateLaunchpadManifestButtonClicked; - this.GenerateGameManifestButton.Clicked += OnGenerateGameManifestButtonClicked; - } + this.GenerateLaunchpadManifestButton.Clicked += OnGenerateLaunchpadManifestButtonClicked; + this.GenerateGameManifestButton.Clicked += OnGenerateGameManifestButtonClicked; + } - /// - /// Exits the application properly when the window is deleted. - /// - /// Sender. - /// The alpha component. - private void OnDeleteEvent(object sender, DeleteEventArgs a) - { - this.TokenSource?.Cancel(); + /// + /// Exits the application properly when the window is deleted. + /// + /// Sender. + /// The alpha component. + private void OnDeleteEvent(object sender, DeleteEventArgs a) + { + this.TokenSource?.Cancel(); - Application.Quit(); - a.RetVal = true; - } - } + Application.Quit(); + a.RetVal = true; + } + } } diff --git a/Launchpad.Utilities/Interface/MainWindow.cs b/Launchpad.Utilities/Interface/MainWindow.cs index 495d1ee8..7d1a72e8 100644 --- a/Launchpad.Utilities/Interface/MainWindow.cs +++ b/Launchpad.Utilities/Interface/MainWindow.cs @@ -33,118 +33,118 @@ namespace Launchpad.Utilities.Interface { - public partial class MainWindow : Window - { - /// - /// The manifest generation handler. - /// - private readonly ManifestGenerationHandler Manifest = new ManifestGenerationHandler(); - - /// - /// The localization catalog. - /// - private readonly ICatalog LocalizationCatalog = new Catalog("Launchpad", "./Content/locale"); - - private readonly IProgress ProgressReporter; - - private CancellationTokenSource TokenSource; - - /// - /// Initializes a new instance of the class. - /// - /// The UI builder. - /// The native handle of the window. - private MainWindow(Builder builder, IntPtr handle) - : base(handle) - { - builder.Autoconnect(this); - - BindUIEvents(); - - this.ProgressReporter = new Progress - ( - e => - { - var progressString = this.LocalizationCatalog.GetString("Hashing {0} : {1} out of {2}"); - this.StatusLabel.Text = string.Format(progressString, e.Filepath, e.CompletedFiles, e.TotalFiles); - - this.MainProgressBar.Fraction = e.CompletedFiles / (double)e.TotalFiles; - } - ); - - this.FolderChooser.SetCurrentFolder(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)); - this.FolderChooser.SelectMultiple = false; - - this.StatusLabel.Text = this.LocalizationCatalog.GetString("Idle"); - } - - private async void OnGenerateGameManifestButtonClicked(object sender, EventArgs e) - { - var targetDirectory = this.FolderChooser.Filename; - - if (!Directory.GetFiles(targetDirectory).Any(s => s.Contains("GameVersion.txt"))) - { - using var dialog = new MessageDialog - ( - this, - DialogFlags.Modal, - MessageType.Question, - ButtonsType.YesNo, - this.LocalizationCatalog.GetString - ( - "No GameVersion.txt file could be found in the target directory. This file is required.\n" + - "Would you like to add one? The version will be \"1.0.0\"." - ) - ); - - if (dialog.Run() == (int) ResponseType.Yes) - { - var gameVersionPath = SysPath.Combine(targetDirectory, "GameVersion.txt"); - await File.WriteAllTextAsync(gameVersionPath, new Version("1.0.0").ToString()); - } - else - { - return; - } - } - - await GenerateManifestAsync(EManifestType.Game); - } - - private async void OnGenerateLaunchpadManifestButtonClicked(object sender, EventArgs e) - { - await GenerateManifestAsync(EManifestType.Launchpad); - } - - private async Task GenerateManifestAsync(EManifestType manifestType) - { - this.TokenSource = new CancellationTokenSource(); - - this.GenerateGameManifestButton.Sensitive = false; - this.GenerateLaunchpadManifestButton.Sensitive = false; - - var targetDirectory = this.FolderChooser.Filename; - - try - { - await this.Manifest.GenerateManifestAsync - ( - targetDirectory, - manifestType, - this.ProgressReporter, - this.TokenSource.Token - ); - - this.StatusLabel.Text = this.LocalizationCatalog.GetString("Finished"); - } - catch (TaskCanceledException) - { - this.StatusLabel.Text = this.LocalizationCatalog.GetString("Cancelled"); - this.MainProgressBar.Fraction = 0; - } - - this.GenerateGameManifestButton.Sensitive = true; - this.GenerateLaunchpadManifestButton.Sensitive = true; - } - } + public partial class MainWindow : Window + { + /// + /// The manifest generation handler. + /// + private readonly ManifestGenerationHandler Manifest = new ManifestGenerationHandler(); + + /// + /// The localization catalog. + /// + private readonly ICatalog LocalizationCatalog = new Catalog("Launchpad", "./Content/locale"); + + private readonly IProgress ProgressReporter; + + private CancellationTokenSource TokenSource; + + /// + /// Initializes a new instance of the class. + /// + /// The UI builder. + /// The native handle of the window. + private MainWindow(Builder builder, IntPtr handle) + : base(handle) + { + builder.Autoconnect(this); + + BindUIEvents(); + + this.ProgressReporter = new Progress + ( + e => + { + var progressString = this.LocalizationCatalog.GetString("Hashing {0} : {1} out of {2}"); + this.StatusLabel.Text = string.Format(progressString, e.Filepath, e.CompletedFiles, e.TotalFiles); + + this.MainProgressBar.Fraction = e.CompletedFiles / (double)e.TotalFiles; + } + ); + + this.FolderChooser.SetCurrentFolder(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)); + this.FolderChooser.SelectMultiple = false; + + this.StatusLabel.Text = this.LocalizationCatalog.GetString("Idle"); + } + + private async void OnGenerateGameManifestButtonClicked(object sender, EventArgs e) + { + var targetDirectory = this.FolderChooser.Filename; + + if (!Directory.GetFiles(targetDirectory).Any(s => s.Contains("GameVersion.txt"))) + { + using var dialog = new MessageDialog + ( + this, + DialogFlags.Modal, + MessageType.Question, + ButtonsType.YesNo, + this.LocalizationCatalog.GetString + ( + "No GameVersion.txt file could be found in the target directory. This file is required.\n" + + "Would you like to add one? The version will be \"1.0.0\"." + ) + ); + + if (dialog.Run() == (int) ResponseType.Yes) + { + var gameVersionPath = SysPath.Combine(targetDirectory, "GameVersion.txt"); + await File.WriteAllTextAsync(gameVersionPath, new Version("1.0.0").ToString()); + } + else + { + return; + } + } + + await GenerateManifestAsync(EManifestType.Game); + } + + private async void OnGenerateLaunchpadManifestButtonClicked(object sender, EventArgs e) + { + await GenerateManifestAsync(EManifestType.Launchpad); + } + + private async Task GenerateManifestAsync(EManifestType manifestType) + { + this.TokenSource = new CancellationTokenSource(); + + this.GenerateGameManifestButton.Sensitive = false; + this.GenerateLaunchpadManifestButton.Sensitive = false; + + var targetDirectory = this.FolderChooser.Filename; + + try + { + await this.Manifest.GenerateManifestAsync + ( + targetDirectory, + manifestType, + this.ProgressReporter, + this.TokenSource.Token + ); + + this.StatusLabel.Text = this.LocalizationCatalog.GetString("Finished"); + } + catch (TaskCanceledException) + { + this.StatusLabel.Text = this.LocalizationCatalog.GetString("Cancelled"); + this.MainProgressBar.Fraction = 0; + } + + this.GenerateGameManifestButton.Sensitive = true; + this.GenerateLaunchpadManifestButton.Sensitive = true; + } + } } diff --git a/Launchpad.Utilities/Options/CLIOptions.cs b/Launchpad.Utilities/Options/CLIOptions.cs index afdf3950..598f0000 100644 --- a/Launchpad.Utilities/Options/CLIOptions.cs +++ b/Launchpad.Utilities/Options/CLIOptions.cs @@ -24,32 +24,32 @@ namespace Launchpad.Utilities.Options { - public class CLIOptions - { - [Option('b', "batch", Required = false, - HelpText = "Run the utilities in batch mode without a UI.")] - public bool RunBatchProcessing - { - get; - set; - } + public class CLIOptions + { + [Option('b', "batch", Required = false, + HelpText = "Run the utilities in batch mode without a UI.")] + public bool RunBatchProcessing + { + get; + set; + } - [Option('d', "directory", Required = false, - HelpText = "The target directory from which the manifest should be generated.")] - public string TargetDirectory - { - get; - set; - } + [Option('d', "directory", Required = false, + HelpText = "The target directory from which the manifest should be generated.")] + public string TargetDirectory + { + get; + set; + } - [Option('m', "manifest", Required = false, - HelpText = "The type of manifest that should be generated. This only affects the output filename, " + - "and not the actual file content. " + - "Valid manifests: Game or Launcher")] - public EManifestType ManifestType - { - get; - set; - } - } -} \ No newline at end of file + [Option('m', "manifest", Required = false, + HelpText = "The type of manifest that should be generated. This only affects the output filename, " + + "and not the actual file content. " + + "Valid manifests: Game or Launcher")] + public EManifestType ManifestType + { + get; + set; + } + } +} diff --git a/Launchpad.Utilities/Program.cs b/Launchpad.Utilities/Program.cs index 72762061..815306e2 100644 --- a/Launchpad.Utilities/Program.cs +++ b/Launchpad.Utilities/Program.cs @@ -36,79 +36,79 @@ namespace Launchpad.Utilities { - internal static class Program - { - /// - /// Logger instance for this class. - /// - private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); + internal static class Program + { + /// + /// Logger instance for this class. + /// + private static readonly ILogger Log = LogManager.GetCurrentClassLogger(); - /// - /// The main entry point for the application. - /// - private static async Task Main(string[] args) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Environment.SetEnvironmentVariable("GSETTINGS_SCHEMA_DIR", "share\\glib-2.0\\schemas\\"); - } + /// + /// The main entry point for the application. + /// + private static async Task Main(string[] args) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Environment.SetEnvironmentVariable("GSETTINGS_SCHEMA_DIR", "share\\glib-2.0\\schemas\\"); + } - var options = new CLIOptions(); - Parser.Default.ParseArguments(args) - .WithParsed(r => options = r) - .WithNotParsed(r => options = null); + var options = new CLIOptions(); + Parser.Default.ParseArguments(args) + .WithParsed(r => options = r) + .WithNotParsed(r => options = null); - if (options is null) - { - // Parsing probably failed, bail out - return; - } + if (options is null) + { + // Parsing probably failed, bail out + return; + } - if (options.RunBatchProcessing) - { - if (string.IsNullOrEmpty(options.TargetDirectory) || options.ManifestType == EManifestType.Unknown) - { - Log.Error("Target directory not set, or manifest type not set."); - return; - } + if (options.RunBatchProcessing) + { + if (string.IsNullOrEmpty(options.TargetDirectory) || options.ManifestType == EManifestType.Unknown) + { + Log.Error("Target directory not set, or manifest type not set."); + return; + } - // At this point, the options should be valid. Run batch processing. - if (Directory.Exists(options.TargetDirectory)) - { - Log.Info("Generating manifest..."); + // At this point, the options should be valid. Run batch processing. + if (Directory.Exists(options.TargetDirectory)) + { + Log.Info("Generating manifest..."); - var manifestGenerationHandler = new ManifestGenerationHandler(); + var manifestGenerationHandler = new ManifestGenerationHandler(); - var progressReporter = new Progress - ( - e => Log.Info($"Processed file {e.Filepath} : {e.Hash} : {e.Filesize}") - ); + var progressReporter = new Progress + ( + e => Log.Info($"Processed file {e.Filepath} : {e.Hash} : {e.Filesize}") + ); - await manifestGenerationHandler.GenerateManifestAsync - ( - options.TargetDirectory, - options.ManifestType, - progressReporter, - CancellationToken.None - ); + await manifestGenerationHandler.GenerateManifestAsync + ( + options.TargetDirectory, + options.ManifestType, + progressReporter, + CancellationToken.None + ); - Log.Info("Generation finished."); - } - else - { - Log.Error("The selected directory did not exist."); - } - } - else if (string.IsNullOrEmpty(options.TargetDirectory) && options.ManifestType == EManifestType.Unknown) - { - // Run a GTK UI instead of batch processing - Application.Init(); - SynchronizationContext.SetSynchronizationContext(new GLibSynchronizationContext()); + Log.Info("Generation finished."); + } + else + { + Log.Error("The selected directory did not exist."); + } + } + else if (string.IsNullOrEmpty(options.TargetDirectory) && options.ManifestType == EManifestType.Unknown) + { + // Run a GTK UI instead of batch processing + Application.Init(); + SynchronizationContext.SetSynchronizationContext(new GLibSynchronizationContext()); - var win = MainWindow.Create(); - win.Show(); - Application.Run(); - } - } - } + var win = MainWindow.Create(); + win.Show(); + Application.Run(); + } + } + } } diff --git a/Launchpad.Utilities/Utility/DirectoryHelpers.cs b/Launchpad.Utilities/Utility/DirectoryHelpers.cs index fe635de7..15b775f4 100644 --- a/Launchpad.Utilities/Utility/DirectoryHelpers.cs +++ b/Launchpad.Utilities/Utility/DirectoryHelpers.cs @@ -25,20 +25,20 @@ namespace Launchpad.Utilities.Utility { - /// - /// Contains helper functions for directory manipulation. - /// - public static class DirectoryHelpers - { - /// - /// Gets the assembly-local directory, that is, the directory where the executing assembly resides. - /// - /// The local dir, terminated by a directory separator. - public static string GetLocalDir() - { - var codeBaseURI = new UriBuilder(Assembly.GetExecutingAssembly().Location).Uri; + /// + /// Contains helper functions for directory manipulation. + /// + public static class DirectoryHelpers + { + /// + /// Gets the assembly-local directory, that is, the directory where the executing assembly resides. + /// + /// The local dir, terminated by a directory separator. + public static string GetLocalDir() + { + var codeBaseURI = new UriBuilder(Assembly.GetExecutingAssembly().Location).Uri; - return Path.GetDirectoryName(Uri.UnescapeDataString(codeBaseURI.AbsolutePath)); - } - } + return Path.GetDirectoryName(Uri.UnescapeDataString(codeBaseURI.AbsolutePath)); + } + } } diff --git a/Launchpad.Utilities/Utility/Events/ManifestGenerationProgressChangedEventArgs.cs b/Launchpad.Utilities/Utility/Events/ManifestGenerationProgressChangedEventArgs.cs index 33e6e22b..3836819d 100644 --- a/Launchpad.Utilities/Utility/Events/ManifestGenerationProgressChangedEventArgs.cs +++ b/Launchpad.Utilities/Utility/Events/ManifestGenerationProgressChangedEventArgs.cs @@ -23,37 +23,37 @@ namespace Launchpad.Utilities.Utility.Events { - public class ManifestGenerationProgressChangedEventArgs : EventArgs - { - public string Filepath - { - get; - set; - } + public class ManifestGenerationProgressChangedEventArgs : EventArgs + { + public string Filepath + { + get; + set; + } - public int TotalFiles - { - get; - set; - } + public int TotalFiles + { + get; + set; + } - public int CompletedFiles - { - get; - set; - } + public int CompletedFiles + { + get; + set; + } - public string Hash - { - get; - set; - } + public string Hash + { + get; + set; + } - public long Filesize - { - get; - set; - } - } + public long Filesize + { + get; + set; + } + } } diff --git a/Scripts/launchpad-publish.sh b/Scripts/launchpad-publish.sh index 80a511df..e82dff79 100755 --- a/Scripts/launchpad-publish.sh +++ b/Scripts/launchpad-publish.sh @@ -30,7 +30,7 @@ echo -e "$LOG_PREFIX Copying files to output directory... $LOG_SUFFIX" # Create the root output directory if [ ! -d "$OUTPUT_ROOT" ]; then - mkdir "$OUTPUT_ROOT" + mkdir "$OUTPUT_ROOT" fi # Create a staging folder for this version @@ -93,46 +93,46 @@ echo -e "$LOG_PREFIX_RED Selecting this option will replace and publish this bui read -p "[y/n]" -r echo "" # (optional) move to a new line if [[ $REPLY =~ ^[Yy]$ ]]; then - read -p "Enter remote host: " -r REMOTEHOST - read -p "Enter remote username: " -r REMOTEUSER - read -p "Enter full path to remote upload directory [/srv/ftp/launcher/]: " -r REMOTEUPLOAD - read -p "Enter full path to remote binary directory [/var/www/files/public/Launchpad/Binaries]: " -r REMOTEUPLOADBINARIES - - # Give the remote launcher dir a default value if no input was provided - if [ -z "$REMOTEUPLOAD" ]; then - REMOTEUPLOAD="/srv/ftp/launcher/" - fi - - # Give the remote binary dir a default value if no input was provided - if [ -z "$REMOTEUPLOADBINARIES" ]; then - REMOTEUPLOADBINARIES="/var/www/files/public/Launchpad/Binaries" - fi - - # Make sure the remote launcher dir ends with a slash - if [[ ! "$REMOTEUPLOADBINARIES" == */ ]]; then - REMOTEUPLOAD+="/" - fi - - # Make sure the remote binary dir ends with a slash - if [[ ! "$REMOTEUPLOADBINARIES" == */ ]]; then - REMOTEUPLOAD+="/" - fi - - echo "" - echo -e "$LOG_PREFIX Uploading files to remote server... $LOG_SUFFIX" - - # Upload the binaries used by the launcher to update itself - ssh $REMOTEUSER@$REMOTEHOST "mkdir -p $REMOTEUPLOAD" + read -p "Enter remote host: " -r REMOTEHOST + read -p "Enter remote username: " -r REMOTEUSER + read -p "Enter full path to remote upload directory [/srv/ftp/launcher/]: " -r REMOTEUPLOAD + read -p "Enter full path to remote binary directory [/var/www/files/public/Launchpad/Binaries]: " -r REMOTEUPLOADBINARIES + + # Give the remote launcher dir a default value if no input was provided + if [ -z "$REMOTEUPLOAD" ]; then + REMOTEUPLOAD="/srv/ftp/launcher/" + fi + + # Give the remote binary dir a default value if no input was provided + if [ -z "$REMOTEUPLOADBINARIES" ]; then + REMOTEUPLOADBINARIES="/var/www/files/public/Launchpad/Binaries" + fi + + # Make sure the remote launcher dir ends with a slash + if [[ ! "$REMOTEUPLOADBINARIES" == */ ]]; then + REMOTEUPLOAD+="/" + fi + + # Make sure the remote binary dir ends with a slash + if [[ ! "$REMOTEUPLOADBINARIES" == */ ]]; then + REMOTEUPLOAD+="/" + fi + + echo "" + echo -e "$LOG_PREFIX Uploading files to remote server... $LOG_SUFFIX" + + # Upload the binaries used by the launcher to update itself + ssh $REMOTEUSER@$REMOTEHOST "mkdir -p $REMOTEUPLOAD" #scp -r "$OUTPUT_ROOT/launchpad-$LAUNCHPAD_ASSEMBLY_VERSION/." "$REMOTEUSER@$REMOTEHOST:$REMOTEUPLOAD" - + # Upload new packaged binaries (zip, tarball, debian) ssh $REMOTEUSER@$REMOTEHOST "mkdir -p $REMOTEUPLOADBINARIES" scp "$OUTPUT_ROOT/launchpad-$LAUNCHPAD_ASSEMBLY_VERSION.zip" "$REMOTEUSER@$REMOTEHOST:$REMOTEUPLOADBINARIES" scp "$OUTPUT_ROOT/launchpad-$LAUNCHPAD_ASSEMBLY_VERSION.tar.xz" "$REMOTEUSER@$REMOTEHOST:$REMOTEUPLOADBINARIES" scp "$OUTPUT_ROOT/launchpad-$LAUNCHPAD_ASSEMBLY_VERSION-all.deb" "$REMOTEUSER@$REMOTEHOST:$REMOTEUPLOADBINARIES" - + echo "" - echo -e "$LOG_PREFIX Upload successful! $LOG_SUFFIX" + echo -e "$LOG_PREFIX Upload successful! $LOG_SUFFIX" fi echo "" diff --git a/Scripts/update-translations.sh b/Scripts/update-translations.sh index fa70b6ce..c652d12c 100755 --- a/Scripts/update-translations.sh +++ b/Scripts/update-translations.sh @@ -14,26 +14,26 @@ unzip launchpad.zip for D in `find . -type d` do - LOCALE_NAME=${D/./} - LOCALE_NAME=${LOCALE_NAME///} - LOCALE_NAME=${LOCALE_NAME/-/_} - - if [ ! -z "$LOCALE_NAME" ]; - then - cp "$D/messages.po" "../$LOCALE_NAME.po" - - mkdir -p "../../Launchpad.Launcher/Content/locale/$LOCALE_NAME/LC_MESSAGES/" - cp "$D/messages.po" "../../Launchpad.Launcher/Content/locale/$LOCALE_NAME/LC_MESSAGES/messages.po" - fi + LOCALE_NAME=${D/./} + LOCALE_NAME=${LOCALE_NAME///} + LOCALE_NAME=${LOCALE_NAME/-/_} + + if [ ! -z "$LOCALE_NAME" ]; + then + cp "$D/messages.po" "../$LOCALE_NAME.po" + + mkdir -p "../../Launchpad.Launcher/Content/locale/$LOCALE_NAME/LC_MESSAGES/" + cp "$D/messages.po" "../../Launchpad.Launcher/Content/locale/$LOCALE_NAME/LC_MESSAGES/messages.po" + fi done rm launchpad.zip for D in `find . -type d` do - if [ ! $D == "." ]; then - rm -r $D - fi + if [ ! $D == "." ]; then + rm -r $D + fi done cd ..