Создайте конвейер оболочки, запущенный из подпроцесса.Сбой Popen, если левая сторона трубы выходит из строя

#linux #bash #shell #subprocess

Вопрос:

Я запускаю команду bash с подпроцессом.popen на python:

 cmd = "bwa-mem2/bwa-mem2 mem -R '@RG\tID:2064-01\tSM:2064-01\tLB:2064-01\tPL:ILLUMINA\tPU:2064-01' reference_genome/human_g1k_v37.fasta BHYHT7CCXY.RJ-1967-987-02.2_1.fastq BHYHT7CCXY.RJ-1967-987-02.2_2.fastq -t 14 | samtools view -bS -o dna_seq/aligned/2064-01/2064-01.6.bam -"

process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
 

Проблема в том, что я получаю код возврата 0, даже если первая команда завершается неудачно.
Я погуглил и узнал о pipefail, и, похоже, это то, что я должен использовать.

Однако я не понимаю, где это написать. Я пытался:

 "set -o pipefail amp;amp; bwa-mem2/bwa-mem2 mem -R '@RG\tID:2064-01\tSM:2064-01\tLB:2064-01\tPL:ILLUMINA\tPU:2064-01' reference_genome/human_g1k_v37.fasta BHYHT7CCXY.RJ-1967-987-02.2_1.fastq BHYHT7CCXY.RJ-1967-987-02.2_2.fastq -t 14 | samtools view -bS -o dna_seq/aligned/2064-01/2064-01.6.bam -"
 

что дает: /bin/sh: 1: набор: Незаконная опция-o pipefail

есть какие-нибудь идеи, как мне это включить?

Редактировать:

Я не уверен, правильно ли редактировать свой ответ при ответе на ответ? не хватило персонажей, чтобы ответить в комментарии:/

В любом случае, я попробовал ваш второй подход без shell=True @Charles Duffy.

(cmd_1 и cmd_2 равны тому, что вы написали в своем решении)

