Fixed & improved pandoc handling (#762)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-05-16 18:27:16 +02:00 committed by GitHub
parent 8f0effd25b
commit 91cfe8dcd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 384 additions and 70 deletions

View File

@ -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."

View File

@ -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."

View File

@ -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."

View File

@ -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);
/// <summary>
/// Prepares a Pandoc process by using the Pandoc process builder.
/// </summary>
/// <returns>The Pandoc process builder with default settings.</returns>
public static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
private static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create();
/// <summary>
/// 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
/// <returns>None</returns>
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<Task> 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<string> 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
{

View File

@ -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.
/// </summary>
public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc";
private static IEnumerable<string> SystemPandocExecutableCandidates(string executableName)
{
var candidates = new List<string>();
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<string> 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<string> candidates, params string[] pathParts)
{
if (pathParts.Any(string.IsNullOrWhiteSpace))
return;
candidates.Add(Path.Combine(pathParts));
}
}

View File

@ -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.

View File

@ -889,7 +889,7 @@ pub async fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> {
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;

View File

@ -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<PathBuf> {
let mut candidates: Vec<PathBuf> = 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<PathBuf>, 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