# क्या SSH पाइपलाइन का एग्जिट स्टेटस विफल कमांड छिपा सकता है?

रिमोट कमांड विफल हो सकती है, फॉर्मेटर भरोसेमंद दिखने वाला आउटपुट लिख सकता है और एजेंट फिर भी सफलता की घोषणा कर सकता है। यह SSH का कोई रहस्य नहीं है। यह सामान्य shell semantics हैं, जो पर्याप्त सबूत के बिना नेटवर्क सीमा पार कर रही हैं।

समाधान केवल हर स्क्रिप्ट में `set -o pipefail` जोड़ देना नहीं है। `pipefail` एक संयुक्त परिणाम बदलता है। महत्वपूर्ण SSH काम चलाने वाले एजेंट को हर पाइपलाइन स्टेज का स्टेटस, अपेक्षित nonzero एग्जिट के लिए स्पष्ट नियम और ऐसा अंतिम रिमोट एग्जिट कोड चाहिए जिसे सफलता न समझा जा सके। Vector को तुरंत कॉपी करें, उसका नाम रखें और wrapper को तय करने दें कि सफलता का अर्थ क्या है।

## हरी आखिरी कमांड पहली विफल कमांड छिपा सकती है

डिफॉल्ट रूप से shell पाइपलाइन आखिरी कमांड का एग्जिट स्टेटस लौटाती है। इसलिए deployment, migration, backup और repair के काम में यह पंक्ति खतरनाक है:

```bash
build_manifest | sign_manifest | tee /var/tmp/manifest.json
```

मान लें `build_manifest` इसलिए विफल हुई क्योंकि वह ज़रूरी फ़ाइल पढ़ नहीं सकी। `sign_manifest` को उपयोगी इनपुट न मिले और वह भी विफल हो सकती है, या खाली परिणाम बना सकती है। `tee` फिर भी फ़ाइल बना सकता है, शून्य बाइट लिख सकता है और शून्य स्टेटस के साथ समाप्त हो सकता है। Shell पूरी पाइपलाइन के लिए शून्य रिपोर्ट करता है। केवल `$?` जांचने वाले caller को सफलता दिखती है।

GNU Bash Reference Manual इसे साफ़ तौर पर बताता है: `pipefail` चालू न हो तो पाइपलाइन आखिरी कमांड का एग्जिट स्टेटस इस्तेमाल करती है। Bash synchronous pipeline की सभी कमांड का इंतज़ार करता है, लेकिन इंतज़ार करना उनके परिणाम सुरक्षित रखने के बराबर नहीं है।

इंटरैक्टिव टर्मिनल पर बैठा इंसान कभी-कभी गायब डेटा या error message देख लेता है। एजेंट का नज़रिया अक्सर सीमित होता है। उसे अधूरा transcript, formatted summary या केवल कमांड का अंतिम परिणाम मिल सकता है। अगर स्क्रिप्ट शून्य कहती है, तो एजेंट के पास कार्रवाई को सफल बताने का आधार होता है, भले ही मांगा गया काम हुआ ही न हो।

टीमें जिस अंतर को अक्सर मिला देती हैं, वह सरल है:

- पाइपलाइन का exit status एक निर्णय मान है।
- उसकी कमांड के statuses उस निर्णय के पीछे का सबूत हैं।

आपको दोनों चाहिए। निर्णय मान तय करता है कि रिमोट कमांड सफलता लौटाएगी या नहीं। सबूत reviewer, log या supervising agent को बताता है कि गलती कहां हुई।

यह सबसे महत्वपूर्ण तब होता है जब पहली स्टेज दुनिया में बदलाव करती है। मान लें रिमोट export production data पढ़ता है, उसे compress और encrypt करता है, फिर upload करता है। Upload client खाली stream भेजने के बाद भी शून्य पर समाप्त हो सकता है। Transcript में `completed` जैसे भरोसा देने वाले शब्द हो सकते हैं, क्योंकि बाद वाले प्रोग्राम ने अपना सीमित काम पूरा किया। यह परिणाम export के सफल होने का झूठा बयान नहीं बनना चाहिए।

## SSH वही लौटाता है जो रिमोट shell लौटाना चुनता है

