■ PowerShellでPing死活監視ツール ～💀Deadmanリスペクト🔥～

![PowerShell_banner.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/2fd4db5e-14e9-42b3-9e38-a3728ed83c5b.png)

<br>

こんにちは :star2:
連日PowerShell投稿になっちゃいました **零壱(ゼロイチ)テクト**です。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/ec08f703-4a5c-46d9-8c78-a7527b229e57.png">

PowerShellで既存のPingやPingツールとは違うPing表示方法はないか模索していた所
かの現在開催中の展示会「`Interop`」で過去に用いられたという
CLIの死活監視ツール「**Deadman**」なるツールを見つけました。

ツールの画面を見て、中身や並列処理はともかく
見た目だけならPowerShellで似たようなツールを作れるのでは
と本能が感じたので作成(**リスペクト**)してみました。

# １．概要/仕様
・最小限のDeadmanの機能を付ける
・見た目も可能な限りDeadmanに寄せる
・Ping結果の記号表示の波は絶対/色も絶対

# ２．コード - Simple版
コードを最小限にするため機能を必要最低限に絞ったバージョンです。

```powershell:Deadman_Simple.ps1
################################
# Deadman Tool - Simple
###############################

# Ping監視対象
$targets = @("8.8.8.8","1.1.1.1","18.65.207.100") # Google, CloudFlare, Qiita

# 各種設定値
$interval = 500          # Ping間隔/ms
$repeat   = 10000        # 繰り返し回数
$maxDisplayCount = 30    # 表示するPing結果の最大履歴数

# コンソールウィンドウのサイズを指定
[console]::WindowWidth = 90
[console]::WindowHeight = 20

# 結果表示用のデータ構造を初期化
$symbolMap    = @{}  # 各対象のPing結果を記号で保持
$lineIndexMap = @{}  # 各対象の結果表示位置（コンソール行番号）を保持
$index = 0           # 表示行番号カウンタ

# ヘッダー部の項目名を表示
Write-Host ("{0,-15} {1,-30} {2,-8} {3}" -f "Target", "HostName", "Rtt", "Result")

# 各初期化処理
foreach ($target in $targets) {
    $symbolMap[$target] = @()            # Ping結果用の空配列
    $lineIndexMap[$target] = $index + 1  # 対象の表示行番号を保存（ヘッダーが0行目のため+1）
    Write-Host ""                        # 対象ごとに空行を挿入して表示領域を確保
    $index++                             # 行カウンタをカウントアップ
}

# 応答時間に応じて表示する記号を返す関数
# RTTが大きいほどブロックの大きさが大きくなる/ms
function Get-SymbolFromRTT($rtt) {
    if ($rtt -ge 1000) { return "▇" }  
    elseif ($rtt -ge 200) { return "▆" }
    elseif ($rtt -ge 100) { return "▅" }
    elseif ($rtt -ge 50)  { return "▃" }
    elseif ($rtt -ge 10)  { return "▂" }
    else { return "▁" }                 
}

# メイン監視：repeat回数分繰り返す
1..$repeat | ForEach-Object {
    # Ping対象を順次ループ
    foreach ($target in $targets) {
        Start-Sleep -Milliseconds $interval  # Ping間隔調整

        # ホスト名をDNSから取得
        try {
            $hostEntry = [System.Net.Dns]::GetHostEntry($target)
            $hostName = $hostEntry.HostName
        } catch {
            $hostName = "[ failed ]"     
        }

        # 取得ホスト名調整
        if ($hostName.Length -gt 27) {
            $hostName = $hostName.Substring(0, 27) + "..."
        }

        # Pingを1回実行して結果を取得
        $pingResult = Test-Connection -ComputerName $target -Count 1 -ErrorAction SilentlyContinue

        if ($pingResult) { # PingOK
            $return = "$($pingResult.ResponseTime)ms"                 # 応答時間を文字列化
            $symbol = Get-SymbolFromRTT($($pingResult.ResponseTime))  # RTTに応じた記号取得
        } else { # PingNG
            $return = "none"    # 応答なしの表示
            $symbol = "X"       # 失敗を「X」で表示
        }

        # Ping結果リストに追加
        $symbolMap[$target] += $symbol

        # Ping結果リストが最大表示数を超えたら結果を左にスライド
        if ($symbolMap[$target].Count -gt $maxDisplayCount) {
            $symbolMap[$target] = $symbolMap[$target][1..($symbolMap[$target].Count - 1)]
        }

        # コンソールのカーソル位置を対象ホストの表示行に移動
        $line = $lineIndexMap[$target]
        [Console]::SetCursorPosition(0, $line)

        # 実行結果更新（表示位置調整）
        Write-Host ("{0,-15} {1,-30} {2,6} {3,0} " -f $target, $hostName, $return, " ") -NoNewline

        # Ping結果の記号履歴を表示。応答なしは赤色/それ以外は緑色
        foreach ($s in $symbolMap[$target]) {
            if ($s -eq "X") {
                Write-Host $s -NoNewline -ForegroundColor Red
            } else {
                Write-Host $s -NoNewline -ForegroundColor Green
            }
        }

    }
}

```

