Skip to content

Commit d983c85

Browse files
committed
Improve FileSettingsStore exception handling
1 parent 231198a commit d983c85

5 files changed

Lines changed: 64 additions & 31 deletions

File tree

src/Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
<_SkipUpgradeNetAnalyzersNuGetWarning>true</_SkipUpgradeNetAnalyzersNuGetWarning>
2828

2929
<!-- Make the assembly, file, and NuGet package versions the same. -->
30-
<Version>5.1.1</Version>
30+
<Version>5.1.2</Version>
3131
</PropertyGroup>
3232

3333
<PropertyGroup Condition="'$(Configuration)'=='Debug'">

src/Menees.Common/Exceptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ namespace Menees
55
using System;
66
using System.Collections.Generic;
77
using System.Diagnostics;
8+
using System.IO;
89
using System.Linq;
10+
using System.Security;
911
using System.Text;
1012
using Menees.Diagnostics;
1113

@@ -228,6 +230,17 @@ public static string GetMessage(Exception ex)
228230
return result;
229231
}
230232

233+
/// <summary>
234+
/// Gets whether an exception is an <see cref="IOException"/>, <see cref="SecurityException"/>,
235+
/// or <see cref="UnauthorizedAccessException"/>.
236+
/// </summary>
237+
/// <param name="ex">The exception to check.</param>
238+
public static bool IsAccessException(Exception ex)
239+
{
240+
bool result = ex != null && (ex is IOException || ex is UnauthorizedAccessException || ex is SecurityException);
241+
return result;
242+
}
243+
231244
#endregion
232245

233246
#region Private Methods

src/Menees.Common/FileSettingsStore.cs

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Menees
1010
using System.Linq;
1111
using System.Security;
1212
using System.Text;
13+
using System.Xml;
1314
using System.Xml.Linq;
1415

1516
#endregion
@@ -30,11 +31,30 @@ public FileSettingsStore()
3031
{
3132
// Make sure the file exists and isn't zero length. If a process was killed while saving data,
3233
// then the settings store file may be empty. If so, we'll pretend it isn't there.
33-
FileInfo file = new(fileName);
34-
if (file.Exists && file.Length > 0)
34+
FileInfo fileInfo = new(fileName);
35+
if (fileInfo.Exists && fileInfo.Length > 0)
3536
{
36-
this.root = XElement.Load(fileName);
37-
break;
37+
try
38+
{
39+
// Only read settings from a file that we have *write* access to so if we're running from a remote read-only file share,
40+
// then it will load a local saved file (e.g., from AppData) instead of loading one from the remote read-only share.
41+
using (FileStream stream = new(fileName, FileMode.Open, FileAccess.ReadWrite, FileShare.Read))
42+
{
43+
this.root = XElement.Load(stream);
44+
}
45+
46+
break;
47+
}
48+
catch (XmlException ex)
49+
{
50+
Log.Warning(typeof(FileSettingsStore), $"Unable to load the user settings from {fileName}.", ex);
51+
}
52+
catch (Exception ex) when (Exceptions.IsAccessException(ex))
53+
{
54+
// Ignore the file if we don't have both read and write access to it. If we're running off a read-only share,
55+
// we know Save() can't write back to that location, so we shouldn't read in from that location on startup.
56+
Log.Debug(typeof(FileSettingsStore), $"Unable to get read/write access to {fileName}.", ex);
57+
}
3858
}
3959
}
4060

@@ -85,28 +105,15 @@ public void Save()
85105
File.SetAttributes(fileName, File.GetAttributes(fileName) | FileAttributes.Hidden);
86106
break;
87107
}
88-
catch (IOException inputOutputEx)
89-
{
90-
errorLogProperties.Add(fileName, inputOutputEx);
91-
}
92-
catch (SecurityException securityEx)
108+
catch (Exception ex) when (Exceptions.IsAccessException(ex))
93109
{
94-
errorLogProperties.Add(fileName, securityEx);
95-
}
96-
catch (UnauthorizedAccessException accessEx)
97-
{
98-
errorLogProperties.Add(fileName, accessEx);
110+
errorLogProperties.Add(fileName, ex);
99111
}
100112
}
101113

