パソコンの起動・停止監視ツール

デモ画面   

はじめに

以前に作成したツール「Word Pressに「Wake On LAN」機能を埋め込み」に再起動・停止機能を追加してみた。
このツールはLAN内のパソコンやルーター等LAN接続機器のON・OFFを一覧で確認できるようにYAMAHAのツールを改造したもので,当環境でのみテストしているので異なる環境での動作は保証しない。
WOL機能があるWindowsマシンがLANに接続されている場合は,このツール画面から起動・停止・再起動が行える。

仕様変更

前仕様からの変更点

1.監視画面に停止欄追加
2.再起動・停止は外部コマンド(PsShutdown)を使用

問題

再起動・停止はWindows標準のPowerShellコマンドを使用しての構想だったが,PowerShellはWindows server2008R2から採用されており初期のバージョンではコマンドプロンプトからPowerShellを起動(PowerShell -command)してのコマンド投入は未対応。Windows Server 2012R2 以降でないとフルに使用不可。Windows Server 2008R2 はpowershellが搭載された最初のバージョンのため制約があるようだ。Windows Server2012R2以降から対応しているので,とりあえず対象serverをアップグレードすることにした。がWindows Server2016で実行時は 呼び出しの深さのオーバーフローでエラーとなったり,PowerShellのバージョンと相手サーバーのWindowsバージョンとの間でも互換性問題なのか上手くいかない場合があるので諦めて外部コマンド(PsShutdown)の使用を検討する。
PsShutdownはWindows XPおよびWindows Server 2003以降で動作する。

PsShutdownを使用するため今まで通り外部プログラムを実行するためPHPのEXEC()を使用と思ったが,どうもうまくいかなくて戻り値に1が帰ってきてコマンド実行されない。
今度はPHPのEXEC()との相性が悪いのか。コマンドプロンプトからの直接入力は上手くいくのだが,PHPのEXEC()を通すとダメ。では頭に「cmd -k」を付加してコマンドプロンプト経由で試したがこれもダメ。
PHP仕様「Windowsでは、exec() 関数はコマンドを起動するために最初に cmd.exe を起動します。」と記載があり結局同じことを繰り返しただけだった。
「cmd.exe を起動せずに外部プログラムを起動したい場合は、proc_open() 関数を bypass_shell オプションを指定して使うようにしてください。」とのことで
他の関数をテストしてみる。PHP: proc_open – Manual が上手くいかない。
結局,Apacheのサービスアカウントの権限の問題だった。

PsToolsを使いこなす | 日経クロステック(xTECH) (nikkei.com)

PsShutdown

このプログラム(PsShutdown – Sysinternals | Microsoft Learn)「psshutdown.exe」はパスの通ったフォルダーに保存する。(例:phpのルートフォルダー「D:\Program Files\php」)
PHPのexec関数で実行する場合はユーザーが被制御側のアカウントでないと接続できないので動作しない。64ビット版の「psshutdown64.exe」が使用可能であればそちらを使用。
コマンドにユーザーIDとパスワードを付加しない場合は,コマンドを実行したアカウント情報が取り込まれる。
ドメインアカウントではなく、ローカルアカウントで実行する場合、認証が成功するのはリモートコンピューターにローカルアカウントと同じユーザー名、同じパスワードのユーザーが存在する場合に限られる。
administraorsグルーブに所属した同一権限の他のユーザーでも戻り値が1となる。
コマンドプロンプトから管理者(被制御側と同一アカウント)としてPsShutdownを実行する場合は問題ないが,Apacheからだとデフォルトのサービスアカウントのローカルユーザー権限ではエラーとなるので,apacheサービスのログオンアカウントを被制御側の管理者アカウントと同じに変更する。
再起動・停止をおこなわないのであれば不要だが,apacheが管理者権限で動作するのでセキュリティに細心の注意が必要であり,公開サーバーではやめておいたほうが良い。又はユーザーidとパスワードをPsShutdownに付加すれば良いのだが,javascriptに入れると見えてしまうのでPHP側でコマンド発行時「execute.php」に付加する方法が良いかも。

<?php
  /* execute.php  20240506 */
$cmd = $_POST['post_cmd'];
$table = array();
$result;

/* PsShutdownの対象サーバーの制御アカウント  追加:y */
$account = 'y';
$uid = 'user';
$psw = 'password';

/* PsShutdownにアカウント指定無い時追加 */
if ($account == 'y' && strpos($cmd, 'PsShutdown') !== FALSE && strpos($cmd, '-u') == FALSE){
  $cmd = $cmd . ' -u "' . $uid . '" -p "' . $psw . '"';
};

exec($cmd, $opt , $return_ver);

