mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-11 19:07:22 +00:00
Prepared release v26.11.1 (#1173)
This commit is contained in:
1 parent
9d1768e2f4
commit
520758771b
11 files changed
+339
-66
No files matched your search
+170
-38
@@ -1,25 +1,62 @@
|
||||
using System.Formats.Tar;
|
||||
using System.IO.Compression;
|
||||
|
||||
using Build.Tools;
|
||||
|
||||
using SharedTools;
|
||||
|
||||
namespace Build.Commands;
|
||||
|
||||
public static class Pdfium
|
||||
{
|
||||
private static readonly HttpClient CLIENT = new()
|
||||
private const int MAX_DOWNLOAD_ATTEMPTS = 3;
|
||||
|
||||
private static readonly TimeSpan CONNECT_TIMEOUT = TimeSpan.FromSeconds(30);
|
||||
private static readonly TimeSpan RESPONSE_TIMEOUT = TimeSpan.FromSeconds(60);
|
||||
private static readonly TimeSpan TRANSFER_TIMEOUT = TimeSpan.FromMinutes(5);
|
||||
private static readonly TimeSpan RETRY_DELAY = TimeSpan.FromSeconds(5);
|
||||
|
||||
//
|
||||
// Without a handler of its own, HttpClient waits for a connection without any limit, so a single
|
||||
// connection attempt which hangs uses up the whole timeout of the request. That happened twice while
|
||||
// preparing v26.11.1, while curl fetched the same file within a second. The handler limits each
|
||||
// connection attempt, the TLS handshake included, and each download attempt has its own time budget:
|
||||
//
|
||||
private static readonly HttpClient CLIENT = new(new SocketsHttpHandler { ConnectTimeout = CONNECT_TIMEOUT })
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(5)
|
||||
Timeout = Timeout.InfiniteTimeSpan
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Installs the Pdfium library of the given version into the resources of the Rust runtime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Every failure ends the build: without the library, AI Studio cannot read PDF files, and Tauri would fail
|
||||
/// later with a far less clear message, because its resources include the folder of the library. The download
|
||||
/// is skipped when the library of this version is already installed, which a version marker in the target
|
||||
/// folder of the runtime records. The marker cannot sit next to the library, because Tauri ships everything
|
||||
/// in that folder.
|
||||
/// </remarks>
|
||||
/// <param name="rid">The RID to install the library for.</param>
|
||||
/// <param name="version">The Chromium revision of the Pdfium build, e.g., 7763.</param>
|
||||
/// <param name="offline">True, when the build must not download anything and uses the library already present.</param>
|
||||
public static async Task InstallAsync(RID rid, string version, bool offline)
|
||||
{
|
||||
Console.Write($"- Installing Pdfium {version} for {rid.UserFriendlyName} ...");
|
||||
|
||||
var cwd = Environment.GetRustRuntimeDirectory();
|
||||
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
|
||||
var library = GetLibraryPath(rid);
|
||||
var pdfiumUrl = GetPdfiumDownloadUrl(rid, version);
|
||||
if (string.IsNullOrWhiteSpace(library.Path) || string.IsNullOrWhiteSpace(pdfiumUrl))
|
||||
{
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: There is no Pdfium library for {rid.UserFriendlyName}.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
var cwd = Environment.GetRustRuntimeDirectory();
|
||||
var pdfiumLibTargetPath = Path.Join(cwd, "resources", "libraries", library.Filename);
|
||||
var versionMarkerPath = Path.Join(cwd, "target", "pdfium.version");
|
||||
var versionMarker = $"{version} {rid.MicrosoftRid}";
|
||||
|
||||
if (offline)
|
||||
{
|
||||
@@ -29,45 +66,90 @@ public static class Pdfium
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($" failed because offline mode is enabled and '{pdfiumLibTargetPath}' does not exist");
|
||||
return;
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: Offline mode is enabled, but '{pdfiumLibTargetPath}' does not exist. Build once without --offline to download it.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(library.Path))
|
||||
if (File.Exists(pdfiumLibTargetPath) && await ReadVersionMarkerAsync(versionMarkerPath) == versionMarker)
|
||||
{
|
||||
Console.WriteLine($" failed to find the library path for {rid.UserFriendlyName}");
|
||||
Console.WriteLine(" already installed, skipping download");
|
||||
return;
|
||||
}
|
||||
|
||||
var pdfiumLibTargetDirectory = Path.Join(cwd, "resources", "libraries");
|
||||
var pdfiumLibTmpTargetPath = Path.Join(pdfiumLibTargetDirectory, $"{library.Filename}.{Guid.NewGuid():N}.tmp");
|
||||
var pdfiumLibArchivePath = library.Path.Replace('\\', '/');
|
||||
|
||||
//
|
||||
// Download the file:
|
||||
//
|
||||
Console.Write(" downloading ...");
|
||||
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
for (var attempt = 1; attempt <= MAX_DOWNLOAD_ATTEMPTS; attempt++)
|
||||
{
|
||||
Console.WriteLine($" failed to download Pdfium {version} for {rid.UserFriendlyName} from {pdfiumUrl}");
|
||||
return;
|
||||
try
|
||||
{
|
||||
await DownloadAndDeployAsync(pdfiumUrl, library.Path.Replace('\\', '/'), pdfiumLibTargetPath, versionMarkerPath, versionMarker);
|
||||
Console.WriteLine(" done.");
|
||||
return;
|
||||
}
|
||||
catch (Exception exception) when (IsTransientFailure(exception))
|
||||
{
|
||||
Console.Write($" failed: {DescribeFailure(exception)}");
|
||||
if (attempt < MAX_DOWNLOAD_ATTEMPTS)
|
||||
{
|
||||
Console.Write($" Retrying in {RETRY_DELAY.TotalSeconds:0} seconds ...");
|
||||
await Task.Delay(RETRY_DELAY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Extract the library from the downloaded file:
|
||||
//
|
||||
Console.Write(" extracting ...");
|
||||
Directory.CreateDirectory(pdfiumLibTargetDirectory);
|
||||
Console.WriteLine();
|
||||
Console.WriteLine($"- Error: Could not download Pdfium {version} for {rid.UserFriendlyName} from {pdfiumUrl} in {MAX_DOWNLOAD_ATTEMPTS} attempts.");
|
||||
if (File.Exists(pdfiumLibTargetPath))
|
||||
Console.WriteLine(" To build with the library which is already present, use --offline.");
|
||||
|
||||
var foundLibrary = false;
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Downloads the Pdfium archive once, extracts the library from it, and puts the library in place.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A failure which the next attempt might not run into, such as a broken connection or a timeout, leaves this
|
||||
/// method as an exception, which IsTransientFailure recognizes. A failure which every attempt would run into,
|
||||
/// such as an unknown version, ends the build right here.
|
||||
/// </remarks>
|
||||
/// <param name="pdfiumUrl">The address of the Pdfium archive.</param>
|
||||
/// <param name="pdfiumLibArchivePath">The path of the library inside the archive, with forward slashes.</param>
|
||||
/// <param name="pdfiumLibTargetPath">Where to put the library.</param>
|
||||
/// <param name="versionMarkerPath">Where to record which version the library has.</param>
|
||||
/// <param name="versionMarker">The version and RID of the library.</param>
|
||||
private static async Task DownloadAndDeployAsync(string pdfiumUrl, string pdfiumLibArchivePath, string pdfiumLibTargetPath, string versionMarkerPath, string versionMarker)
|
||||
{
|
||||
var pdfiumLibTmpTargetPath = $"{pdfiumLibTargetPath}.{Guid.NewGuid():N}.tmp";
|
||||
var receivedResponse = false;
|
||||
using var attemptTimeout = new CancellationTokenSource(RESPONSE_TIMEOUT);
|
||||
try
|
||||
{
|
||||
await using var downloadStream = await response.Content.ReadAsStreamAsync();
|
||||
//
|
||||
// Download the file:
|
||||
//
|
||||
Console.Write(" downloading ...");
|
||||
using var response = await CLIENT.GetAsync(pdfiumUrl, HttpCompletionOption.ResponseHeadersRead, attemptTimeout.Token);
|
||||
receivedResponse = true;
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: The download of {pdfiumUrl} failed with {(int)response.StatusCode} {response.ReasonPhrase}. Check the Pdfium version in metadata.txt.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
//
|
||||
// Extract the library from the downloaded file:
|
||||
//
|
||||
attemptTimeout.CancelAfter(TRANSFER_TIMEOUT);
|
||||
Console.Write(" extracting ...");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(pdfiumLibTargetPath)!);
|
||||
|
||||
var foundLibrary = false;
|
||||
await using var downloadStream = await response.Content.ReadAsStreamAsync(attemptTimeout.Token);
|
||||
await using var uncompressedStream = new GZipStream(downloadStream, CompressionMode.Decompress);
|
||||
await using var tarReader = new TarReader(uncompressedStream);
|
||||
|
||||
while (await tarReader.GetNextEntryAsync() is { } entry)
|
||||
while (await tarReader.GetNextEntryAsync(cancellationToken: attemptTimeout.Token) is { } entry)
|
||||
{
|
||||
if (!string.Equals(entry.Name.Replace('\\', '/'), pdfiumLibArchivePath, StringComparison.Ordinal))
|
||||
continue;
|
||||
@@ -76,35 +158,85 @@ public static class Pdfium
|
||||
break;
|
||||
|
||||
await using var fileStream = File.Create(pdfiumLibTmpTargetPath);
|
||||
await entry.DataStream.CopyToAsync(fileStream);
|
||||
await entry.DataStream.CopyToAsync(fileStream, attemptTimeout.Token);
|
||||
foundLibrary = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!foundLibrary)
|
||||
{
|
||||
Console.WriteLine($" failed to find the library file '{pdfiumLibArchivePath}' in the Pdfium archive");
|
||||
return;
|
||||
Console.WriteLine(" failed.");
|
||||
Console.WriteLine($"- Error: The Pdfium archive {pdfiumUrl} does not contain the library file '{pdfiumLibArchivePath}'.");
|
||||
throw new CommandExitedException(1);
|
||||
}
|
||||
|
||||
//
|
||||
// Deploy the library. The marker goes first, so that it never describes a library
|
||||
// which was only half replaced:
|
||||
//
|
||||
Console.Write(" deploying ...");
|
||||
File.Delete(versionMarkerPath);
|
||||
File.Move(pdfiumLibTmpTargetPath, pdfiumLibTargetPath, true);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(versionMarkerPath)!);
|
||||
await File.WriteAllTextAsync(versionMarkerPath, versionMarker, Environment.UTF8_NO_BOM);
|
||||
}
|
||||
catch (OperationCanceledException) when (attemptTimeout.IsCancellationRequested)
|
||||
{
|
||||
throw new TimeoutException(receivedResponse
|
||||
? $"The download did not finish within {TRANSFER_TIMEOUT.TotalMinutes:0} minutes."
|
||||
: $"There was no answer within {RESPONSE_TIMEOUT.TotalSeconds:0} seconds.");
|
||||
}
|
||||
catch (OperationCanceledException exception) when (exception.GetBaseException() is TimeoutException)
|
||||
{
|
||||
// The handler reports its connect timeout without the time it waited:
|
||||
throw new TimeoutException($"No connection could be established within {CONNECT_TIMEOUT.TotalSeconds:0} seconds.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(pdfiumLibTmpTargetPath))
|
||||
File.Delete(pdfiumLibTmpTargetPath);
|
||||
}
|
||||
|
||||
Console.WriteLine(" done.");
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether another attempt may succeed where a download attempt failed with the given exception.
|
||||
/// </summary>
|
||||
/// <param name="exception">The exception the attempt failed with.</param>
|
||||
/// <returns>True, when the download is worth another attempt.</returns>
|
||||
private static bool IsTransientFailure(Exception exception) => exception is HttpRequestException or OperationCanceledException or TimeoutException or IOException or InvalidDataException;
|
||||
|
||||
/// <summary>
|
||||
/// Describes why a download attempt failed, in one line.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The innermost exception names the actual cause, such as the socket error behind an HttpRequestException
|
||||
/// or the timeout behind a canceled connection attempt, while the outer one often names the host.
|
||||
/// </remarks>
|
||||
/// <param name="exception">The exception the attempt failed with.</param>
|
||||
/// <returns>The reason of the failure.</returns>
|
||||
private static string DescribeFailure(Exception exception)
|
||||
{
|
||||
var innermost = exception.GetBaseException();
|
||||
if (innermost == exception || exception is OperationCanceledException)
|
||||
return innermost.Message;
|
||||
|
||||
return $"{exception.Message} {innermost.Message}";
|
||||
}
|
||||
|
||||
private static async Task<string> ReadVersionMarkerAsync(string versionMarkerPath)
|
||||
{
|
||||
if (!File.Exists(versionMarkerPath))
|
||||
return string.Empty;
|
||||
|
||||
return (await File.ReadAllTextAsync(versionMarkerPath, Encoding.UTF8)).Trim();
|
||||
}
|
||||
|
||||
private static Library GetLibraryPath(RID rid) => rid switch
|
||||
{
|
||||
RID.LINUX_ARM64 or RID.LINUX_X64 => new(Path.Join("lib", "libpdfium.so"), "libpdfium.so"),
|
||||
RID.OSX_ARM64 or RID.OSX_X64 => new(Path.Join("lib", "libpdfium.dylib"), "libpdfium.dylib"),
|
||||
RID.WIN_ARM64 or RID.WIN_X64 => new(Path.Join("bin", "pdfium.dll"), "pdfium.dll"),
|
||||
|
||||
|
||||
_ => new(string.Empty, string.Empty),
|
||||
};
|
||||
|
||||
@@ -115,13 +247,13 @@ public static class Pdfium
|
||||
{
|
||||
RID.LINUX_ARM64 => $"{baseUrl}linux-arm64.tgz",
|
||||
RID.LINUX_X64 => $"{baseUrl}linux-x64.tgz",
|
||||
|
||||
|
||||
RID.OSX_ARM64 => $"{baseUrl}mac-arm64.tgz",
|
||||
RID.OSX_X64 => $"{baseUrl}mac-x64.tgz",
|
||||
|
||||
|
||||
RID.WIN_ARM64 => $"{baseUrl}win-arm64.tgz",
|
||||
RID.WIN_X64 => $"{baseUrl}win-x64.tgz",
|
||||
|
||||
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user