[module] subprocess

markyang92·2021년 5월 15일
1

python

목록 보기
24/42
post-thumbnail

subprocess 모듈에서 특히 Popen class 를 주로 다룬다.

https://docs.python.org/ko/3.9/library/subprocess.html#subprocess.Popen


Popen Class

  • class subprcoess.Popen


    이렇게 proc 인스턴스를 만듦!

  • 핵심: child process를 만든다.
    • 기본 숙지 사항(default)
      • Bourn Shell(/bin/sh) 기반 명령
      • 당연히 bash쉘 기반으로 동작할 것이라 예상하면 안됨!!

bash shell 기반 child process

  • shell=True, executable="/bin/bash"
    현재 입력한 cmdbash child process 로서 사용되게 할 수 있다.
proc=subprocess.Popen(CMD,shell=True,executable="/bin/bash")
  • 예제1.

    • pstree -ps $$ 명령 수행
      • zshell에서 python 스크립트 실행
        -> 내부 subprocess.Popen("pstree -ps $$",shell=True,executable="bin/bash")으로 pstree실행


stdout=subprocess.PIPE: stdout 활용

  • 위 예제에서 child process stdoutsys.stdout으로 설정했는데, 그럼 바로 내 쉘에 보이지만 이를 문자열로 잡아서 활용할 수는 없다.
    • stdout을 활용하고 싶다면, argument를 아래와 같이 주자.
      • stdout = subprocess.PIPE
      • text=True(stdout을 str로 처리, 이 옵션이 False'b'(바이너리)로 처리됨)

  • stdin : 실행된 프로그램의 표준 입력
  • stdout : 실행된 프로그램의 표준 출력
  • stderr : 실행된 프로그램의 표준 에러 파일 핸들 지정
유효한 값Description
PIPE자식에 대한 새 파이프를 만들어낸다.
ex) stdout=subprocess.PIPE
DEVNULLos.devnull이 사용될 것임
말그대로 stdout 등이 필요없을 때 혹은 출력하고 싶지 않을 때 사용
./command >> /dev/null과 동일

bufsize

  • bufsize: 버퍼링 할 사이즈
    • stdin/stdout/stderr 파이프 파일 객체를 만들 때 open() 함수에 해당 인자로 제공된다.
    • bufsize=0: 버퍼링 되지 않음
    • bufsize=1: 줄 버퍼링 의미(universal_newlines=True 인 경우, 텍스트 모드에서만 사용할 수 있다.)
    • bufsize=다른 양수 값: 대략 그 크기의 버퍼를
    • bufsize=음수: 시스템 기본 값 io.DEFAULT_BUFFER_SIZE가 사용됨 의미

executable

  • executable: 실행할 대체 프로그램 지정
    • shell=False일 때, executableargs에 의해 지정된 프로그램을 대체
    • 원래 args 는 여전히 프로그램으로 전달된다.
    • 대부분의 프로그램은 args에 의해 지정된 프로그램을 명령 이름으로 취급한다.
      • 그래서 실제로 실행되는 프로그램과는 다를 수 있다.
    • POSIX에서, args 이름은 ps와 같은 유틸리티에서 실행 파일의 표시 이름이 된다.
    • shell=True이면, POSIX에서 executable 인자는 기본 /bin/sh의 대체 셸 지정

text

  • text = True 시
    • stdin,stdout,stderr의 파일 객체는 지정된 encodingerrors 또는 io.TextIOWrapper 기본 값을 사용하여 텍스트 모드로 열린다.
    • universal_newlines인자는 text와 동등하고 이전 버전과의 호환성을 위해 제공된다.
    • 기본 파일 객체는 바이너리 모드로 열린다.

universal_newlines: python 3.6.x


env

import subprocess

subprocess.call('/bin/bash -c "$GREPDB"', shell=True, env={'GREPDB': 'echo 123'})
  • 서브프로세스 객체에 임시 환경변수 적용한다.
  • 딕셔너리 형태

Popen 객체

1. suprocess.Popen Class의 instance create

proc=subprocess.Popen(args...)

proc.poll()

Popen.poll()

  • usage
# Create Popen instance
proc=subprocess.Popen(...)

var=proc.poll()
print(var)
  • 기능: Popen 생성자로 의해 만들어진 instance(자식 프로세스)가 종료되었는지 확인한다.
return valuedescription
Noneinstance 실행 중
return codeinstance가 종료되었고 리턴코드를 가져옴

Popen.wait()

# Create Popen instance
proc=subprocess.Popen(...)

try:
    ret=proc.wait(timeout=None)	# Wait...
except subprocess.TimeoutExpired:
    Exception occured!	
else:
    print(ret)
  • 기능: Popen 생성자로 의해 만들어진 instance(자식 프로세스)와 상호 작용
    • Popen instance가 종료될 때 까지 프로그램이 그 라인에서 기다림 (busy loop; non-blocking and short sleep)사용해 구
    • 비동기 대기(async wait)는 asyncio 모듈을 사용하라.
    • asyncio.create_subprocess_exec 참고
input argsdescription
timeout=None이 함수가 기다릴 time 지정

return valuedescription
return codetimeout내 종료된 Popen instance가 return 하는 값

exceptdescription
TimeoutExpiredstdout=PIPEstderr=PIPE를 사용하고
자식 프로세스가 파이프에 너무 많은 출력을 보내
OS 파이프 버퍼가 더 많은 데이터를 받아들일 때까지 기다리느라
블록하면 교착 상태에 빠진다.

파이프를 사용할 때 이를 피하려면 Popen.communicate()를 사용 해라.

Popen.communicate()

# Create Popen instance
proc=subprocess.Popen(...)

try:
    ret, err = proc.commiunicate(input=None, timeout=10.0f)
except subprocess.TimeoutExpired:
    Exception occured!	
else:
    ...
  • 기능: Popen 생성자로 의해 만들어진 instance(자식 프로세스)가 종료될 때 까지 그 라인에서 기다리며 상호작용
    • 프로세스와 상호 작용
    • stdin에 데이터 보냄(stdin=PIPE를 사용해 Popen 객체를 만들어야함)
    • stdout, stderr에서 데이터 읽음
    • 프로세스가 종료될 때까지 기다림
input argsdescription
input=None자식 프로세스로 전송될 데이터, 자식으로 데이터를 보내지 않으면 None 설정
스트림이 텍스트 모드로 열렸으면, input 은 문자열이어야 함
스트림이 텍스트 모드로 열리지 않았으면, input은 바이트열
timeout=None이 함수가 기다릴 time 지정

return valuedescription
(stdout_data, stderr_data)스트림이 텍스트 모드로 열렸으면 데이터는 문자열
스트림이 비 텍스트 모드로 열렸으면 데이터는 바이트열
결과 튜플에서 None이외의 값을 얻으려면 Popen 객체 생성 시,
stdout=PIPE, stderr=PIPE로 생성해야함

exceptiondescription
TimeoutExpiredtimeout내에 Popen instance가 종료되지 않았을 때 발생하는 익셉션
stdout=PIPEstderr=PIPE를 사용하고 자식 프로세스가 파이프에
너무 많은 출력을 보내 OS 파이프 버퍼가 더 많은 데이터를
받아들일 때까지 기다리느라 블록하면 교착 상태에 빠진다.
파이프를 사용할 때 이를 피하려면 Popen.communicate()를 사용 해라.
이 예외를 잡고 통신을 다시 시도해도 출력이 손실되지 않는다.
시간제한이 만료되면 자식 프로세스를 죽이지 않는다.
올바르게 하려면 프로세스를 죽이고 통신을 완료해야한다.
  • 간단한 사용 예
cmd = "ps -al"
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
cmd_output, smd_stderr = p.communicate()
cmd_output = cmd_output.decode("utf-8")
cmd_stderr = cmd_stderr.decode("utf-8")
if p.returncode or cmd_stderr:
    # Error Catch

print(cmd_output)
print(std_err)

Popen.send_signal(signal)

  • 시그널 signal 을 자식에게 보낸다.
    • 프로세스가 완료되었으면 아무 것도 하지 않는다.

Popen.terminate()

  • 자식을 멈춘다.
    • POSIX에서 이 메서드는 SIGTERM을 보낸다.
    • Windows에서는 Win32함수 TerminatedProcess()를 보낸다.

Popen.kill()

  • 자식을 죽인다
    • POSIX에서 이 메서드는 SIGKILL을 보낸다.
    • Windows에서 kill()terminate()의 별칭

Popen.args

  • Popen으로전달된 args 인자 - 프로그램 인자의 시퀀스거나 단일 문자열

Popen.stdin

  • stdin=PIPE면 this attribute는 open()이 반환한 쓰기 가능한 스트림 객체
    • encoding, errors인자가 지정되었거나 universal_newlines 인자가 True이면, 스트림은 텍스트 스트림이고, 그렇지 않으면 바이트 스트림이다.
  • stdin!=PIPE이면, this attribute는 None

Popen.stdout

  • stdout=PIPE 이면, this attribute는 open()이 반환한 읽기 가능한 스트림 객체
    • 스트림에서 읽으면 자식 프로세스의 출력 제공
    • encoding, errors 인자가 지정되었거나 universal_newlines 인자가 True 이면, 스트림은 텍스트 스트림이고, 그렇지 않으면 바이트 스트림이다.
  • stdout!=PIPE면, this attribute는 None

