Почему я не могу установить системное время на время перехода на летнее время

Мои времена, они меняются, то есть потому, что они мне нужны. Я тестирую некоторые случаи использования планировщика, который я использую, и это включает в себя поведение вокруг переходов в летнее время.

Код

Из этого сообщения У меня есть рабочий метод, который позволяет мне программно изменить системную дату (перепрошивая большую часть кода):

[StructLayout(LayoutKind.Sequential)]
public struct SYSTEMTIME
{
    public short wYear;
    public short wMonth;
    public short wDayOfWeek;
    public short wDay;
    public short wHour;
    public short wMinute;
    public short wSecond;
    public short wMilliseconds;
}

[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetSystemTime(ref SYSTEMTIME st);

и для моего удобства я просто обертываю, что в этой функции, которую я действительно вызываю:

public static void SetSytemDateTime(DateTime timeToSet)
{
    DateTime uniTime = timeToSet.ToUniversalTime();
    SYSTEMTIME setTime = new SYSTEMTIME()
    {
        wYear = (short)uniTime.Year,
        wMonth = (short)uniTime.Month,
        wDay = (short)uniTime.Day,
        wHour = (short)uniTime.Hour,
        wMinute = (short)uniTime.Minute,
        wSecond = (short)uniTime.Second,
        wMilliseconds = (short)uniTime.Millisecond
    };

    SetSystemTime(ref setTime);
}

Необходимо дополнительное преобразование в универсальное время, иначе я не увижу дату, которую я передал методу в своих часах (вниз на панели задач).

Теперь это отлично работает с учетом этого кода, например:

DateTime timeToSet = new DateTime(2014, 3, 10, 1, 59, 59, 0);
Console.WriteLine("Attemting to set time to {0}", timeToSet);
SetSytemDateTime(timeToSet);
Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);

Thread.Sleep(TimeSpan.FromSeconds(5));

DateTime actualSystemTime = GetNetworkTime();
SetSytemDateTime(actualSystemTime);

Метод GetNetworkTime на самом деле просто захвачен из здесь, поэтому я могу вернуть часы в "реальное" время после тестирования, вы можете его игнорировать для этого вопроса.

Пример вывода # 1

Это делает, что вы ожидаете (немецкий формат DateTime, не путайте): cmdli output attemting to change system time 1

И в панели задач я также вижу, чего я ожидаю:

taskbar clock showing time 1

Пример вывода # 2 (переход на летнее время)

Но теперь к странной части: Переключите первую строку вызывающего кода для

// one second before transition to daylight saving time in Berlin
DateTime timeToSet = new DateTime(2015, 3, 29, 1, 59, 59, 0);

Теперь вывод командной строки, по-видимому, удовлетворяет тому, что мы ожидаем увидеть: cmdli output attemting to change system time 2

Но затем мы посмотрим вниз справа от нашей панели задач и войдем в хмурые земли и увидим время, которое на самом деле не должно существовать на этот день:

taskbar clock showing time 2

Пример вывода # 3 (Переход от летнего времени)

Теперь самое смешное, когда я пытаюсь сделать то же самое для второго до перехода от летнего времени, изменение получает "принято" (снова переключает первую строку кода вызова):

// one second before transition out of daylight saving time in Berlin
DateTime timeToSet = new DateTime(2014, 10, 26, 2, 59, 59, 0);

Мы видим, что мы ожидаем в выводе командной строки:

cmdli output attemting to change system time 3

также в часах панели задач:

taskbar clock showing time 3

Но эта история также имеет печальный конец, пусть один второй проход, и вы ожидаете, что часы показывают 2 'o часы, но вместо этого:

taskbar clock showing time 4

Какое время должно произойти через час в этот конкретный день (если вы вручную переключите время в окнах, это произойдет, как ожидается).

Вопрос

Теперь, чего я здесь не вижу, почему я не могу настроить таргетинг на секунду до перехода на летнее время и почему я не вижу переход от летнего времени, когда я делаю DateTime, программно изменяется таким образом?

Что мне нужно добавить/установить, чтобы я мог?

Ответ 1

