-- | Parsers and types related to cgroups and mounts
module System.CGroup.Types (
-- * CPU quotas
CPUQuota (..),
-- * raw cgroups as viewed in \/proc\/$PID\/cgroup
RawCGroup (..),
parseCGroups,
-- * mounts as viewed in \/proc\/$PID\/mountinfo
Mount (..),
parseMountInfo,
-- * Parser type
Parser,
parseFile,
) where
import Control.Exception (throwIO)
import Data.Char (isSpace)
import Data.Ratio
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.IO as TIO
import Data.Void (Void)
import Path
import Text.Megaparsec
import Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
---------- CPU quotas
-- | A CPU quota is the ratio of CPU time our process can use relative to the
-- scheduler period
--
-- For example:
--
-- @
-- | ratio | description |
-- | ---------------- | ----------- |
-- | 100000 / 100000 | (1) |
-- | 200000 / 100000 | (2) |
-- | 50000 / 100000 | (3) |
-- | max / 100000 | (4) |
-- @
--
-- (1): we can use up to a single CPU core
--
-- (2): we can use up to two CPU cores
--
-- (3): the scheduler will give us a single CPU core for up to 50% of the time
--
-- (4): we can use all available CPU resources (there is no quota)
data CPUQuota
= CPUQuota (Ratio Int)
| NoQuota
deriving (Eq, Ord, Show)
---------- Raw cgroups
-- | A cgroup, as viewed within \/proc\/[pid]\/cgroup
--
-- see cgroups(7): \/proc\/[pid]\/cgroup section
data RawCGroup = RawCGroup
{ rawCGroupId :: Text
, rawCGroupControllers :: [Text]
, rawCGroupPath :: Path Abs Dir
}
deriving (Show)
-- | Parse an entire \/proc\/[pid]\/cgroup file into a list of cgroups
parseCGroups :: Parser [RawCGroup]
parseCGroups = some parseSingleCGroup <* eof
-- | Parse a single cgroup line within \/proc\/[pid]\/cgroup
--
-- hierarchyID:list,of,controllers:path
--
-- In cgroups version 1, a comma-separated list of controllers exists within each group
--
-- In cgroups version 2, the "controllers" section is always an empty string
--
-- see cgroups(7): \/proc\/[pid]\/cgroup section
parseSingleCGroup :: Parser RawCGroup
parseSingleCGroup =
RawCGroup
<$> takeUntil1P ':' -- hierarchy ID number
<*> (splitOnIgnoreEmpty "," <$> takeUntilP ':') -- comma-separated list of controllers
<*> (parseIntoAbsDir =<< takeUntil1P '\n') -- path
-- return the prefix of the input until reaching the supplied character.
-- the character is also consumed as part of this parser.
--
-- this parser succeeds even when the character does not exist in the input
takeUntilP :: Char -> Parser Text
takeUntilP c = takeWhileP Nothing (/= c) <* optional (char c)
-- like 'takeUntilP', but expects a non-empty prefix before the character
takeUntil1P :: Char -> Parser Text
takeUntil1P c = takeWhile1P Nothing (/= c) <* optional (char c)
-- Data.Text.splitOn, but returns empty list on empty haystack, rather than [""]
--
-- >>> Data.Text.splitOn "foo" ""
-- [""]
--
-- >>> splitOnIgnoreEmpty "foo" ""
-- []
splitOnIgnoreEmpty :: Text -> Text -> [Text]
splitOnIgnoreEmpty _ "" = []
splitOnIgnoreEmpty s str = Text.splitOn s str
---------- Mounts
-- | A mount, as viewed within \/proc\/[pid]\/mountinfo
--
-- see proc(5): \/proc\/[pid]\/mountinfo section
data Mount = Mount
{ mountId :: Text
, mountParentId :: Text
, mountStDev :: Text
, mountRoot :: Text
, mountPoint :: Text
, mountOptions :: Text
, mountTags :: [Text]
, mountFilesystemType :: Text
, mountSource :: Text
, mountSuperOptions :: [Text]
}
deriving (Show)
-- | Parse an entire \/proc\/[pid]\/mountinfo file into a list of mounts
parseMountInfo :: Parser [Mount]
parseMountInfo = some parseSingleMount <* eof
-- | Parse a single mount line within \/proc\/[pid]\/mountinfo
--
-- Fields are space-separated
--
-- see proc(5): \/proc\/[pid]\/mountinfo section
parseSingleMount :: Parser Mount
parseSingleMount =
Mount
<$> field -- id
<*> field -- parent id
<*> field -- st_dev
<*> field -- mount root
<*> field -- mount point
<*> field -- mount options
<*> field `manyTill` separator -- optional mount tags, terminated by "-"
<*> field -- filesystem type
<*> field -- mount source
<*> (splitOnIgnoreEmpty "," <$> field) -- super options
<* optional (char '\n')
-- a field in the mountinfo file, terminated by whitespace
field :: Parser Text
field = lexeme $ takeWhile1P Nothing (not . isSpace)
-- separator after optional mount tags ("-")
separator :: Parser Char
separator = lexeme $ char '-'
lexeme :: Parser a -> Parser a
lexeme = L.lexeme (skipMany (char ' '))
parseIntoAbsDir :: Text -> Parser (Path Abs Dir)
parseIntoAbsDir = either (fail . show) pure . parseAbsDir . Text.unpack
-- | Megaparsec Parser
type Parser = Parsec Void Text
-- | Parse a file
parseFile :: Parser a -> Path b File -> IO a
parseFile parser file = either throwIO pure . parse parser (toFilePath file) =<< TIO.readFile (toFilePath file)