Skip to content

Commit

Permalink
Merge branch 'release/1.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cdown committed Jul 31, 2019
2 parents 2b80de9 + 29cbe76 commit 31f69e3
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 25 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ script:

matrix:
include:
# "coverage" toxenv runs tests, so no need to run TOXENV=py37
- python: '3.7'
env: TOXENV=py37,black,pylint,coverage
env: TOXENV=black,pylint,pytype,bandit,coverage
52 changes: 52 additions & 0 deletions contrib/09-timezone
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/bin/sh

if [ "$2" = "up" ]; then
TIMEZONE=$(tzupdate -p)
SYS_TZ=$(tzupdate --print-sys)

if [ "$TIMEZONE" = "$SYS_TZ" ]; then
echo "Timezone unchanged"
exit
fi

GDBUS_DEST="org.freedesktop.Notifications"
GDBUS_OBJ_PATH="/org/freedesktop/Notifications"
GDBUS_ARGS="--session --dest $GDBUS_DEST --object-path $GDBUS_OBJ_PATH"

NOTIFY_METHOD="--method org.freedesktop.Notifications.Notify"
NOTIF_SUMMARY="'Change timezone to $TIMEZONE ?'"
NOTIF_ACTIONS="'[\"changetz\", \"Yes\"]'"
NOTIF_HINTS="'{\"urgency\": <byte 1>}'"
NOTIF_ARGS="tzupdate 0 '' $NOTIF_SUMMARY '' $NOTIF_ACTIONS $NOTIF_HINTS 'int32 -1'"
SEND_NOTIF="gdbus call $GDBUS_ARGS $NOTIFY_METHOD $NOTIF_ARGS"

RECIEVE_ACTION="gdbus monitor $GDBUS_ARGS"
NOTIF_REGEXP="^$GDBUS_OBJ_PATH: $GDBUS_DEST.\\(.*\\)$"

GDBUS_MONITOR_PID=$(mktemp)

USER=$(who | awk -v vt=tty"$(fgconsole)" '$0 ~ vt {print $1}')
DISPLAY=$(w --from "$USER" | awk -F ' +' 'END{print $2}')
XAUTHORITY=$(eval echo ~"$USER"/.Xauthority)

export DISPLAY
export XAUTHORITY
eval "$(dbus-launch --auto-syntax)"

NOTIF_ID=$(eval "$SEND_NOTIF" | sed 's/(uint32 \([0-9]\+\),)/\1/g')
NOTIF_CLOSE_REGEXP="^NotificationClosed (uint32 $NOTIF_ID, uint32 [0-9]\+)$"

( $RECIEVE_ACTION & echo $! >&3 ) 3>"$GDBUS_MONITOR_PID" | while read -r signal
do
recv="$(printf '%s' "$signal" | sed "s~$NOTIF_REGEXP~\\1~")"
if printf '%s' "$recv" | grep "$NOTIF_CLOSE_REGEXP" >/dev/null 2>&1; then
break
fi
if [ "$recv" = "ActionInvoked (uint32 $NOTIF_ID, 'changetz')" ]; then
tzupdate -t "$TIMEZONE"
break
fi
done
kill "$(cat "$GDBUS_MONITOR_PID")"
rm -f "$GDBUS_MONITOR_PID"
fi
9 changes: 9 additions & 0 deletions contrib/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Automatic timezone change detection with NetworkManager
=======================================================

The ``09-timezone`` script can be placed in the
``/etc/NetworkManager/dispatcher.d/`` folder. This script will automatically
launch tzupdate upon network connection. The user will therefore be prompted to
change the system timezone whenever the system connects to a network in a
different timezone. You also need a notification server with action support
like ``xfce4-notifyd`` for instance.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

setup(
name="tzupdate",
version="1.4.0",
version="1.5.0",
description="Set the system timezone based on IP geolocation",
long_description=README,
url="https://github.com/cdown/tzupdate",
Expand Down
1 change: 1 addition & 0 deletions tests/_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
).map(str)

FAKE_TIMEZONE = "Fake/Timezone"
FAKE_ZONEINFO_PATH = "/path/to/zoneinfo"
FAKE_ERROR = "Virus = very yes"

FAKE_SERVICES = [
Expand Down
30 changes: 19 additions & 11 deletions tests/e2e_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_end_to_end_no_args(link_localtime_mock, deb_tz_mock):
FAKE_TIMEZONE, tzupdate.DEFAULT_ZONEINFO_PATH, tzupdate.DEFAULT_LOCALTIME_PATH
)
deb_tz_mock.assert_called_once_with(
FAKE_TIMEZONE, tzupdate.DEFAULT_DEBIAN_TIMEZONE_PATH
FAKE_TIMEZONE, tzupdate.DEFAULT_DEBIAN_TIMEZONE_PATH, True
)