OpenSSH रिमोट shell pipeline के अंदर की कमांड की जांच नहीं करता। वह रिमोट कमांड का स्टेटस लौटाता है, या तब 255 लौटाता है जब SSH को खुद कोई त्रुटि मिले।

यह व्यवहार सही और उपयोगी है। SSH यह नहीं जान सकता कि रिमोट टेक्स्ट pipeline है, shell function है, script है या ऐसा application है जो exit codes को अपने तरीके से इस्तेमाल करता है:

```bash
ssh deploy@host 'generate | transform | tee result.txt'
```

रिमोट login shell उस कमांड को parse करता है। अगर उसकी pipeline semantics आखिरी `tee` का स्टेटस रिपोर्ट करती हैं, तो SSH वही स्टेटस स्थानीय मशीन को लौटाएगा। रिमोट shell द्वारा पुराने परिणाम छोड़ दिए जाने के बाद स्थानीय caller उन्हें दोबारा नहीं बना सकता।

स्थानीय shell में `set -o pipefail` लगाने से रिमोट pipeline ठीक नहीं होती। यह कमांड केवल स्थानीय pipeline के status rules बदलती है:

```bash
set -o pipefail
ssh deploy@host 'generate | transform | tee result.txt'
```

`generate | transform | tee result.txt` का मालिक अब भी रिमोट shell है। उसे अपना स्पष्ट shell और अपनी failure handling चाहिए।

एक दूसरा जाल भी है। यह स्थानीय कमांड SSH के लौटने के बाद एक और pipeline बनाती है:

```bash
ssh deploy@host 'remote command' 2>&1 | tee session.log
```

अब दो अलग pipelines हैं:

1. `remote command` के भीतर रिमोट shell की pipeline हो सकती है।
2. स्थानीय shell में `ssh | tee session.log` pipeline है।

सफल स्थानीय `tee`, SSH transport failure या रिमोट wrapper के nonzero exit को छिपा सकता है। आपको host पर रिमोट pipeline और SSH के आसपास की स्थानीय pipeline दोनों जांचनी होंगी। इस पूरी पंक्ति को एक opaque command मानना false green results के review से बच निकलने का तरीका है।

## Pipefail विफलता पकड़ता है, लेकिन उसका कारण नहीं बताता

`set -o pipefail` Bash का संयुक्त परिणाम बदलता है। इसके चालू होने पर Bash उस सबसे दाईं ओर वाली कमांड का स्टेटस लौटाता है जो nonzero पर समाप्त हुई, या सभी कमांड सफल होने पर शून्य लौटाता है।

कई स्क्रिप्ट के लिए यह वास्तविक सुधार है:

```bash
set -o pipefail
produce_data | validate_data | publish_data
printf 'pipeline status: %s\n' "$?"
```

अगर `produce_data` 17 पर समाप्त हो और बाद की कमांड शून्य लौटाएं, तो pipeline 17 लौटाएगी। अगर `validate_data` 4 लौटाए और `publish_data` शून्य लौटाए, तो pipeline 4 लौटाएगी। Calling process को झूठ के बजाय failure मिलता है।

लेकिन एक से अधिक स्टेज विफल होने पर `pipefail` विवरण खो देता है। मान लें statuses `17 4 0` हैं। Pipeline result 4 होगा, क्योंकि 4 दाईं ओर वाली विफल कमांड से आया है। इससे पता चलता है कि विफलता हुई, लेकिन यह नहीं पता चलता कि validator ने producer की विफलता पैदा की, उस पर प्रतिक्रिया दी या स्वतंत्र रूप से विफल हुआ।

इसीलिए `pipefail` एक guardrail है, report format नहीं। जब आप पूरी pipeline को एक इकाई की तरह विफल करना चाहते हैं तब इसका इस्तेमाल करें। जब आपको बाद में इन सवालों का जवाब देना हो, तब `PIPESTATUS` इस्तेमाल करें:

- किस स्टेज ने nonzero code लौटाया?
- क्या पहली स्टेज विफल होने के बाद अगली स्टेज चली और सफल हुई?
- क्या process ने अपना error लौटाने के बजाय signal प्राप्त किया?
- क्या इस खास कमांड के लिए nonzero code अपेक्षित है?

इस अंतर को `|| true` से न छिपाएं:

