Merge branch 'king-haskell' into king-exit-cleanly

This commit is contained in:
Elliot Glaysher 2019-09-18 11:02:46 -07:00 committed by GitHub
commit ba9bd01e35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 437 additions and 214 deletions

View File

@ -68,6 +68,7 @@ data Cmd
= CmdNew New Opts = CmdNew New Opts
| CmdRun Run Opts | CmdRun Run Opts
| CmdBug Bug | CmdBug Bug
| CmdCon Word16
deriving (Show) deriving (Show)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -270,6 +271,13 @@ bugCmd = fmap CmdBug
$ progDesc "Parse all data in event log" $ progDesc "Parse all data in event log"
) )
conCmd :: Parser Cmd
conCmd = do
port <- argument auto ( metavar "PORT"
<> help "Port of terminal server"
)
pure (CmdCon port)
allFx :: Parser Bug allFx :: Parser Bug
allFx = do allFx = do
bPierPath <- strArgument (metavar "PIER" <> help "Path to pier") bPierPath <- strArgument (metavar "PIER" <> help "Path to pier")
@ -286,3 +294,6 @@ cmd = subparser
<> command "bug" ( info (bugCmd <**> helper) <> command "bug" ( info (bugCmd <**> helper)
$ progDesc "Run a debugging sub-command." $ progDesc "Run a debugging sub-command."
) )
<> command "con" ( info (conCmd <**> helper)
$ progDesc "Connect a terminal to a running urbit."
)

View File

@ -113,6 +113,7 @@ import qualified System.IO.LockFile.Internal as Lock
import qualified Vere.Log as Log import qualified Vere.Log as Log
import qualified Vere.Pier as Pier import qualified Vere.Pier as Pier
import qualified Vere.Serf as Serf import qualified Vere.Serf as Serf
import qualified Vere.Term as Term
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -341,6 +342,14 @@ main = do
CLI.CmdBug (CLI.ValidatePill pax pil seq) -> testPill pax pil seq CLI.CmdBug (CLI.ValidatePill pax pil seq) -> testPill pax pil seq
CLI.CmdBug (CLI.ValidateEvents pax f l) -> checkEvs pax f l CLI.CmdBug (CLI.ValidateEvents pax f l) -> checkEvs pax f l
CLI.CmdBug (CLI.ValidateFX pax f l) -> checkFx pax f l CLI.CmdBug (CLI.ValidateFX pax f l) -> checkFx pax f l
CLI.CmdCon port -> connTerm port
--------------------------------------------------------------------------------
connTerm :: e. HasLogFunc e => Word16 -> RIO e ()
connTerm port =
Term.runTerminalClient (fromIntegral port)
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------

View File

@ -302,23 +302,25 @@ instance FromNoun Ev where
-- Short Event Names ----------------------------------------------------------- -- Short Event Names -----------------------------------------------------------
getSpinnerNameForEvent :: Ev -> Maybe String {-
In the case of the user hitting enter, the cause is technically a
terminal event, but we don't display any name because the cause is
really the user.
-}
getSpinnerNameForEvent :: Ev -> Maybe Text
getSpinnerNameForEvent = \case getSpinnerNameForEvent = \case
EvVane _ -> Nothing EvVane _ -> Nothing
EvBlip b -> case b of EvBlip b -> case b of
BlipEvAmes _ -> Just "ames" BlipEvAmes _ -> Just "ames"
BlipEvArvo _ -> Just "arvo" BlipEvArvo _ -> Just "arvo"
BlipEvBehn _ -> Just "behn" BlipEvBehn _ -> Just "behn"
BlipEvBoat _ -> Just "boat" BlipEvBoat _ -> Just "boat"
BlipEvHttpClient _ -> Just "iris" BlipEvHttpClient _ -> Just "iris"
BlipEvHttpServer _ -> Just "eyre" BlipEvHttpServer _ -> Just "eyre"
BlipEvNewt _ -> Just "newt" BlipEvNewt _ -> Just "newt"
BlipEvSync _ -> Just "clay" BlipEvSync _ -> Just "clay"
BlipEvTerm t -> case t of BlipEvTerm t | isRet t -> Nothing
TermEvBelt _ belt -> case belt of BlipEvTerm t -> Just "term"
-- In the case of the user hitting enter, the cause is technically a where
-- terminal event, but we don't display any name because the cause is isRet (TermEvBelt _ (Ret ())) = True
-- really the user. isRet _ = False
Ret () -> Nothing
_ -> Just "term"
_ -> Just "term"

View File

