PythonのPing3パッケージのソースコードを読んで分かったことのメモ

船井総研デジタルのよもぎたです。

PythonのPing3パッケージのソースコードを調べたので、わかったこと、主に関数の使い方についてまとめておきます。

Ping3パッケージはVer.4.0.4を確認しています。Ping3パッケージのPyPIのページは[こちら](https://pypi.org/project/ping3/)、ソースコードはGitHub上の[こちらのリポジトリ](https://github.com/kyan001/ping3)になります。

# 概要
- GitHubのREADMEのGet Startedにある ``ping`` と ``verbose_ping`` を押さえておけばよい。
- ``ping`` が1回Pingを実行する（ICMP Echo Requestを送信してICMP Echo Replyを受信する）。
- ``verbose_ping`` は ``ping`` をラッピングしている。結果は標準出力に出力される。
- IPv6には対応していない。

# ``ping``

## 引数

### 必須のもの

- ``dest_addr`` : Pingの宛先をIPアドレスまたはFQDNで指定します。

### 任意のもの

- ``timeout``:Pingのタイムアウト値を整数で指定します。単位は秒で、デフォルトは4秒です。
- ``unit``:戻り値の単位を文字列で指定します。指定できる値は ``s`` の秒と、 ``ms`` のミリ秒です。デフォルトは ``s`` です。
- ``src_addr``:Pingの送信元となるIPアドレスを文字列で指定します。デフォルトは ``None`` です。
- ``interface``:Pingの送信元となるインターフェース（NIC）を文字列で指定します。デフォルトは ``None`` です。この引数は、OSがLinuxの場合にだけ指定できます。
- ``ttl``: 送信するICMP Echo RequestのTTL(Time to Live）の値を整数で指定します。デフォルトは ``None`` で、その場合はOSのデフォルト値が使われます。Windowsの場合は128、Linuxの場合は64になります。
- ``seq`` :送信するICMP Echo Requestのシーケンス番号を整数で指定します。デフォルトは0になります。
- ``size``:送信するICMP Echo Requestのペイロードの大きさを整数で指定します。単位はバイトで、デフォルトは56です。

## 戻り値

あて先からの応答時間が``float``で返されます。デフォルトはの単位は秒で、``unit``引数で``ms``を指定した場合はミリ秒になります。

タイムアウトになった場合は``None``が返されます。それ以外の何らかのエラーの場合には、``False``が返されます。

## 例外

Ping3モジュールは``socket``モジュールを使用しています。Socketのタイプとしてまず``SOCK_RAW``でインスタンスの作成を試みて、失敗した場合は``SOCK_DGRAM``でインスタンスの作成を試みます。どちらも失敗した場合、``PermissionError``例外が投げられます。

# ```verbose_ping```

## 引数

### 必須のもの

- ``dest_addr``:Pingの宛先をIPアドレスまたはFQDNで指定します。

### 任意のもの

- ``count``：Pingを実行する回数を整数で指定します。デフォルト値は4です。0を指定すると無限に実行します。
- ``interval``:Pingを実行する間隔を整数で指定します。単位は秒で、デフォルト値は0です。0が指定されるとPingに応答があり次第即次のPingが実行されます。

その他、``ping``と同じ引数が指定できます。

## 戻り値

メソッドとしての戻り値は無くて、代わりにPingの実行結果（応答時間 or タイムアウト or エラー）が標準出力に出力されます。

## 余談

このパッケージを調べたキッカケはPythonでPing監視を実行したかったからです。監視要件に沿って複数回Pingを実行できるようにするつもりだったのですが、結果が標準出力に出力されてしまうとちょっと扱いにくいですね。

# デバッグモード

``ping3.DEBUG = True``とすると、デバッグモードが有効になります。細かくデバッグメッセージの出力が埋め込まれていて、``True``にするとメッセージが標準出力に出力されます。

テストスクリプト
```Python:test_ping.py
import ping3

ping3.DEBUG = True
ping3.ping('www.example.com')
```

実行例
```bash
$ python test_ping.py
[DEBUG] Ping3 Version: 4.0.4
[DEBUG] LOGGER: <Logger ping3 (DEBUG)>
[DEBUG] Function called: ping(www.example.com)
[DEBUG] `[Errno 1] Operation not permitted` when create socket.SOCK_RAW, using socket.SOCK_DGRAM instead.
[DEBUG] Function called: send_one_ping({'sock': <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=1, laddr=('0.0.0.0', 0)>, 'dest_addr': 'www.example.com', 'icmp_id': 51073, 'seq': 0, 'size': 56})
[DEBUG] Destination address: 'www.example.com'
[DEBUG] Destination IP address: 93.184.216.34
[DEBUG] Sent ICMP header: {'type': 8, 'code': 0, 'checksum': 8998, 'id': 51073, 'seq': 0}
[DEBUG] Sent ICMP payload: b'A\xd9C\xb3\xe7\xc9\x00bQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ'
[DEBUG] Function returned: send_one_ping -> None
[DEBUG] Function called: receive_one_ping({'sock': <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_DGRAM, proto=1, laddr=('0.0.0.0', 37)>, 'icmp_id': 51073, 'seq': 0, 'timeout': 4})
[DEBUG] Unprivileged on Linux
[DEBUG] Timeout time: Sat Sep 23 20:44:35 2023 (1695469475.141067)
[DEBUG] Timeout left: 4.00s
[DEBUG] Received time: Sat Sep 23 20:44:31 2023 (1695469471.3031929))
[DEBUG] Received ICMP header: {'type': 0, 'code': 0, 'checksum': 62082, 'id': 37, 'seq': 0}
[DEBUG] Received ICMP payload: b'A\xd9C\xb3\xe7\xc9\x00bQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ'
[DEBUG] Received sent time: Sat Sep 23 20:44:31 2023 (1695469471.1406484)
[DEBUG] Function returned: receive_one_ping -> 0.16254448890686035
[DEBUG] Function returned: ping -> 0.16254448890686035
```

ちなみに上の例は一般ユーザーで実行していて、``socket.SOCK_RAW``が使えずに``socket.DGRAM``で実行していることが分かります。同じスクリプトをrootで実行すると、次のように``socket.RAW``で実行していることが分かります。

```bash
# python test_ping.py
[DEBUG] Ping3 Version: 4.0.4
[DEBUG] LOGGER: <Logger ping3 (DEBUG)>
[DEBUG] Function called: ping(www.example.com)
[DEBUG] Function called: send_one_ping({'sock': <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_RAW, proto=1, laddr=('0.0.0.0', 1)>, 'dest_addr': 'www.example.com', 'icmp_id': 57200, 'seq': 0, 'size': 56})
[DEBUG] Destination address: 'www.example.com'
[DEBUG] Destination IP address: 93.184.216.34
[DEBUG] Sent ICMP header: {'type': 8, 'code': 0, 'checksum': 59052, 'id': 57200, 'seq': 0}
[DEBUG] Sent ICMP payload: b'A\xd9C\xb3\xf6W\x16^QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ'
[DEBUG] Function returned: send_one_ping -> None
[DEBUG] Function called: receive_one_ping({'sock': <socket.socket fd=3, family=AddressFamily.AF_INET, type=SocketKind.SOCK_RAW, proto=1, laddr=('0.0.0.0', 1)>, 'icmp_id': 57200, 'seq': 0, 'timeout': 4})
[DEBUG] Timeout time: Sat Sep 23 20:45:33 2023 (1695469533.3609657)
[DEBUG] Timeout left: 4.00s
[DEBUG] Received time: Sat Sep 23 20:45:29 2023 (1695469529.504645))
[DEBUG] Received IP header: {'version': 69, 'tos': 0, 'len': 84, 'id': 0, 'flags': 16384, 'ttl': 44, 'protocol': 1, 'checksum': 46826, 'src_addr': '93.184.216.34', 'dest_addr': '172.17.181.210'}
[DEBUG] Received ICMP header: {'type': 0, 'code': 0, 'checksum': 61100, 'id': 57200, 'seq': 0}
[DEBUG] Received ICMP payload: b'A\xd9C\xb3\xf6W\x16^QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ'
[DEBUG] Received sent time: Sat Sep 23 20:45:29 2023 (1695469529.3607402)
[DEBUG] Function returned: receive_one_ping -> 0.1439049243927002
[DEBUG] Function returned: ping -> 0.1439049243927002
```

# EXCEPTIONSモード

Pingに応答がなかった時に``ping``メソッドが``Falese``や``None``を返さずに例外を投げるようにする``EXCEPTIONS``モードがあります。

たとえば、存在しないFQDNに向かってPingを実行すると次のようになります。

テストスクリプト
```python:test2_ping.py
import ping3

ping3.EXCEPTIONS = True

try:
    ping3.ping('no.such.domain')
except Exception as e:
    print(e)
```

実行例
```bash
$ python test2_ping.py
Cannot resolve: Unknown host. (Host='no.such.domain')
```

以上です、最後までお読みいただきありがとうございました。