Java Process waitFor(), Hang or Deadlock

  • Java, process.waitFor() not return

  • Java, process.waitFor() not working

  • How to using Java RunTime exec() method

  • Runtime execute hangs / deadlock

자바에서 외부 프로세스를 실행할 때 위와 같은 이슈가 발생했다. 자바에서 외부 프로세스를 실행시키는 경우 아래와 같은 클래스를 이용하며, 아래의 내용을 작성한다.


Process process = Runtime.getRuntime().exec("System Command Line");


import java.lang.Runtime;


Class Runtime

public static Runtime getRuntime() 


(이하 생략)


getRuntimer()

자바 애플리케이션과 관련한 런타임 개체를 반환하는 메소드이다. 런타임 클래스의 대부분 메소드는 인스턴스로서의 메소드이다.


☞ exec(String command) throws IOException

☞ exec(String [] commandArray) throws IOException
지정된 문자열 명령을 토대로 프로세스를 실행하는 메소드이다. 다양한 파라미터 셋으로 오버로딩되어있다. ( String[] commandArray 같은 경우에는 명령어 공백을 무조건 잘라서 각각의 배열 요소로 넣어주어야 한다. )

e.g. >> String [] commandArray = new String[]{"cmd.exe", "/C", "dir"};

e.g. >> String command = new String("cmd.exe /C dir"); 


☞ exec(String command, String[] envp)  throws IOException

지정된 문자열 명령을 토대로 프로세스를 수행한다. 두번째 파라미터의 경우, 하위 프로세스가 현재 프로세스 환경이 아닌 다른 프로세스 환경을 상속하는 경우는 설정해주어야 하고, 현재 프로세스 환경을 상속받아야 하는 경우 null 로 설정한다. 


☞ exec(String command, String[] envp, File file)  throws IOException

세번째 파라미터의 경우, 하위 프로세스가 현재 프로세스의 작업 디렉토리를 상속받는 경우 null 을 설정해주지만, 현재 프로세스의 작업 디렉토리가 아닌 따로 작업 디렉토리의 설정이 필요한 경우에는 디렉토리 설정이 필요하다.


런타임 클래스를 통해서 프로세스 객체를 획득할 수 있다. 그리고 획득한 프로세스 객체를 통해서 자바 외부 프로그램을 실행시킬 수 있다.


echo 명령어 및 파일 생성 명령어 (cmd)

echo 명령어를 통해 텍스트를 생성하고, 그 텍스트를 txt 파일에 넣어 파일을 생성한다. 아래의 명령어를 실행하면, E:\ 드라이브에 Hello World 라는 텍스트 파일이 생기고 그 내부 내용에는 Hello World 라는 내용이 출력되어 있다.

1
E:\>ECHO Hello World > "Hello World !.txt";
cs


echo 명령어 및 파일 생성 (Java external process)

자바에서 런타임 클래스를 이용하여 외부 프로세스를 실행시킨다. 명령어는 동일하다. 아래의 내용을 실행하면 정상적으로 수행하는 것을 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.File;
import java.io.IOException;
 
public class ProcessTester {
    
    public static void main(String[]args) {
        runningProcess();
    }
        
    private static void runningProcess() {
        String cmd = "cmd.exe /C ECHO Hello World > \"Hello World !.txt\"";
        File workingDirectory = new File("E:");
        Process process = null;
        
        try {
            
            process = Runtime.getRuntime().exec(cmd, null, workingDirectory);
            process.waitFor();
            
        } 
        catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
cs


ISSUE

위의 간단한 명령어의 경우에는 무사히 되지만 만약에 프로세스에서 스트림을 받고 이를 프로세스 실행과 동시에 처리하지 않으면 서브 프로세스는 멈추거나 데드락에 빠질 수 있다. 결국 위의 18번째 줄에 있는 waitFor() 이 정상적으로 수행을 하지 못하게 된다. 


waitFor() 메소드에 대한 api 문서 원본

Causes the current thread to wait, if necessary, until the process represented by this Process object has terminated. This method returns immediately if the subprocess has already terminated. If the subprocess has not yet terminated, the calling thread will be blocked until the subprocess exits.


번역기를 돌려서 확인하면, 하위 프로세스가 종료될 때까지 현재 스레드는 계속 기다리는 상태가 된다. 하위 프로세스가 종료되지 않은 경우에는 하위 프로세스가 종료될 때까지 호출한 스레드가 차단된다. 


이러한 문제점 때문에 많은 사람들이 하위 프로세스를 호출하면서 데드락 혹은 행 현상을 겪고있다. 그러면 이러한 문제점은 어떻게 해결할 것인가. 결국 스트림을 잘 다루어주어야 한다.


프로세스에는 세 개의 스트림이 존재한다.

☞ process.getInputStream()

☞ process.getErrorStream()

☞ process.getOutputStream()


위 세 개의 스트림에 대해 각각 별도의 스레드를 만들어주고 프로세스 실행과 동시에 스레드를 수행시킴으로써 프로세스를 실행시킴으로써 겪게되는 데드락 및 행 문제를 해결할 수 있다.


ProcessTester.java

기존의 코드에서 별도의 스레드를 수행시키기 위해 각각의 스트림 객체를 획득하도록 하였다. 스트림 내용이 필요하지 않다면 26줄과 27줄에서 단순 객체를 획득하고 .close() 메소드를 시켜주어도 상관없다. 하지만 내용을 확인해야 한다면 그대로 두면 된다. 


추가적으로 외부 프로세스에 대해서 사용자의 입력을 받아야 한다면 getOutputStream() 메소드에 대한 내용도 별도로 만들어주어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.io.File;
import java.io.IOException;
 
public class ProcessTester {
    
    private static final String STD_IN = "stdin";
    private static final String STD_ERR = "stderr";
    
    public static void main(String[]args) {
        runningProcess();
    }
        
    private static void runningProcess() {
        String cmd = "cmd.exe /C dir";
        File workingDirectory = new File("E:");
        Process process = null;
        ProcessStream processInStream = null;
        ProcessStream processErrStream = null;
 
        try {
 
            process = Runtime.getRuntime().exec(cmd, null, workingDirectory);
            processInStream = new ProcessStream(STD_IN, process.getInputStream());
            processErrStream = new ProcessStream(STD_ERR, process.getErrorStream());
 
            processInStream.start();
            processErrStream.start();
            process.getOutputStream().close();
            
            process.waitFor();
            
        } 
        catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}
cs


ProcessStream.java

스트림에 대한 스레드 클래스이다. while(true) 를 통해서 프로세스로부터 얻어오는 스트림을 받아오고 있다. 따로 내용이 없으면 break 문을 통해 해당 구문을 끝낸다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
 
public class ProcessStream implements Runnable{
    
    private static final String EUC_KR = "euc-kr";
    private String name;
    private InputStream inputStream;
    private Thread thread;
    
    public ProcessStream(String name, InputStream inputStream) {
        this.name = name;
        this.inputStream = inputStream;
    }
    
    public void start() {
        this.thread = new Thread(this);
        thread.start();
    }
 
    @Override
    public void run() {
        InputStreamReader isr = null;
        BufferedReader br = null;
        String lines = "";
        
        try {
            isr = new InputStreamReader(inputStream, EUC_KR);
            br = new BufferedReader(isr);
            
            while(true) {
                String line = br.readLine();
                
                if(line == null)
                    break;
                
                lines += line;
                lines += "\n";
            }
            
            if(!lines.equals("")) {
                System.out.println("[" + name + "]");
                System.out.println(lines);
            }
        }// try
        
        catch(IOException e) {
            e.printStackTrace();
        }// catch
        
        finally{
            try {
                if(br != null)
                    br.close();
                
                if(inputStream != null)
                    inputStream.close();
            }
            catch(IOException e) {
                e.printStackTrace();
            }
        }// finally
        
    }
}
cs


+) 추가적으로 이와 관련된 문제 말고도 메모리 할당 문제가 존재하는 것 같다. 실제 겪어보지 않았기 때문에 내용은 추가하지 않았지만 언젠가 겪게된다면 따로 게시글을 작성할 생각이다. 그리고 단순 Process Class 가 아닌 ProcessBuilder Class 를 사용하는 경우도 있다. 네이버 D2 기술블로그를 보면 자세히 설명되어 있다. 



Posted by doubler
,