if ($cmd == 'arp -a'){
  foreach ($opt as $line) {
      if (preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*([0-9a-f]{2}[\-:][0-9a-f]{2}[\-:][0-9a-f]{2}[\-:][0-9a-f]{2}[\-:][0-9a-f]{2}[\-:][0-9a-f]{2})/i', $line, $matches)) {
          $ip = $matches[1];
          $mac = strtoupper(str_replace(array('-', '.'), ':', $matches[2]));
          $table[$ip] = $mac;
      }
  }
/* arp -aの実行結果を配列で返す */
  $result = $table;

}else {
/*  実行結果と戻り値を配列で返す */
  $result=array(
 	  0 => $opt,
 	  1 => $return_ver
 	);
};
echo json_encode( $result ) ;
return;
?>



リモートコンピュータをIPアドレス指定する場合はポート開放は必要ない。NetBIOS over TCP(NBT)の名前解決を使用する場合は137-139を開放する。

PsShutdownの初回起動時はライセンスに同意を求めるポップアップ画面が表示されるのでPHPのexecから起動したときは同意待ちで止まってしまう。コマンドに「-accepteula」を付加すればポップアップ画面が出ない。又はApacheのユーザーでログインしてコマンドプロンプトからPsShutdownを起動してライセンス同意しても良い。
「PsShutdown -accepteula」は初回起動時にレジストリーに書き込んでしまえば2回目以降からは消しても問題ない。
コマンド説明文記載 -accepteula 「This flag suppresses the display of the license dialog.」

同意画面

停止コマンド投入直後にサーバー側画面に表示
20秒間カウントダウンする。

アカウントの制約についてまとめ

  1. クライアントからのコマンド実行は管理者アカウントを使用する(PsToolsの一部で例外あり)
  2. クライアントの管理者グループに所属する他のローカルアカウントではコマンドを受け付けない「ユーザーアカウント制御(UAC)」
  3. サーバーに登録されているアカウントでコマンドを実行する
  4. サーバーがドメイン参加であれば,ドメインアカウントで実行可能
  5. ドメインアカウントはユーザーアカウント制御(UAC)が無効のため管理者グループに所属する他のアカウントでも実行可能
  6. apacheからphpでコマンド投入する際に制御対象のサーバーOSのバージョンにより
    「- u」+「- p」パラメーター有無でコマンドが効かないことがある。

    ・ クライアント側がドメイン管理者のadministratorでapacheサービスにログオンしていたら「- u」+「- p」パラメーター無しでもWindows Server2008R2(以降)で有効。
    ・ クライアント側が一般ユーザーでapacheサービスにログオンしていたら「- u」+「- p」パラメーター無しでは全OSでコマンド無効となる。
    ・ クライアント側が一般ユーザーでapacheサービスにログオンして「- u」+「- p」パラメーターにドメイン管理者のadministratorを指定したらOSで違いがある。

Windows Server2008R2・Windows7では無効
(何かのタイミングで効くことがあった。Server2008R2は一般ユーザーのコマンドプロンプトからは効いた)

サーバー認証に使用される 'TERMSRV' サービス プリンシパル名を登録できません。次のエラーが発生しました: 指定されたドメインがないか、またはアクセスできません。

Windows Server2012R2,Windows Server2016 (以降)・Windows10~では有効
たぶんWindows8.1も有効

いろいろ制約があるが結局のところユーザーは「administrator」を使用したほうが無難だが,
管理者を使用したくなければドメイン参加の管理者グループ所属のアカウントを使用する。

ソース説明

wol_monitor.js

変更点は下記のとおり
1.監視画面に停止ボタン欄追加
2.再起動はON状態の起動ボタン押下で可能
3.新たにPsShutdownコマンドを追加して停止・再起動を行う。

wol_monitor.js

// wol_monitor.js    2023/11/01
var tlist;           // 監視対象一覧のテーブルのオブジェクト
var clist;           // Ping失敗数カウント用テーブルのオブジェクト
var logfd;           // ログ表示用フィールドのオブジェクト
var intobj;          // Ping定期実行用インターバルのオブジェクト

// 以下パラメーター定義
var authuser ="user";         // ベーシック認証 ユーザーid
var authpass ="pass";         // ベーシック認証 パスワード
var authstr = 'Basic ' + window.btoa(authuser + ':' + authpass);  // Authorizationヘッダー埋込認証用
var maxEntryNum = 20;               // 登録可能最大数
var pingWaitInterval = 1;          // Pingの待ち時間(s)
var pingExecInterval = 3000;       // Pingの一斉実行間隔(ms) 1000ms=1s
var pingErrorCount = 3;            // 何回Pingに失敗したらダウンと判断するか
var remoteuid = "";                // リモートコンピュータ制御ユーザーID  domain\user
var remotepass = "";               // リモートコンピュータ制御パスワード ※注 ブラウザに表示される
var idpass = (remotepass == "") ? "" : " -u \"" + remoteuid + "\" -p \"" + remotepass + "\"";

