Refactored Pandoc handling with improved process abstractions

This commit is contained in:
Thorsten Sommer 2025-05-29 16:12:20 +02:00
parent 1de9e19c49
commit 4c09c434e2
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 46 additions and 24 deletions

View File

@ -46,7 +46,8 @@ public partial class PandocDialog : ComponentBase
private async Task CheckPandocAvailabilityAsync() private async Task CheckPandocAvailabilityAsync()
{ {
this.isPandocAvailable = await Pandoc.CheckAvailabilityAsync(this.RustService); var pandocInstallation = await Pandoc.CheckAvailabilityAsync(this.RustService);
this.isPandocAvailable = pandocInstallation.IsAvailable;
this.showSkeleton = false; this.showSkeleton = false;
await this.InvokeAsync(this.StateHasChanged); await this.InvokeAsync(this.StateHasChanged);
} }

View File

@ -36,15 +36,19 @@ public static partial class Pandoc
/// <param name="outputFormat">The format of the output file (e.g., pdf, docx, etc.).</param> /// <param name="outputFormat">The format of the output file (e.g., pdf, docx, etc.).</param>
/// <param name="additionalArgs">Additional arguments to pass to the pandoc command (optional).</param> /// <param name="additionalArgs">Additional arguments to pass to the pandoc command (optional).</param>
/// <returns>The ProcessStartInfo object configured to run pandoc with the specified parameters.</returns> /// <returns>The ProcessStartInfo object configured to run pandoc with the specified parameters.</returns>
public static async Task<ProcessStartInfo> PreparePandocProcess(RustService rustService, string inputFile, string outputFile, string inputFormat, string outputFormat, string? additionalArgs = null) => new() public static async Task<PandocPreparedProcess> PreparePandocProcess(RustService rustService, string inputFile, string outputFile, string inputFormat, string outputFormat, string? additionalArgs = null)
{ {
FileName = await PandocExecutablePath(rustService), var pandocExecutable = await PandocExecutablePath(rustService);
return new (new ProcessStartInfo
{
FileName = pandocExecutable.Executable,
Arguments = $"{inputFile} -f {inputFormat} -t {outputFormat} {additionalArgs ?? string.Empty} -o {outputFile}", Arguments = $"{inputFile} -f {inputFormat} -t {outputFormat} {additionalArgs ?? string.Empty} -o {outputFile}",
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true, RedirectStandardError = true,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true CreateNoWindow = true
}; }, pandocExecutable.IsLocalInstallation);
}
/// <summary> /// <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. /// Checks if pandoc is available on the system and can be started as a process or is present in AI Studio's data dir.
@ -52,11 +56,12 @@ public static partial class Pandoc
/// <param name="rustService">Global rust service to access file system and data dir.</param> /// <param name="rustService">Global rust service to access file system and data dir.</param>
/// <param name="showMessages">Controls if snackbars are shown to the user.</param> /// <param name="showMessages">Controls if snackbars are shown to the user.</param>
/// <returns>True, if pandoc is available and the minimum required version is met, else false.</returns> /// <returns>True, if pandoc is available and the minimum required version is met, else false.</returns>
public static async Task<bool> CheckAvailabilityAsync(RustService rustService, bool showMessages = true) public static async Task<PandocInstallation> CheckAvailabilityAsync(RustService rustService, bool showMessages = true)
{ {
try try
{ {
var startInfo = await PreparePandocProcess(rustService, string.Empty, string.Empty, string.Empty, string.Empty); var preparedProcess = await PreparePandocProcess(rustService, string.Empty, string.Empty, string.Empty, string.Empty);
var startInfo = preparedProcess.StartInfo;
startInfo.Arguments = "--version"; startInfo.Arguments = "--version";
using var process = Process.Start(startInfo); using var process = Process.Start(startInfo);
@ -66,7 +71,7 @@ public static partial class Pandoc
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Help, "The pandoc process could not be started.")); await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Help, "The pandoc process could not be started."));
LOG.LogInformation("The pandoc process was not started, it was null"); LOG.LogInformation("The pandoc process was not started, it was null");
return false; return new(false, "Was not able to start the pandoc process.", false, string.Empty, preparedProcess.IsLocal);
} }
var output = await process.StandardOutput.ReadToEndAsync(); var output = await process.StandardOutput.ReadToEndAsync();
@ -77,7 +82,7 @@ public static partial class Pandoc
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, "The pandoc process exited unexpectedly.")); await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, "The pandoc process exited unexpectedly."));
LOG.LogError("The pandoc process was exited with code {ProcessExitCode}", process.ExitCode); LOG.LogError("The pandoc process was exited with code {ProcessExitCode}", process.ExitCode);
return false; return new(false, "Pandoc is not available on the system or the process exited unexpectedly.", false, string.Empty, preparedProcess.IsLocal);
} }
var versionMatch = PandocCmdRegex().Match(output); var versionMatch = PandocCmdRegex().Match(output);
@ -87,25 +92,25 @@ public static partial class Pandoc
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Terminal, "pandoc --version returned an invalid format.")); await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Terminal, "pandoc --version returned an invalid format."));
LOG.LogError("pandoc --version returned an invalid format:\n {Output}", output); LOG.LogError("pandoc --version returned an invalid format:\n {Output}", output);
return false; return new(false, "Pandoc is not available on the system or the version could not be parsed.", false, string.Empty, preparedProcess.IsLocal);
} }
var versions = versionMatch.Groups[1].Value; var versions = versionMatch.Groups[1].Value;
var installedVersion = Version.Parse(versions); var installedVersion = Version.Parse(versions);
var installedVersionString = installedVersion.ToString();
if (installedVersion >= MINIMUM_REQUIRED_VERSION) if (installedVersion >= MINIMUM_REQUIRED_VERSION)
{ {
if (showMessages) if (showMessages)
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, $"Pandoc {installedVersion.ToString()} is installed.")); await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, $"Pandoc {installedVersionString} is installed."));
return true; return new(true, string.Empty, true, installedVersionString, preparedProcess.IsLocal);
} }
if (showMessages) if (showMessages)
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Build, $"Pandoc {installedVersion.ToString()} is installed, but it doesn't match the required version ({MINIMUM_REQUIRED_VERSION.ToString()}).")); await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Build, $"Pandoc {installedVersionString} is installed, but it doesn't match the required version ({MINIMUM_REQUIRED_VERSION.ToString()})."));
LOG.LogInformation("Pandoc {Installed} is installed, but it does not match the required version ({Requirement})", installedVersion.ToString(), MINIMUM_REQUIRED_VERSION.ToString());
return false;
LOG.LogInformation("Pandoc {Installed} is installed, but it does not match the required version ({Requirement})", installedVersionString, MINIMUM_REQUIRED_VERSION.ToString());
return new(true, $"Pandoc {installedVersionString} is installed, but it does not match the required version ({MINIMUM_REQUIRED_VERSION.ToString()}).", false, installedVersionString, preparedProcess.IsLocal);
} }
catch (Exception e) catch (Exception e)
{ {
@ -113,7 +118,7 @@ public static partial class Pandoc
await MessageBus.INSTANCE.SendError(new (@Icons.Material.Filled.AppsOutage, "Pandoc is not installed.")); await MessageBus.INSTANCE.SendError(new (@Icons.Material.Filled.AppsOutage, "Pandoc is not installed."));
LOG.LogError("Pandoc is not installed and threw an exception: {Message}", e.Message); LOG.LogError("Pandoc is not installed and threw an exception: {Message}", e.Message);
return false; return new(false, "Pandoc is not installed or could not be started.", false, string.Empty, false);
} }
} }
@ -305,7 +310,7 @@ public static partial class Pandoc
/// </remarks> /// </remarks>
/// <param name="rustService">Global rust service to access file system and data dir.</param> /// <param name="rustService">Global rust service to access file system and data dir.</param>
/// <returns>Path to the pandoc executable.</returns> /// <returns>Path to the pandoc executable.</returns>
private static async Task<string> PandocExecutablePath(RustService rustService) private static async Task<PandocExecutable> PandocExecutablePath(RustService rustService)
{ {
// //
// First, we try to find the pandoc executable in the data directory. // First, we try to find the pandoc executable in the data directory.
@ -320,7 +325,7 @@ public static partial class Pandoc
{ {
var pandocPath = Path.Combine(subdirectory, executableName); var pandocPath = Path.Combine(subdirectory, executableName);
if (File.Exists(pandocPath)) if (File.Exists(pandocPath))
return pandocPath; return new(pandocPath, true);
} }
} }
catch catch
@ -331,7 +336,7 @@ public static partial class Pandoc
// //
// When no local installation was found, we assume that the pandoc executable is in the system PATH. // When no local installation was found, we assume that the pandoc executable is in the system PATH.
// //
return PandocExecutableName; return new(PandocExecutableName, false);
} }
private static async Task<string> GetPandocDataFolder(RustService rustService) => Path.Join(await rustService.GetDataDirectory(), "pandoc"); private static async Task<string> GetPandocDataFolder(RustService rustService) => Path.Join(await rustService.GetDataDirectory(), "pandoc");

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools;
public readonly record struct PandocExecutable(string Executable, bool IsLocalInstallation);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools;
public readonly record struct PandocInstallation(bool CheckWasSuccessful, string ErrorMessage, bool IsAvailable, string Version, bool IsLocalInstallation);

View File

@ -0,0 +1,10 @@
using System.Diagnostics;
namespace AIStudio.Tools;
public class PandocPreparedProcess(ProcessStartInfo startInfo, bool isLocal)
{
public ProcessStartInfo StartInfo => startInfo;
public bool IsLocal => isLocal;
}