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