8000 fix(tools): harden run_command and adopt it in tools/lint by VLukiantsev · Pull Request #1532 · google/xmc · GitHub
[go: up one dir, main page]

Skip to content

fix(tools): harden run_command and adopt it in tools/lint - #1532

Merged
VLukiantsev merged 2 commits into
mainfrom
fix/harden-run-command
Nov 3, 2026
Merged

VLukiantsev merged 2 commits into
mainfrom
fix/harden-run-command

Conversation

@VLukiantsev
Copy link
Copy Markdown
Collaborator

Summary

Hardens the existing shared subprocess helper tools/common/tool_utils.run_command with stdin input, an optional timeout, a configurable decoding error handler, and opt-in conversion of launch/timeout/exit failures into HandledError. Migrates the three tools/lint scripts that called subprocess directly to use it.

Motivation & Context

Review feedback on #1474, #1475, #1476, and #1477 repeatedly flagged subprocess calls that crashed with UnicodeDecodeError on non-UTF-8 output, could hang without a timeout, or turned a missing binary or git failure into an exit-2 traceback instead of a handled error. Each tool fixed this locally. This PR moves the fix into the existing shared helper instead of adding another one.

Design choices:

  • Keyword-only, backward-compatible options. The defaults (input_text=None, timeout=None, errors=None, handled_instruction=None) produce exactly the same subprocess.run behavior as before for all existing callers.
  • HandledError conversion is opt-in (handled_instruction=...), not the default. Existing callers catch the raw exceptions (for example, tools/dev_env/deploy_dev_env.py catches subprocess.CalledProcessError and OSError from verify_helm_min_version, which calls run_command), and HandledError is not an OSError. Converting by default would bypass those handlers. A required hint string also means every converted error carries a remediation instruction.
  • Strict decoding stays the default. Callers choose errors="replace" (lint path listings) or errors="surrogateescape" (precommit_autostage, which writes paths back to git over stdin, where replace would corrupt non-UTF-8 names).

Key Changes

  • tools/common/tool_utils.py: run_command gains input_text, timeout, errors, handled_instruction. A new private _command_error builds the HandledError message: binary name plus OS error, the timeout, or the exit code plus captured stderr.
  • tools/lint/check_legacy_imports.py: uses run_command. It no longer crashes on non-UTF-8 tracked paths (it used to call strict .decode()), and git failures are now handled errors (exit 1) instead of tracebacks.
  • tools/lint/check_agent_doc_paths.py: replaces its local try/except wrapper with run_command(..., handled_instruction=...). The instruction is unchanged. The message text changes from git ls-files failed in <root>: <exc> to the shared format.
  • tools/lint/precommit_autostage.py: _git uses run_command with stdin input. Git failures now produce a handled error that points at XMC_PRECOMMIT_NO_AUTOSTAGE=1 instead of a traceback.
  • Tests: new TestRunCommandHardening covering stdin, errors="replace", strict default, raw exceptions without the opt-in (backward compatibility), handled missing binary, timeout, non-zero exit with stderr, check=False, and logging. Lint tests cover a missing git binary (patch target moved to tools.common.tool_utils.subprocess.run), running outside a repo, non-UTF-8 tracked paths, and non-UTF-8 restaging.

Assumptions

  • Lint git calls get no timeout: they are local, non-interactive git commands (ls-files, diff, hash-object, add) that don't block on network or credentials. If they did hang, the hook would stall as it does today. Adding one later is a single keyword argument.
  • Handled-error message text for check_agent_doc_paths git failures changes (exit code and instruction unchanged). Nothing parses that message; the pre-commit hook only checks the exit status.
  • Out of scope (follow-up), remaining direct subprocess call sites:
    • tools/pr_review/github_utils.py:100, tools/pr_review/reply_pr_comment.py:89, tools/pr_review/wait_for_ci.py:170,190. These are left alone because a parallel change edits these files.
    • tools/release/git_ops.py:32,48,80,154,189, tools/release/github_release_client.py:92.

Verification & Testing

  • Commands Executed: ./.githooks/pre-commit (full run, no bypass: formatters, linters, run_affected_tests.py --skip-emulator-check, 293/293 Bazel tests passed, including every tools/dev_env, tools/release, and deploy target that depends on run_command); ./tools/analyze_diff_size.py --base-ref origin/main... (75 prod / 94 test lines).
  • Coverage & Test Status: coverage.py over the four touched test modules (106 tests pass). Every new or changed line in run_command, _command_error, and the three lint call sites is covered. File totals: tool_utils.py 92%, check_agent_doc_paths.py 98%, check_legacy_imports.py 94%, precommit_autostage.py 92%. The uncovered lines are pre-existing main() entrypoints and venv bootstrap.

Related Issues / Bugs

N/A

TAG=agy
CONV=0640f971-1742-419e-afd7-3309df06d861

Extend tools/common/tool_utils.run_command with keyword-only options
instead of adding a parallel subprocess helper:

- input_text: text written to the command's stdin.
- timeout: kill the command and raise subprocess.TimeoutExpired.
- errors: decoding error handler (e.g. "replace"); None keeps strict
  decoding, so existing callers are unchanged.
- handled_instruction: opt-in conversion of OSError (missing or
  non-executable binary), TimeoutExpired, and CalledProcessError
  (check=True) into HandledError carrying the remediation hint, so
  run_tool exits 1 with JSON instead of exiting 2 with a traceback.

Conversion is opt-in because existing callers (e.g. tools/dev_env)
catch OSError and CalledProcessError from run_command.

Migrate check_legacy_imports.py, check_agent_doc_paths.py, and
precommit_autostage.py to run_command. check_legacy_imports no longer
crashes on non-UTF-8 tracked paths; precommit_autostage turns git
failures into handled errors and round-trips non-UTF-8 paths with
surrogateescape.

TAG=agy
CONV=0640f971-1742-419e-afd7-3309df06d861
@VLukiantsev
Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request enhances the run_command utility in tools/common/tool_utils.py by adding support for stdin input, timeouts, custom decoding error handlers, and wrapping subprocess exceptions into a user-friendly HandledError. Several linting tools are refactored to use this updated helper, and comprehensive unit tests are added. The feedback suggests using shlex.join to safely format commands with spaces and adding a guard to prevent an IndexError in _command_error if the command list is empty.

Comment thread tools/common/tool_utils.py Resolved
@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request enhances the run_command helper in tools/common/tool_utils.py to support input text, timeouts, custom decoding error 8000 handling, and structured HandledError wrapping with remediation instructions. Various linting tools (check_agent_doc_paths.py, check_legacy_imports.py, and precommit_autostage.py) are refactored to utilize this updated helper, improving their robustness against git errors and non-UTF-8 file paths. The review feedback suggests using shlex.join(cmd) instead of " ".join(cmd) in _command_error to ensure command strings are formatted correctly when arguments contain spaces or quotes.

Comment thread tools/common/tool_utils.py Resolved
Use shlex.join when formatting the command in HandledError messages so
arguments containing spaces or quotes stay unambiguous. Existing
"Running:" and "Command failed:" log lines are unchanged.

TAG=agy
CONV=0640f971-1742-419e-afd7-3309df06d861
@VLukiantsev
VLukiantsev added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit 7b745ac Nov 3, 2026
16 checks passed
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.

2 participants

0