function getResource(url, callback) {
    $.ajax({
        type: "get",
        url: url,
//        dataType : 'json',
        contentType: "charset=shift_jis",
        cache: false,
         headers: {
          "Authorization": "Basic " + btoa(authuser + ":" + authpass)
         },
        success: function(data) {
            callback(data);
        }
    });
}

function cmdExecute(cmd, callback, arg1, arg2, arg3) {
    console.log(cmd);
    $.ajax({
        type: "post",
        url: "execute.php",
        cache: false,
        dataType : 'json',
        data: {post_cmd:cmd},
         headers: {
          "Authorization": "Basic " + btoa(authuser + ":" + authpass)
         },
        success: function(result){
        var data = result;
            if (callback != undefined)
                callback(data, arg1, arg2, arg3);
        }
    });
}

//
// 引数がIPアドレスとして正しいものか調べる
//
function isValidIPaddress(target, on) {
    var i;
    var confirm = document.getElementById('confirm');
    var factor;

//    if (target.indexOf(".0") != -1) {
//        if (on)
//            confirm.innerHTML = "<font color=\"red\">入力値に誤りがあります</font>";
//        return 1;
//    }
    factor = target.split('.');
    if (factor.length != 4) {
        if (on)
            confirm.innerHTML = "<font color=\"red\">入力値に誤りがあります</font>";
        return 1;
    }
    for (i = 0; i < 4; i++) {
        if (factor[i].match(/[^0-9]+/)) {
            if (on)
                confirm.innerHTML = "<font color=\"red\">入力値に誤りがあります</font>";
            return 1;
        }
    }
    for (i = 0; i < target.length; i++) {
        if (encodeURI(target.charAt(i)).length >= 4) {
            if (on)
                confirm.innerHTML = "<font color=\"red\">入力値に誤りがあります</font>";
            return 1;
        }
    }
    for (i = 0; i < 4; i++) {
        var num = Number(factor[i]);
        if (num == NaN || factor[i] < 0 || factor[i] > 255) {
            if (on)
                confirm.innerHTML = "<font color=\"red\">入力値に誤りがあります</font>";
            return 1;
        } 
    }

    confirm.innerHTML = " ";
    return 0;
}

//
// 入力されたIPアドレスからMACアドレスを調べる
//
function getMacAddress() {
    var cmd = "arp -a";
    cmdExecute(cmd, showMacAddress);
}

$(document).ready(function() {

    tlist = document.getElementById('target_list');
    clist = document.getElementById('target_list_cnt');
    logfd = document.getElementById('log');

    // コマンドの実行で管理者へ
   updateStatusMain();

    // データベースを開く
    getResource("database.txt", function(data) {
        var i, j, str, rows, line, num, cnt, rec;
        if (data != undefined) {
            str = data.split("\n");
            // 端末の登録
            for (i = 0; i < str.length; i++) {
                line = str[i].split(",");
                if (line.length != 4)
                    continue;
                rows = tlist.insertRow(-1);
                rows.style.height = "40";
                num = tlist.rows.length;
                for (j = 0; j < 8; j++) {
                    rows.insertCell(-1);
                }
                rows.cells[0].innerHTML = String(i + 1);
                rows.cells[1].innerHTML = line[0];
                rows.cells[2].innerHTML = line[1];
                rows.cells[3].innerHTML = line[2];
                rows.cells[4].innerHTML = "確認中";
                if (line[3].indexOf("auto") != -1) {
                    rows.cells[5].innerHTML = "自動起動";
                } else {
                    rows.cells[5].innerHTML = "手動  <input type=\"button\" value=\"起動\" onclick=\"offPortUse(\'" + line[0] + "\', 1);\">";
                }
                rows.cells[6].innerHTML = "<input type=\"button\" value=\"停止\" onclick=\"StopComputer(\'" + line[0] + "\');\">"; 
                rows.cells[7].innerHTML = "<input type=\"button\" value=\"削除\" onclick=\"deleteTarget(\'" + line[0] + "\');\">";
                rows.style.textAlign = "center";

                // 端末毎のPing欠落回数の設定
                rows = clist.insertRow(-1);
                num = clist.rows.length;
                cnt = "cnt_" + line[0];
                rec = "rec_" + line[0];
                rows.insertCell(-1);
                rows.insertCell(-1);
                rows.cells[0].innerHTML = "<input type=\"hidden\" id=\"" + cnt + "\">";
                rows.cells[1].innerHTML = "<input type=\"hidden\" id=\"" + rec + "\">";
                document.getElementById(cnt).value = 0;
                document.getElementById(rec).value = 0;
            }
            // MACアドレスと経路情報の取得と表示
            setTimeout(getMacAddress, 1000);
        }
        updateStatus();
    })

    // ログファイルを開く
    getResource("logfile.txt", function(data) {
        if (data != undefined) {
            var log = document.getElementById('log');
            str = data.split("\r\n");
            for (i = 0; i < str.length; i++) {
                if (i == 0) {
                    log.innerHTML = str[i];
                } else {
                    log.innerHTML = log.innerHTML + "<br>" + str[i];
                }
            }
        }
    })
});

