Repository navigation
[minor] Delegate process execution to ktsu.RunCommand - #147
Conversation
ProcessRunner built its own Process/ProcessStartInfo plumbing. Two gaps in that implementation were correctness bugs rather than line count: - Cancellation only stopped awaiting WaitForExitAsync. Nothing killed the child, so a cancelled run left it — and anything it had started — running unsupervised. For a build tool that means an orphaned compiler or test host still holding file locks. ktsu.RunCommand kills the whole process tree. - Arguments were taken as one pre-joined string, so callers building a command by hand had to quote it themselves. GitHubService did exactly that, in three places, with a quoter that wrapped any value containing a space and escaped nothing — an asset path containing a quote produced a malformed command. ProcessRunner is now an adapter over RunCommand's current, non-obsolete ExecuteAsync overload; every string-taking overload of that API is [Obsolete] and is deliberately avoided. IProcessRunner gains IReadOnlyList<string> overloads, where nothing has to be quoted so nothing can be quoted wrongly, and GitHubService's three hand-quoted call sites now use them. The existing string overloads stay, since ~40 call sites still build their arguments that way; CommandLineArguments splits those back apart using the same CommandLineToArgvW rules .NET applies to ProcessStartInfo.Arguments, which ArgumentList's re-quoting is the inverse of, so what reached a child process before still reaches it. RunCommand's own LineOutputHandler discards a final line that is not newline-terminated, which would have silently dropped output. LineAssembler does the line splitting instead and flushes the remainder, including on the cancelled and failed paths. Adds direct references to ktsu.Semantics.Paths and ktsu.Semantics.Strings, which KTSU0006 requires because CommandOptions.WorkingDirectory is an AbsoluteDirectoryPath that ProcessRunner names directly. Fixes #136 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGwRLtZoAdsa46QDLPYBvR
Both were faults in the tests rather than in ProcessRunner; the Linux leg was green and the two other legs each failed two tests. macOS compared working directories as strings. The temp directory is reached through /var, a symlink to /private/var, so the child reported the resolved form while the test held the unresolved one. The comment in the test even named this case without handling it. Both working-directory tests now drop a sentinel file in the expected directory and look for it through the path the child reported, which settles which directory it is without either side having to resolve anything. Windows used the cmd idiom "<nul set /p=content" to write without a trailing newline, which exited 1 — the form needs quoting as set /p "=content", and that quoting is delicate enough to be its own source of failure. PowerShell writes the bytes directly instead, so there is nothing to quote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGwRLtZoAdsa46QDLPYBvR
|
Status on the CI here, since two different things went red and only one of them was mine.
Four failures across the two legs, all of them faults in the tests I added rather than in
It fails about 35 seconds in, on both heads, before reaching any analysis: That is the Copilot-backed agent hitting an account-level monthly quota, so it will fail identically on every PR in this repository until the quota resets or is raised — nothing in this diff reaches it, and it has posted no findings on either head. I have not spent a re-run on it, because a 402 quota response is not a flake and re-running would return the same thing. Raising or waiting out the Copilot quota is the only fix, and that is an account decision rather than a code change. I am watching this PR and will keep checking until the remaining legs report on Generated by Claude Code |
The quality gate passed, but two findings were real and both were mine. S2699 (blocker): Append_NullCallback_DoesNotThrow asserted nothing — it relied on an exception failing the test, which is invisible to a reader and to Sonar. It now exercises the null-callback path and then asserts the same input through a real callback, so the lines it discards are shown to have been there to discard. Dropping the null check in LineAssembler still fails it. S3776 (critical): CommandLineArguments.NextArgument had a cognitive complexity of 30 against a limit of 15, from two nested scanning loops sharing the read position. The backslash run and the quote are now each consumed by their own method, leaving the main loop as the character walk it reads as. No behaviour change; the splitter tests pin every rule. The remaining 20 findings are MSTEST0049 and MSTEST0068 informational suggestions on test code, left alone deliberately: CollectionAssert is the convention already used throughout this suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TGwRLtZoAdsa46QDLPYBvR
|



Fixes #136
ProcessRunnerbuilt its ownProcess/ProcessStartInfoplumbing. The two gaps the issue names are correctness bugs rather than line count, and both are closed here.Cancellation no longer abandons the child
The old implementation only stopped awaiting
WaitForExitAsync. Nothing killed the child, so a cancelled run left it — and anything it had started — running unsupervised. For a build tool the children are compilers and test hosts, so an abandoned tree holds file locks and keeps consuming a runner.ktsu.RunCommandkills the whole process tree.RunWithCallbackAsync_WhenCancelled_KillsTheChildInsteadOfAbandoningItcovers this: the child writes a marker file shortly after the cancellation point and only then settles into a long sleep, so a merely-abandoned child reaches the marker well before the assertion.Arguments no longer have to be quoted by hand
IProcessRunnergainsIReadOnlyList<string>overloads, where each argument is its own value — nothing has to be quoted, so nothing can be quoted wrongly.GitHubServicehad three hand-quoted call sites (release creation, asset upload, topic setting), all using a quoter that wrapped any value containing a space and escaped nothing, so an asset path containing a quote produced a malformed command. All three now pass a list.The string overloads stay, because ~40 call sites still build their arguments that way.
CommandLineArgumentssplits those back apart using the sameCommandLineToArgvWrules .NET applies toProcessStartInfo.Arguments, andArgumentList's re-quoting is the exact inverse — so a string that reached a child process one way still reaches it that way.Two things worth a reviewer's attention
The obsolete API. Every
string commandoverload ofRunCommand.Execute/ExecuteAsyncis[Obsolete]— verified by reflecting over the shipped assembly, and the reason ktsu-dev/SvnToGit#71 is aCS0618build failure. This targets the currentExecuteAsync(fileName, arguments, handler, options, cancellationToken)overload.LineOutputHandlerdrops unterminated output. The issue's sketch usedRunCommand's own line-splitting handler. It discards a final line that is not newline-terminated:printf 'content'reaches the caller as nothing at all. That would have silently dropped output from any command not ending in a newline.LineAssemblerdoes the splitting over the rawOutputHandlerinstead, flushing the remainder — including on the cancelled and failed paths, where the partial output is exactly what someone diagnosing the failure wants. It followsReadLinesemantics (\n,\r,\r\n), so carriage-return progress output still arrives as written rather than accumulating into one enormous line.New dependencies
Adds direct references to
ktsu.Semantics.Pathsandktsu.Semantics.Strings.KTSU0006requires these to be direct becauseCommandOptions.WorkingDirectoryis anAbsoluteDirectoryPaththatProcessRunnernames itself. This is the caveat the issue flagged for maintainer awareness.Verification
dotnet build KtsuBuild.slnx— clean, 0 warnings.dotnet test KtsuBuild.slnx— 741/741 pass (703 pre-existing, 38 new).Processplumbing → the cancellation test fails with "The child process outlived cancellation and wrote its marker, so it was abandoned rather than killed."LineAssemblerforLineOutputHandler→ the three unterminated-final-line tests fail.RunCommand's line handling, verbatim argument passing and process-tree kill were each confirmed empirically against the real package before writing the adapter, as the triage note asked.The public surface of
IProcessRunneris additive;ProcessRunneris its only implementer in the repository, and the existing tests substitute the interface.🤖 Generated with Claude Code
https://claude.ai/code/session_01TGwRLtZoAdsa46QDLPYBvR
Generated by Claude Code