packages feed

imm-1.9.0.0: src/main/Main.hs

{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings     #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE RecordWildCards       #-}
{-# LANGUAGE TypeApplications      #-}
-- {{{ Imports
import           Alternate
import qualified Core
import           Database
import           HTTP
import           Logger
import           Options
import           XML

import           Control.Concurrent.Async
import           Control.Concurrent.STM.TMChan
import           Control.Exception.Safe
import           Data.Conduit.Combinators      as Conduit (stdin)
import qualified Data.MessagePack              as MsgPack
import           Dhall
import           Imm
import           Imm.Database.Feed             as Database
import qualified Imm.HTTP                      as HTTP
import           Imm.Pretty
import           Pipes.ByteString
import           System.Exit
import           System.Process.Typed
import           URI.ByteString
-- }}}


main :: IO ()
main = do
  AllOptions{..} <- parseOptions
  let GlobalOptions{..} = optionGlobal

  -- Setup logger
  logger <- Logger.mkHandle <$> defaultLogger
  setColorizeLogs logger optionColorizeLogs
  setLogLevel logger optionLogLevel
  log logger Debug $ "Options: " <> pretty optionCommand

  handleAny (log logger Error . pretty . displayException) $ do
    -- Setup database
    database <- Database.mkHandle <$> defaultDatabase
    let database' = if optionReadOnlyDatabase then readOnly logger database else database
    log logger Debug . ("Using database:" <++>) . indent 2 =<< _describeDatabase database'

    case optionCommand of
      Import            -> Core.importOPML logger database' Conduit.stdin
      Subscribe u c     -> Core.subscribe logger database' u c
      Unsubscribe query -> Core.unsubscribe logger database' query
      List              -> Core.listFeeds logger database'
      Describe query    -> Core.describeFeed logger database query
      Reset feedKeys    -> Core.markAsUnprocessed logger database' feedKeys
      Run f c           -> main2 logger database' f =<< resolveCallbacks c optionCallbacksFile

    Database.commit logger database'

  flushLogs logger


resolveCallbacks :: MonadIO m => CallbackMode -> FilePath -> m [Callback]
resolveCallbacks EnableCallbacks callbacksFile = io $ input auto $ fromString callbacksFile
resolveCallbacks _ _                           = return mempty


main2 :: Logger.Handle IO -> Database.Handle IO -> FeedQuery -> [Callback] -> IO ()
main2 logger database feedQuery callbacks = do
  let httpClient = HTTP.mkHandle logger

  feedLocations <- mapM (Database.resolveFeedLocation database)
    =<< Database.resolveEntryKey database feedQuery

  feedLocationsChan <- newTMChanIO
  targetFeedChan <- newTMChanIO
  newItemsChan <- newTMChanIO
  processedChan <- newTMChanIO

  resolveErrorsChan <- newTMChanIO
  fetchErrorsChan <- newTMChanIO
  runErrorsChan <- newTMChanIO
  callbackErrorsChan <- newTMChanIO

  newItemsCount <- newTVarIO (0 :: Int)
  errorsCount <- newTVarIO (0 :: Int)

  -- => Feed IDs events
  producer <- async $ forM_ feedLocations $ atomically . writeTMChan feedLocationsChan

  -- Feed locations => feed direct URIs
  let resolverF feedLocation = do
        uri <- resolveFeedURI logger httpClient feedLocation
        atomically $ writeTMChan targetFeedChan (feedLocation, uri)

  resolvers <- spawnConsumers logger 5 feedLocationsChan resolverF resolveErrorsChan errorsCount

  -- Feed direct URIs events => new item events
  let fetcherF (feedLocation, uri) = do
        feed <- HTTP.withGet logger httpClient uri
          $ toLazyM >=> parseXml xmlParser uri
        let entryKey = ByLocation feedLocation

        unreadElements <- filterM (fmap not . isRead database entryKey) $ getElements feed
        unprocessedElements <- listUnprocessedElements database entryKey
        forM_ (unprocessedElements <> unreadElements) $ \element -> do
          log logger Info $ "New item:" <+> magenta (pretty entryKey) <+> "/" <+> yellow (pretty $ getTitle element)
          atomically $ do
            writeTMChan newItemsChan (entryKey, removeElements feed, element)
            modifyTVar' newItemsCount (+ 1)

  fetchers <- spawnConsumers logger 5 targetFeedChan fetcherF fetchErrorsChan errorsCount

  -- New items events => execute callback => processed/error events
  let runnerF (entryKey, feed, element) = do
        results <- forM callbacks $ \callback@(Callback executable arguments) -> do
          let processInput = byteStringInput $ MsgPack.pack $ Message feed element
              processConfig = proc executable (toString <$> arguments) & setStdin processInput

          log logger Debug $ "Running" <+> cyan (pretty executable) <+> "on" <+> magenta (pretty entryKey) <+> "/" <+> yellow (pretty $ getTitle element)

          (exitCode, output, errors) <- readProcess processConfig
          case exitCode of
            ExitSuccess   -> return $ Right callback
            ExitFailure i -> return $ Left (callback, i, output, errors)

        case lefts results of
          [] -> atomically $ writeTMChan processedChan (entryKey, element)
          e  -> atomically $ do
            writeTMChan callbackErrorsChan ((entryKey, element), e)
            modifyTVar' errorsCount (+ 1)

  runners <- spawnConsumers logger 5 newItemsChan runnerF runErrorsChan errorsCount

  -- Processed events => update database
  storer <- async $ fix $ \recurse -> do
    item <- atomically $ readTMChan processedChan
    forM_ item $ \(feedID, element) -> do
      log logger Debug $ "Updating database for" <+> pretty feedID <+> "/" <+> pretty (getTitle element)
      Database.markAsProcessed logger database feedID element
      recurse

  -- Wait for all async processes to complete
  wait producer
  atomically $ closeTMChan feedLocationsChan

  mapM_ wait resolvers
  atomically $ do
    closeTMChan targetFeedChan
    closeTMChan resolveErrorsChan

  mapM_ wait fetchers
  atomically $ closeTMChan newItemsChan

  mapM_ wait runners
  atomically $ do
    closeTMChan processedChan
    closeTMChan fetchErrorsChan
    closeTMChan callbackErrorsChan
    closeTMChan runErrorsChan

  wait storer

  -- Error events => log
  handleErrors resolveErrorsChan (printResolveError logger)
  handleErrors fetchErrorsChan (printFetchError logger)
  handleErrors callbackErrorsChan (printCallbackError logger)
  handleErrors runErrorsChan (printRunError logger)
  flushLogs logger

  readTVarIO newItemsCount <&> pretty <&> bold <&> (<+> "new items") >>= log logger Info
  readTVarIO errorsCount <&> pretty <&> bold <&> (<+> "errors") >>= log logger Info


spawnConsumers :: Logger.Handle IO -> Int -> TMChan i -> (i -> IO ()) -> TMChan (i, SomeException) -> TVar Int -> IO [Async ()]
spawnConsumers logger n inputChan f errorsChan errorsCount =
  replicateM n $ async $ fix $ \recurse -> do
    maybeItem <- atomically $ readTMChan inputChan
    forM_ maybeItem $ \item -> do
      catchAny (f item) $ \e -> do
        log logger Debug $ "Error:" <+> pretty (displayException e)
        atomically $ do
          writeTMChan errorsChan (item, e)
          modifyTVar' errorsCount (+ 1)
      recurse

handleErrors :: MonadIO m => TMChan (input, error) -> ((input, error) -> m ()) -> m ()
handleErrors errorsChan handler = fix $ \recurse -> do
  items <- atomically $ readTMChan errorsChan
  forM_ items $ \(i, e) -> handler (i, e) >> recurse

printResolveError :: Exception e => Logger.Handle m -> (FeedLocation, e) -> m ()
printResolveError logger (feedLocation, e) = log logger Error $
  bold ("Resolve error for" <+> pretty feedLocation)
  <++> indent 2 (pretty $ displayException e)

printFetchError :: Exception e => Logger.Handle m -> ((FeedLocation, URIRef a), e) -> m ()
printFetchError logger ((_, uri), e) = log logger Error $
  bold ("Fetch error for" <+> prettyURI uri)
  <++> indent 2 (pretty $ displayException e)

printCallbackError :: Logger.Handle m -> ((EntryKey, FeedElement), _) -> m ()
printCallbackError logger ((entryKey, element), e) = log logger Error $
  bold ("Callback error for" <+> pretty entryKey <+> "/" <+> pretty (getTitle element))
  <++> indent 2 prettyErrors
  where prettyErrors = vsep $ do
          (callback, i, stdout', stderr') <- e
          return $ "When running:" <+> pretty callback
            <++> "Exit code:" <+> pretty i
            <++> "Stdout:" <++> indent 2 (pretty $ decodeUtf8 @Text stdout')
            <++> "Stderr:" <++> indent 2 (pretty $ decodeUtf8 @Text stderr')

printRunError :: Exception e => Logger.Handle m -> ((EntryKey, Feed, FeedElement), e) -> m ()
printRunError logger ((entryKey, _feed, element), e) = log logger Error $
  bold ("Error for" <+> pretty entryKey <+> "/" <+> pretty (getTitle element))
  <++> indent 2 (pretty $ displayException e)


xmlParser :: XML.Handle IO
xmlParser = XML.mkHandle defaultXmlParser