packages feed

tai (empty) → 0

raw patch · 7 files changed

+480/−0 lines, 7 filesdep +basedep +clockdep +lenssetup-changed

Dependencies added: base, clock, lens, mtl, parsers, time, trifecta, wreq

Files

+ ChangeLog.md view
@@ -0,0 +1,5 @@+# Revision history for tai++## 0  -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2017, davean, tolt++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of davean, tolt nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Data/Time/Clock/TAI/LeapData.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Data.Time.Clock.TAI.LeapData (+   SourceType(..), LeapSource(..), LeapSources+ , parseLeapSource, mayRetrieveList, parseFileMaybe+ , leapSources+ , sourceLeapData+ ) where++import qualified Control.Exception as E+import Control.Lens+import Data.List (stripPrefix, sort)+import Data.Maybe+import Data.Text.Lazy.Lens+import Data.Time+import Data.Time.Clock.TAI.Parser+import qualified Network.Wreq as Wreq++-- | A support type for deperating out the protocol over which to retrieve data.+data SourceType = File | HTTPS+  deriving (Show, Read, Eq, Bounded, Enum, Ord)++-- | A leap second list location tagged with a protocol for retrieving it.+data LeapSource+  = LeapSource+    { sourceType :: SourceType+    , source :: String+    }+  deriving (Show, Read, Eq, Ord)++-- | Convert a string source to a LeapSource +parseLeapSource :: String -> Maybe LeapSource+parseLeapSource s' | Just s <- stripPrefix "file://" s'  = Just $ LeapSource File s+parseLeapSource s | Just _ <- stripPrefix "https://" s = Just $ LeapSource HTTPS s+parseLeapSource _ = Nothing++-- | LeapSources is a list of strings, specifying URIs the program can+--   potentially access up-to-date leap-second.lists from.+--   Each source begins with a protocol:+--     For local sources "file://" followed by a path to the local file.+--     For remote source sources "https://" prefixed URIs.+type LeapSources = [LeapSource]++-- | A few places we should be able to download up-to-date leap-seconds.list data.+leapSources :: LeapSources+leapSources = sort . mapMaybe parseLeapSource $+  [ "file:///usr/share/zoneinfo/leap-seconds.list"+  , "https://hpiers.obspm.fr/iers/bul/bulc/ntp/leap-seconds.list"+  , "https://www.ietf.org/timezones/data/leap-seconds.list"+  , "https://www.meinberg.de/download/ntp/leap-seconds.list"+  ]++-- | Given a LeapSource, perform the actual aquasition and parsing+mayRetrieveList :: LeapSource -> IO (Maybe LeapSecondList)+mayRetrieveList (LeapSource ty s) = E.handle (\(_::E.SomeException) -> return Nothing) $ do+  curDay <- utctDay <$> getCurrentTime+  case ty of+    File -> parseFileMaybe curDay <$> readFile s+    HTTPS -> do+      r <- Wreq.get s+      return . parseFileMaybe curDay $ (r ^. Wreq.responseBody.utf8.unpacked)++-- | Parse a leap second list file, failing if it is expired.+parseFileMaybe :: Day -> String -> Maybe LeapSecondList+parseFileMaybe curDay str =+  -- We get the day to know if the file is valid, but its not entire clear how to compare \+  -- our UTC day to the day specified in the file.+  case parseLeapSecondList str of+    Left _ -> Nothing+    Right lsl | curDay >= expirationDate lsl -> Nothing+              | otherwise -> Just lsl++-- | Aquire leap-seconds files and parse them,+--   Trying the provided list of sources in order from local, to secure remote (HTTPS),+--   until an up-to-date source is aquired or all options are exausted.+--+--   Currently only local and HTTPS has been implimented.+sourceLeapData :: LeapSources -> IO LeapSecondList+sourceLeapData = do+    firstOption . map mayRetrieveList . sort+  where+    firstOption :: Monad m => [m (Maybe a)] -> m a+    firstOption (io:t) = maybe (firstOption t) return =<< io+    firstOption [] = error "No option for leap-seconds.list serviced our needs!"
+ src/Data/Time/Clock/TAI/Parser.hs view
@@ -0,0 +1,143 @@+module Data.Time.Clock.TAI.Parser (+   parseLeapSecondList+ , LeapSecondList(..)+ ) where++import Control.Applicative+import Control.Monad+import Data.Int+import Data.Maybe++import Text.Parser.Char+import Text.Parser.Combinators+import Text.Parser.Token+import Text.Trifecta.Delta+import Text.Trifecta.Parser+import Text.Trifecta.Result++import Data.Time++-- | Possible entries for the TAI leap-seconds file+data TAIEntry =+    TAILastUpdate Day+  | TAIExpiration Day+  | TAILeapSecond Day Int32+  | TAIHash String+  deriving (Eq, Ord, Show)++data LeapSecondList = LeapSecondList {+  expirationDate :: Day+, lastUpdate :: Day+, expectedHash :: String+, leapSeconds :: [(Day, Int32)]+} deriving (Eq, Ord, Show)++-- | Parse a leap second list+parseLeapSecondList :: String -> Either String LeapSecondList+parseLeapSecondList contents =+  case parseString parseTAIEntries (Columns 0 0) contents of+    (Failure parseError) -> Left $ show parseError+    (Success entries) -> buildLeapSecondList entries++-- | Build a leap second list from a list of TAIEntry. +-- should add in checking of the hash+buildLeapSecondList :: [TAIEntry] -> Either String LeapSecondList+buildLeapSecondList list =+  LeapSecondList <$> expiration <*> updated <*> hash <*> leaps+  where+    expiration =+      single expirations "Too many expiration definitions\n"+    updated =+      single updates "Too many last update definitions\n"+    hash =+      single hashes "Too many hash definitions\n"+    leaps =+      return $ catMaybes $ getLeapSecond <$> list+    expirations =+      catMaybes $ getExpiration <$> list+    updates =+      catMaybes $ getLastUpdate <$> list+    hashes =+      catMaybes $ getHash <$> list+    single (x:[]) _ = Right x+    single _ e = Left e++getExpiration :: TAIEntry -> Maybe Day+getExpiration (TAIExpiration d) = Just d+getExpiration _ = Nothing++getLastUpdate :: TAIEntry -> Maybe Day+getLastUpdate (TAILastUpdate d) = Just d+getLastUpdate _ = Nothing++getLeapSecond :: TAIEntry -> Maybe (Day, Int32)+getLeapSecond (TAILeapSecond day dtai) = Just (day, dtai)+getLeapSecond _ = Nothing++getHash :: TAIEntry -> Maybe String+getHash (TAIHash h) = Just h+getHash _ = Nothing++eol :: Parser ()+eol = void $ char '\n'++parseTAIEntries :: Parser [TAIEntry]+parseTAIEntries =+  catMaybes <$> manyTill taiEntry eof+  where+    taiEntry =+          (Just <$> taiParser)+      <|> (const Nothing <$> parseComment)+    taiParser =+          (parseLeapSecond <?> "Leap second")+      <|> (parseLastUpdate <?> "Last update")+      <|> (parseExpiration <?> "Expiration")+      <|> (parseTAIHash <?> "Hash")++consumeLine :: Parser ()+consumeLine = do+  _<- spaces+  (parseComment <|> eol)++parseComment :: Parser ()+parseComment = void $ do+  _<- char '#'+  _<- manyTill anyChar eol+  return ()++parseLastUpdate :: Parser TAIEntry+parseLastUpdate = do+  _<- string "#$"+  _<- spaces+  lastUpdateNtp <- natural <?> "Last Update"+  _<- spaces+  return . TAILastUpdate $ ntpToDay lastUpdateNtp++parseExpiration :: Parser TAIEntry+parseExpiration = do+  _<- string "#@"+  _<- spaces+  expiration <- natural <?> "Expiration date"+  _<- spaces+  return . TAIExpiration $ ntpToDay expiration++parseLeapSecond :: Parser TAIEntry+parseLeapSecond = do+  time <- natural <?> "Leap second time"+  _<- spaces+  dtai <- natural <?> "dtai"+  _<- consumeLine+  return $ TAILeapSecond (ntpToDay time) $ fromIntegral dtai++parseTAIHash :: Parser TAIEntry+parseTAIHash = do+  _<- string "#h"+  _<- spaces+  hash <- manyTill anyChar eol <?> "Hash"+  return $ TAIHash hash++ntpToDay :: Integer -> Day+ntpToDay n = addDays (div n 86400) ntpEpochDay++ntpEpochDay :: Day+ntpEpochDay = ModifiedJulianDay 15020
+ src/Data/Time/Clock/TAI/Support.hs view
@@ -0,0 +1,182 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE DeriveAnyClass #-}+module Data.Time.Clock.TAI.Support (+   TAISync, UpdatePolicy+ , initSync+ , getTAI, absGuessUtc, utcGuessAbs+ , currentLeapMap+ , periodicBackgroundDownload+ ) where++import qualified Control.Exception as E+import Control.Monad+import Control.Monad.Trans+import Data.Int+import Data.IORef+import Data.Maybe+import Data.Time+import Data.Time.Clock.TAI+import Data.Time.Clock.TAI.LeapData+import Data.Time.Clock.TAI.Parser+import qualified GHC.Event as TM+import qualified System.Clock as Clock+import System.Mem.Weak++-- | Our data about TAI in relation to UTC and this system.+data TAISync =+    TAISync+    { _tsSystemBootEpoch :: AbsoluteTime+      -- ^ The TAI of CLOCK_BOOTTIME's 0 time.+      --   This allows us to keep giving TAI time as accurately as the system's clock+      --   even in the face of failure to update our leap second table in the future.+    , _lslRef :: IORef LeapSecondList+      -- ^ Our TAI/UTC offset information, maintained in the background to the best+      --   of our UpdatePolicy's ability.+    }++-- | A function that enacts the periodic update of the LeapSecondList leap second data.+--   Usually periodicBackgroundDownload will satisfy a user's needs, but some enviroments+--   may want another policy.+type UpdatePolicy = IO (IORef LeapSecondList)++data TimeSyncException =+    TimeSyncTooLongException+  deriving (Show, E.Exception)++-- | Given an UpdatePolicy, generate a TAISync to be used to interact with TAI+--   though the other library functions.+initSync :: MonadIO m => UpdatePolicy -> m TAISync+initSync uppolicy = liftIO $ do+    -- We don't try to be too precise,+    -- if we wanted higher accuracy we'd have to bounce back and forth a few times+    -- Instead we just assume its in the middle of the two times,+    -- and error if the seperation is large.+    sinceBoot <- Clock.getTime Clock.Boottime+    now <- getCurrentTime+    sinceBoot' <- Clock.getTime Clock.Boottime+    -- We get the leap second list after we get our times just to be extra sure+    -- that we have to be able to convert the times.+    lslRef <- liftIO uppolicy+    lsl <- readIORef lslRef+    let nowTAI = utcGuessAbs' lsl now+    let d1 = timeSpec2DiffTime sinceBoot+    let d2 = timeSpec2DiffTime sinceBoot'+    let diffSinceBootMiddle = (d1 + d2) / 2+    -- We don't want too low an accuracy+    unless (d2-d1 < 10^^(-4::Int)) . E.throw $ TimeSyncTooLongException+    return $ TAISync ((negate diffSinceBootMiddle) `addAbsoluteTime` nowTAI) lslRef++-- | Update the leap second table by redownloading the tables periodicly in the background.+--   This policy uses the TimeoutManager to check for a new table every dbetween days,+--   trying again dretry if there is a failure. Recomended values are 30 and 1 for these.+--   Given the validity period for leap second data, this should generally suffice.+--   There can be problems with local files being near their expiration though.+--+--   This policy requires a threaded runtime.+periodicBackgroundDownload :: LeapSources -> Int -> Int -> UpdatePolicy+periodicBackgroundDownload ls dbetween dretry = do+    -- We use a week reference to the leap second list data for simplicity of background+    -- task termination. This is not prompt, but as the background task doesn't directly+    -- hold data other then a weak reference (including not having a stack), or perform+    -- actions, promptness seemed unworthy of the overhead.+    lsl <- sourceLeapData ls+    lslRef <- newIORef lsl+    lslWRef <- mkWeakIORef lslRef (return ())+    regTimed dbetween lslWRef+    return lslRef+  where+    -- Register the leapsecond update handler to be run in a given number of days (approximate)+    regTimed :: Int -> Weak (IORef LeapSecondList) -> IO ()+    regTimed d wr = do+      tm <- TM.getSystemTimerManager+      -- Update a little less then monthly+      void $ TM.registerTimeout tm (d*24*60*60*1000000) (timedUpdater wr)+    timedUpdater :: Weak (IORef LeapSecondList) -> IO ()+    timedUpdater wr = do+      mr <- deRefWeak wr+      case mr of+        -- If the weak ref doesn't deref our need for existance is over.+        Nothing -> return ()+        -- Try to update the leapsecond data, trying again sooner if we fail.+        Just r -> E.handle (\(_::E.SomeException) -> regTimed dretry wr) $ do+          lsl <- sourceLeapData ls+          atomicModifyIORef' r (const (lsl, ()))+          regTimed dbetween wr++timeSpec2DiffTime :: Clock.TimeSpec -> DiffTime+timeSpec2DiffTime ct =+  picosecondsToDiffTime (10^(12::Int) * (fromIntegral $ Clock.sec ct)+                        + 1000 *  (fromIntegral $ Clock.nsec ct))++-- | Get the current TAI time.+getTAI :: MonadIO m => TAISync -> m AbsoluteTime+getTAI (TAISync btb _) =+  liftIO $ ((`addAbsoluteTime` btb) . timeSpec2DiffTime) <$> Clock.getTime Clock.Boottime++lookupDayInList :: LeapSecondList -> Day -> Maybe Int32+lookupDayInList list day+  | day >= expirationDate list = Nothing+  | otherwise = foldl go Nothing $ leapSeconds list+    where+      go Nothing (dayOfLeapSecond, dtai)+        | day >= dayOfLeapSecond = Just dtai+      go (Just dtai) (dayOfLeapSecond, dtai')+        | day >= dayOfLeapSecond = Just $ max dtai dtai'+      go x _ = x++handlingOutOfRange :: LeapSecondList -> Day -> Integer+handlingOutOfRange lsl day = fromIntegral $+  let (maxMapDay, maxDayLeaps) = maximum . leapSeconds $ lsl+      (minMapDay, minDayLeaps) = minimum . leapSeconds $ lsl+   in case (day < minMapDay, day > maxMapDay) of+        (True, False) -> minDayLeaps+        (False, True) -> maxDayLeaps+        (False, False) -> fromJust $ lookupDayInList lsl day+        _ -> error "Day both larger then max and smaller then min!"++-- | Given our information about leap seconds, generate a UTC time from a TAI time+--   as a total function. As the relation between TAI is only known for a specific+--   time range, we give a best-guess outside said time range.+--   Specificly we only know the offset after some point in the past, and+--   up to about 6 months into the future. Outside this range we assume the last+--   known mapping between UTC and TAI doesn't drift.+absGuessUtc :: MonadIO m => TAISync -> AbsoluteTime -> m UTCTime+absGuessUtc (TAISync _ lr) at = liftIO $ (`absGuessUtc'` at) <$> readIORef lr++absGuessUtc' :: LeapSecondList -> AbsoluteTime -> UTCTime++-- | Given our information about leap seconds, generate a TAI time rom a UTC time+--   as a total function. As the relation between TAI is only known for a specific+--   time range, we give a best-guess outside said time range.+--   Specificly we only know the offset after some point in the past, and+--   up to about 6 months into the future. Outside this range we assume the last+--   known mapping between UTC and TAI doesn't drift.+utcGuessAbs :: MonadIO m => TAISync -> UTCTime -> m AbsoluteTime+utcGuessAbs (TAISync _ lr) ut = liftIO $ (`utcGuessAbs'` ut) <$> readIORef lr++utcGuessAbs' :: LeapSecondList -> UTCTime -> AbsoluteTime++#if MIN_VERSION_time(1,7,0)+-- | Gets the current leap second data in a 'time' compatable form.+currentLeapMap :: MonadIO m => TAISync -> m LeapSecondMap+currentLeapMap = fmap leapListToMap . liftIO . readIORef . _lslRef++leapListToMap :: LeapSecondList -> LeapSecondMap+leapListToMap lsl day = fmap fromIntegral . lookup day . leapSeconds $ lsl++absGuessUtc' lsl = fromJust . taiToUTCTime (Just . fromIntegral . handlingOutOfRange lsl)++utcGuessAbs' lsl =  fromJust . utcToTAITime (Just . fromIntegral . handlingOutOfRange lsl)+#else+-- | Gets the current leap second data in a 'time' compatable form.+currentLeapMap :: MonadIO m => TAISync -> m LeapSecondTable+currentLeapMap = fmap leapListToMap . liftIO . readIORef . _lslRef++leapListToMap :: LeapSecondList -> LeapSecondTable+leapListToMap lsl day = fromIntegral . fromJust . lookup day . leapSeconds $ lsl++absGuessUtc' lsl = taiToUTCTime (handlingOutOfRange lsl)++utcGuessAbs' lsl = utcToTAITime (handlingOutOfRange lsl)+#endif
+ tai.cabal view
@@ -0,0 +1,35 @@+name:                tai+version:             0+synopsis:            Support library to enable TAI usage on systems with time kept in UTC.+description:         This library manages leap second data to allow using TAI time inspite of the system clock being kept in UTC.+homepage:            https://oss.xkcd.com/+license:             BSD3+license-file:        LICENSE+author:              davean, tolt+maintainer:          oss@xkcd.com+copyright:           davean & tolt 2017+category:            Time, System+build-type:          Simple+extra-source-files:  ChangeLog.md+cabal-version:       >=1.10++source-repository head+  type: git+  location: https://code.xkrd.net/haskell/tai.git++library+  hs-source-dirs:      src+  default-language:    Haskell2010+  exposed-modules:+        Data.Time.Clock.TAI.Support+        Data.Time.Clock.TAI.Parser+        Data.Time.Clock.TAI.LeapData+  build-depends:+        base >=4.9 && <4.11+      , time >= 1.6.0.1 && < 1.9+      , mtl >= 2.2 && < 2.3+      , trifecta >= 1.6 && < 1.7+      , parsers >= 0.12 && < 0.13+      , clock >= 0.7 && < 0.8+      , wreq >= 0.5 && < 0.6+      , lens >= 4.5 && < 4.16