Say why the folders cannot be made instead of dying
Something already sitting where Documents/N1MM Logger+ should go — a file, or a link to somewhere that is not there — threw out of Directory.CreateDirectory before the program had a window, so the operator got a stack trace and nothing else. CreateFolders answers with what stopped it, and the program shows it in a window naming the path and stops there. NONEMM_HOME moves the whole lot somewhere else, for a station that keeps its files off the Documents folder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
@@ -423,7 +423,13 @@ the migrations for N1MM's own admin databases and the program it speaks with,
|
|||||||
and neither has anything to do with a log.
|
and neither has anything to do with a log.
|
||||||
|
|
||||||
On a system that does not say where Documents is — most Linux systems, until
|
On a system that does not say where Documents is — most Linux systems, until
|
||||||
someone sets `XDG_DOCUMENTS_DIR` — it is `~/Documents/N1MM Logger+`.
|
someone sets `XDG_DOCUMENTS_DIR` — it is `~/Documents/N1MM Logger+`. Setting
|
||||||
|
`NONEMM_HOME` puts the whole lot somewhere else, which is what a station keeping
|
||||||
|
its files off the Documents folder, or running two copies side by side, wants.
|
||||||
|
|
||||||
|
If a folder cannot be made — something else is already sitting at that path, or
|
||||||
|
the disk will not have it — the program says so in a window and stops, rather
|
||||||
|
than dying with a stack trace before it has a window to say anything in.
|
||||||
|
|
||||||
### Operators
|
### Operators
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ public partial class App : Application
|
|||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
UserPaths paths = UserPaths.Default();
|
UserPaths paths = UserPaths.Default();
|
||||||
paths.CreateFolders();
|
if (paths.CreateFolders() is { } problem)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new StartupProblemWindow(problem, paths.Root);
|
||||||
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
return;
|
||||||
|
}
|
||||||
Settings settings = Settings.Load(paths.SettingsFile);
|
Settings settings = Settings.Load(paths.SettingsFile);
|
||||||
Themes.Use(settings.Theme);
|
Themes.Use(settings.Theme);
|
||||||
Themes.Changed += (_, _) => ApplyTheme();
|
Themes.Changed += (_, _) => ApplyTheme();
|
||||||
|
|||||||
@@ -33,7 +33,13 @@ public sealed class UserPaths
|
|||||||
Own = Path.Combine(root, "Nonemm");
|
Own = Path.Combine(root, "Nonemm");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static UserPaths Default() => new(DefaultRoot());
|
/// `NONEMM_HOME` moves the whole lot somewhere else, for a station that
|
||||||
|
/// keeps its files off the Documents folder or runs two copies side by
|
||||||
|
/// side.
|
||||||
|
public static UserPaths Default() =>
|
||||||
|
new(Environment.GetEnvironmentVariable("NONEMM_HOME") is { Length: > 0 } wanted
|
||||||
|
? wanted
|
||||||
|
: DefaultRoot());
|
||||||
|
|
||||||
public string Root { get; }
|
public string Root { get; }
|
||||||
|
|
||||||
@@ -97,12 +103,34 @@ public sealed class UserPaths
|
|||||||
? Wav
|
? Wav
|
||||||
: Path.Combine(Wav, operatorCallsign.Trim().ToUpperInvariant());
|
: Path.Combine(Wav, operatorCallsign.Trim().ToUpperInvariant());
|
||||||
|
|
||||||
public void CreateFolders()
|
/// Makes every folder, and answers with what stopped it rather than
|
||||||
|
/// throwing: this runs before there is a window to report anything in, and
|
||||||
|
/// a program that dies with a stack trace tells the operator nothing.
|
||||||
|
public string? CreateFolders()
|
||||||
{
|
{
|
||||||
foreach (string folder in All())
|
foreach (string folder in All())
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(folder);
|
if (Directory.Exists(folder))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (File.Exists(folder))
|
||||||
|
{
|
||||||
|
return $"{folder} is a file, not a folder. Move it out of the way and start again.";
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(folder);
|
||||||
|
}
|
||||||
|
catch (Exception failure) when (failure is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
// something is already there that is neither a file nor a
|
||||||
|
// folder — a link to somewhere that is not there is the usual
|
||||||
|
// one — or the disk will not have it
|
||||||
|
return $"{folder} could not be made: {failure.Message}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every folder that is made at startup, the root first.
|
/// Every folder that is made at startup, the root first.
|
||||||
|
|||||||
52
src/Nonemm.App/Windows/StartupProblemWindow.cs
Normal file
52
src/Nonemm.App/Windows/StartupProblemWindow.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace Nonemm.App.Windows;
|
||||||
|
|
||||||
|
/// What the operator sees when the program cannot make the folders it keeps
|
||||||
|
/// everything in. There is no log, no settings and no entry window at that
|
||||||
|
/// point, so this says what is wrong and what to do about it, and closing it
|
||||||
|
/// ends the program.
|
||||||
|
public sealed class StartupProblemWindow : Window
|
||||||
|
{
|
||||||
|
public StartupProblemWindow(string problem, string root)
|
||||||
|
{
|
||||||
|
Title = "Nonemm cannot start";
|
||||||
|
Width = 560;
|
||||||
|
SizeToContent = SizeToContent.Height;
|
||||||
|
WindowStartupLocation = WindowStartupLocation.CenterScreen;
|
||||||
|
Content = new StackPanel
|
||||||
|
{
|
||||||
|
Margin = new Avalonia.Thickness(18),
|
||||||
|
Spacing = 10,
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = "The program keeps its files with N1MM's, and could not make them:",
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
},
|
||||||
|
new TextBlock { Text = problem, TextWrapping = TextWrapping.Wrap, FontWeight = FontWeight.Bold },
|
||||||
|
new TextBlock
|
||||||
|
{
|
||||||
|
Text = $"The folder it works in is {root}. Setting NONEMM_HOME to another "
|
||||||
|
+ "path moves the whole lot somewhere else.",
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
FontSize = 11,
|
||||||
|
Opacity = 0.75,
|
||||||
|
},
|
||||||
|
new Button
|
||||||
|
{
|
||||||
|
Content = "Quit",
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Right,
|
||||||
|
IsDefault = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (((StackPanel)Content).Children[^1] is Button quit)
|
||||||
|
{
|
||||||
|
quit.Click += (_, _) => Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ public class UserPathsTests : IDisposable
|
|||||||
{
|
{
|
||||||
UserPaths paths = new(root);
|
UserPaths paths = new(root);
|
||||||
|
|
||||||
paths.CreateFolders();
|
Assert.Null(paths.CreateFolders());
|
||||||
|
|
||||||
string[] made = [.. Directory.GetDirectories(root).Select(Path.GetFileName).OfType<string>().Order()];
|
string[] made = [.. Directory.GetDirectories(root).Select(Path.GetFileName).OfType<string>().Order()];
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
@@ -62,4 +62,32 @@ public class UserPathsTests : IDisposable
|
|||||||
|
|
||||||
Assert.Equal(Path.Combine(root, "Nonemm", "settings.json"), paths.SettingsFile);
|
Assert.Equal(Path.Combine(root, "Nonemm", "settings.json"), paths.SettingsFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A file where the folder should be stops the program starting, so it says
|
||||||
|
/// so rather than throwing at an operator who has no window to read it in.
|
||||||
|
[Fact]
|
||||||
|
public void AFileWhereTheFolderShouldBeIsReported()
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(root);
|
||||||
|
string inTheWay = Path.Combine(root, "N1MM Logger+");
|
||||||
|
File.WriteAllText(inTheWay, "not a folder");
|
||||||
|
UserPaths paths = new(inTheWay);
|
||||||
|
|
||||||
|
string? problem = paths.CreateFolders();
|
||||||
|
|
||||||
|
Assert.NotNull(problem);
|
||||||
|
Assert.Contains(inTheWay, problem);
|
||||||
|
Assert.Contains("is a file", problem);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FoldersThatAreAlreadyThereAreLeftAlone()
|
||||||
|
{
|
||||||
|
UserPaths paths = new(root);
|
||||||
|
paths.CreateFolders();
|
||||||
|
File.WriteAllText(Path.Combine(paths.Databases, "om5m.s3db"), "log");
|
||||||
|
|
||||||
|
Assert.Null(paths.CreateFolders());
|
||||||
|
Assert.Equal("log", File.ReadAllText(Path.Combine(paths.Databases, "om5m.s3db")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user