@ -44,6 +44,9 @@ wsConn :: (FromNoun i, ToNoun o, Show o, HasLogFunc e)
-> RIO e () -> RIO e ()
wsConn pre inp out wsc = do wsConn pre inp out wsc = do
env <- ask env <- ask
logWarn (pre <> "(wcConn) Connected!")
writer <- io $ async $ runRIO env $ forever $ do writer <- io $ async $ runRIO env $ forever $ do
logWarn (pre <> "(wsConn) Waiting for data.") logWarn (pre <> "(wsConn) Waiting for data.")
byt <- io $ toStrict <$> WS.receiveData wsc byt <- io $ toStrict <$> WS.receiveData wsc
@ -81,11 +84,11 @@ wsClient por = do
out <- io $ newTBMChanIO 5 out <- io $ newTBMChanIO 5
con <- pure (mkConn inp out) con <- pure (mkConn inp out)
logDebug "(wsClie) Trying to connect" logDebug "NOUNSERV (wsClie) Trying to connect"
tid <- io $ async tid <- io $ async
$ WS.runClient "127.0.0.1" por "/" $ WS.runClient "127.0.0.1" por "/"
$ runRIO env . wsConn "(wsClie) " inp out $ runRIO env . wsConn "NOUNSERV (wsClie) " inp out
pure $ Client con tid pure $ Client con tid
@ -97,18 +100,18 @@ wsServer = do
con <- io $ newTBMChanIO 5 con <- io $ newTBMChanIO 5
let app pen = do let app pen = do
logError "(wsServer) Got connection! Accepting" logError "NOUNSERV (wsServer) Got connection! Accepting"
wsc <- io $ WS.acceptRequest pen wsc <- io $ WS.acceptRequest pen
inp <- io $ newTBMChanIO 5 inp <- io $ newTBMChanIO 5
out <- io $ newTBMChanIO 5 out <- io $ newTBMChanIO 5
atomically $ writeTBMChan con (mkConn inp out) atomically $ writeTBMChan con (mkConn inp out)
wsConn "(wsServ) " inp out wsc wsConn "NOUNSERV (wsServ) " inp out wsc
tid <- async $ do tid <- async $ do
env <- ask env <- ask
logError "(wsServer) Starting server" logError "NOUNSERV (wsServer) Starting server"
io $ WS.runServer "127.0.0.1" 9999 (runRIO env . app) io $ WS.runServer "127.0.0.1" 9999 (runRIO env . app)
logError "(wsServer) Server died" logError "NOUNSERV (wsServer) Server died"
atomically $ closeTBMChan con atomically $ closeTBMChan con
pure $ Server (readTBMChan con) tid 9999 pure $ Server (readTBMChan con) tid 9999

View File

@ -18,14 +18,17 @@ import Vere.Http.Client (client)
import Vere.Http.Server (serv) import Vere.Http.Server (serv)
import Vere.Log (EventLog) import Vere.Log (EventLog)
import Vere.Serf (Serf, SerfState(..), doJob, sStderr) import Vere.Serf (Serf, SerfState(..), doJob, sStderr)
import Vere.Term
import RIO.Directory import RIO.Directory
import qualified System.Entropy as Ent import qualified System.Console.Terminal.Size as TSize
import qualified Urbit.Time as Time import qualified System.Entropy as Ent
import qualified Vere.Log as Log import qualified Urbit.Time as Time
import qualified Vere.Serf as Serf import qualified Vere.Log as Log
import qualified Vere.Serf as Serf
import qualified Vere.Term as Term
import qualified Vere.Term.API as Term
import qualified Vere.Term.Demux as Term
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
@ -132,38 +135,71 @@ resumed top flags = do
-- Run Pier -------------------------------------------------------------------- -- Run Pier --------------------------------------------------------------------
acquireWorker :: RIO e () -> RAcquire e (Async ())
acquireWorker act = mkRAcquire (async act) cancel
pier :: e. HasLogFunc e pier :: e. HasLogFunc e
=> FilePath => FilePath
-> Maybe Port -> Maybe Port
-> (Serf e, EventLog, SerfState) -> (Serf e, EventLog, SerfState)
-> RAcquire e () -> RAcquire e ()
pier pierPath mPort (serf, log, ss) = do pier pierPath mPort (serf, log, ss) = do
computeQ <- newTQueueIO :: RAcquire e (TQueue Ev) computeQ <- newTQueueIO
persistQ <- newTQueueIO :: RAcquire e (TQueue (Job, FX)) persistQ <- newTQueueIO
executeQ <- newTQueueIO :: RAcquire e (TQueue FX) executeQ <- newTQueueIO
saveM <- newEmptyTMVarIO
shutdownM <- newEmptyTMVarIO
saveM <- newEmptyTMVarIO :: RAcquire e (TMVar ())
shutdownM <- newEmptyTMVarIO :: RAcquire e (TMVar ())
let shutdownEvent = putTMVar shutdownM () let shutdownEvent = putTMVar shutdownM ()
inst <- io (KingId . UV . fromIntegral <$> randomIO @Word16) inst <- io (KingId . UV . fromIntegral <$> randomIO @Word16)
terminalSystem <- initializeLocalTerminal (sz, local) <- Term.localClient
swapMVar (sStderr serf) (tsStderr terminalSystem)
(waitExternalTerm, termServPort) <- Term.termServer
(demux, muxed) <- atomically $ do
res <- Term.mkDemux
Term.addDemux local res
pure (res, Term.useDemux res)
rio $ logInfo $ display $
"TERMSERV Terminal Server running on port: " <> tshow termServPort
let listenLoop = do
logTrace "TERMSERV Waiting for external terminal."
ok <- atomically $ do
waitExternalTerm >>= \case
Nothing -> pure False
Just ext -> Term.addDemux ext demux >> pure True
if ok
then do logTrace "TERMSERV External terminal connected"
listenLoop
else logTrace "TERMSERV Termainal server is dead"
acquireWorker listenLoop
swapMVar (sStderr serf) (atomically . Term.trace muxed)
let ship = who (Log.identity log) let ship = who (Log.identity log)
let (bootEvents, startDrivers) = let (bootEvents, startDrivers) =
drivers pierPath inst ship mPort (writeTQueue computeQ) drivers pierPath inst ship mPort
shutdownEvent terminalSystem (writeTQueue computeQ)
shutdownEvent
(sz, muxed)
io $ atomically $ for_ bootEvents (writeTQueue computeQ) io $ atomically $ for_ bootEvents (writeTQueue computeQ)
tExe <- startDrivers >>= router (readTQueue executeQ) tExe <- startDrivers >>= router (readTQueue executeQ)
tDisk <- runPersist log persistQ (writeTQueue executeQ) tDisk <- runPersist log persistQ (writeTQueue executeQ)
tCpu <- runCompute serf ss (readTQueue computeQ) (takeTMVar saveM) tCpu <- runCompute serf ss
(takeTMVar shutdownM) (tsShowSpinner terminalSystem) (readTQueue computeQ)
(tsHideSpinner terminalSystem) (writeTQueue persistQ) (takeTMVar saveM)
(takeTMVar shutdownM)
(Term.spin muxed)
(Term.stopSpin muxed)
(writeTQueue persistQ)
tSaveSignal <- saveSignalThread saveM tSaveSignal <- saveSignalThread saveM
@ -205,7 +241,7 @@ data Drivers e = Drivers
drivers :: HasLogFunc e drivers :: HasLogFunc e
=> FilePath -> KingId -> Ship -> Maybe Port -> (Ev -> STM ()) -> STM() => FilePath -> KingId -> Ship -> Maybe Port -> (Ev -> STM ()) -> STM()
-> TerminalSystem e -> (TSize.Window Word, Term.Client)
-> ([Ev], RAcquire e (Drivers e)) -> ([Ev], RAcquire e (Drivers e))
drivers pierPath inst who mPort plan shutdownSTM termSys = drivers pierPath inst who mPort plan shutdownSTM termSys =
(initialEvents, runDrivers) (initialEvents, runDrivers)
@ -215,7 +251,7 @@ drivers pierPath inst who mPort plan shutdownSTM termSys =
(httpBorn, runHttp) = serv pierPath inst plan (httpBorn, runHttp) = serv pierPath inst plan
(clayBorn, runClay) = clay pierPath inst plan (clayBorn, runClay) = clay pierPath inst plan
(irisBorn, runIris) = client inst plan (irisBorn, runIris) = client inst plan
(termBorn, runTerm) = term termSys shutdownSTM pierPath inst plan (termBorn, runTerm) = Term.term termSys shutdownSTM pierPath inst plan
initialEvents = mconcat [behnBorn, clayBorn, amesBorn, httpBorn, initialEvents = mconcat [behnBorn, clayBorn, amesBorn, httpBorn,
termBorn, irisBorn] termBorn, irisBorn]
runDrivers = do runDrivers = do
@ -286,7 +322,7 @@ runCompute :: ∀e. HasLogFunc e
-> STM Ev -> STM Ev
-> STM () -> STM ()
-> STM () -> STM ()
-> (Maybe String -> STM ()) -> (Maybe Text -> STM ())
-> STM () -> STM ()
-> ((Job, FX) -> STM ()) -> ((Job, FX) -> STM ())
-> RAcquire e (Async ()) -> RAcquire e (Async ())

