Add additional subprocess diagnostics for the audita interface
This commit is contained in:
@@ -189,11 +189,16 @@ func (r *SubprocessRunner) Run(ctx context.Context, req PolishRequest) (PolishRe
|
|||||||
StderrLogPath: req.StderrLogPath,
|
StderrLogPath: req.StderrLogPath,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf(
|
wrappedMessage := fmt.Sprintf(
|
||||||
"run audita process (binary=%q, stdout_log=%q, stderr_log=%q): %w",
|
"run audita process (binary=%q, stdout_log=%q, stderr_log=%q)",
|
||||||
r.binary,
|
r.binary,
|
||||||
req.StdoutLogPath,
|
req.StdoutLogPath,
|
||||||
req.StderrLogPath,
|
req.StderrLogPath,
|
||||||
|
)
|
||||||
|
wrappedMessage = addSubprocessStreamHint(wrappedMessage, err)
|
||||||
|
return r.failureResult(req, reqModules, runRes, credentialPresent, primaryConcurrencyViaEnv), fmt.Errorf(
|
||||||
|
"%s: %w",
|
||||||
|
wrappedMessage,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -328,6 +333,17 @@ func validateProcessedOutput(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addSubprocessStreamHint(message string, runErr error) string {
|
||||||
|
if runErr == nil {
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
lower := strings.ToLower(runErr.Error())
|
||||||
|
if strings.Contains(lower, "bad file descriptor") || strings.Contains(lower, "exit code 120") {
|
||||||
|
return message + "; hint=audita child process may have started with invalid stderr/stdout descriptors"
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
func validateJSONFile(path string) error {
|
func validateJSONFile(path string) error {
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -247,6 +247,36 @@ func TestSubprocessRunnerSubprocessFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSubprocessRunnerSubprocessFailureAddsStderrDescriptorHint(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
|
}
|
||||||
|
t.Setenv("GO_WANT_AUDITA_HELPER", "1")
|
||||||
|
t.Setenv("AUDITA_HELPER_MODE", "fail_bad_fd")
|
||||||
|
t.Setenv("OPENAI_KEY_SOURCE", "super-secret")
|
||||||
|
t.Setenv("AUDITA_HELPER_RECORD_PATH", filepath.Join(t.TempDir(), "record.json"))
|
||||||
|
|
||||||
|
llmConcurrency := 1
|
||||||
|
runner := mustAuditaRunner(t, SubprocessRunnerConfig{
|
||||||
|
Binary: writeAuditaHelperWrapper(t),
|
||||||
|
Timeout: mustParseAuditaDuration(t, "2s"),
|
||||||
|
LLMAPIKeyEnv: "OPENAI_KEY_SOURCE",
|
||||||
|
Modules: []string{"glossary"},
|
||||||
|
BaseURL: "https://openrouter.ai/api/v1",
|
||||||
|
Model: "openrouter/google/gemma-4-31b-it",
|
||||||
|
LLMConcurrency: &llmConcurrency,
|
||||||
|
Report: true,
|
||||||
|
})
|
||||||
|
req := auditaReqForTest(t, true)
|
||||||
|
_, err := runner.Run(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "hint=audita child process may have started with invalid stderr/stdout descriptors") {
|
||||||
|
t.Fatalf("error = %q, want stderr descriptor hint", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
|
func TestSubprocessRunnerMissingOutputFails(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("helper wrapper script uses /bin/sh")
|
t.Skip("helper wrapper script uses /bin/sh")
|
||||||
@@ -442,6 +472,9 @@ func TestAuditaSubprocessHelper(t *testing.T) {
|
|||||||
case "fail":
|
case "fail":
|
||||||
_, _ = os.Stderr.WriteString("audita helper failure\n")
|
_, _ = os.Stderr.WriteString("audita helper failure\n")
|
||||||
os.Exit(8)
|
os.Exit(8)
|
||||||
|
case "fail_bad_fd":
|
||||||
|
_, _ = os.Stderr.WriteString("OSError: [Errno 9] Bad file descriptor\n")
|
||||||
|
os.Exit(120)
|
||||||
case "missing_output":
|
case "missing_output":
|
||||||
if reportPath != "" {
|
if reportPath != "" {
|
||||||
writeAuditaHelperFile(reportPath, `{"schema":"audita.report.v1","steps":[]}`)
|
writeAuditaHelperFile(reportPath, `{"schema":"audita.report.v1","steps":[]}`)
|
||||||
|
|||||||
@@ -276,9 +276,27 @@ func buildDiagnostics(req RunRequest, result RunResult, stderrTail string) strin
|
|||||||
req.StderrLogPath,
|
req.StderrLogPath,
|
||||||
)
|
)
|
||||||
if strings.TrimSpace(stderrTail) == "" {
|
if strings.TrimSpace(stderrTail) == "" {
|
||||||
|
if hint := fdDiagnosticsHint(result.ExitCode, ""); hint != "" {
|
||||||
|
return details + fmt.Sprintf(" hint=%q", hint)
|
||||||
|
}
|
||||||
return details
|
return details
|
||||||
}
|
}
|
||||||
return details + fmt.Sprintf(" stderr_tail=%q", stderrTail)
|
details = details + fmt.Sprintf(" stderr_tail=%q", stderrTail)
|
||||||
|
if hint := fdDiagnosticsHint(result.ExitCode, stderrTail); hint != "" {
|
||||||
|
details = details + fmt.Sprintf(" hint=%q", hint)
|
||||||
|
}
|
||||||
|
return details
|
||||||
|
}
|
||||||
|
|
||||||
|
func fdDiagnosticsHint(exitCode int, stderrTail string) string {
|
||||||
|
lowerTail := strings.ToLower(stderrTail)
|
||||||
|
if strings.Contains(lowerTail, "bad file descriptor") || strings.Contains(lowerTail, "errno 9") {
|
||||||
|
return "stderr stream appears invalid in child process (fd 2); check wrapper/subprocess environment differences versus direct shell execution"
|
||||||
|
}
|
||||||
|
if exitCode == 120 {
|
||||||
|
return "exit code 120 may indicate interpreter shutdown stream-flush failures (commonly invalid stdout/stderr descriptors in Python processes)"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func readRedactedTail(path string, envOverrides map[string]string, maxBytes int64) string {
|
func readRedactedTail(path string, envOverrides map[string]string, maxBytes int64) string {
|
||||||
|
|||||||
@@ -122,6 +122,35 @@ func TestRunFailureRedactsSensitiveTail(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunFailureAddsBadDescriptorHint(t *testing.T) {
|
||||||
|
exe, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("os.Executable() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
req := RunRequest{
|
||||||
|
Executable: exe,
|
||||||
|
Args: []string{"-test.run=TestSubprocessHelper", "--", "failbadfd"},
|
||||||
|
EnvOverrides: map[string]string{
|
||||||
|
"GO_WANT_SUBPROCESS_HELPER": "1",
|
||||||
|
},
|
||||||
|
StdoutLogPath: filepath.Join(dir, "stdout.log"),
|
||||||
|
StderrLogPath: filepath.Join(dir, "stderr.log"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = Run(context.Background(), req)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Run() error = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "Bad file descriptor") {
|
||||||
|
t.Fatalf("error = %q, want stderr tail content", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "stderr stream appears invalid in child process") {
|
||||||
|
t.Fatalf("error = %q, want bad-descriptor hint", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunTimeout(t *testing.T) {
|
func TestRunTimeout(t *testing.T) {
|
||||||
exe, err := os.Executable()
|
exe, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -329,6 +358,9 @@ func TestSubprocessHelper(t *testing.T) {
|
|||||||
key := os.Getenv("SUBPROCESS_HELPER_ENV_KEY")
|
key := os.Getenv("SUBPROCESS_HELPER_ENV_KEY")
|
||||||
_, _ = os.Stderr.WriteString("secret:" + os.Getenv(key) + "\n")
|
_, _ = os.Stderr.WriteString("secret:" + os.Getenv(key) + "\n")
|
||||||
os.Exit(4)
|
os.Exit(4)
|
||||||
|
case "failbadfd":
|
||||||
|
_, _ = os.Stderr.WriteString("OSError: [Errno 9] Bad file descriptor\n")
|
||||||
|
os.Exit(120)
|
||||||
case "sleep":
|
case "sleep":
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
|
|||||||
Reference in New Issue
Block a user