diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 615b2b078b..f95f9cc8bd 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -3915,9 +3915,10 @@ FROM pg_stat_activity; Sets the time zone used for timestamps written in the server log. Unlike , this value is cluster-wide, so that all sessions will report timestamps consistently. - If not explicitly set, the server initializes this variable to the - time zone specified by its system environment. See for more information. + The built-in default is GMT, but that is typically + overridden in postgresql.conf; initdb + will install a setting there corresponding to its system environment. + See for more information. This parameter can only be set in the postgresql.conf file or on the server command line. @@ -4963,9 +4964,10 @@ SET XML OPTION { DOCUMENT | CONTENT }; Sets the time zone for displaying and interpreting time stamps. - If not explicitly set, the server initializes this variable to the - time zone specified by its system environment. See for more information. + The built-in default is GMT, but that is typically + overridden in postgresql.conf; initdb + will install a setting there corresponding to its system environment. + See for more information. diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index 89b531fa1d..e7b3098f28 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -2286,7 +2286,7 @@ January 8 04:05:06 1999 PST but continue to be prone to arbitrary changes, particularly with respect to daylight-savings rules. PostgreSQL uses the widely-used - zoneinfo time zone database for information about + zoneinfo (Olson) time zone database for information about historical time zone rules. For times in the future, the assumption is that the latest known rules for a given time zone will continue to be observed indefinitely far into the future. @@ -2432,26 +2432,9 @@ January 8 04:05:06 1999 PST The configuration parameter can be set in the file postgresql.conf, or in any of the other standard ways described in . - There are also several special ways to set it: + There are also some special ways to set it: - - - If timezone is not specified in - postgresql.conf or as a server command-line option, - the server attempts to use the value of the TZ - environment variable as the default time zone. If TZ - is not defined or is not any of the time zone names known to - PostgreSQL, the server attempts to - determine the operating system's default time zone by checking the - behavior of the C library function localtime(). The - default time zone is selected as the closest match among - PostgreSQL's known time zones. - (These rules are also used to choose the default value of - , if not specified.) - - - The SQL command SET TIME ZONE diff --git a/doc/src/sgml/ref/set.sgml b/doc/src/sgml/ref/set.sgml index 40e1bd4e60..21745db462 100644 --- a/doc/src/sgml/ref/set.sgml +++ b/doc/src/sgml/ref/set.sgml @@ -239,9 +239,7 @@ SELECT setseed(value); Set the time zone to your local time zone (that is, the - server's default value of timezone; if this - has not been explicitly set anywhere, it will be the zone that - the server's operating system defaults to). + server's default value of timezone). diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c index e3dd472832..4fe08df350 100644 --- a/src/backend/bootstrap/bootstrap.c +++ b/src/backend/bootstrap/bootstrap.c @@ -333,10 +333,6 @@ AuxiliaryProcessMain(int argc, char *argv[]) { if (!SelectConfigFiles(userDoption, progname)) proc_exit(1); - /* If timezone is not set, determine what the OS uses */ - pg_timezone_initialize(); - /* If timezone_abbreviations is not set, select default */ - pg_timezone_abbrev_initialize(); } /* Validate we have been given a reasonable-looking DataDir */ diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c index 239acd0785..87c54b7d48 100644 --- a/src/backend/commands/variable.c +++ b/src/backend/commands/variable.c @@ -259,23 +259,6 @@ check_timezone(char **newval, void **extra, GucSource source) char *endptr; double hours; - if (*newval == NULL) - { - /* - * The boot_val given for TimeZone in guc.c is NULL. When we see this - * we just do nothing. If this isn't overridden from the config file - * then pg_timezone_initialize() will eventually select a default - * value from the environment. This hack has two purposes: to avoid - * wasting cycles loading values that might soon be overridden from - * the config file, and to avoid trying to read the timezone files - * during InitializeGUCOptions(). The latter doesn't work in an - * EXEC_BACKEND subprocess because my_exec_path hasn't been set yet - * and so we can't locate PGSHAREDIR. - */ - Assert(source == PGC_S_DEFAULT); - return true; - } - /* * Initialize the "extra" struct that will be passed to assign_timezone. * We don't want to change any of the three global variables except as @@ -374,7 +357,7 @@ check_timezone(char **newval, void **extra, GucSource source) return false; } - if (!tz_acceptable(new_tz)) + if (!pg_tz_acceptable(new_tz)) { GUC_check_errmsg("time zone \"%s\" appears to use leap seconds", *newval); @@ -427,10 +410,6 @@ assign_timezone(const char *newval, void *extra) { timezone_extra *myextra = (timezone_extra *) extra; - /* Do nothing for the boot_val default of NULL */ - if (!myextra) - return; - session_timezone = myextra->session_timezone; CTimeZone = myextra->CTimeZone; HasCTZSet = myextra->HasCTZSet; @@ -490,20 +469,8 @@ check_log_timezone(char **newval, void **extra, GucSource source) { pg_tz *new_tz; - if (*newval == NULL) - { - /* - * The boot_val given for log_timezone in guc.c is NULL. When we see - * this we just do nothing. If this isn't overridden from the config - * file then pg_timezone_initialize() will eventually select a default - * value from the environment. - */ - Assert(source == PGC_S_DEFAULT); - return true; - } - /* - * Otherwise assume it is a timezone name, and try to load it. + * Assume it is a timezone name, and try to load it. */ new_tz = pg_tzset(*newval); @@ -513,7 +480,7 @@ check_log_timezone(char **newval, void **extra, GucSource source) return false; } - if (!tz_acceptable(new_tz)) + if (!pg_tz_acceptable(new_tz)) { GUC_check_errmsg("time zone \"%s\" appears to use leap seconds", *newval); @@ -538,10 +505,6 @@ check_log_timezone(char **newval, void **extra, GucSource source) void assign_log_timezone(const char *newval, void *extra) { - /* Do nothing for the boot_val default of NULL */ - if (!extra) - return; - log_timezone = *((pg_tz **) extra); } diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c index df4a2aa885..94b57fa7bb 100644 --- a/src/backend/postmaster/postmaster.c +++ b/src/backend/postmaster/postmaster.c @@ -795,21 +795,6 @@ PostmasterMain(int argc, char *argv[]) */ CreateDataDirLockFile(true); - /* - * If timezone is not set, determine what the OS uses. (In theory this - * should be done during GUC initialization, but because it can take as - * much as several seconds, we delay it until after we've created the - * postmaster.pid file. This prevents problems with boot scripts that - * expect the pidfile to appear quickly. Also, we avoid problems with - * trying to locate the timezone files too early in initialization.) - */ - pg_timezone_initialize(); - - /* - * Likewise, init timezone_abbreviations if not already set. - */ - pg_timezone_abbrev_initialize(); - /* * Initialize SSL library, if specified. */ diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index b708328926..072d50c395 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3537,10 +3537,6 @@ PostgresMain(int argc, char *argv[], const char *username) { if (!SelectConfigFiles(userDoption, progname)) proc_exit(1); - /* If timezone is not set, determine what the OS uses */ - pg_timezone_initialize(); - /* If timezone_abbreviations is not set, select default */ - pg_timezone_abbrev_initialize(); } /* diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c index 6e8e5aef4e..f0b3b1feb0 100644 --- a/src/backend/utils/error/elog.c +++ b/src/backend/utils/error/elog.c @@ -1805,24 +1805,20 @@ setup_formatted_log_time(void) { struct timeval tv; pg_time_t stamp_time; - pg_tz *tz; char msbuf[8]; gettimeofday(&tv, NULL); stamp_time = (pg_time_t) tv.tv_sec; /* - * Normally we print log timestamps in log_timezone, but during startup we - * could get here before that's set. If so, fall back to gmt_timezone - * (which guc.c ensures is set up before Log_line_prefix can become - * nonempty). + * Note: we expect that guc.c will ensure that log_timezone is set up + * (at least with a minimal GMT value) before Log_line_prefix can become + * nonempty or CSV mode can be selected. */ - tz = log_timezone ? log_timezone : gmt_timezone; - pg_strftime(formatted_log_time, FORMATTED_TS_LEN, /* leave room for milliseconds... */ "%Y-%m-%d %H:%M:%S %Z", - pg_localtime(&stamp_time, tz)); + pg_localtime(&stamp_time, log_timezone)); /* 'paste' milliseconds into place... */ sprintf(msbuf, ".%03d", (int) (tv.tv_usec / 1000)); @@ -1836,19 +1832,15 @@ static void setup_formatted_start_time(void) { pg_time_t stamp_time = (pg_time_t) MyStartTime; - pg_tz *tz; /* - * Normally we print log timestamps in log_timezone, but during startup we - * could get here before that's set. If so, fall back to gmt_timezone - * (which guc.c ensures is set up before Log_line_prefix can become - * nonempty). + * Note: we expect that guc.c will ensure that log_timezone is set up + * (at least with a minimal GMT value) before Log_line_prefix can become + * nonempty or CSV mode can be selected. */ - tz = log_timezone ? log_timezone : gmt_timezone; - pg_strftime(formatted_start_time, FORMATTED_TS_LEN, "%Y-%m-%d %H:%M:%S %Z", - pg_localtime(&stamp_time, tz)); + pg_localtime(&stamp_time, log_timezone)); } /* @@ -1947,14 +1939,11 @@ log_line_prefix(StringInfo buf, ErrorData *edata) case 't': { pg_time_t stamp_time = (pg_time_t) time(NULL); - pg_tz *tz; char strfbuf[128]; - tz = log_timezone ? log_timezone : gmt_timezone; - pg_strftime(strfbuf, sizeof(strfbuf), "%Y-%m-%d %H:%M:%S %Z", - pg_localtime(&stamp_time, tz)); + pg_localtime(&stamp_time, log_timezone)); appendStringInfoString(buf, strfbuf); } break; diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l index 70abf40a28..809307da8d 100644 --- a/src/backend/utils/misc/guc-file.l +++ b/src/backend/utils/misc/guc-file.l @@ -292,7 +292,6 @@ ProcessConfigFile(GucContext context) if (context == PGC_SIGHUP) { InitializeGUCOptionsFromEnvironment(); - pg_timezone_initialize(); pg_timezone_abbrev_initialize(); /* this selects SQL_ASCII in processes not connected to a database */ SetConfigOption("client_encoding", GetDatabaseEncodingName(), diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 6670997acd..a71729c2e7 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -187,6 +187,7 @@ static bool check_log_stats(bool *newval, void **extra, GucSource source); static bool check_canonical_path(char **newval, void **extra, GucSource source); static bool check_timezone_abbreviations(char **newval, void **extra, GucSource source); static void assign_timezone_abbreviations(const char *newval, void *extra); +static void pg_timezone_abbrev_initialize(void); static const char *show_archive_command(void); static void assign_tcp_keepalives_idle(int newval, void *extra); static void assign_tcp_keepalives_interval(int newval, void *extra); @@ -2547,7 +2548,7 @@ static struct config_string ConfigureNamesString[] = NULL }, &log_timezone_string, - NULL, + "GMT", check_log_timezone, assign_log_timezone, show_log_timezone }, @@ -2827,7 +2828,7 @@ static struct config_string ConfigureNamesString[] = GUC_REPORT }, &timezone_string, - NULL, + "GMT", check_timezone, assign_timezone, show_timezone }, { @@ -3817,7 +3818,7 @@ InitializeGUCOptions(void) * Before log_line_prefix could possibly receive a nonempty setting, make * sure that timezone processing is minimally alive (see elog.c). */ - pg_timezone_pre_initialize(); + pg_timezone_initialize(); /* * Build sorted array of all GUC variables. @@ -4114,6 +4115,15 @@ SelectConfigFiles(const char *userDoption, const char *progname) */ SetConfigOption("data_directory", DataDir, PGC_POSTMASTER, PGC_S_OVERRIDE); + /* + * If timezone_abbreviations wasn't set in the configuration file, install + * the default value. We do it this way because we can't safely install + * a "real" value until my_exec_path is set, which may not have happened + * when InitializeGUCOptions runs, so the bootstrap default value cannot + * be the real desired default. + */ + pg_timezone_abbrev_initialize(); + /* * Figure out where pg_hba.conf is, and make sure the path is absolute. */ @@ -8444,8 +8454,11 @@ assign_timezone_abbreviations(const char *newval, void *extra) * This is called after initial loading of postgresql.conf. If no * timezone_abbreviations setting was found therein, select default. * If a non-default value is already installed, nothing will happen. + * + * This can also be called from ProcessConfigFile to establish the default + * value after a postgresql.conf entry for it is removed. */ -void +static void pg_timezone_abbrev_initialize(void) { SetConfigOption("timezone_abbreviations", "Default", diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index e713defea2..a18f14ae25 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -406,7 +406,7 @@ #log_temp_files = -1 # log temporary files equal or larger # than the specified size in kilobytes; # -1 disables, 0 logs all temp files -#log_timezone = '(defaults to server environment setting)' +#log_timezone = 'GMT' #------------------------------------------------------------------------------ @@ -486,7 +486,7 @@ #datestyle = 'iso, mdy' #intervalstyle = 'postgres' -#timezone = '(defaults to server environment setting)' +#timezone = 'GMT' #timezone_abbreviations = 'Default' # Select the set of available time zone # abbreviations. Currently, there are # Default diff --git a/src/bin/initdb/.gitignore b/src/bin/initdb/.gitignore index 843eaf0c1e..c0ed2e8d16 100644 --- a/src/bin/initdb/.gitignore +++ b/src/bin/initdb/.gitignore @@ -1,4 +1,5 @@ /encnames.c /pqsignal.c +/localtime.c /initdb diff --git a/src/bin/initdb/Makefile b/src/bin/initdb/Makefile index 7dd6683813..8bd1e6d3a9 100644 --- a/src/bin/initdb/Makefile +++ b/src/bin/initdb/Makefile @@ -16,9 +16,9 @@ subdir = src/bin/initdb top_builddir = ../../.. include $(top_builddir)/src/Makefile.global -override CPPFLAGS := -DFRONTEND -I$(libpq_srcdir) $(CPPFLAGS) +override CPPFLAGS := -DFRONTEND -I$(libpq_srcdir) -I$(top_srcdir)/src/timezone $(CPPFLAGS) -OBJS= initdb.o encnames.o pqsignal.o $(WIN32RES) +OBJS= initdb.o findtimezone.o localtime.o encnames.o pqsignal.o $(WIN32RES) all: initdb @@ -35,6 +35,11 @@ encnames.c: % : $(top_srcdir)/src/backend/utils/mb/% pqsignal.c: % : $(top_srcdir)/src/interfaces/libpq/% rm -f $@ && $(LN_S) $< . +# Likewise, pull in localtime.c from src/timezones + +localtime.c: % : $(top_srcdir)/src/timezone/% + rm -f $@ && $(LN_S) $< . + install: all installdirs $(INSTALL_PROGRAM) initdb$(X) '$(DESTDIR)$(bindir)/initdb$(X)' @@ -45,7 +50,7 @@ uninstall: rm -f '$(DESTDIR)$(bindir)/initdb$(X)' clean distclean maintainer-clean: - rm -f initdb$(X) $(OBJS) encnames.c pqsignal.c + rm -f initdb$(X) $(OBJS) encnames.c pqsignal.c localtime.c # ensure that changes in datadir propagate into object file diff --git a/src/bin/initdb/findtimezone.c b/src/bin/initdb/findtimezone.c new file mode 100644 index 0000000000..6d45674b5b --- /dev/null +++ b/src/bin/initdb/findtimezone.c @@ -0,0 +1,1224 @@ +/*------------------------------------------------------------------------- + * + * findtimezone.c + * Functions for determining the default timezone to use. + * + * Portions Copyright (c) 1996-2011, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/bin/initdb/findtimezone.c + * + *------------------------------------------------------------------------- + */ +#include "postgres_fe.h" + +#include +#include +#include + +#include "pgtz.h" + +/* Ideally this would be in a .h file, but it hardly seems worth the trouble */ +extern const char *select_default_timezone(const char *share_path); + + +#ifndef SYSTEMTZDIR +static char tzdirpath[MAXPGPATH]; +#endif + + +/* + * Return full pathname of timezone data directory + * + * In this file, tzdirpath is assumed to be set up by select_default_timezone. + */ +static const char * +pg_TZDIR(void) +{ +#ifndef SYSTEMTZDIR + /* normal case: timezone stuff is under our share dir */ + return tzdirpath; +#else + /* we're configured to use system's timezone database */ + return SYSTEMTZDIR; +#endif +} + + +/* + * Given a timezone name, open() the timezone data file. Return the + * file descriptor if successful, -1 if not. + * + * This is simpler than the backend function of the same name because + * we assume that the input string has the correct case already, so there + * is no need for case-folding. (This is obviously true if we got the file + * name from the filesystem to start with. The only other place it can come + * from is the environment variable TZ, and there seems no need to allow + * case variation in that; other programs aren't likely to.) + * + * If "canonname" is not NULL, then on success the canonical spelling of the + * given name is stored there (the buffer must be > TZ_STRLEN_MAX bytes!). + * This is redundant but kept for compatibility with the backend code. + */ +int +pg_open_tzfile(const char *name, char *canonname) +{ + char fullname[MAXPGPATH]; + + if (canonname) + strlcpy(canonname, name, TZ_STRLEN_MAX + 1); + + strcpy(fullname, pg_TZDIR()); + if (strlen(fullname) + 1 + strlen(name) >= MAXPGPATH) + return -1; /* not gonna fit */ + strcat(fullname, "/"); + strcat(fullname, name); + + return open(fullname, O_RDONLY | PG_BINARY, 0); +} + + + +/* + * Load a timezone definition. + * Does not verify that the timezone is acceptable! + * + * This corresponds to the backend's pg_tzset(), except that we only support + * one loaded timezone at a time. + */ +static pg_tz * +pg_load_tz(const char *name) +{ + static pg_tz tz; + + if (strlen(name) > TZ_STRLEN_MAX) + return NULL; /* not going to fit */ + + /* + * "GMT" is always sent to tzparse(); see comments for pg_tzset(). + */ + if (strcmp(name, "GMT") == 0) + { + if (tzparse(name, &tz.state, TRUE) != 0) + { + /* This really, really should not happen ... */ + return NULL; + } + } + else if (tzload(name, NULL, &tz.state, TRUE) != 0) + { + if (name[0] == ':' || tzparse(name, &tz.state, FALSE) != 0) + { + return NULL; /* unknown timezone */ + } + } + + strcpy(tz.TZname, name); + + return &tz; +} + + +/* + * The following block of code attempts to determine which timezone in our + * timezone database is the best match for the active system timezone. + * + * On most systems, we rely on trying to match the observable behavior of + * the C library's localtime() function. The database zone that matches + * furthest into the past is the one to use. Often there will be several + * zones with identical rankings (since the Olson database assigns multiple + * names to many zones). We break ties arbitrarily by preferring shorter, + * then alphabetically earlier zone names. + * + * Win32's native knowledge about timezones appears to be too incomplete + * and too different from the Olson database for the above matching strategy + * to be of any use. But there is just a limited number of timezones + * available, so we can rely on a handmade mapping table instead. + */ + +#ifndef WIN32 + +#define T_DAY ((time_t) (60*60*24)) +#define T_WEEK ((time_t) (60*60*24*7)) +#define T_MONTH ((time_t) (60*60*24*31)) + +#define MAX_TEST_TIMES (52*100) /* 100 years */ + +struct tztry +{ + int n_test_times; + time_t test_times[MAX_TEST_TIMES]; +}; + +static void scan_available_timezones(char *tzdir, char *tzdirsub, + struct tztry * tt, + int *bestscore, char *bestzonename); + + +/* + * Get GMT offset from a system struct tm + */ +static int +get_timezone_offset(struct tm * tm) +{ +#if defined(HAVE_STRUCT_TM_TM_ZONE) + return tm->tm_gmtoff; +#elif defined(HAVE_INT_TIMEZONE) + return -TIMEZONE_GLOBAL; +#else +#error No way to determine TZ? Can this happen? +#endif +} + +/* + * Convenience subroutine to convert y/m/d to time_t (NOT pg_time_t) + */ +static time_t +build_time_t(int year, int month, int day) +{ + struct tm tm; + + memset(&tm, 0, sizeof(tm)); + tm.tm_mday = day; + tm.tm_mon = month - 1; + tm.tm_year = year - 1900; + + return mktime(&tm); +} + +/* + * Does a system tm value match one we computed ourselves? + */ +static bool +compare_tm(struct tm * s, struct pg_tm * p) +{ + if (s->tm_sec != p->tm_sec || + s->tm_min != p->tm_min || + s->tm_hour != p->tm_hour || + s->tm_mday != p->tm_mday || + s->tm_mon != p->tm_mon || + s->tm_year != p->tm_year || + s->tm_wday != p->tm_wday || + s->tm_yday != p->tm_yday || + s->tm_isdst != p->tm_isdst) + return false; + return true; +} + +/* + * See how well a specific timezone setting matches the system behavior + * + * We score a timezone setting according to the number of test times it + * matches. (The test times are ordered later-to-earlier, but this routine + * doesn't actually know that; it just scans until the first non-match.) + * + * We return -1 for a completely unusable setting; this is worse than the + * score of zero for a setting that works but matches not even the first + * test time. + */ +static int +score_timezone(const char *tzname, struct tztry * tt) +{ + int i; + pg_time_t pgtt; + struct tm *systm; + struct pg_tm *pgtm; + char cbuf[TZ_STRLEN_MAX + 1]; + pg_tz *tz; + + /* Load timezone definition */ + tz = pg_load_tz(tzname); + if (!tz) + return -1; /* unrecognized zone name */ + + /* Reject if leap seconds involved */ + if (!pg_tz_acceptable(tz)) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "Reject TZ \"%s\": uses leap seconds\n", tzname); +#endif + return -1; + } + + /* Check for match at all the test times */ + for (i = 0; i < tt->n_test_times; i++) + { + pgtt = (pg_time_t) (tt->test_times[i]); + pgtm = pg_localtime(&pgtt, tz); + if (!pgtm) + return -1; /* probably shouldn't happen */ + systm = localtime(&(tt->test_times[i])); + if (!systm) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "TZ \"%s\" scores %d: at %ld %04d-%02d-%02d %02d:%02d:%02d %s, system had no data\n", + tzname, i, (long) pgtt, + pgtm->tm_year + 1900, pgtm->tm_mon + 1, pgtm->tm_mday, + pgtm->tm_hour, pgtm->tm_min, pgtm->tm_sec, + pgtm->tm_isdst ? "dst" : "std"); +#endif + return i; + } + if (!compare_tm(systm, pgtm)) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "TZ \"%s\" scores %d: at %ld %04d-%02d-%02d %02d:%02d:%02d %s versus %04d-%02d-%02d %02d:%02d:%02d %s\n", + tzname, i, (long) pgtt, + pgtm->tm_year + 1900, pgtm->tm_mon + 1, pgtm->tm_mday, + pgtm->tm_hour, pgtm->tm_min, pgtm->tm_sec, + pgtm->tm_isdst ? "dst" : "std", + systm->tm_year + 1900, systm->tm_mon + 1, systm->tm_mday, + systm->tm_hour, systm->tm_min, systm->tm_sec, + systm->tm_isdst ? "dst" : "std"); +#endif + return i; + } + if (systm->tm_isdst >= 0) + { + /* Check match of zone names, too */ + if (pgtm->tm_zone == NULL) + return -1; /* probably shouldn't happen */ + memset(cbuf, 0, sizeof(cbuf)); + strftime(cbuf, sizeof(cbuf) - 1, "%Z", systm); /* zone abbr */ + if (strcmp(cbuf, pgtm->tm_zone) != 0) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "TZ \"%s\" scores %d: at %ld \"%s\" versus \"%s\"\n", + tzname, i, (long) pgtt, + pgtm->tm_zone, cbuf); +#endif + return i; + } + } + } + +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "TZ \"%s\" gets max score %d\n", tzname, i); +#endif + + return i; +} + + +/* + * Try to identify a timezone name (in our terminology) that best matches the + * observed behavior of the system timezone library. We cannot assume that + * the system TZ environment setting (if indeed there is one) matches our + * terminology, so we ignore it and just look at what localtime() returns. + */ +static const char * +identify_system_timezone(void) +{ + static char resultbuf[TZ_STRLEN_MAX + 1]; + time_t tnow; + time_t t; + struct tztry tt; + struct tm *tm; + int thisyear; + int bestscore; + char tmptzdir[MAXPGPATH]; + int std_ofs; + char std_zone_name[TZ_STRLEN_MAX + 1], + dst_zone_name[TZ_STRLEN_MAX + 1]; + char cbuf[TZ_STRLEN_MAX + 1]; + + /* Initialize OS timezone library */ + tzset(); + + /* + * Set up the list of dates to be probed to see how well our timezone + * matches the system zone. We first probe January and July of the + * current year; this serves to quickly eliminate the vast majority of the + * TZ database entries. If those dates match, we probe every week for 100 + * years backwards from the current July. (Weekly resolution is good + * enough to identify DST transition rules, since everybody switches on + * Sundays.) This is sufficient to cover most of the Unix time_t range, + * and we don't want to look further than that since many systems won't + * have sane TZ behavior further back anyway. The further back the zone + * matches, the better we score it. This may seem like a rather random + * way of doing things, but experience has shown that system-supplied + * timezone definitions are likely to have DST behavior that is right for + * the recent past and not so accurate further back. Scoring in this way + * allows us to recognize zones that have some commonality with the Olson + * database, without insisting on exact match. (Note: we probe Thursdays, + * not Sundays, to avoid triggering DST-transition bugs in localtime + * itself.) + */ + tnow = time(NULL); + tm = localtime(&tnow); + if (!tm) + return NULL; /* give up if localtime is broken... */ + thisyear = tm->tm_year + 1900; + + t = build_time_t(thisyear, 1, 15); + + /* + * Round back to GMT midnight Thursday. This depends on the knowledge + * that the time_t origin is Thu Jan 01 1970. (With a different origin + * we'd be probing some other day of the week, but it wouldn't matter + * anyway unless localtime() had DST-transition bugs.) + */ + t -= (t % T_WEEK); + + tt.n_test_times = 0; + tt.test_times[tt.n_test_times++] = t; + + t = build_time_t(thisyear, 7, 15); + t -= (t % T_WEEK); + + tt.test_times[tt.n_test_times++] = t; + + while (tt.n_test_times < MAX_TEST_TIMES) + { + t -= T_WEEK; + tt.test_times[tt.n_test_times++] = t; + } + + /* Search for the best-matching timezone file */ + strcpy(tmptzdir, pg_TZDIR()); + bestscore = -1; + resultbuf[0] = '\0'; + scan_available_timezones(tmptzdir, tmptzdir + strlen(tmptzdir) + 1, + &tt, + &bestscore, resultbuf); + if (bestscore > 0) + { + /* Ignore Olson's rather silly "Factory" zone; use GMT instead */ + if (strcmp(resultbuf, "Factory") == 0) + return NULL; + return resultbuf; + } + + /* + * Couldn't find a match in the database, so next we try constructed zone + * names (like "PST8PDT"). + * + * First we need to determine the names of the local standard and daylight + * zones. The idea here is to scan forward from today until we have seen + * both zones, if both are in use. + */ + memset(std_zone_name, 0, sizeof(std_zone_name)); + memset(dst_zone_name, 0, sizeof(dst_zone_name)); + std_ofs = 0; + + tnow = time(NULL); + + /* + * Round back to a GMT midnight so results don't depend on local time of + * day + */ + tnow -= (tnow % T_DAY); + + /* + * We have to look a little further ahead than one year, in case today is + * just past a DST boundary that falls earlier in the year than the next + * similar boundary. Arbitrarily scan up to 14 months. + */ + for (t = tnow; t <= tnow + T_MONTH * 14; t += T_MONTH) + { + tm = localtime(&t); + if (!tm) + continue; + if (tm->tm_isdst < 0) + continue; + if (tm->tm_isdst == 0 && std_zone_name[0] == '\0') + { + /* found STD zone */ + memset(cbuf, 0, sizeof(cbuf)); + strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */ + strcpy(std_zone_name, cbuf); + std_ofs = get_timezone_offset(tm); + } + if (tm->tm_isdst > 0 && dst_zone_name[0] == '\0') + { + /* found DST zone */ + memset(cbuf, 0, sizeof(cbuf)); + strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */ + strcpy(dst_zone_name, cbuf); + } + /* Done if found both */ + if (std_zone_name[0] && dst_zone_name[0]) + break; + } + + /* We should have found a STD zone name by now... */ + if (std_zone_name[0] == '\0') + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not determine system time zone\n"); +#endif + return NULL; /* go to GMT */ + } + + /* If we found DST then try STDDST */ + if (dst_zone_name[0] != '\0') + { + snprintf(resultbuf, sizeof(resultbuf), "%s%d%s", + std_zone_name, -std_ofs / 3600, dst_zone_name); + if (score_timezone(resultbuf, &tt) > 0) + return resultbuf; + } + + /* Try just the STD timezone (works for GMT at least) */ + strcpy(resultbuf, std_zone_name); + if (score_timezone(resultbuf, &tt) > 0) + return resultbuf; + + /* Try STD */ + snprintf(resultbuf, sizeof(resultbuf), "%s%d", + std_zone_name, -std_ofs / 3600); + if (score_timezone(resultbuf, &tt) > 0) + return resultbuf; + + /* + * Did not find the timezone. Fallback to use a GMT zone. Note that the + * Olson timezone database names the GMT-offset zones in POSIX style: plus + * is west of Greenwich. It's unfortunate that this is opposite of SQL + * conventions. Should we therefore change the names? Probably not... + */ + snprintf(resultbuf, sizeof(resultbuf), "Etc/GMT%s%d", + (-std_ofs > 0) ? "+" : "", -std_ofs / 3600); + +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not recognize system time zone, using \"%s\"\n", + resultbuf); +#endif + return resultbuf; +} + +/* + * Recursively scan the timezone database looking for the best match to + * the system timezone behavior. + * + * tzdir points to a buffer of size MAXPGPATH. On entry, it holds the + * pathname of a directory containing TZ files. We internally modify it + * to hold pathnames of sub-directories and files, but must restore it + * to its original contents before exit. + * + * tzdirsub points to the part of tzdir that represents the subfile name + * (ie, tzdir + the original directory name length, plus one for the + * first added '/'). + * + * tt tells about the system timezone behavior we need to match. + * + * *bestscore and *bestzonename on entry hold the best score found so far + * and the name of the best zone. We overwrite them if we find a better + * score. bestzonename must be a buffer of length TZ_STRLEN_MAX + 1. + */ +static void +scan_available_timezones(char *tzdir, char *tzdirsub, struct tztry * tt, + int *bestscore, char *bestzonename) +{ + int tzdir_orig_len = strlen(tzdir); + char **names; + char **namep; + + names = pgfnames(tzdir); + if (!names) + return; + + for (namep = names; *namep; namep++) + { + char *name = *namep; + struct stat statbuf; + + /* Ignore . and .., plus any other "hidden" files */ + if (name[0] == '.') + continue; + + snprintf(tzdir + tzdir_orig_len, MAXPGPATH - tzdir_orig_len, + "/%s", name); + + if (stat(tzdir, &statbuf) != 0) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not stat \"%s\": %s\n", + tzdir, strerror(errno)); +#endif + tzdir[tzdir_orig_len] = '\0'; + continue; + } + + if (S_ISDIR(statbuf.st_mode)) + { + /* Recurse into subdirectory */ + scan_available_timezones(tzdir, tzdirsub, tt, + bestscore, bestzonename); + } + else + { + /* Load and test this file */ + int score = score_timezone(tzdirsub, tt); + + if (score > *bestscore) + { + *bestscore = score; + strlcpy(bestzonename, tzdirsub, TZ_STRLEN_MAX + 1); + } + else if (score == *bestscore) + { + /* Consider how to break a tie */ + if (strlen(tzdirsub) < strlen(bestzonename) || + (strlen(tzdirsub) == strlen(bestzonename) && + strcmp(tzdirsub, bestzonename) < 0)) + strlcpy(bestzonename, tzdirsub, TZ_STRLEN_MAX + 1); + } + } + + /* Restore tzdir */ + tzdir[tzdir_orig_len] = '\0'; + } + + pgfnames_cleanup(names); +} + +#else /* WIN32 */ + +static const struct +{ + const char *stdname; /* Windows name of standard timezone */ + const char *dstname; /* Windows name of daylight timezone */ + const char *pgtzname; /* Name of pgsql timezone to map to */ +} win32_tzmap[] = + +{ + /* + * This list was built from the contents of the registry at + * HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time + * Zones on Windows 2003 R2. + * + * The zones have been matched to Olson timezones by looking at the cities + * listed in the win32 display name (in the comment here) in most cases. + */ + { + "Afghanistan Standard Time", "Afghanistan Daylight Time", + "Asia/Kabul" + }, /* (GMT+04:30) Kabul */ + { + "Alaskan Standard Time", "Alaskan Daylight Time", + "US/Alaska" + }, /* (GMT-09:00) Alaska */ + { + "Arab Standard Time", "Arab Daylight Time", + "Asia/Kuwait" + }, /* (GMT+03:00) Kuwait, Riyadh */ + { + "Arabian Standard Time", "Arabian Daylight Time", + "Asia/Muscat" + }, /* (GMT+04:00) Abu Dhabi, Muscat */ + { + "Arabic Standard Time", "Arabic Daylight Time", + "Asia/Baghdad" + }, /* (GMT+03:00) Baghdad */ + { + "Argentina Standard Time", "Argentina Daylight Time", + "America/Buenos_Aires" + }, /* (GMT-03:00) Buenos Aires */ + { + "Armenian Standard Time", "Armenian Daylight Time", + "Asia/Yerevan" + }, /* (GMT+04:00) Yerevan */ + { + "Atlantic Standard Time", "Atlantic Daylight Time", + "Canada/Atlantic" + }, /* (GMT-04:00) Atlantic Time (Canada) */ + { + "AUS Central Standard Time", "AUS Central Daylight Time", + "Australia/Darwin" + }, /* (GMT+09:30) Darwin */ + { + "AUS Eastern Standard Time", "AUS Eastern Daylight Time", + "Australia/Canberra" + }, /* (GMT+10:00) Canberra, Melbourne, Sydney */ + { + "Azerbaijan Standard Time", "Azerbaijan Daylight Time", + "Asia/Baku" + }, /* (GMT+04:00) Baku */ + { + "Azores Standard Time", "Azores Daylight Time", + "Atlantic/Azores" + }, /* (GMT-01:00) Azores */ + { + "Bangladesh Standard Time", "Bangladesh Daylight Time", + "Asia/Dhaka" + }, /* (GMT+06:00) Dhaka */ + { + "Canada Central Standard Time", "Canada Central Daylight Time", + "Canada/Saskatchewan" + }, /* (GMT-06:00) Saskatchewan */ + { + "Cape Verde Standard Time", "Cape Verde Daylight Time", + "Atlantic/Cape_Verde" + }, /* (GMT-01:00) Cape Verde Is. */ + { + "Caucasus Standard Time", "Caucasus Daylight Time", + "Asia/Baku" + }, /* (GMT+04:00) Baku, Tbilisi, Yerevan */ + { + "Cen. Australia Standard Time", "Cen. Australia Daylight Time", + "Australia/Adelaide" + }, /* (GMT+09:30) Adelaide */ + { + "Central America Standard Time", "Central America Daylight Time", + "CST6CDT" + }, /* (GMT-06:00) Central America */ + { + "Central Asia Standard Time", "Central Asia Daylight Time", + "Asia/Dhaka" + }, /* (GMT+06:00) Astana, Dhaka */ + { + "Central Brazilian Standard Time", "Central Brazilian Daylight Time", + "America/Cuiaba" + }, /* (GMT-04:00) Cuiaba */ + { + "Central Europe Standard Time", "Central Europe Daylight Time", + "Europe/Belgrade" + }, /* (GMT+01:00) Belgrade, Bratislava, Budapest, + * Ljubljana, Prague */ + { + "Central European Standard Time", "Central European Daylight Time", + "Europe/Sarajevo" + }, /* (GMT+01:00) Sarajevo, Skopje, Warsaw, + * Zagreb */ + { + "Central Pacific Standard Time", "Central Pacific Daylight Time", + "Pacific/Noumea" + }, /* (GMT+11:00) Magadan, Solomon Is., New + * Caledonia */ + { + "Central Standard Time", "Central Daylight Time", + "US/Central" + }, /* (GMT-06:00) Central Time (US & Canada) */ + { + "Central Standard Time (Mexico)", "Central Daylight Time (Mexico)", + "America/Mexico_City" + }, /* (GMT-06:00) Guadalajara, Mexico City, + * Monterrey - New */ + { + "China Standard Time", "China Daylight Time", + "Asia/Hong_Kong" + }, /* (GMT+08:00) Beijing, Chongqing, Hong Kong, + * Urumqi */ + { + "Dateline Standard Time", "Dateline Daylight Time", + "Etc/GMT+12" + }, /* (GMT-12:00) International Date Line West */ + { + "E. Africa Standard Time", "E. Africa Daylight Time", + "Africa/Nairobi" + }, /* (GMT+03:00) Nairobi */ + { + "E. Australia Standard Time", "E. Australia Daylight Time", + "Australia/Brisbane" + }, /* (GMT+10:00) Brisbane */ + { + "E. Europe Standard Time", "E. Europe Daylight Time", + "Europe/Bucharest" + }, /* (GMT+02:00) Bucharest */ + { + "E. South America Standard Time", "E. South America Daylight Time", + "America/Araguaina" + }, /* (GMT-03:00) Brasilia */ + { + "Eastern Standard Time", "Eastern Daylight Time", + "US/Eastern" + }, /* (GMT-05:00) Eastern Time (US & Canada) */ + { + "Egypt Standard Time", "Egypt Daylight Time", + "Africa/Cairo" + }, /* (GMT+02:00) Cairo */ + { + "Ekaterinburg Standard Time", "Ekaterinburg Daylight Time", + "Asia/Yekaterinburg" + }, /* (GMT+05:00) Ekaterinburg */ + { + "Fiji Standard Time", "Fiji Daylight Time", + "Pacific/Fiji" + }, /* (GMT+12:00) Fiji, Kamchatka, Marshall Is. */ + { + "FLE Standard Time", "FLE Daylight Time", + "Europe/Helsinki" + }, /* (GMT+02:00) Helsinki, Kyiv, Riga, Sofia, + * Tallinn, Vilnius */ + { + "Georgian Standard Time", "Georgian Daylight Time", + "Asia/Tbilisi" + }, /* (GMT+03:00) Tbilisi */ + { + "GMT Standard Time", "GMT Daylight Time", + "Europe/London" + }, /* (GMT) Greenwich Mean Time : Dublin, + * Edinburgh, Lisbon, London */ + { + "Greenland Standard Time", "Greenland Daylight Time", + "America/Godthab" + }, /* (GMT-03:00) Greenland */ + { + "Greenwich Standard Time", "Greenwich Daylight Time", + "Africa/Casablanca" + }, /* (GMT) Casablanca, Monrovia */ + { + "GTB Standard Time", "GTB Daylight Time", + "Europe/Athens" + }, /* (GMT+02:00) Athens, Istanbul, Minsk */ + { + "Hawaiian Standard Time", "Hawaiian Daylight Time", + "US/Hawaii" + }, /* (GMT-10:00) Hawaii */ + { + "India Standard Time", "India Daylight Time", + "Asia/Calcutta" + }, /* (GMT+05:30) Chennai, Kolkata, Mumbai, New + * Delhi */ + { + "Iran Standard Time", "Iran Daylight Time", + "Asia/Tehran" + }, /* (GMT+03:30) Tehran */ + { + "Jerusalem Standard Time", "Jerusalem Daylight Time", + "Asia/Jerusalem" + }, /* (GMT+02:00) Jerusalem */ + { + "Jordan Standard Time", "Jordan Daylight Time", + "Asia/Amman" + }, /* (GMT+02:00) Amman */ + { + "Kamchatka Standard Time", "Kamchatka Daylight Time", + "Asia/Kamchatka" + }, /* (GMT+12:00) Petropavlovsk-Kamchatsky */ + { + "Korea Standard Time", "Korea Daylight Time", + "Asia/Seoul" + }, /* (GMT+09:00) Seoul */ + { + "Mauritius Standard Time", "Mauritius Daylight Time", + "Indian/Mauritius" + }, /* (GMT+04:00) Port Louis */ + { + "Mexico Standard Time", "Mexico Daylight Time", + "America/Mexico_City" + }, /* (GMT-06:00) Guadalajara, Mexico City, + * Monterrey */ + { + "Mexico Standard Time 2", "Mexico Daylight Time 2", + "America/Chihuahua" + }, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan */ + { + "Mid-Atlantic Standard Time", "Mid-Atlantic Daylight Time", + "Atlantic/South_Georgia" + }, /* (GMT-02:00) Mid-Atlantic */ + { + "Middle East Standard Time", "Middle East Daylight Time", + "Asia/Beirut" + }, /* (GMT+02:00) Beirut */ + { + "Montevideo Standard Time", "Montevideo Daylight Time", + "America/Montevideo" + }, /* (GMT-03:00) Montevideo */ + { + "Morocco Standard Time", "Morocco Daylight Time", + "Africa/Casablanca" + }, /* (GMT) Casablanca */ + { + "Mountain Standard Time", "Mountain Daylight Time", + "US/Mountain" + }, /* (GMT-07:00) Mountain Time (US & Canada) */ + { + "Mountain Standard Time (Mexico)", "Mountain Daylight Time (Mexico)", + "America/Chihuahua" + }, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan - + * New */ + { + "Myanmar Standard Time", "Myanmar Daylight Time", + "Asia/Rangoon" + }, /* (GMT+06:30) Rangoon */ + { + "N. Central Asia Standard Time", "N. Central Asia Daylight Time", + "Asia/Novosibirsk" + }, /* (GMT+06:00) Novosibirsk */ + { + "Namibia Standard Time", "Namibia Daylight Time", + "Africa/Windhoek" + }, /* (GMT+02:00) Windhoek */ + { + "Nepal Standard Time", "Nepal Daylight Time", + "Asia/Katmandu" + }, /* (GMT+05:45) Kathmandu */ + { + "New Zealand Standard Time", "New Zealand Daylight Time", + "Pacific/Auckland" + }, /* (GMT+12:00) Auckland, Wellington */ + { + "Newfoundland Standard Time", "Newfoundland Daylight Time", + "Canada/Newfoundland" + }, /* (GMT-03:30) Newfoundland */ + { + "North Asia East Standard Time", "North Asia East Daylight Time", + "Asia/Irkutsk" + }, /* (GMT+08:00) Irkutsk, Ulaan Bataar */ + { + "North Asia Standard Time", "North Asia Daylight Time", + "Asia/Krasnoyarsk" + }, /* (GMT+07:00) Krasnoyarsk */ + { + "Pacific SA Standard Time", "Pacific SA Daylight Time", + "America/Santiago" + }, /* (GMT-04:00) Santiago */ + { + "Pacific Standard Time", "Pacific Daylight Time", + "US/Pacific" + }, /* (GMT-08:00) Pacific Time (US & Canada); + * Tijuana */ + { + "Pacific Standard Time (Mexico)", "Pacific Daylight Time (Mexico)", + "America/Tijuana" + }, /* (GMT-08:00) Tijuana, Baja California */ + { + "Pakistan Standard Time", "Pakistan Daylight Time", + "Asia/Karachi" + }, /* (GMT+05:00) Islamabad, Karachi */ + { + "Paraguay Standard Time", "Paraguay Daylight Time", + "America/Asuncion" + }, /* (GMT-04:00) Asuncion */ + { + "Romance Standard Time", "Romance Daylight Time", + "Europe/Brussels" + }, /* (GMT+01:00) Brussels, Copenhagen, Madrid, + * Paris */ + { + "Russian Standard Time", "Russian Daylight Time", + "Europe/Moscow" + }, /* (GMT+03:00) Moscow, St. Petersburg, + * Volgograd */ + { + "SA Eastern Standard Time", "SA Eastern Daylight Time", + "America/Buenos_Aires" + }, /* (GMT-03:00) Buenos Aires, Georgetown */ + { + "SA Pacific Standard Time", "SA Pacific Daylight Time", + "America/Bogota" + }, /* (GMT-05:00) Bogota, Lima, Quito */ + { + "SA Western Standard Time", "SA Western Daylight Time", + "America/Caracas" + }, /* (GMT-04:00) Caracas, La Paz */ + { + "Samoa Standard Time", "Samoa Daylight Time", + "Pacific/Midway" + }, /* (GMT-11:00) Midway Island, Samoa */ + { + "SE Asia Standard Time", "SE Asia Daylight Time", + "Asia/Bangkok" + }, /* (GMT+07:00) Bangkok, Hanoi, Jakarta */ + { + "Malay Peninsula Standard Time", "Malay Peninsula Daylight Time", + "Asia/Kuala_Lumpur" + }, /* (GMT+08:00) Kuala Lumpur, Singapore */ + { + "South Africa Standard Time", "South Africa Daylight Time", + "Africa/Harare" + }, /* (GMT+02:00) Harare, Pretoria */ + { + "Sri Lanka Standard Time", "Sri Lanka Daylight Time", + "Asia/Colombo" + }, /* (GMT+06:00) Sri Jayawardenepura */ + { + "Taipei Standard Time", "Taipei Daylight Time", + "Asia/Taipei" + }, /* (GMT+08:00) Taipei */ + { + "Tasmania Standard Time", "Tasmania Daylight Time", + "Australia/Hobart" + }, /* (GMT+10:00) Hobart */ + { + "Tokyo Standard Time", "Tokyo Daylight Time", + "Asia/Tokyo" + }, /* (GMT+09:00) Osaka, Sapporo, Tokyo */ + { + "Tonga Standard Time", "Tonga Daylight Time", + "Pacific/Tongatapu" + }, /* (GMT+13:00) Nuku'alofa */ + { + "Ulaanbaatar Standard Time", "Ulaanbaatar Daylight Time", + "Asia/Ulaanbaatar", + }, /* (GMT+08:00) Ulaanbaatar */ + { + "US Eastern Standard Time", "US Eastern Daylight Time", + "US/Eastern" + }, /* (GMT-05:00) Indiana (East) */ + { + "US Mountain Standard Time", "US Mountain Daylight Time", + "US/Arizona" + }, /* (GMT-07:00) Arizona */ + { + "Coordinated Universal Time", "Coordinated Universal Time", + "UTC" + }, /* (GMT) Coordinated Universal Time */ + { + "UTC+12", "UTC+12", + "Etc/GMT+12" + }, /* (GMT+12:00) Coordinated Universal Time+12 */ + { + "UTC-02", "UTC-02", + "Etc/GMT-02" + }, /* (GMT-02:00) Coordinated Universal Time-02 */ + { + "UTC-11", "UTC-11", + "Etc/GMT-11" + }, /* (GMT-11:00) Coordinated Universal Time-11 */ + { + "Venezuela Standard Time", "Venezuela Daylight Time", + "America/Caracas", + }, /* (GMT-04:30) Caracas */ + { + "Vladivostok Standard Time", "Vladivostok Daylight Time", + "Asia/Vladivostok" + }, /* (GMT+10:00) Vladivostok */ + { + "W. Australia Standard Time", "W. Australia Daylight Time", + "Australia/Perth" + }, /* (GMT+08:00) Perth */ +#ifdef NOT_USED + /* Could not find a match for this one (just a guess). Excluded for now. */ + { + "W. Central Africa Standard Time", "W. Central Africa Daylight Time", + "WAT" + }, /* (GMT+01:00) West Central Africa */ +#endif + { + "W. Europe Standard Time", "W. Europe Daylight Time", + "CET" + }, /* (GMT+01:00) Amsterdam, Berlin, Bern, Rome, + * Stockholm, Vienna */ + { + "West Asia Standard Time", "West Asia Daylight Time", + "Asia/Karachi" + }, /* (GMT+05:00) Islamabad, Karachi, Tashkent */ + { + "West Pacific Standard Time", "West Pacific Daylight Time", + "Pacific/Guam" + }, /* (GMT+10:00) Guam, Port Moresby */ + { + "Yakutsk Standard Time", "Yakutsk Daylight Time", + "Asia/Yakutsk" + }, /* (GMT+09:00) Yakutsk */ + { + NULL, NULL, NULL + } +}; + +static const char * +identify_system_timezone(void) +{ + int i; + char tzname[128]; + char localtzname[256]; + time_t t = time(NULL); + struct tm *tm = localtime(&t); + HKEY rootKey; + int idx; + + if (!tm) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not identify system time zone: localtime() failed\n"); +#endif + return NULL; /* go to GMT */ + } + + memset(tzname, 0, sizeof(tzname)); + strftime(tzname, sizeof(tzname) - 1, "%Z", tm); + + for (i = 0; win32_tzmap[i].stdname != NULL; i++) + { + if (strcmp(tzname, win32_tzmap[i].stdname) == 0 || + strcmp(tzname, win32_tzmap[i].dstname) == 0) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "TZ \"%s\" matches system time zone \"%s\"\n", + win32_tzmap[i].pgtzname, tzname); +#endif + return win32_tzmap[i].pgtzname; + } + } + + /* + * Localized Windows versions return localized names for the timezone. + * Scan the registry to find the English name, and then try matching + * against our table again. + */ + memset(localtzname, 0, sizeof(localtzname)); + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones", + 0, + KEY_READ, + &rootKey) != ERROR_SUCCESS) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not open registry key to identify system time zone: error code %lu\n", + GetLastError()); +#endif + return NULL; /* go to GMT */ + } + + for (idx = 0;; idx++) + { + char keyname[256]; + char zonename[256]; + DWORD namesize; + FILETIME lastwrite; + HKEY key; + LONG r; + + memset(keyname, 0, sizeof(keyname)); + namesize = sizeof(keyname); + if ((r = RegEnumKeyEx(rootKey, + idx, + keyname, + &namesize, + NULL, + NULL, + NULL, + &lastwrite)) != ERROR_SUCCESS) + { + if (r == ERROR_NO_MORE_ITEMS) + break; +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not enumerate registry subkeys to identify system time zone: %d\n", + (int) r); +#endif + break; + } + + if ((r = RegOpenKeyEx(rootKey, keyname, 0, KEY_READ, &key)) != ERROR_SUCCESS) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not open registry subkey to identify system time zone: %d\n", + (int) r); +#endif + break; + } + + memset(zonename, 0, sizeof(zonename)); + namesize = sizeof(zonename); + if ((r = RegQueryValueEx(key, "Std", NULL, NULL, (unsigned char *) zonename, &namesize)) != ERROR_SUCCESS) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not query value for key \"std\" to identify system time zone \"%s\": %d\n", + keyname, (int) r); +#endif + RegCloseKey(key); + continue; /* Proceed to look at the next timezone */ + } + if (strcmp(tzname, zonename) == 0) + { + /* Matched zone */ + strcpy(localtzname, keyname); + RegCloseKey(key); + break; + } + memset(zonename, 0, sizeof(zonename)); + namesize = sizeof(zonename); + if ((r = RegQueryValueEx(key, "Dlt", NULL, NULL, (unsigned char *) zonename, &namesize)) != ERROR_SUCCESS) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not query value for key \"dlt\" to identify system time zone \"%s\": %d\n", + keyname, (int) r); +#endif + RegCloseKey(key); + continue; /* Proceed to look at the next timezone */ + } + if (strcmp(tzname, zonename) == 0) + { + /* Matched DST zone */ + strcpy(localtzname, keyname); + RegCloseKey(key); + break; + } + + RegCloseKey(key); + } + + RegCloseKey(rootKey); + + if (localtzname[0]) + { + /* Found a localized name, so scan for that one too */ + for (i = 0; win32_tzmap[i].stdname != NULL; i++) + { + if (strcmp(localtzname, win32_tzmap[i].stdname) == 0 || + strcmp(localtzname, win32_tzmap[i].dstname) == 0) + { +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "TZ \"%s\" matches localized system time zone \"%s\" (\"%s\")\n", + win32_tzmap[i].pgtzname, tzname, localtzname); +#endif + return win32_tzmap[i].pgtzname; + } + } + } + +#ifdef DEBUG_IDENTIFY_TIMEZONE + fprintf(stderr, "could not find a match for system time zone \"%s\"\n", + tzname); +#endif + return NULL; /* go to GMT */ +} +#endif /* WIN32 */ + + +/* + * Return true if the given zone name is valid and is an "acceptable" zone. + */ +static bool +validate_zone(const char *tzname) +{ + pg_tz *tz; + + if (!tzname || !tzname[0]) + return false; + + tz = pg_load_tz(tzname); + if (!tz) + return false; + + if (!pg_tz_acceptable(tz)) + return false; + + return true; +} + +/* + * Identify a suitable default timezone setting based on the environment. + * + * The installation share_path must be passed in, as that is the default + * location for the timezone database directory. + * + * We first look to the TZ environment variable. If not found or not + * recognized by our own code, we see if we can identify the timezone + * from the behavior of the system timezone library. When all else fails, + * return NULL, indicating that we should default to GMT. + */ +const char * +select_default_timezone(const char *share_path) +{ + const char *tzname; + + /* Initialize timezone directory path, if needed */ +#ifndef SYSTEMTZDIR + snprintf(tzdirpath, sizeof(tzdirpath), "%s/timezone", share_path); +#endif + + /* Check TZ environment variable */ + tzname = getenv("TZ"); + if (validate_zone(tzname)) + return tzname; + + /* Nope, so try to identify the system timezone */ + tzname = identify_system_timezone(); + if (validate_zone(tzname)) + return tzname; + + return NULL; +} diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 1dbd6f603a..e535fdad1e 100644 --- a/src/bin/initdb/initdb.c +++ b/src/bin/initdb/initdb.c @@ -61,6 +61,9 @@ #include "getopt_long.h" #include "miscadmin.h" +/* Ideally this would be in a .h file, but it hardly seems worth the trouble */ +extern const char *select_default_timezone(const char *share_path); + /* * these values are passed in by makefile defines @@ -947,8 +950,9 @@ static void setup_config(void) { char **conflines; - char repltok[100]; + char repltok[TZ_STRLEN_MAX + 100]; char path[MAXPGPATH]; + const char *default_timezone; fputs(_("creating configuration files ... "), stdout); fflush(stdout); @@ -1011,6 +1015,17 @@ setup_config(void) "#default_text_search_config = 'pg_catalog.simple'", repltok); + default_timezone = select_default_timezone(share_path); + if (default_timezone) + { + snprintf(repltok, sizeof(repltok), "timezone = '%s'", + escape_quotes(default_timezone)); + conflines = replace_token(conflines, "#timezone = 'GMT'", repltok); + snprintf(repltok, sizeof(repltok), "log_timezone = '%s'", + escape_quotes(default_timezone)); + conflines = replace_token(conflines, "#log_timezone = 'GMT'", repltok); + } + snprintf(path, sizeof(path), "%s/postgresql.conf", pg_data); writefile(path, conflines); @@ -2796,14 +2811,6 @@ main(int argc, char *argv[]) sprintf(pgdenv, "PGDATA=%s", pg_data); putenv(pgdenv); - /* - * Also ensure that TZ is set, so that we don't waste time identifying the - * system timezone each of the many times we start a standalone backend. - * It's okay to use a hard-wired value here because nothing done during - * initdb cares about the timezone setting. - */ - putenv("TZ=GMT"); - if ((ret = find_other_exec(argv[0], "postgres", PG_BACKEND_VERSIONSTR, backend_exec)) < 0) { diff --git a/src/include/pgtime.h b/src/include/pgtime.h index e73cfa51a4..9dc794026c 100644 --- a/src/include/pgtime.h +++ b/src/include/pgtime.h @@ -40,6 +40,11 @@ struct pg_tm typedef struct pg_tz pg_tz; typedef struct pg_tzenum pg_tzenum; +/* Maximum length of a timezone name (not including trailing null) */ +#define TZ_STRLEN_MAX 255 + +/* these functions are in localtime.c */ + extern struct pg_tm *pg_localtime(const pg_time_t *timep, const pg_tz *tz); extern struct pg_tm *pg_gmtime(const pg_time_t *timep); extern int pg_next_dst_boundary(const pg_time_t *timep, @@ -52,22 +57,20 @@ extern int pg_next_dst_boundary(const pg_time_t *timep, extern size_t pg_strftime(char *s, size_t max, const char *format, const struct pg_tm * tm); -extern void pg_timezone_pre_initialize(void); -extern void pg_timezone_initialize(void); -extern pg_tz *pg_tzset(const char *tzname); -extern bool tz_acceptable(pg_tz *tz); extern bool pg_get_timezone_offset(const pg_tz *tz, long int *gmtoff); extern const char *pg_get_timezone_name(pg_tz *tz); +extern bool pg_tz_acceptable(pg_tz *tz); + +/* these functions and variables are in pgtz.c */ + +extern pg_tz *session_timezone; +extern pg_tz *log_timezone; + +extern void pg_timezone_initialize(void); +extern pg_tz *pg_tzset(const char *tzname); extern pg_tzenum *pg_tzenumerate_start(void); extern pg_tz *pg_tzenumerate_next(pg_tzenum *dir); extern void pg_tzenumerate_end(pg_tzenum *dir); -extern pg_tz *session_timezone; -extern pg_tz *log_timezone; -extern pg_tz *gmt_timezone; - -/* Maximum length of a timezone name (not including trailing null) */ -#define TZ_STRLEN_MAX 255 - #endif /* _PGTIME_H */ diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index 6b3b5c6694..d99d624fae 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -333,8 +333,6 @@ extern ArrayType *GUCArrayAdd(ArrayType *array, const char *name, const char *va extern ArrayType *GUCArrayDelete(ArrayType *array, const char *name); extern ArrayType *GUCArrayReset(ArrayType *array); -extern void pg_timezone_abbrev_initialize(void); - #ifdef EXEC_BACKEND extern void write_nondefault_variables(GucContext context); extern void read_nondefault_variables(void); diff --git a/src/timezone/localtime.c b/src/timezone/localtime.c index 782b99adfb..037cdbbdcd 100644 --- a/src/timezone/localtime.c +++ b/src/timezone/localtime.c @@ -16,6 +16,7 @@ #include +#include "datatype/timestamp.h" #include "private.h" #include "pgtz.h" #include "tzfile.h" @@ -734,6 +735,7 @@ tzparse(const char *name, struct state * sp, int lastditch) * can't assume pg_open_tzfile() is sane yet, and we don't care about * leap seconds anyway. */ + sp->goback = sp->goahead = FALSE; load_result = -1; } else @@ -1476,3 +1478,29 @@ pg_get_timezone_name(pg_tz *tz) return tz->TZname; return NULL; } + +/* + * Check whether timezone is acceptable. + * + * What we are doing here is checking for leap-second-aware timekeeping. + * We need to reject such TZ settings because they'll wreak havoc with our + * date/time arithmetic. + */ +bool +pg_tz_acceptable(pg_tz *tz) +{ + struct pg_tm *tt; + pg_time_t time2000; + + /* + * To detect leap-second timekeeping, run pg_localtime for what should be + * GMT midnight, 2000-01-01. Insist that the tm_sec value be zero; any + * other result has to be due to leap seconds. + */ + time2000 = (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY; + tt = pg_localtime(&time2000, tz); + if (!tt || tt->tm_sec != 0) + return false; + + return true; +} diff --git a/src/timezone/pgtz.c b/src/timezone/pgtz.c index 254ddb8ad6..7063bf458e 100644 --- a/src/timezone/pgtz.c +++ b/src/timezone/pgtz.c @@ -10,8 +10,6 @@ * *------------------------------------------------------------------------- */ -#define NO_REDEFINE_TIMEFUNCS - #include "postgres.h" #include @@ -22,28 +20,19 @@ #include "miscadmin.h" #include "pgtz.h" #include "storage/fd.h" -#include "tzfile.h" -#include "utils/datetime.h" -#include "utils/guc.h" #include "utils/hsearch.h" + /* Current session timezone (controlled by TimeZone GUC) */ pg_tz *session_timezone = NULL; /* Current log timezone (controlled by log_timezone GUC) */ pg_tz *log_timezone = NULL; -/* Fallback GMT timezone for last-ditch error message formatting */ -pg_tz *gmt_timezone = NULL; -static pg_tz gmt_timezone_data; - static bool scan_directory_ci(const char *dirname, const char *fname, int fnamelen, char *canonname, int canonnamelen); -static const char *identify_system_timezone(void); -static pg_tz *get_pg_tz_for_zone(const char *tzname); -static pg_tz *select_default_timezone(void); /* @@ -80,7 +69,7 @@ pg_TZDIR(void) * timezone database does not contain case-equivalent names). * * If "canonname" is not NULL, then on success the canonical spelling of the - * given name is stored there (it is assumed to be > TZ_STRLEN_MAX bytes!). + * given name is stored there (the buffer must be > TZ_STRLEN_MAX bytes!). */ int pg_open_tzfile(const char *name, char *canonname) @@ -175,1054 +164,6 @@ scan_directory_ci(const char *dirname, const char *fname, int fnamelen, } -/* - * The following block of code attempts to determine which timezone in our - * timezone database is the best match for the active system timezone. - * - * On most systems, we rely on trying to match the observable behavior of - * the C library's localtime() function. The database zone that matches - * furthest into the past is the one to use. Often there will be several - * zones with identical rankings (since the zic database assigns multiple - * names to many zones). We break ties arbitrarily by preferring shorter, - * then alphabetically earlier zone names. - * - * Win32's native knowledge about timezones appears to be too incomplete - * and too different from the zic database for the above matching strategy - * to be of any use. But there is just a limited number of timezones - * available, so we can rely on a handmade mapping table instead. - */ - -#ifndef WIN32 - -#define T_DAY ((time_t) (60*60*24)) -#define T_WEEK ((time_t) (60*60*24*7)) -#define T_MONTH ((time_t) (60*60*24*31)) - -#define MAX_TEST_TIMES (52*100) /* 100 years */ - -struct tztry -{ - int n_test_times; - time_t test_times[MAX_TEST_TIMES]; -}; - -static void scan_available_timezones(char *tzdir, char *tzdirsub, - struct tztry * tt, - int *bestscore, char *bestzonename); - - -/* - * Get GMT offset from a system struct tm - */ -static int -get_timezone_offset(struct tm * tm) -{ -#if defined(HAVE_STRUCT_TM_TM_ZONE) - return tm->tm_gmtoff; -#elif defined(HAVE_INT_TIMEZONE) - return -TIMEZONE_GLOBAL; -#else -#error No way to determine TZ? Can this happen? -#endif -} - -/* - * Convenience subroutine to convert y/m/d to time_t (NOT pg_time_t) - */ -static time_t -build_time_t(int year, int month, int day) -{ - struct tm tm; - - memset(&tm, 0, sizeof(tm)); - tm.tm_mday = day; - tm.tm_mon = month - 1; - tm.tm_year = year - 1900; - - return mktime(&tm); -} - -/* - * Does a system tm value match one we computed ourselves? - */ -static bool -compare_tm(struct tm * s, struct pg_tm * p) -{ - if (s->tm_sec != p->tm_sec || - s->tm_min != p->tm_min || - s->tm_hour != p->tm_hour || - s->tm_mday != p->tm_mday || - s->tm_mon != p->tm_mon || - s->tm_year != p->tm_year || - s->tm_wday != p->tm_wday || - s->tm_yday != p->tm_yday || - s->tm_isdst != p->tm_isdst) - return false; - return true; -} - -/* - * See how well a specific timezone setting matches the system behavior - * - * We score a timezone setting according to the number of test times it - * matches. (The test times are ordered later-to-earlier, but this routine - * doesn't actually know that; it just scans until the first non-match.) - * - * We return -1 for a completely unusable setting; this is worse than the - * score of zero for a setting that works but matches not even the first - * test time. - */ -static int -score_timezone(const char *tzname, struct tztry * tt) -{ - int i; - pg_time_t pgtt; - struct tm *systm; - struct pg_tm *pgtm; - char cbuf[TZ_STRLEN_MAX + 1]; - pg_tz tz; - - - /* - * Load timezone directly. Don't use pg_tzset, because we don't want all - * timezones loaded in the cache at startup. - */ - if (tzload(tzname, NULL, &tz.state, TRUE) != 0) - { - if (tzname[0] == ':' || tzparse(tzname, &tz.state, FALSE) != 0) - { - return -1; /* can't handle the TZ name at all */ - } - } - - /* Reject if leap seconds involved */ - if (!tz_acceptable(&tz)) - { - elog(DEBUG4, "Reject TZ \"%s\": uses leap seconds", tzname); - return -1; - } - - /* Check for match at all the test times */ - for (i = 0; i < tt->n_test_times; i++) - { - pgtt = (pg_time_t) (tt->test_times[i]); - pgtm = pg_localtime(&pgtt, &tz); - if (!pgtm) - return -1; /* probably shouldn't happen */ - systm = localtime(&(tt->test_times[i])); - if (!systm) - { - elog(DEBUG4, "TZ \"%s\" scores %d: at %ld %04d-%02d-%02d %02d:%02d:%02d %s, system had no data", - tzname, i, (long) pgtt, - pgtm->tm_year + 1900, pgtm->tm_mon + 1, pgtm->tm_mday, - pgtm->tm_hour, pgtm->tm_min, pgtm->tm_sec, - pgtm->tm_isdst ? "dst" : "std"); - return i; - } - if (!compare_tm(systm, pgtm)) - { - elog(DEBUG4, "TZ \"%s\" scores %d: at %ld %04d-%02d-%02d %02d:%02d:%02d %s versus %04d-%02d-%02d %02d:%02d:%02d %s", - tzname, i, (long) pgtt, - pgtm->tm_year + 1900, pgtm->tm_mon + 1, pgtm->tm_mday, - pgtm->tm_hour, pgtm->tm_min, pgtm->tm_sec, - pgtm->tm_isdst ? "dst" : "std", - systm->tm_year + 1900, systm->tm_mon + 1, systm->tm_mday, - systm->tm_hour, systm->tm_min, systm->tm_sec, - systm->tm_isdst ? "dst" : "std"); - return i; - } - if (systm->tm_isdst >= 0) - { - /* Check match of zone names, too */ - if (pgtm->tm_zone == NULL) - return -1; /* probably shouldn't happen */ - memset(cbuf, 0, sizeof(cbuf)); - strftime(cbuf, sizeof(cbuf) - 1, "%Z", systm); /* zone abbr */ - if (strcmp(cbuf, pgtm->tm_zone) != 0) - { - elog(DEBUG4, "TZ \"%s\" scores %d: at %ld \"%s\" versus \"%s\"", - tzname, i, (long) pgtt, - pgtm->tm_zone, cbuf); - return i; - } - } - } - - elog(DEBUG4, "TZ \"%s\" gets max score %d", tzname, i); - return i; -} - - -/* - * Try to identify a timezone name (in our terminology) that best matches the - * observed behavior of the system timezone library. We cannot assume that - * the system TZ environment setting (if indeed there is one) matches our - * terminology, so we ignore it and just look at what localtime() returns. - */ -static const char * -identify_system_timezone(void) -{ - static char resultbuf[TZ_STRLEN_MAX + 1]; - time_t tnow; - time_t t; - struct tztry tt; - struct tm *tm; - int thisyear; - int bestscore; - char tmptzdir[MAXPGPATH]; - int std_ofs; - char std_zone_name[TZ_STRLEN_MAX + 1], - dst_zone_name[TZ_STRLEN_MAX + 1]; - char cbuf[TZ_STRLEN_MAX + 1]; - - /* Initialize OS timezone library */ - tzset(); - - /* - * Set up the list of dates to be probed to see how well our timezone - * matches the system zone. We first probe January and July of the - * current year; this serves to quickly eliminate the vast majority of the - * TZ database entries. If those dates match, we probe every week for 100 - * years backwards from the current July. (Weekly resolution is good - * enough to identify DST transition rules, since everybody switches on - * Sundays.) This is sufficient to cover most of the Unix time_t range, - * and we don't want to look further than that since many systems won't - * have sane TZ behavior further back anyway. The further back the zone - * matches, the better we score it. This may seem like a rather random - * way of doing things, but experience has shown that system-supplied - * timezone definitions are likely to have DST behavior that is right for - * the recent past and not so accurate further back. Scoring in this way - * allows us to recognize zones that have some commonality with the zic - * database, without insisting on exact match. (Note: we probe Thursdays, - * not Sundays, to avoid triggering DST-transition bugs in localtime - * itself.) - */ - tnow = time(NULL); - tm = localtime(&tnow); - if (!tm) - return NULL; /* give up if localtime is broken... */ - thisyear = tm->tm_year + 1900; - - t = build_time_t(thisyear, 1, 15); - - /* - * Round back to GMT midnight Thursday. This depends on the knowledge - * that the time_t origin is Thu Jan 01 1970. (With a different origin - * we'd be probing some other day of the week, but it wouldn't matter - * anyway unless localtime() had DST-transition bugs.) - */ - t -= (t % T_WEEK); - - tt.n_test_times = 0; - tt.test_times[tt.n_test_times++] = t; - - t = build_time_t(thisyear, 7, 15); - t -= (t % T_WEEK); - - tt.test_times[tt.n_test_times++] = t; - - while (tt.n_test_times < MAX_TEST_TIMES) - { - t -= T_WEEK; - tt.test_times[tt.n_test_times++] = t; - } - - /* Search for the best-matching timezone file */ - strcpy(tmptzdir, pg_TZDIR()); - bestscore = -1; - resultbuf[0] = '\0'; - scan_available_timezones(tmptzdir, tmptzdir + strlen(tmptzdir) + 1, - &tt, - &bestscore, resultbuf); - if (bestscore > 0) - { - /* Ignore zic's rather silly "Factory" time zone; use GMT instead */ - if (strcmp(resultbuf, "Factory") == 0) - return NULL; - return resultbuf; - } - - /* - * Couldn't find a match in the database, so next we try constructed zone - * names (like "PST8PDT"). - * - * First we need to determine the names of the local standard and daylight - * zones. The idea here is to scan forward from today until we have seen - * both zones, if both are in use. - */ - memset(std_zone_name, 0, sizeof(std_zone_name)); - memset(dst_zone_name, 0, sizeof(dst_zone_name)); - std_ofs = 0; - - tnow = time(NULL); - - /* - * Round back to a GMT midnight so results don't depend on local time of - * day - */ - tnow -= (tnow % T_DAY); - - /* - * We have to look a little further ahead than one year, in case today is - * just past a DST boundary that falls earlier in the year than the next - * similar boundary. Arbitrarily scan up to 14 months. - */ - for (t = tnow; t <= tnow + T_MONTH * 14; t += T_MONTH) - { - tm = localtime(&t); - if (!tm) - continue; - if (tm->tm_isdst < 0) - continue; - if (tm->tm_isdst == 0 && std_zone_name[0] == '\0') - { - /* found STD zone */ - memset(cbuf, 0, sizeof(cbuf)); - strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */ - strcpy(std_zone_name, cbuf); - std_ofs = get_timezone_offset(tm); - } - if (tm->tm_isdst > 0 && dst_zone_name[0] == '\0') - { - /* found DST zone */ - memset(cbuf, 0, sizeof(cbuf)); - strftime(cbuf, sizeof(cbuf) - 1, "%Z", tm); /* zone abbr */ - strcpy(dst_zone_name, cbuf); - } - /* Done if found both */ - if (std_zone_name[0] && dst_zone_name[0]) - break; - } - - /* We should have found a STD zone name by now... */ - if (std_zone_name[0] == '\0') - { - ereport(LOG, - (errmsg("could not determine system time zone"), - errdetail("The PostgreSQL time zone will be set to \"%s\".", - "GMT"), - errhint("You can specify the correct timezone in postgresql.conf."))); - return NULL; /* go to GMT */ - } - - /* If we found DST then try STDDST */ - if (dst_zone_name[0] != '\0') - { - snprintf(resultbuf, sizeof(resultbuf), "%s%d%s", - std_zone_name, -std_ofs / 3600, dst_zone_name); - if (score_timezone(resultbuf, &tt) > 0) - return resultbuf; - } - - /* Try just the STD timezone (works for GMT at least) */ - strcpy(resultbuf, std_zone_name); - if (score_timezone(resultbuf, &tt) > 0) - return resultbuf; - - /* Try STD */ - snprintf(resultbuf, sizeof(resultbuf), "%s%d", - std_zone_name, -std_ofs / 3600); - if (score_timezone(resultbuf, &tt) > 0) - return resultbuf; - - /* - * Did not find the timezone. Fallback to use a GMT zone. Note that the - * zic timezone database names the GMT-offset zones in POSIX style: plus - * is west of Greenwich. It's unfortunate that this is opposite of SQL - * conventions. Should we therefore change the names? Probably not... - */ - snprintf(resultbuf, sizeof(resultbuf), "Etc/GMT%s%d", - (-std_ofs > 0) ? "+" : "", -std_ofs / 3600); - - ereport(LOG, - (errmsg("could not recognize system time zone"), - errdetail("The PostgreSQL time zone will be set to \"%s\".", - resultbuf), - errhint("You can specify the correct timezone in postgresql.conf."))); - return resultbuf; -} - -/* - * Recursively scan the timezone database looking for the best match to - * the system timezone behavior. - * - * tzdir points to a buffer of size MAXPGPATH. On entry, it holds the - * pathname of a directory containing TZ files. We internally modify it - * to hold pathnames of sub-directories and files, but must restore it - * to its original contents before exit. - * - * tzdirsub points to the part of tzdir that represents the subfile name - * (ie, tzdir + the original directory name length, plus one for the - * first added '/'). - * - * tt tells about the system timezone behavior we need to match. - * - * *bestscore and *bestzonename on entry hold the best score found so far - * and the name of the best zone. We overwrite them if we find a better - * score. bestzonename must be a buffer of length TZ_STRLEN_MAX + 1. - */ -static void -scan_available_timezones(char *tzdir, char *tzdirsub, struct tztry * tt, - int *bestscore, char *bestzonename) -{ - int tzdir_orig_len = strlen(tzdir); - DIR *dirdesc; - struct dirent *direntry; - - dirdesc = AllocateDir(tzdir); - if (!dirdesc) - { - ereport(LOG, - (errcode_for_file_access(), - errmsg("could not open directory \"%s\": %m", tzdir))); - return; - } - - while ((direntry = ReadDir(dirdesc, tzdir)) != NULL) - { - struct stat statbuf; - - /* Ignore . and .., plus any other "hidden" files */ - if (direntry->d_name[0] == '.') - continue; - - snprintf(tzdir + tzdir_orig_len, MAXPGPATH - tzdir_orig_len, - "/%s", direntry->d_name); - - if (stat(tzdir, &statbuf) != 0) - { - ereport(LOG, - (errcode_for_file_access(), - errmsg("could not stat \"%s\": %m", tzdir))); - tzdir[tzdir_orig_len] = '\0'; - continue; - } - - if (S_ISDIR(statbuf.st_mode)) - { - /* Recurse into subdirectory */ - scan_available_timezones(tzdir, tzdirsub, tt, - bestscore, bestzonename); - } - else - { - /* Load and test this file */ - int score = score_timezone(tzdirsub, tt); - - if (score > *bestscore) - { - *bestscore = score; - strlcpy(bestzonename, tzdirsub, TZ_STRLEN_MAX + 1); - } - else if (score == *bestscore) - { - /* Consider how to break a tie */ - if (strlen(tzdirsub) < strlen(bestzonename) || - (strlen(tzdirsub) == strlen(bestzonename) && - strcmp(tzdirsub, bestzonename) < 0)) - strlcpy(bestzonename, tzdirsub, TZ_STRLEN_MAX + 1); - } - } - - /* Restore tzdir */ - tzdir[tzdir_orig_len] = '\0'; - } - - FreeDir(dirdesc); -} -#else /* WIN32 */ - -static const struct -{ - const char *stdname; /* Windows name of standard timezone */ - const char *dstname; /* Windows name of daylight timezone */ - const char *pgtzname; /* Name of pgsql timezone to map to */ -} win32_tzmap[] = - -{ - /* - * This list was built from the contents of the registry at - * HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time - * Zones on Windows 2003 R2. - * - * The zones have been matched to zic timezones by looking at the cities - * listed in the win32 display name (in the comment here) in most cases. - */ - { - "Afghanistan Standard Time", "Afghanistan Daylight Time", - "Asia/Kabul" - }, /* (GMT+04:30) Kabul */ - { - "Alaskan Standard Time", "Alaskan Daylight Time", - "US/Alaska" - }, /* (GMT-09:00) Alaska */ - { - "Arab Standard Time", "Arab Daylight Time", - "Asia/Kuwait" - }, /* (GMT+03:00) Kuwait, Riyadh */ - { - "Arabian Standard Time", "Arabian Daylight Time", - "Asia/Muscat" - }, /* (GMT+04:00) Abu Dhabi, Muscat */ - { - "Arabic Standard Time", "Arabic Daylight Time", - "Asia/Baghdad" - }, /* (GMT+03:00) Baghdad */ - { - "Argentina Standard Time", "Argentina Daylight Time", - "America/Buenos_Aires" - }, /* (GMT-03:00) Buenos Aires */ - { - "Armenian Standard Time", "Armenian Daylight Time", - "Asia/Yerevan" - }, /* (GMT+04:00) Yerevan */ - { - "Atlantic Standard Time", "Atlantic Daylight Time", - "Canada/Atlantic" - }, /* (GMT-04:00) Atlantic Time (Canada) */ - { - "AUS Central Standard Time", "AUS Central Daylight Time", - "Australia/Darwin" - }, /* (GMT+09:30) Darwin */ - { - "AUS Eastern Standard Time", "AUS Eastern Daylight Time", - "Australia/Canberra" - }, /* (GMT+10:00) Canberra, Melbourne, Sydney */ - { - "Azerbaijan Standard Time", "Azerbaijan Daylight Time", - "Asia/Baku" - }, /* (GMT+04:00) Baku */ - { - "Azores Standard Time", "Azores Daylight Time", - "Atlantic/Azores" - }, /* (GMT-01:00) Azores */ - { - "Bangladesh Standard Time", "Bangladesh Daylight Time", - "Asia/Dhaka" - }, /* (GMT+06:00) Dhaka */ - { - "Canada Central Standard Time", "Canada Central Daylight Time", - "Canada/Saskatchewan" - }, /* (GMT-06:00) Saskatchewan */ - { - "Cape Verde Standard Time", "Cape Verde Daylight Time", - "Atlantic/Cape_Verde" - }, /* (GMT-01:00) Cape Verde Is. */ - { - "Caucasus Standard Time", "Caucasus Daylight Time", - "Asia/Baku" - }, /* (GMT+04:00) Baku, Tbilisi, Yerevan */ - { - "Cen. Australia Standard Time", "Cen. Australia Daylight Time", - "Australia/Adelaide" - }, /* (GMT+09:30) Adelaide */ - { - "Central America Standard Time", "Central America Daylight Time", - "CST6CDT" - }, /* (GMT-06:00) Central America */ - { - "Central Asia Standard Time", "Central Asia Daylight Time", - "Asia/Dhaka" - }, /* (GMT+06:00) Astana, Dhaka */ - { - "Central Brazilian Standard Time", "Central Brazilian Daylight Time", - "America/Cuiaba" - }, /* (GMT-04:00) Cuiaba */ - { - "Central Europe Standard Time", "Central Europe Daylight Time", - "Europe/Belgrade" - }, /* (GMT+01:00) Belgrade, Bratislava, Budapest, - * Ljubljana, Prague */ - { - "Central European Standard Time", "Central European Daylight Time", - "Europe/Sarajevo" - }, /* (GMT+01:00) Sarajevo, Skopje, Warsaw, - * Zagreb */ - { - "Central Pacific Standard Time", "Central Pacific Daylight Time", - "Pacific/Noumea" - }, /* (GMT+11:00) Magadan, Solomon Is., New - * Caledonia */ - { - "Central Standard Time", "Central Daylight Time", - "US/Central" - }, /* (GMT-06:00) Central Time (US & Canada) */ - { - "Central Standard Time (Mexico)", "Central Daylight Time (Mexico)", - "America/Mexico_City" - }, /* (GMT-06:00) Guadalajara, Mexico City, - * Monterrey - New */ - { - "China Standard Time", "China Daylight Time", - "Asia/Hong_Kong" - }, /* (GMT+08:00) Beijing, Chongqing, Hong Kong, - * Urumqi */ - { - "Dateline Standard Time", "Dateline Daylight Time", - "Etc/GMT+12" - }, /* (GMT-12:00) International Date Line West */ - { - "E. Africa Standard Time", "E. Africa Daylight Time", - "Africa/Nairobi" - }, /* (GMT+03:00) Nairobi */ - { - "E. Australia Standard Time", "E. Australia Daylight Time", - "Australia/Brisbane" - }, /* (GMT+10:00) Brisbane */ - { - "E. Europe Standard Time", "E. Europe Daylight Time", - "Europe/Bucharest" - }, /* (GMT+02:00) Bucharest */ - { - "E. South America Standard Time", "E. South America Daylight Time", - "America/Araguaina" - }, /* (GMT-03:00) Brasilia */ - { - "Eastern Standard Time", "Eastern Daylight Time", - "US/Eastern" - }, /* (GMT-05:00) Eastern Time (US & Canada) */ - { - "Egypt Standard Time", "Egypt Daylight Time", - "Africa/Cairo" - }, /* (GMT+02:00) Cairo */ - { - "Ekaterinburg Standard Time", "Ekaterinburg Daylight Time", - "Asia/Yekaterinburg" - }, /* (GMT+05:00) Ekaterinburg */ - { - "Fiji Standard Time", "Fiji Daylight Time", - "Pacific/Fiji" - }, /* (GMT+12:00) Fiji, Kamchatka, Marshall Is. */ - { - "FLE Standard Time", "FLE Daylight Time", - "Europe/Helsinki" - }, /* (GMT+02:00) Helsinki, Kyiv, Riga, Sofia, - * Tallinn, Vilnius */ - { - "Georgian Standard Time", "Georgian Daylight Time", - "Asia/Tbilisi" - }, /* (GMT+03:00) Tbilisi */ - { - "GMT Standard Time", "GMT Daylight Time", - "Europe/London" - }, /* (GMT) Greenwich Mean Time : Dublin, - * Edinburgh, Lisbon, London */ - { - "Greenland Standard Time", "Greenland Daylight Time", - "America/Godthab" - }, /* (GMT-03:00) Greenland */ - { - "Greenwich Standard Time", "Greenwich Daylight Time", - "Africa/Casablanca" - }, /* (GMT) Casablanca, Monrovia */ - { - "GTB Standard Time", "GTB Daylight Time", - "Europe/Athens" - }, /* (GMT+02:00) Athens, Istanbul, Minsk */ - { - "Hawaiian Standard Time", "Hawaiian Daylight Time", - "US/Hawaii" - }, /* (GMT-10:00) Hawaii */ - { - "India Standard Time", "India Daylight Time", - "Asia/Calcutta" - }, /* (GMT+05:30) Chennai, Kolkata, Mumbai, New - * Delhi */ - { - "Iran Standard Time", "Iran Daylight Time", - "Asia/Tehran" - }, /* (GMT+03:30) Tehran */ - { - "Jerusalem Standard Time", "Jerusalem Daylight Time", - "Asia/Jerusalem" - }, /* (GMT+02:00) Jerusalem */ - { - "Jordan Standard Time", "Jordan Daylight Time", - "Asia/Amman" - }, /* (GMT+02:00) Amman */ - { - "Kamchatka Standard Time", "Kamchatka Daylight Time", - "Asia/Kamchatka" - }, /* (GMT+12:00) Petropavlovsk-Kamchatsky */ - { - "Korea Standard Time", "Korea Daylight Time", - "Asia/Seoul" - }, /* (GMT+09:00) Seoul */ - { - "Mauritius Standard Time", "Mauritius Daylight Time", - "Indian/Mauritius" - }, /* (GMT+04:00) Port Louis */ - { - "Mexico Standard Time", "Mexico Daylight Time", - "America/Mexico_City" - }, /* (GMT-06:00) Guadalajara, Mexico City, - * Monterrey */ - { - "Mexico Standard Time 2", "Mexico Daylight Time 2", - "America/Chihuahua" - }, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan */ - { - "Mid-Atlantic Standard Time", "Mid-Atlantic Daylight Time", - "Atlantic/South_Georgia" - }, /* (GMT-02:00) Mid-Atlantic */ - { - "Middle East Standard Time", "Middle East Daylight Time", - "Asia/Beirut" - }, /* (GMT+02:00) Beirut */ - { - "Montevideo Standard Time", "Montevideo Daylight Time", - "America/Montevideo" - }, /* (GMT-03:00) Montevideo */ - { - "Morocco Standard Time", "Morocco Daylight Time", - "Africa/Casablanca" - }, /* (GMT) Casablanca */ - { - "Mountain Standard Time", "Mountain Daylight Time", - "US/Mountain" - }, /* (GMT-07:00) Mountain Time (US & Canada) */ - { - "Mountain Standard Time (Mexico)", "Mountain Daylight Time (Mexico)", - "America/Chihuahua" - }, /* (GMT-07:00) Chihuahua, La Paz, Mazatlan - - * New */ - { - "Myanmar Standard Time", "Myanmar Daylight Time", - "Asia/Rangoon" - }, /* (GMT+06:30) Rangoon */ - { - "N. Central Asia Standard Time", "N. Central Asia Daylight Time", - "Asia/Novosibirsk" - }, /* (GMT+06:00) Novosibirsk */ - { - "Namibia Standard Time", "Namibia Daylight Time", - "Africa/Windhoek" - }, /* (GMT+02:00) Windhoek */ - { - "Nepal Standard Time", "Nepal Daylight Time", - "Asia/Katmandu" - }, /* (GMT+05:45) Kathmandu */ - { - "New Zealand Standard Time", "New Zealand Daylight Time", - "Pacific/Auckland" - }, /* (GMT+12:00) Auckland, Wellington */ - { - "Newfoundland Standard Time", "Newfoundland Daylight Time", - "Canada/Newfoundland" - }, /* (GMT-03:30) Newfoundland */ - { - "North Asia East Standard Time", "North Asia East Daylight Time", - "Asia/Irkutsk" - }, /* (GMT+08:00) Irkutsk, Ulaan Bataar */ - { - "North Asia Standard Time", "North Asia Daylight Time", - "Asia/Krasnoyarsk" - }, /* (GMT+07:00) Krasnoyarsk */ - { - "Pacific SA Standard Time", "Pacific SA Daylight Time", - "America/Santiago" - }, /* (GMT-04:00) Santiago */ - { - "Pacific Standard Time", "Pacific Daylight Time", - "US/Pacific" - }, /* (GMT-08:00) Pacific Time (US & Canada); - * Tijuana */ - { - "Pacific Standard Time (Mexico)", "Pacific Daylight Time (Mexico)", - "America/Tijuana" - }, /* (GMT-08:00) Tijuana, Baja California */ - { - "Pakistan Standard Time", "Pakistan Daylight Time", - "Asia/Karachi" - }, /* (GMT+05:00) Islamabad, Karachi */ - { - "Paraguay Standard Time", "Paraguay Daylight Time", - "America/Asuncion" - }, /* (GMT-04:00) Asuncion */ - { - "Romance Standard Time", "Romance Daylight Time", - "Europe/Brussels" - }, /* (GMT+01:00) Brussels, Copenhagen, Madrid, - * Paris */ - { - "Russian Standard Time", "Russian Daylight Time", - "Europe/Moscow" - }, /* (GMT+03:00) Moscow, St. Petersburg, - * Volgograd */ - { - "SA Eastern Standard Time", "SA Eastern Daylight Time", - "America/Buenos_Aires" - }, /* (GMT-03:00) Buenos Aires, Georgetown */ - { - "SA Pacific Standard Time", "SA Pacific Daylight Time", - "America/Bogota" - }, /* (GMT-05:00) Bogota, Lima, Quito */ - { - "SA Western Standard Time", "SA Western Daylight Time", - "America/Caracas" - }, /* (GMT-04:00) Caracas, La Paz */ - { - "Samoa Standard Time", "Samoa Daylight Time", - "Pacific/Midway" - }, /* (GMT-11:00) Midway Island, Samoa */ - { - "SE Asia Standard Time", "SE Asia Daylight Time", - "Asia/Bangkok" - }, /* (GMT+07:00) Bangkok, Hanoi, Jakarta */ - { - "Malay Peninsula Standard Time", "Malay Peninsula Daylight Time", - "Asia/Kuala_Lumpur" - }, /* (GMT+08:00) Kuala Lumpur, Singapore */ - { - "South Africa Standard Time", "South Africa Daylight Time", - "Africa/Harare" - }, /* (GMT+02:00) Harare, Pretoria */ - { - "Sri Lanka Standard Time", "Sri Lanka Daylight Time", - "Asia/Colombo" - }, /* (GMT+06:00) Sri Jayawardenepura */ - { - "Taipei Standard Time", "Taipei Daylight Time", - "Asia/Taipei" - }, /* (GMT+08:00) Taipei */ - { - "Tasmania Standard Time", "Tasmania Daylight Time", - "Australia/Hobart" - }, /* (GMT+10:00) Hobart */ - { - "Tokyo Standard Time", "Tokyo Daylight Time", - "Asia/Tokyo" - }, /* (GMT+09:00) Osaka, Sapporo, Tokyo */ - { - "Tonga Standard Time", "Tonga Daylight Time", - "Pacific/Tongatapu" - }, /* (GMT+13:00) Nuku'alofa */ - { - "Ulaanbaatar Standard Time", "Ulaanbaatar Daylight Time", - "Asia/Ulaanbaatar", - }, /* (GMT+08:00) Ulaanbaatar */ - { - "US Eastern Standard Time", "US Eastern Daylight Time", - "US/Eastern" - }, /* (GMT-05:00) Indiana (East) */ - { - "US Mountain Standard Time", "US Mountain Daylight Time", - "US/Arizona" - }, /* (GMT-07:00) Arizona */ - { - "Coordinated Universal Time", "Coordinated Universal Time", - "UTC" - }, /* (GMT) Coordinated Universal Time */ - { - "UTC+12", "UTC+12", - "Etc/GMT+12" - }, /* (GMT+12:00) Coordinated Universal Time+12 */ - { - "UTC-02", "UTC-02", - "Etc/GMT-02" - }, /* (GMT-02:00) Coordinated Universal Time-02 */ - { - "UTC-11", "UTC-11", - "Etc/GMT-11" - }, /* (GMT-11:00) Coordinated Universal Time-11 */ - { - "Venezuela Standard Time", "Venezuela Daylight Time", - "America/Caracas", - }, /* (GMT-04:30) Caracas */ - { - "Vladivostok Standard Time", "Vladivostok Daylight Time", - "Asia/Vladivostok" - }, /* (GMT+10:00) Vladivostok */ - { - "W. Australia Standard Time", "W. Australia Daylight Time", - "Australia/Perth" - }, /* (GMT+08:00) Perth */ -#ifdef NOT_USED - /* Could not find a match for this one (just a guess). Excluded for now. */ - { - "W. Central Africa Standard Time", "W. Central Africa Daylight Time", - "WAT" - }, /* (GMT+01:00) West Central Africa */ -#endif - { - "W. Europe Standard Time", "W. Europe Daylight Time", - "CET" - }, /* (GMT+01:00) Amsterdam, Berlin, Bern, Rome, - * Stockholm, Vienna */ - { - "West Asia Standard Time", "West Asia Daylight Time", - "Asia/Karachi" - }, /* (GMT+05:00) Islamabad, Karachi, Tashkent */ - { - "West Pacific Standard Time", "West Pacific Daylight Time", - "Pacific/Guam" - }, /* (GMT+10:00) Guam, Port Moresby */ - { - "Yakutsk Standard Time", "Yakutsk Daylight Time", - "Asia/Yakutsk" - }, /* (GMT+09:00) Yakutsk */ - { - NULL, NULL, NULL - } -}; - -static const char * -identify_system_timezone(void) -{ - int i; - char tzname[128]; - char localtzname[256]; - time_t t = time(NULL); - struct tm *tm = localtime(&t); - HKEY rootKey; - int idx; - - if (!tm) - { - ereport(LOG, - (errmsg("could not identify system time zone: localtime() failed"), - errdetail("The PostgreSQL time zone will be set to \"%s\".", - "GMT"), - errhint("You can specify the correct timezone in postgresql.conf."))); - return NULL; /* go to GMT */ - } - - memset(tzname, 0, sizeof(tzname)); - strftime(tzname, sizeof(tzname) - 1, "%Z", tm); - - for (i = 0; win32_tzmap[i].stdname != NULL; i++) - { - if (strcmp(tzname, win32_tzmap[i].stdname) == 0 || - strcmp(tzname, win32_tzmap[i].dstname) == 0) - { - elog(DEBUG4, "TZ \"%s\" matches system time zone \"%s\"", - win32_tzmap[i].pgtzname, tzname); - return win32_tzmap[i].pgtzname; - } - } - - /* - * Localized Windows versions return localized names for the timezone. - * Scan the registry to find the English name, and then try matching - * against our table again. - */ - memset(localtzname, 0, sizeof(localtzname)); - if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, - "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones", - 0, - KEY_READ, - &rootKey) != ERROR_SUCCESS) - { - ereport(LOG, - (errmsg("could not open registry key to identify system time zone: error code %lu", - GetLastError()), - errdetail("The PostgreSQL time zone will be set to \"%s\".", - "GMT"), - errhint("You can specify the correct timezone in postgresql.conf."))); - return NULL; /* go to GMT */ - } - - for (idx = 0;; idx++) - { - char keyname[256]; - char zonename[256]; - DWORD namesize; - FILETIME lastwrite; - HKEY key; - LONG r; - - memset(keyname, 0, sizeof(keyname)); - namesize = sizeof(keyname); - if ((r = RegEnumKeyEx(rootKey, - idx, - keyname, - &namesize, - NULL, - NULL, - NULL, - &lastwrite)) != ERROR_SUCCESS) - { - if (r == ERROR_NO_MORE_ITEMS) - break; - ereport(LOG, - (errmsg_internal("could not enumerate registry subkeys to identify system time zone: %d", (int) r))); - break; - } - - if ((r = RegOpenKeyEx(rootKey, keyname, 0, KEY_READ, &key)) != ERROR_SUCCESS) - { - ereport(LOG, - (errmsg_internal("could not open registry subkey to identify system time zone: %d", (int) r))); - break; - } - - memset(zonename, 0, sizeof(zonename)); - namesize = sizeof(zonename); - if ((r = RegQueryValueEx(key, "Std", NULL, NULL, (unsigned char *) zonename, &namesize)) != ERROR_SUCCESS) - { - ereport(LOG, - (errmsg_internal("could not query value for key \"std\" to identify system time zone \"%s\": %d", - keyname, (int) r))); - RegCloseKey(key); - continue; /* Proceed to look at the next timezone */ - } - if (strcmp(tzname, zonename) == 0) - { - /* Matched zone */ - strcpy(localtzname, keyname); - RegCloseKey(key); - break; - } - memset(zonename, 0, sizeof(zonename)); - namesize = sizeof(zonename); - if ((r = RegQueryValueEx(key, "Dlt", NULL, NULL, (unsigned char *) zonename, &namesize)) != ERROR_SUCCESS) - { - ereport(LOG, - (errmsg_internal("could not query value for key \"dlt\" to identify system time zone \"%s\": %d", - keyname, (int) r))); - RegCloseKey(key); - continue; /* Proceed to look at the next timezone */ - } - if (strcmp(tzname, zonename) == 0) - { - /* Matched DST zone */ - strcpy(localtzname, keyname); - RegCloseKey(key); - break; - } - - RegCloseKey(key); - } - - RegCloseKey(rootKey); - - if (localtzname[0]) - { - /* Found a localized name, so scan for that one too */ - for (i = 0; win32_tzmap[i].stdname != NULL; i++) - { - if (strcmp(localtzname, win32_tzmap[i].stdname) == 0 || - strcmp(localtzname, win32_tzmap[i].dstname) == 0) - { - elog(DEBUG4, "TZ \"%s\" matches localized system time zone \"%s\" (\"%s\")", - win32_tzmap[i].pgtzname, tzname, localtzname); - return win32_tzmap[i].pgtzname; - } - } - } - - ereport(LOG, - (errmsg("could not find a match for system time zone \"%s\"", - tzname), - errdetail("The PostgreSQL time zone will be set to \"%s\".", - "GMT"), - errhint("You can specify the correct timezone in postgresql.conf."))); - return NULL; /* go to GMT */ -} -#endif /* WIN32 */ - - - /* * We keep loaded timezones in a hashtable so we don't have to * load and parse the TZ definition file every time one is selected. @@ -1262,8 +203,18 @@ init_timezone_hashtable(void) /* * Load a timezone from file or from cache. * Does not verify that the timezone is acceptable! + * + * "GMT" is always interpreted as the tzparse() definition, without attempting + * to load a definition from the filesystem. This has a number of benefits: + * 1. It's guaranteed to succeed, so we don't have the failure mode wherein + * the bootstrap default timezone setting doesn't work (as could happen if + * the OS attempts to supply a leap-second-aware version of "GMT"). + * 2. Because we aren't accessing the filesystem, we can safely initialize + * the "GMT" zone definition before my_exec_path is known. + * 3. It's quick enough that we don't waste much time when the bootstrap + * default timezone setting is later overridden from postgresql.conf. */ -struct pg_tz * +pg_tz * pg_tzset(const char *name) { pg_tz_cache *tzp; @@ -1300,7 +251,20 @@ pg_tzset(const char *name) return &tzp->tz; } - if (tzload(uppername, canonname, &tzstate, TRUE) != 0) + /* + * "GMT" is always sent to tzparse(), as per discussion above. + */ + if (strcmp(uppername, "GMT") == 0) + { + if (tzparse(uppername, &tzstate, TRUE) != 0) + { + /* This really, really should not happen ... */ + elog(ERROR, "could not initialize GMT time zone"); + } + /* Use uppercase name as canonical */ + strcpy(canonname, uppername); + } + else if (tzload(uppername, canonname, &tzstate, TRUE) != 0) { if (uppername[0] == ':' || tzparse(uppername, &tzstate, FALSE) != 0) { @@ -1325,176 +289,27 @@ pg_tzset(const char *name) } -/* - * Check whether timezone is acceptable. - * - * What we are doing here is checking for leap-second-aware timekeeping. - * We need to reject such TZ settings because they'll wreak havoc with our - * date/time arithmetic. - * - * NB: this must NOT ereport(ERROR). The caller must get control back so that - * it can restore the old value of TZ if we don't like the new one. - */ -bool -tz_acceptable(pg_tz *tz) -{ - struct pg_tm *tt; - pg_time_t time2000; - - /* - * To detect leap-second timekeeping, run pg_localtime for what should be - * GMT midnight, 2000-01-01. Insist that the tm_sec value be zero; any - * other result has to be due to leap seconds. - */ - time2000 = (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * SECS_PER_DAY; - tt = pg_localtime(&time2000, tz); - if (!tt || tt->tm_sec != 0) - return false; - - return true; -} - - -/* - * Get a pg_tz struct for the given timezone name. Returns NULL if name - * is invalid or not an "acceptable" zone. - */ -static pg_tz * -get_pg_tz_for_zone(const char *tzname) -{ - pg_tz *tz; - - if (!tzname || !tzname[0]) - return NULL; - - tz = pg_tzset(tzname); - if (!tz) - return NULL; - - if (!tz_acceptable(tz)) - return NULL; - - return tz; -} - -/* - * Identify a suitable default timezone setting based on the environment. - * - * We first look to the TZ environment variable. If not found or not - * recognized by our own code, we see if we can identify the timezone - * from the behavior of the system timezone library. When all else fails, - * fall back to GMT. - */ -static pg_tz * -select_default_timezone(void) -{ - pg_tz *def_tz; - - def_tz = get_pg_tz_for_zone(getenv("TZ")); - if (def_tz) - return def_tz; - - def_tz = get_pg_tz_for_zone(identify_system_timezone()); - if (def_tz) - return def_tz; - - def_tz = get_pg_tz_for_zone("GMT"); - if (def_tz) - return def_tz; - - ereport(FATAL, - (errmsg("could not select a suitable default time zone"), - errdetail("It appears that your GMT time zone uses leap seconds. PostgreSQL does not support leap seconds."))); - return NULL; /* keep compiler quiet */ -} - - -/* - * Pre-initialize timezone library - * - * This is called before GUC variable initialization begins. Its purpose - * is to ensure that elog.c has a pgtz variable available to format timestamps - * with, in case log_line_prefix is set to a value requiring that. We cannot - * set log_timezone yet. - */ -void -pg_timezone_pre_initialize(void) -{ - /* - * We can't use tzload() because we may not know where PGSHAREDIR is (in - * particular this is true in an EXEC_BACKEND subprocess). Since this - * timezone variable will only be used for emergency fallback purposes, it - * seems OK to just use the "lastditch" case provided by tzparse(). - */ - if (tzparse("GMT", &gmt_timezone_data.state, TRUE) != 0) - elog(FATAL, "could not initialize GMT time zone"); - strcpy(gmt_timezone_data.TZname, "GMT"); - gmt_timezone = &gmt_timezone_data; -} - /* * Initialize timezone library * - * This is called after initial loading of postgresql.conf. If no TimeZone - * setting was found therein, we try to derive one from the environment. - * Likewise for log_timezone. - * - * Note: this is also called from ProcessConfigFile, to re-establish valid - * GUC settings if the GUCs have been reset to default following their - * removal from postgresql.conf. + * This is called before GUC variable initialization begins. Its purpose + * is to ensure that log_timezone has a valid value before any logging GUC + * variables could become set to values that require elog.c to provide + * timestamps (e.g., log_line_prefix). We may as well initialize + * session_timestamp to something valid, too. */ void pg_timezone_initialize(void) { - pg_tz *def_tz = NULL; - /* - * Make sure that session_timezone and log_timezone are set. - * (session_timezone could still be NULL even if a timezone value was set - * in postgresql.conf, if that setting was interval-based rather than - * timezone-based.) + * We may not yet know where PGSHAREDIR is (in particular this is true in + * an EXEC_BACKEND subprocess). So use "GMT", which pg_tzset forces to + * be interpreted without reference to the filesystem. This corresponds + * to the bootstrap default for these variables in guc.c, although in + * principle it could be different. */ - if (!session_timezone) - { - def_tz = select_default_timezone(); - session_timezone = def_tz; - } - if (!log_timezone) - { - /* Don't duplicate work */ - if (!def_tz) - def_tz = select_default_timezone(); - log_timezone = def_tz; - } - - /* - * Now, set the timezone and log_timezone GUCs if they're still default. - * (This will redundantly call pg_tzset().) - * - * We choose to label these values PGC_S_ENV_VAR, rather than - * PGC_S_DYNAMIC_DEFAULT which would be functionally equivalent, because - * they came either from getenv("TZ") or from libc behavior that's - * determined by process environment of some kind. - * - * Note: in the case where a setting has just been removed from - * postgresql.conf, this code will not do what you might expect, namely - * call select_default_timezone() and install that value as the setting. - * Rather, the previously active setting --- typically the one from - * postgresql.conf --- will be reinstalled, relabeled as PGC_S_ENV_VAR. If - * we did try to install the "correct" default value, the effect would be - * that each postmaster child would independently run an extremely - * expensive search of the timezone database, bringing the database to its - * knees for possibly multiple seconds. This is so unpleasant, and could - * so easily be triggered quite unintentionally, that it seems better to - * violate the principle of least astonishment. - */ - if (GetConfigOptionResetString("timezone") == NULL) - SetConfigOption("timezone", pg_get_timezone_name(session_timezone), - PGC_POSTMASTER, PGC_S_ENV_VAR); - - if (GetConfigOptionResetString("log_timezone") == NULL) - SetConfigOption("log_timezone", pg_get_timezone_name(log_timezone), - PGC_POSTMASTER, PGC_S_ENV_VAR); + session_timezone = pg_tzset("GMT"); + log_timezone = session_timezone; } @@ -1608,7 +423,7 @@ pg_tzenumerate_next(pg_tzenum *dir) continue; } - if (!tz_acceptable(&dir->tz)) + if (!pg_tz_acceptable(&dir->tz)) { /* Ignore leap-second zones */ continue;