From aa81eae65a3f07f3b10f4a8a05ec1a4dce6c9984 Mon Sep 17 00:00:00 2001 From: Scott Lamb Date: Fri, 31 Aug 2018 17:19:24 -0700 Subject: [PATCH] more robust timezone detection (fixes #12) --- Dockerfile | 1 - guide/install-manual.md | 3 +- scripts/script-functions.sh | 12 ------- src/cmds/run.rs | 72 +++++++++++++++++++++++++++++++++---- 4 files changed, 67 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index 146f50c..2e873a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,6 @@ RUN apt-get update && \ apt-get install -y apt-utils && \ apt-get install -y apt-transport-https tzdata git curl sudo vim && \ rm -rf /var/lib/apt/lists/* -RUN ln -fs /usr/share/zoneinfo/America/Los_Angeles /etc/localtime && dpkg-reconfigure -f noninteractive tzdata RUN groupadd -r moonfire-nvr && \ useradd moonfire-nvr --no-log-init -m -r -g moonfire-nvr && \ echo 'moonfire-nvr ALL=(ALL) NOPASSWD: ALL' >>/etc/sudoers diff --git a/guide/install-manual.md b/guide/install-manual.md index 1ca0d91..2550656 100644 --- a/guide/install-manual.md +++ b/guide/install-manual.md @@ -42,7 +42,8 @@ all non-Rust dependencies: libncursesw5-dev \ libsqlite3-dev \ libssl-dev \ - pkgconf + pkgconf \ + tzdata Next, you need Rust 1.27+ and Cargo. The easiest way to install them is by following the instructions at [rustup.rs](https://www.rustup.rs/). diff --git a/scripts/script-functions.sh b/scripts/script-functions.sh index 142b971..75aac15 100755 --- a/scripts/script-functions.sh +++ b/scripts/script-functions.sh @@ -311,20 +311,8 @@ prep_moonfire_user() sudo chown ${NVR_USER}:${NVR_GROUP} "${NVR_HOME}" } -# Correct possible timezone issues -# -fix_localtime() -{ - if [ ! -L /etc/localtime ] && [ -f /etc/timezone ] && - [ -f "/usr/share/zoneinfo/`cat /etc/timezone`" ]; then - echo_info -x "Correcting /etc/localtime setup issue..." - sudo ln -sf /usr/share/zoneinfo/`cat /etc/timezone` /etc/localtime - fi -} - pre_install_prep() { prep_moonfire_user setup_db - fix_localtime } diff --git a/src/cmds/run.rs b/src/cmds/run.rs index d9ca10d..a12436f 100644 --- a/src/cmds/run.rs +++ b/src/cmds/run.rs @@ -46,6 +46,7 @@ use web; // These are used in a hack to get the name of the current time zone (e.g. America/Los_Angeles). // They seem to be correct for Linux and macOS at least. const LOCALTIME_PATH: &'static str = "/etc/localtime"; +const TIMEZONE_PATH: &'static str = "/etc/timezone"; const ZONEINFO_PATHS: [&'static str; 2] = [ "/usr/share/zoneinfo/", // Linux, macOS < High Sierra "/var/db/timezone/zoneinfo/" // macOS High Sierra @@ -87,15 +88,71 @@ fn setup_shutdown() -> impl Future + Send { .map_err(|_| ()) } -fn resolve_zone() -> String { - let p = ::std::fs::read_link(LOCALTIME_PATH).expect("unable to read localtime symlink"); - let p = p.to_str().expect("localtime symlink destination must be valid UTF-8"); +fn trim_zoneinfo(p: &str) -> &str { for zp in &ZONEINFO_PATHS { if p.starts_with(zp) { - return p[zp.len()..].into(); + return &p[zp.len()..]; + } + } + return p; +} + +/// Attempt to resolve the timezone of the server. +/// The Javascript running in the browser needs this to match the server's timezone calculations. +fn resolve_zone() -> Result { + // If the environmental variable `TZ` exists, is valid UTF-8, and doesn't just reference + // `/etc/localtime/`, use that. + if let Ok(tz) = ::std::env::var("TZ") { + let mut p: &str = &tz; + + // Strip of an initial `:` if present. Having `TZ` set in this way is a trick to avoid + // repeated `tzset` calls: + // https://blog.packagecloud.io/eng/2017/02/21/set-environment-variable-save-thousands-of-system-calls/ + if p.starts_with(':') { + p = &p[1..]; + } + + p = trim_zoneinfo(p); + + if !p.starts_with('/') { + return Ok(p.to_owned()); + } + if p != LOCALTIME_PATH { + bail!("Unable to resolve env TZ={} to a timezone.", &tz); + } + } + + // If `LOCALTIME_PATH` is a symlink, use that. On some systems, it's instead a copy of the + // desired timezone, which unfortunately doesn't contain its own name. + match ::std::fs::read_link(LOCALTIME_PATH) { + Ok(localtime_dest) => { + let localtime_dest = match localtime_dest.to_str() { + Some(d) => d, + None => bail!("{} symlink destination is invalid UTF-8", LOCALTIME_PATH), + }; + let p = trim_zoneinfo(localtime_dest); + if p.starts_with('/') { + bail!("Unable to resolve {} symlink destination {} to a timezone.", + LOCALTIME_PATH, &localtime_dest); + } + return Ok(p.to_owned()); + }, + Err(e) => { + use ::std::io::ErrorKind; + if e.kind() != ErrorKind::NotFound && e.kind() != ErrorKind::InvalidInput { + bail!("Unable to read {} symlink: {}", LOCALTIME_PATH, e); + } + }, + }; + + // If `TIMEZONE_PATH` is a file, use its contents as the zone name. + match ::std::fs::read_to_string(TIMEZONE_PATH) { + Ok(z) => return Ok(z), + Err(e) => { + bail!("Unable to resolve timezone from TZ env, {}, or {}. Last error: {}", + LOCALTIME_PATH, TIMEZONE_PATH, e); } } - panic!("{} points to unexpected path {}", LOCALTIME_PATH, p) } struct Syncer { @@ -121,8 +178,9 @@ pub fn run() -> Result<(), Error> { } info!("Directories are opened."); - let s = web::Service::new(db.clone(), Some(&args.flag_ui_dir), args.flag_allow_origin, - resolve_zone())?; + let zone = resolve_zone()?; + info!("Resolved timezone: {}", &zone); + let s = web::Service::new(db.clone(), Some(&args.flag_ui_dir), args.flag_allow_origin, zone)?; // Start a streamer for each stream. let shutdown_streamers = Arc::new(AtomicBool::new(false));