require "./env" class Time::Location def __cached_zone=(zone) @cached_zone = zone end def self.__clear_location_cache @@local = nil @@location_cache.clear end end ZONEINFO_ZIP = datapath("zoneinfo.zip") def with_zoneinfo(path = ZONEINFO_ZIP, &) with_env("TZDIR": path) do Time::Location.__clear_location_cache yield end end def with_tz(tz, &) old_local = Time::Location.local begin with_env("TZ": tz) do # Reset local time zone Time::Location.local = Time::Location.load_local yield end ensure Time::Location.local = old_local end end {% if flag?(:win32) %} lib LibC struct LUID lowPart : DWORD highPart : Long end struct LUID_AND_ATTRIBUTES luid : LUID attributes : DWORD end struct TOKEN_PRIVILEGES privilegeCount : DWORD privileges : LUID_AND_ATTRIBUTES[1] end TOKEN_QUERY = 0x0008 TOKEN_ADJUST_PRIVILEGES = 0x0020 TokenPrivileges = 3 SE_PRIVILEGE_ENABLED = 0x00000002_u32 fun OpenProcessToken(processHandle : HANDLE, desiredAccess : DWORD, tokenHandle : HANDLE*) : BOOL fun GetTokenInformation(tokenHandle : HANDLE, tokenInformationClass : Int, tokenInformation : Void*, tokenInformationLength : DWORD, returnLength : DWORD*) : BOOL fun LookupPrivilegeValueW(lpSystemName : LPWSTR, lpName : LPWSTR, lpLuid : LUID*) : BOOL fun AdjustTokenPrivileges(tokenHandle : HANDLE, disableAllPrivileges : BOOL, newState : TOKEN_PRIVILEGES*, bufferLength : DWORD, previousState : TOKEN_PRIVILEGES*, returnLength : DWORD*) : BOOL fun SetDynamicTimeZoneInformation(lpTimeZoneInformation : DYNAMIC_TIME_ZONE_INFORMATION*) : BOOL end module Crystal::System::Time private SeTimeZonePrivilege = System.wstr_literal "SeTimeZonePrivilege" # Enable the `SeTimeZonePrivilege` privilege before changing the system time # zone. This is necessary because the privilege is by default granted but # disabled for any new process. This only needs to be done once per run. class_getter?(time_zone_privilege_enabled : Bool) { detect_time_zone_privilege_enabled? } private def self.detect_time_zone_privilege_enabled? : Bool if LibC.LookupPrivilegeValueW(nil, SeTimeZonePrivilege, out time_zone_luid) == 0 raise RuntimeError.from_winerror("LookupPrivilegeValueW") end # if the process token already has the privilege, and the privilege is already enabled, # we don't need to do anything else if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_QUERY, out token) != 0 begin LibC.GetTokenInformation(token, LibC::TokenPrivileges, nil, 0, out len) buf = Pointer(UInt8).malloc(len).as(LibC::TOKEN_PRIVILEGES*) LibC.GetTokenInformation(token, LibC::TokenPrivileges, buf, len, out _) privileges = Slice.new(pointerof(buf.value.@privileges).as(LibC::LUID_AND_ATTRIBUTES*), buf.value.privilegeCount) # if the process token doesn't have the privilege, there is no way # `AdjustTokenPrivileges` could grant or enable it privilege = privileges.find(&.luid.== time_zone_luid) return false unless privilege return true if privilege.attributes.bits_set?(LibC::SE_PRIVILEGE_ENABLED) ensure LibC.CloseHandle(token) end end if LibC.OpenProcessToken(LibC.GetCurrentProcess, LibC::TOKEN_ADJUST_PRIVILEGES, out adjust_token) != 0 new_privileges = LibC::TOKEN_PRIVILEGES.new( privilegeCount: 1, privileges: StaticArray[ LibC::LUID_AND_ATTRIBUTES.new( luid: time_zone_luid, attributes: LibC::SE_PRIVILEGE_ENABLED, ), ], ) if LibC.AdjustTokenPrivileges(adjust_token, 0, pointerof(new_privileges), 0, nil, nil) != 0 return true if WinError.value.error_success? end end false end end def with_system_time_zone(dtzi : LibC::DYNAMIC_TIME_ZONE_INFORMATION, *, file = __FILE__, line = __LINE__, &) unless Crystal::System::Time.time_zone_privilege_enabled? pending! "Unable to set system time zone", file: file, line: line end LibC.GetDynamicTimeZoneInformation(out old_dtzi) unless LibC.SetDynamicTimeZoneInformation(pointerof(dtzi)) != 0 error = WinError.value raise RuntimeError.from_os_error("Failed to set system time zone", error) unless error.error_privilege_not_held? pending! "Unable to set system time zone", file: file, line: line end begin yield ensure LibC.SetDynamicTimeZoneInformation(pointerof(old_dtzi)) end end {% end %}