diff --git a/README.md b/README.md index f99ec03..6d728db 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,15 @@ ## 它是一个简单的时钟同步小工具,目前适用于 Windows 操作系统 -因为 Windows 默认的时间同步似乎有点缓慢,所以我制作了它。 +因为 Windows 默认的时间同步似乎有点缓慢,所以我制作了它, +它现在支持通过 TCP 协议,从 HTTP(s) 协议的网站上获取时间。 ## 下载地址 - [本地下载](https://github.com/lalakii/WINtp/releases) -- [蓝奏云 1](https://a01.lanzoui.com/iHXP02ecxgwf) -- [蓝奏云 2](https://a01.lanzout.com/iHXP02ecxgwf) -- [蓝奏云 3](https://a01.lanzouv.com/iHXP02ecxgwf) +- [蓝奏云 1](https://a01.lanzoui.com/iqtKI2ef4bkh) +- [蓝奏云 2](https://a01.lanzout.com/iqtKI2ef4bkh) +- [蓝奏云 3](https://a01.lanzouv.com/iqtKI2ef4bkh) ### 如何使用它? @@ -31,37 +32,39 @@ ### 配置文件示例 ```conf -# 一个简单的时间同步小工具【WINtp】 +# 一个简单的时间同步小工具 WINtp # -# 此配置文件不是必须的,如果你不需要配置,可以将其清空,只需要保留时间同步 AutoSyncTime = true +# 此配置文件是必须的,用于存储程序运行所需的配置参数 # -# 默认内置了"time.asia.apple.com", "time.windows.com", "rhel.pool.ntp.org",这3个ntp服务器地址 +# 如果你需要恢复默认值,直接删除它,程序启动后会自动生成一份新的 # -# 如果你想要自定义ntp服务器,可以直接写在这个文件里面,每行一个 -# -# 可以先用ping测试ntp服务器的延迟怎么样噢 +# 如果你想要自定义ntp服务器,按照规律添加即可。 +Ntps = time.asia.apple.com;ntp.tencent.com;ntp.aliyun.com;rhel.pool.ntp.org; -cn.pool.ntp.org -asia.pool.ntp.org -cn.ntp.org.cn -ntp.aliyun.com -time.apple.com -time.cloudflare.com -centos.pool.ntp.org +# 【自动同步系统时间】开关,如果被注释了,或是值为 false 将不会同步系统时间(防止杀软误报添加这个配置项) +AutoSyncTime = true -# 自动同步系统时间开关,如果取消注释将不会同步时间(防止杀软误报添加了这个配置项) +# 如果因为某些原因,无法访问 UDP 端口。 +# 这时你可以取消下面的注释,程序将通过 TCP 协议,从指定的网站上获取时间 ~ +# 这个选项不会与 Ntps = 冲突,它们可以同时存在,也可以二选一。 +# 可以访问 UDP 端口时,通常不要启用它。 # -AutoSyncTime = true +# Urls = www.baidu.com;www.qq.com;www.google.com; +# +# 默认从 80 端口获取数据,如需使用 443,取消下面的注释 +# +# UseSSL = true # -# 如果【不想使用我内置的ntp地址】,取消注释掉下面的行 +# 如果【需要打印详细信息】,取消下面的注释 # -# useDefaultNtpServer = false +# Verbose = true # -# 如果【需要打印详细信息】,取消注释下面的行 +# 如果【需要自定义超时】,修改下面的参数,单位是毫秒。默认30秒无法获取时间即超时,程序退出。 +# 网络较差时可将数值调大一些 # -# verbose = true +# Timeout = 30000 # #################### # by lalaki.cn ## diff --git a/README_en.md b/README_en.md index 8cb14cf..3a8dc79 100644 --- a/README_en.md +++ b/README_en.md @@ -1,4 +1,4 @@ -# WINtp —— NTP time synchronization client, a faster implementation of time synchronization. +# WINtp — NTP time synchronization client, providing a faster implementation for time synchronization. [![Latest Version](https://img.shields.io/github/v/release/lalakii/WINtp?logo=github)](https://github.com/lalakii/WINtp/releases) [![License: Apache-2.0 (shields.io)](https://img.shields.io/badge/License-Apache--2.0-c02041?logo=apache)](LICENSE) @@ -9,59 +9,61 @@ [ [中文](README.md) | [English](README_en.md) ] -## It is a simple clock synchronization tool, currently suitable for Windows operating system. +## It is a simple clock synchronization tool, currently suitable for Windows operating systems -Because the default time synchronization in Windows seems a bit slow, I created it. +Because the default time synchronization in Windows seems a bit slow, I created this tool. It now supports retrieving time from websites using the TCP protocol over HTTP(s). -## Download +## Downloads - [Github](https://github.com/lalakii/WINtp/releases) -- [Lanzou 1](https://a01.lanzoui.com/iHXP02ecxgwf) -- [Lanzou 2](https://a01.lanzout.com/iHXP02ecxgwf) -- [Lanzou 3](https://a01.lanzouv.com/iHXP02ecxgwf) +- [Lanzou 1](https://a01.lanzoui.com/iqtKI2ef4bkh) +- [Lanzou 2](https://a01.lanzout.com/iqtKI2ef4bkh) +- [Lanzou 3](https://a01.lanzouv.com/iqtKI2ef4bkh) ### How to use it? -Typically, you just need to double-click to run it. The software has no UI interface; it will automatically synchronize the system time upon startup. +Typically, you just need to double-click to run it. The software has no UI interface; it will automatically synchronize the system time after starting. -After completion, it exits immediately and runs no background processes. +Once completed, it exits immediately and runs no background processes. -It is recommended to install it as a system service, or you can set it to start at boot according to your preference. +It is recommended to install it as a system service, but you can also set it as a startup item according to your preference. -### Configuration File Example +### Configuration ```conf -# A Simple NTP Client【WINtp】 +# A simple time synchronization tool WINtp # -# This configuration file is not mandatory. If you do not need configuration, you can clear it, just keep the time synchronization setting: AutoSyncTime = true. +# This configuration file is required to store the configuration parameters needed for the program to run # -# By default, the built-in NTP server addresses are "time.asia.apple.com", "time.windows.com", and "rhel.pool.ntp.org". +# If you need to restore the default values, simply delete this file, and a new one will be automatically generated when the program starts # -# If you want to customize the NTP servers, you can write them directly in this file, one per line. -# -# You can first use ping to test the latency of the NTP servers. +# If you want to customize the NTP server, just add it according to the format. +Ntps = time.asia.apple.com;ntp.tencent.com;ntp.aliyun.com;rhel.pool.ntp.org; -cn.pool.ntp.org -asia.pool.ntp.org -cn.ntp.org.cn -ntp.aliyun.com -time.apple.com -time.cloudflare.com -centos.pool.ntp.org +# The [Automatic System Time Synchronization] switch. If it is commented out or set to false, the system time will not be synchronized (this configuration item is added to prevent false positives from antivirus software). +AutoSyncTime = true -# Automatic system time synchronization switch. If uncommented, time synchronization will not occur (this configuration item was added to prevent false positives from antivirus software). +# If, for some reason, you are unable to access the UDP port. +# At this point, you can uncomment the line below, and the program will retrieve the time from the specified website using the TCP protocol. +# This option will not conflict with "Ntps =", they can coexist or be used as alternatives. +# When the UDP port is accessible, it is generally not recommended to enable this option. # -AutoSyncTime = true +# Urls = www.baidu.com;www.qq.com;www.google.com; +# +# By default, data is retrieved from port 80. To use port 443, uncomment the line below. +# +# UseSSL = true # -# If you do not want to use my built-in NTP address, uncomment the line below. +# If [Detailed Information Printing] is needed, uncomment the line below. # -# useDefaultNtpServer = false +# Verbose = true # -# If you need to print detailed information, uncomment the line below. +# If [Custom Timeout] is needed, modify the parameter below. The unit is milliseconds. By default, if the time cannot be retrieved within 30 seconds, the program will time out and exit. +# You can increase the value when the network is poor. # -# verbose = true +# Timeout = 30000 # #################### # by lalaki.cn ## diff --git a/WINtp.cs b/WINtp.cs index 14a5c4b..3e5eae0 100644 --- a/WINtp.cs +++ b/WINtp.cs @@ -1,33 +1,42 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.Sockets; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using System.Threading; [assembly: AssemblyProduct("WINtp")] -[assembly: AssemblyVersion("1.3.0.0")] -[assembly: AssemblyFileVersion("1.3.0.0")] -[assembly: AssemblyTitle("简单好用的时间同步小工具")] +[assembly: AssemblyVersion("1.4.0.0")] +[assembly: AssemblyFileVersion("1.4.0.0")] +[assembly: AssemblyTitle("A Simple NTP Client")] [assembly: AssemblyCopyright("Copyright (C) 2024 lalaki.cn")] [System.ComponentModel.DesignerCategory("")] public class WINtp : System.ServiceProcess.ServiceBase { + private const int NTP_TYPE = 0; + private const int HTTP_TYPE = 1; + private const int WINTP_TIMEDOUT_ERROR = 1; + private const int PROCESS_START_ERROR = 2; + private const int NTP_CONNECTION_ERROR = 3; private static readonly Assembly asm = typeof(WINtp).Assembly; private static readonly ManualResetEvent evt = new(false); private CancellationTokenSource cts; private bool autoSyncTime = false; private static SystemTime st; private bool verbose = false; + private bool useSSL = false; + private static readonly char[] comments = ['-', '#', '/', ';', '<', '=', ':']; public static void Main(string[] args) { using var ntp = new WINtp(); - if (args != null && args.Contains("-k", StringComparer.OrdinalIgnoreCase)) + if (args != null && new string[] { "-k", "/k" }.Contains(args.LastOrDefault(), StringComparer.OrdinalIgnoreCase)) { Run(ntp); } @@ -37,12 +46,17 @@ public static void Main(string[] args) } } - public void LoadProfile(object args) + private struct TimeServer + { + public int type; + public string host; + } + + private void LoadProfile(object args) { var timeout = 0; - var useDefaultNtpServer = true; var cfgPath = Path.Combine(Path.GetDirectoryName(asm.Location), "ntp.ini"); - List ntpServers = ["time.asia.apple.com", "time.windows.com", "rhel.pool.ntp.org"]; + List servers = []; var hasCfg = File.Exists(cfgPath); if (hasCfg) { @@ -60,130 +74,188 @@ public void LoadProfile(object args) while (reader.Peek() != -1) { var itemCfg = ("" + reader.ReadLine()).Trim().Replace(" ", "").ToLower(); //为了兼容性才这样写 - if (StartsWithoutComment(itemCfg)) + if (StartsWithoutComment(itemCfg) && itemCfg.Contains("=")) { - if (itemCfg.Contains("=")) + if (itemCfg.Contains("autosynctime=true")) { - if (itemCfg.Contains("usedefaultntpserver=false")) - { - useDefaultNtpServer = false; - } - else if (itemCfg.Contains("autosynctime=true")) - { - autoSyncTime = true; - } - else if (itemCfg.Contains("verbose=true")) - { - verbose = true; - } - else if (itemCfg.Contains("timeout=")) - { - int.TryParse(itemCfg.Substring(itemCfg.IndexOf('=') + 1), out timeout); - } + autoSyncTime = true; } - else if (!ntpServers.Contains(itemCfg)) + else if (itemCfg.Contains("verbose=true")) { - ntpServers.Add(itemCfg); + verbose = true; + } + else if (itemCfg.Contains("usessl=true")) + { + useSSL = true; + } + else if (itemCfg.Contains("timeout=")) + { + int.TryParse(itemCfg.Substring(itemCfg.IndexOf('=') + 1), out timeout); + } + else + { + var isNtp = itemCfg.Contains("ntps="); + if (isNtp || itemCfg.Contains("urls=")) + { + var strArr = (itemCfg.Substring(itemCfg.IndexOf('=') + 1) + "").Trim().Split(';'); + foreach (var it in strArr) + { + var mHost = it.Trim(); + if (mHost != "") + { + var serv = new TimeServer + { + host = mHost, + type = isNtp ? NTP_TYPE : HTTP_TYPE + }; + if (!servers.Contains(serv)) + { + servers.Add(serv); + } + } + } + } } } } - if (!useDefaultNtpServer && ntpServers.Count > 3) + if (servers.Count != 0) { - ntpServers.RemoveRange(0, 3); - } - using (cts = new CancellationTokenSource()) - { - ntpServers.ForEach(it => ThreadPool.QueueUserWorkItem(GetNtpTime, it)); - if (!evt.WaitOne(timeout <= 0 ? 30000 : timeout)) + using (cts = new CancellationTokenSource()) { - Environment.FailFast(asm.Location + " 同步时间失败,网络不畅通。"); - } - if (args != null) - { - Thread.Sleep(3000); - Stop(); + servers.ForEach(it => ThreadPool.QueueUserWorkItem(GetNetTime, it)); + bool hasTimedOut = !evt.WaitOne(timeout <= 0 ? 30000 : timeout); + evt.Close(); + if (hasTimedOut) + { + Environment.FailFast(GetFailureMessage(WINTP_TIMEDOUT_ERROR)); + } + if (args != null) + { + Thread.Sleep(3000); + Stop(); + } } } } - public static bool StartsWithoutComment(string str) + private static bool StartsWithoutComment(string str) { - return str != "" && !new char[] { '-', '#', '/', ';', '<', '=', ':' }.Contains(str.First()); + return str != "" && !comments.Contains(str.First()); } // stackoverflow.com/a/3294698/162671 - private static ulong SwapEndianness(byte[] data, int index) + private static ulong SwapEndianness(ulong x) { - ulong x = BitConverter.ToUInt32(data, index); return (((x >> 24) & 0x000000ff) | ((x >> 8) & 0x0000ff00) | ((x << 8) & 0x00ff0000) | ((x << 24) & 0xff000000)) * 1000; } - private void GetNtpTime(object serv) + private void GetNetTime(object obj) { - var time = new DateTime(1900, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); - byte[] data = new byte[48]; - data[0] = 0x1B; + var serv = (TimeServer)obj; IPAddress[] ip = null; - while (true) + DateTime time = DateTime.MinValue; + while (!cts.IsCancellationRequested) { try { - ip = Dns.GetHostAddresses((string)serv); - using Socket socket = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.SendTimeout = socket.ReceiveTimeout = 5000; - socket.Connect(ip, 123); - socket.Send(data); - socket.Receive(data); - time = time.AddMilliseconds((SwapEndianness(data, 44) >> 32) + SwapEndianness(data, 40)); + ip = Dns.GetHostAddresses(serv.host); + time = serv.type == HTTP_TYPE ? GetHttpTime(ip, serv.host) : GetNtpTime(ip); } catch { - if (ip == null && !cts.IsCancellationRequested) + if (!cts.IsCancellationRequested && ip == null) { Thread.Sleep(1000); continue; } else { + if (verbose) + { + using var log = new EventLog("Application", ".", string.Format("{0} cannot connect to \"{1}\"", asm.GetName().Name, serv)); + log.WriteEntry(GetFailureMessage(NTP_CONNECTION_ERROR), EventLogEntryType.Error); + } Thread.CurrentThread.Abort(); } } break; } - if (!cts.IsCancellationRequested) + if (!DateTime.MinValue.Equals(time) && !cts.IsCancellationRequested) { cts.Cancel(); - SetSystemTime(time, serv); + if (autoSyncTime) + { + Win32SetSystemTime(time); + } + if (verbose) + { + var msg = string.Format("/c echo {0} Get datetime from \"{1}\", AutoSyncTime: {2} && pause", time.ToLocalTime(), serv.host, autoSyncTime); + try + { + Process.Start("cmd.exe", msg); + } + catch + { + Environment.FailFast(GetFailureMessage(PROCESS_START_ERROR)); + } + } evt.Set(); } } - private void SetSystemTime(DateTime time, object serv) + private DateTime GetHttpTime(IPAddress[] ip, string host) { - st.wYear = (short)time.Year; - st.wMonth = (short)time.Month; - st.wDay = (short)time.Day; - st.wHour = (short)time.Hour; - st.wMinute = (short)time.Minute; - st.wSecond = (short)time.Second; - if (autoSyncTime) - { - SetSystemTime(ref st); - } - if (verbose) + using var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + client.Connect(ip, useSSL ? 443 : 80); + client.Send(Encoding.ASCII.GetBytes(string.Format("HEAD / HTTP/1.1\r\nHost: {0}\r\nConnection: close\r\n\r\n", host))); + var reader = new StreamReader(new NetworkStream(client)); + while (reader.Peek() != -1) { - var msg = string.Format("/c echo Get Datetime from \"{0}\", {1} && pause", serv, time.ToLocalTime()); - try + var line = reader.ReadLine(); + if (line.StartsWith("Date:", StringComparison.OrdinalIgnoreCase)) { - System.Diagnostics.Process.Start("cmd.exe", msg); + return Convert.ToDateTime(line.Substring(5)).ToUniversalTime();//rfc1123 } - catch + } + throw new SocketException(); + } + + private static DateTime GetNtpTime(IPAddress[] ip) + { + var time = new DateTime(1900, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + var data = new byte[48]; + data[0] = 0x1B; + using Socket socket = new(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + socket.Connect(ip, 123); + socket.Send(data); + socket.Receive(data); + unsafe + { + fixed (byte* ptr = data) { - Environment.FailFast(asm.Location + " 无法输出详细信息,请检查此电脑的 PATH 环境变量。"); + var intPart = (uint*)(ptr + 40); + var fractPart = (uint*)(ptr + 44); + return time.AddMilliseconds(SwapEndianness(*intPart) + (SwapEndianness(*fractPart) >> 32)); } } } + private static string GetFailureMessage(int errorCode) + { + return string.Format("程序路径: {0}\r\n代码: {1}, 可在 https://wintp.sourceforge.io/ 获取帮助。", asm.Location, errorCode); + } + + private static void Win32SetSystemTime(DateTime time) + { + st.wYear = (short)time.Year; + st.wMonth = (short)time.Month; + st.wDay = (short)time.Day; + st.wHour = (short)time.Hour; + st.wMinute = (short)time.Minute; + st.wSecond = (short)time.Second; + SetSystemTime(ref st); + } + protected override void OnStart(string[] args) { ThreadPool.QueueUserWorkItem(LoadProfile, args); diff --git a/WINtp.csproj b/WINtp.csproj index e134298..182093b 100644 --- a/WINtp.csproj +++ b/WINtp.csproj @@ -2,6 +2,7 @@ + preview {D4A656D9-E8D8-4EAA-8EC3-0C4DD9A45EE6} WinExe @@ -9,32 +10,33 @@ WINtp v4.6.2 512 - false + true false WINtp.manifest bin\ - S3903;S1186; + S3903; true AnyCPU true - - - + true + False + none + false + - + - - - IF NOT "%25DEV_PRIVATE_KEY%25"=="" ( + OnOutputUpdated + IF NOT "%2525DEV_PRIVATE_KEY%2525"=="" ( lalaki_sign "$(TargetPath)" ) diff --git a/ntp.ini b/ntp.ini index 58e7f60..1713d8b 100644 --- a/ntp.ini +++ b/ntp.ini @@ -1,39 +1,37 @@ # 一个简单的时间同步小工具 WINtp # -# 此配置文件不是必须的,如果你不需要配置,可以将其清空,只需要保留时间同步 AutoSyncTime = true +# 此配置文件是必须的,用于存储程序运行所需的配置参数 # -# 默认内置了"time.asia.apple.com", "time.windows.com", "rhel.pool.ntp.org",这3个ntp服务器地址 +# 如果你需要恢复默认值,直接删除它,程序启动后会自动生成一份新的 # -# 如果你想要自定义ntp服务器,可以直接写在这个文件里面,每行一个 +# 如果你想要自定义ntp服务器,按照规律添加即可。 +Ntps = time.asia.apple.com;ntp.tencent.com;ntp.aliyun.com;rhel.pool.ntp.org; -cn.pool.ntp.org -asia.pool.ntp.org -cn.ntp.org.cn -ntp.aliyun.com -time.apple.com -time.cloudflare.com -centos.pool.ntp.org +# 【自动同步系统时间】开关,如果被注释了,或是值为 false 将不会同步系统时间(防止杀软误报添加这个配置项) +# AutoSyncTime = true -# 自动同步系统时间开关,如果取消注释将不会同步时间(防止杀软误报添加这个配置项) +# 如果因为某些原因,无法访问 UDP 端口。 +# 这时你可以取消下面的注释,程序将通过 TCP 协议,从指定的网站上获取时间 ~ +# 这个选项不会与 Ntps = 冲突,它们可以同时存在,也可以二选一。 +# 可以访问 UDP 端口时,通常不要启用它。 # -AutoSyncTime = true +# Urls = www.baidu.com;www.qq.com;www.google.com; # -# 如果【不想使用我内置的ntp地址】,取消注释掉下面的行 +# 默认从 80 端口获取数据,如需使用 443,取消下面的注释 # -# useDefaultNtpServer = false +# UseSSL = true # -# 如果【需要打印详细信息】,取消注释下面的行 +# 如果【需要打印详细信息】,取消下面的注释 # -# verbose = true +# Verbose = true # # 如果【需要自定义超时】,修改下面的参数,单位是毫秒。默认30秒无法获取时间即超时,程序退出。 +# 网络较差时可将数值调大一些 # -# timeout=30000 +# Timeout=30000 # -# 如有需要自行将其注册为系统服务,在开机时启动一次就好~~ 同步完成会立即退出,无后台。 -# -############# +#################### # by lalaki.cn ## -############# \ No newline at end of file +#################### \ No newline at end of file