From 01f61507bab7b34e3eb8b21079792c473d3932e0 Mon Sep 17 00:00:00 2001 From: "lei.chen" Date: Fri, 30 Oct 2026 02:01:50 +0000 Subject: [PATCH] fix(xcecli): jobs submit with explicit command must not read stdin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bug-1980: ParseCmdArgs treated any non-terminal stdin as a potential job script and io.ReadAll'ed it before considering the command args, so a pipe that stayed open without writing data or EOF blocked submit forever — no error, no RPC, no job. Found as a day-old hung process in the user's process list (an esd-recover session's own submit probe). bsub semantics the CLI claims to follow are the reverse: stdin is the job script only when NO command is given on the command line. Gate the stdin read on len(args) == 0; an explicit command wins and stdin is never touched (also removes the ambiguity of stdin bytes being silently reinterpreted as a config script next to an explicit command). - TestParseCmdArgs_ExplicitCommandIgnoresHeldOpenStdin: held-open pipe + explicit args; RED pre-fix (blocked in io.ReadAll), GREEN post-fix. - Stdin-script path (no args) verified live: script → submitted → DONE. - Live: submit with held-open stdin went rc=124/no-job to instant submit → RUN → DONE on t1. - xcecli go test ./... 11/11 packages ok. Co-Authored-By: Claude Code --- src/frontend/xcecli/cmd/jobs/submit.go | 11 +++++-- src/frontend/xcecli/cmd/jobs/submit_test.go | 35 +++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/frontend/xcecli/cmd/jobs/submit.go b/src/frontend/xcecli/cmd/jobs/submit.go index 86134c46..5c15efba 100755 --- a/src/frontend/xcecli/cmd/jobs/submit.go +++ b/src/frontend/xcecli/cmd/jobs/submit.go @@ -102,9 +102,16 @@ func (js *JobSubmitter) ParseCmdArgs(cmd *cobra.Command, args []string) error { } } - // check if there is standard input redirection + // check if there is standard input redirection -- but only when the + // command line carries no command of its own. A piped stdin is the job + // script (bsub < script semantics, parsed as #xce job submit lines by + // parseConfigFile below); an explicit command on the command line wins + // and stdin must not be touched at all: reading it would block forever + // on a pipe that stays open without writing data (bug-1980 -- a hung + // submit observed in live use), and any stdin bytes would be silently + // reinterpreted as a config script next to the explicit command. fileInfo, err := os.Stdin.Stat() - if err == nil && (fileInfo.Mode()&os.ModeCharDevice) == 0 { + if err == nil && len(args) == 0 && (fileInfo.Mode()&os.ModeCharDevice) == 0 { if tmpFileName, err := js.handleStdin(); err != nil { errMsg := request.ExtractErrorMessage(err) return fmt.Errorf("error reading from stdin: [%s]", errMsg) diff --git a/src/frontend/xcecli/cmd/jobs/submit_test.go b/src/frontend/xcecli/cmd/jobs/submit_test.go index d2ac9466..fc83f823 100644 --- a/src/frontend/xcecli/cmd/jobs/submit_test.go +++ b/src/frontend/xcecli/cmd/jobs/submit_test.go @@ -376,3 +376,38 @@ func TestWaitForInteractiveURL_SuspendedJobKeepsWaiting(t *testing.T) { t.Errorf("url = %q, want http://h:9000", url) } } + +// bug-1980: `jobs submit ` with a piped stdin used to block forever +// when the pipe stayed open with no data (heredoc quoting accidents, generic +// tooling that keeps a stdin pipe around): ParseCmdArgs treated ANY +// non-terminal stdin as a potential job script and io.ReadAll'ed it before +// looking at the command args. LSF (bsub) semantics the CLI claims to follow +// are the reverse: stdin is the job script only when no command is given on +// the command line -- an explicit command must win and never touch stdin. +func TestParseCmdArgs_ExplicitCommandIgnoresHeldOpenStdin(t *testing.T) { + pr, pw, err := os.Pipe() + if err != nil { + t.Fatalf("pipe: %v", err) + } + defer pr.Close() //nolint:errcheck // read end: closed after Parse returns + t.Cleanup(func() { _ = pw.Close() }) + // Keep the write end open for the duration of the call: the pre-fix code + // blocks in io.ReadAll here; the post-fix code must not read stdin at all. + + oldStdin := os.Stdin + os.Stdin = pr + t.Cleanup(func() { os.Stdin = oldStdin }) + + js := NewJobSubmitter(nil) + cmd := NewSubmitJobsCmd(nil) + parseErr := js.ParseCmdArgs(cmd, []string{"sleep", "10"}) + if parseErr != nil { + t.Fatalf("ParseCmdArgs with explicit command and held-open stdin: %v", parseErr) + } + if js.command != "sleep 10" { + t.Errorf("command = %q, want %q", js.command, "sleep 10") + } + if strings.Contains(js.configFile, "xcecli_stdin_") { + t.Errorf("stdin was consumed as a config file (%s) despite an explicit command", js.configFile) + } +}