技術メモ

神奈川在住のITエンジニアの備忘録。おもにプログラミングやネットワーク技術について、学んだことを自分の中で整理するためにゆるゆると書いています。ちゃんと検証できていない部分もあるのでご参考程度となりますが、誰かのお役に立てれば幸いです。

ファイルを開いているプログラムを特定する方法

Windows上で、ファイルを移動したり、ファイル名を変更したい時、何らかのプログラムがそのファイルを掴んでいて、やりたいことができないことがある。通常は、現状で開いているアプリを確認すればどのプログラムが対象のファイルを開いているのか分かるので、そのプログラムを閉じてから移動や変更を実施すれば良い。

ただ、時々、落としたと思ったプログラムが意図せず残存してファイルを掴んでいたり、バックグラウンドで動くプログラムがこっそりファイルを掴んでいるなど、どのプログラムを落とせば良いのか分からない時がある。そんな時は、以下の方法で対象のファイルを掴んでいるプログラムを特定する。(以下はWindows10での操作例)

  • リソースモニターを開く。リソースモニターはタスクマネージャーの「パフォーマンス」タブから辿れる。
  • リソースモニターの「関連付けられたハンドル」のテキストボックスに対象のファイルのフルパスを入力しEnterを押下する。

これで、「関連付けられたハンドル」配下の「イメージ」にファイルを掴んでいるプログラムが表示される。

ちなみに、Linuxでは lsof というコマンドがあり、上記と同等のことができる。

staticな内部クラス

Javaではクラスの中にクラスを定義することができる。これを内部クラスと言う。この内部クラスを定義する際、staticを付けることができる。今日はどういう時に内部クラスにstaticを付けるべきなのか書く。

結論から言ってしまうと、内部クラスにはstaticを付けるべき、ということになる。名著「Effective Java」にもそう書いてあるらしい。以下、その理由について述べる。

 

内部クラスにstaticを付けると、内部クラスから外部クラス(エンクロージングクラスとも言う)のフィールド変数にアクセスできなくなる。これは、言い換えると、外部クラスのフィールド変数のスコープが (staticなしの内部クラスの場合と比べて) 小さくなるということである。プログラミングの定石として変数のスコープはできるだけ小さくすべきなので、これは利点である。

「それじゃあ、内部クラスから外部クラスのフィールド変数にアクセスしたい場合に対応できないよ!」という反論もあるかもしれないが、それは外部クラスと内部クラスが相互に参照し合っている(結合度が強い)ということになるため、クラス設計を見直した方が良いかもしれない。

他にstaticな内部クラスとstaticなしの内部クラスとで大した機能差はないので、内部クラスにはstaticを付けるべき、ということになる。

 

ちなみに、staticなものからstaticでないものにアクセスできないのは内部クラスに限った話ではなく、例えば、staticなメソッド内からはフィールド変数にアクセスできない。これは、staticなメソッドはクラスの持ち物であり、フィールド変数はクラスから生成されたインスタンスの持ち物であり、インスタンスは複数存在することがあるので、staticなメソッドからどのインスタンスのものを参照すれば良いのか分からないためである(と理解している)。

ソースコードを解析する時のコツ

大規模なJavaプログラムのソース(内部仕様書なし)を解析することが多いので、その際に身に付けたソース解析テクニックについて纏めてみる。

 

マニュアルなどでプログラムの外部仕様を把握しておく

これが分かっていないと、ソースを見ても???となってしまうと思う。仕様がちゃんと分かっていれば、ソースを読んだときに、その部分がどのように動くのかイメージしやすい。

 

最初から細部には拘らない

最初に概要を抑えるとその後のソース解析がスムーズに進むので、まずはメソッドの中身にはあまり踏み込まず、メソッド名から何をするものなのかを推測して、ソースを浅く読み進めていく。その後、必要に応じて、メソッドの内容をちゃんと見ていく。

 

クラスの役割を意識する

Javaのようなオブジェクト指向の言語では、クラスAがクラスBのメソッドを呼び出して、さらにクラスBがクラスCのメソッドを呼び出して・・・といった感じで、どんどん見るべきソースファイルが切り替わっていく。この流れをただ漫然と追っていると、全体の構造が頭に入らず、一通り読んでも何だかしっくりこないことがある。そんな時は各クラスの役割と関係性を意識すると良い。例えば、クラスのフィールド変数や、publicメソッドなど外部に公開しているメソッドに着目すると、そのクラスの役割が分かることがある。

 

まずは正常処理を中心に追う

例外処理のためのコード (catch節) がソースの多くの部分を占めていることがある。概要を把握するには、そういった例外処理は一旦気にしないことにして、正常系の処理の内容把握に集中する。そうすると、最初に見る範囲が減って精神的にも楽になる。例外系の処理は、その後に必要に応じて追えば良い。

 