function updateStatusMain() {
    updateStatus();
    setInterval(updateStatus, pingExecInterval);
}

//
// Pingによる疎通確認を行う
//
function updateStatus() {
    // 対象が無ければ何もしない
    if (tlist.rows.length > 1) {
        var i;
        for (i = 1; i < tlist.rows.length; i++){
            updateStatusEach(i)
        }
        intobj = setTimeout(function() {checkPing();}, 2000);
    }
}

//
// テーブルのNoを指定してPingによる疎通確認を行う
//
function updateStatusEach(id) {
    var target;
    var cmd = "nwolc -p ";

    if (tlist.rows.length == id)
        return;

    target = tlist.rows[id].cells[1].innerHTML;
    if (isValidIPaddress(target, 0))
        return;
    cmd = cmd + target + " -t " + pingWaitInterval;
    cmdExecute(cmd, checkLuaStatus);

}

//
// Pingの結果を確認するため、show status luaを実行する DUMMY
//
function checkPing() {
//
}

//
// Pingの結果を表示に反映させる
// ダウン時の自動起動を実行する
//
function checkLuaStatus(data) {
    var i, j, id;
    var str = String(data[0]).split(' ');
    var str1 = data;
    var str2;
    var target;
    var cel2,cel4, cel5;
    var cnt, rect, cnt_ele, rec_ele, cnt_num, rec_num;

    for (i = 1; i < tlist.rows.length; i++) {
        target = tlist.rows[i].cells[1].innerHTML;
        cel2 = tlist.rows[i].cells[2];
        cel4 = tlist.rows[i].cells[4];
        cel5 = tlist.rows[i].cells[5];
        cnt = "cnt_" + target;
        rec = "rec_" + target;
        cnt_ele = document.getElementById(cnt);
        rec_ele = document.getElementById(rec);
        cnt_num = Number(cnt_ele.value);
        rec_num = Number(rec_ele.value);

            if (str[0] == target) {
                if (str1[1] == 0) {
                  if (cel4.innerHTML.indexOf("停止処理中") == -1 && cel4.innerHTML.indexOf("再起動停止中") == -1 ) {
                    if (cel4.innerHTML == "OFF" || cel4.innerHTML.indexOf("確認中") != -1 || cel4.innerHTML.indexOf("WOL起動中") != -1 || cel4.innerHTML.indexOf("再起動中") != -1 ) {
                        makeStateLog(target, 1);
                    }
                    cel4.innerHTML = "ON";
                    cel4.style.backgroundColor = "#7bdf2e";
                    cnt_ele.value = 0;
                    rec_ele.value = 0;
                    // MACアドレスの更新
                    if (cel2.innerHTML == "") {
                        getMacAddress();
                    }
                  }
                } else {
                    if (cnt_num < pingErrorCount) {
                        cnt_num += 1;
                    }
                    cnt_ele.value = cnt_num;
                    if (rec_num > 0) {
                        break;
                    }
                    if (cnt_num >= pingErrorCount || cel4.innerHTML.indexOf("再起動停止中") != -1 ) {
                      if (cel4.innerHTML.indexOf("WOL起動中") == -1  && cel4.innerHTML.indexOf("再起動中") == -1 ) {
                        if (cel4.innerHTML == "ON" || cel4.innerHTML.indexOf("確認中") != -1 || cel4.innerHTML.indexOf("停止処理中") != -1 || cel4.innerHTML.indexOf("再起動停止中") != -1 ) {
                            makeStateLog(target, 0);
                        }
                        if (cel4.innerHTML.indexOf("再起動停止中") != -1 ) {
                            cel4.innerHTML = "再起動中";
                            cel4.style.backgroundColor = "#87cefa";
                        } else {
                            cel4.innerHTML = "OFF";
                            cel4.style.backgroundColor = "#F83131";
                        }
                        if (cel5.innerHTML == "自動起動") {
                            // 自動起動
                            offPortUse(target, 0);
                        }
                      }
                    }
                }
                break;
            }
    }
}