Expand All @@ -33,6 +33,15 @@ def test_print_only_no_link(link_localtime_mock, deb_tz_mock):
assert_false(deb_tz_mock.called)


@mock.patch("tzupdate.write_debian_timezone")
@mock.patch("tzupdate.link_localtime")
def test_print_sys_tz_no_link(link_localtime_mock, deb_tz_mock):
args = ["--print-system-timezone"]
tzupdate.main(args, services=FAKE_SERVICES)
assert_false(link_localtime_mock.called)
assert_false(deb_tz_mock.called)


@httpretty.activate
@mock.patch("tzupdate.write_debian_timezone")
@mock.patch("tzupdate.link_localtime")
Expand All @@ -46,22 +55,19 @@ def test_explicit_paths(link_localtime_mock, deb_tz_mock):
link_localtime_mock.assert_called_once_with(
FAKE_TIMEZONE, zoneinfo_path, localtime_path
)
deb_tz_mock.assert_called_once_with(FAKE_TIMEZONE, deb_path)
deb_tz_mock.assert_called_once_with(FAKE_TIMEZONE, deb_path, True)


@httpretty.activate
@mock.patch("tzupdate.write_debian_timezone")
@mock.patch("tzupdate.link_localtime")
def test_explicit_ip(_unused_ll, _unused_deb):
setup_basic_api_response()
@mock.patch("tzupdate.get_timezone")
def test_explicit_ip(get_timezone_mock, _unused_ll, _unused_deb):
ip_addr = "1.2.3.4"
args = ["-a", ip_addr]
tzupdate.main(args, services=FAKE_SERVICES)

# TODO (#16): httpretty.last_request() and
# get_timezone_for_ip.assert_called_once_with don't work for testing here
# because of the threading we use. We need to work out a good solution for
# this in
get_timezone_mock.assert_called_once_with(
ip_addr, timeout=mock.ANY, services=FAKE_SERVICES
)


@mock.patch("tzupdate.write_debian_timezone")
Expand All @@ -73,7 +79,9 @@ def test_explicit_timezone(link_localtime_mock, deb_tz_mock):
link_localtime_mock.assert_called_once_with(
timezone, tzupdate.DEFAULT_ZONEINFO_PATH, tzupdate.DEFAULT_LOCALTIME_PATH
)
deb_tz_mock.assert_called_once_with(timezone, tzupdate.DEFAULT_DEBIAN_TIMEZONE_PATH)
deb_tz_mock.assert_called_once_with(
timezone, tzupdate.DEFAULT_DEBIAN_TIMEZONE_PATH, True
)


@httpretty.activate
Expand Down
55 changes: 52 additions & 3 deletions tests/unit_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@
IP_ADDRESSES,
FAKE_SERVICES,
FAKE_TIMEZONE,
FAKE_ZONEINFO_PATH,
setup_basic_api_response,
)
from nose.tools import assert_raises, eq_ as eq, assert_true, assert_is_none, assert_in
from nose.tools import (
assert_raises,
eq_ as eq,
assert_true,
assert_is_none,
assert_in,
assert_false,
)
from parameterized import parameterized
from hypothesis import given, settings
from hypothesis.strategies import sampled_from, none, one_of, text, integers
Expand All @@ -34,6 +42,13 @@ def test_get_timezone_for_ip(ip, service):
fake_queue.put.assert_called_once_with(FAKE_TIMEZONE)


def test_get_sys_timezone():
systz = tzupdate.get_sys_timezone(
FAKE_ZONEINFO_PATH, FAKE_ZONEINFO_PATH + "/" + FAKE_TIMEZONE
)
assert systz == FAKE_TIMEZONE


