packages feed

hercules-ci-agent-0.9.0: hercules-ci-agent-worker/Hercules/Agent/Worker/NixDaemon.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}

module Hercules.Agent.Worker.NixDaemon where

import Control.Concurrent.STM.TVar (modifyTVar, newTVarIO, readTVar)
import qualified Data.Binary
import qualified Data.Binary.Get
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Map as M
import Hercules.Agent.Binary (decodeBinaryFromHandle)
import qualified Hercules.Agent.WorkerProtocol.Command.StartDaemon as StartDaemon
import Hercules.Agent.WorkerProtocol.Event.DaemonStarted (DaemonStarted (DaemonStarted, _dummy))
import qualified Hercules.CNix as CNix
import qualified Language.C.Inline.Cpp as C
import qualified Language.C.Inline.Cpp.Exception as C
import Network.Socket
import Protolude
import System.Posix.Internals (setNonBlockingFD)
import UnliftIO (BufferMode (NoBuffering), hSetBuffering, timeout)
import UnliftIO.IO (hFlush)

C.context C.cppCtx

C.include "<nix/config.h>"
C.include "<nix/daemon.hh>"
C.include "<iostream>"
C.using "namespace nix"

nixDaemon :: IO ()
-- nixDaemon = withKatip $ Logger.withLoggerConduit (_) $ Logger.withTappedStderr Logger.tapper $ liftIO $ do
nixDaemon = do
  hSetBuffering stdin NoBuffering
  startCmd <-
    getStdinMessage >>= \case
      Left _ -> throwIO $ FatalError "Could not decode command"
      Right Nothing -> do
        putErrText "warning: Exit before receiving start command"
        exitSuccess -- We're done.
      Right (Just r) -> pure r
  let path = StartDaemon.socketPath startCmd

  CNix.init

  clientThreads <- newTVarIO mempty

  sock <- socket AF_UNIX Stream 0
  bind sock (SockAddrUnix path)
  listen sock 100

  putStdoutMessage DaemonStarted {_dummy = True}

  let socketLoop =
        forever do
          (clientSocket, _) <- accept sock
          (pid, _uid, _gid) <- getPeerCredential clientSocket
          uninterruptibleMask \_unmask -> do
            let removeMe = do
                  t <- myThreadId
                  atomically do
                    modifyTVar clientThreads (M.delete t)
            t <-
              forkFinally -- TODO forkOS?
                (handleClient clientSocket)
                \case
                  Left _e -> removeMe -- >> putErrText ("Connection for pid " <> showPid pid <> " ended: " <> toS (displayException _e))
                  Right _ -> removeMe
            atomically do
              modifyTVar clientThreads (M.insert t pid)
  withAsync socketLoop \_socketLoopAsync ->
    void getStdinMessage

  void $ timeout (10 * 60 * 1000 * 1000) do
    hadThreads <- do
      ts <- atomically do
        readTVar clientThreads
      let hasThreads = not (null ts)
      when hasThreads do
        putErrLn ("Waiting for termination of connections from pids " <> unwords (showPid <$> toList ts))
      pure hasThreads
    atomically do
      ts <- readTVar clientThreads
      guard $ null ts
    when hadThreads do
      putErrText "All connections terminated; exiting"

showPid :: Maybe C.CUInt -> Text
showPid = maybe "unknown pid" show

putStdoutMessage :: DaemonStarted -> IO ()
putStdoutMessage msg = do
  BS.hPut stdout $ BL.toStrict $ Data.Binary.encode msg
  hFlush stdout

getStdinMessage ::
  IO
    ( Either
        (ByteString, Data.Binary.Get.ByteOffset, [Char])
        (Maybe StartDaemon.StartDaemon)
    )
getStdinMessage = decodeBinaryFromHandle stdin

handleClient :: Socket -> IO ()
handleClient clientSocket = withFdSocket clientSocket \fd -> flip finally (close clientSocket) do
  setNonBlockingFD fd False
  [C.throwBlock| void {
    ref<Store> store = openStore();
    FdSource from($(int fd));
    FdSink to($(int fd));
    daemon::TrustedFlag trusted = daemon::NotTrusted;
    daemon::RecursiveFlag recursive = daemon::NotRecursive;
    std::function<void(Store &)> authHook = [](Store &){};
    daemon::processConnection(
        store
        , from
        , to
        , trusted
        , recursive
        , authHook
        );
  }|]