Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Fronter.NET.Tests/Extensions/TranslationSourceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,37 @@ public void InvalidSavedLanguageFallsBackToEnglish() {
}
}

[Fact]
public void TranslationsAreLoadedFromBaseDirectoryNotWorkingDirectory() {
// Reproduce the macOS launch scenario: CWD is a different directory that
// happens to contain its own localization files with different content.
var realBaseDir = CreateLocalizationRoot();
var decoyWorkingDirectory = CreateLocalizationRoot();
var previousWorkingDirectory = Directory.GetCurrentDirectory();
try {
WriteLocalizationFile(realBaseDir, "english", "EXIT", "Exit");
WriteLocalizationFile(decoyWorkingDirectory, "english", "EXIT", "Decoy");

Directory.SetCurrentDirectory(decoyWorkingDirectory);

var source = new TranslationSource(realBaseDir);

// Translations must come from the base directory, not the working directory.
Assert.Equal("Exit", source.Translate("EXIT"));

source.SaveLanguage("english");

// The saved language file must be written next to the executable's files.
Assert.True(File.Exists(Path.Combine(realBaseDir, "Configuration", "fronter-language.txt")));
Assert.False(File.Exists(Path.Combine(decoyWorkingDirectory, "Configuration", "fronter-language.txt")));
}
finally {
Directory.SetCurrentDirectory(previousWorkingDirectory);
Cleanup(realBaseDir);
Cleanup(decoyWorkingDirectory);
}
}

