summaryrefslogtreecommitdiff
path: root/lambda-calcul/haskell/src/Minilang/REPL/Net.hs
blob: 43d2560e1436d5013e1c25f2fb845b7882f1502f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- | A version of REPL that interacts through a websocket.
module Minilang.REPL.Net where

import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TVar
import Control.Monad.Catch
import Control.Monad.State
import Data.Aeson
  ( ToJSON,
    eitherDecode,
    encode,
    object,
    (.=),
  )
import Data.ByteString (ByteString)
import qualified Data.ByteString.Lazy as LBS
import Data.Either (fromRight)
import Data.Map as Map
import Data.Serialize hiding (encode)
import Data.Text (Text, pack, unpack)
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Minilang.Log
import Minilang.REPL (withInputs)
import Minilang.REPL.Pure (render)
import Minilang.REPL.Purer (purerREPL)
import Minilang.REPL.Run (runREPL)
import Minilang.REPL.Types
import Network.Wai (Application)
import Network.Wai.Handler.WebSockets
import qualified Network.WebSockets as WS
import System.Directory (getCurrentDirectory)
import System.FilePath (takeFileName, (</>))

-- | Configuration of the WS server
data ServerConfig = ServerConfig
  { -- | The host/IP to listen on. If 0.0.0.0, then the server listens
    configHost :: String,
    -- | The port to listen on. If 0, a random port will be assigned.
    configPort :: Int
  }
  deriving (Eq, Show)

data NetEnv = NetEnv
  { connection :: WS.Connection,
    logger :: LoggerEnv IO,
    repl :: TVar REPLEnv
    --    , storage :: FileStorage
  }

newtype Net m a = Net {runNet :: StateT NetEnv m a}
  deriving (Functor, Applicative, Monad, MonadTrans, MonadIO, MonadState NetEnv)

instance Serialize In where
  put out = do
    let bs = encode out
    putWord64be (fromIntegral $ LBS.length bs)
    putLazyByteString bs

  get = do
    len <- getWord64be
    bs <- getLazyByteString (fromIntegral len)
    case eitherDecode bs of
      Right out -> pure out
      Left err -> fail err

-- doStore :: In -> Net IO (Either Text ())
-- doStore out = do
--     fs <- gets storage
--     res <- liftIO $ store fs (pure $ Right out) handleStorageResult
--     case res of
--         WriteSucceed _ -> pure $ Right ()
--         err -> pure $ Left (pack $ show err)
--   where
--     handleStorageResult (Right (WriteSucceed _)) = pure $ Right ()
--     handleStorageResult (Right err) = pure $ Left $ pack $ show err
--     handleStorageResult (Left err) = pure $ Left $ pack err

netLog :: (ToJSON a) => a -> Net IO ()
netLog logEntry = gets logger >>= liftIO . flip logInfo logEntry

instance MonadREPL (Net IO) where
  input = do
    inp <- gets connection >>= lift . wsReceive
    -- res <- doStore inp
    -- case res of
    --   Right () -> netLog (object ["action" .= ("input" :: Text), "content" .= inp])
    --   Left txt -> netLog (object ["action" .= ("storageError" :: Text), "content" .= txt])
    netLog (object ["action" .= ("input" :: Text), "content" .= inp])
    pure inp

  output a = do
    gets connection >>= lift . wsSend a
    netLog
      ( object
          [ "action" .= ("output" :: Text),
            "content" .= a,
            "rendered" .= render a
          ]
      )

  prompt = pure ()
  getEnv = gets repl >>= lift . readTVarIO
  setEnv e' = gets repl >>= lift . atomically . flip writeTVar e'

  -- TODO what does this mean? perhaps it coiuld be used to load some
  -- virtual files edited by user in their env?
  load = const (pure (Right ""))

instance (MonadThrow m) => MonadThrow (Net m) where
  throwM = lift . throwM

instance (MonadCatch m) => MonadCatch (Net m) where
  Net m `catch` f = Net $ m `catch` \e -> runNet (f e)

-- instance TypeChecker (Net IO) where
--     emit (CheckD ev) = gets logger >>= \env -> liftIO $ logInfo env ev
--     emit _ = pure ()

wsReceive :: WS.Connection -> IO In
wsReceive cnx =
  fromRight (In "") . eitherDecode <$> WS.receiveData cnx

wsSend :: Out -> WS.Connection -> IO ()
wsSend out cnx =
  WS.sendBinaryData cnx (encode out)

safeDecodeUtf8 :: ByteString -> Text
safeDecodeUtf8 = decodeUtf8With lenientDecode

clientHandler :: LoggerEnv IO -> TVar (Map ByteString (TVar REPLEnv)) -> WS.PendingConnection -> IO ()
clientHandler loggerEnv envs cnx = do
  let path = WS.requestPath $ WS.pendingRequest cnx
  logInfo loggerEnv $ object ["action" .= ("StartedREPL" :: Text), "path" .= safeDecodeUtf8 path]
  conn <- WS.acceptRequest cnx
  -- storageFile <- mkStorageFile path
  -- withStorage storageFile $ \fileStorage -> do
  replEnv <- newREPLEnv
  WS.withPingThread conn 30 (pure ()) $
    evalStateT (runNet runREPL) (NetEnv conn loggerEnv replEnv)
  where
    newREPLEnv = newTVarIO initialREPL

-- findOrCreateREPL :: (H.Store IO store) => store -> ByteString -> IO (TVar REPLEnv)
-- findOrCreateREPL storageFile path = do
--   (env, shouldLoad) <- atomically $ do
--     envMaps <- readTVar envs
--     (newEnv, shouldLoad) <- case Map.lookup path envMaps of
--       Nothing -> newTVar initialREPL >>= \v -> pure (v, True)
--       Just e -> pure (e, False)
--     writeTVar envs (Map.insert path newEnv envMaps)
--     pure (newEnv, shouldLoad)
--   -- FIXME there is a race condition here
--   if shouldLoad
--     then replay storageFile env
--     else pure env

-- mkStorageFile path = do
--   let f = takeFileName (unpack (safeDecodeUtf8 path))
--   dir <- getCurrentDirectory
--   pure $ defaultOptions {storageFilePath = dir </> f}

-- replay :: (H.Store IO store) => store -> TVar REPLEnv -> IO (TVar REPLEnv)
-- replay storage envVar = do
--   res <- H.load storage
--   case res of
--     LoadSucceed coms -> do
--       atomically $ do
--         env <- readTVar envVar
--         let newEnv = withInputs env coms
--         writeTVar envVar (purerREPL newEnv)
--       logInfo loggerEnv $
--         object
--           [ "action" .= ("Loaded" :: Text),
--             "numCommands" .= length coms
--           ]
--       pure envVar
--     err -> do
--       logInfo loggerEnv $ object ["action" .= ("ErrorLoading" :: Text), "error" .= pack (show err)]
--       pure envVar

runNetREPL :: LoggerEnv IO -> TVar (Map ByteString (TVar REPLEnv)) -> Application -> Application
runNetREPL loggerEnv envs = websocketsOr WS.defaultConnectionOptions (clientHandler loggerEnv envs)