diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index 314d30c2..73b7b83b 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -6973,6 +6973,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
+-- AI Studio couldn't install Pandoc because the archive was not found.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
+
+-- Pandoc doesn't seem to be installed.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed."
+
-- Was not able to validate the Pandoc installation.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
@@ -6994,20 +7000,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
--- Pandoc was not installed successfully, because the archive was not found.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found."
+-- AI Studio couldn't install Pandoc because the archive type is unknown.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown."
-- Pandoc is not available on the system or the process had issues.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
--- Pandoc was not installed successfully, because the archive type is unknown.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
+-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive."
--- It seems that Pandoc is not installed.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
+-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead."
--- The latest Pandoc version was not found, installing version {0} instead.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
+-- AI Studio couldn't install Pandoc.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
-- Pandoc is required for Microsoft Word export.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
index fb4216bd..32598b6f 100644
--- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua
@@ -6975,6 +6975,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Fehler
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Fehler beim Abrufen der Authentifizierungsmethoden: Der ERI-Server hat keine gültige Antwort zurückgegeben."
+-- AI Studio couldn't install Pandoc because the archive was not found.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde."
+
+-- Pandoc doesn't seem to be installed.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc scheint nicht installiert zu sein."
+
-- Was not able to validate the Pandoc installation.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Die Pandoc-Installation konnte nicht überprüft werden."
@@ -6996,20 +7002,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} ist insta
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} ist installiert, entspricht aber nicht der benötigten Version (v{1})."
--- Pandoc was not installed successfully, because the archive was not found.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc wurde nicht erfolgreich installiert, da das Archiv nicht gefunden wurde."
+-- AI Studio couldn't install Pandoc because the archive type is unknown.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio konnte Pandoc nicht installieren, da der Archivtyp unbekannt ist."
-- Pandoc is not available on the system or the process had issues.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc ist auf dem System nicht verfügbar oder der Vorgang ist auf Probleme gestoßen."
--- Pandoc was not installed successfully, because the archive type is unknown.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc wurde nicht erfolgreich installiert, da der Archivtyp unbekannt ist."
+-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio konnte Pandoc nicht installieren, da die ausführbare Datei im Archiv nicht gefunden wurde."
--- It seems that Pandoc is not installed.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "Es scheint, dass Pandoc nicht installiert ist."
+-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio konnte die neueste Pandoc-Version nicht finden und installiert stattdessen Version {0}."
--- The latest Pandoc version was not found, installing version {0} instead.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "Die neueste Pandoc-Version wurde nicht gefunden, stattdessen wird Version {0} installiert."
+-- AI Studio couldn't install Pandoc.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio konnte Pandoc nicht installieren."
-- Pandoc is required for Microsoft Word export.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc wird für den Export nach Microsoft Word benötigt."
diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
index 26be03ee..c837f96d 100644
--- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua
@@ -6975,6 +6975,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed
-- Failed to retrieve the authentication methods: the ERI server did not return a valid response.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response."
+-- AI Studio couldn't install Pandoc because the archive was not found.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found."
+
+-- Pandoc doesn't seem to be installed.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed."
+
-- Was not able to validate the Pandoc installation.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation."
@@ -6996,20 +7002,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal
-- Pandoc v{0} is installed, but it does not match the required version (v{1}).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})."
--- Pandoc was not installed successfully, because the archive was not found.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found."
+-- AI Studio couldn't install Pandoc because the archive type is unknown.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown."
-- Pandoc is not available on the system or the process had issues.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues."
--- Pandoc was not installed successfully, because the archive type is unknown.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown."
+-- AI Studio couldn't install Pandoc because the executable was not found in the archive.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive."
--- It seems that Pandoc is not installed.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed."
+-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead."
--- The latest Pandoc version was not found, installing version {0} instead.
-UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
+-- AI Studio couldn't install Pandoc.
+UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc."
-- Pandoc is required for Microsoft Word export.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export."
diff --git a/app/MindWork AI Studio/Tools/Pandoc.cs b/app/MindWork AI Studio/Tools/Pandoc.cs
index c5826eaa..8767b1ee 100644
--- a/app/MindWork AI Studio/Tools/Pandoc.cs
+++ b/app/MindWork AI Studio/Tools/Pandoc.cs
@@ -35,12 +35,13 @@ public static partial class Pandoc
private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE;
private static readonly HttpClient WEB_CLIENT = new();
+ private static readonly SemaphoreSlim INSTALLATION_LOCK = new(1, 1);
///
/// Prepares a Pandoc process by using the Pandoc process builder.
///
/// The Pandoc process builder with default settings.
- public static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
+ private static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
///
/// Checks if pandoc is available on the system and can be started as a process or is present in AI Studio's data dir.
@@ -145,12 +146,12 @@ public static partial class Pandoc
catch (Exception e)
{
if (showMessages)
- await MessageBus.INSTANCE.SendError(new(@Icons.Material.Filled.AppsOutage, TB("It seems that Pandoc is not installed.")));
+ await MessageBus.INSTANCE.SendError(new(@Icons.Material.Filled.AppsOutage, TB("Pandoc doesn't seem to be installed.")));
if(shouldLog)
LOG.LogError(e, "Pandoc availability check failed. This usually means Pandoc is not installed or not in the system PATH.");
- return new(false, TB("It seems that Pandoc is not installed."), false, string.Empty, false);
+ return new(false, TB("Pandoc doesn't seem to be installed."), false, string.Empty, false);
}
finally
{
@@ -165,76 +166,230 @@ public static partial class Pandoc
/// None
public static async Task InstallAsync(RustService rustService)
{
+ await INSTALLATION_LOCK.WaitAsync();
+
var latestVersion = await FetchLatestVersionAsync();
var installDir = await GetPandocDataFolder(rustService);
- ClearFolder(installDir);
+ var installParentDir = Path.GetDirectoryName(installDir) ?? Path.GetTempPath();
+ var stagingDir = Path.Combine(installParentDir, $"pandoc-install-{Guid.NewGuid():N}");
+ var pandocTempDownloadFile = Path.GetTempFileName();
LOG.LogInformation("Trying to install Pandoc v{0} to '{1}'...", latestVersion, installDir);
try
{
- if (!Directory.Exists(installDir))
- Directory.CreateDirectory(installDir);
-
- // Create a temporary file to download the archive to:
- var pandocTempDownloadFile = Path.GetTempFileName();
+ if (!Directory.Exists(installParentDir))
+ Directory.CreateDirectory(installParentDir);
//
// Download the latest Pandoc archive from GitHub:
//
- var uri = await GenerateArchiveUriAsync();
- var response = await WEB_CLIENT.GetAsync(uri);
+ var uri = GenerateArchiveUri(latestVersion);
+ if (string.IsNullOrWhiteSpace(uri))
+ {
+ await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive type is unknown.")));
+ LOG.LogError("Pandoc was not installed, no archive is available for architecture '{Architecture}'.", CPU_ARCHITECTURE.ToUserFriendlyName());
+ return;
+ }
+
+ using var response = await WEB_CLIENT.GetAsync(uri);
if (!response.IsSuccessStatusCode)
{
- await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found.")));
+ await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive was not found.")));
LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage);
return;
}
// Download the archive to the temporary file:
- await using var tempFileStream = File.Create(pandocTempDownloadFile);
- await response.Content.CopyToAsync(tempFileStream);
+ await using (var tempFileStream = File.Create(pandocTempDownloadFile))
+ {
+ await response.Content.CopyToAsync(tempFileStream);
+ await tempFileStream.FlushAsync();
+ }
+ Directory.CreateDirectory(stagingDir);
if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
- ZipFile.ExtractToDirectory(pandocTempDownloadFile, installDir);
+ await RunWithRetriesAsync(
+ () =>
+ {
+ ZipFile.ExtractToDirectory(pandocTempDownloadFile, stagingDir, true);
+ return Task.CompletedTask;
+ },
+ "extracting the Pandoc ZIP archive");
}
else if (uri.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase))
{
- await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read);
- await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
- await TarFile.ExtractToDirectoryAsync(uncompressedStream, installDir, true);
+ await RunWithRetriesAsync(
+ async () =>
+ {
+ await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read);
+ await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress);
+ await TarFile.ExtractToDirectoryAsync(uncompressedStream, stagingDir, true);
+ },
+ "extracting the Pandoc TAR archive");
}
else
{
- await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive type is unknown.")));
+ await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive type is unknown.")));
LOG.LogError("Pandoc was not installed, the archive is unknown: url='{0}'", uri);
return;
}
- File.Delete(pandocTempDownloadFile);
-
+ var stagedPandocExecutable = FindExecutableInDirectory(stagingDir, PandocProcessBuilder.PandocExecutableName);
+ if (string.IsNullOrWhiteSpace(stagedPandocExecutable))
+ {
+ await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the executable was not found in the archive.")));
+ LOG.LogError("Pandoc was not installed, the executable was not found in the extracted archive: '{StagingDir}'.", stagingDir);
+ return;
+ }
+
+ LOG.LogInformation("Found Pandoc executable in downloaded archive: '{Executable}'.", stagedPandocExecutable);
+
+ await ReplaceInstallationDirectoryAsync(stagingDir, installDir);
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, string.Format(TB("Pandoc v{0} was installed successfully."), latestVersion)));
LOG.LogInformation("Pandoc v{0} was installed successfully.", latestVersion);
}
catch (Exception ex)
{
+ await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc.")));
LOG.LogError(ex, "An error occurred while installing Pandoc.");
}
+ finally
+ {
+ TryDeleteFile(pandocTempDownloadFile);
+
+ if (Directory.Exists(stagingDir))
+ await TryDeleteFolderAsync(stagingDir);
+
+ INSTALLATION_LOCK.Release();
+ }
}
- private static void ClearFolder(string path)
+ private static async Task ReplaceInstallationDirectoryAsync(string stagingDir, string installDir)
{
- if (!Directory.Exists(path))
- return;
-
+ var backupDir = $"{installDir}.backup-{Guid.NewGuid():N}";
+ var hasBackup = false;
+ var stagingWasMoved = false;
+
try
{
- Directory.Delete(path, true);
+ if (Directory.Exists(installDir))
+ {
+ await MoveDirectoryWithRetriesAsync(installDir, backupDir, "moving the previous Pandoc installation to backup");
+ hasBackup = true;
+ }
+
+ await MoveDirectoryWithRetriesAsync(stagingDir, installDir, "moving the new Pandoc installation into place");
+ stagingWasMoved = true;
}
catch (Exception ex)
{
- LOG.LogError(ex, "Error clearing pandoc installation directory.");
+ if (hasBackup && !stagingWasMoved && !Directory.Exists(installDir) && Directory.Exists(backupDir))
+ {
+ try
+ {
+ await MoveDirectoryWithRetriesAsync(backupDir, installDir, "restoring the previous Pandoc installation");
+ hasBackup = false;
+ }
+ catch (Exception rollbackEx)
+ {
+ LOG.LogError(rollbackEx, "Error restoring previous Pandoc installation directory. Keeping backup directory at: '{BackupDir}'.", backupDir);
+ }
+ }
+
+ LOG.LogError(ex, "Error replacing pandoc installation directory.");
+ throw;
+ }
+ finally
+ {
+ if (hasBackup && stagingWasMoved && Directory.Exists(backupDir))
+ await TryDeleteFolderAsync(backupDir);
+ }
+ }
+
+ private static string FindExecutableInDirectory(string rootDirectory, string executableName)
+ {
+ if (!Directory.Exists(rootDirectory))
+ return string.Empty;
+
+ var rootExecutablePath = Path.Combine(rootDirectory, executableName);
+ if (File.Exists(rootExecutablePath))
+ return rootExecutablePath;
+
+ foreach (var subdirectory in Directory.GetDirectories(rootDirectory, "*", SearchOption.AllDirectories))
+ {
+ var pandocPath = Path.Combine(subdirectory, executableName);
+ if (File.Exists(pandocPath))
+ return pandocPath;
+ }
+
+ return string.Empty;
+ }
+
+ private static async Task MoveDirectoryWithRetriesAsync(string sourceDir, string destinationDir, string operationName)
+ {
+ await RunWithRetriesAsync(
+ () =>
+ {
+ Directory.Move(sourceDir, destinationDir);
+ return Task.CompletedTask;
+ },
+ operationName,
+ maxAttempts: 8);
+ }
+
+ private static async Task RunWithRetriesAsync(Func operation, string operationName, int maxAttempts = 4)
+ {
+ for (var attempt = 1; attempt <= maxAttempts; attempt++)
+ {
+ try
+ {
+ await operation();
+ return;
+ }
+ catch (Exception ex) when (attempt < maxAttempts && ex is IOException or UnauthorizedAccessException)
+ {
+ LOG.LogWarning(ex, "Error while {OperationName}; retrying attempt {Attempt}/{MaxAttempts}.", operationName, attempt + 1, maxAttempts);
+ await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt));
+ }
+ }
+ }
+
+ private static void TryDeleteFile(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
+ return;
+
+ try
+ {
+ File.Delete(path);
+ }
+ catch (Exception ex)
+ {
+ LOG.LogWarning(ex, "Was not able to delete temporary Pandoc archive: '{Path}'.", path);
+ }
+ }
+
+ private static async Task TryDeleteFolderAsync(string path)
+ {
+ if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
+ return;
+
+ try
+ {
+ await RunWithRetriesAsync(
+ () =>
+ {
+ Directory.Delete(path, true);
+ return Task.CompletedTask;
+ },
+ $"deleting temporary Pandoc directory '{path}'",
+ maxAttempts: 3);
+ }
+ catch (Exception ex)
+ {
+ LOG.LogWarning(ex, "Was not able to delete temporary Pandoc directory: '{Path}'.", path);
}
}
@@ -248,7 +403,7 @@ public static partial class Pandoc
if (!response.IsSuccessStatusCode)
{
LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}", response.StatusCode, response.RequestMessage);
- await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("The latest Pandoc version was not found, installing version {0} instead."), FALLBACK_VERSION.ToString())));
+ await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("AI Studio couldn't find the latest Pandoc version and will install version {0} instead."), FALLBACK_VERSION.ToString())));
return FALLBACK_VERSION.ToString();
}
@@ -257,7 +412,7 @@ public static partial class Pandoc
if (!versionMatch.Success)
{
LOG.LogError("The latest version regex returned nothing: {0}", versionMatch.Groups.ToString());
- await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("The latest Pandoc version was not found, installing version {0} instead."), FALLBACK_VERSION.ToString())));
+ await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("AI Studio couldn't find the latest Pandoc version and will install version {0} instead."), FALLBACK_VERSION.ToString())));
return FALLBACK_VERSION.ToString();
}
@@ -272,6 +427,11 @@ public static partial class Pandoc
public static async Task GenerateArchiveUriAsync()
{
var version = await FetchLatestVersionAsync();
+ return GenerateArchiveUri(version);
+ }
+
+ private static string GenerateArchiveUri(string version)
+ {
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-";
return CPU_ARCHITECTURE switch
{
diff --git a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs
index 6d95ad9f..6d0909f8 100644
--- a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs
+++ b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs
@@ -220,6 +220,17 @@ public sealed class PandocProcessBuilder
}
}
+ foreach (var candidate in SystemPandocExecutableCandidates(PandocExecutableName))
+ {
+ if (!File.Exists(candidate))
+ continue;
+
+ if (shouldLog)
+ LOGGER.LogInformation("Found system Pandoc installation at: '{Path}'.", candidate);
+
+ return new(candidate, false);
+ }
+
//
// When no local installation was found, we assume that the pandoc executable is in the system PATH:
//
@@ -238,4 +249,59 @@ public sealed class PandocProcessBuilder
/// Reads the os platform to determine the used executable name.
///
public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc";
+
+ private static IEnumerable SystemPandocExecutableCandidates(string executableName)
+ {
+ var candidates = new List();
+
+ switch (CPU_ARCHITECTURE)
+ {
+ case RID.WIN_X64 or RID.WIN_ARM64:
+ AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Pandoc", executableName);
+ AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Pandoc", executableName);
+ AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Pandoc", executableName);
+ break;
+
+ case RID.OSX_X64 or RID.OSX_ARM64:
+ AddCandidate(candidates, "/opt/homebrew/bin", executableName);
+ AddCandidate(candidates, "/usr/local/bin", executableName);
+ AddCandidate(candidates, "/usr/bin", executableName);
+ break;
+
+ case RID.LINUX_X64 or RID.LINUX_ARM64:
+ AddCandidate(candidates, "/usr/local/bin", executableName);
+ AddCandidate(candidates, "/usr/bin", executableName);
+ AddCandidate(candidates, "/snap/bin", executableName);
+
+ var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ AddCandidate(candidates, homeDirectory, ".local", "bin", executableName);
+ break;
+ }
+
+ foreach (var pathDirectory in GetPathDirectories())
+ AddCandidate(candidates, pathDirectory, executableName);
+
+ var comparer = CPU_ARCHITECTURE is RID.WIN_X64 or RID.WIN_ARM64
+ ? StringComparer.OrdinalIgnoreCase
+ : StringComparer.Ordinal;
+ return candidates.Distinct(comparer);
+ }
+
+ private static IEnumerable GetPathDirectories()
+ {
+ var pathValue = Environment.GetEnvironmentVariable("PATH");
+ if (string.IsNullOrWhiteSpace(pathValue))
+ yield break;
+
+ foreach (var pathDirectory in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ yield return pathDirectory;
+ }
+
+ private static void AddCandidate(List candidates, params string[] pathParts)
+ {
+ if (pathParts.Any(string.IsNullOrWhiteSpace))
+ return;
+
+ candidates.Add(Path.Combine(pathParts));
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md
index af2ad840..2fa98028 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md
@@ -1,5 +1,7 @@
# v26.5.5, build 240 (2026-05-xx xx:xx UTC)
- Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base.
+- Improved the Pandoc management and detection process to make it more reliable.
+- Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency.
- Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system.
- Upgraded Rust to v1.95.0.
- Upgraded .NET to v9.0.16.
diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs
index 1abd7951..f9ec3dbb 100644
--- a/runtime/src/app_window.rs
+++ b/runtime/src/app_window.rs
@@ -889,7 +889,7 @@ pub async fn resume_shortcuts(_token: APIToken) -> Json {
continue;
}
- match register_shortcut_with_callback(&app_handle, shortcut, *shortcut_id, event_sender.clone()) {
+ match register_shortcut_with_callback(app_handle, shortcut, *shortcut_id, event_sender.clone()) {
Ok(_) => {
info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id);
success_count += 1;
diff --git a/runtime/src/pandoc.rs b/runtime/src/pandoc.rs
index f2dc6a8f..4b6f91f4 100644
--- a/runtime/src/pandoc.rs
+++ b/runtime/src/pandoc.rs
@@ -1,13 +1,16 @@
-use std::path::{Path, PathBuf};
+use std::collections::HashSet;
+use std::env;
use std::fs;
+use std::path::{Path, PathBuf};
use std::sync::OnceLock;
-use log::warn;
+use log::{info, warn};
use tokio::process::Command;
use crate::environment::DATA_DIRECTORY;
use crate::metadata::META_DATA;
/// Tracks whether the RID mismatch warning has been logged.
static HAS_LOGGED_RID_MISMATCH: OnceLock<()> = OnceLock::new();
+static HAS_LOGGED_PANDOC_PATH: OnceLock<()> = OnceLock::new();
pub struct PandocExecutable {
pub executable: String,
@@ -114,28 +117,43 @@ impl PandocProcessBuilder {
// Any local installation should be preferred over the system-wide installation.
let data_folder = PathBuf::from(DATA_DIRECTORY.get().unwrap());
let local_installation_root_directory = data_folder.join("pandoc");
+ let executable_name = Self::pandoc_executable_name();
if local_installation_root_directory.exists() {
- let executable_name = Self::pandoc_executable_name();
+ if let Ok(pandoc_path) = Self::find_executable_in_dir(&local_installation_root_directory, &executable_name) {
+ HAS_LOGGED_PANDOC_PATH.get_or_init(|| {
+ info!(Source = "PandocProcessBuilder"; "Found local Pandoc installation at: '{}'.", pandoc_path.to_string_lossy()
+ );
+ });
- if let Ok(entries) = fs::read_dir(&local_installation_root_directory) {
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- if let Ok(pandoc_path) = Self::find_executable_in_dir(&path, &executable_name) {
- return PandocExecutable {
- executable: pandoc_path.to_string_lossy().to_string(),
- is_local_installation: true,
- };
- }
- }
- }
+ return PandocExecutable {
+ executable: pandoc_path.to_string_lossy().to_string(),
+ is_local_installation: true,
+ };
+ }
+ }
+
+ for candidate in Self::system_pandoc_executable_candidates(&executable_name) {
+ if candidate.exists() && candidate.is_file() {
+ HAS_LOGGED_PANDOC_PATH.get_or_init(|| {
+ info!(Source = "PandocProcessBuilder"; "Found system Pandoc installation at: '{}'.", candidate.to_string_lossy()
+ );
+ });
+
+ return PandocExecutable {
+ executable: candidate.to_string_lossy().to_string(),
+ is_local_installation: false,
+ };
}
}
// When no local installation was found, we assume that the pandoc executable is in the system PATH:
+ HAS_LOGGED_PANDOC_PATH.get_or_init(|| {
+ warn!(Source = "PandocProcessBuilder"; "Falling back to system PATH for the Pandoc executable: '{}'.", executable_name);
+ });
+
PandocExecutable {
- executable: Self::pandoc_executable_name(),
+ executable: executable_name,
is_local_installation: false,
}
}
@@ -161,6 +179,56 @@ impl PandocProcessBuilder {
Err("Executable not found".into())
}
+ fn system_pandoc_executable_candidates(executable_name: &str) -> Vec {
+ let mut candidates: Vec = Vec::new();
+ match env::consts::OS {
+ "windows" => {
+ Self::push_env_candidate(&mut candidates, "LOCALAPPDATA", &["Pandoc", executable_name]);
+ Self::push_env_candidate(&mut candidates, "ProgramFiles", &["Pandoc", executable_name]);
+ Self::push_env_candidate(&mut candidates, "ProgramFiles(x86)", &["Pandoc", executable_name]);
+ },
+ "macos" => {
+ candidates.push(PathBuf::from("/opt/homebrew/bin").join(executable_name));
+ candidates.push(PathBuf::from("/usr/local/bin").join(executable_name));
+ candidates.push(PathBuf::from("/usr/bin").join(executable_name));
+ },
+ "linux" => {
+ candidates.push(PathBuf::from("/usr/local/bin").join(executable_name));
+ candidates.push(PathBuf::from("/usr/bin").join(executable_name));
+ candidates.push(PathBuf::from("/snap/bin").join(executable_name));
+
+ if let Some(home_dir) = env::var_os("HOME") {
+ candidates.push(PathBuf::from(home_dir).join(".local").join("bin").join(executable_name));
+ }
+ },
+ _ => {},
+ }
+
+ if let Some(path_value) = env::var_os("PATH") {
+ for path_dir in env::split_paths(&path_value) {
+ candidates.push(path_dir.join(executable_name));
+ }
+ }
+
+ let mut seen = HashSet::new();
+ candidates
+ .into_iter()
+ .filter(|path| seen.insert(path.clone()))
+ .collect()
+ }
+
+ fn push_env_candidate(candidates: &mut Vec, env_name: &str, parts: &[&str]) {
+ if let Some(root) = env::var_os(env_name) {
+ let mut path = PathBuf::from(root);
+
+ for part in parts {
+ path.push(part);
+ }
+
+ candidates.push(path);
+ }
+ }
+
/// Determines the executable name based on the current OS at runtime.
///
/// This uses runtime detection instead of metadata to ensure correct behavior