Что Эндрю Мортон и Марк предлагал быть на месте!

Хотя я должен сказать, что я до сих пор не понимаю, почему я не смог бы достичь того же, используя SetSystemTime (Выполнение преобразований универсальное время, конечно), он действительно работает, используя SetLocalTime.

Пожалуйста, обратите внимание на сообщение Marc, я просто пишу это, поэтому есть полный пример кода, чтобы продемонстрировать, как будут выглядеть тесты, если они будут успешно выполняться.

Этот код запускает 3 теста:

  • установить время системы в произвольное время (не приближаясь к летнему переходному времени), подождать 5 секунд, а затем установить системное время на правильное время и снова подождать 5 секунд.
  • установите время системы на одну секунду перед переходом на летнее время, подождите 5 секунд и установите системное время на правильное время и подождите 5 секунд снова.
  • установите время системы на одну секунду до перехода из летнего времени, подождите 5 секунд и установите системное время на правильное время и подождите 5 секунд снова.

(Проводя полный рабочий пример, но обратите внимание, чтобы воспроизвести это в вашей системе, вам, возможно, придется использовать разные значения DateTime, из-за летнее время (если вы не работаете в часовом поясе в Берлине), а также вам может понадобиться [или просто нравится] использовать другой NTP-сервер в GetNetworkTime())

// complete example use this as Program.cs in a console application project
namespace SystemDateManipulator101
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;

    /// <summary>
    /// Program class.
    /// </summary>
    public class Program
    {
        #region Methods

        static void Main(string[] args)
        {
            // test one: set system time to a random time that is not near daylight savings time transition
            DateTime timeToSet = new DateTime(2014, 5, 5, 4, 59, 59, 0);
            Console.WriteLine("timeToSet Kind: {0}", timeToSet.Kind);
            Console.WriteLine("Attemting to set time to {0}", timeToSet);
            SetLocalSytemDateTime(timeToSet);
            Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);
            Thread.Sleep(TimeSpan.FromSeconds(5));
            DateTime actualSystemTime = GetNetworkTime();
            SetLocalSytemDateTime(actualSystemTime);

            Thread.Sleep(TimeSpan.FromSeconds(5));

            // test two: set system time to one second before transition to daylight savings time in Berlin
            timeToSet = new DateTime(2015, 3, 29, 1, 59, 59, 0);
            Console.WriteLine("timeToSet Kind: {0}", timeToSet.Kind);
            Console.WriteLine("Attemting to set time to {0}", timeToSet);
            SetLocalSytemDateTime(timeToSet);
            Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);
            Thread.Sleep(TimeSpan.FromSeconds(5));
            actualSystemTime = GetNetworkTime();
            SetLocalSytemDateTime(actualSystemTime);

            Thread.Sleep(TimeSpan.FromSeconds(5));

            // test three: set system time to one second before transition out of daylight savings time in Berlin
            timeToSet = new DateTime(2014, 10, 26, 2, 59, 59, 0);
            Console.WriteLine("timeToSet Kind: {0}", timeToSet.Kind);
            Console.WriteLine("Attemting to set time to {0}", timeToSet);
            SetLocalSytemDateTime(timeToSet);
            Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);
            Thread.Sleep(TimeSpan.FromSeconds(5));
            actualSystemTime = GetNetworkTime();
            SetLocalSytemDateTime(actualSystemTime);

            Console.Read();
        }

        #endregion

        // https://stackoverflow.com/a/12150289/162671
        public static DateTime GetNetworkTime()
        {
            //default Windows time server
            const string ntpServer = "time.windows.com";

            // NTP message size - 16 bytes of the digest (RFC 2030)
            var ntpData = new byte[48];

            //Setting the Leap Indicator, Version Number and Mode values
            ntpData[0] = 0x1B; //LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)

            var addresses = Dns.GetHostEntry(ntpServer).AddressList;

            //The UDP port number assigned to NTP is 123
            var ipEndPoint = new IPEndPoint(addresses[0], 123);
            //NTP uses UDP
            var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            socket.Connect(ipEndPoint);

            //Stops code hang if NTP is blocked
            socket.ReceiveTimeout = 3000;

            socket.Send(ntpData);
            socket.Receive(ntpData);
            socket.Close();

            //Offset to get to the "Transmit Timestamp" field (time at which the reply 
            //departed the server for the client, in 64-bit timestamp format."
            const byte serverReplyTime = 40;

            //Get the seconds part
            ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);

            //Get the seconds fraction
            ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);

            //Convert From big-endian to little-endian
            intPart = SwapEndianness(intPart);
            fractPart = SwapEndianness(fractPart);

            var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);

            //**UTC** time
            var networkDateTime = (new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc)).AddMilliseconds((long)milliseconds);

            return networkDateTime.ToLocalTime();
        }

        // stackoverflow.com/a/3294698/162671
        static uint SwapEndianness(ulong x)
        {
            return (uint)(((x & 0x000000ff) << 24) +
                           ((x & 0x0000ff00) << 8) +
                           ((x & 0x00ff0000) >> 8) +
                           ((x & 0xff000000) >> 24));
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct SYSTEMTIME
        {
            public short wYear;
            public short wMonth;
            public short wDayOfWeek;
            public short wDay;
            public short wHour;
            public short wMinute;
            public short wSecond;
            public short wMilliseconds;
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool SetSystemTime(ref SYSTEMTIME st);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool SetLocalTime(ref SYSTEMTIME st);

        public static void SetSystemDateTime(DateTime timeToSet)
        {
            DateTime uniTime = timeToSet.ToUniversalTime();
            SYSTEMTIME setTime = new SYSTEMTIME()
            {
                wYear = (short)uniTime.Year,
                wMonth = (short)uniTime.Month,
                wDay = (short)uniTime.Day,
                wHour = (short)uniTime.Hour,
                wMinute = (short)uniTime.Minute,
                wSecond = (short)uniTime.Second,
                wMilliseconds = (short)uniTime.Millisecond
            };

            SetSystemTime(ref setTime);
        }

        public static void SetLocalSytemDateTime(DateTime timeToSet)
        {
            SYSTEMTIME setTime = new SYSTEMTIME()
            {
                wYear = (short)timeToSet.Year,
                wMonth = (short)timeToSet.Month,
                wDay = (short)timeToSet.Day,
                wHour = (short)timeToSet.Hour,
                wMinute = (short)timeToSet.Minute,
                wSecond = (short)timeToSet.Second,
                wMilliseconds = (short)timeToSet.Millisecond
            };

            SetLocalTime(ref setTime);
            // yes this second call is really necessary, because the system uses the daylight saving time setting of the current time, not the new time you are setting
            // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724936%28v=vs.85%29.aspx
            SetLocalTime(ref setTime);
        }
    }
}