//
// マジックバケットを送信する
//
function offPortUse(target, manual) {
    var i;
    var rec = "rec_" + target;
    var rec_ele = document.getElementById(rec);
    var ipaddr;
    var macaddr;
    var pstatus;

    for (i = 1; i < tlist.rows.length; i++) {
        if (tlist.rows[i].cells[1].innerHTML == target) {
            ipaddr = tlist.rows[i].cells[1].innerHTML;
            macaddr = tlist.rows[i].cells[2].innerHTML;
            pstatus = tlist.rows[i].cells[4].innerHTML;
            break;
        }
    }

    if (macaddr == "") {
        return;
    }
    if (pstatus == "ON") {
       cmd0 = "PsShutdown -r \\\\" + ipaddr + idpass;
//       cmd0 = "PsShutdown -accepteula -r \\\\" + ipaddr + idpass;
       tlist.rows[i].cells[4].innerHTML = "再起動停止中";
       tlist.rows[i].cells[4].style.backgroundColor = "#FFFF00";
       makePoeLog(manual, target, 1);
    } else {
       var mac = macaddr.replace(/:/g, '' );
       cmd0 = "nwolc -m " + mac;
       tlist.rows[i].cells[4].innerHTML = "WOL起動中";
       tlist.rows[i].cells[4].style.backgroundColor = "#FF9900";
       makePoeLog(manual, target, 0);
       rec_ele.value = 1;
    }
    cmdExecute(cmd0);


    // Pingの監視をリスタート
    clearTimeout(intobj);
    setTimeout(updateStatus, 3000);
}

//
// 給電設定変更のログを作る
//
function makePoeLog(manual, target, on) {
    var dateinfo = new Date();
    var year = dateinfo.getFullYear();
    var month = dateinfo.getMonth() + 1;
    var day = dateinfo.getDate();
    var hour = dateinfo.getHours();
    var minute = dateinfo.getMinutes();
    var second = dateinfo.getSeconds();
    var time;
    var method = (manual == 1) ? "[手動操作]" : "[自動起動]";
    if (on == 2) {
       var proc = "へ停止コマンドを送信しました";
       } else {
          if (on == 1) {
             var proc = "へ再起動コマンドを送信しました";
            } else {
               var proc =  "へマジックパケットを送信しました";
            }
       }
    var str;

    month = doubleNumber(month);
    day = doubleNumber(day);
    hour = doubleNumber(hour);
    minute = doubleNumber(minute);
    second = doubleNumber(second);
    time = year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second;
    str = time + " " + method + target + proc;
    logfd.innerHTML = str + "<br>" + logfd.innerHTML;
    LogSave(str);
}

//
// 監視状態変更のログを作る
//
function makeStateLog(target, on) {
    var dateinfo = new Date();
    var year = dateinfo.getFullYear();
    var month = dateinfo.getMonth() + 1;
    var day = dateinfo.getDate();
    var hour = dateinfo.getHours();
    var minute = dateinfo.getMinutes();
    var second = dateinfo.getSeconds();
    var time;
    var proc = (on == 1) ? "の状態が [<b><font color=\"green\">ON</font></b>] になりました" : "の状態が [<b><font color=\"red\">OFF</font></b>] になりました";
    var str;

    month = doubleNumber(month);
    day = doubleNumber(day);
    hour = doubleNumber(hour);
    minute = doubleNumber(minute);
    second = doubleNumber(second);
    time = year + "/" + month + "/" + day + " " + hour + ":" + minute + ":" + second;
    str = time + " " + target + proc;
    logfd.innerHTML = str + "<br>" + logfd.innerHTML;
    LogSave(str);
}

//
// 1桁の数字を2桁にする(1->01)
//
function doubleNumber(num) {
    num += "";
    if (num.length === 1) {
        num = "0" + num;
    }
    return num;
}

//
// 端末の登録を行う
//
function registDevice1() {
    var i, num, ret;
    var target = document.forms.id_target_form.elements.target.value;
    var comment = document.forms.id_target_form.elements.comment.value;
    var confirm = document.getElementById('confirm');
    var cmd;

    // IPアドレス入力チェック
    if (isValidIPaddress(target, 1))
        return;

    // コメント入力チェック
    if (comment.indexOf(',') != -1) {
        confirm.innerHTML = "<font color=\"red\">コメントに , は使用できません</font>";
        return;
    }

    // 設定数上限の確認
    if (tlist.rows.length == maxEntryNum + 1) {
        confirm.innerHTML = "<font color=\"red\">これ以上登録できません</font>";
        return;
    }

    // 管理者への昇格およびARPテーブルの更新
    cmd = "nwolc -p " + target;
    cmdExecute(cmd);
    setTimeout(registDevice2, 1000);
}

