Skip to content

Commit

Permalink
feat: use new exception for used by another process errors
Browse files Browse the repository at this point in the history
  • Loading branch information
anna-is-cute committed Mar 10, 2024
1 parent 977212d commit 1847532
Show file tree
Hide file tree
Showing 11 changed files with 109 additions and 36 deletions.
4 changes: 2 additions & 2 deletions DownloadCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal static DownloadCodes Create(string path) {

internal static DownloadCodes? Load(string path) {
try {
var json = File.ReadAllText(path);
var json = FileHelper.ReadAllText(path);
var codes = JsonConvert.DeserializeObject<DownloadCodes>(json);
if (codes == null) {
return null;
Expand All @@ -51,7 +51,7 @@ internal void Save() {
json = JsonConvert.SerializeObject(this);
}

File.WriteAllText(this.FilePath, json);
FileHelper.WriteAllText(this.FilePath, json);
}

internal bool TryGetCode(Guid packageId, out string? code) {
Expand Down
24 changes: 12 additions & 12 deletions DownloadTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ private IEnumerable<Task> DownloadBatchedFiles(IDownloadTask_GetVersion_NeededFi
}
blake3.Initialize();
await using var file = FileHelper.OpenRead(path);
await using var file = FileHelper.OpenReadIfExists(path);
if (file == null) {
continue;
}
Expand Down Expand Up @@ -522,7 +522,7 @@ await content.ReadAsStreamAsync(this.CancellationToken.Token),
var path = allUi
? Path.ChangeExtension(Path.Join(filesPath, hash), $"{discriminators[0]}{extensions[0]}")
: Path.ChangeExtension(Path.Join(filesPath, hash), extensions[0]);
await using var file = File.Create(path);
await using var file = FileHelper.Create(path);
// make a stream that's only capable of reading the
// amount of compressed bytes
await using var limited = new LimitedStream(stream, (int) batchedFileInfo.SizeCompressed);
Expand Down Expand Up @@ -671,7 +671,7 @@ void RemoveExtra(HashSet<string> present, IReadOnlyDictionary<string, HashSet<st
foreach (var file in hashes[extra]) {
var extraPath = Path.Join(filesPath, file);
Plugin.Log.Info($"removing extra file {extraPath}");
File.Delete(extraPath);
FileHelper.Delete(extraPath);

done += 1;
this.SetStateData(done, total);
Expand Down Expand Up @@ -741,7 +741,7 @@ await Retry(3, $"Error downloading {baseUri}/{hash}", async _ => {
using var resp = await Plugin.Client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, this.CancellationToken.Token);
resp.EnsureSuccessStatusCode();
await using var file = File.Create(path);
await using var file = FileHelper.Create(path);
await using var stream = new GloballyThrottledStream(
await resp.Content.ReadAsStreamAsync(this.CancellationToken.Token),
this.Entries
Expand All @@ -767,7 +767,7 @@ Task<bool> CheckHash(string path, string expected) {
// make sure checksum matches
using var blake3 = new Blake3HashAlgorithm();
blake3.Initialize();
await using var file = FileHelper.OpenRead(path);
await using var file = FileHelper.OpenReadIfExists(path);
if (file == null) {
// if the file couldn't be found, retry by throwing
// exception for the first two tries
Expand Down Expand Up @@ -828,7 +828,7 @@ private async Task ConstructMeta(IDownloadTask_GetVersion info, HeliosphereMeta
var json = JsonConvert.SerializeObject(meta, Formatting.Indented);

var path = Path.Join(this.PenumbraModPath, "meta.json");
await using var file = File.Create(path);
await using var file = FileHelper.Create(path);
await file.WriteAsync(Encoding.UTF8.GetBytes(json), this.CancellationToken.Token);
this.State += 1;
}
Expand Down Expand Up @@ -868,7 +868,7 @@ private async Task<HeliosphereMeta> ConstructHeliosphereMeta(IDownloadTask_GetVe

var metaJson = JsonConvert.SerializeObject(meta, Formatting.Indented);
var path = Path.Join(this.PenumbraModPath, "heliosphere.json");
await using var file = File.Create(path);
await using var file = FileHelper.Create(path);
await file.WriteAsync(Encoding.UTF8.GetBytes(metaJson), this.CancellationToken.Token);

// save cover image
Expand All @@ -878,7 +878,7 @@ private async Task<HeliosphereMeta> ConstructHeliosphereMeta(IDownloadTask_GetVe

try {
using var image = await GetImage(info.Variant.Package.Id, coverImage.Id, this.CancellationToken.Token);
await using var cover = File.Create(coverPath);
await using var cover = FileHelper.Create(coverPath);
await image.Content.CopyToAsync(cover, this.CancellationToken.Token);
} catch (Exception ex) {
ErrorHelper.Handle(ex, "Could not download cover image");
Expand Down Expand Up @@ -921,7 +921,7 @@ private async Task ConstructDefaultMod(IDownloadTask_GetVersion info) {
var json = JsonConvert.SerializeObject(defaultMod, Formatting.Indented);

var path = Path.Join(this.PenumbraModPath, "default_mod.json");
await using var output = File.Create(path);
await using var output = FileHelper.Create(path);
await output.WriteAsync(Encoding.UTF8.GetBytes(json), this.CancellationToken.Token);
this.StateData += 1;
}
Expand All @@ -937,14 +937,14 @@ private async Task ConstructGroups(IDownloadTask_GetVersion info) {
var oldGroups = new List<ModGroup>();
foreach (var existing in existingGroups) {
try {
var text = await File.ReadAllTextAsync(existing);
var text = await FileHelper.ReadAllTextAsync(existing);
var group = JsonConvert.DeserializeObject<ModGroup>(text);
oldGroups.Add(group);
} catch (Exception ex) {
Plugin.Log.Warning(ex, "Could not deserialise old group");
}

File.Delete(existing);
FileHelper.Delete(existing);
}

var modGroups = new Dictionary<string, ModGroup>(info.Groups.Count);
Expand Down Expand Up @@ -1186,7 +1186,7 @@ private async Task ConstructGroups(IDownloadTask_GetVersion info) {
.ToString();
var json = JsonConvert.SerializeObject(list[i], Formatting.Indented);
var path = Path.Join(this.PenumbraModPath, $"group_{i + 1:000}_{slug}.json");
await using var file = File.Create(path);
await using var file = FileHelper.Create(path);
await file.WriteAsync(Encoding.UTF8.GetBytes(json), this.CancellationToken.Token);
this.StateData += 1;
}
Expand Down
17 changes: 17 additions & 0 deletions Exceptions/AlreadyInUseException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Diagnostics;

internal class AlreadyInUseException : IOException {
internal AlreadyInUseException(IOException inner, string path, List<Process> processes) : base(
$"File '{path}' is already in use by {string.Join(", ", processes.Select(ProcessTitle))}",
inner
) {
}

private static string ProcessTitle(Process p) {
if (string.IsNullOrWhiteSpace(p.MainWindowTitle)) {
return p.ProcessName;
} else {
return $"{p.MainWindowTitle} ({p.ProcessName})";
}
}
}
4 changes: 2 additions & 2 deletions ImportTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ private async Task<Dictionary<string, List<string>>> Hash() {
using var hasher = new Blake3HashAlgorithm();
hasher.Initialize();

await using var file = File.OpenRead(filePath);
await using var file = FileHelper.OpenRead(filePath);
var hashBytes = await hasher.ComputeHashAsync(file);
var hash = Base64.Url.Encode(hashBytes);

Expand Down Expand Up @@ -239,7 +239,7 @@ private void Delete() {

// delete all top-level files
foreach (var filePath in Directory.EnumerateFiles(this._fullDirectory!)) {
File.Delete(filePath);
FileHelper.Delete(filePath);
}

// delete the old mod from penumbra
Expand Down
4 changes: 2 additions & 2 deletions Model/HeliosphereMeta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ internal class HeliosphereMeta {
internal string ErrorName => $"{this.Name} v{this.Version} (P:{this.Id.ToCrockford()} Va:{this.VariantId.ToCrockford()} Ve:{this.VersionId.ToCrockford()})";

internal static async Task<HeliosphereMeta?> Load(string path) {
var text = await File.ReadAllTextAsync(path);
var text = await FileHelper.ReadAllTextAsync(path);
var obj = JsonConvert.DeserializeObject<JObject>(text);
if (obj == null) {
return null;
Expand All @@ -49,7 +49,7 @@ internal class HeliosphereMeta {
var (meta, changed) = await Convert(obj);
if (changed) {
var json = JsonConvert.SerializeObject(meta, Formatting.Indented);
await using var file = File.Create(path);
await using var file = FileHelper.Create(path);
await file.WriteAsync(Encoding.UTF8.GetBytes(json));
}

Expand Down
4 changes: 2 additions & 2 deletions PackageState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ await this.Plugin.Framework.RunOnFrameworkThread(() => {
Plugin.Log.Debug(" writing new meta");
var json = JsonConvert.SerializeObject(meta, Formatting.Indented);
var path = Path.Join(penumbraPath, newName, "heliosphere.json");
await using var file = File.Create(path);
await using var file = FileHelper.Create(path);
await file.WriteAsync(Encoding.UTF8.GetBytes(json));

return (newName, parts);
Expand Down Expand Up @@ -419,7 +419,7 @@ private async Task AttemptLoad() {
private async Task<bool> AttemptLoadSingle() {
byte[] bytes;
try {
bytes = await File.ReadAllBytesAsync(this.CoverImagePath);
bytes = await FileHelper.ReadAllBytesAsync(this.CoverImagePath);
} catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException) {
return true;
}
Expand Down
1 change: 1 addition & 0 deletions Util/Consts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ namespace Heliosphere.Util;

internal static class Consts {
internal const string DefaultVariant = "Default";
internal const int UsedByAnotherProcess = unchecked((int) 0x80070020);
}
2 changes: 1 addition & 1 deletion Util/DependencyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal static class DependencyHelper {
internal static async Task<bool> CheckDependencies(Plugin plugin) {
var dllPath = Path.GetDirectoryName(plugin.GetType().Assembly.Location)!;
var infoFilePath = Path.Join(dllPath, $"{InternalName}.deps.json");
var infoFileJson = await File.ReadAllTextAsync(infoFilePath);
var infoFileJson = await FileHelper.ReadAllTextAsync(infoFilePath);
var info = JsonConvert.DeserializeObject<DependencyInfo>(infoFileJson)!;

var dlls = Directory.EnumerateFiles(dllPath)
Expand Down
2 changes: 1 addition & 1 deletion Util/ErrorHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ internal static bool IsAntiVirus(this Exception ex) {
// could not delete file after waiting, av probably blocked
case DeleteFileException:
// being used by another process or access denied
case IOException { HResult: unchecked((int) 0x80070020) or unchecked((int) 0x80070005) }:
case IOException { HResult: Consts.UsedByAnotherProcess or unchecked((int) 0x80070005) }:
return true;
default:
return false;
Expand Down
81 changes: 68 additions & 13 deletions Util/FileHelper.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
namespace Heliosphere.Util;

internal static class FileHelper {


/// <summary>
/// Try to open a file for reading. If the file doesn't exist, returns null.
/// <br/>
Expand All @@ -10,22 +12,75 @@ internal static class FileHelper {
/// </summary>
/// <param name="path">path to open</param>
/// <returns>FileStream if the file exists</returns>
internal static FileStream? OpenRead(string path) {
/// <exception cref="AlreadyInUseException"/>
internal static FileStream? OpenReadIfExists(string path) {
return Wrap(path, path => {
try {
return File.OpenRead(path);
} catch (Exception ex) when (ex is DirectoryNotFoundException or FileNotFoundException) {
return null;
}
});
}

internal static FileStream OpenRead(string path) {
return Wrap(path, File.OpenRead);
}

/// <summary>
/// Create a file at the given path. See <see cref="File.Create(string)"/>.
/// </summary>
/// <param name="path">path to file to create</param>
/// <returns>FileStream of created file</returns>
/// <exception cref="AlreadyInUseException"/>
internal static FileStream Create(string path) {
return Wrap(path, File.Create);
}

internal static string ReadAllText(string path) {
return Wrap(path, File.ReadAllText);
}

internal async static Task<string> ReadAllTextAsync(string path) {
return await WrapAsync(path, path => File.ReadAllTextAsync(path));
}

internal async static Task<byte[]> ReadAllBytesAsync(string path) {
return await WrapAsync(path, path => File.ReadAllBytesAsync(path));
}

internal static void WriteAllText(string path, string text) {
Wrap(path, path => File.WriteAllText(path, text));
}

internal static void Delete(string path) {
Wrap(path, File.Delete);
}

private static T Wrap<T>(string path, Func<string, T> action) {
try {
return File.OpenRead(path);
} catch (Exception ex) when (ex is DirectoryNotFoundException or FileNotFoundException) {
return null;
} catch (Exception ex) when (ex is IOException { HResult: unchecked((int) 0x80070020) }) {
return action(path);
} catch (Exception ex) when (ex is IOException { HResult: Consts.UsedByAnotherProcess } io) {
var procs = RestartManager.GetLockingProcesses(path);
if (procs.Count > 0) {
var usedBy = string.Join(
", ",
procs.Select(proc => $"{proc.MainWindowTitle} ({proc.ProcessName})")
);
Plugin.Log.Warning($"Path '{path}' is being used by {usedBy}");
}
throw new AlreadyInUseException(io, path, procs);
}
}

throw;
private static void Wrap(string path, Action<string> action) {
try {
action(path);
} catch (Exception ex) when (ex is IOException { HResult: Consts.UsedByAnotherProcess } io) {
var procs = RestartManager.GetLockingProcesses(path);
throw new AlreadyInUseException(io, path, procs);
}
}

private static async Task<T> WrapAsync<T>(string path, Func<string, Task<T>> action) {
try {
return await action(path);
} catch (Exception ex) when (ex is IOException { HResult: Consts.UsedByAnotherProcess } io) {
var procs = RestartManager.GetLockingProcesses(path);
throw new AlreadyInUseException(io, path, procs);
}
}
}
2 changes: 1 addition & 1 deletion Util/PathHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ internal static async Task<bool> CreateDirectory(string path, TimeSpan? timeout
}

internal static async Task<bool> WaitForDelete(string path, TimeSpan? timeout = null, TimeSpan? wait = null) {
File.Delete(path);
FileHelper.Delete(path);

var max = timeout ?? TimeSpan.FromSeconds(5);
var cts = new CancellationTokenSource(max);
Expand Down

0 comments on commit 1847532

Please sign in to comment.