# ３．コード - Detail版
Simple版だけでも良かったのですが、リスペクトもかねて
少しでもオリジナルに似た見た目になるよう頑張っちゃったバージョンです。

```powershell:Deadman_Detail.ps1
################################
# Deadman Tool - Detail
################################

# Ping監視対象
$targets = @("8.8.8.8", "1.1.1.1","18.65.207.100") # Google, CloudFlare, Qiita

# 各種設定値
$interval = 500          # Ping間隔/ms
$repeat = 10000          # 繰り返し回数
$maxDisplayCount = 30    # 表示するPing結果の最大履歴数

# コンソールウィンドウのサイズを指定
[console]::WindowWidth = 107
[console]::WindowHeight = 20

# 結果表示用のデータ構造を初期化
$symbolMap    = @{}  # 各各対象のPing結果を記号で保持
$lineIndexMap = @{}  # 各対象の結果表示位置（コンソール行番号）を保持
$rttMap       = @{}  # RTT合計値と応答回数を格納するハッシュ（平均計算用）
$index = 0           # 表示行番号カウンタ

# ヘッダー部の項目名を表示（表示位置調整）
Write-Host ("{0,-15} {1,-30} {2,-6} {3,-8} {4,-8} {5}" -f "Target", "HostName", "Loss", "Avg", "Rtt", "Result")

# 各初期化処理
foreach ($target in $targets) {
    $symbolMap[$target] = @()                      # Ping結果用の空配列
    $rttMap[$target]    = @{ Sum = 0; Count = 0 }  # RTTの累積値とPing回数を初期化
    $lineIndexMap[$target] = $index + 1            # 対象の表示行番号を保存（ヘッダーが0行目のため+1）
    Write-Host ""                                  # 空行挿入して表示領域を確保
    $index++                                       # 行カウンタをカウントアップ
}

# 応答時間に応じて表示する記号を返す関数
# RTTが大きいほどブロックの大きさが大きくなる/ms
function Get-SymbolFromRTT($rtt) {
    if ($rtt -ge 1000) { return "▇" }  
    elseif ($rtt -ge 200) { return "▆" } 
    elseif ($rtt -ge 100) { return "▅" }
    elseif ($rtt -ge 50)  { return "▃" }
    elseif ($rtt -ge 10)  { return "▂" }
    else { return "▁" }                 
}

# メイン監視：repeat回数分繰り返す
1..$repeat | ForEach-Object {
    # Ping対象を順次ループ
    foreach ($target in $targets) {
        Start-Sleep -Milliseconds $interval  # Ping間隔調整

        # ホスト名をDNSから取得
        try {
            $hostEntry = [System.Net.Dns]::GetHostEntry($target)
            $hostName = $hostEntry.HostName
        } catch {
            $hostName = "[ failed ]"
        }

        # 取得ホスト名調整
        if ($hostName.Length -gt 27) {
            $hostName = $hostName.Substring(0, 27) + "..."
        }

        # Pingを1回実行して結果を取得
        $pingResult = Test-Connection -ComputerName $target -Count 1 -ErrorAction SilentlyContinue

        if ($pingResult) { # PingOK
            $rtt    = $pingResult.ResponseTime  # RTT取得
            $return = "$($rtt)ms"               # 表示用文字列作成
            $symbol = Get-SymbolFromRTT($rtt)   # RTTに応じた記号取得

            # 平均計算用にRTTを累積/応答回数をカウントアップ
            $rttMap[$target].Sum += $rtt
            $rttMap[$target].Count++
        } else { # PingNG
            $return = "none"    # 応答なしの表示
            $symbol = "X"       # 失敗を「X」で表示
        }

        # Ping結果リストに追加
        $symbolMap[$target] += $symbol

        # Ping結果リストが最大表示数を超えたら結果を左にスライド
        if ($symbolMap[$target].Count -gt $maxDisplayCount) {
            $symbolMap[$target] = $symbolMap[$target][1..($symbolMap[$target].Count - 1)]
        }

        # Loss率計算（履歴内のX記号の割合）
        $totalCount = $symbolMap[$target].Count                                    # Ping総数
        $lossCount = ($symbolMap[$target] | Where-Object { $_ -eq "X" }).Count     # PingNG数
        $lossPercent = "{0,3}%" -f [math]::Round(($lossCount / $totalCount) * 100) # パーセント表示に整形

        # RTT平均計算/未計測の場合は「n/a」
        if ($rttMap[$target].Count -gt 0) {
            $avgRtt = "{0,4}ms" -f [math]::Round($rttMap[$target].Sum / $rttMap[$target].Count)
        } else {
            $avgRtt = " n/a"
        }

        # コンソールのカーソル位置を対象ホストの表示行に移動
        $line = $lineIndexMap[$target]
        [Console]::SetCursorPosition(0, $line)

        # 実行結果更新（表示位置調整）
        Write-Host ("{0,-15} {1,-30} {2,-6} {3,-7} {4,7} {5,0}" -f $target, $hostName, $lossPercent, $avgRtt, $return,"  ") -NoNewline

        # Ping結果の記号履歴を表示。応答なしは赤色/それ以外は緑色
        foreach ($s in $symbolMap[$target]) {
            if ($s -eq "X") {
                Write-Host $s -NoNewline -ForegroundColor Red
            } else {
                Write-Host $s -NoNewline -ForegroundColor Green
            }
        }
    }
}

```
コードはお好きにお使いください。


