unix.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import logging
  2. import os
  3. import re
  4. import sys
  5. import warnings
  6. from datetime import timezone
  7. from tzlocal import utils
  8. import zoneinfo
  9. _cache_tz = None
  10. _cache_tz_name = None
  11. log = logging.getLogger("tzlocal")
  12. def _get_localzone_name(_root="/"):
  13. """Tries to find the local timezone configuration.
  14. This method finds the timezone name, if it can, or it returns None.
  15. The parameter _root makes the function look for files like /etc/localtime
  16. beneath the _root directory. This is primarily used by the tests.
  17. In normal usage you call the function without parameters."""
  18. # First try the ENV setting.
  19. tzenv = utils._tz_name_from_env()
  20. if tzenv:
  21. return tzenv
  22. # Are we under Termux on Android?
  23. if os.path.exists(os.path.join(_root, "system/bin/getprop")):
  24. log.debug("This looks like Termux")
  25. import subprocess
  26. try:
  27. androidtz = (
  28. subprocess.check_output(["getprop", "persist.sys.timezone"])
  29. .strip()
  30. .decode()
  31. )
  32. return androidtz
  33. except (OSError, subprocess.CalledProcessError):
  34. # proot environment or failed to getprop
  35. log.debug("It's not termux?")
  36. pass
  37. # Now look for distribution specific configuration files
  38. # that contain the timezone name.
  39. # Stick all of them in a dict, to compare later.
  40. found_configs = {}
  41. for configfile in ("etc/timezone", "var/db/zoneinfo"):
  42. tzpath = os.path.join(_root, configfile)
  43. try:
  44. with open(tzpath) as tzfile:
  45. data = tzfile.read()
  46. log.debug(f"{tzpath} found, contents:\n {data}")
  47. etctz = data.strip("/ \t\r\n")
  48. if not etctz:
  49. # Empty file, skip
  50. continue
  51. for etctz in etctz.splitlines():
  52. # Get rid of host definitions and comments:
  53. if " " in etctz:
  54. etctz, dummy = etctz.split(" ", 1)
  55. if "#" in etctz:
  56. etctz, dummy = etctz.split("#", 1)
  57. if not etctz:
  58. continue
  59. found_configs[tzpath] = etctz.replace(" ", "_")
  60. except (OSError, UnicodeDecodeError):
  61. # File doesn't exist or is a directory, or it's a binary file.
  62. continue
  63. # CentOS has a ZONE setting in /etc/sysconfig/clock,
  64. # OpenSUSE has a TIMEZONE setting in /etc/sysconfig/clock and
  65. # Gentoo has a TIMEZONE setting in /etc/conf.d/clock
  66. # We look through these files for a timezone:
  67. zone_re = re.compile(r"\s*ZONE\s*=\s*\"")
  68. timezone_re = re.compile(r"\s*TIMEZONE\s*=\s*\"")
  69. end_re = re.compile('"')
  70. for filename in ("etc/sysconfig/clock", "etc/conf.d/clock"):
  71. tzpath = os.path.join(_root, filename)
  72. try:
  73. with open(tzpath, "rt") as tzfile:
  74. data = tzfile.readlines()
  75. log.debug(f"{tzpath} found, contents:\n {data}")
  76. for line in data:
  77. # Look for the ZONE= setting.
  78. match = zone_re.match(line)
  79. if match is None:
  80. # No ZONE= setting. Look for the TIMEZONE= setting.
  81. match = timezone_re.match(line)
  82. if match is not None:
  83. # Some setting existed
  84. line = line[match.end() :]
  85. etctz = line[: end_re.search(line).start()]
  86. # We found a timezone
  87. found_configs[tzpath] = etctz.replace(" ", "_")
  88. except (OSError, UnicodeDecodeError):
  89. # UnicodeDecode handles when clock is symlink to /etc/localtime
  90. continue
  91. # systemd distributions use symlinks that include the zone name,
  92. # see manpage of localtime(5) and timedatectl(1)
  93. tzpath = os.path.join(_root, "etc/localtime")
  94. if os.path.exists(tzpath) and os.path.islink(tzpath):
  95. log.debug(f"{tzpath} found")
  96. etctz = os.path.realpath(tzpath)
  97. start = etctz.find("/") + 1
  98. while start != 0:
  99. etctz = etctz[start:]
  100. try:
  101. zoneinfo.ZoneInfo(etctz)
  102. tzinfo = f"{tzpath} is a symlink to"
  103. found_configs[tzinfo] = etctz.replace(" ", "_")
  104. # Only need first valid relative path in simlink.
  105. break
  106. except zoneinfo.ZoneInfoNotFoundError:
  107. pass
  108. start = etctz.find("/") + 1
  109. if len(found_configs) > 0:
  110. log.debug(f"{len(found_configs)} found:\n {found_configs}")
  111. # We found some explicit config of some sort!
  112. if len(found_configs) > 1:
  113. # Uh-oh, multiple configs. See if they match:
  114. unique_tzs = _get_unique_tzs(found_configs, _root)
  115. if len(unique_tzs) != 1 and "etc/timezone" in str(found_configs.keys()):
  116. # For some reason some distros are removing support for /etc/timezone,
  117. # which is bad, because that's the only place where the timezone is stated
  118. # in plain text, and what's worse, they don't delete it. So we can't trust
  119. # it now, so when we have conflicting configs, we just ignore it, with a warning.
  120. log.warning("/etc/timezone is deprecated in some distros, and no longer reliable. "
  121. "tzlocal is ignoring it, and you can likely delete it.")
  122. found_configs = {k: v for k, v in found_configs.items() if "etc/timezone" not in k}
  123. unique_tzs = _get_unique_tzs(found_configs, _root)
  124. if len(unique_tzs) != 1:
  125. message = "Multiple conflicting time zone configurations found:\n"
  126. for key, value in found_configs.items():
  127. message += f"{key}: {value}\n"
  128. message += "Fix the configuration, or set the time zone in a TZ environment variable.\n"
  129. raise zoneinfo.ZoneInfoNotFoundError(message)
  130. # We found exactly one config! Use it.
  131. return list(found_configs.values())[0]
  132. def _get_unique_tzs(found_configs, _root):
  133. unique_tzs = set()
  134. zoneinfopath = os.path.join(_root, "usr", "share", "zoneinfo")
  135. directory_depth = len(zoneinfopath.split(os.path.sep))
  136. for tzname in found_configs.values():
  137. # Look them up in /usr/share/zoneinfo, and find what they
  138. # really point to:
  139. path = os.path.realpath(os.path.join(zoneinfopath, *tzname.split("/")))
  140. real_zone_name = "/".join(path.split(os.path.sep)[directory_depth:])
  141. unique_tzs.add(real_zone_name)
  142. return unique_tzs
  143. def _get_localzone(_root="/"):
  144. """Creates a timezone object from the timezone name.
  145. If there is no timezone config, it will try to create a file from the
  146. localtime timezone, and if there isn't one, it will default to UTC.
  147. The parameter _root makes the function look for files like /etc/localtime
  148. beneath the _root directory. This is primarily used by the tests.
  149. In normal usage you call the function without parameters."""
  150. # First try the ENV setting.
  151. tzenv = utils._tz_from_env()
  152. if tzenv:
  153. return tzenv
  154. tzname = _get_localzone_name(_root)
  155. if tzname is None:
  156. # No explicit setting existed. Use localtime
  157. log.debug("No explicit setting existed. Use localtime")
  158. for filename in ("etc/localtime", "usr/local/etc/localtime"):
  159. tzpath = os.path.join(_root, filename)
  160. if not os.path.exists(tzpath):
  161. continue
  162. with open(tzpath, "rb") as tzfile:
  163. tz = zoneinfo.ZoneInfo.from_file(tzfile, key="local")
  164. break
  165. else:
  166. warnings.warn("Can not find any timezone configuration, defaulting to UTC.")
  167. utcname = [x for x in zoneinfo.available_timezones() if "UTC" in x]
  168. if utcname:
  169. tz = zoneinfo.ZoneInfo(utcname[0])
  170. else:
  171. tz = timezone.utc
  172. else:
  173. tz = zoneinfo.ZoneInfo(tzname)
  174. if _root == "/":
  175. # We are using a file in etc to name the timezone.
  176. # Verify that the timezone specified there is actually used:
  177. utils.assert_tz_offset(tz, error=False)
  178. return tz
  179. def get_localzone_name() -> str:
  180. """Get the computers configured local timezone name, if any."""
  181. global _cache_tz_name
  182. if _cache_tz_name is None:
  183. _cache_tz_name = _get_localzone_name()
  184. return _cache_tz_name
  185. def get_localzone() -> zoneinfo.ZoneInfo:
  186. """Get the computers configured local timezone, if any."""
  187. global _cache_tz
  188. if _cache_tz is None:
  189. _cache_tz = _get_localzone()
  190. return _cache_tz
  191. def reload_localzone() -> zoneinfo.ZoneInfo:
  192. """Reload the cached localzone. You need to call this if the timezone has changed."""
  193. global _cache_tz_name
  194. global _cache_tz
  195. _cache_tz_name = _get_localzone_name()
  196. _cache_tz = _get_localzone()
  197. return _cache_tz