Windowsの時刻合わせをW32Timeサービス非依存で自動化する


# 今回使うあれそれ
- C#
- NTPサーバー
- タスクスケジューラ

# 大まかな実装方針
方針も何も上記3要素が挙がればやることなどひとつしかありません。
NTPサーバーから取得した時刻情報で内部の時計を設定するプログラムをC#で作り、それをタスクスケジューラで定期的に起動します。

# 実装
以下、全文です。
9割以上[ここのサンプルコード](http://nonsoft.la.coocan.jp/SoftSample/CS.NET/SampleNtpDateTime.html "NTPサーバの現在日時をシステム時計に
設定するサンプル(C#.NET)")のままです。
気合い入れて作る場合はログの出力機能とかも追加で実装してあげるといいと思います。

```C#:Program.cs
using System;


namespace PCClockUpDater {
    class Program {
        static void Main(string[] args) {
            SetSysTime(getTimeByNTP(args.Length == 0 ? "time.windows.com" : args[0]));
        }

        static DateTime getTimeByNTP(string NTPServerString) {
            // NTPサーバへの接続用UDP生成
            System.Net.Sockets.UdpClient objSck;
            System.Net.IPEndPoint ipAny = new System.Net.IPEndPoint(System.Net.IPAddress.Any, 0);
            objSck = new System.Net.Sockets.UdpClient(ipAny);

            // NTPサーバへのリクエスト送信
            Byte[] sdat = new Byte[48];
            sdat[0] = 0xB;
            objSck.Send(sdat, sdat.GetLength(0), NTPServerString, 123);

            // NTPサーバから日時データ受信
            Byte[] rdat = objSck.Receive(ref ipAny);

            // 1900年1月1日からの経過時間(日時分秒)
            long lngAllS; // 1900年1月1日からの経過秒数
            long lngD;    // 日
            long lngH;    // 時
            long lngM;    // 分
            long lngS;    // 秒

            // 1900年1月1日からの経過秒数計算
            lngAllS = (long)(
                      rdat[40] * Math.Pow(2, (8 * 3)) +
                      rdat[41] * Math.Pow(2, (8 * 2)) +
                      rdat[42] * Math.Pow(2, (8 * 1)) +
                      rdat[43]);

            // 1900年1月1日からの経過(日時分秒)計算
            lngD = lngAllS / (24 * 60 * 60); // 日
            lngS = lngAllS % (24 * 60 * 60); // 残りの秒数
            lngH = lngS / (60 * 60);         // 時
            lngS = lngS % (60 * 60);         // 残りの秒数
            lngM = lngS / 60;                // 分
            lngS = lngS % 60;                // 秒

            // 現在の日時(DateTime)計算
            DateTime dtTime = new DateTime(1900, 1, 1);
            dtTime = dtTime.AddDays(lngD);
            dtTime = dtTime.AddHours(lngH);
            dtTime = dtTime.AddMinutes(lngM);
            dtTime = dtTime.AddSeconds(lngS);

            // グリニッジ標準時から日本時間への変更
            dtTime = dtTime.AddHours(9);

            // 現在の日時表示
            Console.WriteLine(dtTime);

            return dtTime;
        }

        // システム時計の日時設定
        static private void SetSysTime(DateTime dtm) {
            SystemTime sTime = new SystemTime();
            sTime.wYear = (ushort)dtm.Year;
            sTime.wMonth = (ushort)dtm.Month;
            sTime.wDay = (ushort)dtm.Day;
            sTime.wHour = (ushort)dtm.Hour;
            sTime.wMinute = (ushort)dtm.Minute;
            sTime.wSecond = (ushort)dtm.Second;
            sTime.wMiliseconds = (ushort)dtm.Millisecond;
            SetLocalTime(ref sTime);
        }

        // システム時計の日時設定APIの引数
        [System.Runtime.InteropServices.StructLayout(
         System.Runtime.InteropServices.LayoutKind.Sequential)]
        public struct SystemTime {
            public ushort wYear;
            public ushort wMonth;
            public ushort wDayOfWeek;
            public ushort wDay;
            public ushort wHour;
            public ushort wMinute;
            public ushort wSecond;
            public ushort wMiliseconds;
        }

        // システム時計の日時設定APIの定義
        [System.Runtime.InteropServices.DllImport("kernel32.dll")]
        public static extern bool SetLocalTime(ref SystemTime sysTime);

    }
}

```

まるっとそのまま使っては面白くないので、コマンドライン引数によりNTPサーバーを指定できるようにしました。
指定が無ければWindowsがデフォルトで使っているのと同じtime.windows.comで処理します。

```C#:Main()部分
static void Main(string[] args) {
    SetSysTime(getTime(args.Length == 0 ? "time.windows.com" : args[0]));
}
```

# 注意点
Vista以降のWindowsでは、そのまま実行しても時刻を取得できるだけで内部時計への反映は弾かれます。
これはユーザーアカウント制御機能に邪魔されるからだそうで、管理者権限での実行により解決します。
上手く動かない場合、コーディング時も運用時もまずは権限を疑うが吉です。

# 運用
生成した実行ファイルをタスクスケジューラから叩きます。
設定はお好みで大丈夫ですが、Vista以降のWindowsなら管理者権限での実行が必須です。
![2021-11-06_12h17_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164419/e6f9d881-fce5-555c-ea34-ca4cabf974f6.png)
「最上位の特権で実行する」を選択しても上手く動かず実行結果欄には権限が無い旨のメッセージが表示される場合は、たぶん一度再起動してあげればちゃんと動きます。

また、前述の通りに実装した場合はコマンドライン引数によりNTPサーバーの指定が可能です。
![2021-11-06_12h11_28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164419/ad00e3a1-45fd-dcc0-fe6d-9d7d029c3059.png)




# 余談その1
特に意識していなかったのでこれを読んでいる人も意識する必要は無いと思いますが、時刻設定部分の処理はWIN32APIを使っているようです。
何らかの事情でそれでは困るという場合は、VB.NETのプロパティにC#からアクセスすることで解決するっぽいです。
Microsoft.VisualBasic.dllを参照するらしいので[ここ](https://dobon.net/vb/dotnet/system/setlocaltime.html "システム時計の日時を設定する")を参考にしてみてください。

# 余談その2
他に何か機能があれば話は別ですが、これだけならわざわざ画面を出すようなものでもないです。画面無しで走ってくれるようにしてあげるとハッピーになれると思います。
コンソールアプリケーションとしてプロジェクトを作成した場合は、プロジェクトのプロパティから「出力の種類」を「Windowsアプリケーション」にすれば画面が表示されなくなります。![2021-11-06_11h44_19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164419/fc3b63bc-6794-56cf-81fe-080051002ace.png)

# 余談その3
エラー処理については何も考えていないので、セルフで適宜実装してください。
実行時の成否をWindowsの通知機能で表示できたら便利な気がします。
[こちらの記事](https://qiita.com/horyu/items/4da2a79c6fa7311d350a "Windowsで通知用アプリを作ろうとした時に悩んだ諸々")が参考になりそうです。

# 余談その4
NTPサーバーからの時刻取得は、いつでも確実に正確な時刻を取得できる保証はありません。回線速度やアクセス状況により〝大きな誤差〟が生じる可能性が情報通信研究機構でも[明言](https://www.nict.go.jp/JST/JST5.html "日本標準時")されています。
5秒や10秒ズレても困るような運用が想定されるのであれば

- 複数のNTPサーバーから時刻を取得して比較、一定以上の乖離があるようならば再取得
- 電波時計モジュールとラズパイで自前のNTPサーバーを用意

などを検討するのもありかもです。

# おしまい
以上、お疲れ様でした！