# 【オマケ】コード － ワンライナー版

```powershell:Deadman_Oneliner.ps1
$ip = "8.8.8.8";$sbs = @();1..10000 | ForEach-Object{Start-Sleep -Milliseconds 500;$ping=Test-Connection -ComputerName $ip -Count 1 -ErrorAction SilentlyContinue;$rtt="none";$sy="X";if ($ping) {$rtt="$($ping.ResponseTime)ms";$sy="▁▂▃▅▆▇"[[Math]::Min([Math]::Floor([Math]::Log([Math]::Max($ping.ResponseTime,5)/5)/[Math]::Log(3)),5)]};$sbs += $sy;if($sbs.Count -gt 50){$sbs = $sbs[1..($sbs.Count - 1)]};[console]::SetCursorPosition(0, 1);Write-Host("Target: {0,-8} RTT: {1,-8} Result:" -f $ip,$rtt) -NoNewline;foreach ($s in $sbs){$col=if($s -eq "X"){"Red"}else{"Green"};Write-Host $s -NoNewline -ForegroundColor $col}}
```
　※ワンライナーという発想というか文化というかコードの書き方を知ってしまいました。
　　とりあえず目先のコードをワンライナー化したので、オマケで載せます。
　　機能を削るか迷って、コードのために対象IPを1つに絞って作りました。