102114
if (!saved)
103115
{
104-
const string Message = "Unable to save the user settings to a file store.";
105-
106-
// Log out the exceptions first since we can't pass those additional
107-
// details into the new exception we're throwing.
108-
Log.Error(typeof(FileSettingsStore), Message, null, errorLogProperties);
109-
throw Exceptions.NewInvalidOperationException(Message);
116+
throw Exceptions.NewInvalidOperationException("Unable to save the user settings to a file store.", errorLogProperties);
110117
}
111118
}
112119

@@ -126,7 +133,10 @@ private static IEnumerable<string> GetPotentialFileNames()
126133
List<string> result = new();
127134

128135
// First, try the application's base directory. That gives the best isolation for side-by-side app usage,
129-
// but a non-admin user may not have permissions to write to this directory.
136+
// but a non-admin user may not have permissions to write to this directory. Side-by-side isolation is
137+
// important if old and new versions of the software are run on the same machine. (New versions will
138+
// typically be able to read in old settings, but they'll only write out settings in the new format. After
139+
// that "upgrade", the old version wouldn't be able to read the settings in the new format.)
130140
string path = Path.Combine(ApplicationInfo.BaseDirectory, fileName);
131141
result.Add(path);
132142

tests/Menees.Common.Tests/DialogFilterItemTest.cs

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,10 @@ public class DialogFilterItemTest
1212
[TestMethod]
1313
public void ConstructorTest()
1414
{
15-
DialogFilterItem item = new("cs");
16-
item.ItemName.ShouldContain("C# Source Files", Case.Insensitive);
17-
item.Masks.SequenceEqual(new[] { "*.cs" }).ShouldBeTrue();
18-
item.ToString().ShouldContain("C# Source Files (*.cs)|*.cs", Case.Insensitive);
19-
20-
item = new DialogFilterItem("vb", false);
21-
item.ItemName.ShouldBe("Visual Basic Source File");
22-
item.Masks.SequenceEqual(new[] { "*.vb" }).ShouldBeTrue();
23-
item.ToString().ShouldBe("Visual Basic Source File (*.vb)|*.vb");
15+
DialogFilterItem item = new DialogFilterItem("exe", false);
16+
item.ItemName.ShouldBe("Application", StringCompareShould.IgnoreCase);
17+
item.Masks.SequenceEqual(new[] { "*.exe" }).ShouldBeTrue();
18+
item.ToString().ShouldBe("Application (*.exe)|*.exe", StringCompareShould.IgnoreCase);
2419

2520
item = new DialogFilterItem("Text Files", "txt");
2621
item.ItemName.ShouldBe("Text Files");

tests/Menees.Common.Tests/ExceptionsTest.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
using Shouldly;
88
using System.Diagnostics;
99
using System.Collections.Generic;
10+
using System.IO;
11+
using System.Security;
1012

1113
namespace Menees.Common.Tests
1214
{
@@ -124,6 +126,19 @@ public void GetMessageTest()
124126
Trace.Write(output);
125127
}
126128

129+
[TestMethod]
130+
public void IsAccessExceptionTest()
131+
{
132+
Exceptions.IsAccessException(new IOException()).ShouldBeTrue();
133+
Exceptions.IsAccessException(new SecurityException()).ShouldBeTrue();
134+
Exceptions.IsAccessException(new UnauthorizedAccessException()).ShouldBeTrue();
135+
Exceptions.IsAccessException(new PathTooLongException()).ShouldBeTrue();
136+
Exceptions.IsAccessException(new FileNotFoundException()).ShouldBeTrue();
137+
138+
Exceptions.IsAccessException(new ArgumentException()).ShouldBeFalse();
139+
Exceptions.IsAccessException(new InvalidOperationException()).ShouldBeFalse();
140+
}
141+
127142
private static void CheckMessage(string actualMessage, string expectedMessage)
128143
{
129144
// .NET Core's AggregateException.Message includes the inner exceptions in the first line of the message.

0 commit comments

Comments
 (0)