Если вы хотите испытать странность, которую я описал в своем вопросе, вы все равно можете просто заменить вызовы на SetLocalSytemDateTime на SetSytemDateTime.

Ответ 2

Я могу объяснить ваш пример №3.

  • 26 октября 2014 года в Германии, когда часы приближаются к 3:00 утра, час составляет reset до 2:00 утра, дважды повторяя значения с 2:00:00 до 2:59:59. Это называется переходом "назад".

  • Когда вы вызываете ToUniversalTime в локальное время, которое находится в этом переходе, оно неоднозначно..Net предположит, что вы имели в виду исходное значение в стандартное время, а не дневное время.

  • Другими словами, время 2:59:59 существует дважды, а .Net принимает второй.

  • Поэтому, через секунду, действительно, 3:00:00.

Если вы хотите контролировать это, вы должны использовать тип DateTimeOffset вместо типа DateTime, где вы можете явно указать смещение. Вы также можете проверить это условие с помощью TimeZoneInfo.IsAmbiguousTime.

Что касается вашего примера # 2, оказалось бы, что SetSystemTime имеет ту же проблему, что и для SetLocalTime в MSDN. Когда вы устанавливаете системное время, вы правильно устанавливаете время по UTC, но для отображения используются текущие настройки для преобразования в локальный часовой пояс.