Popen.stderr

  • stderr=PIPE 이면, this attribute는 open()이 반환한 읽기 가능한 스트림 객체이다.
    • 스트림에서 읽으면 자식 프로세스의 에러 출력을 제공한다.
    • encoing, errors 인자가 지정되었거나 universal_newlines 인자가 True이면, 스트림은 텍스트 스트림이고, 그렇지 않으면 바이트 스트림이다.
  • stderr!=PIPE 이면, this attribute는 None

다른 OS PIPE 버퍼가 차고 자식 프로세스를 blocking하는 것으로 인한 교착 상태를 피하려면 .stdin.write, stdout.read, stderr.read 대신 communicate()를 사용하라


Popen.pid

  • 자식 프로세스 ID
  • Shell 인자를 True로 설정하면, 이것은 생성된 셸의 프로세스 ID

Popen.returncode

  • poll()wait() 그리고 communicate()에 의해 설정된 자식 반환 코드
  • None 값은 프로세스가 아직 종료되지 않았음을 나타냄
  • 음수 값 -N은 자식이 시그널 N에 의해 종료되었음 나타냄(POSIX)

사용 예

1. Bash shell 사용 커맨드


2. 여러 줄로 사용하기


subprocess.check_output


3. exception 사용

#!/usr/bin/env python3

import subprocess
import signal
import errno

def subprocess_setup():
    # Python installs a SIGPIPE handler by default. This is usually not what
    # non-Python subprocesses expect.
    signal.signal(signal.SIGPIPE, signal.SIG_DFL)

class CmdError(RuntimeError):
    def __init__(self, command, msg=None):
        self.command = command
        self.msg = msg
    
    def __str__(self):
        if not isinstance(self.command, str):
            cmd = subprocess.list2cmdline(self.command)
        else:
            cmd = self.command
        
        msg = "Execution of '%s' failed" % cmd
        if self.msg:
            msg += ': %s' % self.msg
        return msg

class NotFoundError(CmdError):
    def __str__(self):
        return CmdError.__str__(self) + ": command not found"

class ExecutionError(CmdError):
    def __init__(self, command, exitcode, stdout = None, stderr = None):
        CmdError.__init__(self, command)
        self.exitcode = exitcode
        self.stdout = stdout
        self.stderr = stderr
        self.extra_message = None

    def __str__(self):
        message = ""
        if self.stderr:
            message += self.stderr
        if self.stdout:
            message += self.stdout
        if message:
            message = ":\n" + message
        return (CmdError.__str__(self) +
                " with exit code %s" % self.exitcode + message + (self.extra_message or ""))

class Popen(subprocess.Popen):
    defaults = {
        "close_fds": True,
        "preexec_fn": subprocess_setup,
        "stdout": subprocess.PIPE,
        "stderr": subprocess.PIPE,
        "stdin": subprocess.PIPE,
        "shell": False,
    }

    def __init__(self, *args, **kwargs):
        options = dict(self.defaults)
        options.update(kwargs)
        subprocess.Popen.__init__(self, *args, **options)

def run(cmd, input=None, **options):
    if isinstance(cmd, str) and not "shell" in options:
        options["shell"] = True
    
    try:
        pipe = Popen(cmd, **options)
    except OSError as exc:
        if exc.errno == 2:
            raise NotFoundError(cmd)
        else:
            raise CmdError(cmd, exc)
    
    stdout, stderr = pipe.communicate(input)
    if not stdout is None:
        stdout = stdout.decode("utf-8")
    if not stderr is None:
        stderr = stderr.decode("utf-8")

    if pipe.returncode != 0:
        raise ExecutionError(cmd, pipe.returncode, stdout, stderr)
    return stdout, stderr

if __name__ == '__main__':
    stdout, stderr = run('echo hello')
    print(stdout)
    print(stderr)

    try: 
        stdout, stderr = run('dummy')
        print(stdout)
        print(stderr)
    except Exception as exc:
        print(exc)

        

environ 활용

os.environ 참고

import os

print(os.environ) 
# os.environ에서 Dictionary 형태로 'Key': Value, 'Key': [Value1, Value2...] 로 되어있음
  • 보통 export PATH=/update/dir:$PATH 와 똑같은 짓을 하려면
import os

existing_PATH=os.getenv("PATH")	# 기존의 $PATH 값을 가져온다.

new_path="/update/dir:"+existing_path	# /update/dir:$PATH

os.environ["PATH"] = new_path	

print(os.getenv("PATH")) 	# 확인

profile
pllpokko@alumni.kaist.ac.kr

0개의 댓글