diff --git a/ADB.cs b/ADB.cs index 24ae682..e293481 100644 --- a/ADB.cs +++ b/ADB.cs @@ -357,7 +357,7 @@ namespace AndroidSideloader string directoryToDelete = Path.Combine(Environment.CurrentDirectory, packagename); if (Directory.Exists(directoryToDelete) && directoryToDelete != Environment.CurrentDirectory) { - Directory.Delete(directoryToDelete, true); + FileSystemUtilities.TryDeleteDirectory(directoryToDelete); } progressCallback?.Invoke(100, null); @@ -732,7 +732,7 @@ namespace AndroidSideloader { if (directoryToDelete != Environment.CurrentDirectory) { - Directory.Delete(directoryToDelete, true); + FileSystemUtilities.TryDeleteDirectory(directoryToDelete); } } diff --git a/AndroidSideloader.csproj b/AndroidSideloader.csproj index 3185b2c..229460c 100644 --- a/AndroidSideloader.csproj +++ b/AndroidSideloader.csproj @@ -244,6 +244,7 @@ + Form diff --git a/MainForm.cs b/MainForm.cs index 4743e62..bf91441 100755 --- a/MainForm.cs +++ b/MainForm.cs @@ -434,6 +434,20 @@ namespace AndroidSideloader changeTitle(isOffline ? "Starting in Offline Mode..." : "Initializing..."); + // Non-blocking WebView cleanup + _ = Task.Run(() => + { + try + { + string webViewDirectoryPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL", "EBWebView"); + if (Directory.Exists(webViewDirectoryPath)) + { + FileSystemUtilities.TryDeleteDirectory(webViewDirectoryPath); + } + } + catch { } + }); + // Non-blocking background cleanup _ = Task.Run(() => { @@ -441,7 +455,7 @@ namespace AndroidSideloader { if (Directory.Exists(Sideloader.TempFolder)) { - Directory.Delete(Sideloader.TempFolder, true); + FileSystemUtilities.TryDeleteDirectory(Sideloader.TempFolder); _ = Directory.CreateDirectory(Sideloader.TempFolder); } } @@ -588,20 +602,6 @@ namespace AndroidSideloader } } - // WebView cleanup in background - _ = Task.Run(() => - { - try - { - string webViewDirectoryPath = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "RSL", "EBWebView"); - if (Directory.Exists(webViewDirectoryPath)) - { - Directory.Delete(webViewDirectoryPath, true); - } - } - catch { } - }); - // Pre-initialize trailer player in background try { @@ -831,7 +831,8 @@ namespace AndroidSideloader try { string titleSuffix = string.IsNullOrWhiteSpace(txt) ? "" : " | " + txt; - this.Invoke(() => { + this.Invoke(() => + { Text = "Rookie Sideloader " + Updater.LocalVersion + titleSuffix; rookieStatusLabel.Text = txt; }); @@ -843,8 +844,9 @@ namespace AndroidSideloader await Task.Delay(TimeSpan.FromSeconds(5)); // Reset to base title without any status message - this.Invoke(() => { - Text = "Rookie Sideloader " + Updater.LocalVersion; + this.Invoke(() => + { + Text = "Rookie Sideloader " + Updater.LocalVersion; rookieStatusLabel.Text = ""; }); } @@ -985,14 +987,16 @@ namespace AndroidSideloader output = await ADB.CopyOBBWithProgressAsync( path, - (progress, eta) => this.Invoke(() => { + (progress, eta) => this.Invoke(() => + { progressBar.Value = progress; string etaStr = eta.HasValue && eta.Value.TotalSeconds > 0 ? $" · ETA: {eta.Value:mm\\:ss}" : ""; speedLabel.Text = $"Progress: {progress}%{etaStr}"; }), - status => this.Invoke(() => { + status => this.Invoke(() => + { progressBar.StatusText = status; }), folderName); @@ -1489,7 +1493,7 @@ namespace AndroidSideloader File.Delete($"{settings.MainDir}\\{gameZipName}"); this.Invoke(() => FlexibleMessageBox.Show(Program.form, $"Upload of {currentlyUploading} is complete! Thank you for your contribution!")); - Directory.Delete($"{settings.MainDir}\\{packageName}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}"); }) { IsBackground = true @@ -1750,7 +1754,7 @@ namespace AndroidSideloader await Task.Delay(100); } - Directory.Delete($"{zippath}\\{datazip}", true); + FileSystemUtilities.TryDeleteDirectory($"{zippath}\\{datazip}"); } } } @@ -1907,7 +1911,7 @@ namespace AndroidSideloader await Task.Delay(100); } - Directory.Delete(foldername, true); + FileSystemUtilities.TryDeleteDirectory(foldername); changeTitle(""); } // BMBF Zip extraction then push to BMBF song folder on Quest. @@ -1937,7 +1941,7 @@ namespace AndroidSideloader await Task.Delay(100); } - Directory.Delete($"{zippath}\\{datazip}", true); + FileSystemUtilities.TryDeleteDirectory($"{zippath}\\{datazip}"); } else if (extension == ".txt") { @@ -2635,7 +2639,7 @@ namespace AndroidSideloader _ = ADB.RunCommandToString(cmd, path); if (Directory.Exists($"{settings.MainDir}\\{game.Pckgcommand}")) { - Directory.Delete($"{settings.MainDir}\\{game.Pckgcommand}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{game.Pckgcommand}"); } Program.form.changeTitle("Uploading to server, you can continue to use Rookie while it uploads."); @@ -3740,7 +3744,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord // only delete after extraction; allows for resume if the fetch fails midway. if (Directory.Exists($"{settings.DownloadDir}\\{gameName}")) { - Directory.Delete($"{settings.DownloadDir}\\{gameName}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{gameName}"); } } } @@ -3909,14 +3913,14 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord if (UsingPublicConfig) { if (Directory.Exists($"{settings.DownloadDir}\\{cancelledHash}")) - Directory.Delete($"{settings.DownloadDir}\\{cancelledHash}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{cancelledHash}"); if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}")) - Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{cancelledGame}"); } else { if (Directory.Exists($"{settings.DownloadDir}\\{cancelledGame}")) - Directory.Delete($"{settings.DownloadDir}\\{cancelledGame}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{cancelledGame}"); } } } @@ -4024,7 +4028,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord if (Directory.Exists($"{settings.DownloadDir}\\{gameNameHash}")) { - Directory.Delete($"{settings.DownloadDir}\\{gameNameHash}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.DownloadDir}\\{gameNameHash}"); } } @@ -4099,7 +4103,8 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord // Use async method with progress output += await ADB.SideloadWithProgressAsync( apkFile, - (progress, eta) => this.Invoke(() => { + (progress, eta) => this.Invoke(() => + { if (progress == 0) { progressBar.IsIndeterminate = true; @@ -4113,7 +4118,8 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord UpdateProgressStatus("Installing", percent: (int)Math.Round(progress), eta: eta); progressBar.StatusText = $"Installing · {progress:0.0}%"; }), - status => this.Invoke(() => { + status => this.Invoke(() => + { if (!string.IsNullOrEmpty(status)) { if (status.Contains("Completing Installation")) @@ -4192,7 +4198,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord if (settings.DeleteAllAfterInstall && !nodeviceonstart && DeviceConnected) { changeTitle("Deleting game files"); - try { Directory.Delete(settings.DownloadDir + "\\" + gameName, true); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); } + try { FileSystemUtilities.TryDeleteDirectory(settings.DownloadDir + "\\" + gameName); } catch (Exception ex) { _ = FlexibleMessageBox.Show(Program.form, $"Error deleting game files: {ex.Message}"); } } // Update device space after successful installation @@ -4405,7 +4411,7 @@ If the problem persists, visit our Telegram (https://t.me/VRPirates) or Discord timerticked = false; if (Directory.Exists(Path.Combine(Environment.CurrentDirectory, CurrPCKG))) { - Directory.Delete(Path.Combine(Environment.CurrentDirectory, CurrPCKG), true); + FileSystemUtilities.TryDeleteDirectory(Path.Combine(Environment.CurrentDirectory, CurrPCKG)); } changeTitle(""); @@ -5753,10 +5759,10 @@ function onYouTubeIframeAPIReady() { score += extraWords * -3; // -3 per extra word // Hard penalties for junk - if (title.Contains("review") || - title.Contains("tutorial") || - title.Contains("how to") || - title.Contains("reaction")) + if (title.Contains("review") || + title.Contains("tutorial") || + title.Contains("how to") || + title.Contains("reaction")) score -= 30; if (score > bestScore) @@ -6053,7 +6059,7 @@ function onYouTubeIframeAPIReady() { ulong VersionInt = ulong.Parse(Utilities.StringUtilities.KeepOnlyNumbers(InstalledVersionCode)); if (Directory.Exists($"{settings.MainDir}\\{packageName}")) { - Directory.Delete($"{settings.MainDir}\\{packageName}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}"); } ProcessOutput output = new ProcessOutput("", ""); @@ -6119,7 +6125,7 @@ function onYouTubeIframeAPIReady() { File.Copy($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip", $"{Environment.GetFolderPath(Environment.SpecialFolder.Desktop)}\\{GameName} v{VersionInt} {packageName}.zip"); File.Delete($"{settings.MainDir}\\{GameName} v{VersionInt} {packageName}.zip"); - Directory.Delete($"{settings.MainDir}\\{packageName}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}"); isworking = false; changeTitle(""); progressBar.IsIndeterminate = false; @@ -6386,7 +6392,7 @@ function onYouTubeIframeAPIReady() { gamesListView.SelectedItems.Clear(); _rightClickedItem.Selected = true; - + UpdateFavoriteMenuItemText(); favoriteGame.Show(gamesListView, e.Location); } @@ -6494,7 +6500,9 @@ function onYouTubeIframeAPIReady() { if (string.IsNullOrEmpty(firmware)) { firmware = string.Empty; - } else { + } + else + { firmware = Utilities.StringUtilities.RemoveEverythingBeforeFirst(firmware, "-v"); firmware = Utilities.StringUtilities.KeepOnlyNumbers(firmware); } @@ -6506,7 +6514,7 @@ function onYouTubeIframeAPIReady() { long totalSpace = 0; long usedSpace = 0; long freeSpace = 0; - + if (lines.Length > 1) { string[] parts = lines[1].Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); @@ -8123,7 +8131,7 @@ function onYouTubeIframeAPIReady() { // Confirm uninstall DialogResult dialogresult = FlexibleMessageBox.Show( - $"Are you sure you want to uninstall {gameName}?", + $"Are you sure you want to uninstall {gameName}?", "Proceed with uninstall?", MessageBoxButtons.YesNo); if (dialogresult == DialogResult.No) @@ -8140,7 +8148,7 @@ function onYouTubeIframeAPIReady() { } DialogResult dialogresult2 = FlexibleMessageBox.Show( - $"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\", + $"Do you want to attempt to automatically backup any saves to {backupFolder}\\{DateTime.Today.ToString("yyyy.MM.dd")}\\", "Attempt Game Backup?", MessageBoxButtons.YesNo); if (dialogresult2 == DialogResult.Yes) @@ -8153,7 +8161,8 @@ function onYouTubeIframeAPIReady() { progressBar.IsIndeterminate = true; progressBar.OperationType = ""; - await Task.Run(() => { + await Task.Run(() => + { output += Sideloader.UninstallGame(packageName); }); diff --git a/Sideloader.cs b/Sideloader.cs index e39b671..7de688e 100644 --- a/Sideloader.cs +++ b/Sideloader.cs @@ -204,7 +204,7 @@ namespace AndroidSideloader if (Directory.Exists($"{settings.MainDir}\\{packageName}")) { - Directory.Delete($"{settings.MainDir}\\{packageName}", true); + FileSystemUtilities.TryDeleteDirectory($"{settings.MainDir}\\{packageName}"); } _ = Directory.CreateDirectory($"{settings.MainDir}\\{packageName}"); diff --git a/Sideloader/GetDependencies.cs b/Sideloader/GetDependencies.cs index 3671f23..0b901f4 100644 --- a/Sideloader/GetDependencies.cs +++ b/Sideloader/GetDependencies.cs @@ -309,7 +309,7 @@ namespace AndroidSideloader } File.Move(file, destFile); } - Directory.Delete(dirExtractedRclone, true); + FileSystemUtilities.TryDeleteDirectory(dirExtractedRclone); // Restore vrp.download.config if it was backed up if (hasConfig && File.Exists(tempConfigPath)) diff --git a/Sideloader/RCLONE.cs b/Sideloader/RCLONE.cs index 9228005..a0247d1 100644 --- a/Sideloader/RCLONE.cs +++ b/Sideloader/RCLONE.cs @@ -290,7 +290,7 @@ namespace AndroidSideloader { if (Directory.Exists(path)) { - Directory.Delete(path, true); + FileSystemUtilities.TryDeleteDirectory(path); } } catch (Exception ex) diff --git a/Utilities/FileSystemUtilities.cs b/Utilities/FileSystemUtilities.cs new file mode 100644 index 0000000..f16b660 --- /dev/null +++ b/Utilities/FileSystemUtilities.cs @@ -0,0 +1,91 @@ +using System; +using System.IO; +using System.Threading; + +namespace AndroidSideloader.Utilities +{ + internal static class FileSystemUtilities + { + public static bool TryDeleteDirectory(string directoryPath, int maxRetries = 3, int delayMs = 150) // 3x 150ms = 450ms total + { + if (string.IsNullOrWhiteSpace(directoryPath)) + return true; + + if (!Directory.Exists(directoryPath)) + return true; + + Exception lastError = null; + + // Retry deletion several times in case of lock ups + for (int attempt = 0; attempt <= maxRetries; attempt++) + { + try + { + StripReadOnlyAttributes(directoryPath); + Directory.Delete(directoryPath, true); + return true; + } + catch (DirectoryNotFoundException) + { + return true; + } + catch (Exception ex) when (ex is UnauthorizedAccessException || ex is IOException) + { + lastError = ex; + + if (attempt < maxRetries) + { + Thread.Sleep(delayMs); + continue; + } + + break; + } + catch (Exception ex) + { + // Non-retryable error + lastError = ex; + break; + } + } + + // Last resort: rename then delete + try + { + string renamedPath = directoryPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + + ".deleting." + DateTime.UtcNow.Ticks; + + Directory.Move(directoryPath, renamedPath); + + StripReadOnlyAttributes(renamedPath); + Directory.Delete(renamedPath, true); + return true; + } + catch (Exception ex) + { + lastError = ex; + } + + Logger.Log($"Failed to delete directory: {directoryPath}. Error: {lastError}", LogLevel.WARNING); + return false; + } + + private static void StripReadOnlyAttributes(string directoryPath) + { + var root = new DirectoryInfo(directoryPath); + if (!root.Exists) return; + + root.Attributes &= ~FileAttributes.ReadOnly; + + foreach (var dir in root.EnumerateDirectories("*", SearchOption.AllDirectories)) + { + dir.Attributes &= ~FileAttributes.ReadOnly; + } + + foreach (var file in root.EnumerateFiles("*", SearchOption.AllDirectories)) + { + file.Attributes &= ~FileAttributes.ReadOnly; + } + } + } +} \ No newline at end of file