Skip to content

[RUN-3112] Fix sudo hang and exit-code parse crash in SSHJ executor#111

Open
ncofreortiz-hub wants to merge 3 commits into
mainfrom
RUN-3112
Open

[RUN-3112] Fix sudo hang and exit-code parse crash in SSHJ executor#111
ncofreortiz-hub wants to merge 3 commits into
mainfrom
RUN-3112

Conversation

@ncofreortiz-hub

Copy link
Copy Markdown

Summary

Fixes the sudo-over-PTY flow introduced in PR #66 (Rundeck 5.8.0, RPL-40), which customers reported as either hanging forever or failing with Failed: Unknown: For input string: "[?2004l0".

Three separate defects in SudoCommand.java, all still present on main:

  1. Bogus timeout unit.withTimeout(30000, TimeUnit.SECONDS) is ~8.3 hours, not 30 seconds. Any failed prompt match therefore looked like an infinite hang instead of failing fast. Fixed to TimeUnit.MILLISECONDS.
  2. Fragile prompt regexPROMPT_PATTERN = "~.*\\$" assumes every shell prompt contains a literal ~ and ends in $. Root shells (#) and custom PS1 values never match, so the initial expect() blocks. Broadened to .*[#$]\s*.
  3. Unsanitized exit-code parsingexpectit's removeNonPrintable() filter strips only the leading ESC byte of an ANSI control sequence (e.g. a bracketed-paste-mode toggle), leaving the visible remainder ([?2004l) glued to the real output. That string was fed directly into Integer.parseInt() with no validation, producing the reported NumberFormatException. Added sanitizeExitCode()/isValidExitCode() to strip escape remnants and raise a clear IOException instead of an opaque parse crash if the response is still not a plain integer.

Test plan

  • ./gradlew compileJava compileTestGroovy — compiles clean
  • ./gradlew test --tests SudoCommandSpec --tests SSHJExecSpec — 14/14 pass (new SudoCommandSpec covers all three fixes + regex/sanitize edge cases; existing SSHJExecSpec regression still green)
  • Manual end-to-end verification against a real RHEL9/Ubuntu node with password auth (not run in this environment — no live SSH target available)

🤖 Generated with Claude Code

Three bugs in the sudo flow added in 5.8.0 (PR #66):

1. Expect timeout was set to 30000 TimeUnit.SECONDS (~8.3h) instead of
   milliseconds, so a failed prompt match looked like an infinite hang.
2. PROMPT_PATTERN "~.*\$" assumed every shell prompt contains a literal
   '~' and ends in '$', which fails for root shells (#) or custom PS1,
   causing the initial expect() to never match.
3. Leaked ANSI control-sequence remnants (e.g. bracketed-paste-mode
   toggles) in the captured exit-code response crashed with
   NumberFormatException: For input string: "[?2004l0" since the
   response was fed into Integer.parseInt() unsanitized.

Co-Authored-By: Claude <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses customer-reported hangs and exit-code parsing failures in the SSHJ sudo-over-PTY flow by tightening Expect timeout configuration, broadening prompt detection, and sanitizing exit-code output to avoid ANSI/control-sequence contamination.

Changes:

  • Fixes the Expect timeout unit to milliseconds so prompt-matching failures fail fast.
  • Updates prompt matching and adds exit-code sanitization/validation to prevent parse crashes.
  • Adds Spock coverage for prompt-regex and exit-code sanitization/validation behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/main/java/com/plugin/sshjplugin/sudo/SudoCommand.java Adjusts prompt regex/timeout and adds exit-code sanitization + validation logic in the sudo shell flow.
src/test/groovy/com/plugin/sshjplugin/sudo/SudoCommandSpec.groovy Adds tests for the new prompt regex and exit-code sanitization/validation helpers.

Comment thread src/main/java/com/plugin/sshjplugin/sudo/SudoCommand.java Outdated
Comment thread src/main/java/com/plugin/sshjplugin/sudo/SudoCommand.java
…overflow

- PROMPT_PATTERN lacked an end-of-input anchor, so expectit's find()-based
  matching could fire on any mid-output '$'/'#' (e.g. "PATH=$PATH:...")
  instead of the actual shell prompt. Anchored with $.
- isValidExitCode only checked digit shape via regex, which still let an
  overflowing digit string through to the caller's Integer.parseInt(),
  reintroducing the crash this PR fixes. Now validates via parseInt itself.

Co-Authored-By: Claude <noreply@anthropic.com>
@fdevans fdevans changed the title RUN-3112: Fix sudo hang and exit-code parse crash in SSHJ executor [RUN-3112] Fix sudo hang and exit-code parse crash in SSHJ executor Jul 8, 2026
// Was "30000, TimeUnit.SECONDS" (~8.3 hours) - a unit typo that made
// the sudo flow appear to hang indefinitely instead of failing fast
// when a prompt/pattern never matched.
.withTimeout(30000, TimeUnit.MILLISECONDS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this TO enough for any kind of command to be triggered? @ncofreortiz-hub

…timeout

Reducing the single global expect timeout from 30000s to 30000ms fixed the
hang on a broken prompt match, but it also capped every step at 30s -
including the wait for the sudo'd command itself to finish, which the
original (buggy) huge value was there to accommodate for long-running
commands.

Split it: prompt/password exchanges keep a short 30s fail-fast timeout,
while the wait for the command to complete now uses the same
user-configurable ssh-command-timeout (0 = unlimited) the non-sudo path
already honors, via a new SudoCommand#setCommandTimeoutMs plumbed through
SudoCommandBuilder from SSHJExec.

Co-Authored-By: Claude <noreply@anthropic.com>
// prompt-detection default above.
Expect commandWait = commandTimeoutMs > 0
? expect.withTimeout(commandTimeoutMs, TimeUnit.MILLISECONDS)
: expect.withInfiniteTimeout();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
: expect.withInfiniteTimeout();
: expect.withTimeout(Long.MAX_VALUE, TimeUnit.DAYS);

since withInfiniteTimeout is deprecated, we could use something like this

@ronaveva ronaveva left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, make sure to solve the pending observation before merging

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants