1. 程式人生 > >Java程序呼叫外部shell指令碼

Java程序呼叫外部shell指令碼

原文連結: http://freewind886.blog.163.com/blog/static/66192464201261462759238/

由於使用ProcessBuilder 發生了阻塞 ,根據方法4搞定,記錄下!

前段時間實現一個小功能,在長時間執行的管理伺服器master(Java程序)上增加一種呼叫shell指令碼傳送報警的方式(已有郵件和簡訊報警)。指令碼名稱和相對路徑固定,每傳送一次報警master就會呼叫一次指令碼(可能會很頻繁),報警內容是JSON格式的訊息,以$1引數傳入指令碼。使用者可以自定義shell指令碼的內容,例如再呼叫python指令碼將報警內容傳送到指定的伺服器。master只負責呼叫指令碼傳入報警資訊,確保執行指令碼的執行緒能合理退出,如果有異常也要列印錯誤日誌方便問題排查。

方案1

classExecuteThreadextendsThread{ privateString cmdString;

ExecuteThread(String cmd){ this.cmdString = cmd }

publicvoid run(){ String[] cmdArray ={"/bin/bash","-c", cmdString }; ProcessBuilder builder =newProcessBuilder(cmdArray);         process = builder.start();

// 獲取錯誤輸出 InputStream stderr = process

.getErrorStream();

// 使用Reader進行輸入讀取和列印 InputStreamReader isr =newInputStreamReader(stderr); BufferedReader br =newBufferedReader(isr); String line =null; System.out.println("<ERROR>"); while((line = br.readLine())!=null) System.out.println(line);

// 獲取執行返回值 int exitCode = process.waitFor(); if(exitCode

!=0){ // 進行錯誤處理 } } }

方案1的問題在於當指令碼本身有問題導致執行時間過長時,整個操作就會在讀取輸出的地方卡住(br.readLine()),整個執行外部指令碼操作的執行緒就會卡住無法退出。

classExecuteThreadextendsThread{ privateString cmdString;

ExecuteThread(String cmd){ this.cmdString = cmd }

publicvoid run(){ String[] cmdArray ={"/bin/bash","-c", cmdString }; ProcessBuilder builder =newProcessBuilder(cmdArray);         process = builder.start();

// 獲取錯誤輸出 InputStream stderr = process.getErrorStream();

// 使用StreamGobbler進行輸入讀取和列印 newStreamGobbler(stderr).start();

// 獲取執行返回值 int exitCode = process.waitFor(); if(exitCode !=0){ // 進行錯誤處理 } } }

classStreamGobblerextendsThread{ privateInputStream input;

publicStreamGobbler(InputStream input){ this.input = input; }

publicvoid run(){ InputStreamReader isr =newInputStreamReader(input); BufferedReader br =newBufferedReader(isr); String line =null; System.out.println("<ERROR>"); while((line = br.readLine())!=null) System.out.println(line); } }

對於指令碼執行超時的問題,方案2沒有解決,執行輸出讀取的StreamGobbler執行緒還是會因為readLine()卡住,而ExecuteThread會由於process.waitFor()而卡住。
如果每呼叫一次指令碼就多出2個無法退出的執行緒,那master遲早會因為資源耗盡而崩潰。

方案3
對於Process.waitFor()的阻塞,可以呼叫Process.destroy()解除,而對於readLine()的阻塞,則嘗試使用Reader.close或InputStream.close()來解除。

classExecuteThreadextendsThread{ // 增加close()方法,外部判定任務執行超時後進行資源清理 // 其他部分程式碼不變 publicvoid close(){         process.destroy();         stderr.close(); } }

// 使用ExecuteThread的靜態方法 staticvoid executeCmd(String cmd){ ExecuteThread execThread =newExecuteThread(cmd).start(); try{         execThread.join(timeoutInMillis); }finally{         execThread.close(); } }

針對方案3進行了多次的測試,Process.destroy()可以解除Process.waitFor()的阻塞,而stderr.close()卻無法讓阻塞的readLine()中斷退出。也就是說,當用戶寫了個有問題的指令碼,每次都執行很長一段時間甚至不退出,那麼每傳送一次報警就多1個阻塞的StreamGobbler執行緒,方案2中存在的問題沒有解決。

方案4
在這個執行緒執行緒退出的問題上,也嘗試了不少的方案,最後找到一個比較醜的辦法,在sun jdk1.5和1.6上測試通過。

classExecuteThreadextendsThread{ privateString cmdString; privatevolatileProcess process; privatevolatileFileChannel inputChannel;

ExecuteThread(String cmd){ this.cmdString = cmd }

publicvoid run(){ String[] cmdArray ={"/bin/bash","-c", cmdString }; ProcessBuilder builder =newProcessBuilder(cmdArray);         process = builder.start();

// 獲取指令碼錯誤輸出 InputStream errorStream = process.getErrorStream(); if(errorStream instanceofFileInputStream){         inputChannel =((FileInputStream) errorStream).getChannel(); }else{ thrownewException("無法將指令碼子程序的輸出流轉為管道"); }

// inputChannel.read(buffer)會因為inputChannel的關閉而退出,不會一直阻塞; /* 以下的處理也可以用另一個執行緒來執行,這裡放在同一個執行緒是使得在呼叫指令碼出錯(例如指令碼檔案被誤刪除)         迅速退出的情況下也能獲取到相應的錯誤資訊,避免ExecuteThread比讀取輸出執行緒結束得更快。

*/ ByteArrayOutputStream byteArrayStream =newByteArrayOutputStream(32); ByteBuffer buffer =ByteBuffer.allocate(32); WritableByteChannel channelOut =Channels.newChannel(byteArrayStream); try{ while(inputChannel.read(buffer)>-1){               buffer.flip();              channelOut.write(buffer);              buffer.clear(); } }catch(IOException ioe){ // 當指令碼執行超時,由於channel的關閉必然會丟擲異常 }

// 獲取執行返回值 int exitCode = process.waitFor(); if(exitCode !=0){ // 進行錯誤處理 } }

publicvoid close(){         process.destroy();         inputChannel.close(); } }