001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.fukurou.util;
017
018import java.io.BufferedReader;
019import java.io.InputStream;
020import java.io.InputStreamReader;
021import java.io.File;
022import java.io.IOException;
023// import java.text.DateFormat;
024// import java.text.SimpleDateFormat;
025// import java.util.Date;
026// import java.util.Locale;
027
028/**
029 * Shell は、Runtime.exec の簡易的に実行するクラスです。
030 * 複雑な処理は通常の Runtime.exec を使用する必要がありますが,ほとんどの
031 * プロセス実行については、このクラスで十分であると考えています。
032 *
033 * このクラスでは、OS(特にWindows)でのバッチファイルの実行において、
034 * OS自動認識を行い、簡易的なコマンドをセットするだけで実行できるように
035 * しています。
036 *
037 * @version  4.0
038 * @author   Kazuhiko Hasegawa
039 * @since    JDK5.0,
040 */
041public class Shell {
042        /** Shell オブジェクトの状態を表します。正常  {@value} */
043        public static final int OK      = 0;            // 0:正常
044        /** Shell オブジェクトの状態を表します。実行中  {@value} */
045        public static final int RUNNING = 1;            // 1:実行中
046        /** Shell オブジェクトの状態を表します。取消  {@value} */
047        public static final int CANCEL  = 9;            // 9:取消
048        /** Shell オブジェクトの状態を表します。異常終了(負)  {@value} */
049        public static final int ERROR   = -1;           // -1:異常終了(負)
050
051        // private static final String CMD_95  = "C:\\windows\\command.com /c ";
052        private static final String CMD_NT  = "C:\\WINNT\\system32\\cmd.exe /c ";
053        private static final String CMD_XP  = "C:\\WINDOWS\\system32\\cmd.exe /c ";
054        private static final String OS_NAME = System.getProperty("os.name");
055        private static final String CR      = System.getProperty("line.separator");
056        private String          command         = null;
057        private File            workDir         = null;
058        private String[]        envp            = null;
059        private boolean         isWait          = true;         // プロセスの終了を待つかどうか (デフォルト 待つ)
060        private Process         prcs            = null;
061        private ProcessReader pr1               = null;
062        private ProcessReader pr2               = null;
063        private int     rtnCode                 = ERROR;        // 0:正常  1:実行中  9:取消  -1:異常終了(負)
064//      private final DateFormat formatter = new SimpleDateFormat( "yyyy/MM/dd HH:mm:ss",Locale.JAPAN );
065
066        // 3.6.1.0 (2005/01/05) タイムアウト時間を設定
067        private long timeout                    = 0 ;   // 初期値は、タイムアウトなし
068
069        // 3.8.9.2 (2007/07/13) Windows Vista対応
070        // 5.6.7.1 (2013/07/09) NTでもunknown時はCMD_XPとする
071        private static final String CMD_COM ;
072        static {
073                if( (OS_NAME.indexOf( "NT" ) >= 0 ||
074                                OS_NAME.indexOf( "2000" ) >= 0)
075                        && OS_NAME.indexOf( "unknown" ) < 0 ) {
076                                CMD_COM = CMD_NT ;
077                }
078        //      else if( OS_NAME.indexOf( "XP" ) >= 0 ||
079        //                       OS_NAME.indexOf( "2003" ) >= 0
080        //                       OS_NAME.indexOf( "Vista" ) >= 0 ) {
081        //                      CMD_COM = CMD_XP ;
082        //      }
083                else {
084                        CMD_COM = CMD_XP ;
085                }
086        }
087
088        /**
089         * プロセスを実行する時に引き渡すコマンド
090         * 第2引数には、コマンドがBATかEXEかを指定できます。
091         * true の場合は,バッチコマンドとして処理されます。
092         *
093         * @og.rev 3.3.3.0 (2003/07/09) Windows XP 対応
094         * @og.rev 3.7.0.1 (2005/01/31) Windows 2003 対応, Windows 95 除外
095         * @og.rev 3.8.9.2 (2007/07/13) Windows Vista 対応
096         *
097         * @param       cmd     コマンド
098         * @param       batch   true:バッチファイル/false:EXEファイル
099         */
100        public void setCommand( final String cmd,final boolean batch ) {
101                if( batch ) {
102                        command = CMD_COM + cmd;
103                }
104                else {
105                        command = cmd ;
106                }
107        }
108
109        /**
110         * プロセスを実行する時に引き渡すコマンド
111         *
112         * @param   cmd EXEコマンド
113         */
114        public void setCommand( final String cmd ) {
115                setCommand( cmd,false );
116        }
117
118        /**
119         * プロセスの実行処理の終了を待つかどうか
120         *
121         * @param       flag    true:待つ(デフォルト)/ false:待たない
122         */
123        public void setWait( final boolean flag ) {
124                isWait = flag;
125        }
126
127        /**
128         * プロセスの実行処理のタイムアウトを設定します。
129         * ゼロ(0) の場合は、割り込みが入るまで待ちつづけます。
130         *
131         * @param       tout    タイムアウト時間(秒) ゼロは、無制限
132         *
133         */
134        public void setTimeout( final int tout ) {
135                timeout = (long)tout * 1000;
136        }
137
138        /**
139         * 作業ディレクトリを指定します。
140         *
141         * シェルを実行する、作業ディレクトリを指定します。
142         * 指定しない場合は、このJava仮想マシンの作業ディレクトリで実行されます。
143         *
144         * @param   dir 作業ディレクトリ
145         */
146        public void setWorkDir( final File dir ) {
147                workDir = dir;
148        }
149
150        /**
151         * 環境変数設定の配列指定します。
152         *
153         * 環境変数を、name=value という形式で、文字列配列で指定します。
154         * null の場合は、現在のプロセスの環境設定を継承します。
155         *
156         * @param   env 文字列の配列。配列の各要素は、name=value という形式で環境変数設定を保持する。
157         */
158        public void setEnvP( final String[] env ) {
159                if( env != null && env.length > 0 ) {
160                        int size = env.length;
161                        envp = new String[size];
162                        System.arraycopy( env,0,envp,0,size );
163                }
164                else {
165                        envp = null;
166                }
167        }
168
169        /**
170         * プロセスの実行処理
171         *
172         * @return  サブプロセスの終了コードを返します。0 は正常終了を示す
173         */
174        public int exec() {
175                Runtime rt = Runtime.getRuntime();
176                Thread wait = null;
177                try {
178                        prcs = rt.exec( command,envp,workDir );         // 3.3.3.0 (2003/07/09)
179                        pr1 = new ProcessReader( prcs.getInputStream() );
180                        pr1.start();
181                        pr2 = new ProcessReader( prcs.getErrorStream() );
182                        pr2.start();
183
184                        if( isWait ) {
185                                // 3.6.1.0 (2005/01/05)
186                                wait = new WaitJoin( timeout,prcs );
187                                wait.start();
188                                rtnCode = prcs.waitFor();
189                                if( rtnCode > OK ) { rtnCode = -rtnCode; }
190                        }
191                        else {
192                                rtnCode = RUNNING;      // プロセスの終了を待たないので、1:処理中 を返します。
193                        }
194                }
195                catch(IOException ex) {
196                        String errMsg = "入出力エラーが発生しました。";
197                        LogWriter.log( errMsg );
198                        LogWriter.log( ex );
199                }
200                catch(InterruptedException ex) {
201                        String errMsg = "現在のスレッドが待機中にほかのスレッドによって強制終了されました。";
202                        LogWriter.log( errMsg );
203                        LogWriter.log( ex );
204                }
205                finally {
206                        if( wait != null ) { wait.interrupt(); }
207                }
208
209                return rtnCode;
210        }
211
212        /**
213         * プロセスの実行時の標準出力を取得します。
214         *
215         * @return 実行時の標準出力文字列
216         */
217        public String getStdoutData() {
218                final String rtn ;
219                if( pr1 == null ) {
220                        rtn = "\n.......... Process is not Running. ....";
221                }
222                else if( pr1.isEnd() ) {
223                        rtn = pr1.getString();
224                }
225                else {
226                        rtn = pr1.getString() + "\n......... stdout Process is under execution. ...";
227                }
228                return rtn ;
229        }
230
231        /**
232         * プロセスの実行時のエラー出力を取得します。
233         *
234         * @return 実行時の標準出力文字列
235         */
236        public String getStderrData() {
237                final String rtn ;
238                if( pr2 == null ) {
239                        rtn = "\n.......... Process is not Running. ....";
240                }
241                else if( pr2.isEnd() ) {
242                        rtn = pr2.getString();
243                }
244                else {
245                        rtn = pr2.getString() + "\n......... stderr Process is under execution. ...";
246                }
247                return rtn ;
248        }
249
250        /**
251         * プロセスが実際に実行するコマンドを取得します。
252         * バッチコマンドかどうかで、実行されるコマンドが異なりますので、
253         * ここで取得して確認することができます。
254         * 主にデバッグ用途です。
255         *
256         * @return 実行時の標準出力文字列
257         */
258        public String getCommand() {
259                return command;
260        }
261
262        /**
263         * サブプロセスを終了します。
264         * この Process オブジェクトが表すサブプロセスは強制終了されます。
265         *
266         */
267        public void destroy() {
268                if( prcs != null ) { prcs.destroy() ; }
269                rtnCode = CANCEL;
270        }
271
272        /**
273         * プロセスが終了しているかどうか[true/false]を確認します。
274         * この Process オブジェクトが表すサブプロセスは強制終了されます。
275         *
276         * @return      プロセスが終了しているかどうか[true/false]
277         */
278        public boolean isEnd() {
279                boolean flag = true;
280                if( rtnCode == RUNNING ) {
281                        flag = pr1.isEnd() && pr2.isEnd() ;
282                        if( flag ) { rtnCode = OK; }
283                }
284                return flag ;
285        }
286
287        /**
288         * サブプロセスの終了コードを返します。
289         *
290         * @return この Process オブジェクトが表すサブプロセスの終了コード。0 は正常終了を示す
291         * @throws  IllegalThreadStateException この Process オブジェクトが表すサブプロセスがまだ終了していない場合
292         */
293        public int exitValue() {
294                if( rtnCode == RUNNING && isEnd() ) {
295                        rtnCode = prcs.exitValue();
296                        if( rtnCode > OK ) { rtnCode = -rtnCode ; }
297                }
298                return rtnCode;
299        }
300
301        /**
302         * この Shell のインフォメーション(情報)を出力します。
303         * コマンド、開始時刻、終了時刻、状態(実行中、終了)などの情報を、
304         * 出力します。
305         *
306         * @og.rev 5.5.7.2 (2012/10/09) HybsDateUtil を利用するように修正します。
307         *
308         * @return      インフォメーション(情報)
309         */
310        @Override
311        public String toString() {
312                boolean isEnd = isEnd() ;
313//              String st = formatter.format( new Date( pr1.getStartTime() ) ) ;
314//              String ed = ( isEnd ) ? formatter.format( new Date( pr1.getEndTime() ) ) : "----/--/-- --:--:--" ;
315                String st = HybsDateUtil.getDate( pr1.getStartTime() , "yyyy/MM/dd HH:mm:ss" ) ;
316                String ed = ( isEnd ) ? HybsDateUtil.getDate( pr1.getEndTime() , "yyyy/MM/dd HH:mm:ss" ) : "----/--/-- --:--:--" ;
317
318                StringBuilder buf = new StringBuilder();
319                buf.append( "command     = [" ).append( getCommand() ).append( "]\n" );
320                buf.append( "  isEnd     = [" ).append( isEnd        ).append( "]\n" );
321                buf.append( "  rtnCode   = [" ).append( exitValue()  ).append( "]\n" );
322                buf.append( "  startTime = [" ).append( st           ).append( "]\n" );
323                buf.append( "  endTime   = [" ).append( ed           ).append( "]\n" );
324
325                return buf.toString();
326        }
327
328        /**
329         * stdout と stderr の取得をスレッド化する為のインナークラスです。
330         * これ自身が、Thread の サブクラスになっています。
331         *
332         * @version  4.0
333         * @author   Kazuhiko Hasegawa
334         * @since    JDK5.0,
335         */
336        static class ProcessReader extends Thread {
337                private final BufferedReader in ;
338                private final StringBuilder inStream = new StringBuilder();
339                private boolean  endFlag = false;
340                private long    startTime       = -1;
341                private long    endTime         = -1;
342
343                /**
344                 * コンストラクター。
345                 *
346                 * ここで、スレッド化したい入力ストリームを引数に、オブジェクトを生成します。
347                 *
348                 * @param ins InputStream 入力ストリーム
349                 *
350                 */
351                ProcessReader( InputStream ins ) {
352//                      in = new BufferedReader( new InputStreamReader(ins) );
353                        in = new BufferedReader( new InputStreamReader(ins,StringUtil.DEFAULT_CHARSET) );       // 5.5.2.6 (2012/05/25) findbugs対応
354                        setDaemon( true );              // 3.5.4.6 (2004/01/30)
355                }
356
357                /**
358                 * Thread が実行された場合に呼び出される、run メソッドです。
359                 *
360                 * Thread のサブクラスは、このメソッドをオーバーライドしなければなりません。
361                 *
362                 */
363                public void run() {
364                        startTime = System.currentTimeMillis() ;
365                        String outline;
366                        try {
367                                while ((outline = in.readLine()) != null) {
368                                        inStream.append( outline );
369                                        inStream.append( CR );
370                                }
371                        }
372                        catch(IOException ex) {
373                                String errMsg = "入出力エラーが発生しました。";
374                                LogWriter.log( errMsg );
375                                LogWriter.log( ex );
376                        }
377                        finally {
378                                Closer.ioClose( in );
379                        }
380                        endTime = System.currentTimeMillis() ;
381                        endFlag = true;
382                }
383
384                /**
385                 * 現在書き込みが行われているストリームを文字列にして返します。
386                 *
387                 * @return      ストリームの文字列
388                 *
389                 */
390                public String getString() {
391                        return inStream.toString();
392                }
393
394                /**
395                 * ストリームからの読取が終了しているか確認します。
396                 *
397                 * @return      読取終了(true) / 読み取り中(false)
398                 *
399                 */
400                public boolean isEnd() {
401                        return endFlag;
402                }
403
404                /**
405                 * ストリーム処理の開始時刻を返します。
406                 * 開始していない状態は、-1 を返します。
407                 *
408                 * @return      開始時刻
409                 *
410                 */
411                public long getStartTime() {
412                        return startTime;
413                }
414
415                /**
416                 * ストリーム処理の終了時刻を返します。
417                 * 終了していない状態は、-1 を返します。
418                 *
419                 * @return      終了時刻
420                 *
421                 */
422                public long getEndTime() {
423                        return endTime;
424                }
425        }
426
427        /**
428         * スレッドのウェイト処理クラス
429         * 指定のタイムアウト時間が来ると、設定されたプロセスを、強制終了(destroy)します。
430         * 指定のプロセス側は、処理が終了した場合は、このThreadに、割り込み(interrupt)
431         * をかけて、この処理そのものを終了させてください。
432         *
433         * @version  4.0
434         * @author   Kazuhiko Hasegawa
435         * @since    JDK5.0,
436         */
437        static class WaitJoin extends Thread {
438                private static final long MAX_WAIT = 3600 * 1000 ;      // 1時間に設定
439
440                private final long wait ;
441                private final Process prcs;
442
443                /**
444                 * コンストラクター
445                 *
446                 * @param wait long ウェイトする時間(ミリ秒)
447                 * @param prcs Process 強制終了(destroy) させるプロセス
448                 */
449                WaitJoin( final long wait,Process prcs ) {
450                        this.wait = ( wait > 0L ) ? wait : MAX_WAIT ;
451                        this.prcs = prcs;
452                }
453
454                /**
455                 * Thread の run() メソッド
456                 * コンストラクタで指定のミリ秒だけウェイトし、それが経過すると、
457                 * 指定のプロセスを強制終了(destroy)させます。
458                 * 外部より割り込み(interrupt)があると、ウェイト状態から復帰します。
459                 * 先に割り込みが入っている場合は、wait せずに抜けます。
460                 *
461                 * @og.rev 5.4.2.2 (2011/12/14) Threadでwaitをかける場合、synchronized しないとエラーになる 対応
462                 */
463                public void run() {
464                        try {
465                                long startTime = System.currentTimeMillis() ;
466                                boolean waitFlag = true;
467                                synchronized( this ) {
468                                        while( ! isInterrupted() && waitFlag ) {
469                                                wait( wait );
470                                                waitFlag = ( startTime + wait ) > System.currentTimeMillis() ;
471                                        }
472                                }
473                                prcs.destroy() ;
474                                System.out.println( "タイムアウトにより強制終了しました。" );
475                        }
476                        catch( InterruptedException ex ) {
477                                LogWriter.log( "終了しました。" );
478                        }
479                }
480        }
481}