function registDevice2() {
    var i, num;
    var ret = 0;
    var target = document.forms.id_target_form.elements.target.value;
    var comment = document.forms.id_target_form.elements.comment.value;
    var recover = document.forms.id_target_form.id_recover1.checked;
    var rows, cnt, rec, cmd;

    // 登録情報の上書き
    for (i = 1; i < tlist.rows.length; i++) {
        rows = tlist.rows[i];
        if (rows.cells[1].innerHTML == target) {
            rows.cells[3].innerHTML = comment;
            rows.cells[4].innerHTML = "確認中";
            if (recover == true)
                rows.cells[5].innerHTML = "自動起動";
            else
                rows.cells[5].innerHTML = "手動  <input type=\"button\" value=\"起動\" onclick=\"offPortUse(\'" + target + "\', 1);\">";
            return;
        }
    }

    // 端末の登録
    rows = tlist.insertRow(-1);
    rows.style.height = "40";
    num = tlist.rows.length;
    for (i = 0; i < 8; i++) {
        rows.insertCell(-1);
    }
    rows.cells[0].innerHTML = String(num - 1);
    rows.cells[1].innerHTML = target;
    rows.cells[2].innerHTML = "";
    rows.cells[3].innerHTML = comment;
    rows.cells[4].innerHTML = "確認中";
    if (recover == true)
        rows.cells[5].innerHTML = "自動起動";
    else
        rows.cells[5].innerHTML = "手動  <input type=\"button\" value=\"起動\" onclick=\"offPortUse(\'" + target + "\', 1);\">";
        rows.cells[6].innerHTML = "<input type=\"button\" value=\"停止\" onclick=\"StopComputer(\'" + target + "\');\">";
        rows.cells[7].innerHTML = "<input type=\"button\" value=\"削除\" onclick=\"deleteTarget(\'" + target + "\');\">";
        rows.style.textAlign = "center";


    // 端末毎のPing欠落回数の設定
    rows = clist.insertRow(-1);
    num = clist.rows.length;
    cnt = "cnt_" + target;
    rec = "rec_" + target;
    rows.insertCell(-1);
    rows.insertCell(-1);
    rows.cells[0].innerHTML = "<input type=\"hidden\" id=\"" + cnt + "\">";
    rows.cells[1].innerHTML = "<input type=\"hidden\" id=\"" + rec + "\">";
    document.getElementById(cnt).value = 0;
    document.getElementById(rec).value = 0;

    // MACアドレスと経路情報の取得と表示
    setTimeout(getMacAddress, 1000);

    return;
}

//
// 導出したMACアドレスを表示する
//
function showMacAddress(data) {
    var i, j;
    var str1 = JSON.stringify(data).split(',');
    var str2;
    var target_ip;
    var macaddr;

    for (i = 1; i < tlist.rows.length; i++) {
        target_ip = tlist.rows[i].cells[1].innerHTML;
        for (j = 0; j < str1.length; j++) {
            str1[j] = str1[j].replace(/{"|"}/g, '');
            str1[j] = str1[j].replace(/":"/g, ' ');
            str2 = str1[j].replace(/"/g, '');
            str2 = str2.split(' ');
            if (str2[0] == target_ip) {          // ip
                macaddr = str2[1].replace( /-/g , ':' );
                tlist.rows[i].cells[2].innerHTML = macaddr;
            }
        }
    }
}

//
// 現状の監視対象のリストを保存する
//
async function saveTargetList() {
   var i, str;
var rows, line = "";
   await DataClear();
   for (i = 1; i < tlist.rows.length; i++) {
//  async内のawaitから呼び出した関数(_sleep)からPromiseが返されるまで待機
       await _sleep(300);
       rows = tlist.rows[i];
       if (isValidIPaddress(rows.cells[1].innerHTML, 0))
           continue;
       rows.cells[0].style.backgroundColor = "#F83131";
       line = rows.cells[1].innerHTML + "," + rows.cells[2].innerHTML + "," + rows.cells[3].innerHTML + ",";
       str = rows.cells[5].innerHTML.split(" ");
       if (str[0] == "自動起動")
           line = line + "auto ";
       else
           line = line + "manual ";
       await DataSave(line);
       await _sleep(300);
       rows.cells[0].style.backgroundColor = "#7bdf2e";
   }
}

//
// sleep処理
//
function _sleep(ms) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}

//
// データベースを保存する
//
function DataSave(data) {
dataurl = "database.php"

  $(function(){
    //ajax送信
    $.ajax({
        url : dataurl,
        type : "POST",
        dataType : 'json',
        data : {post_data:data},
        headers: {
         "Authorization": "Basic " + btoa(authuser + ":" + authpass)
        },
        xhrFields: {
          withCredentials: true
        }
    });
  });
}

