2017年 6月

Java SE 9 の予習 その5

Java

Java SE 9 の予習 その5です。

今回は地味にアップデートを積み重ねている ProcessAPI をゴニョゴニョしてみました。

JDK 9 でどのようアップデートされたか試してみます。

まずプロセス ID が直感的に取得できるようになってます。

今までは次のような解りにくいコードを書かなければいけませんでした。

それが JDK 9 では ProcessHandle インターフェイスが用意され便利に使えるファクトリメソッドあるので次のように優しいコードで書くことができるようになりました。

実行中のプロセスIDとコマンドを取得するにはつぎのようなシンプルなコードになります。

static Stream<ProcessHandle> allProcesses​() メソッドで現在のプロセスから見えるすべてのプロセスのスナップショットを返します。

ただし、これは非同期で処理されるため注意が必要です。

スナップショットを作成したあとにプロセスの開始、終了などが問題になる場合も考えられます。

このコードの実行結果は次のようになります。(長いので途中省略してます。)

それでは JDK 9 から追加された public static interface ProcessHandle.Info インターフェイスについて調べてみましょう。

プロセスに関する情報のスナップショット。

プロセスの属性はオペレーティングシステムによって異なり、すべての実装で利用できるわけではありません。

プロセスに関する情報は、要求を行うプロセスのオペレーティングシステム権限によって制限されます。

もし値が利用可能であるなら、戻り型は明示的なテストとアクションを許す Optional<T> です。

ProcessHandle.Info info​() メソッドはプロセスに関する情報のスナップショットを返します。

ProcessHandle.Infoインスタンスには、利用可能なプロセスに関する情報を返すアクセサメソッドがあります。

Optional<String[]> arguments​() メソッドはプロセスの引数の文字列の配列を返します。(プロセスの引数の Optional<String[]>)

ただし、いくつかのプラットフォームでは、ネイティブアプリケーションは起動後に引数配列を自由に変更でき、このメソッドは変更された値のみを表示することがあります。

Optional<Instant> startInstant​() メソッドはプロセスの開始時刻を返します。(プロセスの開始時刻の Optional<Instant> です。)

Optional<Duration> totalCpuDuration​() メソッドはプロセスの累積合計 cputime を返します。(累積合計 cputime の Optional<Duration>)

Optional<String> user​() メソッドはプロセスのユーザーを返します。(プロセスのユーザーについての Optional<String>)

Optional<String> command​() メソッドはプロセスの実行可能パス名を返しています。(プロセスの実行可能パス名の Optional<String>)

Optional<String> commandLine​() メソッドはプロセスのコマンドラインを返します。(プロセスのコマンドラインの Optional<String>)

command() メソッドと arguments() メソッドが Optinal.empty でない Optional<String> を返す場合、これは単に空白で区切られた2つの関数の値を連結する便利なメソッドです。

それ以外の場合は、コマンドラインのベストエフォート、プラットフォーム依存の表現を返します。

ただし、システムの制限により、返された実行可能パス名と引数が一部のプラットフォームで切り捨てられることがあります。

実行可能なパス名には、完全なパス情報がない実行可能ファイルの名前のみが含まれている場合があります。

空白が異なる引数を区切っているのか、単一の引数の一部なのかは決まりません。

以上、ProcessHandle.Info の便利なアクセサメソッドを調べてみました。

今回のプログラムでは全て試してないのですが Windows だと情報取得できるけど Linux (Ubunts) だと取れないとかありました。

でも JDK 9 での ProcessAPI はこれだけでも開発者に優しくなったと思います。

それではいろいろゴニョゴニョしていきましょう。

次に任意のプロセス情報の取得を試してみます。

static Optional<ProcessHandle> of​(long pid) メソッドで Optional<ProcessHandle> を返します。

引数は当然プロセス ID です。

プロセスが存在しない場合は、 empty を返します。

このコードの実行結果は次のようになります。

次はカレントプロセスの情報を取得してみましょう。

static ProcessHandle current​() メソッドで現在のプロセスの ProcessHandle を返します。

API ドキュメントに次のような注意書きがあります。

ProcessHandle を使用して現在のプロセスを破棄することはできません。

代わりに System.exit を使用してください。

実行結果は次のようになります。

それでは子プロセス情報の取得を試してみます。

エディタを二つ起動させてみました。

Stream<ProcessHandle> children​() メソッドでカレントプロセスの直接の子のスナップショットを返します。

