mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 12:13:48 +00:00
Fixed & improved pandoc handling (#762)
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
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:
1 parent
8f0effd25b
commit
91cfe8dcd0
8 files changed
+384
-70
No files matched your search
@@ -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
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user