private static string CreateLocalizationRoot() {
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(Path.Combine(tempDir, "Configuration"));
Expand Down
49 changes: 49 additions & 0 deletions Fronter.NET.Tests/FronterPathsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System;
using System.IO;
using Xunit;

namespace Fronter.Tests;

// This collection disables parallelization, so changing the process working
// directory in these tests cannot affect other tests.
[Collection("Sequential")]
public sealed class FronterPathsTests {
[Fact]
public void AllPathsAreRootedInBaseDirectoryIndependentlyOfWorkingDirectory() {
// Reproduce the macOS launch scenario: the process working directory is
// NOT the directory containing the executable (Finder/Dock set CWD to "/").
var decoyDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var previousWorkingDirectory = Directory.GetCurrentDirectory();
try {
Directory.CreateDirectory(decoyDirectory);
Directory.SetCurrentDirectory(decoyDirectory);

var baseDirectory = FronterPaths.BaseDirectory;
Assert.Equal(AppContext.BaseDirectory, baseDirectory);
Assert.Equal(Path.Combine(baseDirectory, "log.txt"), FronterPaths.LogFilePath);
Assert.Equal(Path.Combine(baseDirectory, "Configuration", "fronter-theme.txt"), FronterPaths.ThemeFilePath);
Assert.Equal(Path.Combine(baseDirectory, "temp"), FronterPaths.TempDirectoryPath);
Assert.Equal(Path.Combine(baseDirectory, "Updater"), FronterPaths.UpdaterDirectoryPath);
Assert.Equal(Path.Combine(baseDirectory, "Updater-running"), FronterPaths.UpdaterRunningDirectoryPath);

// Every path must be absolute and point inside the base directory.
string[] paths = [
FronterPaths.LogFilePath,
FronterPaths.ThemeFilePath,
FronterPaths.TempDirectoryPath,
FronterPaths.UpdaterDirectoryPath,
FronterPaths.UpdaterRunningDirectoryPath,
];
foreach (var path in paths) {
Assert.True(Path.IsPathFullyQualified(path), $"Path is not fully qualified: {path}");
Assert.StartsWith(baseDirectory, path);
}
}
finally {
Directory.SetCurrentDirectory(previousWorkingDirectory);
if (Directory.Exists(decoyDirectory)) {
Directory.Delete(decoyDirectory, recursive: true);
}
}
}
}
33 changes: 33 additions & 0 deletions Fronter.NET.Tests/Services/LoggingConfiguratorTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using log4net;
using System;
using System.IO;
using System.Text;
using Xunit;
Expand Down Expand Up @@ -26,4 +27,36 @@ public void MessagesAreLoggedToLogTxtFile() {
Assert.Contains("Test warning", logFileContent);
Assert.Contains("Test error", logFileContent);
}

[Fact]
public void LogsAreWrittenNextToExecutableNotCurrentWorkingDirectory() {
// Reproduce the macOS launch scenario: CWD is not the app's directory.
var decoyDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var previousWorkingDirectory = Directory.GetCurrentDirectory();
try {
Directory.CreateDirectory(decoyDirectory);
Directory.SetCurrentDirectory(decoyDirectory);

LoggingConfigurator.ConfigureLogging(useConsole: false);

var logger = LogManager.GetLogger(typeof(LoggingConfiguratorTests));
logger.Info("Path resolution test message");

Assert.True(File.Exists(FronterPaths.LogFilePath), $"Expected log file at {FronterPaths.LogFilePath}");

using var fs = new FileStream(FronterPaths.LogFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var sr = new StreamReader(fs, Encoding.Default);
var logFileContent = sr.ReadToEnd();
Assert.Contains("Path resolution test message", logFileContent);

// Nothing may be written into the working directory.
Assert.False(File.Exists(Path.Combine(decoyDirectory, "log.txt")));
}
finally {
Directory.SetCurrentDirectory(previousWorkingDirectory);
if (Directory.Exists(decoyDirectory)) {
Directory.Delete(decoyDirectory, recursive: true);
}
}
}
}
2 changes: 1 addition & 1 deletion Fronter.NET/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace Fronter;

internal sealed class App : Application {
private static readonly ILog logger = LogManager.GetLogger("Frontend");
private const string FronterThemePath = "Configuration/fronter-theme.txt";
private static readonly string FronterThemePath = FronterPaths.ThemeFilePath;
private const string DefaultTheme = "Default";

public override void Initialize() {
Expand Down
4 changes: 2 additions & 2 deletions Fronter.NET/Extensions/TranslationSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal sealed partial class TranslationSource : ReactiveObject {
private readonly Lock translationsLock = new();
private readonly string baseDirectory;
private readonly Dictionary<string, List<string>> localizationFilePathsByLanguage = new(StringComparer.Ordinal);
private readonly HashSet<string> loadedTranslationLanguages = new(StringComparer.Ordinal);
private readonly HashSet<string> loadedTranslationLanguages = [with(StringComparer.Ordinal)];
private int deferredTranslationsLoadStarted;
private Task? deferredTranslationsLoadTask;

Expand All @@ -32,7 +32,7 @@ internal TranslationSource(string baseDirectory) {

string languagesPath = Path.Combine(baseDirectory, "languages.txt");
if (!File.Exists(languagesPath)) {
logger.Error("No languages dictionary found!");
logger.Error($"No languages dictionary found at {languagesPath}!");
return;
}

Expand Down
25 changes: 25 additions & 0 deletions Fronter.NET/FronterPaths.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using System.IO;

namespace Fronter;

// Central place for resolving app-relative file paths.
// All paths must be rooted at AppContext.BaseDirectory (the directory containing
// the executable) rather than the current working directory: on macOS, launching
// the app from Finder/Dock sets the working directory to "/", which used to make
// the app unable to find its files (see ImperatorToCK3 issue #2471).
internal static class FronterPaths {
public static string BaseDirectory => AppContext.BaseDirectory;

public static string ConfigurationDirectoryPath => Path.Combine(BaseDirectory, "Configuration");

public static string ThemeFilePath => Path.Combine(ConfigurationDirectoryPath, "fronter-theme.txt");

public static string LogFilePath => Path.Combine(BaseDirectory, "log.txt");

public static string TempDirectoryPath => Path.Combine(BaseDirectory, "temp");

public static string UpdaterDirectoryPath => Path.Combine(BaseDirectory, "Updater");

public static string UpdaterRunningDirectoryPath => Path.Combine(BaseDirectory, "Updater-running");
}
2 changes: 1 addition & 1 deletion Fronter.NET/LoggingConfigurator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static void ConfigureLogging(bool useConsole = false) {
layout.ActivateOptions();
var fileAppender = new FileAppender {
Name = "file",
File = "log.txt",
File = FronterPaths.LogFilePath,
AppendToFile = false,
Threshold = Level.All,
Layout = layout,
Expand Down
8 changes: 4 additions & 4 deletions Fronter.NET/Services/ConverterLauncher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,27 +237,27 @@ private static async Task<bool> GetSaveUploadConsent() {
}

private static async Task AttachLogAndSaveToSentry(Config config, SentryHelper sentryHelper) {
sentryHelper.AddAttachment("log.txt");
sentryHelper.AddAttachment(FronterPaths.LogFilePath);

var saveLocation = config.RequiredFiles.FirstOrDefault(f => f.Name.Equals("SaveGame"))?.Value;
if (saveLocation is null) {
return;
}

Directory.CreateDirectory("temp");
Directory.CreateDirectory(FronterPaths.TempDirectoryPath);

// Create zip with save file.
var dateTimeString = DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture);
var asciiSaveName = CommonFunctions.TrimExtension(Path.GetFileName(saveLocation)).FoldToASCII();
var archivePath = $"temp/SaveGame_{dateTimeString}_{asciiSaveName}.zip";
var archivePath = Path.Combine(FronterPaths.TempDirectoryPath, $"SaveGame_{dateTimeString}_{asciiSaveName}.zip");
using (var zip = ZipFile.Open(archivePath, ZipArchiveMode.Create)) {
zip.CreateEntryFromFile(saveLocation, new FileInfo(saveLocation).Name);
}

// Sentry allows up to 20 MB per compressed request.
// So we need to calculate whether we can fit the save archive.
// Otherwise we upload it to Backblaze.
var logSize = new FileInfo("log.txt").Length; // Size in bytes.
var logSize = new FileInfo(FronterPaths.LogFilePath).Length; // Size in bytes.
const int spaceForBaseRequest = 1024 * 1024 / 2; // 0.5 MB, arbitrary.
var saveSizeLimitForSentry = (20 * 1024 * 1024) - (logSize + spaceForBaseRequest);
var saveArchiveSize = new FileInfo(archivePath).Length;
Expand Down
4 changes: 2 additions & 2 deletions Fronter.NET/Services/UpdateChecker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -434,8 +434,8 @@ public static async Task RunInstallerAndDie(string installerUrl, Config config,
}

public static void StartUpdaterAndDie(string archiveUrl, string converterBackendDirName) {
var updaterDirPath = Path.Combine(".", "Updater");
var updaterRunningDirPath = Path.Combine(".", "Updater-running");
var updaterDirPath = FronterPaths.UpdaterDirectoryPath;
var updaterRunningDirPath = FronterPaths.UpdaterRunningDirectoryPath;

const string manualUpdateHint = "Try updating the converter manually.";
if (Directory.Exists(updaterRunningDirPath) && !FileSystemHelper.TryDeleteFolder(updaterRunningDirPath)) {
Expand Down
Loading