プロセスの直接のチルドレンであるプロセスの ProcessHandles の連続的なストリームを返すと言うことです。

注意すべきはプロセスが作られて、そして非同期に終わること。 プロセスが生きているという保証がありません。

このコードの実行結果は次のようになります。

次はプロセスの終了処理を試してみます。

先ほど起動したエディタを終了させてみましょう。

プロセスの終了処理にpublic abstract void destroy​() メソッドと public Process destroyForcibly​() メソッドをそれぞれ使ってみました。

public abstract void destroy​() メソッドはプロセスを終了させます。

このProcessオブジェクトが表す処理が正常に終了したか否かは実装依存です。

強制的なプロセス破棄がプロセスの即時終了と定義されるのに対して、正常終了はプロセスが正常に終了することを可能にします。

プロセスが生存していない場合、何も実行されません。

プロセスが終了すると、onExit() メソッドの CompletableFuture が完了します。

56行目の geditProcess1.waitFor(); の終了待ちがないと public boolean isAlive​() メソッドが true を返す場合があります。

JDK 9 からは public CompletableFuture<Process> onExit​() メソッドを使うのが良いのかもしれません。

シンプルに public abstract int waitFor​() メソッドを使って対応してしまいました。(^_^;

public Process destroyForcibly​() メソッドもプロセスを終了させます。

ただし、このプロセスオブジェクトによって表されたプロセスは強制的に終了させられます。

ProcessBuilder.start()およびRuntime.exec(java.lang.String)によって返されたProcessオブジェクトでこのメソッドを呼び出すと、強制的にプロセスが終了します。

プロセスが終了すると、onExit()のCompletableFutureが完了します。

プロセスはすぐに終了しないことがあります。つまり、isAlive() メソッドは destroyForcibly() メソッドが呼び出された後、短時間の間 true を返すことがあります。

こちらも waitFor​() メソッドを使って対応しました。

このコードの実行結果は次のようになります。

ここまで試してみて JDK 9 での ProcessAPI は OS に依存しないコードで美しく処理が可能となったようです。

次は public static List<Process> startPipeline(List<ProcessBuilder> builders) メソッドを使ったパイプライン処理をためしてみます。

先ず、ProcessBuilder クラスがどんなものか復習しておきましょう。

J2SE 5.0 (Tiger) から存在するクラスです。

このクラスは、オペレーティングシステムのプロセスを作成するために使用されます。

各 ProcessBuilder インスタンスは、プロセス属性のコレクションを管理します。

start() メソッドは、これらの属性を持つ新しい Process インスタンスを作成します。

同じインスタンスからstart() メソッドを繰り返し呼び出すと、同一または関連する属性を持つ新しいサブプロセスを作成できます。

public static List<Process> startPipeline(List<ProcessBuilder> builders) メソッドを呼び出して、各プロセスの出力を次のプロセスに直接送り新しいプロセスのパイプラインを作成することができます。

各プロセスにはそれぞれのProcessBuilderの属性があります。

各プロセスビルダは、次のプロセス属性を管理します。

  • コマンド、呼び出される外部プログラムファイルを示す文字列のリストとその引数(存在する場合)。
    有効なオペレーティング・システム・コマンドを表す文字列リストは、システムによって異なります。
    たとえば、各概念的な引数はこのリストの要素であるのが普通ですが、プログラムがコマンドライン文字列をトークン化することを期待されるオペレーティングシステムがあります。
    このようなシステムでは、Java実装では厳密に2つの要素を含む必要があります。
  • 環境変数は、変数から値へのシステム依存のマッピングです。
    初期値は、現在のプロセスの環境のコピーです。
  • 作業ディレクトリ。 デフォルト値は、現在のプロセスの現在の作業ディレクトリです。
    通常、システムプロパティ user.dir で指定されたディレクトリです。
  • 標準入力のソース。 デフォルトでは、サブプロセスはパイプからの入力を読み取ります。
    Java コードは、Process.getOutputStream() によって返される出力ストリームを介してこのパイプにアクセスできます。
    ただし、標準入力は redirectInput を使用して別のソースにリダイレクトされます。
    この場合、Process.getOutputStream() は null 出力ストリームを返します。
    • 書き込みメソッドは常に IOException をスローします。
    • close() メソッドは何もしない。
  • 標準出力と標準エラーの出力先です。 デフォルトでは、サブプロセスは標準出力と標準エラーをパイプに書き込みます。
    Javaコードは、Process.getOutputStream() メソッドおよびProcess.getErrorStream() メソッドによって返された入力ストリームを介してこれらのパイプにアクセスできます。
    ただし、標準出力と標準エラーは、redirectOutput と redirectError を使用して他の宛先にリダイレクトされます。
    この場合、Process.getInputStream() メソッドまたは Process.getErrorStream() メソッドは null 入力ストリームを返します。
    • 読み込みメソッドは常に-1を返します
    • 使用可能なメソッドは常に0を返します。
    • close() メソッドは何もしない。
  • redirectErrorStream プロパティ。最初は、このプロパティは false です。
    つまり、サブプロセスの標準出力とエラー出力は、2つの別々のストリームに送信され、Process.getInputStream() メソッドとProcess.getErrorStream() メソッドを使用してアクセスできます。
    値が true に設定されている場合は、次のようになります。
    • 標準エラーは標準出力とマージされ、常に同じ宛先に送信されます。
      (これにより、エラーメッセージを対応する出力と簡単に関連付けることができます)
    • 標準エラーと標準出力の共通の宛先は、redirectOutput を使用してリダイレクトできます。
    • サブプロセスの作成時に redirectError() メソッドで設定されたリダイレクトは無視されます。
    • Process.getErrorStream() メソッドから返されるストリームは常に null 入力ストリームになります

プロセスビルダの属性を変更すると、そのオブジェクトの start() メソッドによってその後に開始されたプロセスに影響しますが、以前に開始したプロセスや Java プロセス自体には影響しません。

ほとんどのエラーチェックはstart() メソッドによって実行されます。 start() メソッドが失敗するようにオブジェクトの状態を変更することは可能です。

たとえば、コマンド属性を空のリストに設定しても、start() メソッドが呼び出されない限り例外はスローされません。

このクラスは同期されていないことに注意してください。

複数のスレッドが同時にProcessBuilderインスタンスにアクセスし、少なくとも1つのスレッドが属性の1つを構造的に変更する場合は、外部と同期する必要があります。

ProcessBuilder クラスの API ドキュメントをザックリ調べてみました。

それでは public static List<Process> startPipeline​(List<ProcessBuilder> builders) メソッドについて API ドキュメントもザックリ読んでみます。

各ProcessBuilderのプロセスを開始し、標準出力ストリームと標準入力ストリームによってリンクされたプロセスのパイプラインを作成します。

各ProcessBuilderの属性は、各プロセスの開始時に標準出力が次のプロセスの標準入力に導かれることを除いて、それぞれのプロセスを開始するために使用されます。

最初のプロセスの標準入力と最後のプロセスの標準出力のリダイレクトは、それぞれのProcessBuilderのリダイレクト設定を使用して初期化されます。

他のすべてのProcessBuilderリダイレクトはRedirect.PIPEでなければなりません。

中間プロセス間のすべての入出力ストリームにはアクセスできません。

最初のプロセスを除くすべてのプロセスの標準入力は null 出力ストリームです。

最後のプロセスを除くすべてのプロセスの標準出力は null 入力ストリームです。

各 ProcessBuilder のredirectErrorStream は、それぞれのプロセスに適用されます。 true に設定すると、エラーストリームは標準出力と同じストリームに書き込まれます。

いずれかのプロセス起動で例外がスローされると、すべてのプロセスが強制的に破棄されます。

public static List<Process> startPipeline​(List<ProcessBuilder> builders) メソッドは、start() メソッドと同じように各 ProcessBuilder で同じチェックを実行します。

新しいプロセスは、command() メソッドによって与えられたコマンドと引数を、directory() メソッドによって与えられた作業ディレクトリ内で environment() メソッドによって与えられたプロセス環境で呼び出します。

このメソッドは、コマンドが有効なオペレーティングシステムコマンドであることをチェックします。

有効なコマンドはシステムによって異なりますが、コマンドは空でない文字列のリストでなければなりません。

一部のオペレーティングシステムでプロセスを開始するには、最小限のシステム依存環境変数セットが必要な場合があります。

その結果、サブプロセスは ProcessBuilder の environment() メソッド内の環境変数設定を超えて追加の環境変数設定を継承することがあります。

セキュリティマネージャが存在する場合、このオブジェクトのコマンド配列の最初のコンポーネントを引数として呼び出された public void checkExec​(String cmd) メソッドが呼び出されます。

これにより、SecurityException がスローされることがあります。

オペレーティングシステムのプロセスを開始することは、システムによって大きく異なります。 間違っていることがある多くのものの中には次のものがあります。

  • オペレーティングシステムのプログラムファイルが見つかりませんでした。
  • プログラムファイルへのアクセスが拒否されました。
  • 作業ディレクトリが存在しません。
  • コマンド引数に無効な文字(NULなど)があります

そのような場合、例外がスローされます。 例外の正確な性質はシステムに依存しますが、常にIOExceptionのサブクラスになります。

オペレーティングシステムがプロセスの作成をサポートしていない場合、UnsupportedOperationExceptionがスローされます。

このプロセスビルダを後で変更しても、返されるプロセスには影響しません。

このメソッドの引数は List<ProcessBuilder> で返り値は List<Process> です。対応する ProcessBuilder から開始されます。

これってコマンドをつなぐパイプを可能にしているだけですよね。

では、cat -n /home/yucchi/valhalla/README | head -n 34 | tail -n 11| cowsay -n -f ghostbusters を実行するコードを組んで確認してみましょう。

ちなみに Ubuntu の端末で実行すると下図のようになります。

3

プログラムのコードは下記のように組んでみました。

これを NetBeans IDE Dev 版 で実行すると下図のようになります。

0

見事に再現されています。

えっ? cowsay -n -f ghostbusters コマンドでエラーがでるって?

これは標準ではいってないので cowsay でググってインストールしてくださいね!

ちなみに 89行目からの処理は 91 行目にある public CompletableFuture<Process> onExit​() メソッドで返されるプロセスのための新しい CompletableFuture <Process> を利用してます。

public CompletableFuture<Process> onExit​() メソッドについてザックリ調べておきましょう。

プロセスの終了のためのCompletableFuture <Process> を返します。

CompletableFuture は、プロセス終了時に同期または非同期に実行される可能性のある依存する関数またはアクションをトリガーする機能を提供します。

プロセスが終了すると、CompletableFuture はプロセスの終了ステータスに関係なく完了します。

onExit() メソッドを呼び出すと、get() メソッドはプロセスが終了するのを待ち、プロセスを返します。

Future は、プロセスが完了したかどうかをチェックしたり、プロセスが終了するのを待つために使用できます。

CompletableFutureをキャンセルしても、プロセスには影響しません。

ProcessBuilder.start() メソッドから返されたプロセスは、プロセスの終了を待つための効率的なメカニズムを提供するために、デフォルトの実装をオーバーライドします。

onExit() メソッドを使用すると、追加の同時実行性とプロセス結果への便利なアクセスの両方を可能にする waitFor() メソッドの代替手段となります。

ラムダ式を使用して、プロセス実行の結果を評価することができます。

値が使用される前に他の処理が行われる場合、onExit() メソッドは現在のスレッドを解放し、値が必要な場合にのみブロックするのに便利なメカニズムです。

たとえば、2つのファイルを比較するプロセスを起動し、それらが同一であればブール値を取得します。

このプロセスは、ComputableFuture が完了して依存アクションが呼び出される前に、isAlive() メソッドで終了したことが観察されることがあります。

実装要件:
この実装は、正常に戻るまで別のスレッドでwaitFor()を繰り返し実行します。

waitFor() メソッドの実行が中断された場合、スレッドの割り込みステータスは保持されます。

waitFor() メソッドが正常に終了すると、CompletableFuture はプロセスの終了ステータスに関係なく完了します。

多数のプロセスが並行して待機している場合、この実装はスレッドスタックのための多くのメモリを消費する可能性があります。

外部実装はこのメソッドをオーバーライドし、より効率的な実装を提供する必要があります。

たとえば、基になるプロセスに委任するには、次のようにします。

以上 API ドキュメントをザックリ読んでみました。

これはいろいろな処理を順序立てて外部プログラムを利用する場合に有効かもしれませんね。(^_^)

このプログラムではただ単に使ってみただけで有効利用はなにもしていません。(^_^;

いろいろゴニョゴニョやってみたけど JDK 9 での ProcessAPI のアップデートは開発者に優しくなりました。

外部プログラムを利用する開発者にとって JDK 9 のリリースは待ち遠しいことでしょう。

最後にプログラム全体のコードを載せておきます。

お終い!

Java SE 9 の予習

Java SE 9 の予習 その2

Java SE 9 の予習 その3

Java SE 9 の予習 その4

Hatena タグ: