[Linux] Delphi で proc ディレクトリからハードウェア情報を取得する

# ハードウェア情報の取得

OS からハードウェア情報を取得する方法は OS 毎に様々です。

例えば Windows だったら [Win32 API](https://docs.microsoft.com/en-us/windows/desktop/apiindex/windows-api-list) を呼んで取ったり [WMI (Windows Management Instrumentation)](https://docs.microsoft.com/en-us/windows/desktop/wmisdk/wmi-start-page) を使って取ったりできます。

同様に Linux もいくつか取り方があるようですが一番簡単なのが proc ディレクトリから情報を読み出す、という方法です。
UNIX 系 OS はデバイスなどもファイルシステムにマッピングされるため、ハードウェアの情報もファイルとして取り出せます。それらの情報があるのが proc ディレクトリです。

ためしに Ubuntu (Windows Store 版) で、proc ディレクトリを見てみるとこんな感じです。

```bash
xxx@XXX:/$ ls -alF /proc/
total 0
dr-xr-xr-x 9 root root    0 Sep 29 17:24 ./
drwxr-xr-x 1 root root 4096 Sep 29 17:23 ../
dr-xr-xr-x 7 root root    0 Sep 29 17:24 1/
dr-xr-xr-x 7 root root    0 Sep 29 18:05 3/
dr-xr-xr-x 7 jun  xxx     0 Sep 29 18:05 4/
dr-xr-xr-x 7 jun  xxx     0 Sep 29 18:05 6298/
dr-xr-xr-x 2 root root    0 Sep 29 17:24 bus/
-r--r--r-- 1 root root    0 Sep 29 17:24 cgroups
-r--r--r-- 1 root root    0 Sep 29 17:24 cmdline
-r--r--r-- 1 root root    0 Sep 29 17:24 cpuinfo
-r--r--r-- 1 root root    0 Sep 29 17:24 filesystems
-r--r--r-- 1 root root    0 Sep 29 17:24 interrupts
-r--r--r-- 1 root root    0 Sep 29 17:24 loadavg
-r--r--r-- 1 root root    0 Sep 29 17:24 meminfo
lrwxrwxrwx 1 root root    0 Sep 29 17:24 mounts -> self/mounts
lrwxrwxrwx 1 root root    0 Sep 29 17:24 net -> self/net/
lrwxrwxrwx 1 root root    0 Sep 29 17:24 self -> 6298/
-r--r--r-- 1 root root    0 Sep 29 17:24 stat
dr-xr-xr-x 6 root root    0 Sep 29 17:24 sys/
dr-xr-xr-x 2 root root    0 Sep 29 17:24 tty/
-r--r--r-- 1 root root    0 Sep 29 17:24 uptime
-r--r--r-- 1 root root    0 Sep 29 17:24 version
-r--r--r-- 1 root root    0 Sep 29 17:24 version_signature
```

色々なファイル・ディレクトリがあります。
それらが、どのような意味を持つのかは[検索してみて下さい](https://www.google.co.jp/search?q=linux+proc)。

ここでは、この中の１つ "cpuinfo" を見てみます。
cat で中身を表示すると

```bash
xxx@XXX:/$ cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 94
model name      : Intel(R) Core(TM) i7-6700T CPU @ 2.80GHz
stepping        : 3
(以下略)
```

こんな風に CPU の情報が取得できました。
つまり cpuinfo はその名の通り CPU の情報を返すファイル、ということです。

さて、では proc ディレクトリ配下のファイルを プログラムから、しかも Delphi から取得するには、どのようにするのでしょうか？

# Delphi で読み出す

## 罠

proc 配下のファイルも当然ファイルなので、普通にファイルとして扱えます。

…しかし！そこに罠が！

例えば、次のように…

```pascal
procedure ReadCPUInfo;
var
  SL: TStringList;
begin
  SL := TStringList.Create;
  try
    SL.LoadFromFile('/proc/cpuinfo');
    Writeln('----------');
    Writeln(SL.Text);
    Writeln('----------');
  finally
    SL.DisposeOf;
  end;
end;
```

とすると、表示されるのは…

```
----------

----------
```

上記のようになり、SL.Text の部分は空行になっています。

何故空行になるかは、先ほどの ls proc の内容を見ると判ります。

```bash:cpuinfo
-r--r--r-- 1 root root    0 Sep 29 17:24 cpuinfo
```

となっていて、ファイルサイズが 0 になっているのです！
そして、[TStringList.LoadFromFile](http://docwiki.embarcadero.com/Libraries/Tokyo/ja/System.Classes.TStrings.LoadFromFile) の内部から呼ばれる [TStrings.LoadFromStream] (http://docwiki.embarcadero.com/Libraries/Tokyo/ja/System.Classes.TStrings.LoadFromStream) は、ファイルサイズを見てバッファを確保するため 0 バイトを返すファイルは読み込めません！

すごく当たり前の実装なのですが、今回のような場合は使えません。

## 罠の回避

では、どのように読み込むかというと、

1. File Stream を 1 バイト読み込む。
1. 読み出したバイト数が 1 の場合 1 に戻り、それ以外の場合は終了。

とするだけです。

それを実装すると下記の様になります。

```pascal
function ReadCPUInfo: String;
var
  Ch: Char;
  FS: TFileStream;
begin
  Result := '';
  FS := TFileStream.Create('/proc/cpuinfo', fmOpenRead);
  try
    while FS.Read(Ch, 1) = 1 do // 1バイト読み込んだ戻値が 1 の間処理を継続
      Write(Ch);
    Writeln;
  finally
    FS.DisposeOf;
  end;
end;
```

これを実行すると下記の出力が得られ、先に実行した cat /proc/cpuinfo と同じ結果になりました。

```bash
processor       : 0
vendor_id       : GenuineIntel
cpu family      : 6
model           : 94
model name      : Intel(R) Core(TM) i7-6700T CPU @ 2.80GHz
stepping        : 3
(以下略)
```

ここでは cpuinfo だけ読むようにしてみましたが、他にも色々取得できます。
ですので、汎用化してみます。

## 汎用化

汎用化したソースが以下です。
短いので全文載せて起きます。

```pascal
(*
 * Hardware Information reader from proc dicrectory.
 *
 * PLATFORMS
 *   Linux
 *
 * LICENSE
 *   Copyright (c) 2018 HOSOKAWA Jun
 *   Released under the MIT license
 *   http://opensource.org/licenses/mit-license.php
 *
 * USAGE
 *   Call ReadHardInfo with information filename.
 *
 * SAMPLE
 *   Info := TProcDirectory.ReadInfo('cpuinfo');
 *
 * 2018/09/28 Version 1.0.0
 * Programmed by HOSOKAWA Jun (twitter: @pik)
 *)

 unit PK.HardInfo.ProcDirectory.Linux;

interface

type
  TProcDirectory = record
  private const
    PROC_PATH = '/proc';
  public
    class function ReadInfo(const iFilename: String): String; static;
  end;

implementation

uses
  System.Classes,
  System.SysUtils,
  System.IOUtils;

class function TProcDirectory.ReadInfo(const iFilename: String): String;
var
  Ch: Char;
  Path: String;
  FS: TFileStream;
  SB: TStringBuilder;
begin
  Path := TPath.Combine(PROC_PATH, iFilename);
  if not TFile.Exists(Path) then
    Exit('');

  FS := TFileStream.Create(Path, fmOpenRead);
  try
    SB := TStringBuilder.Create;
    try
      // The file in the proc directory, it's size return -1,
      // Therefore, only one byte can be read.
      while FS.Read(Ch, 1) = 1 do
        SB.Append(Ch);

      Result := SB.ToString;
    finally
      SB.DisposeOf;
    end;
  finally
    FS.DisposeOf;
  end;
end;

end.
```

使う時は、こんな風にします。

```pascal
Writeln(TProcDirectory.ReadInfo('cpuinfo'));
```

# まとめ
なお、この罠は当然 Delphi に限ったことではないです。
やはり、各 OS について詳しく知っていないと、マルチプラットフォームアプリケーションの開発は難しいですね！