//
// 現状の監視対象のリストをIPアドレスで並び替えする
//
function datasort() {
dataurl = "datasort.php"

  $(function(){
    //ajax送信
    $.ajax({
        url : dataurl,
        type : "POST",
        dataType : 'json',
        headers: {
         "Authorization": "Basic " + btoa(authuser + ":" + authpass)
        }
    }).always(
        function() {
           location.reload(true);
        }
      );
  });
}

//
// コンピューターを停止する
//
function StopComputer(target) {
  var i;
  var ipaddr;
  var macaddr;
  var pstatus;

  for (i = 1; i < tlist.rows.length; i++) {
      if (tlist.rows[i].cells[1].innerHTML == target) {
          ipaddr = tlist.rows[i].cells[1].innerHTML;
          macaddr = tlist.rows[i].cells[2].innerHTML;
          pstatus = tlist.rows[i].cells[4].innerHTML;
          break;
      }
  }

  if (macaddr == "" || pstatus == "OFF") {
      return;
  }
  cmd0 = "PsShutdown -s \\\\" + ipaddr + idpass;
//  cmd0 = "PsShutdown -accepteula -s \\\\" + ipaddr + idpass;
  cmdExecute(cmd0);
  tlist.rows[i].cells[4].innerHTML = "停止処理中";
  tlist.rows[i].cells[4].style.backgroundColor = "#FF69B4";
  makePoeLog(1, target, 2);
}

//
// データベースを消去する
//
function DataClear() {
  datacurl = "dataclear.php"
  var xhr = new XMLHttpRequest();
  xhr.open('POST', datacurl);
  xhr.setRequestHeader("Authorization" , authstr);
  xhr.withCredentials = true;
  xhr.send();
}

//
// 監視対象一覧から1行削除する
//
function deleteTarget(target) {
    var i, rows;

    // tlistから削除
    for (i = 1; i < tlist.rows.length; i++) {
        if (tlist.rows[i].cells[1].innerHTML == target) {
            tlist.deleteRow(i);
            break;
        }
    }
    for (i = 1; i < tlist.rows.length; i++) {
        tlist.rows[i].cells[0].innerHTML = i;
    }

    // clistから削除
    for (i = 0; i < clist.rows.length; i++) {
        if (clist.rows[i].cells[0].innerHTML.indexOf("id=\"cnt_" + target + "\"") != -1) {
            clist.deleteRow(i);
            break;
        }
    }
}

//
// ログファイルを保存する
//
function LogSave(log) {
logurl = "logfile.php"

  $(function(){
    //ajax送信
    $.ajax({
        url : logurl,
        type : 'POST',
        dataType : 'json',
        data : {post_log:log},
        headers: {
         'Authorization': 'Basic ' + btoa(authuser + ':' + authpass)
        },
        xhrFields: {
          withCredentials: true
        }
    });
  });
}

//
//
// ログファイルを消去する
//
function LogClear() {
  logcurl = "logclear.php"
  var xhr = new XMLHttpRequest();
  xhr.open('POST', logcurl);
  xhr.setRequestHeader("Authorization" , authstr);
  xhr.withCredentials = true;
  xhr.send();
}

//
// ログファイルの削除とログフィールドのクリア
//
function clearLog() {
    logfd.innerHTML = "";
    LogClear();
}

wol.html

1.監視画面に停止欄の追加

wol.html

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<!-- wol.html -->
<html>

<head>
    <meta http-equiv="Content-Type" content="text/html; charset=shift_jis">
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.3/jquery.min.js"></script>
    <script type="text/javascript" src="wol_monitor.js"></script>
    <link href="custom.css" rel="stylesheet" type="text/css"/>
<title>ネットワーク機器監視システム</title>
</head>

<body>

<!--監視対象の登録-->
<div id="register_field">
<img class="register_title_icon" src="register_icon.png" alt=""><h1 class="register_title">監視対象の登録</h1>
<form name="target_form" id="id_target_form" action="">
    <table border="1">
        <tbody>
            <tr>
                <td class="td">監視対象IPアドレス</td>
                <td><input type="text" name="target" id="id_target" maxlength="15"></td>
            </tr>
            <tr>
            <td class="td">機器名・コメント</td>
               <td><input type="text" name="comment" id="id_comment" maxlength="30"></td>
            </tr>
                <tr>
                <td class="td">WOL起動方法</td>
                <td><input type="radio" name="recover" id="id_recover1" value="auto">自動起動<br>
                   <input type="radio" name="recover" id="id_recover2" value="manual" checked="">手動起動</td>
           </tr>
      </tbody>
   </table>
   <div id="confirm"> </div>
   <div align="right"><input type="button" class="normal_button" value="登  録" onclick="registDevice1();"></div>
</form>
</div>

<hr>