処理の起点を把握する

解析対象のソースの箇所が何を起点として実行されるものなのかを把握しておくと、全体の理解が進むことがある。逆にこれを押さえていないと、解析対象のソースがプログラム全体のどこ辺に位置するのか分からず、もっと上位まで追わないと機能理解に見落としがありそうでモヤモヤする。(精神的に良くない。) ちなみに、処理の起点としては、例えば、Mainから実行される、プログラム内のタイマーから実行される、ユーザ操作に起因して実行される、(監視キューにタスクが到着した等を契機に)コールバックとして実行される、などがある。

 

ソース中のコメントやログ出力の内容に着目

コメントを参考にするのは常套手段だが、ソース中に書かれているログ出力用のコードも参考になることが多い。人間なので、プログラムのコードよりは自然言語の方が理解が速いので、こういったものも有効活用したい。

 

実際に動かしてみる

実際にプログラムを動かして細かい挙動を把握しておくと、ソースコードの理解が進みやすい。その際、もし詳細にログが出るように設定できるならそうしておく。そうしておくと、ログからソースコードのどこの処理を通っているのかを知ることができ、調査対象範囲を絞ることができる。

 

メモを取る

ソース解析した内容は簡単にメモに残しておくと、あとあと見直した時の理解がだいぶ速い。解析した時はその内容を鮮明に覚えているが、しばらくすると忘れてしまうことも多いし、メモに残すとちゃんとやっている感(?)も出る。あと、メモを書くと、自分の頭の中が整理できるという利点もある。

 

 

この他、普段からやっておくこととして、Javaデザインパターンやイディオムに慣れておく、自分に合ったエディタや統合開発環境を探して慣れておく、といったことも重要である。特に、イディオムに慣れておくと、ちょっとソースを見ただけで、「ここでDBからxxxテーブルの情報を取ってきているな」とか「ここで外部プロセスと通信しているな」とかがすぐに分かるようになるので、解析スピードがだいぶ上がる。

tcp/udpポートを確保し続けるスクリプトを少しエンハンスした。

以前、tcp/udpポートを確保し続けるスクリプト holdPort.pl を作成した。
akrad.hatenablog.com


これに引数でholdするIPアドレスを指定できる機能を追加した。例えば、「perl holdPort.pl -port 8080 -ip 127.0.0.1」と実行すると、holdPort.pl は 127.0.0.1:8080/tcp をholdする。IPアドレスを指定しなければ、INADDR ANY (0.0.0.0) でholdする。

use strict;
use warnings;
use Socket;
use Getopt::Long 'GetOptions';

my $ip;
my $port;
my $udp;
GetOptions(
    'ip=s'   => \$ip,
    'port=i' => \$port,
    'udp'    => \$udp
);

my $sockAddr;
if ($ip) {
    my $packedIp = inet_aton($ip);
    $sockAddr = sockaddr_in($port, $packedIp);
}
else {
    $sockAddr = sockaddr_in($port, INADDR_ANY);
}

my $serverSock;

