diff --git a/.gitignore b/.gitignore index 09a7fd6..3831da5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,21 @@ +# Ignore build outputs +**/bin/ +**/obj/ +*.nupkg + +# macOS +.DS_Store + +# Visual Studio / Rider +.vs/ +*.user +*.suo + +# VS Code +.vscode/ + +# Solution files that should not be committed from local experimentation +test-server.sln *.swp test-server /recordings diff --git a/sdks/dotnet/BinaryInstaller.cs b/sdks/dotnet/BinaryInstaller.cs new file mode 100644 index 0000000..33f9c65 --- /dev/null +++ b/sdks/dotnet/BinaryInstaller.cs @@ -0,0 +1,228 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.IO; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text.Json; +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; + +namespace TestServerSdk +{ + public static class BinaryInstaller + { + private const string GithubOwner = "google"; + private const string GithubRepo = "test-server"; + private const string ProjectName = "test-server"; + + /// + /// Ensures the test-server binary for the given version is present under /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. + /// + 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."); + + using var doc = JsonDocument.Parse(File.ReadAllText(repoChecksumsPath)); + var versionNode = doc.RootElement.TryGetProperty(version, out var vNode) + ? vNode + : throw new InvalidOperationException($"Checksums.json does not contain an entry for version {version}."); + + var (goOs, archPart, archiveExt, platform) = GetPlatformDetails(); + var archiveName = $"{ProjectName}_{goOs}_{archPart}{archiveExt}"; + + var expectedChecksumNode = versionNode.TryGetProperty(archiveName, out var cNode) + ? cNode + : throw new InvalidOperationException($"Checksums.json for {version} does not contain an entry for {archiveName}."); + + var expectedChecksum = expectedChecksumNode.GetString(); + if (string.IsNullOrEmpty(expectedChecksum) || expectedChecksum.StartsWith("PLEASE_RUN_UPDATE_SCRIPT")) + throw new InvalidOperationException($"Checksum for {archiveName} in {version} looks invalid or is a placeholder."); + + var binDir = Path.GetFullPath(outDir); + Directory.CreateDirectory(binDir); + var finalBinaryPath = Path.Combine(binDir, platform == "win32" ? ProjectName + ".exe" : ProjectName); + if (File.Exists(finalBinaryPath)) + { + Console.WriteLine($"{ProjectName} binary already exists at {finalBinaryPath}. Skipping download."); + EnsureExecutable(finalBinaryPath); + return; + } + + var downloadUrl = $"https://github.com/{GithubOwner}/{GithubRepo}/releases/download/{version}/{archiveName}"; + var archivePath = Path.Combine(binDir, archiveName); + + try + { + await DownloadFileAsync(downloadUrl, archivePath); + 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}"); + } + catch + { + if (File.Exists(archivePath)) + { + try { File.Delete(archivePath); } catch { } + } + throw; + } + } + + private static string RepoRootPathFrom(string checksumsPath) + { + // checksumsPath is expected to be /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() + { + string platform; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) platform = "darwin"; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) platform = "linux"; + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) platform = "win32"; + else throw new PlatformNotSupportedException("Unsupported OS platform"); + + var arch = RuntimeInformation.ProcessArchitecture; + string archPart; + if (arch == Architecture.X64) archPart = "x86_64"; + else if (arch == Architecture.Arm64) archPart = "arm64"; + else throw new PlatformNotSupportedException("Unsupported architecture"); + + string goOs = platform == "darwin" ? "Darwin" : platform == "linux" ? "Linux" : "Windows"; + string archiveExt = platform == "win32" ? ".zip" : ".tar.gz"; + return (goOs, archPart, archiveExt, platform); + } + + private static async Task DownloadFileAsync(string url, string destinationPath) + { + Console.WriteLine($"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."); + } + + private static async Task ComputeSha256Async(string filePath) + { + using var stream = File.OpenRead(filePath); + using var sha = SHA256.Create(); + var hash = await sha.ComputeHashAsync(stream); + return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant(); + } + + private static void ExtractArchive(string archivePath, string archiveExt, string destDir) + { + Console.WriteLine($"Extracting {archivePath} to {destDir}..."); + if (archiveExt == ".zip") + { + ZipFile.ExtractToDirectory(archivePath, destDir); + } + 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); + reader.WriteEntryToDirectory(destDir, new ExtractionOptions { ExtractFullPath = true, Overwrite = true }); + } + } + File.Delete(archivePath); + Console.WriteLine("Extraction complete."); + } + + private static void EnsureExecutable(string binaryPath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return; + try + { + var psi = new ProcessStartInfo + { + FileName = "chmod", + Arguments = $"755 {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"); + p.WaitForExit(); + if (p.ExitCode != 0) + { + var err = p.StandardError.ReadToEnd(); + Console.WriteLine($"chmod failed: {err}"); + } + else + { + Console.WriteLine($"Set executable permissions on {binaryPath}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Could not set executable permission: {ex.Message}"); + } + } + + private static string QuotePath(string p) => p.Contains(' ') ? '"' + p + '"' : p; + } +} diff --git a/sdks/dotnet/README.md b/sdks/dotnet/README.md new file mode 100644 index 0000000..6e7d8d7 --- /dev/null +++ b/sdks/dotnet/README.md @@ -0,0 +1,16 @@ +This folder contains the .NET SDK for `test-server`. It provides a small runtime wrapper to start/stop the `test-server` binary and a helper installer that downloads and verifies the native binary. During test runtime, the SDK first checks if the `test-server` binary is already downloaded and verified, otherwise it downloads and verifies it. + +## Example of setting TestServerOptions + +```csharp +using TestServerSdk; + +var binaryPathDir = "dir/you/want/the/binary/to/be/downloaded"; +var options = new TestServerOptions +{ + ConfigPath = Path.GetFullPath("../test-server.yml"), + RecordingDir = Path.GetFullPath("../Recordings"), + Mode = "replay", + BinaryPath = Path.GetFullPath(Path.Combine(binaryPathDir, "test-server")) +}; +``` diff --git a/sdks/dotnet/TestServerSdk.cs b/sdks/dotnet/TestServerSdk.cs new file mode 100644 index 0000000..41cdde3 --- /dev/null +++ b/sdks/dotnet/TestServerSdk.cs @@ -0,0 +1,153 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Text.Json; +using YamlDotNet.RepresentationModel; +using System.Net.Http; + +namespace TestServerSdk +{ + public class TestServerOptions + { + public required string ConfigPath { get; set; } + public required string RecordingDir { get; set; } + public required string Mode { get; set; } // "record" or "replay" + public required string BinaryPath { get; set; } + + public Action? OnStdOut { get; set; } + public Action? OnStdErr { get; set; } + public Action? OnExit { get; set; } + public Action? OnError { get; set; } + } + + public class TestServerProcess + { + private Process? _process; + private readonly TestServerOptions _options; + private readonly string _binaryPath; + + public TestServerProcess(TestServerOptions options) + { + _options = options; + _binaryPath = GetBinaryPath(); + } + + private string GetBinaryPath() + { + var binaryName = Environment.OSVersion.Platform == PlatformID.Win32NT ? "test-server.exe" : "test-server"; + + var p = Path.GetFullPath(_options.BinaryPath); + if (File.Exists(p)) return p; + + // If the binary does not exist at the provided path, attempt to install it into that folder + try + { + var targetDir = Path.GetDirectoryName(p) ?? Path.GetFullPath(Directory.GetCurrentDirectory()); + Console.WriteLine($"[TestServerSdk] test-server not found at {p}. Installing into {targetDir}..."); + BinaryInstaller.EnsureBinaryAsync(targetDir, "v0.2.6").GetAwaiter().GetResult(); + if (File.Exists(p)) return p; + throw new FileNotFoundException($"After installation, test-server binary still not found at: {p}"); + } + catch (Exception ex) + { + throw new FileNotFoundException($"TestServerOptions.BinaryPath was set but file not found and installer failed: {p}", ex); + } + } + + public async Task StartAsync() + { + var args = $"{_options.Mode} --config {_options.ConfigPath} --recording-dir {_options.RecordingDir}"; + var psi = new ProcessStartInfo + { + FileName = _binaryPath, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + _process = new Process { StartInfo = psi, EnableRaisingEvents = true }; + _process.OutputDataReceived += (s, e) => { if (e.Data != null) _options.OnStdOut?.Invoke(e.Data); }; + _process.ErrorDataReceived += (s, e) => { if (e.Data != null) _options.OnStdErr?.Invoke(e.Data); }; + _process.Exited += (s, e) => _options.OnExit?.Invoke(_process?.ExitCode, _process?.ExitCode.ToString() ?? string.Empty); + try + { + _process.Start(); + _process.BeginOutputReadLine(); + _process.BeginErrorReadLine(); + await AwaitHealthyTestServer(); + return _process; + } + catch (Exception ex) + { + _options.OnError?.Invoke(ex); + throw; + } + } + + public async Task StopAsync() + { + if (_process == null || _process.HasExited) + return; + _process.Kill(); + await Task.Run(() => _process.WaitForExit(5000)); + } + + private async Task AwaitHealthyTestServer() + { + var yaml = File.ReadAllText(_options.ConfigPath); + var input = new StringReader(yaml); + var yamlStream = new YamlStream(); + yamlStream.Load(input); + var root = (YamlMappingNode)yamlStream.Documents[0].RootNode; + if (!root.Children.ContainsKey(new YamlScalarNode("endpoints"))) return; + var endpoints = (YamlSequenceNode)root.Children[new YamlScalarNode("endpoints")]; + foreach (YamlMappingNode endpoint in endpoints) + { + if (!endpoint.Children.ContainsKey(new YamlScalarNode("health"))) continue; + var sourceType = endpoint.Children[new YamlScalarNode("source_type")].ToString(); + var sourcePort = endpoint.Children[new YamlScalarNode("source_port")].ToString(); + var healthPath = endpoint.Children[new YamlScalarNode("health")].ToString(); + var url = $"{sourceType}://localhost:{sourcePort}{healthPath}"; + await HealthCheck(url); + } + } + + private async Task HealthCheck(string url) + { + using var client = new HttpClient(); + const int maxRetries = 10; + int delay = 100; + for (int i = 0; i < maxRetries; i++) + { + try + { + var response = await client.GetAsync(url); + if (response.IsSuccessStatusCode) return; + } + catch { } + await Task.Delay(delay); + delay *= 2; + } + throw new Exception($"Health check failed for {url}"); + } + } +} diff --git a/sdks/dotnet/TestServerSdk.csproj b/sdks/dotnet/TestServerSdk.csproj new file mode 100644 index 0000000..c30cf49 --- /dev/null +++ b/sdks/dotnet/TestServerSdk.csproj @@ -0,0 +1,22 @@ + + + net8.0 + latest + enable + enable + true + 0.1.0 + false + false + Library + + + + + + + + PreserveNewest + + + diff --git a/sdks/dotnet/checksums.json b/sdks/dotnet/checksums.json new file mode 100644 index 0000000..d00da43 --- /dev/null +++ b/sdks/dotnet/checksums.json @@ -0,0 +1,82 @@ +{ + "v0.0.1": { + "test-server_Darwin_arm64.tar.gz": "8d5ff282451b8d49fa6f290ec11d4ee9fcc948f28ae2713f928f4e3eeaf35d2e", + "test-server_Darwin_x86_64.tar.gz": "793b97d96d1a4a65fdef4510266047725aca6a7f39be3958f039b5f5d377c5f3", + "test-server_Linux_arm64.tar.gz": "b77c68d7549eb8f1ba0569434f11236cb08222bead8235bef2f6dc194eff4318", + "test-server_Linux_i386.tar.gz": "e9ec96227854a9def19cd112dfc22bfc776a6595314fb104b80f9d74d27a8ba2", + "test-server_Linux_x86_64.tar.gz": "35a157c5c9fbf2639ac8f8282f45186397ebd428b31808780421e2fa34923866", + "test-server_Windows_arm64.zip": "bf708e74aa1e6fd15529031c9a8f7d75b8fcc35cb7ef24c8c81a5b3e1ba2fca4", + "test-server_Windows_i386.zip": "13c5a1cde66b2795cb49b02dc672da66bbc2f934c67f733b7313ad4c10c68c96", + "test-server_Windows_x86_64.zip": "6105a98d7b245a3b8868c173d2e36c0e2a41c9e88a0e266b0f318b21f96a313f" + }, + "v0.2.0": { + "test-server_Darwin_arm64.tar.gz": "87a63147c318e012e5963fd4ae706aede56267db1913272baeeafe4b9aef95c0", + "test-server_Darwin_x86_64.tar.gz": "79dee942fd1673d4000f99742464cb7cf238b773523a0da294da9017f30e43c4", + "test-server_Linux_arm64.tar.gz": "50b3667ee7c7543b08decff81936654cb878a8ad84d1ed2f9d4f40108f027be1", + "test-server_Linux_i386.tar.gz": "9599bf857fac1594ef38ed44193f8da7374ac2d1e82e5aa169d474b6ffece04b", + "test-server_Linux_x86_64.tar.gz": "84b5b2ef12e002461fa9961d689d415fec80780231d8dd107c6eb40bbc327759", + "test-server_Windows_arm64.zip": "d00178c9bc523ee9c37576efff94f1ba09c4b30e375636b603e9b46ca5be5a25", + "test-server_Windows_i386.zip": "1beff64cd68fffa7bd4936d6a2df8641cc371b0822a9ef647e3ab15710ced045", + "test-server_Windows_x86_64.zip": "dfa622a481a8abad115a177e12fd5bfc7fc0270cb37618511ba183b34ad6f0d1" + }, + "v0.2.1": { + "test-server_Darwin_arm64.tar.gz": "3bd64892e9943e65e2bd769b15a212f6d54021ff526ef42c0e4f8dc13be25eb9", + "test-server_Darwin_x86_64.tar.gz": "35941ef52f8c2fd3ac49b1128f964b81e04ceedb5e1f359cd51ece7dde15ff98", + "test-server_Linux_arm64.tar.gz": "e284be5cdc497db55ea09471e6dfcd3768b40d3f0eff915794b04386a6d0f18e", + "test-server_Linux_i386.tar.gz": "12cb4f8167baca5965b90cf4d2157bff78380f1f7853ccf45b87e26abd63f52d", + "test-server_Linux_x86_64.tar.gz": "5dab0a8041cfee8801a91ded6a98a682a3952649d35e6a05c3edfb2c32383c7b", + "test-server_Windows_arm64.zip": "884e84dc43491ecbfbb2c03f6d98ec8d247a4eb7c80a3cac61849c1ccbcfc92c", + "test-server_Windows_i386.zip": "51980525c42121674aef6953eddb0579d4897576c06ae411064ad92e828a45e7", + "test-server_Windows_x86_64.zip": "e368ac54ec00443ddcde0c63ee806b7b865be403388b69f256992ac49acad7e1" + }, + "v0.2.2": { + "test-server_Darwin_arm64.tar.gz": "1e568d5447597dd06f535806a5e52fe2063c7f9b57edf3b590699a9bd0675b8d", + "test-server_Darwin_x86_64.tar.gz": "a6e3127cf5622332c4b4200957ce5ddab2ff84e91b4ff984514071a716877852", + "test-server_Linux_arm64.tar.gz": "87789743585853dddca65a88aaaccd7463fc2b714671438f0f711dac1cea8ea4", + "test-server_Linux_i386.tar.gz": "0482509d6dcd80be203988aadf3e5421e2116e43b33971c8540148de80bfa0da", + "test-server_Linux_x86_64.tar.gz": "89798849206ae210309cad36b3275c333a19d12d45941327384279be17dca07d", + "test-server_Windows_arm64.zip": "be8500c4577da4930397ecfebd626b2a090ab045975e594550c16f087ee343b7", + "test-server_Windows_i386.zip": "345f894e0e789442802a66806623f7a28b95cafac6d10ac5d7aa44084fb73bc5", + "test-server_Windows_x86_64.zip": "8d64f303463a697550903bb3587f63e8a37efa96b9eea1929d5cbd825f2840e4" + }, + "v0.2.3": { + "test-server_Darwin_arm64.tar.gz": "e7ed97903e1850755321da023838bc27c30bf44365d4c347d0405ad2db80d901", + "test-server_Darwin_x86_64.tar.gz": "33af03f84b644efb7113371433a78bc35cf406fc909eac1f33f6003fec8afd38", + "test-server_Linux_arm64.tar.gz": "c1355f56d5c8480c71ad7c8c4e01160cd9b60e977af3bc15ae6599ae04958cd1", + "test-server_Linux_i386.tar.gz": "50619693d9b6a27a05d72a6af7d364333f67aa9b5c67428c85e0e8dbadd44dd3", + "test-server_Linux_x86_64.tar.gz": "7af3c0502b5c242565cb494a50a50188d38c342e08523f8168198ebb73506062", + "test-server_Windows_arm64.zip": "dc5cc3b28404fec303b5afc31da45de24cb69ce36a74590985b2b054d7cf78b9", + "test-server_Windows_i386.zip": "b42b75ae4d538aa1df3893612458c449ad6c132cae99feb9deaac075b04bd1dd", + "test-server_Windows_x86_64.zip": "22b7d25b7ad3bb3b586a6fba2996420f67a795131b9be3a06ccffe92cfd3f234" + }, + "v0.2.4": { + "test-server_Darwin_arm64.tar.gz": "a80eca2362245ceb0f0b60cbc7121dc2bb35c0d95224051f7c5bb815f9cb3bb7", + "test-server_Darwin_x86_64.tar.gz": "4c55c667b1419ec09aef536cdf753d20ddb29af29199a099273ffed732e2b4f1", + "test-server_Linux_arm64.tar.gz": "c0a2a6b74a29dc6e2b4d17079872f0e64d9a3d392dc35d4ac8a6b02ba2b5278d", + "test-server_Linux_i386.tar.gz": "c20dbbbb89d00dcbd15ded3077d270111cf59118beff45e68114ff8f0bb3e79c", + "test-server_Linux_x86_64.tar.gz": "acddf79900182c4e7a0dfb03562f0dd24bfde922d50ec5f767cb234942b204fa", + "test-server_Windows_arm64.zip": "07bf8adc4a9c5aa353d95ce0afbf075c4c069c926ac14178ecfb283f6d072b4d", + "test-server_Windows_i386.zip": "62e5bc50e64ffa0fd0878203b351a393a990d70c75800572831d273a99b9d2b4", + "test-server_Windows_x86_64.zip": "5902ebf807667243b29af5ca1b7622aba7be5fc2d50378b35f066bea85832f8b" + }, + "v0.2.5": { + "test-server_Darwin_arm64.tar.gz": "fe652704eee0f4568a2d4f8c3a43732dc28cab4d2bd9dfc6294372f6587e4e2b", + "test-server_Darwin_x86_64.tar.gz": "cf1a4bca0297b394173deaf1515cba6382dd73cad7e53057a5f2e77d6f3c0d33", + "test-server_Linux_arm64.tar.gz": "874b3799f6669dfd278cade47c1c3ac1f40754ffc8dc296e5143381eae76bd84", + "test-server_Linux_i386.tar.gz": "5ae39f7e06e9fefd9574674aee3450d73b77c3cab3a7b81aea5688320c823978", + "test-server_Linux_x86_64.tar.gz": "716d4bde33a842f4a97a8a8c9037027bd6a314cf4b4a769ee0acd7a37ca5e171", + "test-server_Windows_arm64.zip": "9894d08f6e9c2e58991c78418e65a6f8a12712c86a4ac98b18d74783c601f6f7", + "test-server_Windows_i386.zip": "b0daa8cac3133470afa9a049ad08a283c5c48c929a139c685773dee31b20d99e", + "test-server_Windows_x86_64.zip": "88c55b9208d66516a674b79be59fed3ec0262f4f96a499628b9ea609f13e3fc8" + }, + "v0.2.6": { + "test-server_Darwin_arm64.tar.gz": "8e3f9b5a7ab4d5e398f44c0bdbe4e8f009b863b63d31dfadf00834d306c7b746", + "test-server_Darwin_x86_64.tar.gz": "4a59bb73ae6009ac92a274b4a0e6ce534b7ff90b0afb7312f8ae7e015cbcefe8", + "test-server_Linux_arm64.tar.gz": "f3273dce4bb2f492cc703fe790af37b6e0db1b258e94f770bd86493b5aa5558e", + "test-server_Linux_i386.tar.gz": "3f3878103935bf1507836360ba103bf7a5d1034fd21a285074574b22e67f58a4", + "test-server_Linux_x86_64.tar.gz": "f007c2a940dade8a1e4c08f2c954f768a351e3fa3b050dcc1753bf65e637b983", + "test-server_Windows_arm64.zip": "466137be1dad084fcdef86a8894080a2ef1086dfd3ee15bc123a6d2053515841", + "test-server_Windows_i386.zip": "6980c83e2118ed739dad53af29dc302b78ec89804f7ff7d7b5e39dcadbab3e83", + "test-server_Windows_x86_64.zip": "8a4e36c8fa2d17a256a31956a3cb2851d27a30f423449911caf0b3ec76b9a602" + } +} diff --git a/sdks/dotnet/tools/installer/Installer.csproj b/sdks/dotnet/tools/installer/Installer.csproj new file mode 100644 index 0000000..1dae1ab --- /dev/null +++ b/sdks/dotnet/tools/installer/Installer.csproj @@ -0,0 +1,12 @@ + + + Exe + net8.0 + enable + enable + false + + + + + diff --git a/sdks/dotnet/tools/installer/Program.cs b/sdks/dotnet/tools/installer/Program.cs new file mode 100644 index 0000000..77080ee --- /dev/null +++ b/sdks/dotnet/tools/installer/Program.cs @@ -0,0 +1,40 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.Threading.Tasks; +using TestServerSdk; + +namespace TestServerSdk.InstallerTool +{ + class Program + { + static async Task Main(string[] args) + { + var version = args.Length > 0 ? args[0] : "v0.2.6"; + try + { + await BinaryInstaller.EnsureBinaryAsync(version); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine($"Installer failed: {ex.Message}"); + return 2; + } + } + } +}