<!--起動状況ログ-->
<div id="log_field">
    <img class="log_title_icon" src="log.png" alt=""><h1 class="log_title">起動状況ログ</h1>
    <div id="log" class="log"></div>
    <div> </div>
    <div align="right"><input type="button" class="normal_button" value="ログのクリア" onclick="clearLog();"></div>
</div>

<hr>

<!--監視対象一覧-->
<div id="list_field">
<div id="list_field_title">
    <img class="list_title_icon" src="camera.png" alt=""><h1 class="list_title">監視対象一覧</h1>
</div>
<div class="target_poe" id="target_poe"></div>
<table align="right">
<tr align="right" style="padding-bottom: 10px; margin-top: -20px;">
   <td> <input type="button" class="blink_button" value="監視対象情報を保存" onclick="saveTargetList();"/>
   <td> <input type="button" class="blink_button" value="並び替え" onclick="datasort();"/>
</tr>
</table>
<table border="1" id="target_list" width="100%"><tbody>
    <tr class="tr">
        <th width="6">№</th>
        <th width="130">IPアドレス</th>
        <th width="130">MACアドレス</th>
        <th>機器名・<br>コメント</th>
        <th width="100">状態</th>
        <th width="200">起動方法</th>
        <th width="100">停止</th>
        <th width="100">削除</th>
   </tr>
</tbody></table>

<table border="0" id="target_list_cnt"><tbody>
</tbody></table>
</div>

</body>
</html>

 

考察

PowerShellのバージョンが低いWindows Server2008R2では動作しなかったので,Windows Server2012R2に更新してからテストしたので完成が遅くなってしまった。
Windows Server 2008R2 ではコマンドプロンプトやPHPのexecから「powershell -command」が効かないが,今回追加したPsShutdownは動作するので,Windowsのバージョンアップは必要なかった。(-_-;)汗
本ツールを使用することで従来のVPN+リモートデスクトップを使用した再起動・停止が不要となった。\(^。^)/
だたし自分自身のMACアドレスは「arp -a」コマンドから取得できないので,監視対象に登録してもMACアドレス欄は空白となる。あまり必要性は無いが今後時間があれば考えたい。
自動起動の機能を入れているが当初は使い道が無いのではとの懸念もあったが,開発用サーバー数台が立ち上がっていないときなどに,開発用サーバー専用のツール画面を作成しておいて,全て自動起動登録しておけば,ツール画面を立ち上げただけで数台のサーバーが起動でき,起動確認も可能となる。また,作業が終了すればツール画面から停止させることができる。がすぐに画面を閉じないと再起動してしまう。(-_-;)汗
結局手動起動登録しておいて起動ボタン押下のほうがよいのかも。
(*・ω・)(*-ω-)(*・ω・)(*-ω-)ウンウン♪
これから若干の手直しが必要だが,ほぼこれで完成形に近いものとなった。

これまでの開発履歴
1.YAMAHAルーターのカスタムGUIのHTMLファイルからWOL (ルーター完結)
 (2022/12/04)
 パソコンのリモート操作環境構築 – na-blog – Page 2 (na-3.com)

2.サーバーからWOL (サーバー完結)
 (2022/12/04)
 パソコンのリモート操作環境構築 – na-blog – Page 3 (na-3.com)

3.YAMAHAルーターのカスタムGUIからWOL。データーはサーバー保存 (ルーターとサーバー併用)
 (2023/02/14)
 ヤマハルーター・PoE監視デモサンプルをWOL用に改造 – na-blog (na-3.com)

4.上記を元にサーバー完結に変更
 (2023/03/20)
 Word Pressに「Wake On LAN」機能を埋め込み – na-blog (na-3.com)

5.上記に再起動・停止機能追加 (本記事)
 (2023/11/12)
 パソコンの起動・停止監視ツール – na-blog (na-3.com)

デモ画面

デモ仕様制限
1.テストデータを使用しており保存や並び替えは不可。
2.サーバーへのコマンド出力や返答も不可。
3.ベーシック認証は外してある。
4.並び替えでリセット

おまけ

必要ファイルをまとめてみた。
以前に作成したものも含めて下記のとおり
1~15のファイルはWordPressのフォルダー(例\wordpress\wp-content\wol)に保存してアクセス制限を設定する。
1 wol_monitor.js  メインの処理
2 wol.html
3 custom.css
4 database.php
5 logfile.php
6 execute.php
7 datasort.php
8 dataclear.php
9 logclear.php
10 database.txt
11 camera.png
12 log.png
13 logo.png
14 register_icon.png
15 skeleton.png

以下の16,17はパスの通ったフォルダーに保存。(PHPインストールフォルダーのルート等 例:D:\Program Files\php)
16 nwolc.exe
17 psshutdown.exe