imm-1.7.0.0: src/main/Main.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
-- {{{ Imports
import Imm
import Imm.Database as Database
import qualified Imm.HTTP as HTTP
import Imm.Pretty
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.Map as Map
import qualified Data.MessagePack as MsgPack
import qualified Data.Text as Text
import qualified Data.Text.IO as Text
import Dhall
import Relude.Unsafe (at)
import System.Exit
import System.IO (hFlush)
import System.Process.Typed
-- }}}
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
List -> Core.listFeeds logger database'
ShowFeed feedRef -> Core.showFeed logger database =<< toFeedID database' feedRef
OnFeedRef t c -> main2 logger database' optionCallbacksFile t c
Database.commit logger database'
flushLogs logger
main2 :: Logger.Handle IO -> Database.Handle IO FeedTable -> FilePath -> Maybe FeedRef -> CommandOnFeedRef -> IO ()
main2 logger database callbacksFile feedRef command = do
feedIDs <- resolveTarget database ByPassConfirmation feedRef
case command of
Unsubscribe -> Database.deleteList logger database feedIDs
Reset -> mapM_ (Database.markAsUnprocessed logger database) feedIDs
Run callbackMode -> do
callbacks <- case callbackMode of
EnableCallbacks -> input auto $ fromString callbacksFile
_ -> return []
main3 logger database callbacks feedIDs
main3 :: Logger.Handle IO -> Database.Handle IO FeedTable -> [Callback] -> [FeedID] -> IO ()
main3 logger database callbacks feedIDs = do
httpClient <- HTTP.mkHandle <$> defaultManager
targetFeedChan <- newTMChanIO
newItemsChan <- newTMChanIO
processedChan <- newTMChanIO
fetchErrorsChan <- newTMChanIO
callbackErrorsChan <- newTMChanIO
newItemsCount <- newTVarIO (0 :: Int)
errorsCount <- newTVarIO (0 :: Int)
-- => Feed IDs events
producer <- async $ forM_ feedIDs $ atomically . writeTMChan targetFeedChan
-- Feed IDs events => new item events
fetchers <- replicateM 5 $ async $ fix $ \recurse -> do
target <- atomically $ readTMChan targetFeedChan
forM_ target $ \feedID@(FeedID uri) -> do
catchAny
(do
feed <- HTTP.get logger httpClient uri >>= parseXml xmlParser uri
unreadElements <- filterM (fmap not . isRead database feedID) $ getElements feed
unprocessedElements <- listUnprocessedElements database feedID
forM_ (unprocessedElements <> unreadElements) $ \element -> do
log logger Info $ "New item:" <+> magenta (pretty feedID) <+> "/" <+> yellow (pretty $ getTitle element)
atomically $ do
writeTMChan newItemsChan (feedID, removeElements feed, element)
count <- readTVar newItemsCount
writeTVar newItemsCount (count + 1) )
(\e -> atomically $ do
writeTMChan fetchErrorsChan (feedID, e)
count <- readTVar errorsCount
writeTVar errorsCount (count + 1) )
recurse
-- New items events => execute callback => processed/error events
runners <- replicateM 5 $ async $ fix $ \recurse -> do
newItem <- atomically $ readTMChan newItemsChan
forM_ newItem $ \(feedID, feed, element) -> do
results <- forM callbacks $ \callback@(Callback executable arguments) -> do
let processInput = byteStringInput $ MsgPack.pack $ Message feed element
processConfig = proc executable (map toString arguments) & setStdin processInput
log logger Debug $ "Running" <+> cyan (pretty executable) <+> "on" <+> magenta (pretty feedID) <+> "/" <+> 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 (feedID, element)
e -> atomically $ do
writeTMChan callbackErrorsChan (feedID, element, e)
count <- readTVar errorsCount
writeTVar errorsCount (count + 1)
recurse
-- 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 targetFeedChan
mapM_ wait fetchers
atomically $ closeTMChan newItemsChan
mapM_ wait runners
atomically $ do
closeTMChan processedChan
closeTMChan fetchErrorsChan
closeTMChan callbackErrorsChan
wait storer
-- Error events => log
printErrors logger fetchErrorsChan callbackErrorsChan
flushLogs logger
readTVarIO newItemsCount <&> pretty <&> bold <&> (<+> "new items") >>= log logger Info
readTVarIO errorsCount <&> pretty <&> bold <&> (<+> "errors") >>= log logger Info
printErrors :: (MonadIO m, Exception e, Pretty a1, Pretty a2, Pretty a3, Pretty a4, ConvertUtf8 Text b1, ConvertUtf8 Text b2)
=> Logger.Handle m -> TMChan (a1, e) -> TMChan (a2, FeedElement, [(a3, a4, b1, b2)]) -> m ()
printErrors logger fetchErrorsChan callbackErrorsChan = do
fix $ \recurse -> do
items <- atomically $ readTMChan fetchErrorsChan
forM_ items $ \(feedID, e) -> do
log logger Error $ bold ("Fetch error for" <+> pretty feedID) <++> indent 2 (pretty $ displayException e)
recurse
fix $ \recurse -> do
items <- atomically $ readTMChan callbackErrorsChan
forM_ items $ \(feedID, element, errors) -> do
let prettyErrors = vsep $ do
(callback, i, stdout', stderr') <- errors
return $ "When running:" <+> pretty callback
<++> "Exit code:" <+> pretty i
<++> "Stdout:" <++> indent 2 (pretty $ decodeUtf8 @Text stdout')
<++> "Stderr:" <++> indent 2 (pretty $ decodeUtf8 @Text stderr')
log logger Error $ bold ("Callback error for" <+> pretty feedID <+> "/" <+> pretty (getTitle element)) <++> indent 2 prettyErrors
recurse
xmlParser :: XML.Handle IO
xmlParser = XML.mkHandle defaultXmlParser
-- * Util
data SafeGuard = AskConfirmation | ByPassConfirmation
deriving(Eq, Read, Show)
data InterruptedException = InterruptedException deriving(Eq, Read, Show)
instance Exception InterruptedException where
displayException _ = "Process interrupted"
promptConfirm :: Text -> IO ()
promptConfirm s = do
Text.putStr $ s <> " Confirm [Y/n] "
hFlush stdout
x <- Text.getLine
unless (Text.null x || x == "Y") $ throwM InterruptedException
resolveTarget :: MonadIO m => MonadThrow m => Database.Handle m FeedTable -> SafeGuard -> Maybe FeedRef -> m [FeedID]
resolveTarget database s Nothing = do
result <- Map.keys <$> Database.fetchAll database
when (s == AskConfirmation) $ liftIO $ promptConfirm $ "This will affect " <> show (length result) <> " feeds."
return result
resolveTarget database _ (Just feedRef) = do
result <- toFeedID database feedRef
return [result]
toFeedID :: Monad m => Database.Handle m FeedTable -> FeedRef -> m FeedID
toFeedID database (ByUID i) = fst . at (i-1) . Map.toList <$> Database.fetchAll database
toFeedID _ (ByURI uri) = return $ FeedID uri