mirror of
https://github.com/JustArchiNET/ArchiSteamFarm.git
synced 2026-01-16 08:25:28 +00:00
* Closes #3156 * Misc * Misc * Rewrite update mechanism ONCE AGAIN, this time to eradicate FSW * Make creating debug directory non-fatal again, like it used to be * Deduplicate code * Remove dead code * Print update cleanup just once * Address remaining feedback, go back to _old and _new * One more nice improvement
This commit is contained in:
committed by
GitHub
parent
edc7c38ba0
commit
ae9dfca3b3
@@ -73,6 +73,8 @@ public static class ASF {
|
||||
|
||||
internal static readonly SemaphoreSlim OpenConnectionsSemaphore = new(WebBrowser.MaxConnections, WebBrowser.MaxConnections);
|
||||
|
||||
internal static string DebugDirectory => Path.Combine(SharedInfo.DebugDirectory, OS.ProcessStartTime.ToString("yyyy-MM-dd-THH-mm-ss", CultureInfo.InvariantCulture));
|
||||
|
||||
internal static ICrossProcessSemaphore? ConfirmationsSemaphore { get; private set; }
|
||||
internal static ICrossProcessSemaphore? GiftsSemaphore { get; private set; }
|
||||
internal static ICrossProcessSemaphore? InventorySemaphore { get; private set; }
|
||||
@@ -779,36 +781,9 @@ public static class ASF {
|
||||
await UpdateSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
|
||||
try {
|
||||
// If backup directory from previous update exists, it's a good idea to purge it now
|
||||
string backupDirectory = Path.Combine(SharedInfo.HomeDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCleanup);
|
||||
|
||||
for (byte i = 0; (i < WebBrowser.MaxTries) && Directory.Exists(backupDirectory); i++) {
|
||||
if (i > 0) {
|
||||
// It's entirely possible that old process is still running, wait a short moment for eventual cleanup
|
||||
await Task.Delay(5000).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try {
|
||||
Directory.Delete(backupDirectory, true);
|
||||
} catch (Exception e) {
|
||||
ArchiLogger.LogGenericDebuggingException(e);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (Directory.Exists(backupDirectory)) {
|
||||
ArchiLogger.LogGenericError(Strings.WarningFailed);
|
||||
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.Done);
|
||||
// If directories from previous update exist, it's a good idea to purge them now
|
||||
if (!await Utilities.UpdateCleanup(SharedInfo.HomeDirectory).ConfigureAwait(false)) {
|
||||
return (false, null);
|
||||
}
|
||||
|
||||
ArchiLogger.LogGenericInfo(Strings.UpdateCheckingNewVersion);
|
||||
@@ -994,7 +969,7 @@ public static class ASF {
|
||||
// We're ready to start update process, handle any plugin updates ready for new version
|
||||
await PluginsCore.UpdatePlugins(newVersion, true, updateChannel, updateOverride, forced).ConfigureAwait(false);
|
||||
|
||||
return Utilities.UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory);
|
||||
return await Utilities.UpdateFromArchive(zipArchive, SharedInfo.HomeDirectory).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
|
||||
@@ -50,8 +50,12 @@ using Zxcvbn;
|
||||
namespace ArchiSteamFarm.Core;
|
||||
|
||||
public static class Utilities {
|
||||
private const byte MaxSharingViolationTries = 15;
|
||||
private const uint SharingViolationHResult = 0x80070020;
|
||||
private const byte TimeoutForLongRunningTasksInSeconds = 60;
|
||||
|
||||
private static readonly FrozenSet<char> DirectorySeparators = new HashSet<char>(2) { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }.ToFrozenSet();
|
||||
|
||||
// normally we'd just use words like "steam" and "farm", but the library we're currently using is a bit iffy about banned words, so we need to also add combinations such as "steamfarm"
|
||||
private static readonly FrozenSet<string> ForbiddenPasswordPhrases = new HashSet<string>(10, StringComparer.InvariantCultureIgnoreCase) { "archisteamfarm", "archi", "steam", "farm", "archisteam", "archifarm", "steamfarm", "asf", "asffarm", "password" }.ToFrozenSet(StringComparer.InvariantCultureIgnoreCase);
|
||||
|
||||
@@ -322,117 +326,76 @@ public static class Utilities {
|
||||
return (result.Score < 4, suggestions is { Count: > 0 } ? string.Join(' ', suggestions.Where(static suggestion => suggestion.Length > 0)) : null);
|
||||
}
|
||||
|
||||
internal static bool UpdateFromArchive(ZipArchive zipArchive, string targetDirectory) {
|
||||
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 we'll move all our existing files to a backup directory
|
||||
string backupDirectory = Path.Combine(targetDirectory, SharedInfo.UpdateDirectory);
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(targetDirectory, "*", SearchOption.AllDirectories)) {
|
||||
string fileName = Path.GetFileName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(fileName)) {
|
||||
ASF.ArchiLogger.LogNullError(fileName);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string relativeFilePath = Path.GetRelativePath(targetDirectory, file);
|
||||
|
||||
if (string.IsNullOrEmpty(relativeFilePath)) {
|
||||
ASF.ArchiLogger.LogNullError(relativeFilePath);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath);
|
||||
|
||||
switch (relativeDirectoryName) {
|
||||
case null:
|
||||
ASF.ArchiLogger.LogNullError(relativeDirectoryName);
|
||||
|
||||
return false;
|
||||
case "":
|
||||
// No directory, root folder
|
||||
switch (fileName) {
|
||||
case Logging.NLogConfigurationFile:
|
||||
case SharedInfo.LogFile:
|
||||
// Files with those names in root directory we want to keep
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
case SharedInfo.ArchivalLogsDirectory:
|
||||
case SharedInfo.ConfigDirectory:
|
||||
case SharedInfo.DebugDirectory:
|
||||
case SharedInfo.PluginsDirectory:
|
||||
case SharedInfo.UpdateDirectory:
|
||||
// Files in those directories we want to keep in their current place
|
||||
continue;
|
||||
default:
|
||||
// Files in subdirectories of those directories we want to keep as well
|
||||
if (RelativeDirectoryStartsWith(relativeDirectoryName, SharedInfo.ArchivalLogsDirectory, SharedInfo.ConfigDirectory, SharedInfo.DebugDirectory, SharedInfo.PluginsDirectory, SharedInfo.UpdateDirectory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
string targetBackupDirectory = relativeDirectoryName.Length > 0 ? Path.Combine(backupDirectory, relativeDirectoryName) : backupDirectory;
|
||||
Directory.CreateDirectory(targetBackupDirectory);
|
||||
|
||||
string targetBackupFile = Path.Combine(targetBackupDirectory, fileName);
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
// Firstly, ensure once again our directories are purged and ready to work with
|
||||
if (!await UpdateCleanup(targetDirectory).ConfigureAwait(false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// We can now get rid of directories that are empty
|
||||
DeleteEmptyDirectoriesRecursively(targetDirectory);
|
||||
// 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);
|
||||
|
||||
if (!Directory.Exists(targetDirectory)) {
|
||||
Directory.CreateDirectory(targetDirectory);
|
||||
}
|
||||
zipArchive.ExtractToDirectory(updateDirectory, true);
|
||||
|
||||
// Now enumerate over files in the zip archive, skip directory entries that we're not interested in (we can create them ourselves if needed)
|
||||
foreach (ZipArchiveEntry zipFile in zipArchive.Entries.Where(static zipFile => !string.IsNullOrEmpty(zipFile.Name))) {
|
||||
string file = Path.GetFullPath(Path.Combine(targetDirectory, zipFile.FullName));
|
||||
// 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);
|
||||
|
||||
if (!file.StartsWith(targetDirectory, StringComparison.Ordinal)) {
|
||||
throw new InvalidOperationException(nameof(file));
|
||||
}
|
||||
Directory.CreateDirectory(backupDirectory);
|
||||
|
||||
if (File.Exists(file)) {
|
||||
// This is possible only with files that we decided to leave in place during our backup function
|
||||
string targetBackupFile = $"{file}.bak";
|
||||
MoveAllUpdateFiles(targetDirectory, backupDirectory, true);
|
||||
|
||||
File.Move(file, targetBackupFile, true);
|
||||
}
|
||||
// Finally, we can move the newly extracted files to target directory
|
||||
MoveAllUpdateFiles(updateDirectory, targetDirectory, false);
|
||||
|
||||
// Check if this file requires its own folder
|
||||
if (zipFile.Name != zipFile.FullName) {
|
||||
string? directory = Path.GetDirectoryName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(directory)) {
|
||||
ASF.ArchiLogger.LogNullError(directory);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(directory)) {
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
// We're not interested in extracting placeholder files (but we still want directories created for them, done above)
|
||||
switch (zipFile.Name) {
|
||||
case ".gitkeep":
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.ExtractToFile(file);
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -491,23 +454,104 @@ public static class Utilities {
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeleteEmptyDirectoriesRecursively(string directory) {
|
||||
private static async Task DeletePotentiallyUsedDirectory(string directory) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
|
||||
if (!Directory.Exists(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);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
foreach (string subDirectory in Directory.EnumerateDirectories(directory)) {
|
||||
DeleteEmptyDirectoriesRecursively(subDirectory);
|
||||
private static void MoveAllUpdateFiles(string sourceDirectory, string targetDirectory, bool keepUserFiles) {
|
||||
ArgumentException.ThrowIfNullOrEmpty(sourceDirectory);
|
||||
ArgumentException.ThrowIfNullOrEmpty(sourceDirectory);
|
||||
|
||||
// Determine if targetDirectory is within sourceDirectory, if yes we need to skip it from enumeration further below
|
||||
string targetRelativeDirectoryPath = Path.GetRelativePath(sourceDirectory, targetDirectory);
|
||||
|
||||
foreach (string file in Directory.EnumerateFiles(sourceDirectory, "*", SearchOption.AllDirectories)) {
|
||||
string fileName = Path.GetFileName(file);
|
||||
|
||||
if (string.IsNullOrEmpty(fileName)) {
|
||||
throw new InvalidOperationException(nameof(fileName));
|
||||
}
|
||||
|
||||
if (!Directory.EnumerateFileSystemEntries(directory).Any()) {
|
||||
Directory.Delete(directory);
|
||||
string relativeFilePath = Path.GetRelativePath(sourceDirectory, file);
|
||||
|
||||
if (string.IsNullOrEmpty(relativeFilePath)) {
|
||||
throw new InvalidOperationException(nameof(relativeFilePath));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ASF.ArchiLogger.LogGenericException(e);
|
||||
|
||||
string? relativeDirectoryName = Path.GetDirectoryName(relativeFilePath);
|
||||
|
||||
switch (relativeDirectoryName) {
|
||||
case null:
|
||||
throw new InvalidOperationException(nameof(keepUserFiles));
|
||||
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);
|
||||
|
||||
File.Move(file, targetUpdateFile, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,8 +562,6 @@ public static class Utilities {
|
||||
throw new ArgumentNullException(nameof(prefixes));
|
||||
}
|
||||
|
||||
HashSet<char> separators = [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar];
|
||||
|
||||
return prefixes.Where(prefix => (directory.Length > prefix.Length) && separators.Contains(directory[prefix.Length])).Any(prefix => directory.StartsWith(prefix, StringComparison.Ordinal));
|
||||
return prefixes.Any(prefix => !string.IsNullOrEmpty(prefix) && (directory.Length > prefix.Length) && DirectorySeparators.Contains(directory[prefix.Length]) && directory.StartsWith(prefix, StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user