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