shrub/pkg/hs/urbit-king/lib/Urbit/King/Main.hs

966 lines
31 KiB
Haskell
Raw Normal View History

{- |
# Signal Handling (SIGTERM, SIGINT)
We handle SIGTERM by causing the main thread to raise a `UserInterrupt`
exception. This is the same behavior as SIGINT (the signal sent upon
`CTRL-C`).
The main thread is therefore responsible for handling this exception
and causing everything to shut down properly.
# Crashing and Shutting Down
Rule number one: The King never crashes.
This rule is asperational at the moment, but it needs to become as
close to truth as possible. Shut down ships in extreme cases, but
never let the king go down.
-}
{-
TODO These some old scribbled notes. They don't belong here
anymore. Do something about it.
2020-01-23 07:16:09 +03:00
# Event Pruning
- `king discard-events NUM_EVENTS`: Delete the last `n` events from
the event log.
- `king discard-events-interactive`: Iterate through the events in
the event log, from last to first, pretty-print each event, and
ask if it should be pruned.
# Implement subcommands to test event and effect parsing.
- `king * --collect-fx`: All effects that come from the serf get
written into the `effects` LMDB database.
- `king clear-fx PIER`: Deletes all collected effects.
- `king full-replay PIER`: Replays the whole event log events, print
any failures. On success, replace the snapshot.
# Full Replay -- An Integration Test
- Copy the event log:
- Create a new event log at the destination.
- Stream events from the first event log.
- Parse each event.
- Re-Serialize each event.
- Verify that the round-trip was successful.
- Write the event into the new database.
- Replay the event log at the destination.
- If `--collect-fx` is set, then record effects as well.
- Snapshot.
- Verify that the final mug is the same as it was before.
# Implement Remaining Serf Flags
- `DebugRam`: Memory debugging.
- `DebugCpu`: Profiling
- `CheckCorrupt`: Heap Corruption Tests
- `CheckFatal`: TODO What is this?
- `Verbose`: TODO Just the `-v` flag?
- `DryRun`: TODO Just the `-N` flag?
- `Quiet`: TODO Just the `-q` flag?
- `Hashless`: Don't use hashboard for jets.
-}
module Urbit.King.Main (main) where
2020-01-23 07:16:09 +03:00
import Urbit.Prelude
2020-01-23 07:16:09 +03:00
import Data.Conduit
import Network.HTTP.Client.TLS
import RIO.Directory
import Urbit.Arvo
import Urbit.King.Config
import Urbit.Vere.Dawn
import Urbit.Vere.Pier
import Urbit.Vere.Ports
import Urbit.Vere.Eyre.Multi (multiEyre, MultiEyreConf(..))
import Urbit.Vere.Pier.Types
import Urbit.Vere.Serf
import Urbit.King.App
2020-01-24 09:23:43 +03:00
import Control.Concurrent (myThreadId)
import Control.Exception (AsyncException(UserInterrupt))
import System.Process (system)
import System.IO (hPutStrLn)
import Urbit.Noun.Time (Wen)
import Urbit.Vere.LockFile (lockFile)
2020-01-23 07:16:09 +03:00
2020-02-04 04:27:16 +03:00
import qualified Data.Set as Set
import qualified Data.Text as T
import qualified Network.HTTP.Client as C
import qualified System.Posix.Signals as Sys
import qualified System.Posix.Resource as Sys
2020-02-04 04:27:16 +03:00
import qualified System.ProgressBar as PB
import qualified System.Random as Sys
import qualified Urbit.EventLog.LMDB as Log
2020-02-04 04:27:16 +03:00
import qualified Urbit.King.CLI as CLI
import qualified Urbit.King.EventBrowser as EventBrowser
import qualified Urbit.Ob as Ob
import qualified Urbit.Vere.Pier as Pier
import qualified Urbit.Vere.Serf as Serf
import qualified Urbit.Vere.Term as Term
2020-01-23 07:16:09 +03:00
2020-01-23 07:16:09 +03:00
--------------------------------------------------------------------------------
removeFileIfExists :: HasLogFunc env => FilePath -> RIO env ()
removeFileIfExists pax = do
exists <- doesFileExist pax
when exists $ do
removeFile pax
-- Compile CLI Flags to Pier Configuration -------------------------------------
2020-01-23 07:16:09 +03:00
{-
TODO: This is not all of the flags.
Urbit is basically useless with hashboard, so we ignore that flag.
-}
toSerfFlags :: CLI.Opts -> [Serf.Flag]
2020-01-23 07:16:09 +03:00
toSerfFlags CLI.Opts{..} = catMaybes m
where
m = [ setFrom oQuiet Serf.Quiet
, setFrom oTrace Serf.Trace
, setFrom (oHashless || True) Serf.Hashless
, setFrom oQuiet Serf.Quiet
, setFrom oVerbose Serf.Verbose
, setFrom (oDryRun || isJust oDryFrom) Serf.DryRun
2020-01-23 07:16:09 +03:00
]
setFrom True flag = Just flag
setFrom False _ = Nothing
2020-01-23 07:16:09 +03:00
toPierConfig :: FilePath -> Maybe Text -> CLI.Opts -> PierConfig
toPierConfig pierPath serfExe o@(CLI.Opts{..}) = PierConfig { .. }
where
_pcPierPath = pierPath
_pcDryRun = oDryRun || isJust oDryFrom
_pcSerfExe = serfExe
_pcSerfFlags = toSerfFlags o
2020-01-23 07:16:09 +03:00
toNetworkConfig :: CLI.Opts -> NetworkConfig
toNetworkConfig CLI.Opts {..} = NetworkConfig { .. }
where
dryRun = oDryRun || isJust oDryFrom
offline = dryRun || oOffline
mode = case (dryRun, offline, oLocalhost) of
(True, _ , _ ) -> NMNone
(_ , True, _ ) -> NMNone
(_ , _ , True) -> NMLocalhost
(_ , _ , _ ) -> NMNormal
_ncNetMode = mode
_ncAmesPort = oAmesPort
_ncHttpPort = oHttpPort
_ncHttpsPort = oHttpsPort
_ncLocalPort = oLoopbackPort
_ncNoAmes = oNoAmes
_ncNoHttp = oNoHttp
_ncNoHttps = oNoHttps
2020-01-23 07:16:09 +03:00
logStderr :: HasStderrLogFunc e => RIO LogFunc a -> RIO e a
logStderr action = do
logFunc <- view stderrLogFuncL
runRIO logFunc action
logSlogs :: HasStderrLogFunc e => RIO e (TVar ((Atom, Tank) -> IO ()))
logSlogs = logStderr $ do
env <- ask
newTVarIO (runRIO env . logOther "serf" . display . T.strip . tankToText . snd)
2020-05-13 22:35:57 +03:00
tryBootFromPill
:: Bool
-> Pill
-> Bool
-> Ship
-> LegacyBootEvent
-> Feed
2020-05-13 22:35:57 +03:00
-> RIO PierEnv ()
tryBootFromPill oExit pill lite ship boot feed = do
mStart <- newEmptyMVar
vSlog <- logSlogs
runOrExitImmediately vSlog (bootedPier vSlog) oExit mStart []
where
bootedPier vSlog = do
view pierPathL >>= lockFile
rio $ logInfo "Starting boot"
sls <- Pier.booted vSlog pill lite ship boot feed
rio $ logInfo "Completed boot"
pure sls
2020-01-23 07:16:09 +03:00
runOrExitImmediately
:: TVar ((Atom, Tank) -> IO ())
-> RAcquire PierEnv (Serf, Log.EventLog)
-> Bool
-> MVar ()
-> [Ev]
2020-05-13 22:35:57 +03:00
-> RIO PierEnv ()
runOrExitImmediately vSlog getPier oExit mStart injected = do
rwith getPier (if oExit then shutdownImmediately else runPier)
where
shutdownImmediately :: (Serf, Log.EventLog) -> RIO PierEnv ()
shutdownImmediately (serf, log) = do
logInfo "Sending shutdown signal"
Serf.stop serf
logInfo "Shutdown!"
runPier :: (Serf, Log.EventLog) -> RIO PierEnv ()
runPier serfLog = do
runRAcquire (Pier.pier serfLog vSlog mStart injected)
tryPlayShip
2020-05-13 22:35:57 +03:00
:: Bool
-> Bool
-> Maybe Word64
-> MVar ()
-> [Ev]
2020-05-13 22:35:57 +03:00
-> RIO PierEnv ()
tryPlayShip exitImmediately fullReplay playFrom mStart injected = do
2020-05-22 21:12:28 +03:00
when fullReplay wipeSnapshot
vSlog <- logSlogs
runOrExitImmediately vSlog (resumeShip vSlog) exitImmediately mStart injected
2020-05-22 21:12:28 +03:00
where
wipeSnapshot = do
shipPath <- view pierPathL
logInfo "wipeSnapshot"
logInfo $ display $ pack @Text ("Wiping " <> north shipPath)
logInfo $ display $ pack @Text ("Wiping " <> south shipPath)
2020-05-22 21:12:28 +03:00
removeFileIfExists (north shipPath)
removeFileIfExists (south shipPath)
north shipPath = shipPath <> "/.urb/chk/north.bin"
south shipPath = shipPath <> "/.urb/chk/south.bin"
resumeShip :: TVar ((Atom, Tank) -> IO ())
-> RAcquire PierEnv (Serf, Log.EventLog)
resumeShip vSlog = do
2020-05-22 21:12:28 +03:00
view pierPathL >>= lockFile
rio $ logInfo "RESUMING SHIP"
sls <- Pier.resumed vSlog playFrom
rio $ logInfo "SHIP RESUMED"
2020-05-22 21:12:28 +03:00
pure sls
2020-01-23 07:16:09 +03:00
runRAcquire :: (MonadUnliftIO (m e), MonadIO (m e), MonadReader e (m e))
=> RAcquire e a -> m e a
runRAcquire act = rwith act pure
2020-01-23 07:16:09 +03:00
--------------------------------------------------------------------------------
2020-05-22 21:12:28 +03:00
checkEvs :: FilePath -> Word64 -> Word64 -> RIO KingEnv ()
2020-01-23 07:16:09 +03:00
checkEvs pierPath first last = do
2020-05-22 21:12:28 +03:00
rwith (Log.existing logPath) $ \log -> do
let ident = Log.identity log
let pbSty = PB.defStyle { PB.stylePostfix = PB.exact }
logInfo (displayShow ident)
2020-01-23 07:16:09 +03:00
last <- atomically $ Log.lastEv log <&> \lastReal -> min last lastReal
2020-01-23 07:16:09 +03:00
2020-05-22 21:12:28 +03:00
let evCount = fromIntegral (last - first)
2020-01-23 07:16:09 +03:00
2020-05-22 21:12:28 +03:00
pb <- PB.newProgressBar pbSty 10 (PB.Progress 1 evCount ())
runConduit $ Log.streamEvents log first .| showEvents
pb
first
(fromIntegral $ lifecycleLen ident)
where
logPath :: FilePath
logPath = pierPath <> "/.urb/log"
showEvents
:: PB.ProgressBar ()
-> EventId
-> EventId
-> ConduitT ByteString Void (RIO KingEnv) ()
showEvents pb eId _ | eId > last = pure ()
showEvents pb eId cycle = await >>= \case
Nothing -> do
lift $ PB.killProgressBar pb
lift $ logInfo "Everything checks out."
2020-05-22 21:12:28 +03:00
Just bs -> do
lift $ PB.incProgress pb 1
lift $ do
n <- io $ cueBSExn bs
when (eId > cycle) $ do
(mug, wen, evNoun) <- unpackJob n
fromNounErr evNoun & \case
Left err -> logError (displayShow (eId, err))
Right (_ :: Ev) -> pure ()
showEvents pb (succ eId) cycle
unpackJob :: Noun -> RIO KingEnv (Mug, Wen, Noun)
unpackJob = io . fromNounExn
2020-01-23 07:16:09 +03:00
--------------------------------------------------------------------------------
collectAllFx :: FilePath -> RIO KingEnv ()
collectAllFx = error "TODO"
{-
2020-01-23 07:16:09 +03:00
{-|
This runs the serf at `$top/.tmpdir`, but we disable snapshots,
so this should never actually be created. We just do this to avoid
letting the serf use an existing snapshot.
-}
2020-05-22 21:12:28 +03:00
collectAllFx :: FilePath -> RIO KingEnv ()
2020-01-23 07:16:09 +03:00
collectAllFx top = do
logInfo $ display $ pack @Text top
vSlog <- logSlogs
rwith (collectedFX vSlog) $ \() ->
logInfo "Done collecting effects!"
2020-01-23 07:16:09 +03:00
where
tmpDir :: FilePath
2020-01-25 03:20:13 +03:00
tmpDir = top </> ".tmpdir"
2020-01-23 07:16:09 +03:00
collectedFX :: TVar (Text -> IO ()) -> RAcquire KingEnv ()
collectedFX vSlog = do
2020-01-23 07:16:09 +03:00
lockFile top
log <- Log.existing (top <> "/.urb/log")
serf <- Pier.runSerf vSlog tmpDir serfFlags
rio $ Serf.collectFX serf log
2020-01-23 07:16:09 +03:00
serfFlags :: [Serf.Flag]
2020-01-23 07:16:09 +03:00
serfFlags = [Serf.Hashless, Serf.DryRun]
-}
2020-01-23 07:16:09 +03:00
2020-01-23 07:16:09 +03:00
--------------------------------------------------------------------------------
2020-05-22 21:12:28 +03:00
replayPartEvs :: FilePath -> Word64 -> RIO KingEnv ()
replayPartEvs top last = do
logInfo $ display $ pack @Text top
fetchSnapshot
rwith replayedEvs $ \() ->
logInfo "Done replaying events!"
where
2020-05-22 21:12:28 +03:00
fetchSnapshot :: RIO KingEnv ()
fetchSnapshot = do
snap <- Pier.getSnapshot top last
case snap of
Nothing -> pure ()
Just sn -> do
liftIO $ system $ "cp -r \"" <> sn <> "\" \"" <> tmpDir <> "\""
pure ()
tmpDir :: FilePath
2020-01-25 03:20:13 +03:00
tmpDir = top </> ".partial-replay" </> show last
2020-05-22 21:12:28 +03:00
replayedEvs :: RAcquire KingEnv ()
replayedEvs = do
lockFile top
log <- Log.existing (top <> "/.urb/log")
let onSlog = print
let onStdr = print
let onDead = error "DIED"
let config = Serf.Config "urbit-worker" tmpDir serfFlags onSlog onStdr onDead
(serf, info) <- io (Serf.start config)
rio $ do
eSs <- Serf.execReplay serf log (Just last)
case eSs of
Left bail -> error (show bail)
Right 0 -> io (Serf.snapshot serf)
Right num -> pure ()
io $ threadDelay 500000 -- Copied from runOrExitImmediately
pure ()
serfFlags :: [Serf.Flag]
serfFlags = [Serf.Hashless]
--------------------------------------------------------------------------------
2020-01-23 07:16:09 +03:00
{-|
Interesting
-}
2020-12-19 04:00:56 +03:00
testPill :: HasKingEnv e => FilePath -> Bool -> Bool -> RIO e ()
2020-01-23 07:16:09 +03:00
testPill pax showPil showSeq = do
logInfo "Reading pill file."
2020-01-23 07:16:09 +03:00
pillBytes <- readFile pax
logInfo "Cueing pill file."
2020-01-23 07:16:09 +03:00
pillNoun <- io $ cueBS pillBytes & either throwIO pure
logInfo "Parsing pill file."
2020-01-23 07:16:09 +03:00
pill <- fromNounErr pillNoun & either (throwIO . uncurry ParseErr) pure
logInfo "Using pill to generate boot sequence."
bootSeq <- genBootSeq
(Ship 0)
pill
False
(Fake (Ship 0))
2021-06-17 15:48:09 +03:00
(Feed1 $ Germs (Ship 0) [])
2020-01-23 07:16:09 +03:00
logInfo "Validate jam/cue and toNoun/fromNoun on pill value"
2020-01-23 07:16:09 +03:00
reJam <- validateNounVal pill
logInfo "Checking if round-trip matches input file:"
2020-01-23 07:16:09 +03:00
unless (reJam == pillBytes) $ do
logInfo " Our jam does not match the file...\n"
logInfo " This is surprising, but it is probably okay."
2020-01-23 07:16:09 +03:00
when showPil $ do
logInfo "\n\n== Pill ==\n"
2020-01-23 07:16:09 +03:00
io $ pPrint pill
when showSeq $ do
logInfo "\n\n== Boot Sequence ==\n"
2020-01-23 07:16:09 +03:00
io $ pPrint bootSeq
validateNounVal :: (HasLogFunc e, Eq a, ToNoun a, FromNoun a)
=> a -> RIO e ByteString
validateNounVal inpVal = do
logInfo " jam"
2020-01-23 07:16:09 +03:00
inpByt <- evaluate $ jamBS $ toNoun inpVal
logInfo " cue"
2020-01-23 07:16:09 +03:00
outNon <- cueBS inpByt & either throwIO pure
logInfo " fromNoun"
2020-01-23 07:16:09 +03:00
outVal <- fromNounErr outNon & either (throwIO . uncurry ParseErr) pure
logInfo " toNoun"
2020-01-23 07:16:09 +03:00
outNon <- evaluate (toNoun outVal)
logInfo " jam"
2020-01-23 07:16:09 +03:00
outByt <- evaluate $ jamBS outNon
logInfo "Checking if: x == cue (jam x)"
2020-01-23 07:16:09 +03:00
unless (inpVal == outVal) $
error "Value fails test: x == cue (jam x)"
logInfo "Checking if: jam x == jam (cue (jam x))"
2020-01-23 07:16:09 +03:00
unless (inpByt == outByt) $
error "Value fails test: jam x == jam (cue (jam x))"
pure outByt
2020-01-23 07:16:09 +03:00
--------------------------------------------------------------------------------
2020-08-13 17:19:35 +03:00
pillFrom :: CLI.PillSource -> RIO HostEnv Pill
2020-05-13 22:06:32 +03:00
pillFrom = \case
CLI.PillSourceFile pillPath -> do
logInfo $ display $ "boot: reading pill from " ++ (pack pillPath :: Text)
2020-05-13 22:06:32 +03:00
io (loadFile pillPath >>= either throwIO pure)
2020-01-23 07:16:09 +03:00
2020-05-13 22:06:32 +03:00
CLI.PillSourceURL url -> do
logInfo $ display $ "boot: retrieving pill from " ++ (pack url :: Text)
2020-05-13 22:06:32 +03:00
-- Get the jamfile with the list of stars accepting comets right now.
manager <- io $ C.newManager tlsManagerSettings
request <- io $ C.parseRequest url
response <- io $ C.httpLbs (C.setRequestCheckStatus request) manager
let body = toStrict $ C.responseBody response
2020-01-23 07:16:09 +03:00
2020-05-13 22:06:32 +03:00
noun <- cueBS body & either throwIO pure
fromNounErr noun & either (throwIO . uncurry ParseErr) pure
2020-01-23 07:16:09 +03:00
multiOnFatal :: HasKingEnv e => e -> IO ()
multiOnFatal env = runRIO env $ do
(view stderrLogFuncL >>=) $ flip runRIO $ logError
("Urbit is shutting down because of a problem with the HTTP server.\n"
<> "Please restart it at your leisure.")
view killKingActionL >>= atomically
2020-05-13 22:35:57 +03:00
newShip :: CLI.New -> CLI.Opts -> RIO KingEnv ()
newShip CLI.New{..} opts = do
{-
TODO XXX HACK
Because the "new ship" flow *may* automatically start the ship,
we need to create this, but it's not actually correct.
The right solution is to separate out the "new ship" flow from the
"run ship" flow, and possibly sequence them from the outside if
that's really needed.
-}
env <- ask
multi <- multiEyre (multiOnFatal env) (MultiEyreConf Nothing Nothing True)
-- TODO: We hit the same problem as above: we need a host env to boot a ship
-- because it may autostart the ship, so build an inactive port configuration.
let ports = buildInactivePorts
-- here we are with a king env, and we now need a multi env.
runHostEnv multi ports $ case nBootType of
CLI.BootComet -> do
2020-01-23 07:16:09 +03:00
pill <- pillFrom nPillSource
putStrLn "boot: retrieving list of stars currently accepting comets"
starList <- dawnCometList
putStrLn ("boot: " ++ (tshow $ length starList) ++
" star(s) currently accepting comets")
putStrLn "boot: mining a comet"
eny <- io $ Sys.randomIO
let seed = mineComet (Set.fromList starList) eny
putStrLn ("boot: found comet " ++ renderShip (sShip seed))
putStrLn ("code: " ++ (tshow $ deriveCode $ sRing seed))
2021-06-03 02:04:45 +03:00
bootFromSeed pill $ Feed0 seed
2020-01-23 07:16:09 +03:00
CLI.BootFake name -> do
2020-01-23 07:16:09 +03:00
pill <- pillFrom nPillSource
ship <- shipFrom name
2021-06-17 15:48:09 +03:00
runTryBootFromPill pill name ship (Fake ship) (Feed1 $ Germs ship [])
2020-01-23 07:16:09 +03:00
CLI.BootFromKeyfile keyFile -> do
2020-01-23 07:16:09 +03:00
text <- readFileUtf8 keyFile
asAtom <- case cordToUW (Cord $ T.strip text) of
Nothing -> error "Couldn't parse keyfile. Hint: keyfiles start with 0w?"
Just (UW a) -> pure a
asNoun <- cueExn asAtom
2021-06-03 02:04:45 +03:00
feed :: Feed <- case fromNoun asNoun of
2020-01-23 07:16:09 +03:00
Nothing -> error "Keyfile does not seem to contain a seed."
Just s -> pure s
pill <- pillFrom nPillSource
2021-06-03 02:04:45 +03:00
bootFromSeed pill feed
2020-01-23 07:16:09 +03:00
where
2020-08-13 17:19:35 +03:00
shipFrom :: Text -> RIO HostEnv Ship
2020-01-23 07:16:09 +03:00
shipFrom name = case Ob.parsePatp name of
Left x -> error "Invalid ship name"
Right p -> pure $ Ship $ fromIntegral $ Ob.fromPatp p
pierPath :: Text -> FilePath
pierPath name = case nPierPath of
Just x -> x
Nothing -> "./" <> unpack name
nameFromShip :: HasKingEnv e => Ship -> RIO e Text
2020-01-23 07:16:09 +03:00
nameFromShip s = name
where
nameWithSig = Ob.renderPatp $ Ob.patp $ fromIntegral s
name = case stripPrefix "~" nameWithSig of
Nothing -> error "Urbit.ob didn't produce string with ~"
Just x -> pure x
2021-06-03 02:04:45 +03:00
bootFromSeed :: Pill -> Feed -> RIO HostEnv ()
bootFromSeed pill feed = do
ethReturn <- dawnVent nEthNode feed
2020-01-23 07:16:09 +03:00
case ethReturn of
Left x -> error $ unpack x
Right dawn -> do
let ship = sShip $ dSeed dawn
2020-01-23 07:16:09 +03:00
name <- nameFromShip ship
runTryBootFromPill pill name ship (Dawn dawn) feed
2020-01-23 07:16:09 +03:00
-- Now that we have all the information for running an application with a
-- PierConfig, do so.
runTryBootFromPill :: Pill
-> Text
-> Ship
-> LegacyBootEvent
-> Feed
2020-08-13 17:19:35 +03:00
-> RIO HostEnv ()
runTryBootFromPill pill name ship bootEvent feed = do
2020-08-14 17:23:39 +03:00
vKill <- view (kingEnvL . kingEnvKillSignal)
let pierConfig = toPierConfig (pierPath name) nSerfExe opts
2020-01-23 07:16:09 +03:00
let networkConfig = toNetworkConfig opts
runPierEnv pierConfig networkConfig vKill $
tryBootFromPill True pill nLite ship bootEvent feed
2020-01-23 07:16:09 +03:00
runShipEnv :: Maybe Text -> CLI.Run -> CLI.Opts -> TMVar () -> RIO PierEnv a
-> RIO HostEnv a
runShipEnv serfExe (CLI.Run pierPath) opts vKill act = do
runPierEnv pierConfig netConfig vKill act
where
pierConfig = toPierConfig pierPath serfExe opts
netConfig = toNetworkConfig opts
runShip
:: CLI.Run -> CLI.Opts -> Bool -> RIO PierEnv ()
runShip (CLI.Run pierPath) opts daemon = do
mStart <- newEmptyMVar
if daemon
then runPier mStart
else do
-- Wait until the pier has started up, then connect a terminal. If
-- the terminal ever shuts down, ask the ship to go down.
connectionThread <- async $ do
readMVar mStart
finally (connTerm pierPath) $ do
view killPierActionL >>= atomically
-- Run the pier until it finishes, and then kill the terminal.
finally (runPier mStart) $ do
cancel connectionThread
where
runPier :: MVar () -> RIO PierEnv ()
runPier mStart = do
injections <- loadInjections (CLI.oInjectEvents opts)
tryPlayShip
(CLI.oExit opts)
(CLI.oFullReplay opts)
(CLI.oDryFrom opts)
mStart
injections
loadInjections :: [CLI.Injection] -> RIO PierEnv [Ev]
loadInjections injections = do
2020-10-22 17:53:28 +03:00
perInjection :: [[Ev]] <- for injections $ \case
CLI.InjectOneEvent filePath -> do
logInfo $ display $ "boot: reading injected event from " ++
(pack filePath :: Text)
io (loadFile filePath >>= either throwIO (pure . singleton))
CLI.InjectManyEvents filePath -> do
logInfo $ display $ "boot: reading injected event list from " ++
(pack filePath :: Text)
io (loadFile filePath >>= either throwIO pure)
pure $ concat perInjection
2020-01-23 07:16:09 +03:00
2020-08-18 22:43:31 +03:00
buildPortHandler :: HasLogFunc e => CLI.Nat -> RIO e PortControlApi
buildPortHandler CLI.NatNever = pure buildInactivePorts
-- TODO: Figure out what to do about logging here. The "port: " messages are
-- the sort of thing that should be put on the muxed terminal log, but we don't
-- have that at this layer.
2020-08-18 22:43:31 +03:00
buildPortHandler CLI.NatAlways = buildNatPorts (io . hPutStrLn stderr . unpack)
buildPortHandler CLI.NatWhenPrivateNetwork =
buildNatPortsWhenPrivate (io . hPutStrLn stderr . unpack)
2020-01-23 07:16:09 +03:00
startBrowser :: HasLogFunc e => FilePath -> RIO e ()
startBrowser pierPath = runRAcquire $ do
-- lockFile pierPath
log <- Log.existing (pierPath <> "/.urb/log")
rio $ EventBrowser.run log
checkDawn :: HasLogFunc e => String -> FilePath -> RIO e ()
checkDawn provider keyfilePath = do
2020-01-23 07:16:09 +03:00
-- The keyfile is a jammed Seed then rendered in UW format
text <- readFileUtf8 keyfilePath
asAtom <- case cordToUW (Cord $ T.strip text) of
Nothing -> error "Couldn't parse keyfile. Hint: keyfiles start with 0w?"
Just (UW a) -> pure a
asNoun <- cueExn asAtom
2021-06-03 02:04:45 +03:00
feed :: Feed <- case fromNoun asNoun of
2020-01-23 07:16:09 +03:00
Nothing -> error "Keyfile does not seem to contain a seed."
Just s -> pure s
2021-06-03 02:04:45 +03:00
print $ show feed
2020-01-23 07:16:09 +03:00
2021-06-03 02:04:45 +03:00
e <- dawnVent provider feed
2020-01-23 07:16:09 +03:00
print $ show e
checkComet :: HasLogFunc e => RIO e ()
checkComet = do
starList <- dawnCometList
putStrLn "Stars currently accepting comets:"
let starNames = map (Ob.renderPatp . Ob.patp . fromIntegral) starList
print starNames
putStrLn "Trying to mine a comet..."
eny <- io $ Sys.randomIO
let s = mineComet (Set.fromList starList) eny
print s
main :: IO ()
main = do
2020-08-15 05:25:07 +03:00
(args, log) <- CLI.parseArgs
hSetBuffering stdout NoBuffering
setupSignalHandlers
setRLimits
2020-08-15 05:25:07 +03:00
runKingEnv args log $ case args of
CLI.CmdRun ko ships -> runShips ko ships
CLI.CmdNew n o -> newShip n o
CLI.CmdBug (CLI.CollectAllFX pax ) -> collectAllFx pax
CLI.CmdBug (CLI.EventBrowser pax ) -> startBrowser pax
CLI.CmdBug (CLI.ValidatePill pax pil s) -> testPill pax pil s
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.ReplayEvents pax l ) -> replayPartEvs pax l
CLI.CmdBug (CLI.CheckDawn provider pax ) -> checkDawn provider pax
CLI.CmdBug CLI.CheckComet -> checkComet
CLI.CmdCon pier -> connTerm pier
2020-01-23 07:16:09 +03:00
where
2020-08-15 05:25:07 +03:00
runKingEnv args log =
let
verb = verboseLogging args
runStderr = case args of
CLI.CmdRun {} -> runKingEnvStderrRaw
_ -> runKingEnvStderr
CLI.Log {..} = log
in case logTarget lTarget args of
CLI.LogFile f -> runKingEnvLogFile verb lLevel f
CLI.LogStderr -> runStderr verb lLevel
2020-08-15 05:25:07 +03:00
CLI.LogOff -> runKingEnvNoLog
setupSignalHandlers = do
2020-01-23 07:16:09 +03:00
mainTid <- myThreadId
let onKillSig = throwTo mainTid UserInterrupt
for_ [Sys.sigTERM, Sys.sigINT] $ \sig -> do
Sys.installHandler sig (Sys.Catch onKillSig) Nothing
2020-01-23 07:16:09 +03:00
setRLimits = do
2021-03-22 04:27:50 +03:00
openFiles <- Sys.getResourceLimit Sys.ResourceOpenFiles
let soft = case Sys.hardLimit openFiles of
Sys.ResourceLimit lim -> Sys.ResourceLimit lim
Sys.ResourceLimitInfinity -> Sys.ResourceLimit 10240 -- macOS
Sys.ResourceLimitUnknown -> Sys.ResourceLimit 10240
Sys.setResourceLimit Sys.ResourceOpenFiles
2021-03-22 04:27:50 +03:00
openFiles { Sys.softLimit = soft }
verboseLogging :: CLI.Cmd -> Bool
verboseLogging = \case
CLI.CmdRun ko ships -> any CLI.oVerbose (ships <&> \(_, o, _) -> o)
_ -> False
2020-08-15 05:25:07 +03:00
-- If the user hasn't specified where to log, what we do depends on what
-- command she has issued. Notably, the LogFile Nothing outcome means that
-- runKingEnvLogFile should run an IO action to get the official app data
-- directory and open a canonically named log file there.
logTarget :: Maybe (CLI.LogTarget FilePath)
-> CLI.Cmd
-> CLI.LogTarget (Maybe FilePath)
logTarget = \case
Just (CLI.LogFile f) -> const $ CLI.LogFile (Just f)
Just CLI.LogStderr -> const $ CLI.LogStderr
Just CLI.LogOff -> const $ CLI.LogOff
Nothing -> \case
CLI.CmdCon _ -> CLI.LogFile Nothing
CLI.CmdRun ko [(_,_,daemon)] | daemon -> CLI.LogStderr
| otherwise -> CLI.LogFile Nothing
CLI.CmdRun ko _ -> CLI.LogStderr
_ -> CLI.LogStderr
2020-01-23 07:16:09 +03:00
{-
Runs a ship but restarts it if it crashes or shuts down on it's own.
Once `waitForKillRequ` returns, the ship will be terminated and this
routine will exit.
-}
2020-05-13 22:35:57 +03:00
runShipRestarting
:: Maybe Text -> CLI.Run -> CLI.Opts -> RIO HostEnv ()
runShipRestarting serfExe r o = do
let pier = pack (CLI.rPierPath r)
loop = runShipRestarting serfExe r o
onKill <- view onKillKingSigL
vKillPier <- newEmptyTMVarIO
tid <- asyncBound $ runShipEnv serfExe r o vKillPier $ runShip r o True
let onShipExit = Left <$> waitCatchSTM tid
onKillRequ = Right <$> onKill
atomically (onShipExit <|> onKillRequ) >>= \case
Left exit -> do
case exit of
2020-05-13 22:06:32 +03:00
Left err -> logError $ display (tshow err <> ": " <> pier)
Right () ->
logError $ display ("Ship exited on it's own. Why? " <> pier)
threadDelay 250_000
loop
Right () -> do
logTrace $ display (pier <> " shutdown requested")
atomically $ putTMVar vKillPier ()
race_ (wait tid) $ do
threadDelay 5_000_000
logInfo $ display (pier <> " not down after 5s, killing with fire.")
cancel tid
2020-05-13 22:06:32 +03:00
logTrace $ display ("Ship terminated: " <> pier)
{-
TODO This is messy and shared a lot of logic with `runShipRestarting`.
-}
runShipNoRestart
:: Maybe Text -> CLI.Run -> CLI.Opts -> Bool -> RIO HostEnv ()
runShipNoRestart serfExe r o d = do
-- killing ship same as killing king
2020-08-14 17:23:39 +03:00
vKill <- view (kingEnvL . kingEnvKillSignal)
tid <- asyncBound (runShipEnv serfExe r o vKill $ runShip r o d)
onKill <- view onKillKingSigL
let pier = pack (CLI.rPierPath r)
let onShipExit = Left <$> waitCatchSTM tid
onKillRequ = Right <$> onKill
atomically (onShipExit <|> onKillRequ) >>= \case
Left (Left err) -> do
logError $ display (tshow err <> ": " <> pier)
Left (Right ()) -> do
logError $ display (pier <> " exited on it's own. Why?")
Right () -> do
logTrace $ display (pier <> " shutdown requested")
race_ (wait tid) $ do
threadDelay 5_000_000
logTrace $ display (pier <> " not down after 5s, killing with fire.")
cancel tid
logTrace $ display (pier <> " terminated.")
2020-08-15 05:25:07 +03:00
runShips :: CLI.Host -> [(CLI.Run, CLI.Opts, Bool)] -> RIO KingEnv ()
runShips CLI.Host {..} ships = do
let meConf = MultiEyreConf
2020-08-15 05:25:07 +03:00
{ mecHttpPort = fromIntegral <$> hSharedHttpPort
, mecHttpsPort = fromIntegral <$> hSharedHttpsPort
, mecLocalhostOnly = False -- TODO Localhost-only needs to be
-- a king-wide option.
}
env <- ask
multi <- multiEyre (multiOnFatal env) meConf
ports <- buildPortHandler hUseNatPmp
2020-08-13 17:19:35 +03:00
runHostEnv multi ports (go ships)
where
2020-08-13 17:19:35 +03:00
go :: [(CLI.Run, CLI.Opts, Bool)] -> RIO HostEnv ()
go = \case
[] -> pure ()
[rod] -> runSingleShip hSerfExe rod
ships -> runMultipleShips hSerfExe (ships <&> \(r, o, _) -> (r, o))
-- TODO Duplicated logic.
runSingleShip :: Maybe Text -> (CLI.Run, CLI.Opts, Bool) -> RIO HostEnv ()
runSingleShip serfExe (r, o, d) = do
shipThread <- async (runShipNoRestart serfExe r o d)
{-
Wait for the ship to go down.
Since `waitCatch` will never throw an exception, the `onException`
block will only happen if this thread is killed with an async
exception. The one we expect is `UserInterrupt` which will be raised
on this thread upon SIGKILL or SIGTERM.
If this thread is killed, we first ask the ship to go down, wait
for the ship to actually go down, and then go down ourselves.
-}
onException (void $ waitCatch shipThread) $ do
logTrace "KING IS GOING DOWN"
atomically =<< view killKingActionL
waitCatch shipThread
pure ()
runMultipleShips :: Maybe Text -> [(CLI.Run, CLI.Opts)] -> RIO HostEnv ()
runMultipleShips serfExe ships = do
shipThreads <- for ships $ \(r, o) -> do
async (runShipRestarting serfExe r o)
2020-01-23 07:16:09 +03:00
{-
Since `spin` never returns, this will run until the main
thread is killed with an async exception. The one we expect is
`UserInterrupt` which will be raised on this thread upon SIGKILL
or SIGTERM.
2020-01-23 07:16:09 +03:00
Once that happens, we send a shutdown signal which will cause all
ships to be shut down, and then we `wait` for them to finish before
returning.
This is different than the single-ship flow, because ships never
go down on their own in this flow. If they go down, they just bring
themselves back up.
-}
let spin = forever (threadDelay maxBound)
finally spin $ do
2020-05-13 22:06:32 +03:00
logTrace "KING IS GOING DOWN"
view killKingActionL >>= atomically
for_ shipThreads waitCatch
2020-01-23 07:16:09 +03:00
--------------------------------------------------------------------------------
connTerm :: forall e. HasLogFunc e => FilePath -> RIO e ()
connTerm = Term.runTerminalClient
2020-01-23 07:16:09 +03:00
2020-01-23 07:16:09 +03:00
--------------------------------------------------------------------------------
checkFx :: HasLogFunc e
=> FilePath -> Word64 -> Word64 -> RIO e ()
checkFx pierPath first last =
rwith (Log.existing logPath) $ \log ->
runConduit $ streamFX log first last
.| tryParseFXStream
where
logPath = pierPath <> "/.urb/log"
streamFX :: HasLogFunc e
=> Log.EventLog -> Word64 -> Word64
-> ConduitT () ByteString (RIO e) ()
streamFX log first last = do
Log.streamEffectsRows log first .| loop
where
loop = await >>= \case Nothing -> pure ()
Just (eId, bs) | eId > last -> pure ()
Just (eId, bs) -> yield bs >> loop
tryParseFXStream :: HasLogFunc e => ConduitT ByteString Void (RIO e) ()
tryParseFXStream = loop
where
loop = await >>= \case
Nothing -> pure ()
Just bs -> do
n <- liftIO (cueBSExn bs)
fromNounErr n & either (logError . displayShow) pure
loop
{-
tryCopyLog :: IO ()
tryCopyLog = do
let logPath = "/Users/erg/src/urbit/zod/.urb/falselog/"
falselogPath = "/Users/erg/src/urbit/zod/.urb/falselog2/"
persistQ <- newTQueueIO
releaseQ <- newTQueueIO
(ident, nextEv, events) <-
with (do { log <- Log.existing logPath
; Pier.runPersist log persistQ (writeTQueue releaseQ)
; pure log
})
\log -> do
ident <- pure $ Log.identity log
events <- runConduit (Log.streamEvents log 1 .| consume)
nextEv <- Log.nextEv log
pure (ident, nextEv, events)
print ident
print nextEv
print (length events)
persistQ2 <- newTQueueIO
releaseQ2 <- newTQueueIO
with (do { log <- Log.new falselogPath ident
; Pier.runPersist log persistQ2 (writeTQueue releaseQ2)
; pure log
})
$ \log2 -> do
let writs = zip [1..] events <&> \(id, a) ->
(Writ id Nothing a, [])
print "About to write"
for_ writs $ \w ->
atomically (writeTQueue persistQ2 w)
print "About to wait"
replicateM_ 100 $ do
atomically $ readTQueue releaseQ2
print "Done"
-}