@httpretty.activate
@given(one_of(IP_ADDRESSES, none()), sampled_from(FAKE_SERVICES))
@settings(max_examples=20)
Expand Down Expand Up @@ -161,9 +176,43 @@ def test_link_localtime_localtime_missing_no_raise(

@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path(timezone, tz_path):
def test_debian_tz_path_exists_not_forced(timezone, tz_path):
mo = mock.mock_open()
with mock.patch("tzupdate.open", mo, create=True):
tzupdate.write_debian_timezone(timezone, tz_path, must_exist=True)
mo.assert_called_once_with(tz_path, "r+")
mo().seek.assert_called_once_with(0)
mo().write.assert_called_once_with(timezone + "\n")


@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path_doesnt_exist_not_forced(timezone, tz_path):
mo = mock.mock_open()
mo.side_effect = OSError(errno.ENOENT, "")
with mock.patch("tzupdate.open", mo, create=True):
tzupdate.write_debian_timezone(timezone, tz_path, must_exist=True)
mo.assert_called_once_with(tz_path, "r+")


@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path_other_error_raises(timezone, tz_path):
mo = mock.mock_open()
code = errno.EPERM
mo.side_effect = OSError(code, "")
with mock.patch("tzupdate.open", mo, create=True):
with assert_raises(OSError) as thrown_exc:
tzupdate.write_debian_timezone(timezone, tz_path, must_exist=True)
eq(thrown_exc.exception.errno, code)


@given(text(), text())
@settings(max_examples=20)
def test_debian_tz_path_doesnt_exist_forced(timezone, tz_path):
mo = mock.mock_open()
with mock.patch("tzupdate.open", mo, create=True):
tzupdate.write_debian_timezone(timezone, tz_path)
tzupdate.write_debian_timezone(timezone, tz_path, must_exist=False)
mo.assert_called_once_with(tz_path, "w")
mo().seek.assert_called_once_with(0)
mo().write.assert_called_once_with(timezone + "\n")
17 changes: 16 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,22 @@ commands =
[testenv:black]
skipsdist = True
deps =
{[testenv]deps}
black
commands =
black --check .

[testenv:pytype]
skipsdist = True
deps =
{[testenv]deps}
pytype
commands =
pytype -d import-error .

[testenv:bandit]
skipsdist = True
deps =
{[testenv]deps}
bandit
commands =
bandit tzupdate.py
51 changes: 43 additions & 8 deletions tzupdate.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@
GeoIPService("https://freegeoip.app/json/{ip}", ("time_zone",), None),
GeoIPService("https://ipapi.co/{ip}/json/", ("timezone",), ("reason",)),
GeoIPService("http://worldtimeapi.org/api/ip/{ip}", ("timezone",), ("error",)),
GeoIPService(
"https://timezoneapi.io/api/ip/?{ip}",
("data", "timezone", "id"),
("meta", "message"),
),
]
)

Expand Down Expand Up @@ -105,16 +100,29 @@ def get_timezone_for_ip(ip, service, queue_obj):
queue_obj.put(tz)


def write_debian_timezone(timezone, debian_timezone_path):
def write_debian_timezone(timezone, debian_timezone_path, must_exist=True):
"""
Debian and derivatives also have /etc/timezone, which is used for a human
readable timezone. Without this, dpkg-reconfigure will nuke /etc/localtime
on reconfigure.
If must_exist is True, we won't create debian_timezone_path if it doesn't
already exist.
"""
old_umask = os.umask(0o133)
mode = "w"

if must_exist:
mode = "r+"

try:
with open(debian_timezone_path, "w") as debian_tz_f:
with open(debian_timezone_path, mode) as debian_tz_f:
debian_tz_f.seek(0)
debian_tz_f.write(timezone + "\n")
except OSError as thrown_exc:
if must_exist and thrown_exc.errno == errno.ENOENT:
return
raise
finally:
os.umask(old_umask)

Expand Down Expand Up @@ -178,6 +186,12 @@ def link_localtime(timezone, zoneinfo_path, localtime_path):
return zoneinfo_tz_path


def get_sys_timezone(zoneinfo_abspath, localtime_abspath):
return localtime_abspath.replace(
os.path.commonprefix([zoneinfo_abspath, localtime_abspath]) + os.path.sep, "", 1
)


def parse_args(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
Expand All @@ -186,6 +200,11 @@ def parse_args(argv):
action="store_true",
help="print the timezone, but don't update the localtime file",
)
parser.add_argument(
"--print-system-timezone",
action="store_true",
help="print the current system timezone",
)
parser.add_argument(
"-a", "--ip", help="use this IP instead of automatically detecting it"
)
Expand All @@ -212,6 +231,11 @@ def parse_args(argv):
default=DEFAULT_DEBIAN_TIMEZONE_PATH,
help="path to Debian timezone name file (default: %(default)s)",
)
parser.add_argument(
"--always-write-debian-timezone",
action="store_true",
help="create debian timezone file even if it doesn't exist (default: %(default)s)",
)
parser.add_argument(
"-s",
"--timeout",
Expand Down Expand Up @@ -239,6 +263,15 @@ def main(argv=None, services=SERVICES):
args = parse_args(argv)
logging.basicConfig(level=args.log_level)

if args.print_system_timezone:
print(
get_sys_timezone(
os.path.realpath(args.zoneinfo_path),
os.path.realpath(args.localtime_path),
)
)
return

if args.timezone:
timezone = args.timezone
log.debug("Using explicitly passed timezone: %s", timezone)
Expand All @@ -249,7 +282,9 @@ def main(argv=None, services=SERVICES):
print(timezone)
else:
link_localtime(timezone, args.zoneinfo_path, args.localtime_path)
write_debian_timezone(timezone, args.debian_timezone_path)
write_debian_timezone(
timezone, args.debian_timezone_path, not args.always_write_debian_timezone
)
print("Set system timezone to %s." % timezone)


Expand Down

0 comments on commit 31f69e3

Please sign in to comment.