Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 46 additions & 62 deletions sdks/dotnet/BinaryInstaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.IO.Compression;
using SharpCompress.Archives;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Readers;
using SharpCompress.Common;
using System.Reflection;

namespace TestServerSdk
{
Expand All @@ -37,17 +35,35 @@ public static class BinaryInstaller
private const string ProjectName = "test-server";

/// <summary>
/// Ensures the test-server binary for the given version is present under <repo>/sdks/dotnet/bin.
/// It will download the release asset from GitHub, verify SHA256 using the checksums.json found in the repo, extract it and set executable bits.
/// Ensures the test-server binary for the given version is present in the specified output directory.
/// It will download the release asset from GitHub, verify its SHA256 checksum, extract it, and set executable permissions.
/// The checksums are read from a 'checksums.json' file expected to be embeded into the TestServerSdk.dll.
/// </summary>
public static async Task EnsureBinaryAsync(string outDir, string version = "v0.2.6")
{
var embeddedCandidate = Path.Combine(AppContext.BaseDirectory, "checksums.json");
var repoChecksumsPath = File.Exists(embeddedCandidate)
? embeddedCandidate
: FindChecksumsJson() ?? throw new FileNotFoundException("Could not locate sdks/typescript/checksums.json in repository parents or embedded in output. Please run this command from within the repo or provide checksums.json manually.");
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "TestServerSdk.checksums.json";
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to make checksums.json an asset file for the runtime, but it brings in lots of brittles in the directory findings. And directory structure may have some subtlety per operating system as well. After many rounds of testing, I decided to embed the checksums.json into the TestServerSdk.dll assembly is the best practice to reduce runtime corruption.

string checksumsJson;

Console.WriteLine($"[SDK] Attempting to read embedded resource: '{resourceName}'");

using var doc = JsonDocument.Parse(File.ReadAllText(repoChecksumsPath));
using (var stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
var availableResources = string.Join(", ", assembly.GetManifestResourceNames());
throw new FileNotFoundException(
$"Could not find the embedded resource '{resourceName}'. " +
$"This is a packaging error. Available resources: [{availableResources}]");
}
using (var reader = new StreamReader(stream))
{
checksumsJson = reader.ReadToEnd();
}
}
Console.WriteLine($"[SDK] Found and read embedded checksums file successfully.");

using var doc = JsonDocument.Parse(checksumsJson);
var versionNode = doc.RootElement.TryGetProperty(version, out var vNode)
? vNode
: throw new InvalidOperationException($"Checksums.json does not contain an entry for version {version}.");
Expand All @@ -65,10 +81,12 @@ public static async Task EnsureBinaryAsync(string outDir, string version = "v0.2

var binDir = Path.GetFullPath(outDir);
Directory.CreateDirectory(binDir);
var finalBinaryPath = Path.Combine(binDir, platform == "win32" ? ProjectName + ".exe" : ProjectName);
var binaryName = platform == "win32" ? ProjectName + ".exe" : ProjectName;
var finalBinaryPath = Path.Combine(binDir, binaryName);

if (File.Exists(finalBinaryPath))
{
Console.WriteLine($"{ProjectName} binary already exists at {finalBinaryPath}. Skipping download.");
Console.WriteLine($"[SDK] {ProjectName} binary already exists at {finalBinaryPath}. Skipping download.");
EnsureExecutable(finalBinaryPath);
return;
}
Expand All @@ -82,50 +100,22 @@ public static async Task EnsureBinaryAsync(string outDir, string version = "v0.2
var actualChecksum = await ComputeSha256Async(archivePath);
if (!string.Equals(actualChecksum, expectedChecksum, StringComparison.OrdinalIgnoreCase))
{
File.Delete(archivePath);
throw new InvalidOperationException($"Checksum mismatch for {archiveName}. Expected: {expectedChecksum}, Actual: {actualChecksum}");
}

ExtractArchive(archivePath, archiveExt, binDir);
EnsureExecutable(finalBinaryPath);

Console.WriteLine($"{ProjectName} ready at {finalBinaryPath}");
Console.WriteLine($"[SDK] {ProjectName} ready at {finalBinaryPath}");
}
catch
finally
{
// Ensure the downloaded archive is cleaned up even if something goes wrong.
if (File.Exists(archivePath))
{
try { File.Delete(archivePath); } catch { }
try { File.Delete(archivePath); } catch { /* Best effort */ }
}
throw;
}
}

private static string RepoRootPathFrom(string checksumsPath)
{
// checksumsPath is expected to be <repo>/sdks/dotnet/checksums.json
return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(checksumsPath) ?? string.Empty, "..", ".."));
}