View File

@ -1,43 +1,44 @@
module Vere.Term (initializeLocalTerminal, term, TerminalSystem(..)) where module Vere.Term
( module Term
import Arvo hiding (Term) , localClient
import Urbit.Time , connectToRemote
import UrbitPrelude hiding (getCurrentTime) , runTerminalClient
import Vere.Pier.Types , termServer
, term
) where
import Arvo hiding (Term)
import Data.Char import Data.Char
import Data.List ((!!))
import Foreign.Marshal.Alloc import Foreign.Marshal.Alloc
import Foreign.Ptr import Foreign.Ptr
import Foreign.Storable import Foreign.Storable
import RIO.FilePath
import System.Posix.IO import System.Posix.IO
import System.Posix.Terminal import System.Posix.Terminal
import Urbit.Time
import UrbitPrelude hiding (getCurrentTime)
import Vere.Pier.Types
import RIO.Directory (createDirectoryIfMissing) import Data.List ((!!))
import RIO.FilePath import RIO.Directory (createDirectoryIfMissing)
import System.Console.Terminfo.Base import Vere.Term.API (Client(Client))
import Data.ByteString.Internal import qualified Data.ByteString.Internal as BS
import qualified Data.ByteString.UTF8 as BS
import qualified System.Console.Terminal.Size as TSize
import qualified System.Console.Terminfo.Base as T
import qualified Vere.NounServ as Serv
import qualified Vere.Term.API as Term
import qualified Data.ByteString as B
import qualified Data.ByteString.UTF8 as UTF8
-- Types ----------------------------------------------------------------------- -- Types -----------------------------------------------------------------------
-- Output to the attached terminal is either a series of vere blits, or it is an
-- injected printf line from the interpreter.
data VereOutput = VereBlitOutput [Blit]
| VerePrintOutput String
| VereBlankLine
| VereShowSpinner (Maybe String)
| VereHideSpinner
-- All stateful data in the printing to stdOutput. -- All stateful data in the printing to stdOutput.
data LineState = LineState data LineState = LineState
{ lsLine :: String { lsLine :: Text
, lsCurPos :: Int , lsCurPos :: Int
, lsSpinTimer :: Maybe (Async ()) , lsSpinTimer :: Maybe (Async ())
, lsSpinCause :: Maybe String , lsSpinCause :: Maybe Text
, lsSpinFirstRender :: Bool , lsSpinFirstRender :: Bool
, lsSpinFrame :: Int , lsSpinFrame :: Int
, lsPrevEndTime :: Wen , lsPrevEndTime :: Wen
@ -52,21 +53,7 @@ data ReadData = ReadData
, rdUTF8width :: Int , rdUTF8width :: Int
} }
-- Minimal terminal interface. -- Private data to the Client that we keep around for stop().
--
-- A Terminal can either be local or remote. Either way, the Terminal, from the
-- view of the caller, a terminal has a thread which when exits indicates that
-- the session is over, and has a general in/out queue in the types of the
-- vere/arvo interface.
data TerminalSystem e = TerminalSystem
{ tsReadQueue :: TQueue Belt
, tsWriteQueue :: TQueue VereOutput
, tsStderr :: Text -> RIO e ()
, tsShowSpinner :: Maybe String -> STM ()
, tsHideSpinner :: STM ()
}
-- Private data to the TerminalSystem that we keep around for stop().
data Private = Private data Private = Private
{ pReaderThread :: Async () { pReaderThread :: Async ()
, pWriterThread :: Async () , pWriterThread :: Async ()
@ -76,6 +63,9 @@ data Private = Private
-- Utils ----------------------------------------------------------------------- -- Utils -----------------------------------------------------------------------
termText :: Text -> T.TermOutput
termText = T.termText . unpack
initialBlew w h = EvBlip $ BlipEvTerm $ TermEvBlew (UD 1, ()) w h initialBlew w h = EvBlip $ BlipEvTerm $ TermEvBlew (UD 1, ()) w h
initialHail = EvBlip $ BlipEvTerm $ TermEvHail (UD 1, ()) () initialHail = EvBlip $ BlipEvTerm $ TermEvHail (UD 1, ()) ()
@ -83,10 +73,14 @@ initialHail = EvBlip $ BlipEvTerm $ TermEvHail (UD 1, ()) ()
-- Version one of this is punting on the ops_u.dem flag: whether we're running -- Version one of this is punting on the ops_u.dem flag: whether we're running
-- in daemon mode. -- in daemon mode.
spinners = ['|', '/', '-', '\\'] spinners :: [Text]
spinners = ["|", "/", "-", "\\"]
leftBracket = ['«'] leftBracket :: Text
rightBracket = ['»'] leftBracket = "«"
rightBracket :: Text
rightBracket = "»"
_spin_cool_us = 500000 _spin_cool_us = 500000
_spin_warm_us = 50000 _spin_warm_us = 50000
@ -95,10 +89,10 @@ _spin_idle_us = 500000
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
runMaybeTermOutput :: Terminal -> (Terminal -> Maybe TermOutput) -> RIO e () runMaybeTermOutput :: T.Terminal -> (T.Terminal -> Maybe T.TermOutput) -> RIO e ()
runMaybeTermOutput t getter = case (getter t) of runMaybeTermOutput t getter = case (getter t) of
Nothing -> pure () Nothing -> pure ()
Just x -> io $ runTermOutput t x Just x -> io $ T.runTermOutput t x
rioAllocaBytes :: (MonadIO m, MonadUnliftIO m) rioAllocaBytes :: (MonadIO m, MonadUnliftIO m)
=> Int -> (Ptr a -> m b) -> m b => Int -> (Ptr a -> m b) -> m b
@ -116,50 +110,100 @@ isTerminalBlit _ = True
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
-- Initializes the generalized input/output parts of the terminal. {-
-- TODO XX HACK: We don't have any good way of handling client
initializeLocalTerminal :: forall e. HasLogFunc e disconnect, so we just retry. This will probably waste CPU.
=> RAcquire e (TerminalSystem e) -}
initializeLocalTerminal = fst <$> mkRAcquire start stop termServer :: e. HasLogFunc e
=> RAcquire e (STM (Maybe Client), Port)
termServer = fst <$> mkRAcquire start stop
where where
start :: HasLogFunc e => RIO e (TerminalSystem e, Private) stop = cancel . snd
start = do start = do
-- Initialize the writing side of the terminal serv <- Serv.wsServer @Belt @[Term.Ev]
--
pTerminal <- io $ setupTermFromEnv
-- TODO: We still need to actually get the size from the terminal somehow.
tsWriteQueue <- newTQueueIO let getClient = do
spinnerMVar <- newEmptyTMVarIO Serv.sAccept serv <&> \case
Nothing -> Nothing
Just c -> Just $ Client
{ give = Serv.cSend c
, take = Serv.cRecv c >>= \case
Nothing -> empty
Just ev -> pure ev
}
pure ( (getClient, Port $ fromIntegral $ Serv.sData serv)
, Serv.sAsync serv
)
connectToRemote :: e. HasLogFunc e
=> Port
-> Client
-> RAcquire e (Async (), Async ())
connectToRemote port local = mkRAcquire start stop
where
stop (x, y) = cancel x >> cancel y
start = do
Serv.Client{..} <- Serv.wsClient (fromIntegral port)
ferry <- async $ forever $ atomically $ asum
[ Term.take local >>= Serv.cSend cConn
, Serv.cRecv cConn >>= \case
Nothing -> empty
Just ev -> Term.give local ev
]
pure (ferry, cAsync)
runTerminalClient :: e. HasLogFunc e => Port -> RIO e ()
runTerminalClient port = runRAcquire $ do
(tsize, local) <- localClient
(tid1, tid2) <- connectToRemote port local
atomically $ waitSTM tid1 <|> waitSTM tid2
where
runRAcquire :: RAcquire e () -> RIO e ()
runRAcquire act = rwith act $ const $ pure ()
{-
Initializes the generalized input/output parts of the terminal.
-}
localClient :: e. HasLogFunc e => RAcquire e (TSize.Window Word, Client)
localClient = fst <$> mkRAcquire start stop
where
start :: HasLogFunc e => RIO e ((TSize.Window Word, Client), Private)
start = do
pTerminal <- io $ T.setupTermFromEnv
tsWriteQueue <- newTQueueIO
spinnerMVar <- newEmptyTMVarIO
pWriterThread <- pWriterThread <-
asyncBound (writeTerminal pTerminal tsWriteQueue spinnerMVar) asyncBound (writeTerminal pTerminal tsWriteQueue spinnerMVar)
pPreviousConfiguration <- io $ getTerminalAttributes stdInput pPreviousConfiguration <- io $ getTerminalAttributes stdInput
-- Create a new configuration where we put the terminal in raw mode and -- Create a new configuration where we put the terminal in raw mode and
-- disable a bunch of preprocessing. -- disable a bunch of preprocessing.
let newTermSettings = let newTermSettings = flip withTime 0
flip withTime 0 . $ flip withMinInput 1
flip withMinInput 1 $ $ foldl' withoutMode pPreviousConfiguration
foldl' withoutMode pPreviousConfiguration disabledFlags $ disabledFlags
io $ setTerminalAttributes stdInput newTermSettings Immediately io $ setTerminalAttributes stdInput newTermSettings Immediately
tsReadQueue <- newTQueueIO tsReadQueue <- newTQueueIO
pReaderThread <- asyncBound pReaderThread <- asyncBound
(readTerminal tsReadQueue tsWriteQueue (bell tsWriteQueue)) (readTerminal tsReadQueue tsWriteQueue (bell tsWriteQueue))
let tsStderr = \txt -> let client = Client { take = readTQueue tsReadQueue
atomically $ writeTQueue tsWriteQueue $ VerePrintOutput $ unpack txt , give = writeTQueue tsWriteQueue
}
let tsShowSpinner = \str -> tsize <- io $ TSize.size <&> fromMaybe (TSize.Window 80 24)
writeTQueue tsWriteQueue $ VereShowSpinner str
let tsHideSpinner = writeTQueue tsWriteQueue $ VereHideSpinner
pure (TerminalSystem{..}, Private{..}) pure ((tsize, client), Private{..})
stop :: HasLogFunc e stop :: HasLogFunc e
=> (TerminalSystem e, Private) -> RIO e () => ((TSize.Window Word, Client), Private) -> RIO e ()
stop (TerminalSystem{..}, Private{..}) = do stop ((_, Client{..}), Private{..}) = do
-- Note that we don't `cancel pReaderThread` here. This is a deliberate -- Note that we don't `cancel pReaderThread` here. This is a deliberate
-- decision because fdRead calls into a native function which the runtime -- decision because fdRead calls into a native function which the runtime
-- can't kill. If we were to cancel here, the internal `waitCatch` would -- can't kill. If we were to cancel here, the internal `waitCatch` would
@ -173,21 +217,27 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
-- take the terminal out of raw mode -- take the terminal out of raw mode
io $ setTerminalAttributes stdInput pPreviousConfiguration Immediately io $ setTerminalAttributes stdInput pPreviousConfiguration Immediately
-- A list of terminal flags that we disable {-
disabledFlags = [ A list of terminal flags that we disable.
-- lflag
StartStopOutput, KeyboardInterrupts, EnableEcho, EchoLF, TODO: Terminal library missing CSIZE?
ProcessInput, ExtendedFunctions, -}
-- iflag disabledFlags :: [TerminalMode]
MapCRtoLF, CheckParity, StripHighBit, disabledFlags = [ StartStopOutput
-- cflag, todo: Terminal library missing CSIZE? , KeyboardInterrupts
EnableParity, , EnableEcho
-- oflag , EchoLF
ProcessOutput , ProcessInput
] , ExtendedFunctions
, MapCRtoLF
, CheckParity
, StripHighBit
, EnableParity
, ProcessOutput
]
getCap term cap = getCap term cap =
getCapability term (tiGetOutput1 cap) :: Maybe TermOutput T.getCapability term (T.tiGetOutput1 cap) :: Maybe T.TermOutput
vtClearScreen t = getCap t "clear" vtClearScreen t = getCap t "clear"
vtClearToBegin t = getCap t "el" vtClearToBegin t = getCap t "el"
@ -209,48 +259,49 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
-- Writes data to the terminal. Both the terminal reading, normal logging, -- Writes data to the terminal. Both the terminal reading, normal logging,
-- and effect handling can all emit bytes which go to the terminal. -- and effect handling can all emit bytes which go to the terminal.
writeTerminal :: Terminal -> TQueue VereOutput -> TMVar () -> RIO e () writeTerminal :: T.Terminal -> TQueue [Term.Ev] -> TMVar () -> RIO e ()
writeTerminal t q spinner = do writeTerminal t q spinner = do
currentTime <- io $ now currentTime <- io $ now
loop (LineState "" 0 Nothing Nothing True 0 currentTime) loop (LineState "" 0 Nothing Nothing True 0 currentTime)
where where
loop ls@LineState{..} = do writeBlank :: LineState -> RIO e LineState
x <- atomically $ writeBlank ls = do
Right <$> readTQueue q <|> io $ T.runTermOutput t $ termText "\r\n"
Left <$> takeTMVar spinner pure ls
case x of
Right (VereBlitOutput blits) -> do
ls <- foldM (writeBlit t) ls blits
loop ls
Right (VerePrintOutput p) -> do
io $ runTermOutput t $ termText "\r"
runMaybeTermOutput t vtClearToBegin
io $ runTermOutput t $ termText p
ls <- termRefreshLine t ls
loop ls
Right VereBlankLine -> do
io $ runTermOutput t $ termText "\r\n"
loop ls
Right (VereShowSpinner txt) -> do
current <- io $ now
-- Figure out how long to wait to show the spinner. When we don't
-- have a vane name to display, we assume its a user action and
-- trigger immediately. Otherwise, if we receive an event shortly
-- after a previous spin, use a shorter delay to avoid giving the
-- impression of a half-idle system.
let delay = case txt of
Nothing -> 0
Just _ ->
if (gap current lsPrevEndTime ^. microSecs) <
_spin_idle_us
then _spin_warm_us
else _spin_cool_us
spinTimer <- async $ spinnerHeartBeat delay _spin_rate_us spinner writeTrace :: LineState -> Text -> RIO e LineState
loop ls { lsSpinTimer = Just spinTimer, writeTrace ls p = do
lsSpinCause = txt, io $ T.runTermOutput t $ termText "\r"
lsSpinFirstRender = True } runMaybeTermOutput t vtClearToBegin
Right VereHideSpinner -> do io $ T.runTermOutput t $ termText p
termRefreshLine t ls
{-
Figure out how long to wait to show the spinner. When we
don't have a vane name to display, we assume its a user
action and trigger immediately. Otherwise, if we receive an
event shortly after a previous spin, use a shorter delay to
avoid giving the impression of a half-idle system.
-}
doSpin :: LineState -> Maybe Text -> RIO e LineState
doSpin ls@LineState{..} mTxt = do
current <- io $ now
delay <- pure $ case mTxt of
Nothing -> 0
Just _ ->
if (gap current lsPrevEndTime ^. microSecs) < _spin_idle_us
then _spin_warm_us
else _spin_cool_us
spinTimer <- async $ spinnerHeartBeat delay _spin_rate_us spinner
pure $ ls { lsSpinTimer = Just spinTimer
, lsSpinCause = mTxt
, lsSpinFirstRender = True
}
unspin :: LineState -> RIO e LineState
unspin ls@LineState{..} = do
maybe (pure ()) cancel lsSpinTimer maybe (pure ()) cancel lsSpinTimer
-- We do a final flush of the spinner mvar to ensure we don't -- We do a final flush of the spinner mvar to ensure we don't
-- have a lingering signal which will redisplay the spinner after -- have a lingering signal which will redisplay the spinner after
@ -264,41 +315,55 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
else pure ls else pure ls
endTime <- io $ now endTime <- io $ now
loop ls { lsSpinTimer = Nothing, lsPrevEndTime = endTime } pure $ ls { lsSpinTimer = Nothing, lsPrevEndTime = endTime }
Left () -> do
let spinner = [spinners !! lsSpinFrame] ++ case lsSpinCause of
Nothing -> []
Just str -> leftBracket ++ str ++ rightBracket
io $ runTermOutput t $ termText spinner execEv :: LineState -> Term.Ev -> RIO e LineState
termSpinnerMoveLeft t (length spinner) execEv ls = \case
Term.Blits bs -> foldM (writeBlit t) ls bs
Term.Trace p -> writeTrace ls (unCord p)
Term.Blank -> writeBlank ls
Term.Spinr (Just txt) -> doSpin ls (unCord <$> txt)
Term.Spinr Nothing -> unspin ls
loop ls { lsSpinFirstRender = False, spin :: LineState -> RIO e LineState
lsSpinFrame = (lsSpinFrame + 1) `mod` (length spinners) spin ls@LineState{..} = do
let spinner = (spinners !! lsSpinFrame) ++ case lsSpinCause of
Nothing -> ""
Just str -> leftBracket ++ str ++ rightBracket
io $ T.runTermOutput t $ termText spinner
termSpinnerMoveLeft t (length spinner)
let newFrame = (lsSpinFrame + 1) `mod` (length spinners)
pure $ ls { lsSpinFirstRender = False
, lsSpinFrame = newFrame
} }
loop :: LineState -> RIO e ()
loop ls = do
join $ atomically $ asum
[ readTQueue q >>= pure . (foldM execEv ls >=> loop)
, takeTMVar spinner >> pure (spin ls >>= loop)
]
-- Writes an individual blit to the screen -- Writes an individual blit to the screen
writeBlit :: Terminal -> LineState -> Blit -> RIO e LineState writeBlit :: T.Terminal -> LineState -> Blit -> RIO e LineState
writeBlit t ls = \case writeBlit t ls = \case
Bel () -> do Bel () -> do runMaybeTermOutput t vtSoundBell
runMaybeTermOutput t vtSoundBell pure ls
pure ls Clr () -> do runMaybeTermOutput t vtClearScreen
Clr () -> do termRefreshLine t ls
runMaybeTermOutput t vtClearScreen Hop w -> termShowCursor t ls (fromIntegral w)
termRefreshLine t ls Lin c -> do ls2 <- termShowClear t ls
(Hop w) -> do termShowLine t ls2 (pack c)
termShowCursor t ls (fromIntegral w) Mor () -> termShowMore t ls
(Lin c) -> do Sag path noun -> pure ls
ls2 <- termShowClear t ls Sav path atom -> pure ls
termShowLine t ls2 (pack c) Url url -> pure ls
(Mor ()) -> do
termShowMore t ls
(Sag path noun) -> pure ls
(Sav path atom) -> pure ls
(Url url) -> pure ls
-- Moves the cursor to the requested position -- Moves the cursor to the requested position
termShowCursor :: Terminal -> LineState -> Int -> RIO e LineState termShowCursor :: T.Terminal -> LineState -> Int -> RIO e LineState
termShowCursor t ls@LineState{..} {-line pos)-} newPos = do termShowCursor t ls@LineState{..} {-line pos)-} newPos = do
if newPos < lsCurPos then do if newPos < lsCurPos then do
replicateM_ (lsCurPos - newPos) (runMaybeTermOutput t vtParmLeft) replicateM_ (lsCurPos - newPos) (runMaybeTermOutput t vtParmLeft)
@ -309,33 +374,32 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
else else
pure ls pure ls
-- Moves the cursor left without any mutation of the LineState. Used only -- Moves the cursor left without any mutation of the LineState. Used only
-- in cursor spinning. -- in cursor spinning.
termSpinnerMoveLeft :: Terminal -> Int -> RIO e () termSpinnerMoveLeft :: T.Terminal -> Int -> RIO e ()
termSpinnerMoveLeft t count = termSpinnerMoveLeft t count =
replicateM_ count (runMaybeTermOutput t vtParmLeft) replicateM_ count (runMaybeTermOutput t vtParmLeft)
-- Displays and sets the current line -- Displays and sets the current line
termShowLine :: Terminal -> LineState -> String -> RIO e LineState termShowLine :: T.Terminal -> LineState -> Text -> RIO e LineState
termShowLine t ls newStr = do termShowLine t ls newStr = do
io $ runTermOutput t $ termText newStr io $ T.runTermOutput t $ termText newStr
pure ls { lsLine = newStr, lsCurPos = (length newStr) } pure ls { lsLine = newStr, lsCurPos = (length newStr) }
termShowClear :: Terminal -> LineState -> RIO e LineState termShowClear :: T.Terminal -> LineState -> RIO e LineState
termShowClear t ls = do termShowClear t ls = do
io $ runTermOutput t $ termText "\r" io $ T.runTermOutput t $ termText "\r"
runMaybeTermOutput t vtClearToBegin runMaybeTermOutput t vtClearToBegin
pure ls { lsLine = "", lsCurPos = 0 } pure ls { lsLine = "", lsCurPos = 0 }
-- New Current Line -- New Current Line
termShowMore :: Terminal -> LineState -> RIO e LineState termShowMore :: T.Terminal -> LineState -> RIO e LineState
termShowMore t ls = do termShowMore t ls = do
io $ runTermOutput t $ termText "\r\n" io $ T.runTermOutput t $ termText "\r\n"
pure ls { lsLine = "", lsCurPos = 0 } pure ls { lsLine = "", lsCurPos = 0 }
-- Redraw the current LineState, maintaining the current curpos -- Redraw the current LineState, maintaining the current curpos
termRefreshLine :: Terminal -> LineState -> RIO e LineState termRefreshLine :: T.Terminal -> LineState -> RIO e LineState
termRefreshLine t ls = do termRefreshLine t ls = do
let line = (lsLine ls) let line = (lsLine ls)
curPos = (lsCurPos ls) curPos = (lsCurPos ls)
@ -344,8 +408,8 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
termShowCursor t ls curPos termShowCursor t ls curPos
-- ring my bell -- ring my bell
bell :: TQueue VereOutput -> RIO e () bell :: TQueue [Term.Ev] -> RIO e ()
bell q = atomically $ writeTQueue q $ VereBlitOutput [Bel ()] bell q = atomically $ writeTQueue q $ [Term.Blits [Bel ()]]
-- Reads data from stdInput and emit the proper effect -- Reads data from stdInput and emit the proper effect
-- --
@ -356,9 +420,9 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
-- A better way to do this would be to get some sort of epoll on stdInput, -- A better way to do this would be to get some sort of epoll on stdInput,
-- since that's kinda closer to what libuv does? -- since that's kinda closer to what libuv does?
readTerminal :: forall e. HasLogFunc e readTerminal :: forall e. HasLogFunc e
=> TQueue Belt -> TQueue VereOutput -> (RIO e ()) -> RIO e () => TQueue Belt -> TQueue [Term.Ev] -> (RIO e ()) -> RIO e ()
readTerminal rq wq bell = readTerminal rq wq bell =
rioAllocaBytes 1 $ \ buf -> loop (ReadData buf False False B.empty 0) rioAllocaBytes 1 $ \ buf -> loop (ReadData buf False False mempty 0)
where where
loop :: ReadData -> RIO e () loop :: ReadData -> RIO e ()
loop rd@ReadData{..} = do loop rd@ReadData{..} = do
@ -375,7 +439,7 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
Right _ -> do Right _ -> do
w <- io $ peek rdBuf w <- io $ peek rdBuf
-- print ("{" ++ (show w) ++ "}") -- print ("{" ++ (show w) ++ "}")
let c = w2c w let c = BS.w2c w
if rdEscape then if rdEscape then
if rdBracket then do if rdBracket then do
case c of case c of
@ -404,13 +468,13 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
rd@ReadData{..} <- pure rd { rdUTF8 = snoc rdUTF8 w } rd@ReadData{..} <- pure rd { rdUTF8 = snoc rdUTF8 w }
if length rdUTF8 /= rdUTF8width then loop rd if length rdUTF8 /= rdUTF8width then loop rd
else do else do
case UTF8.decode rdUTF8 of case BS.decode rdUTF8 of
Nothing -> Nothing ->
error "empty utf8 accumulation buffer" error "empty utf8 accumulation buffer"
Just (c, bytes) | bytes /= rdUTF8width -> Just (c, bytes) | bytes /= rdUTF8width ->
error "utf8 character size mismatch?!" error "utf8 character size mismatch?!"
Just (c, bytes) -> sendBelt $ Txt $ Tour $ [c] Just (c, bytes) -> sendBelt $ Txt $ Tour $ [c]
loop rd { rdUTF8 = B.empty, rdUTF8width = 0 } loop rd { rdUTF8 = mempty, rdUTF8width = 0 }
else if w >= 32 && w < 127 then do else if w >= 32 && w < 127 then do
sendBelt $ Txt $ Tour $ [c] sendBelt $ Txt $ Tour $ [c]
loop rd loop rd
@ -427,11 +491,11 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
-- ETX (^C) -- ETX (^C)
logDebug $ displayShow "Ctrl-c interrupt" logDebug $ displayShow "Ctrl-c interrupt"
atomically $ do atomically $ do
writeTQueue wq $ VerePrintOutput "interrupt\r\n" writeTQueue wq [Term.Trace "interrupt\r\n"]
writeTQueue rq $ Ctl $ Cord "c" writeTQueue rq $ Ctl $ Cord "c"
loop rd loop rd
else if w <= 26 then do else if w <= 26 then do
sendBelt $ Ctl $ Cord $ pack [w2c (w + 97 - 1)] sendBelt $ Ctl $ Cord $ pack [BS.w2c (w + 97 - 1)]
loop rd loop rd
else if w == 27 then do else if w == 27 then do
loop rd { rdEscape = True } loop rd { rdEscape = True }
@ -450,12 +514,18 @@ initializeLocalTerminal = fst <$> mkRAcquire start stop
-------------------------------------------------------------------------------- --------------------------------------------------------------------------------
term :: forall e. HasLogFunc e term :: forall e. HasLogFunc e
=> TerminalSystem e -> (STM ()) -> FilePath -> KingId -> QueueEv => (TSize.Window Word, Client)
-> (STM ())
-> FilePath
-> KingId
-> QueueEv
-> ([Ev], RAcquire e (EffCb e TermEf)) -> ([Ev], RAcquire e (EffCb e TermEf))
term TerminalSystem{..} shutdownSTM pierPath king enqueueEv = term (tsize, Client{..}) shutdownSTM pierPath king enqueueEv =
(initialEvents, runTerm) (initialEvents, runTerm)
where where
initialEvents = [(initialBlew 80 24), initialHail] TSize.Window wi hi = tsize
initialEvents = [(initialBlew hi wi), initialHail]
runTerm :: RAcquire e (EffCb e TermEf) runTerm :: RAcquire e (EffCb e TermEf)
runTerm = do runTerm = do
@ -467,7 +537,7 @@ term TerminalSystem{..} shutdownSTM pierPath king enqueueEv =
readBelt :: RIO e () readBelt :: RIO e ()
readBelt = forever $ do readBelt = forever $ do
b <- atomically $ readTQueue tsReadQueue b <- atomically take
let blip = EvBlip $ BlipEvTerm $ TermEvBelt (UD 1, ()) $ b let blip = EvBlip $ BlipEvTerm $ TermEvBelt (UD 1, ()) $ b
atomically $ enqueueEv $ blip atomically $ enqueueEv $ blip
@ -475,7 +545,7 @@ term TerminalSystem{..} shutdownSTM pierPath king enqueueEv =
handleEffect = \case handleEffect = \case
TermEfBlit _ blits -> do TermEfBlit _ blits -> do
let (termBlits, fsWrites) = partition isTerminalBlit blits let (termBlits, fsWrites) = partition isTerminalBlit blits
atomically $ writeTQueue tsWriteQueue (VereBlitOutput termBlits) atomically $ give [Term.Blits termBlits]
for_ fsWrites handleFsWrite for_ fsWrites handleFsWrite
TermEfInit _ _ -> pure () TermEfInit _ _ -> pure ()
TermEfLogo path _ -> do TermEfLogo path _ -> do