　※Ping結果の帯域が工夫ポイントです。
　　IFを使わず計算で処理できるようPing戻り値を3倍数で計算・判定させていて
　　つまりは細い方から太い方にかけて、5msを基準に
　　**15ms**未満 → **45ms**未満 → **135ms**未満 → **405ms**未満 → **1215ms**未満 →　それ以上
　　の6段階に処理を丸めてコードを短くしました。
　
# ４．結果
#### ■ Simple版 - 実行結果
![Deadman_Simple_Result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/c839e5d0-adb3-4a31-ba22-08cd67dc37ab.png)


#### ■ Detail版 - 実行結果
![Deadman_Detail_Result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/a4f8078c-33a4-42b1-979d-ea94a629424e.png)

　※末尾2つのIPは適当なPingが飛ぶ海外プロキシサーバです。
　　国内のあらゆるサイトでPingの戻り値が良すぎたため入れました。

#### ■ ワンライナー版 - 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4049822/cc415e1c-bc5e-44aa-8a2c-44e22b321897.png)

# ５．備考
✅ 改良案は……ありません。
　　あえて改良案というと、オリジナル要素を全て実装する、ぐらいでしょうか。

✅ なんかオリジナルのconfファイル見るとホスト名は自前で設定しているようですね。
　　自動取得より楽なのでそこは改良案かもしれません。

・Ver5.1のPowerShellで開発なので並列処理はなく、オリジナルより遅いです。
　Ver7では並列処理可能みたいですが、実行できる方が減るので今回は無し

・よって挙動はご覧の通り、対象ホストへのPingはシングルタスクです。

・Pingの戻り値による線の太さは、見た目重視でオリジナルよりパターンが少ないです。

・悔いはありません。想像以上に近しいものができて楽しかったです。

・例によってコメントは後付けです。Simple版とDetail版で差異があったらゴメンナサイ。

*****
　　それでは
　　　　　　　どこかの誰かの何かの足しになれば幸いです。
　　　　　　　　　　　　　　　　　　　　　　　　01000010 01011001 01000101


# ６．参考
Deadmanの挙動を知るため数々のサイト様を参考にさせて頂きました。
大変、感謝いたします。

#### ■ @engishoma様：deadmanをサクッと構築してみる
https://qiita.com/engishoma/items/db54aaf10c91f9584d41

#### ■ @johejo (Mitsuo Heijo)様：pingツールを作った
https://qiita.com/johejo/items/f19af3c9e602b1be80bb

#### ■ Github/Deadman
https://github.com/upa/deadman

#### ■ ShowNet
https://www.facebook.com/interop.shownet/posts/%E5%86%99%E7%9C%9F%E3%81%AFshownet%E3%81%AB%E3%81%A6%E6%A9%9F%E5%99%A8%E3%81%AE%E6%AD%BB%E6%B4%BB%E7%9B%A3%E8%A6%96%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8Bping%E7%9B%A3%E8%A6%96%E3%83%84%E3%83%BC%E3%83%ABdeadman-%E3%81%AE%E7%9B%A3%E8%A6%96%E7%94%BB%E9%9D%A2%E3%81%A7%E3%81%99%E7%94%BB%E9%9D%A2%E3%81%AF%E5%90%84%E6%A9%9F%E5%99%A8%E3%81%AE%E3%83%9E%E3%83%8D%E3%82%B8%E3%83%A1%E3%83%B3%E3%83%88ip%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%81%AB%E5%AF%BE%E3%81%99%E3%82%8Bping%E3%81%AE%E7%B5%90%E6%9E%9C%E3%82%92/1358371210944164/?locale=ja_JP

#### ■ GenkgGorosuke様：deadmanでshownet気分を味わう
https://gorosuke5656.hatenablog.com/entry/2024/06/17/123534

#### ■ Sig9様：deadman で Ping 監視する
https://sig9.org/blog/2017/05/27/

#### ■@yugo-yamamoto(ゆうご 山本)様：PowerShellをワンライナーで書きたい
https://qiita.com/bd8z/items/7a3d4eb10e8cf8bcc3b9

こちらの方でワンライナーという考え方を知りました。
これは素晴らしい発想ですね。