```bash
produce_data | validate_data | publish_data || true
```

यह तरीका लोकप्रिय है क्योंकि इससे स्क्रिप्ट चलती रहती है। साथ ही यह caller के पास मौजूद एकमात्र signal भी मिटा देता है। अगर किसी स्टेज का nonzero लौटाना वैध हो सकता है, तो actual vector कॉपी करने के बाद उस स्टेज के लिए स्वीकार्य status लिखें। पूरी pipeline की failure को माफ़ न करें।

## अगर एक कमांड भी पहले चला दी तो PIPESTATUS गायब हो जाता है

Bash हर स्टेज का exit code `PIPESTATUS` array में देता है। यह array जानबूझकर fragile है: इसमें सबसे हाल में चली foreground pipeline का विवरण होता है और अगली कमांड इसे बदल सकती है।

यह ठीक दिखता है, लेकिन गलत है:

```bash
source_data | normalize | upload
pipeline_rc=$?
printf 'pipeline result: %s\n' "$pipeline_rc"
statuses=("${PIPESTATUS[@]}")
```

अंतिम assignment तक पहुंचने से पहले `pipeline_rc=$?` assignment और `printf` चल चुके हैं। `PIPESTATUS` अब `source_data | normalize | upload` का विवरण नहीं रखता।

Array को सबसे पहले कॉपी करें:

```bash
source_data | normalize | upload
statuses=("${PIPESTATUS[@]}")
```

फिर pipeline के संयुक्त code पर निर्भर हुए बिना इसे जांचें:

```bash
printf 'source_data=%s normalize=%s upload=%s\n' \\
  "${statuses[0]}" "${statuses[1]}" "${statuses[2]}"
```

इसी कारण सामान्य `set -e` diagnosis को और कठिन बना सकता है। `pipefail` चालू होने पर विफल pipeline Bash को अगली पंक्ति में `PIPESTATUS` कॉपी करने से पहले exit करा सकती है। Shell error handling में कई संदर्भ-आधारित अपवाद हैं और केवल `set -e` पर निर्भर स्क्रिप्ट अक्सर command failure के ठीक समय कम सबूत देती हैं।

जिस pipeline के statuses महत्वपूर्ण हों, उसे चलाने और snapshot लेने के लिए जरूरी कुछ पंक्तियों में `errexit` बंद करें। फिर स्पष्ट निर्णय लें। यह magic shell option से अधिक code है, लेकिन incident के समय इसे पढ़ा जा सकता है।

## रिमोट प्रोग्राम को आवश्यक shell के तहत चलाएं

`PIPESTATUS` Bash array है। यह portable POSIX `sh` syntax नहीं है और POSIX shell में `pipefail` आवश्यक नहीं है। SSH के जरिए चलाया गया रिमोट command ऐसे login shell में चल सकता है जिसे आपने चुना ही नहीं। किसी host पर Bash हो सकता है, दूसरे पर `dash`, `zsh` या restricted shell।

अनिर्दिष्ट रिमोट shell को Bash syntax न भेजें और यह उम्मीद न करें कि मशीन आपकी बात मानेगी। Bash को स्पष्ट रूप से शुरू करें:

```bash
ssh deploy@host 'bash -s' <<'REMOTE_SCRIPT'
printf 'alpha\n' | grep 'beta' | tee /var/tmp/example.out
statuses=("${PIPESTATUS[@]}")
printf 'stages=%s,%s,%s\n' \\
  "${statuses[0]}" "${statuses[1]}" "${statuses[2]}" >&2
REMOTE_SCRIPT
```

Quoted heredoc delimiter महत्वपूर्ण है। `<<'REMOTE_SCRIPT'` स्थानीय shell को variables, command substitutions और backslashes expand करने से रोकता है, इससे पहले कि वह script भेजे। रिमोट Bash process को वही text मिलता है जो आपने लिखा।

macOS पर system Bash पुराना है, लेकिन वह indexed arrays, `PIPESTATUS` और `set -o pipefail` सपोर्ट करता है। इसका मतलब यह नहीं कि `/bin/sh` Bash है। `#!/bin/bash` तभी मदद करता है जब आप file को सीधे चलाते हैं। अगर आप `ssh host '...'` को one-line command देते हैं, तो remote login shell उसे parse करेगा, जब तक आप स्पष्ट रूप से Bash शुरू न करें।