private static string? FindChecksumsJson()
{
// Start from AppContext.BaseDirectory and walk up to find sdks/dotnet/checksums.json
var dir = new DirectoryInfo(AppContext.BaseDirectory);
for (int i = 0; i < 8 && dir != null; i++)
{
var candidate = Path.Combine(dir.FullName, "sdks", "dotnet", "checksums.json");
if (File.Exists(candidate)) return candidate;
dir = dir.Parent;
}
// Also try the current working directory
dir = new DirectoryInfo(Directory.GetCurrentDirectory());
for (int i = 0; i < 4 && dir != null; i++)
{
var candidate = Path.Combine(dir.FullName, "sdks", "dotnet", "checksums.json");
if (File.Exists(candidate)) return candidate;
dir = dir.Parent;
}
return null;
}

private static (string goOs, string archPart, string archiveExt, string platform) GetPlatformDetails()
Expand All @@ -149,14 +139,14 @@ private static (string goOs, string archPart, string archiveExt, string platform

private static async Task DownloadFileAsync(string url, string destinationPath)
{
Console.WriteLine($"Downloading {url} -> {destinationPath}...");
Console.WriteLine($"[TestServerSDK] Downloading {url} -> {destinationPath}...");
using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
using var resp = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
resp.EnsureSuccessStatusCode();
using var stream = await resp.Content.ReadAsStreamAsync();
using var fs = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None);
await stream.CopyToAsync(fs);
Console.WriteLine("Download complete.");
Console.WriteLine("[TestServerSDK] Download complete.");
}

private static async Task<string> ComputeSha256Async(string filePath)
Expand All @@ -169,27 +159,22 @@ private static async Task<string> ComputeSha256Async(string filePath)

private static void ExtractArchive(string archivePath, string archiveExt, string destDir)
{
Console.WriteLine($"Extracting {archivePath} to {destDir}...");
Console.WriteLine($"[TestServerSDK] Extracting {archivePath} to {destDir}...");
if (archiveExt == ".zip")
{
ZipFile.ExtractToDirectory(archivePath, destDir);
System.IO.Compression.ZipFile.ExtractToDirectory(archivePath, destDir, true);
}
else
{
using var fileStream = File.OpenRead(archivePath);
using var reader = ReaderFactory.Open(fileStream);
while (reader.MoveToNextEntry())
{
var entry = reader.Entry;
if (entry.IsDirectory) continue;
var outPath = Path.Combine(destDir, entry.Key);
var outDir = Path.GetDirectoryName(outPath);
if (!string.IsNullOrEmpty(outDir)) Directory.CreateDirectory(outDir);
if (reader.Entry.IsDirectory) continue;
reader.WriteEntryToDirectory(destDir, new ExtractionOptions { ExtractFullPath = true, Overwrite = true });
}
}
File.Delete(archivePath);
Console.WriteLine("Extraction complete.");
Console.WriteLine("[TestServerSDK] Extraction complete.");
}

private static void EnsureExecutable(string binaryPath)
Expand All @@ -200,26 +185,25 @@ private static void EnsureExecutable(string binaryPath)
var psi = new ProcessStartInfo
{
FileName = "chmod",
Arguments = $"755 {QuotePath(binaryPath)}",
Arguments = $"+x {QuotePath(binaryPath)}",
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false
};
using var p = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start 'chmod' process to set executable permissions");
using var p = Process.Start(psi) ?? throw new InvalidOperationException("Failed to start 'chmod' process.");
p.WaitForExit();
if (p.ExitCode != 0)
{
var err = p.StandardError.ReadToEnd();
Console.WriteLine($"chmod failed: {err}");
Console.WriteLine($"[TestServerSDK WARNING] chmod failed: {p.StandardError.ReadToEnd()}");
}
else
{
Console.WriteLine($"Set executable permissions on {binaryPath}");
Console.WriteLine($"[TestServerSDK] Set executable permissions on {binaryPath}");
}
}
catch (Exception ex)
{
Console.WriteLine($"Could not set executable permission: {ex.Message}");
Console.WriteLine($"[TestServerSDK WARNING] Could not set executable permission: {ex.Message}");
}
}

Expand Down
Loading
Loading