Files
ArchiSteamFarm/ArchiSteamFarm/Core/Utilities.cs

566 lines
20 KiB
C#
Raw Normal View History

// ----------------------------------------------------------------------------------------------
2019-02-16 17:34:17 +01:00
// _ _ _ ____ _ _____
2017-11-18 17:27:06 +01:00
// / \ _ __ ___ | |__ (_)/ ___| | |_ ___ __ _ _ __ ___ | ___|__ _ _ __ _ __ ___
// / _ \ | '__|/ __|| '_ \ | |\___ \ | __|/ _ \ / _` || '_ ` _ \ | |_ / _` || '__|| '_ ` _ \
// / ___ \ | | | (__ | | | || | ___) || |_| __/| (_| || | | | | || _|| (_| || | | | | | | |
// /_/ \_\|_| \___||_| |_||_||____/ \__|\___| \__,_||_| |_| |_||_| \__,_||_| |_| |_| |_|
// ----------------------------------------------------------------------------------------------
2024-03-17 02:35:40 +01:00
// |
// Copyright 2015-2024 Łukasz "JustArchi" Domeradzki
2018-07-27 04:52:14 +02:00
// Contact: JustArchi@JustArchi.net
2024-03-17 02:35:40 +01:00
// |
2018-07-27 04:52:14 +02:00
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
2024-03-17 02:35:40 +01:00
// |
2018-07-27 04:52:14 +02:00
// http://www.apache.org/licenses/LICENSE-2.0
2024-03-17 02:35:40 +01:00
// |
2018-07-27 04:52:14 +02:00
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2015-10-28 19:21:27 +01:00
2016-04-01 20:18:21 +02:00
using System;
using System.Collections;
2024-01-03 13:46:54 +01:00
using System.Collections.Frozen;
2017-01-28 15:29:38 +01:00
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
2019-04-30 14:31:24 +02:00
using System.IO;
using System.IO.Compression;
2016-04-01 20:18:21 +02:00
using System.Linq;
using System.Net;
using System.Resources;
using System.Security.Cryptography;
2018-02-26 19:48:29 +01:00
using System.Threading;
using System.Threading.Tasks;
using AngleSharp.Dom;
using AngleSharp.XPath;
using ArchiSteamFarm.Localization;
using ArchiSteamFarm.NLog;
using ArchiSteamFarm.Storage;
using Humanizer;
2019-01-10 23:44:32 +01:00
using JetBrains.Annotations;
using Microsoft.IdentityModel.JsonWebTokens;
using SteamKit2;
2015-10-25 06:16:50 +01:00
2021-11-10 21:23:24 +01:00
namespace ArchiSteamFarm.Core;
2021-11-10 21:23:24 +01:00
public static class Utilities {
private const byte MaxSharingViolationTries = 15;
private const uint SharingViolationHResult = 0x80070020;
2021-11-10 21:23:24 +01:00
private const byte TimeoutForLongRunningTasksInSeconds = 60;
2024-04-05 12:29:41 +02:00
private const uint UnauthorizedAccessHResult = 0x80070005;
private static readonly FrozenSet<char> DirectorySeparators = new HashSet<char>(2) { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.ToFrozenSet();
Add workaround for LINQ race condition with concurrent collections This is some next-level race condition, so for those interested: - Concurrent collections are thread-safe in a way that each operation is atomic - Naturally if you call two atomic operations in a row, the result is no longer atomic, since there could be some changes between the first and the last - Certain LINQ operations such as OrderBy(), Reverse(), ToArray(), among more, use internal buffer for operation with certain optimization that checks if input is ICollection, if yes, it calls Count and CopyTo(), for OrderBy in this example - In result, such LINQ call is not guaranteed to be thread-safe, since it assumes those two calls to be atomic, while they're not in reality. This issue is quite hard to spot in real applications, since it's not that easy to trigger it (you need to call the operation on ICollection and then have another thread modifying it while enumerating). This is probably why we've never had any real problem until I've discovered this madness with @Aareksio in entirely different project. As a workaround, we'll explicitly convert some ICollection inputs to IEnumerable, in particular around OrderBy(), so the optimization is skipped and the result is not corrupted. I've added unit tests which ensure this workaround works properly, and you can easily reproduce the problem by removing AsLinqThreadSafeEnumerable() in them. See https://github.com/dotnet/runtime/discussions/50687 for more insight I have no clue who thought that ignoring this issue is a good idea, at the very least concurrent collections should have opt-out mechanism from those optimizations, there is no reason for them to not do that.
2024-08-16 03:25:58 +02:00
[PublicAPI]
public static IEnumerable<T> AsLinqThreadSafeEnumerable<T>(this ICollection<T> collection) {
ArgumentNullException.ThrowIfNull(collection);
Add workaround for LINQ race condition with concurrent collections This is some next-level race condition, so for those interested: - Concurrent collections are thread-safe in a way that each operation is atomic - Naturally if you call two atomic operations in a row, the result is no longer atomic, since there could be some changes between the first and the last - Certain LINQ operations such as OrderBy(), Reverse(), ToArray(), among more, use internal buffer for operation with certain optimization that checks if input is ICollection, if yes, it calls Count and CopyTo(), for OrderBy in this example - In result, such LINQ call is not guaranteed to be thread-safe, since it assumes those two calls to be atomic, while they're not in reality. This issue is quite hard to spot in real applications, since it's not that easy to trigger it (you need to call the operation on ICollection and then have another thread modifying it while enumerating). This is probably why we've never had any real problem until I've discovered this madness with @Aareksio in entirely different project. As a workaround, we'll explicitly convert some ICollection inputs to IEnumerable, in particular around OrderBy(), so the optimization is skipped and the result is not corrupted. I've added unit tests which ensure this workaround works properly, and you can easily reproduce the problem by removing AsLinqThreadSafeEnumerable() in them. See https://github.com/dotnet/runtime/discussions/50687 for more insight I have no clue who thought that ignoring this issue is a good idea, at the very least concurrent collections should have opt-out mechanism from those optimizations, there is no reason for them to not do that.
2024-08-16 03:25:58 +02:00
// See: https://github.com/dotnet/runtime/discussions/50687
return collection.Select(static entry => entry);
Add workaround for LINQ race condition with concurrent collections This is some next-level race condition, so for those interested: - Concurrent collections are thread-safe in a way that each operation is atomic - Naturally if you call two atomic operations in a row, the result is no longer atomic, since there could be some changes between the first and the last - Certain LINQ operations such as OrderBy(), Reverse(), ToArray(), among more, use internal buffer for operation with certain optimization that checks if input is ICollection, if yes, it calls Count and CopyTo(), for OrderBy in this example - In result, such LINQ call is not guaranteed to be thread-safe, since it assumes those two calls to be atomic, while they're not in reality. This issue is quite hard to spot in real applications, since it's not that easy to trigger it (you need to call the operation on ICollection and then have another thread modifying it while enumerating). This is probably why we've never had any real problem until I've discovered this madness with @Aareksio in entirely different project. As a workaround, we'll explicitly convert some ICollection inputs to IEnumerable, in particular around OrderBy(), so the optimization is skipped and the result is not corrupted. I've added unit tests which ensure this workaround works properly, and you can easily reproduce the problem by removing AsLinqThreadSafeEnumerable() in them. See https://github.com/dotnet/runtime/discussions/50687 for more insight I have no clue who thought that ignoring this issue is a good idea, at the very least concurrent collections should have opt-out mechanism from those optimizations, there is no reason for them to not do that.
2024-08-16 03:25:58 +02:00
}
[PublicAPI]
public static string GenerateChecksumFor(byte[] source) {
ArgumentNullException.ThrowIfNull(source);
byte[] hash = SHA512.HashData(source);
return Convert.ToHexString(hash);
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static string GetArgsAsText(string[] args, byte argsToSkip, string delimiter) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(args);
2019-01-15 12:30:29 +01:00
2021-11-10 21:23:24 +01:00
if (args.Length <= argsToSkip) {
throw new InvalidOperationException($"{nameof(args.Length)} && {nameof(argsToSkip)}");
2019-01-15 12:30:29 +01:00
}
ArgumentException.ThrowIfNullOrEmpty(delimiter);
2019-01-15 12:30:29 +01:00
2021-11-10 21:23:24 +01:00
return string.Join(delimiter, args.Skip(argsToSkip));
}
2019-01-15 12:30:29 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static string GetArgsAsText(string text, byte argsToSkip) {
ArgumentException.ThrowIfNullOrEmpty(text);
2019-01-15 12:30:29 +01:00
2021-11-10 21:23:24 +01:00
string[] args = text.Split(Array.Empty<char>(), argsToSkip + 1, StringSplitOptions.RemoveEmptyEntries);
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
return args[^1];
}
2020-11-14 22:37:00 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static string? GetCookieValue(this CookieContainer cookieContainer, Uri uri, string name) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(cookieContainer);
ArgumentNullException.ThrowIfNull(uri);
ArgumentException.ThrowIfNullOrEmpty(name);
2021-11-10 21:23:24 +01:00
CookieCollection cookies = cookieContainer.GetCookies(uri);
2020-09-13 21:43:25 +02:00
2023-12-02 15:16:26 +01:00
return cookies.FirstOrDefault(cookie => cookie.Name == name)?.Value;
2021-11-10 21:23:24 +01:00
}
2020-09-13 21:43:25 +02:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static ulong GetUnixTime() => (ulong) DateTimeOffset.UtcNow.ToUnixTimeSeconds();
2016-12-23 03:37:29 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static async void InBackground(Action action, bool longRunning = false) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(action);
2021-11-10 21:23:24 +01:00
TaskCreationOptions options = TaskCreationOptions.DenyChildAttach;
2021-11-10 21:23:24 +01:00
if (longRunning) {
options |= TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness;
}
2021-11-10 21:23:24 +01:00
await Task.Factory.StartNew(action, CancellationToken.None, options, TaskScheduler.Default).ConfigureAwait(false);
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static void InBackground<T>(Func<T> function, bool longRunning = false) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(function);
2023-08-10 21:36:17 +02:00
InBackground(void () => function(), longRunning);
2021-11-10 21:23:24 +01:00
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static async Task<IList<T>> InParallel<T>(IEnumerable<Task<T>> tasks) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(tasks);
2021-11-10 21:23:24 +01:00
switch (ASF.GlobalConfig?.OptimizationMode) {
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
2023-12-11 23:55:13 +01:00
List<T> results = [];
2021-11-10 21:23:24 +01:00
foreach (Task<T> task in tasks) {
results.Add(await task.ConfigureAwait(false));
}
2018-12-15 00:27:15 +01:00
return results;
2021-11-10 21:23:24 +01:00
default:
return await Task.WhenAll(tasks).ConfigureAwait(false);
}
2021-11-10 21:23:24 +01:00
}
[PublicAPI]
public static async Task InParallel(IEnumerable<Task> tasks) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(tasks);
2021-11-10 21:23:24 +01:00
switch (ASF.GlobalConfig?.OptimizationMode) {
case GlobalConfig.EOptimizationMode.MinMemoryUsage:
foreach (Task task in tasks) {
await task.ConfigureAwait(false);
}
2021-11-10 21:23:24 +01:00
break;
default:
await Task.WhenAll(tasks).ConfigureAwait(false);
2018-12-15 00:27:15 +01:00
2021-11-10 21:23:24 +01:00
break;
}
2021-11-10 21:23:24 +01:00
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static bool IsClientErrorCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError;
2020-08-23 19:24:10 +02:00
[PublicAPI]
public static bool IsRedirectionCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.Ambiguous and < HttpStatusCode.BadRequest;
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static bool IsServerErrorCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.InternalServerError and < (HttpStatusCode) 600;
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static bool IsSuccessCode(this HttpStatusCode statusCode) => statusCode is >= HttpStatusCode.OK and < HttpStatusCode.Ambiguous;
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static bool IsValidCdKey(string key) {
ArgumentException.ThrowIfNullOrEmpty(key);
2022-11-16 12:33:17 +01:00
return GeneratedRegexes.CdKey().IsMatch(key);
2021-11-10 21:23:24 +01:00
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static bool IsValidHexadecimalText(string text) {
ArgumentException.ThrowIfNullOrEmpty(text);
2021-11-10 21:23:24 +01:00
return (text.Length % 2 == 0) && text.All(Uri.IsHexDigit);
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static IList<INode> SelectNodes(this IDocument document, string xpath) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(document);
return document.Body.SelectNodes(xpath);
2021-11-10 21:23:24 +01:00
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static IEnumerable<T> SelectNodes<T>(this IDocument document, string xpath) where T : class, INode {
ArgumentNullException.ThrowIfNull(document);
2022-08-08 12:40:19 +02:00
return document.Body.SelectNodes(xpath).OfType<T>();
}
[PublicAPI]
public static IEnumerable<T> SelectNodes<T>(this IElement element, string xpath) where T : class, INode {
2022-06-04 21:41:07 +02:00
ArgumentNullException.ThrowIfNull(element);
return element.SelectNodes(xpath).OfType<T>();
2022-06-04 21:41:07 +02:00
}
2021-05-06 23:32:50 +02:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static INode? SelectSingleNode(this IDocument document, string xpath) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(document);
return document.Body.SelectSingleNode(xpath);
}
[PublicAPI]
public static T? SelectSingleNode<T>(this IDocument document, string xpath) where T : class, INode {
ArgumentNullException.ThrowIfNull(document);
return document.Body.SelectSingleNode(xpath) as T;
}
[PublicAPI]
public static T? SelectSingleNode<T>(this IElement element, string xpath) where T : class, INode {
ArgumentNullException.ThrowIfNull(element);
return element.SelectSingleNode(xpath) as T;
2021-11-10 21:23:24 +01:00
}
2019-01-12 17:51:24 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static IEnumerable<T> ToEnumerable<T>(this T item) {
yield return item;
}
2019-01-12 17:51:24 +01:00
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static string ToHumanReadable(this TimeSpan timeSpan) => timeSpan.Humanize(3, maxUnit: TimeUnit.Year, minUnit: TimeUnit.Second);
[PublicAPI]
public static Task<T> ToLongRunningTask<T>(this AsyncJob<T> job) where T : CallbackMsg {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(job);
2021-11-10 21:23:24 +01:00
job.Timeout = TimeSpan.FromSeconds(TimeoutForLongRunningTasksInSeconds);
2021-11-10 21:23:24 +01:00
return job.ToTask();
}
2021-11-10 21:23:24 +01:00
[PublicAPI]
public static Task<AsyncJobMultiple<T>.ResultSet> ToLongRunningTask<T>(this AsyncJobMultiple<T> job) where T : CallbackMsg {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(job);
2021-11-10 21:23:24 +01:00
job.Timeout = TimeSpan.FromSeconds(TimeoutForLongRunningTasksInSeconds);
2021-11-10 21:23:24 +01:00
return job.ToTask();
}
[PublicAPI]
public static bool TryReadJsonWebToken(string token, [NotNullWhen(true)] out JsonWebToken? result) {
ArgumentException.ThrowIfNullOrEmpty(token);
try {
result = new JsonWebToken(token);
} catch (Exception e) {
ASF.ArchiLogger.LogGenericDebuggingException(e);
result = null;
return false;
}
return true;
}
internal static ulong MathAdd(ulong first, int second) {
if (second >= 0) {
2022-06-05 12:39:04 +02:00
return first + (uint) second;
}
2022-06-05 12:39:04 +02:00
return first - (uint) -second;
}
internal static void OnProgressChanged(string fileName, byte progressPercentage) {
ArgumentException.ThrowIfNullOrEmpty(fileName);
ArgumentOutOfRangeException.ThrowIfGreaterThan(progressPercentage, 100);
const byte printEveryPercentage = 10;
if (progressPercentage % printEveryPercentage != 0) {
return;
2021-11-10 21:23:24 +01:00
}
ASF.ArchiLogger.LogGenericDebug($"{fileName} {progressPercentage}%...");
2021-11-10 21:23:24 +01:00
}
internal static async Task<bool> UpdateCleanup(string targetDirectory) {
ArgumentException.ThrowIfNullOrEmpty(targetDirectory);
bool updateCleanup = false;
try {
string updateDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectoryNew);
if (Directory.Exists(updateDirectory)) {
if (!updateCleanup) {
updateCleanup = true;
ASF.ArchiLogger.LogGenericInfo(Strings.UpdateCleanup);
}
Directory.Delete(updateDirectory, true);
}
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectoryOld);
if (Directory.Exists(backupDirectory)) {
if (!updateCleanup) {
updateCleanup = true;
ASF.ArchiLogger.LogGenericInfo(Strings.UpdateCleanup);
}
await DeletePotentiallyUsedDirectory(backupDirectory).ConfigureAwait(false);
}
} catch (Exception e) {
ASF.ArchiLogger.LogGenericException(e);
return false;
}
if (updateCleanup) {
ASF.ArchiLogger.LogGenericInfo(Strings.Done);
}
return true;
}
internal static async Task<bool> UpdateFromArchive(ZipArchive zipArchive, string targetDirectory) {
ArgumentNullException.ThrowIfNull(zipArchive);
ArgumentException.ThrowIfNullOrEmpty(targetDirectory);
// Firstly, ensure once again our directories are purged and ready to work with
if (!await UpdateCleanup(targetDirectory).ConfigureAwait(false)) {
return false;
}
// Now extract the zip file to entirely new location, this decreases chance of corruptions if user kills the process during this stage
string updateDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectoryNew);
zipArchive.ExtractToDirectory(updateDirectory, true);
// Now, critical section begins, we're going to move all files from target directory to a backup directory
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectoryOld);
Directory.CreateDirectory(backupDirectory);
MoveAllUpdateFiles(targetDirectory, backupDirectory);
// Finally, we can move the newly extracted files to target directory
MoveAllUpdateFiles(updateDirectory, targetDirectory, backupDirectory);
// Critical section has finished, we can now cleanup the update directory, backup directory must wait for the process restart
Directory.Delete(updateDirectory, true);
// The update process is done
return true;
}
2021-11-10 21:23:24 +01:00
internal static void WarnAboutIncompleteTranslation(ResourceManager resourceManager) {
2021-12-12 01:12:54 +01:00
ArgumentNullException.ThrowIfNull(resourceManager);
2021-11-10 21:23:24 +01:00
// Skip translation progress for English and invariant (such as "C") cultures
switch (CultureInfo.CurrentUICulture.TwoLetterISOLanguageName) {
case "en" or "iv" or "qps":
2021-11-10 21:23:24 +01:00
return;
}
2021-11-10 21:23:24 +01:00
// We can't dispose this resource set, as we can't be sure if it isn't used somewhere else, rely on GC in this case
ResourceSet? defaultResourceSet = resourceManager.GetResourceSet(CultureInfo.GetCultureInfo("en-US"), true, true);
2021-11-10 21:23:24 +01:00
if (defaultResourceSet == null) {
ASF.ArchiLogger.LogNullError(defaultResourceSet);
2021-11-10 21:23:24 +01:00
return;
}
2021-11-10 21:23:24 +01:00
HashSet<DictionaryEntry> defaultStringObjects = defaultResourceSet.Cast<DictionaryEntry>().ToHashSet();
2021-11-10 21:23:24 +01:00
if (defaultStringObjects.Count == 0) {
2023-07-25 11:26:22 +02:00
// This means we don't have entries for English, so there is nothing to check against
// Can happen e.g. for plugins with no strings declared which are calling this function
2021-11-10 21:23:24 +01:00
return;
}
2021-11-10 21:23:24 +01:00
// We can't dispose this resource set, as we can't be sure if it isn't used somewhere else, rely on GC in this case
ResourceSet? currentResourceSet = resourceManager.GetResourceSet(CultureInfo.CurrentUICulture, true, true);
2021-11-10 21:23:24 +01:00
if (currentResourceSet == null) {
ASF.ArchiLogger.LogNullError(currentResourceSet);
2021-11-10 21:23:24 +01:00
return;
}
2021-11-10 21:23:24 +01:00
HashSet<DictionaryEntry> currentStringObjects = currentResourceSet.Cast<DictionaryEntry>().ToHashSet();
2021-11-10 21:23:24 +01:00
if (currentStringObjects.Count >= defaultStringObjects.Count) {
// Either we have 100% finished translation, or we're missing it entirely and using en-US
HashSet<DictionaryEntry> testStringObjects = currentStringObjects.ToHashSet();
testStringObjects.ExceptWith(defaultStringObjects);
2021-11-10 21:23:24 +01:00
// If we got 0 as final result, this is the missing language
// Otherwise it's just a small amount of strings that happen to be the same
if (testStringObjects.Count == 0) {
currentStringObjects = testStringObjects;
}
2021-11-10 21:23:24 +01:00
}
2021-11-10 21:23:24 +01:00
if (currentStringObjects.Count < defaultStringObjects.Count) {
float translationCompleteness = currentStringObjects.Count / (float) defaultStringObjects.Count;
2024-08-05 02:37:50 +02:00
ASF.ArchiLogger.LogGenericInfo(Strings.FormatTranslationIncomplete($"{CultureInfo.CurrentUICulture.Name} ({CultureInfo.CurrentUICulture.EnglishName})", translationCompleteness.ToString("P1", CultureInfo.CurrentCulture)));
}
2015-10-25 06:16:50 +01:00
}
private static async Task DeletePotentiallyUsedDirectory(string directory) {
ArgumentException.ThrowIfNullOrEmpty(directory);
for (byte i = 1; (i <= MaxSharingViolationTries) && Directory.Exists(directory); i++) {
if (i > 1) {
await Task.Delay(1000).ConfigureAwait(false);
}
try {
Directory.Delete(directory, true);
} catch (IOException e) when ((i < MaxSharingViolationTries) && ((uint) e.HResult == SharingViolationHResult)) {
// It's entirely possible that old process is still running, we allow this to happen and add additional delay
ASF.ArchiLogger.LogGenericDebuggingException(e);
2024-04-05 11:48:42 +02:00
continue;
2024-04-05 12:29:41 +02:00
} catch (UnauthorizedAccessException e) when ((i < MaxSharingViolationTries) && ((uint) e.HResult == UnauthorizedAccessHResult)) {
2024-04-05 11:48:42 +02:00
// It's entirely possible that old process is still running, we allow this to happen and add additional delay
ASF.ArchiLogger.LogGenericDebuggingException(e);
continue;
}
return;
}
}
private static void MoveAllUpdateFiles(string sourceDirectory, string targetDirectory, string? backupDirectory = null) {
ArgumentException.ThrowIfNullOrEmpty(sourceDirectory);
ArgumentException.ThrowIfNullOrEmpty(targetDirectory);
// Determine if targetDirectory is within sourceDirectory, if yes we need to skip it from enumeration further below
string targetRelativeDirectoryPath = Path.GetRelativePath(sourceDirectory, targetDirectory);
// We keep user files if backup directory is null, as it means we're creating one
bool keepUserFiles = string.IsNullOrEmpty(backupDirectory);
foreach (string file in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories)) {
string fileName = Path.GetFileName(file);
if (string.IsNullOrEmpty(fileName)) {
throw new InvalidOperationException(nameof(fileName));
}
string relativeFilePath = Path.GetRelativePath(sourceDirectory, file);
if (string.IsNullOrEmpty(relativeFilePath)) {
throw new InvalidOperationException(nameof(relativeFilePath));
}
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath);
switch (relativeDirectoryName) {
case null:
throw new InvalidOperationException(nameof(relativeDirectoryName));
case "":
// No directory, root folder
switch (fileName) {
case Logging.NLogConfigurationFile when keepUserFiles:
case SharedInfo.LogFile when keepUserFiles:
// Files with those names in root directory we want to keep
continue;
}
break;
case SharedInfo.ArchivalLogsDirectory when keepUserFiles:
case SharedInfo.ConfigDirectory when keepUserFiles:
case SharedInfo.DebugDirectory when keepUserFiles:
case SharedInfo.PluginsDirectory when keepUserFiles:
case SharedInfo.UpdateDirectoryNew:
case SharedInfo.UpdateDirectoryOld:
// Files in those constant directories we want to keep in their current place
continue;
default:
// If we're moving files deeper into source location, we need to skip the newly created location from it
if (!string.IsNullOrEmpty(targetRelativeDirectoryPath) && ((relativeDirectoryName == targetRelativeDirectoryPath) || RelativeDirectoryStartsWith(relativeDirectoryName, targetRelativeDirectoryPath))) {
continue;
}
// Below code block should match the case above, it handles subdirectories
if (RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.UpdateDirectoryNew, SharedInfo.UpdateDirectoryOld)) {
continue;
}
if (keepUserFiles && RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory)) {
continue;
}
break;
}
// We're going to move this file out of the current place, overwriting existing one if needed
string targetUpdateDirectory;
if (relativeDirectoryName.Length > 0) {
// File inside a subdirectory
targetUpdateDirectory = Path.Combine(targetDirectory, relativeDirectoryName);
Directory.CreateDirectory(targetUpdateDirectory);
} else {
// File in root directory
targetUpdateDirectory = targetDirectory;
}
string targetUpdateFile = Path.Combine(targetUpdateDirectory, fileName);
// If target update file exists and we have a backup directory, we should consider moving it to the backup directory regardless whether or not we did that before as part of backup procedure
// This achieves two purposes, firstly, we ensure additional backup of user file in case something goes wrong, and secondly, we decrease a possibility of overwriting files that are in-use on Windows, since we move them out of the picture first
if (!string.IsNullOrEmpty(backupDirectory) && File.Exists(targetUpdateFile)) {
string targetBackupDirectory;
if (relativeDirectoryName.Length > 0) {
// File inside a subdirectory
targetBackupDirectory = Path.Combine(backupDirectory, relativeDirectoryName);
Directory.CreateDirectory(targetBackupDirectory);
} else {
// File in root directory
targetBackupDirectory = backupDirectory;
}
string targetBackupFile = Path.Combine(targetBackupDirectory, fileName);
File.Move(targetUpdateFile, targetBackupFile, true);
}
File.Move(file, targetUpdateFile, true);
}
}
private static bool RelativeDirectoryStartsWith(string directory, params string[] prefixes) {
ArgumentException.ThrowIfNullOrEmpty(directory);
if ((prefixes == null) || (prefixes.Length == 0)) {
throw new ArgumentNullException(nameof(prefixes));
}
return prefixes.Any(prefix => !string.IsNullOrEmpty(prefix) && (directory.Length > prefix.Length) && DirectorySeparators.Contains(directory[prefix.Length]) && directory.StartsWith(prefix, StringComparison.Ordinal));
}
}