Это код, который я использую:

 try:
        
   p1 = Popen(shlex.split(cmd_1), stdout=PIPE)
   p2 = Popen(shlex.split(cmd_2), stdin=p1.stdout, stdout=PIPE, stderr=STDOUT, text=True)
   p1.stdout.close()
   output, error = p2.communicate()
   p1.wait()    
    
   rc_1 = p1.poll()
   rc_2 = p2.poll()
   print("rc_1:", rc_1)
   print("rc_2:", rc_2)

   if rc_1 == 0 and rc_2 == 0:
       self.log_to_file("DEBUG", "# Process ended with returncode = 0")
       if text: self.log_to_file("INFO", f"{text} succesfully 
            
   else:
       print("Raise exception")
       raise Exception(f"stdout: {output} stderr: {error}")

except Exception as e:
    print(f"Error: {e} in misc.run_command()")
    self.log_to_file("ERROR", f"# Process ended with returncode != 0, {e}")
 

это результат, который я получаю, когда намеренно вызываю ошибку, переименовывая один файл:

 [E::main_mem] failed to open file `/home/jonas/BASE/dna_seq/reads/2064-01/test_BHYHT7CCXY.RJ-1967-987-02.2_2.fastq.gz'.
free(): double free detected in tcache 2
rc_1: -6
rc_2: 0
Raise exception
Error: stdout:  stderr: None in misc.run_command()
 ERROR: # Process ended with returncode != 0, stdout:  stderr: None
 

Похоже, он фиксирует неверный код возврата.

Но почему stdout пусто и stderr= None ? Как я могу захватить выходные данные, чтобы они были зарегистрированы в регистраторе как при успешном выполнении процесса, так и при его сбое?

Комментарии:

1. Вы должны заставить свою оболочку быть bash , а не sh , pipefail чтобы быть доступным вариантом.

2. Или, лучше, вообще прекратите использовать любую оболочку и просто создайте два отдельных Popen объекта, по одному для каждой стороны трубопровода, оба с shell=False .

3. Кстати, я не уверен, что оригинальное название, в котором запрашивался «правильный» код возврата, было подходящим. Иногда поведение оболочки по умолчанию более «правильное», чем pipefail поведение-например, подумайте о том, как curl ... | head имеет ненулевой статус выхода всякий head раз, когда прекращается чтение до curl завершения записи, вызывая EPIPE, тем самым заставляя левую часть конвейера иметь статус неудачного выхода, даже если все прошло идеально.

4. конечно, вы абсолютно правы! thx:)

Ответ №1:

Во-Первых, С Помощью Оболочки

Вместо того, чтобы разрешать shell=True указывать sh по умолчанию, укажите bash явно, чтобы убедиться, что pipefail это доступная функция:

 shell_script = r'''
set -o pipefail || exit
bwa-mem2/bwa-mem2 mem 
  -R '@RGtID:2064-01tSM:2064-01tLB:2064-01tPL:ILLUMINAtPU:2064-01' 
  reference_genome/human_g1k_v37.fasta 
  BHYHT7CCXY.RJ-1967-987-02.2_1.fastq 
  BHYHT7CCXY.RJ-1967-987-02.2_2.fastq 
  -t 14 
  | samtools view -bS 
    -o dna_seq/aligned/2064-01/2064-01.6.bam -
'''

process = subprocess.Popen(["bash", "-c", shell_script],
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE,
                           text=True)
 

Это работает, но это не лучший доступный вариант.


Во-Вторых, Вообще Без Оболочки

 p1 = subprocess.Popen(
  ['bwa-mem2/bwa-mem2', 'mem',
   '-R', r'@RGtID:2064-01tSM:2064-01tLB:2064-01tPL:ILLUMINAtPU:2064-01',
   'reference_genome/human_g1k_v37.fasta',
   'BHYHT7CCXY.RJ-1967-987-02.2_1.fastq',
   'BHYHT7CCXY.RJ-1967-987-02.2_2.fastq', '-t', '14'],
  stdout=subprocess.PIPE)
p2 = subprocess.Popen(
  ['samtools', 'view', '-bS',
   '-o', 'dna_seq/aligned/2064-01/2064-01.6.bam', '-'],
  stdin=p1.stdout,
  stdout=subprocess.PIPE,
  stderr=subprocess.PIPE,
  text=True)
p1.stdout.close()
output, _ = p2.communicate() # let p2 finish running
p1.wait()                    # ensure p1 has properly exited

print(f'bwa-mem2 exited with status {p1.returncode}')
print(f'samtools exited with status {p2.returncode}')
 

…что позволяет проверять p1.returncode и p2.returncode отдельно.

Комментарии:

1. Большое вам спасибо за полезные ответы! Я попробую это и вернусь со своими результатами!:)

2. кстати… когда вы используете p1.stdout в качестве stdin для p2, как это работает? работают ли p1 и p2 одновременно, и p1 перенаправляет свой вывод на p2 с течением времени? и почему вы используете p1.stdout.close()? что произойдет, если вы этим не воспользуетесь? и как работает .communicate ()? это постоянно связывается с попеном или только один раз, когда это делается? и почему p1.wait (), но не p2.wait? извините за множество вопросов, но мне бы очень хотелось все это понять:)

3. Да, они работают одновременно-именно так работают конвейеры оболочки! Закрытие копии процесса Python p1.stdout гарантирует, что, как только p1 закроет свою сторону, p2 увидит EOF, потому что других копий файлового дескриптора больше нигде нет.

4. Что касается только необходимости звонить p1.wait() , это потому wait() , что подразумевается communicate() . Существует риск, если мы только вызовем p2.communicate() , но не p1.wait() вызовем, что Python еще не получит статус выхода p1 и поэтому не установит returncode свойство. Когда все работает правильно, интерпретатор также заполняет это, если он получает SIGCHILD от операционной системы сообщение о том, что программа, которую он запустил, завершилась, но «явное лучше, чем неявное» во многих местах, и это, возможно, одно из них-доставка сигнала не всегда надежна на 100%.

5. @biomedswe, я только сейчас заметил, что вы отредактировали вопрос. В общем, продолжение следует в комментариях к ответу, если речь идет о чем-то, что делает ответ непригодным для использования; или в совершенно новом вопросе, если вы решили свою первую проблему, но у вас возникла новая и другая проблема.