View File

@ -0,0 +1,41 @@
module Vere.Term.API (Ev(..), Client(..), trace, spin, stopSpin) where
import UrbitPrelude hiding (trace)
import Arvo (Blit, Belt)
-- External Types --------------------------------------------------------------
{-
Input Event for terminal driver:
%blits -- list of blits from arvo.
%trace -- stderr line from runtime.
%blank -- print a blank line
%spinr -- Start or stop the spinner
-}
data Ev = Blits [Blit]
| Trace Cord
| Blank
| Spinr (Maybe (Maybe Cord))
deriving (Show)
data Client = Client
{ take :: STM Belt
, give :: [Ev] -> STM ()
}
deriveNoun ''Ev
-- Utilities -------------------------------------------------------------------
trace :: Client -> Text -> STM ()
trace ts = give ts . singleton . Trace . Cord
spin :: Client -> Maybe Text -> STM ()
spin ts = give ts . singleton . Spinr . Just . fmap Cord
stopSpin :: Client -> STM ()
stopSpin ts = give ts [Spinr Nothing]

View File

@ -0,0 +1,50 @@
{-# OPTIONS_GHC -Wwarn #-}
{-
This allows multiple (zero or more) terminal clients to connect to
the *same* logical arvo terminal. Terminals that connect will be
given full event history since the creation of the demuxer.
-}
module Vere.Term.Demux (Demux, mkDemux, addDemux, useDemux) where
import UrbitPrelude
import Arvo (Belt)
import Vere.Term.API (Client(Client))
import qualified Vere.Term.API as Term
--------------------------------------------------------------------------------
data Demux = Demux
{ dConns :: TVar [Client]
, dStash :: TVar [[Term.Ev]]
}
mkDemux :: STM Demux
mkDemux = Demux <$> newTVar [] <*> newTVar []
addDemux :: Client -> Demux -> STM ()
addDemux conn Demux{..} = do
stash <- concat . reverse <$> readTVar dStash
modifyTVar' dConns (conn:)
Term.give conn stash
useDemux :: Demux -> Client
useDemux d = Client { give = dGive d, take = dTake d }
-- Internal --------------------------------------------------------------------
dGive :: Demux -> [Term.Ev] -> STM ()
dGive Demux{..} ev = do
modifyTVar' dStash (ev:)
conns <- readTVar dConns
for_ conns $ \c -> Term.give c ev
dTake :: Demux -> STM Belt
dTake Demux{..} = do
conns <- readTVar dConns
asum (Term.take <$> conns)

View File

@ -37,6 +37,7 @@ dependencies:
- classy-prelude - classy-prelude
- conduit - conduit
- containers - containers
- data-default
- data-fix - data-fix
- directory - directory
- entropy - entropy
@ -81,6 +82,7 @@ dependencies:
- tasty-th - tasty-th
- template-haskell - template-haskell
- terminal-progress-bar - terminal-progress-bar
- terminal-size
- terminfo - terminfo
- text - text
- these - these
@ -98,7 +100,6 @@ dependencies:
- warp - warp
- warp-tls - warp-tls
- websockets - websockets
- data-default
default-extensions: default-extensions:
- ApplicativeDo - ApplicativeDo