From b7cd999aae226233850031d54f636ef6d250d7d1 Mon Sep 17 00:00:00 2001 From: jeroenvollenbrock Date: Sun, 15 Apr 2018 13:18:52 +0200 Subject: [PATCH] Monitor system timezone changes --- .gitignore | 1 + binding.gyp | 1 + index.js | 4 +-- src/binding.cc | 13 ++++++++- src/monitor.cc | 48 +++++++++++++++++++++++++++++++++ src/monitor.h | 23 ++++++++++++++++ src/monitor_android.cc | 9 +++++++ src/monitor_unix.cc | 53 +++++++++++++++++++++++++++++++++++++ src/monitor_windows.cc | 60 ++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 src/monitor.cc create mode 100644 src/monitor.h create mode 100644 src/monitor_android.cc create mode 100644 src/monitor_unix.cc create mode 100644 src/monitor_windows.cc diff --git a/.gitignore b/.gitignore index 08d0ad1..32190ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .nyc_output build +node_modules \ No newline at end of file diff --git a/binding.gyp b/binding.gyp index ae25db6..e3042b5 100644 --- a/binding.gyp +++ b/binding.gyp @@ -5,6 +5,7 @@ " #include +#include "monitor.h" + using v8::Date; using v8::FunctionTemplate; using v8::Isolate; @@ -11,7 +13,11 @@ using Nan::GetFunction; using Nan::New; using Nan::Set; -NAN_METHOD(Reset) { +TimeZoneMonitor *_monitor; + +void TimeZoneMonitor::Notify() { + Nan::HandleScope scope; + Isolate *isolate = Isolate::GetCurrent(); #ifdef _WIN32 _tzset(); @@ -21,7 +27,12 @@ NAN_METHOD(Reset) { Date::DateTimeConfigurationChangeNotification(isolate); } +NAN_METHOD(Reset) { + _monitor->Notify(); +} + NAN_MODULE_INIT(Init) { + _monitor = TimeZoneMonitor::Create(); Set(target, New("reset").ToLocalChecked(), GetFunction(New(Reset)).ToLocalChecked()); } diff --git a/src/monitor.cc b/src/monitor.cc new file mode 100644 index 0000000..5f4bb0d --- /dev/null +++ b/src/monitor.cc @@ -0,0 +1,48 @@ +#include "monitor.h" + +static const uint64_t kDebounceDelay = 500; + +void TimeZoneMonitor::_asyncWorkCb(uv_async_t *work) { + TimeZoneMonitor *monitor = static_cast(work->data); + monitor->ThreadedCallback(); + monitor->DebouncedNotify(); +} + +void TimeZoneMonitor::tzDelayExpired(uv_timer_t* handle) { + TimeZoneMonitor *monitor = static_cast(handle->data); + monitor->Notify(); +} + +TimeZoneMonitor::TimeZoneMonitor() { + uv_async_init(uv_default_loop(), &_asyncWork, _asyncWorkCb); + uv_timer_init(uv_default_loop(), &_debouncer); + _asyncWork.data = this; + _debouncer.data = this; + + uv_unref((uv_handle_t*)&_asyncWork); + uv_unref((uv_handle_t*)&_debouncer); +} + +void TimeZoneMonitor::ThreadedNotify() { + uv_async_send(&_asyncWork); +} + +void TimeZoneMonitor::DebouncedNotify() { + //Some platforms write the file instead of symlinking it, add a small delay just to be sure + uv_timer_start(&_debouncer, TimeZoneMonitor::tzDelayExpired, kDebounceDelay, 0); +} + +#ifdef _WIN32 +# include "monitor_windows.cc" +#elif __ANDROID__ +# include "monitor_android.cc" +#elif __APPLE__ +# include "monitor_unix.cc" +#elif __unix__ +# include "monitor_unix.cc" +#else +// static +TimeZoneMonitor *TimeZoneMonitor::Create() { + return new TimeZoneMonitor(); +} +#endif \ No newline at end of file diff --git a/src/monitor.h b/src/monitor.h new file mode 100644 index 0000000..3f2e219 --- /dev/null +++ b/src/monitor.h @@ -0,0 +1,23 @@ + +#ifndef _TZD_MONITOR_H +#define _TZD_MONITOR_H +#include + +class TimeZoneMonitor { + public: + // Returns a new TimeZoneMonitor object specific to the platform. + static TimeZoneMonitor *Create(); + void Notify(); + protected: + TimeZoneMonitor(); //Creation can only occur through create. + void DebouncedNotify(); + void ThreadedNotify(); + virtual void ThreadedCallback() {} + private: + static void tzDelayExpired(uv_timer_t* handle); + static void _asyncWorkCb(uv_async_t *work); + uv_async_t _asyncWork; + uv_timer_t _debouncer; +}; + +#endif \ No newline at end of file diff --git a/src/monitor_android.cc b/src/monitor_android.cc new file mode 100644 index 0000000..565428b --- /dev/null +++ b/src/monitor_android.cc @@ -0,0 +1,9 @@ + +//TODO: We should catch the TIMEZONE_CHANGED intent and fire ThreadedNotify +//Unfortunatly, it looks like this part requires some jni magic which may not work from inside a module +//Lets disable the notifications for now... + +// static +TimeZoneMonitor *TimeZoneMonitor::Create() { + return new TimeZoneMonitor(); +} \ No newline at end of file diff --git a/src/monitor_unix.cc b/src/monitor_unix.cc new file mode 100644 index 0000000..9b160d6 --- /dev/null +++ b/src/monitor_unix.cc @@ -0,0 +1,53 @@ +#include +#include +#include "monitor.h" + + +// There is no true standard for where time zone information is actually +// stored. glibc uses /etc/localtime, uClibc uses /etc/TZ, and some older +// systems store the name of the time zone file within /usr/share/zoneinfo +// in /etc/timezone. Different libraries and custom builds may mean that +// still more paths are used. Just watch all three of these paths, because +// false positives are harmless, assuming the false positive rate is +// reasonable. +static const char* kFilesToWatch[] = { + "localtime", + "timezone", + "TZ", +}; +// libuv seems to have some troubles when monitoring symlinks. +//It does however work if we monitor the parent folder non-recursively +static const char* kWatchFolder = "/etc/"; + +class TimeZoneMonitorUnix : public TimeZoneMonitor { + public: + TimeZoneMonitorUnix(); + private: + static void tzChange(uv_fs_event_t *handle, const char *filename, int events, int status); + uv_fs_event_t fileWatcher; +}; + + +void TimeZoneMonitorUnix::tzChange(uv_fs_event_t *handle, const char *filename, int events, int status) { + TimeZoneMonitorUnix *monitor = static_cast(handle->data); + for (size_t i = 0; i < sizeof(kFilesToWatch)/sizeof(kFilesToWatch[0]); i++) { + if(strcmp(kFilesToWatch[i], filename) == 0) { + monitor->DebouncedNotify(); //Debounce the notification (sometimes multiple files change) + return; + } + } +} + +TimeZoneMonitorUnix::TimeZoneMonitorUnix() +{ + uv_fs_event_init(uv_default_loop(), &fileWatcher); + fileWatcher.data = this; + uv_unref((uv_handle_t*)&fileWatcher); + + uv_fs_event_start(&fileWatcher, TimeZoneMonitorUnix::tzChange, kWatchFolder, 0); +} + +// static +TimeZoneMonitor *TimeZoneMonitor::Create() { + return new TimeZoneMonitorUnix(); +} \ No newline at end of file diff --git a/src/monitor_windows.cc b/src/monitor_windows.cc new file mode 100644 index 0000000..158c473 --- /dev/null +++ b/src/monitor_windows.cc @@ -0,0 +1,60 @@ +#include "monitor.h" + +static const DWORD dwFilter = REG_NOTIFY_CHANGE_LAST_SET; + +class TimeZoneMonitorWindows : public TimeZoneMonitor { + public: + TimeZoneMonitorWindows(); + protected: + void ThreadedCallback(); + private: + void Notify(); + void SetUp(void); + static void TimeZoneChangeThreadedCallback(PVOID context, BOOLEAN timeout); + HANDLE hTZEvent; + HKEY hRegKey; +}; + + +void TimeZoneMonitorWindows::ThreadedCallback() { + //Setup only causes notify to be called once + SetUp(); +} + +void TimeZoneMonitorWindows::SetUp() { + ResetEvent(hTZEvent); + // Watch the registry key for a change of value, must be run on main thread + RegNotifyChangeKeyValue(hRegKey, TRUE, + dwFilter, hTZEvent, TRUE); +} + +TimeZoneMonitorWindows::TimeZoneMonitorWindows() { + + HANDLE hWait; + LONG lErrorCode; + lErrorCode = RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation", 0, KEY_NOTIFY, &hRegKey); + if (lErrorCode != ERROR_SUCCESS) return; + + // Create an event. + hTZEvent = CreateEvent(NULL, TRUE, FALSE, NULL); + if (hTZEvent == NULL) return; + + SetUp(); + + //Invoke the TimeZoneChangeThreadedCallback upon a change + RegisterWaitForSingleObject( + &hWait, hTZEvent, TimeZoneChangeThreadedCallback, + this, INFINITE, WT_EXECUTEINWAITTHREAD + ); +} + +//static +void TimeZoneMonitorWindows::TimeZoneChangeThreadedCallback(PVOID context, BOOLEAN timeout) { + TimeZoneMonitorWindows *monitor = static_cast(context); + monitor->ThreadedNotify(); //First enter the main eventloop, then debounce the notify +} + +// static +TimeZoneMonitor *TimeZoneMonitor::Create() { + return new TimeZoneMonitorWindows(); +} \ No newline at end of file