diff --git a/sdks/dotnet/BinaryInstaller.cs b/sdks/dotnet/BinaryInstaller.cs index 33f9c65..99e87bf 100644 --- a/sdks/dotnet/BinaryInstaller.cs +++ b/sdks/dotnet/BinaryInstaller.cs @@ -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 { @@ -37,17 +35,35 @@ public static class BinaryInstaller 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. + /// 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. /// 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"; + 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}."); @@ -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; } @@ -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 /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() @@ -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 ComputeSha256Async(string filePath) @@ -169,10 +159,10 @@ private static async Task 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 { @@ -180,16 +170,11 @@ private static void ExtractArchive(string archivePath, string archiveExt, string 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) @@ -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}"); } } diff --git a/sdks/dotnet/LICENSE b/sdks/dotnet/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/sdks/dotnet/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/sdks/dotnet/TestServerSdk.cs b/sdks/dotnet/TestServerSdk.cs index 41cdde3..c86eecc 100644 --- a/sdks/dotnet/TestServerSdk.cs +++ b/sdks/dotnet/TestServerSdk.cs @@ -64,11 +64,11 @@ private string GetBinaryPath() 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}"); + throw new FileNotFoundException($"[TestServerSdk] 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); + throw new FileNotFoundException($"[TestServerSdk] TestServerOptions.BinaryPath was set but file not found and installer failed: {p}", ex); } } diff --git a/sdks/dotnet/TestServerSdk.csproj b/sdks/dotnet/TestServerSdk.csproj index c30cf49..65596e0 100644 --- a/sdks/dotnet/TestServerSdk.csproj +++ b/sdks/dotnet/TestServerSdk.csproj @@ -4,8 +4,16 @@ latest enable enable - true - 0.1.0 + + 0.1.1 + Google LLC + A .NET SDK to manage the test-server process for integration testing. + https://github.com/google/test-server + https://github.com/google/test-server.git + git + Apache-2.0 + README.md + false false Library @@ -15,8 +23,14 @@ - - PreserveNewest - + + + + + + + + + diff --git a/sdks/dotnet/tools/installer/Installer.csproj b/sdks/dotnet/tools/installer/Installer.csproj index 1dae1ab..b24a4cc 100644 --- a/sdks/dotnet/tools/installer/Installer.csproj +++ b/sdks/dotnet/tools/installer/Installer.csproj @@ -4,9 +4,9 @@ net8.0 enable enable - false + false - + diff --git a/sdks/dotnet/tools/installer/Program.cs b/sdks/dotnet/tools/installer/Program.cs index 77080ee..88f2f65 100644 --- a/sdks/dotnet/tools/installer/Program.cs +++ b/sdks/dotnet/tools/installer/Program.cs @@ -18,23 +18,15 @@ using System.Threading.Tasks; using TestServerSdk; -namespace TestServerSdk.InstallerTool +// This program is just a thin wrapper around the installer logic in the SDK. +if (args.Length == 0) { - 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; - } - } - } + Console.WriteLine("Usage: installer [version]"); + return 1; } + +string outDir = args[0]; +string version = args.Length > 1 ? args[1] : "v0.2.6"; + +await BinaryInstaller.EnsureBinaryAsync(outDir, version); +return 0;