Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proof Of Concept Repl Bots via nix-shell #32

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
29 changes: 20 additions & 9 deletions app/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,35 @@ module Main where

import CofreeBot
import CofreeBot.Bot.Behaviors.Calculator.Language
import Control.Monad
import Data.Profunctor
import qualified Data.Text as T
import GHC.Conc (threadDelay)
import Network.Matrix.Client
import qualified Options.Applicative as Opt
import OptionsParser
import System.Environment.XDG.BaseDir ( getUserCacheDir )
import System.Process.Typed
import Data.Foldable (traverse_)

--main :: IO ()
--main = withProcessWait_ ghciConfig $ \process -> do
-- runSimpleBot (simplifySessionBot (T.intercalate "\n" . printCalcOutput) programP $ sessionize mempty $ calculatorBot) mempty
-- runSimpleBot (ghciBot process) mempty
main :: IO ()
main = cliMain

-- let ghciBot' = ghciBot process
-- calcBot = simplifySessionBot (T.intercalate "\n" . printCalcOutput) programP $ sessionize mempty $ calculatorBot
-- runSimpleBot (rmap (\(x :& y) -> x <> y ) $ ghciBot' /\ calcBot) (mempty)
cliMain :: IO ()
cliMain = withProcesses replConfigs $ \handles@Repls{..} -> do
void $ threadDelay 1e7
traverse_ (hGetOutput . getStdout) handles
runTextBot (rmap (\(x :& y :& z :& q) -> x <> y <> z <> q) $
nodeBot node /\ pythonBot python /\ ghciBot ghci /\ mitSchemeBot mitScheme) mempty
--runSimpleBot (simplifySessionBot (T.intercalate "\n" . printCalcOutput) programP $ sessionize mempty $ calculatorBot) mempty
--let ghciBot' = ghciBot process
-- calcBot = simplifySessionBot (T.intercalate "\n" . printCalcOutput) programP $ sessionize mempty $ calculatorBot
--runTextBot (rmap (\(x :& y) -> x <> y ) $ ghciBot' /\ calcBot) (mempty)

main :: IO ()
main = do
matrixMain :: IO ()
matrixMain = withProcesses replConfigs $ \Repls{..} -> do
void $ threadDelay 1e7
void $ hGetOutput (getStdout ghci)
command <- Opt.execParser parserInfo
xdgCache <- getUserCacheDir "cofree-bot"
let calcBot =
Expand Down
8 changes: 7 additions & 1 deletion cofree-bot.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@ library
CofreeBot.Bot.Behaviors.Calculator
CofreeBot.Bot.Behaviors.Calculator.Language
CofreeBot.Bot.Behaviors.CoinFlip
CofreeBot.Bot.Behaviors.GHCI
CofreeBot.Bot.Behaviors.Hello
CofreeBot.Bot.Behaviors.Repl
CofreeBot.Bot.Behaviors.Repl.GHCI
CofreeBot.Bot.Behaviors.Repl.MitScheme
CofreeBot.Bot.Behaviors.Repl.Node
CofreeBot.Bot.Behaviors.Repl.Python
CofreeBot.Bot.Behaviors.Repl.SBCL
CofreeBot.Bot.Behaviors.Repl.Util
CofreeBot.Bot.Context
CofreeBot.Utils

Expand Down
5 changes: 2 additions & 3 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
#!/usr/bin/env bash
set -euxo pipefail

nix build .#docker
image=$(docker load -i result | sed -n 's#^Loaded image: \([a-zA-Z0-9\.\/\-\:]*\)#\1#p')
docker push $image
nix build .#deploy-bot
./result
40 changes: 37 additions & 3 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
# cabal2nix uses IFD
hsPkgs = evalPkgs.haskell.packages.${compiler}.override {
overrides = hfinal: hprev: {
cofree-bot = hfinal.callCabal2nix "cofree-bot" ./. { };
cofree-bot = hfinal.callCabal2nix "cofree-bot"
(pkgs.lib.sourceFilesBySuffices ./. [ ".hs" ".cabal" ".project" ])
{ };

# command to reproduce:
# cabal2nix https://github.com/softwarefactory-project/matrix-client-haskell --subpath matrix-client --revision f8610d8956bd146105292bb75821ca078d01b5ff > .nix/deps/matrix-client.nix
Expand Down Expand Up @@ -76,14 +78,46 @@
# ];
};

packages = flake-utils.lib.flattenTree {
docker = import ./nix/docker.nix {
packages = flake-utils.lib.flattenTree rec {
bot-image = import ./nix/docker.nix {
inherit pkgs;
cofree-bot = hsPkgs.cofree-bot;
};

repls = import ./nix/repl-containers.nix {
inherit pkgs;
};

cofree-bot = hsPkgs.cofree-bot;

deploy-bot =
pkgs.writeScript "deploy-bot"
''
#!${pkgs.bash}/bin/bash
set -euxo pipefail

image=$(${pkgs.docker}/bin/docker load -i ${bot-image} | sed -n 's#^Loaded image: \([a-zA-Z0-9\.\/\-\:]*\)#\1#p')
${pkgs.docker}/bin/docker push $image

for x in ${repls}/*; do
image=$(${pkgs.docker}/bin/docker load -i $x | sed -n 's#^Loaded image: \([a-zA-Z0-9\.\/\-\:]*\)#\1#p')
${pkgs.docker}/bin/docker push $image
done
'';

deploy-repls =
pkgs.writeScript "deploy-bot"
''
#!${pkgs.bash}/bin/bash
set -euxo pipefail
for x in ${repls}/*; do
${pkgs.docker}/bin/docker load -i $x
done
'';
Comment on lines +93 to +116
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonathanLorimer We are building these scripts for building deploy scripts for the bot and the repl images. We tried to make these apps rather then packages with pkgs.runCommand but werent able to run docker load. Do you know of any tricks to perform side effects in a flake output or is that off the table?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we need to get these images onto the server and loaded into docker. I don't want to put them on the github docker hub because they are going to take up too much space as we add more images. Can we modify our github deploy action to sync them onto the server directly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the scripts, I would suggest moving them to a separate file parameterized by their dependencies.
I would also suggest using writeShellApplication https://github.com/NixOS/nixpkgs/blob/master/pkgs/build-support/trivial-builders.nix#L274-L305 it will set a bunch of sane defaults as well as the shebang, and you can specify sed as a runtime input. It will also run shellcheck in the check phase.

};



checks = {
pre-commit-check = pre-commit-hooks.lib.${system}.run {
src = ./.;
Expand Down
31 changes: 31 additions & 0 deletions nix/repl-containers.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{ pkgs }:
let
mkImage = pkg: cmd:
pkgs.dockerTools.buildLayeredImage {
name = "ghcr.io/cofree-coffee/${pkg.name}-repl";
created = "now";
tag = "latest";
contents = [
pkg
];
config = {
Cmd = cmd;
};
};

mkPath = xs:
with pkgs.lib;
with pkgs.lib.attrsets;
let paths = mapAttrsToList (name: image: "ln -s ${image} $out/${name}") xs;
in "mkdir -p $out" + (foldl' (a: b: a + ";" + b) "" paths);

images =
{
python = mkImage pkgs.python3 [ "python" "-iq" ];
node = mkImage pkgs.nodejs [ "node" "-i" ];
ghci = mkImage pkgs.ghc [ "ghci" ];
mitScheme = mkImage pkgs.mitscheme [ "mit-scheme" ];
sbcl = mkImage pkgs.sbcl [ "sbcl" ];
};
Comment on lines +22 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
images =
{
python = mkImage pkgs.python3 [ "python" "-iq" ];
node = mkImage pkgs.nodejs [ "node" "-i" ];
ghci = mkImage pkgs.ghc [ "ghci" ];
mitScheme = mkImage pkgs.mitscheme [ "mit-scheme" ];
sbcl = mkImage pkgs.sbcl [ "sbcl" ];
};
images =
foldl' (acc {name, package, cmd}: acc // { "${name}" = mkImage package cmd }) {} supportedLanguages;
supportedLanguages =
[ { name = "python"; package = pkgs.python3; cmd = [ "python" "-iq" ]}
{ name = "node"; package = pkgs.nodejs; cmd = [ "node" "-i]}
{ name = "ghci"; package = pgks.ghc; cmd = [ "ghci"]}
{ name = "mitScheme"; package = pkgs.mitscheme; cmd = [ "mit-scheme ]
{ name = "sbcl"; package = pkgs.sbcl; cmd = [ "sbcl"]}
];

By more scalable do you mean something like this (like easier to add, although I am not sure this is an improvement). Or do you mean that a lot of docker containers is expensive.

in
pkgs.runCommand "repls" { } (mkPath images)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonathanLorimer for REPL bots I am building docker images with nix. Do you have any thoughts on how we can design this derivation to be more scalable for adding more languages?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly think these could all just be systemd jobs inside nix shells though...

  systemd.services.python-repl = {
      description = "Python repl";
      wantedBy = [ "multi-user.target" ];
      after = [ "network.target" ];
      serviceConfig = {
        Type = "notify";
        ExecStart = "nix-shell -p python3 --command "python -iq"";
      };
    };

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could abstract the python specific stuff in a nix module and then your nixos configuration would just do this:

sercives.language-repls = [
  {name = "python"; package = pkgs.python3; binName = "python3"; opts = "-iq";}
   ...
]; 

you wouldn't even need the nix shell bit above if you did direct interpolation:
ExecStart = "${package}/bin/${binName} ${opts}"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JonathanLorimer I don't think that's a good idea. The point of containerization is to isolate the server from untrusted code execution, so that if someone runs rm -rf / as a joke it doesn't actually nuke the entire server. You can increase the isolation of the server from these untrusted REPLs by using Linux namespacing stuff, but the more of that you do, the more of existing containerization solutions you reinvent.

4 changes: 2 additions & 2 deletions src/CofreeBot/Bot/Behaviors.hs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
module CofreeBot.Bot.Behaviors
( module Calculator
, module CoinFlip
, module GHCI
, module Hello
, module Repl
) where

import CofreeBot.Bot.Behaviors.Calculator
as Calculator
import CofreeBot.Bot.Behaviors.CoinFlip
as CoinFlip
import CofreeBot.Bot.Behaviors.GHCI as GHCI
import CofreeBot.Bot.Behaviors.Hello as Hello
import CofreeBot.Bot.Behaviors.Repl as Repl
49 changes: 0 additions & 49 deletions src/CofreeBot/Bot/Behaviors/GHCI.hs

This file was deleted.

27 changes: 27 additions & 0 deletions src/CofreeBot/Bot/Behaviors/Repl.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module CofreeBot.Bot.Behaviors.Repl
( module Util
, module GHCI
, module MitScheme
, module Node
, module Python
, module SBCL
, replConfigs
) where

import CofreeBot.Bot.Behaviors.Repl.Util as Util
import CofreeBot.Bot.Behaviors.Repl.GHCI as GHCI
import CofreeBot.Bot.Behaviors.Repl.MitScheme as MitScheme
import CofreeBot.Bot.Behaviors.Repl.Node as Node
import CofreeBot.Bot.Behaviors.Repl.Python as Python
import CofreeBot.Bot.Behaviors.Repl.SBCL as SBCL
import System.IO
import System.Process.Typed

replConfigs :: Repls (ProcessConfig Handle Handle ())
replConfigs = Repls
{ python = pythonConfig
, ghci = ghciConfig
, node = nodeConfig
, mitScheme = mitSchemeConfig
-- , sbcl = sbclConfig
}
24 changes: 24 additions & 0 deletions src/CofreeBot/Bot/Behaviors/Repl/GHCI.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module CofreeBot.Bot.Behaviors.Repl.GHCI
( ghciBot
, ghciConfig
) where

import CofreeBot.Bot
import CofreeBot.Bot.Behaviors.Repl.Util
import CofreeBot.Utils
import Data.Profunctor
import System.IO
import System.Process.Typed

ghciBot' :: Process Handle Handle () -> ReplBot
ghciBot' = replBot "ghci: "

ghciBot :: Process Handle Handle () -> ReplBot
ghciBot p =
dimap (distinguish (/= "ghci: :q")) indistinct
$ pureStatelessBot (const $ ["I'm Sorry Dave"])
\/ ghciBot' p

ghciConfig :: ProcessConfig Handle Handle ()
ghciConfig = replConfig "docker run -i --rm haskell 2>&1"

14 changes: 14 additions & 0 deletions src/CofreeBot/Bot/Behaviors/Repl/MitScheme.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module CofreeBot.Bot.Behaviors.Repl.MitScheme
( mitSchemeBot
, mitSchemeConfig
) where

import CofreeBot.Bot.Behaviors.Repl.Util
import System.IO
import System.Process.Typed

mitSchemeBot :: Process Handle Handle () -> ReplBot
mitSchemeBot = replBot "mit-scheme: "

mitSchemeConfig :: ProcessConfig Handle Handle ()
mitSchemeConfig = replConfig "nix-shell -p mitscheme --run 'mit-scheme' 2>&1"
14 changes: 14 additions & 0 deletions src/CofreeBot/Bot/Behaviors/Repl/Node.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module CofreeBot.Bot.Behaviors.Repl.Node
( nodeBot
, nodeConfig
) where

import CofreeBot.Bot.Behaviors.Repl.Util
import System.IO
import System.Process.Typed

nodeBot :: Process Handle Handle () -> ReplBot
nodeBot = replBot "node: "

nodeConfig :: ProcessConfig Handle Handle ()
nodeConfig = replConfig "nix-shell -p nodejs --run 'node -i' 2>&1"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i need to get this command running in a docker container.

14 changes: 14 additions & 0 deletions src/CofreeBot/Bot/Behaviors/Repl/Python.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module CofreeBot.Bot.Behaviors.Repl.Python
( pythonBot
, pythonConfig
) where

import CofreeBot.Bot.Behaviors.Repl.Util
import System.IO
import System.Process.Typed

pythonBot :: Process Handle Handle () -> ReplBot
pythonBot = replBot "python: "

pythonConfig :: ProcessConfig Handle Handle ()
pythonConfig = replConfig "nix-shell -p python3 --run 'python -iq' 2>&1"
14 changes: 14 additions & 0 deletions src/CofreeBot/Bot/Behaviors/Repl/SBCL.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module CofreeBot.Bot.Behaviors.Repl.SBCL
( sbclBot
, sbclConfig
) where

import CofreeBot.Bot.Behaviors.Repl.Util
import System.IO
import System.Process.Typed

sbclBot :: Process Handle Handle () -> ReplBot
sbclBot = replBot "sbcl: "

sbclConfig :: ProcessConfig Handle Handle ()
sbclConfig = replConfig "nix-shell -p sbcl --run sbcl 2>&1"
58 changes: 58 additions & 0 deletions src/CofreeBot/Bot/Behaviors/Repl/Util.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{-# LANGUAGE NumDecimals #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
module CofreeBot.Bot.Behaviors.Repl.Util where

import CofreeBot.Bot
import Control.Exception (bracket)
import Control.Monad
import Control.Monad.Loops ( whileM )
import Data.Attoparsec.Text as A
import Data.Foldable
import qualified Data.Text as T
import GHC.Conc ( threadDelay )
import System.IO
import System.Process.Typed

type ReplBot = Bot IO () T.Text [T.Text]

data ReplConfig = ReplConfig
{ rcPrompt :: T.Text
, rcImageName :: T.Text
, rcShellCmd :: T.Text
, rcStartupDelay :: Int
, rcLineDelay :: Int
}

hGetOutput :: Handle -> IO String
hGetOutput handle = whileM (hReady handle) (hGetChar handle)

replBot :: T.Text -> Process Handle Handle () -> ReplBot
replBot prompt p =
mapMaybeBot (either (const Nothing) Just . parseOnly (replInputParser prompt))
$ Bot
$ \i s -> do
hPutStrLn (getStdin p) $ T.unpack i
hFlush (getStdin p)
void $ threadDelay 1e6
o <- hGetOutput (getStdout p)
pure $ BotAction (pure $ T.pack o) s

replConfig :: String -> ProcessConfig Handle Handle ()
replConfig = setStdin createPipe . setStdout createPipe . shell

replInputParser :: T.Text -> Parser T.Text
replInputParser prompt = do
void $ string prompt
T.pack <$> many1 anyChar

data Repls a = Repls
{ python :: a
, ghci :: a
, node :: a
, mitScheme :: a
-- , sbcl :: a
} deriving (Functor, Foldable, Traversable)

withProcesses :: Repls (ProcessConfig i o e) -> (Repls (Process i o e) -> IO r) -> IO r
withProcesses cfgs = bracket (traverse startProcess cfgs) (traverse_ stopProcess)