В частности, параметр ActiveTimeBias в реестре используется для преобразования UTC-to-local. Подробнее в этой статье.

Из эксперимента, казалось бы, если время больше, чем на час от перехода на летнее время, то оно также запускает обновление до ActiveTimeBias, и все это хорошо.

Итак, чтобы повторить, вы получите это поведение только в том случае, если верно следующее:

  • Вы устанавливаете время, которое находится в стандартное время.

  • Текущее местное время находится в дневном свете.

  • Вы устанавливаете время, которое не более чем за час до перехода spring -forward DST.

Имея это в виду, я написал этот код, который должен обойти обе проблемы:

public static void SetSystemDateTimeSafely(DateTime timeToSet,
                                           bool withEarlierWhenAmbiguous = true)
{
    TimeZoneInfo timeZone = TimeZoneInfo.Local;
    bool isAmbiguous = timeZone.IsAmbiguousTime(timeToSet);

    DateTime utcTimeToSet = timeToSet.ToUniversalTime();
    if (isAmbiguous && withEarlierWhenAmbiguous)
        utcTimeToSet = utcTimeToSet.AddHours(-1);

    TimeSpan offset = timeZone.GetUtcOffset(utcTimeToSet);
    TimeSpan offsetOneHourLater = timeZone.GetUtcOffset(utcTimeToSet.AddHours(1));

    if (offset != offsetOneHourLater)
    {
        TimeSpan currentOffset = timeZone.GetUtcOffset(DateTime.UtcNow);
        if (offset != currentOffset)
        {
            SetSystemDateTime(utcTimeToSet.AddHours(-1));
        }
    }

    SetSystemDateTime(utcTimeToSet);
}

private static void SetSystemDateTime(DateTime utcDateTime)
{
    if (utcDateTime.Kind != DateTimeKind.Utc)
    {
        throw new ArgumentException();
    }

    SYSTEMTIME st = new SYSTEMTIME
    {
        wYear = (short)utcDateTime.Year,
        wMonth = (short)utcDateTime.Month,
        wDay = (short)utcDateTime.Day,
        wHour = (short)utcDateTime.Hour,
        wMinute = (short)utcDateTime.Minute,
        wSecond = (short)utcDateTime.Second,
        wMilliseconds = (short)utcDateTime.Millisecond
    };

    SetSystemTime(ref st);
}

[StructLayout(LayoutKind.Sequential)]
public struct SYSTEMTIME
{
    public short wYear;
    public short wMonth;
    public short wDayOfWeek;
    public short wDay;
    public short wHour;
    public short wMinute;
    public short wSecond;
    public short wMilliseconds;
}

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetSystemTime(ref SYSTEMTIME st);

Теперь вы можете вызывать SetSystemDateTimeSafely с любой датой, которая вам нравится, и она компенсирует это нечетное поведение.

Это работает, сначала устанавливая значение, которое находится перед проблематичным диапазоном, но только при необходимости. Затем он начинает устанавливать правильное значение сразу после.

Единственным недостатком, о котором я могу думать, является то, что он поднимет два сообщения WM_TIMECHANGE, которые могут сбивать с толку при чтении в системном событии журналы.

Если вы оставите параметр withEarlierWhenAmbiguous на нем по умолчанию true, у него будет поведение выбора первого экземпляра, который вы ожидали от вашего примера # 3. Если вы установите его на false, у него будет поведение по умолчанию .NET по выбору второго экземпляра.

Ответ 3

Это только предположение, но документы MSDN в SetSystemTime (основная функция, которую вы вызываете) говорят, что она работает в UTC, которая по определению не имеет понятия о летнем времени. Я предполагаю, что окна просто "делают то, что вы говорите", и тот факт, что время "незаконно" (с точки зрения того, как мы выражаем локальные времена), действительно не вступает в игру.

Функция SetSystemTime

Использование SetLocalTime может делать то, что вы хотите, хотя документы в этом состоянии используют "текущую информацию о часовом поясе" (предположительно пользователь, а не систему), чтобы определить летнее время, которое также не может быть тем, что вы хотите для воспроизводимого теста.