Conversation
- Create ISdkCheckerService interface - Implement SdkCheckerService to detect SDK installation - Get installed SDK versions and paths - Provide download URL for .Net SDK - Add comprehensive unit tests Closes #5
There was a problem hiding this comment.
Pull request overview
Adds a service for detecting whether the .NET SDK is installed, reporting installed versions/path, and exposing a recommended SDK download URL (issue #5), along with initial tests.
Changes:
- Introduced
ISdkCheckerServiceandSdkCheckerServiceto querydotnetvia CLI. - Added
SdkInfoDTO carrying install/version/path/version-list plus recommended version and download URL. - Added xUnit tests covering basic “non-null/returns” behavior for the new service.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| BannerlordModEditor.Common/Services/SdkCheckerService.cs | Implements SDK detection by invoking dotnet commands and parsing their output into SdkInfo. |
| BannerlordModEditor.Common.Tests/Services/SdkCheckerServiceTests.cs | Adds baseline tests for service methods and returned DTO fields. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public async Task<SdkInfo> CheckSdkAsync() | ||
| { | ||
| return await Task.Run(() => CheckSdk()); |
There was a problem hiding this comment.
CheckSdkAsync wraps a synchronous, blocking implementation in Task.Run, which is generally discouraged for library/service code (it consumes a thread-pool thread and doesn’t make the underlying work non-blocking). Prefer either (a) making the underlying process execution truly async (e.g., async read + WaitForExitAsync) or (b) removing the async method and keeping only CheckSdk() if true async isn’t feasible.
| public async Task<SdkInfo> CheckSdkAsync() | |
| { | |
| return await Task.Run(() => CheckSdk()); | |
| public Task<SdkInfo> CheckSdkAsync() | |
| { | |
| return Task.FromResult(CheckSdk()); |
| var output = process.StandardOutput.ReadToEnd(); | ||
| process.WaitForExit(); | ||
|
|
There was a problem hiding this comment.
This can deadlock/hang: RedirectStandardError = true but stderr is never drained. If dotnet writes enough to stderr, the child process can block, causing ReadToEnd()/WaitForExit() to hang indefinitely. Fix by either reading stderr as well (or not redirecting it), and consider using WaitForExitAsync plus async reads to avoid blocking.
| var output = process.StandardOutput.ReadToEnd(); | |
| process.WaitForExit(); | |
| var stdoutTask = process.StandardOutput.ReadToEndAsync(); | |
| var stderrTask = process.StandardError.ReadToEndAsync(); | |
| process.WaitForExit(); | |
| Task.WaitAll(stdoutTask, stderrTask); | |
| var output = stdoutTask.Result; |
| var process = new Process | ||
| { | ||
| StartInfo = new ProcessStartInfo | ||
| { | ||
| FileName = "dotnet", | ||
| Arguments = "--version", | ||
| RedirectStandardOutput = true, | ||
| UseShellExecute = false, | ||
| CreateNoWindow = true | ||
| } | ||
| }; | ||
|
|
||
| process.Start(); | ||
| var output = process.StandardOutput.ReadToEnd().Trim(); | ||
| process.WaitForExit(); |
There was a problem hiding this comment.
All process invocations use blocking ReadToEnd() + WaitForExit() without any timeout/cancellation. In environments where dotnet is slow/hung/misconfigured, this can block the calling thread (and CheckSdkAsync will still tie up a thread-pool thread). Consider adding a bounded timeout (and/or cancellation token support) and cleaning up the process if it exceeds the limit.
| foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries)) | ||
| { | ||
| var parts = line.Trim().Split(' '); | ||
| if (parts.Length > 0) | ||
| { | ||
| versions.Add(parts[0]); | ||
| } | ||
| } |
There was a problem hiding this comment.
Parsing uses Split(' ') without removing empty entries. If the --list-sdks output contains multiple spaces/tabs, parts[0] can be empty, producing empty version strings in versions. Use a split that removes empty entries (and preferably handles tabs) before taking the first token.
| public bool IsSdkInstalled() | ||
| { | ||
| try | ||
| { | ||
| var process = new Process | ||
| { | ||
| StartInfo = new ProcessStartInfo | ||
| { | ||
| FileName = "dotnet", | ||
| Arguments = "--version", | ||
| RedirectStandardOutput = true, | ||
| RedirectStandardError = true, | ||
| UseShellExecute = false, | ||
| CreateNoWindow = true | ||
| } | ||
| }; | ||
|
|
||
| process.Start(); | ||
| var output = process.StandardOutput.ReadToEnd(); | ||
| process.WaitForExit(); | ||
|
|
||
| return process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output); |
There was a problem hiding this comment.
Process-start logic is duplicated across IsSdkInstalled, GetSdkVersion, GetInstalledVersions, and GetSdkPath, making it harder to consistently apply fixes (timeouts, stderr draining, disposal, etc.). Consider extracting a single helper (e.g., RunDotnet(args) returning exit code + stdout/stderr) and reuse it for all commands.
| public bool IsSdkInstalled() | |
| { | |
| try | |
| { | |
| var process = new Process | |
| { | |
| StartInfo = new ProcessStartInfo | |
| { | |
| FileName = "dotnet", | |
| Arguments = "--version", | |
| RedirectStandardOutput = true, | |
| RedirectStandardError = true, | |
| UseShellExecute = false, | |
| CreateNoWindow = true | |
| } | |
| }; | |
| process.Start(); | |
| var output = process.StandardOutput.ReadToEnd(); | |
| process.WaitForExit(); | |
| return process.ExitCode == 0 && !string.IsNullOrWhiteSpace(output); | |
| private (int ExitCode, string StdOut, string StdErr) RunDotnet(string arguments) | |
| { | |
| var startInfo = new ProcessStartInfo | |
| { | |
| FileName = "dotnet", | |
| Arguments = arguments, | |
| RedirectStandardOutput = true, | |
| RedirectStandardError = true, | |
| UseShellExecute = false, | |
| CreateNoWindow = true | |
| }; | |
| using var process = new Process | |
| { | |
| StartInfo = startInfo | |
| }; | |
| process.Start(); | |
| string stdout = process.StandardOutput.ReadToEnd(); | |
| string stderr = process.StandardError.ReadToEnd(); | |
| process.WaitForExit(); | |
| return (process.ExitCode, stdout, stderr); | |
| } | |
| public bool IsSdkInstalled() | |
| { | |
| try | |
| { | |
| var result = RunDotnet("--version"); | |
| return result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.StdOut); |
| public async Task CheckSdkAsync_ReturnsValidInfo() | ||
| { | ||
| var info = await _service.CheckSdkAsync(); | ||
|
|
||
| Assert.NotNull(info); | ||
| Assert.NotNull(info.RecommendedVersion); | ||
| Assert.NotNull(info.DownloadUrl); | ||
| } |
There was a problem hiding this comment.
These tests execute the real dotnet CLI on the machine running the tests. That makes them environment-dependent and potentially flaky in CI (e.g., missing SDK, different PATH, localized output). To make them true unit tests, introduce an abstraction for process execution (or inject a runner) and mock outputs to deterministically test parsing/version detection/path extraction; then add tests for --list-sdks parsing and GetSdkPath() behavior.
Summary
ISdkCheckerServiceinterfaceSdkCheckerServiceto detect SDK installationChanges
BannerlordModEditor.Common/Services/SdkCheckerService.cs- ImplementationBannerlordModEditor.Common.Tests/Services/SdkCheckerServiceTests.cs- Unit testsCloses #5