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}