{-# LANGUAGE CPP #-} -- SPDX-License-Identifier: BSD-3-Clause module Tasks ( TaskFilter(..), TaskReq(..), BeforeAfter(..), tasksCmd, parseTaskState, kojiMethods, fedoraKojiHub ) where import Control.Monad.Extra import qualified Data.ByteString.Lazy.UTF8 as U import Data.Char (isDigit, toUpper) import Data.List.Extra import Data.Maybe #if !MIN_VERSION_base(4,11,0) import Data.Monoid ((<>)) #endif import Data.RPM.NVR import qualified Data.Text as T import qualified Data.Text.IO as T import Data.Text.Format.Numbers import Data.Time.Clock import Data.Time.Format import Data.Time.LocalTime import Distribution.Koji import Distribution.Koji.API import Network.HTTP.Directory import Network.HTTP.Simple import SimpleCmd import System.FilePath import Text.Pretty.Simple import Common import User data TaskReq = Task Int | Parent Int | Build String | Package String | TaskQuery | Pattern String data TaskFilter = TaskPackage String | TaskNVR String data BeforeAfter = Before String | After String instance Show BeforeAfter where show (Before _) = "before" show (After _) = "after" getTimedate :: BeforeAfter -> String getTimedate (Before s) = s getTimedate (After s) = s capitalize :: String -> String capitalize "" = "" capitalize (h:t) = toUpper h : t -- FIXME short output summary tasksCmd :: Maybe String -> Maybe UserOpt -> Int -> [TaskState] -> [String] -> Maybe BeforeAfter -> Maybe String -> Bool -> Bool -> Maybe TaskFilter -> Bool -> TaskReq -> IO () tasksCmd mhub museropt limit states archs mdate mmethod details debug mfilter' tail' taskreq = do let server = maybe fedoraKojiHub hubURL mhub when (server /= fedoraKojiHub && museropt == Just UserSelf) $ error' "--mine currently only works with Fedora Koji" tz <- getCurrentTimeZone case taskreq of Task taskid -> do when (isJust museropt || isJust mdate || isJust mfilter') $ error' "cannot use --task together with --user, timedate, or filter" mtask <- kojiGetTaskInfo server (TaskId taskid) whenJust mtask$ \task -> do when debug $ pPrintCompact task whenJust (maybeTaskResult task) $ printTask True tz Build bld -> do when (isJust mdate || isJust mfilter') $ error' "cannot use --build together with timedate or filter" mtaskid <- if all isDigit bld then ((fmap TaskId . lookupStruct "task_id") =<<) <$> getBuild server (InfoID (read bld)) else kojiGetBuildTaskID server bld whenJust mtaskid $ \(TaskId taskid) -> tasksCmd (Just server) museropt limit states archs mdate mmethod details debug mfilter' tail' (Parent taskid) Package pkg -> do when (head pkg == '-') $ error' $ "bad combination: not a package " ++ pkg when (isJust mdate || isJust mfilter') $ error' "cannot use --package together with timedate or filter" mpkgid <- getPackageID server pkg case mpkgid of Nothing -> error' $ "no package id found for " ++ pkg Just pkgid -> do builds <- listBuilds server $ ("packageID", ValueInt pkgid):buildQueryOpts forM_ builds $ \bld -> do let mtaskid = (fmap TaskId . lookupStruct "task_id") bld whenJust mtaskid $ \(TaskId taskid) -> tasksCmd (Just server) museropt 10 states archs mdate mmethod details debug mfilter' tail' (Parent taskid) Pattern pat -> do let buildquery = ("pattern", ValueString pat):buildQueryOpts when debug $ print buildquery builds <- listBuilds server buildquery when debug $ print builds forM_ builds $ \bld -> do let mtaskid = (fmap TaskId . lookupStruct "task_id") bld whenJust mtaskid $ \(TaskId taskid) -> tasksCmd (Just server) museropt 10 states archs mdate mmethod details debug mfilter' tail' (Parent taskid) _ -> do query <- setupQuery server let queryopts = [("limit",ValueInt limit), ("order", ValueString "-id")] when debug $ print $ query ++ queryopts tasks <- listTasks server query queryopts when debug $ mapM_ pPrintCompact tasks let detailed = details || length tasks == 1 (mapM_ (printTask detailed tz) . filterResults . mapMaybe maybeTaskResult) tasks where buildQueryOpts = [("queryOpts",ValueStruct [("limit",ValueInt limit), ("order",ValueString "-build_id")])] setupQuery server = do case taskreq of Parent parent -> return $ ("parent", ValueInt parent) : commonParams _ -> do mdatestring <- case mdate of Nothing -> return Nothing Just date -> Just <$> cmd "date" ["+%F %T%z", "--date=" ++ dateString date] when (isNothing mmethod) $ warning $ "buildArch tasks" ++ if details || limit == 1 then "" else "\n" whenJust mdatestring $ \date -> warning $ maybe "" show mdate +-+ date mowner <- maybeGetKojiUser server museropt return $ [("owner", ValueInt (getID owner)) | Just owner <- [mowner]] ++ [("complete" ++ (capitalize . show) date, ValueString datestring) | Just date <- [mdate], Just datestring <- [mdatestring]] ++ commonParams where commonParams = [("decode", ValueBool True)] ++ [("state", ValueArray (map taskStateToValue states)) | notNull states] ++ [("arch", ValueArray (map (ValueString . kojiArch) archs)) | notNull archs] ++ [("method", ValueString method) | let method = fromMaybe "buildArch" mmethod] kojiArch :: String -> String kojiArch "i686" = "i386" kojiArch "armv7hl" = "armhfp" kojiArch a = a dateString :: BeforeAfter -> String -- make time refer to past not future dateString beforeAfter = let timedate = getTimedate beforeAfter in case words timedate of [t] | t `elem` ["hour", "day", "week", "month", "year"] -> "last " ++ t [t] | t `elem` ["today", "yesterday"] -> t ++ " 00:00" [t] | any (lower t `isPrefixOf`) ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] -> "last " ++ t ++ " 00:00" [n,_unit] | all isDigit n -> timedate ++ " ago" _ -> timedate maybeTaskResult :: Struct -> Maybe TaskResult maybeTaskResult st = do arch <- lookupStruct "arch" st let mstart_time = readTime' <$> lookupStruct "start_ts" st mend_time = readTime' <$> lookupStruct "completion_ts" st taskid <- lookupStruct "id" st method <- lookupStruct "method" st state <- getTaskState st request <- lookupStruct "request" st let package = case (getString . head) request of Nothing -> Left $ unwords $ map showValue $ take 2 request Just req -> let file = takeFileName req in if ".src.rpm" `isSuffixOf` file then Right $ readNVR $ removeSuffix ".src.rpm" file else Left $ takeBaseName file mparent' = lookupStruct "parent" st :: Maybe Int return $ TaskResult package arch method state mparent' taskid mstart_time mend_time where showValue :: Value -> String showValue (ValueString cs) = cs showValue (ValueInt i) = show i showValue val = show val filterResults :: [TaskResult] -> [TaskResult] filterResults ts = case mfilter' of Nothing -> ts Just (TaskPackage pkg) -> filter (isPackage pkg . taskPackage) ts Just (TaskNVR nvr) -> filter (isNVR nvr . taskPackage) ts where isPackage pkg (Left p) = takeBaseName p == pkg isPackage pkg (Right (NVR n _)) = n == pkg isNVR _ (Left _) = False isNVR nvr (Right nvr') = nvr `isPrefixOf` showNVR nvr' printTask :: Bool -> TimeZone -> TaskResult -> IO () printTask detailed tz task = do let mendtime = mtaskEndTime task mtime <- if isNothing mendtime then Just <$> getCurrentTime else return Nothing if detailed then do putStrLn "" (mapM_ putStrLn . formatTaskResult mtime tz) task buildlogSize tail' (taskId task) else (putStrLn . compactTaskResult tz) task pPrintCompact = #if MIN_VERSION_pretty_simple(4,0,0) pPrintOpt CheckColorTty (defaultOutputOptionsDarkBg {outputOptionsCompact = True}) #else pPrint #endif compactTaskResult :: TimeZone -> TaskResult -> String compactTaskResult tz (TaskResult pkg arch method state _mparent taskid mstart mend) = let time = case mend of Just end -> compactZonedTime tz end Nothing -> maybe "" (compactZonedTime tz) mstart in showPackage pkg +-+ (if method == "buildArch" then arch else method) +-+ "(" ++ show taskid ++ ")" +-+ show state +-+ time -- FIXME show task owner formatTaskResult :: Maybe UTCTime -> TimeZone -> TaskResult -> [String] formatTaskResult mtime tz (TaskResult pkg arch method state mparent taskid mstart mend) = [ showPackage pkg +-+ (if method == "buildArch" then arch else method) +-+ show state , "https://koji.fedoraproject.org/koji/taskinfo?taskID=" ++ show taskid +-+ maybe "" (\p -> "(parent: " ++ show p ++ ")") mparent] ++ [formatTime defaultTimeLocale "Start: %c" (utcToZonedTime tz start) | Just start <- [mstart]] ++ [formatTime defaultTimeLocale "End: %c" (utcToZonedTime tz end) | Just end <- [mend]] #if MIN_VERSION_time(1,9,1) ++ case mtime of Just now -> ["current duration: " ++ formatTime defaultTimeLocale "%Hh %Mm %Ss" dur | Just start <- [mstart], let dur = diffUTCTime now start] Nothing -> ["duration: " ++ formatTime defaultTimeLocale "%Hh %Mm %Ss" dur | Just start <- [mstart], Just end <- [mend], let dur = diffUTCTime end start] #endif showPackage :: Either String NVR -> String showPackage (Left p) = p showPackage (Right nvr) = showNVR nvr data TaskResult = TaskResult {taskPackage :: Either String NVR, _taskArch :: String, _taskMethod :: String, _taskState :: TaskState, _mtaskParent :: Maybe Int, taskId :: Int, _mtaskStartTime :: Maybe UTCTime, mtaskEndTime :: Maybe UTCTime } #if !MIN_VERSION_koji(0,0,3) taskStateToValue :: TaskState -> Value taskStateToValue = ValueInt . fromEnum parseTaskState :: String -> TaskState parseTaskState s = case lower s of "free" -> TaskFree "open" -> TaskOpen "close" -> TaskClosed "closed" -> TaskClosed "cancel" -> TaskCanceled "canceled" -> TaskCanceled "assigned" -> TaskAssigned "fail" -> TaskFailed "failed" -> TaskFailed _ -> error' $! "unknown task state: " ++ s #endif data LastLog = WholeBuild | BuildTail | RootLog deriving Eq logUrl :: Int -> LastLog -> String logUrl taskid lastlog = if lastlog == BuildTail then "https://koji.fedoraproject.org/koji/getfile?taskID=" ++ tid ++ "&name=build.log&offset=-4000" else "https://kojipkgs.fedoraproject.org/work/tasks" lastFew tid logName <.> "log" where tid = show taskid lastFew = let few = dropWhile (== '0') $ takeEnd 4 tid in if null few then "0" else few logName = if lastlog == RootLog then "root" else "build" buildlogSize :: Bool -> Int -> IO () buildlogSize tail' taskid = do let buildlog = logUrl taskid WholeBuild exists <- httpExists' buildlog when exists $ do putStr $ buildlog ++ " " msize <- httpFileSize' buildlog whenJust msize $ \size -> do putStr "(" (T.putStr . kiloBytes) size putStrLn ")" -- FIXME check if short build.log ends with srpm lastlog <- if size < 1500 then do putStrLn $ logUrl taskid RootLog return RootLog else return BuildTail when tail' $ displayLog taskid lastlog where kiloBytes s = prettyI (Just ',') (fromInteger s `div` 1000) <> T.pack "kB" kojiMethods :: [String] kojiMethods = nub . sort $ -- www/kojiweb/index.py _TASKS (["build", "buildSRPMFromSCM", "rebuildSRPM", "buildArch", "chainbuild", "maven", "buildMaven", "chainmaven", "wrapperRPM", "winbuild", "vmExec", "waitrepo", "tagBuild", "newRepo", "createrepo", "distRepo", "createdistrepo", "buildNotification", "tagNotification", "dependantTask", "livecd", "createLiveCD", "appliance", "createAppliance", "image", "indirectionimage", "createImage", "livemedia", "createLiveMedia", "buildContainer"] ++ -- https://koji.fedoraproject.org/koji/tasks ["all", "appliance", "build", "buildArch", "buildContainer", "buildNotification", "buildSRPMFromSCM", "chainbuild", "chainmaven", "createAppliance", "createContainer", "createImage", "createLiveCD", "createLiveMedia", "createdistrepo", "createrepo", "dependantTask", "distRepo", "image", "indirectionimage", "livecd", "livemedia", "newRepo", "rebuildSRPM", "runroot", "tagBuild", "tagNotification", "waitrepo"]) displayLog :: Int -> LastLog -> IO () displayLog tid lastlog = do req <- parseRequest $ logUrl tid lastlog resp <- httpLBS req let out = U.toString $ getResponseBody resp ls = lines out putStrLn "" if lastlog == RootLog then let excluded = ["Executing command:", "Child return code was: 0", "child environment: None", "ensuring that dir exists:", "touching file:", "creating dir:", "kill orphans"] in putStr $ unlines $ map (dropPrefix "DEBUG ") $ takeEnd 30 $ filter (\l -> not (any (`isInfixOf` l) excluded)) ls else if last ls == "Child return code was: 0" then putStr out else do let err = "Child return code was: 1" putStr $ unlines $ takeWhile (err /=) ls putStrLn err