if ($udp) {
    #------------------
    # UDP
    #------------------
    socket($serverSock, PF_INET, SOCK_DGRAM, getprotobyname('udp'))
        or die "Cannot create socket: $!";
    bind($serverSock, $sockAddr) or die "Cannot bind $port: $!";
    
    while (1) {
        recv($serverSock, my $buf, 1024, 0) or die "Cannot recv: $!";
    }
}
else {
    #------------------
    # TCP
    #------------------
    socket($serverSock, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
        or die "Cannot create socket: $!";
    bind($serverSock, $sockAddr) or die "Cannot bind $port: $!";
    listen($serverSock, SOMAXCONN) or die "Cannot listen: $!";

    my $clientSock;
    while (1) {
        accept($clientSock, $serverSock) or die "Cannot accept: $!";
    }
}


IPアドレスを指定できるようにしたのは、ポートを確保する際、IPアドレスまで一致していないと、このスクリプトの目的であるポート・バッティングを引き起こせないため。例えば、スクリプト127.0.0.1:8080 をholdしている状態で、別プロセスとしてスクリプトを実行して 0.0.0.0:8080 をholdしてもエラーにならない。逆も同様で、0.0.0.0:8080 をhold している状態で 127.0.0.1:8080 をholdしてもエラーにならない。Windows上で実機検証したらそうなった。なぜポート・バッティングにならないのか分かっていないので、今後調査する。

関数の処理結果を関数外に伝える方法

プログラミング一般な話として、主題の件について纏めてみる。関数の処理結果を関数外に伝える方法としては、戻り値で伝える方法、引数で伝える方法、グローバル変数で伝える方法、外部ファイルで伝える方法がある。それぞれについて思うところを書いていく。

 

  • 戻り値で伝える方法:この方法は、数学の「関数」と合っていて、直感的で分かりやすい。ただ、関数の戻り値は通常1つしか指定できないので、もし複数項目を戻り値で返したい場合は、それ用の構造体orクラスを用意する必要がある。この方法を使うとソースが見やすくなるので、基本的にはこの方法を使えば良いと思う。

 

  • 引数で伝える方法:ネットワークプログラミングで使用されるC言語のreceive関数のように、関数内で、受け取った引数に処理結果を入れる方法である。インプットである引数にアウトプットである処理結果を入れるので、直感に合わず、ちょっとソースが見にくくなるかもしれない。自分としては、これを使わないとソースが書きにくい場合のみでしか使わない。

 

  • グローバル変数で伝える方法:これは簡単な方法であるが、よく言われるように、グローバル変数を多用すると、それがどこから変更されているのか分かりにくくなるので要注意。関数の処理結果がいろんな箇所から参照される必要がある、つまり、処理結果がグローバル変数であるべき時に使用すると良いと思う。

 

  • 外部ファイルで伝える方法:これは上記の3つとは毛色が異なり、処理結果をメモリ上ではなく、DBやログなどの外部ファイルに格納する方法である。処理結果を (一時的なメモリ領域ではなく) 永続化して残したい場合に使用する。

 

以上、思うところをざっと書いてみたが、ケースバイケースで適切な方法でコーディングしていきたい。

Javaにおける処理の失敗の伝え方

Javaのメソッド内の処理の失敗を上位に伝える方法として、以下がある。

  • 例外で伝える
  • 戻り値で伝える

C言語出身の自分としては、まず「戻り値で伝える」から入ったが、Javaのような例外処理機構を持った言語では例外を使って使っていった方が良いのではないかと思う。その理由は以下の通り。

  • 例外オブジェクトに失敗時の情報を込めることができる。具体的には、例外オブジェクトの型や例外オブジェクト内のメッセージにより、失敗時の情報を上位に伝えることができる。一方で、戻り値の場合はそれができない。戻り値が 0,1 以外の値も取るようにして、それぞれに意味を与えておけばできなくはないが、Javaでは普通そこまではやらないでしょう。
  • 処理結果を受け取る上位のメソッド側のコードが、正常系のものと異常系(例外を受け取った時の処理)で分けられるため、コードが見やすくなる。コードの見やすさは、特に保守フェーズに入ると重要なものとなる。

例外を使う場合、受け取る側では try~catch 節が必要になりちょっと面倒だったり、例外は所謂 goto 文なので、人によっては処理の流れが追いにくくなるのかもしれないが、基本的には例外を使っていった方が良いコードになると考えている。

tcp/udpポートを確保し続けるスクリプト

tcp/udpポートを確保し続ける perl スクリプト holdPort.pl を作成した。テスト等でポート・バッティングを起こすために、指定したポートを確保したい時に使うと便利。
使い方は簡単で、例えば「perl holdPort.pl -p 80」で、このスクリプトは停止するまで 80/tcp を握り続ける。udpポートを握りたい場合は、「perl holdPort.pl -p 80 -u」とする。

holdPort.pl

use strict;
use warnings;
use Socket;
use Getopt::Long 'GetOptions';

my $port;
my $udp;
GetOptions(
    'port=i' => \$port,
    'udp'    => \$udp
);

my $packAddr = sockaddr_in($port, INADDR_ANY);
my $serverSock;

if ($udp) {
    #------------------
    # UDP
    #------------------
    socket($serverSock, PF_INET, SOCK_DGRAM, getprotobyname('udp'))
        or die "Cannot create socket: $!";
    bind($serverSock, $packAddr) or die "Cannot bind $port: $!";
    
    while (1) {
        recv($serverSock, my $buf, 1024, 0) or die "Cannot recv: $!";
    }
}
else {
    #------------------
    # TCP
    #------------------
    socket($serverSock, PF_INET, SOCK_STREAM, getprotobyname('tcp'))
        or die "Cannot create socket: $!";
    bind($serverSock, $packAddr) or die "Cannot bind $port: $!";
    listen($serverSock, SOMAXCONN) or die "Cannot listen: $!";

    my $clientSock;
    while (1) {
        accept($clientSock, $serverSock) or die "Cannot accept: $!";
    }
}

ちなみに、このスクリプトが 80/tcp を握っている状態で、別プロンプトから実行した同スクリプトに 80/udp を握らせたら、問題なく握れた。つまり、同じポート番号でも tcpudp では別物となるということ。
# これまで tcpudp とで同じポート番号を使ってポート・バッティングしたとか聞いたことないし、そもそもOS内で tcpudp は分けて管理されているはずなので試すまでもなかったが、一応やってみた。