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