नियमित automation path के लिए रिमोट wrapper को versioned script में रखें और उसका absolute path चलाएं। थोड़े समय के agent work के लिए quoted heredoc के साथ `bash -s` की जांच आसान होती है, क्योंकि पूरा remote program स्थानीय action request में दिखाई देता है।

## Wrapper को stages का नाम देना और ईमानदार परिणाम लौटाना चाहिए

एक उपयोगी remote wrapper चार काम करता है। वह pipeline चलाता है, status vector तुरंत कॉपी करता है, machine-readable record लिखता है और आवश्यक stage विफल होने पर nonzero exit करता है।

यह उदाहरण तीन-stage data transfer इस्तेमाल करता है। Commands बदलें, लेकिन control flow बनाए रखें। यह जानबूझकर pipeline के बाद का निर्णय लेने के लिए `set -e` पर निर्भर नहीं करता।

```bash
#!/usr/bin/env bash
set -uo pipefail

run_export() {
  local -a status
  local stage
  local -a names=("collect" "compress" "send")

  set +e
  collect_records | gzip -c | send_archive --destination daily
  status=("${PIPESTATUS[@]}")
  set -e

  if ((${#status[@]} != ${#names[@]})); then
    printf 'agent_pipeline_error pipeline=export reason=status_count expected=%s got=%s\n' \\
      "${#names[@]}" "${#status[@]}" >&2
    return 70
  fi

  for stage in "${!names[@]}"; do
    printf 'agent_pipeline_status pipeline=export stage=%s code=%s\n' \\
      "${names[$stage]}" "${status[$stage]}" >&2
  done

  for stage in "${!status[@]}"; do
    if (( status[stage] != 0 )); then
      printf 'agent_pipeline_result pipeline=export outcome=failed\n' >&2
      return "${status[$stage]}"
    fi
  done

  printf 'agent_pipeline_result pipeline=export outcome=ok\n' >&2
  return 0
}

run_export
```

सफल compressor और sender के साथ failed collection इस तरह का output देती है:

```text
agent_pipeline_status pipeline=export stage=collect code=23
agent_pipeline_status pipeline=export stage=compress code=0
agent_pipeline_status pipeline=export stage=send code=0
agent_pipeline_result pipeline=export outcome=failed
```

Wrapper 23 पर exit करता है। SSH स्थानीय process को 23 लौटाता है। Agent बता सकता है कि export `collect` पर विफल हुआ, भले ही `send_archive` ने empty stream के लिए completion message लिखा हो।

लौटाए गए code से अधिक महत्वपूर्ण उसके पीछे का अनुशासन है। इस wrapper में pipeline order के अनुसार पहला nonzero status चुना जाता है। Bash `pipefail` इसके बजाय दाईं ओर वाला nonzero status चुनता है। दोनों policies काम कर सकती हैं, अगर आप उन्हें स्पष्ट करें और test करें। Operations work में मुझे पहला failed stage अधिक उपयोगी लगता है, क्योंकि वह आम तौर पर शुरुआती fault के करीब होता है। पूरा status vector action record में रखें, ताकि किसी को एक संख्या से अनुमान न लगाना पड़े।

Stage names केवल सजावट नहीं हैं। `0=23,1=0,2=0` देखने पर व्यक्ति को script फिर से खोलनी पड़ती है। `collect=23,compress=0,send=0` supervisor को failure route करने, context जोड़ने या retry सुरक्षित है या नहीं तय करने देता है।

## स्थानीय logging दूसरा false success बना सकती है

Operators को स्थानीय transcript चाहिए। Agents को भी चाहिए। इसे पाने का सामान्य तरीका यह है:

```bash
ssh deploy@host 'bash -s' < remote-export.sh 2>&1 | tee ssh-export.log
```

अगर SSH 23 लौटाता है, लेकिन स्थानीय `tee` transcript लिखकर शून्य लौटाती है, तो default रूप से स्थानीय pipeline शून्य लौटाती है। आपने रिमोट झूठ ठीक किया और स्थानीय झूठ बना दिया।

स्थानीय statuses भी कॉपी करें:

```bash
set +e
ssh deploy@host 'bash -s' < remote-export.sh 2>&1 | tee ssh-export.log
local_status=("${PIPESTATUS[@]}")
set -e

ssh_rc=${local_status[0]}
tee_rc=${local_status[1]}
printf 'ssh=%s tee=%s\n' "$ssh_rc" "$tee_rc" >&2

if (( ssh_rc != 0 )); then
  exit "$ssh_rc"
fi
if (( tee_rc != 0 )); then
  exit "$tee_rc"
fi
```

सिर्फ स्थानीय `pipefail` चालू करके रुक न जाएं। इससे `ssh` या `tee` में से कोई भी विफल होने पर nonzero aggregate result मिलता है, जो default behavior से बेहतर है। लेकिन यह agent को नहीं बता सकता कि remote action विफल हुआ, network connection विफल हुआ या local logging विफल हुई। इन मामलों में अलग निर्णय लेने पड़ते हैं।

SSH status 255 को अलग तरह से संभालें। OpenSSH इसे remote command result के बजाय SSH client path की त्रुटि के लिए सुरक्षित रखता है। Wrapper को इसे transport या SSH execution failure के रूप में रिपोर्ट करना चाहिए, यह दावा नहीं करना चाहिए कि किसी नामित remote pipeline stage ने 255 लौटाया।

स्थानीय और रिमोट results अलग रखने का एक और व्यावहारिक कारण है। Transcript में कई remote pipeline records, login shell की warnings और SSH diagnostic हो सकते हैं। अगर agent free-form text में दिखाई देने वाली आखिरी संख्या पढ़ेगा, तो कभी न कभी गलत संख्या चुन लेगा। पहचानने योग्य records इस्तेमाल करें और final action result को वास्तविक process exit status से जोड़ें।

## SIGPIPE के लिए लिखित अपवाद चाहिए, blanket pardon नहीं

`pipefail` ऐसी विफलता सामने लाता है जिसे कई scripts पहले अनदेखा करती थीं: SIGPIPE। Bash में signal number `N` से समाप्त process को `128 + N` status मिलता है। SIGPIPE अक्सर 141 के रूप में दिखता है।

जानबूझकर होने वाला सामान्य मामला है:

```bash
generate_many_lines | head -n 10
```

`head` दस lines पढ़कर सफलतापूर्वक समाप्त हो जाता है। Generator लिखना जारी रख सकता है, reader न बचने पर SIGPIPE पा सकता है और 141 पर समाप्त हो सकता है। `pipefail` के साथ pipeline विफल दिख सकती है, भले ही मांगा गया दस-line sample तैयार हो गया हो।

इसका अर्थ यह नहीं कि हर pipeline में 141 harmless है। Network client, compressor या data producer को SIGPIPE इसलिए मिल सकता है कि अप्रत्याशित downstream consumer crash हो गया या input अस्वीकार कर दिया। हर 141 को सफलता मानने से टूटा हुआ transfer छिप जाएगा।

सही नियम सीमित है: signal-derived status को तभी स्वीकार करें जब उस stage का जल्दी रुकना command के intended contract का हिस्सा हो। यह अपवाद global shell setting में नहीं, उसी stage के पास लिखें।

उदाहरण के लिए, जानबूझकर बनाए गए preview का wrapper `generate_many_lines=141` तभी स्वीकार कर सकता है जब `head=0` हो:

```bash
if (( status[0] == 141 && status[1] == 0 )); then
  printf 'agent_pipeline_result pipeline=preview outcome=ok reason=expected_sigpipe\n' >&2
  return 0
fi
```

हर दूसरा nonzero result failure ही रहेगा। यह छोटी-सी specificity एक आम overcorrection रोकती है: लोग `pipefail` चालू करते हैं, एक बार noisy 141 देखते हैं और फिर पूरे automation estate में इसे बंद कर देते हैं।

## Agent को command output से अलग सबूत चाहिए

Agent को prose पढ़कर success तय नहीं करनी चाहिए। Commands विफल होने से पहले success words लिख सकती हैं, tools warnings को results के साथ मिला सकते हैं और एक stage गलत हो जाने के बाद remote script final line लिख सकती है।

Action contract को दो layers में बांटें:

1. Process exit code तय करे कि requested action सफल हुआ या नहीं।
2. Structured status records हर महत्वपूर्ण pipeline stage का विवरण दें।

