packages feed

sectile-0.1.0.0: src/Data/Sectile/System/Linux/Network.hs

-- |
-- Module        : Data.Sectile.System.Linux.Network
-- Copyright     : Gautier DI FOLCO
-- License       : ISC
--
-- Maintainer    : Gautier DI FOLCO <foss@difolco.dev>
-- Stability     : Stable
-- Portability   : Portable
module Data.Sectile.System.Linux.Network
  ( networkStats,
    networkUp,
    networkDown,
    parseNetDevBytes,
    NetDirection (..),
  )
where

import qualified Control.Exception as Exception
import qualified Data.ByteString.Builder as B
import Data.Maybe (mapMaybe)
import Data.Sectile.System.Linux.Internal
import Data.Sectile.Types
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.Lazy.Encoding as TLE
import qualified Data.Time.Clock.POSIX as POSIX
import qualified System.Directory as Dir
import qualified System.Exit as Exit
import qualified System.Process as Process

-- | Display network speed for a given list of interfaces.
networkStats :: Name -> [T.Text] -> NetDirection -> Segment IO
networkStats name@(Name nameB) ifaces direction = Segment $ do
  contentRes <- tryReadFile "/proc/net/dev"
  case contentRes of
    Left _ -> pure $ mkFormatted nameB typeName (errMsg nameB) []
    Right content -> do
      let statsList = mapMaybe (\iface -> parseNetDevBytes iface direction content) ifaces
      case statsList of
        [] -> pure $ mkFormatted nameB typeName (errMsg nameB) []
        _ -> do
          let totalBytes = sum statsList
          now <- POSIX.getPOSIXTime
          let nowMs = round (now * 1000) :: Int

          sessionOutRes <-
            (Right <$> Process.readProcessWithExitCode "tmux" ["display-message", "-p", "#S"] "")
              `Exception.catch` (\(_ :: IOError) -> pure $ Left ())
          let session = case sessionOutRes of
                Right (Exit.ExitSuccess, out, _) -> T.unpack (T.strip (T.pack out))
                _ -> "default"

          let segName = T.unpack $ TL.toStrict $ TLE.decodeUtf8 $ B.toLazyByteString nameB
          let memFile = "/tmp/tmux-net-speeds-mem-" <> session <> "-" <> segName

          fileExists <- Dir.doesFileExist memFile
          rate <-
            if fileExists
              then do
                fileContent <- tryReadFile memFile
                case fileContent of
                  Right fc -> do
                    case T.words (T.strip fc) of
                      [tsStr, bytesStr] -> do
                        case (readInt tsStr, readInt bytesStr) of
                          (Just tsPrev, Just bytesPrev) -> do
                            let dt = nowMs - tsPrev
                            if dt > 0
                              then pure $ Just $ (totalBytes - bytesPrev) * 1000 `div` dt
                              else pure Nothing
                          _ -> pure Nothing
                      _ -> pure Nothing
                  Left _ -> pure Nothing
              else pure Nothing

          _ <- tryWriteFile memFile (T.pack (show nowMs) <> " " <> T.pack (show totalBytes))

          let (txt, bnds) = case rate of
                Just r -> (formatKiB (r `div` 1024) <> "/s", unitBindings "B/s" name (fromIntegral r))
                Nothing -> ("  -  B/s", unitBindings "B/s" name 0)

          pure $ do
            _ <- appendBindings bnds
            mkFormatted nameB typeName txt [("Interfaces", T.intercalate "," ifaces)]
  where
    typeName = case direction of
      NetTransmit -> "networkUp"
      NetReceive -> "networkDown"

-- | Display network upload speed for a given list of interfaces.
networkUp :: Name -> [T.Text] -> Segment IO
networkUp name ifaces = networkStats name ifaces NetTransmit

-- | Display network download speed for a given list of interfaces.
networkDown :: Name -> [T.Text] -> Segment IO
networkDown name ifaces = networkStats name ifaces NetReceive

-- | Parse /proc/net/dev for a specific interface returning bytes.
parseNetDevBytes :: T.Text -> NetDirection -> T.Text -> Maybe Int
parseNetDevBytes iface direction content =
  let lns = T.lines content
      ifacePrefix = T.strip iface <> ":"
      matchLine l =
        let stripped = T.stripStart l
         in T.isPrefixOf ifacePrefix stripped
   in case filter matchLine lns of
        (l : _) ->
          let parts = T.words $ T.drop (T.length ifacePrefix) $ T.stripStart l
              -- Receive: bytes(0) packets(1) ...
              -- Transmit: bytes(8) packets(9) ...
              idx = case direction of
                NetReceive -> 0
                NetTransmit -> 8
           in case drop idx parts of
                (val : _) -> readInt val
                _ -> Nothing
        _ -> Nothing

-- | Direction of network traffic: received (download) or transmitted (upload).
data NetDirection = NetReceive | NetTransmit