सामान्य command output debugging के लिए उपलब्ध रखें, लेकिन agent से control flow अनुमान लगाने को न कहें। ऊपर वाले wrapper में stderr पर `agent_pipeline_status` और `agent_pipeline_result` से शुरू होने वाले records जाते हैं। Calling program उस stream को सुरक्षित रख सकता है, केवल इन exact records को parse कर सकता है और बाकी output इंसान को दिखा सकता है।

Untrusted command output में दिखाई देने वाले marker पर केवल इसलिए भरोसा न करें कि वह status record जैसा दिखता है। अगर pipeline stage किसी दूसरे user या system से मिले data को संभालती है, तो उस data में आपके record जैसी line हो सकती है। बेहतर तरीका है कि wrapper stage output capture करे और pipeline खत्म होने के बाद खुद records लिखे। अधिक जोखिम वाले काम में restrictive permissions वाली dedicated result file इस्तेमाल करें, फिर final record लिखने से पहले wrapper उसे पढ़कर validate करे।

Agent की report में remote exit code, local SSH exit code और उपलब्ध होने पर नामित remote stage statuses शामिल होने चाहिए। इन परिणामों में भी अंतर स्पष्ट होना चाहिए:

- remote action चला और किसी नामित stage पर विफल हुआ;
- remote wrapper पूरा status record नहीं बना सका;
- SSH action channel स्थापित या कायम नहीं रख सका;
- remote action पूरा होने के बाद local transcript capture विफल हुई।

ये operational रूप से अलग तथ्य हैं। Network loss के बाद retry करने से पूरा हो चुका remote mutation दोबारा हो सकता है। Failed validation stage के बाद retry सुरक्षित हो सकता है। Local `tee` विफल होने के बाद retry बेकार हो सकता है, क्योंकि remote work पहले ही हो चुका है।

Sallyport agent से SSH credential दूर रखते हुए SSH action चला सकता है, लेकिन remote command को अब भी यह ईमानदार exit और evidence contract चाहिए।

## Agent तक पहुंचने से पहले failure paths test करें

Shell wrapper भरोसा तभी कमाता है जब वह नियंत्रित तरीकों से विफल हो। Happy path test करना सबसे कम महत्वपूर्ण branch साबित करता है।

ऐसी disposable commands बनाएं जो आपके देखने लायक statuses लौटाएं:

```bash
fail_23() { printf 'collector failed\n' >&2; return 23; }
pass_through() { cat; }
succeed() { cat >/dev/null; return 0; }

set +e
fail_23 | pass_through | succeed
status=("${PIPESTATUS[@]}")
set -e
printf 'observed=%s,%s,%s\n' "${status[0]}" "${status[1]}" "${status[2]}"
```

अपेक्षित परिणाम `23,0,0` है। फिर यही pattern उस exact SSH invocation से चलाएं जिसे agent इस्तेमाल करता है। केवल local shell test पर न रुकें, क्योंकि remote shell selection, heredoc quoting, local transcript pipeline और wrapper exit behavior पहली जांच के बाहर होते हैं।

कम से कम इन मामलों को test करें:

- हर stage सफल हो और wrapper zero पर exit करे;
- शुरुआती stage विफल हो, जबकि बाद की stages zero लौटाएं;
- कुछ input consume करने के बाद बीच वाली stage विफल हो;
- SSH connect या authenticate न कर सके;
- local `tee` transcript न लिख सके;
- जानबूझकर बनाई गई `head` pipeline अपेक्षित SIGPIPE rule को trigger करे।

हर मामले के लिए expected exit code और expected stage records लिखें। अगर test पहली stage के nonzero लौटाने के बाद success बताता है, तो wrapper अपना काम नहीं कर रहा।

आसान shortcut यह है कि agent हर action के बाद transcript देखे और तय करे कि output सही दिखता है या नहीं। यह load में विफल होगा, tools की wording बदलने पर विफल होगा और output truncate होने पर भी विफल होगा। Exit codes control channel हैं। Stage records evidence channel हैं। दोनों को अलग रखें, SSH के पार दोनों सुरक्षित रखें और किसी final `tee` को यह तय न करने दें कि remote action हुआ था या नहीं।
