shrub/pkg/king/app/Main.hs

473 lines
14 KiB
Haskell
Raw Normal View History

{-
# Booting a Ship
- TODO Don't just boot, also run the ship (unless `-x` is set).
- TODO Figure out why ships booted by us don't work.
# 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.
# `-L` -- Local-Only Networking
Localhost-only networking, even on real ships.
# `-O` -- Networking Disabled
Run networking drivers, but configure them to never send any packages
and to never open any ports.
# `-N` -- Dry Run
Disable all persistence and use no-op networking.
# `-x` -- Exit Immediately
When creating a new ship, or booting an existing one, simply get to
a good state, snapshot, and then exit. Don't do anything that has
any effect on the outside world, just boot or catch the snapshot up
to the event log.
# 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.
- `Trace`: TODO What does this do?
-}
2019-08-28 14:06:48 +03:00
module Main (main) where
import UrbitPrelude
2019-07-16 03:01:45 +03:00
import Data.RAcquire
2019-08-13 07:57:30 +03:00
import Arvo
import Control.Exception (AsyncException(UserInterrupt))
import Data.Acquire
import Data.Conduit
2019-08-22 02:49:08 +03:00
import Data.Conduit.List hiding (replicate, take)
2019-08-13 07:57:30 +03:00
import Noun hiding (Parser)
import Vere.Pier
import Vere.Pier.Types
import Vere.Serf
2019-07-16 03:01:45 +03:00
import Control.Concurrent (myThreadId, runInBoundThread)
import Control.Lens ((&))
import Data.Default (def)
import System.Directory (doesFileExist, removeFile)
import System.Directory (createDirectoryIfMissing, getHomeDirectory)
import System.Environment (getProgName)
import System.Posix.Signals (Handler(Catch), installHandler, sigTERM)
import Text.Show.Pretty (pPrint)
import Urbit.Time (Wen)
2019-08-13 07:57:30 +03:00
import qualified CLI
import qualified Data.Set as Set
import qualified EventBrowser
import qualified System.IO.LockFile.Internal as Lock
import qualified Vere.Log as Log
import qualified Vere.Pier as Pier
import qualified Vere.Serf as Serf
--------------------------------------------------------------------------------
class HasAppName env where
appNameL :: Lens' env Utf8Builder
data App = App
2019-08-28 14:06:48 +03:00
{ _appLogFunc :: !LogFunc
, _appName :: !Utf8Builder
}
makeLenses ''App
instance HasLogFunc App where
logFuncL = appLogFunc
instance HasAppName App where
appNameL = appName
runApp :: RIO App a -> IO a
runApp inner = do
2019-08-29 03:26:59 +03:00
home <- getHomeDirectory
let logDir = home <> "/log"
createDirectoryIfMissing True logDir
withTempFile logDir "king-" $ \tmpFile hFile -> do
hSetBuffering hFile LineBuffering
logOptions <- logOptionsHandle hFile True
<&> setLogUseTime True
<&> setLogUseLoc False
withLogFunc logOptions $ \logFunc -> do
let app = App { _appLogFunc = logFunc
, _appName = "Alice"
}
runRIO app inner
--------------------------------------------------------------------------------
zod :: Ship
zod = 0
--------------------------------------------------------------------------------
removeFileIfExists :: HasLogFunc env => FilePath -> RIO env ()
removeFileIfExists pax = do
exists <- io $ doesFileExist pax
when exists $ do
io $ removeFile pax
--------------------------------------------------------------------------------
tryBootFromPill :: HasLogFunc e => FilePath -> FilePath -> Ship -> RIO e ()
tryBootFromPill pillPath shipPath ship = do
rwith bootedPier $ \(serf, log, ss) -> do
logTrace "Booting"
logTrace $ displayShow ss
io $ threadDelay 500000
2019-08-28 15:22:56 +03:00
ss <- shutdown serf 0
logTrace $ displayShow ss
logTrace "Booted!"
where
bootedPier = do
lockFile shipPath
Pier.booted pillPath shipPath [] ship
runAcquire :: (MonadUnliftIO m, MonadIO m)
=> Acquire a -> m a
runAcquire act = with act pure
runRAcquire :: (MonadUnliftIO (m e), MonadIO (m e), MonadReader e (m e))
=> RAcquire e a -> m e a
runRAcquire act = rwith act pure
tryPlayShip :: HasLogFunc e => FilePath -> RIO e ()
tryPlayShip shipPath = do
runRAcquire $ do
lockFile shipPath
2019-08-28 14:45:49 +03:00
rio $ logTrace "RESUMING SHIP"
2019-08-28 15:22:56 +03:00
sls <- Pier.resumed shipPath []
2019-08-28 14:45:49 +03:00
rio $ logTrace "SHIP RESUMED"
Pier.pier shipPath Nothing sls
lockFile :: HasLogFunc e => FilePath -> RAcquire e ()
lockFile pax = void $ mkRAcquire start stop
where
fil = pax <> "/.vere.lock"
stop handle = do
logInfo $ display @Text $ ("Releasing lock file: " <> pack fil)
io $ Lock.unlock fil handle
params = def { Lock.retryToAcquireLock = Lock.No }
start = do
logInfo $ display @Text $ ("Taking lock file: " <> pack fil)
io (Lock.lock params fil)
tryResume :: HasLogFunc e => FilePath -> RIO e ()
tryResume shipPath = do
rwith resumedPier $ \(serf, log, ss) -> do
logTrace (displayShow ss)
threadDelay 500000
2019-08-28 15:22:56 +03:00
ss <- shutdown serf 0
logTrace (displayShow ss)
logTrace "Resumed!"
where
resumedPier = do
lockFile shipPath
Pier.resumed shipPath []
tryFullReplay :: HasLogFunc e => FilePath -> RIO e ()
tryFullReplay shipPath = do
wipeSnapshot
tryResume shipPath
where
wipeSnapshot = do
logTrace "wipeSnapshot"
logDebug $ display $ pack @Text ("Wiping " <> north)
logDebug $ display $ pack @Text ("Wiping " <> south)
removeFileIfExists north
removeFileIfExists south
north = shipPath <> "/.urb/chk/north.bin"
south = shipPath <> "/.urb/chk/south.bin"
--------------------------------------------------------------------------------
checkEvs :: forall e. HasLogFunc e => FilePath -> Word64 -> Word64 -> RIO e ()
2019-08-22 03:54:00 +03:00
checkEvs pierPath first last = do
2019-08-29 03:26:59 +03:00
rwith (Log.existing logPath) $ \log -> do
let ident = Log.identity log
logTrace (displayShow ident)
runConduit $ Log.streamEvents log first
2019-08-22 03:54:00 +03:00
.| showEvents first (fromIntegral $ lifecycleLen ident)
where
2019-08-22 03:54:00 +03:00
logPath :: FilePath
logPath = pierPath <> "/.urb/log"
showEvents :: EventId -> EventId -> ConduitT ByteString Void (RIO e) ()
2019-08-22 03:54:00 +03:00
showEvents eId _ | eId > last = pure ()
showEvents eId cycle =
await >>= \case
Nothing -> lift $ logTrace "Everything checks out."
2019-08-22 03:54:00 +03:00
Just bs -> do
lift $ do
n <- io $ cueBSExn bs
2019-08-22 03:54:00 +03:00
when (eId > cycle) $ do
(mug, wen, evNoun) <- unpackJob n
fromNounErr evNoun &
either (logError . displayShow) pure
2019-08-22 03:54:00 +03:00
showEvents (succ eId) cycle
unpackJob :: Noun -> RIO e (Mug, Wen, Noun)
unpackJob = io . fromNounExn
--------------------------------------------------------------------------------
{-
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.
-}
collectAllFx :: e. HasLogFunc e => FilePath -> RIO e ()
collectAllFx top = do
logTrace $ display $ pack @Text top
rwith collectedFX $ \() ->
logTrace "Done collecting effects!"
where
tmpDir :: FilePath
tmpDir = top <> "/.tmpdir"
collectedFX :: RAcquire e ()
collectedFX = do
lockFile top
2019-08-29 03:26:59 +03:00
log <- Log.existing (top <> "/.urb/log")
2019-08-28 15:22:56 +03:00
serf <- Serf.run (Serf.Config tmpDir serfFlags)
rio $ Serf.collectFX serf log
serfFlags :: Serf.Flags
serfFlags = [Serf.Hashless, Serf.DryRun]
--------------------------------------------------------------------------------
2019-08-22 02:49:08 +03:00
{-
Interesting
-}
testPill :: HasLogFunc e => FilePath -> Bool -> Bool -> RIO e ()
2019-08-22 02:49:08 +03:00
testPill pax showPil showSeq = do
putStrLn "Reading pill file."
pillBytes <- readFile pax
putStrLn "Cueing pill file."
pillNoun <- io $ cueBS pillBytes & either throwIO pure
2019-08-22 02:49:08 +03:00
putStrLn "Parsing pill file."
2019-08-28 14:45:49 +03:00
pill <- fromNounErr pillNoun & either (throwIO . uncurry ParseErr) pure
2019-08-22 02:49:08 +03:00
putStrLn "Using pill to generate boot sequence."
2019-08-28 14:45:49 +03:00
bootSeq <- generateBootSeq zod pill
2019-08-22 02:49:08 +03:00
putStrLn "Validate jam/cue and toNoun/fromNoun on pill value"
reJam <- validateNounVal pill
putStrLn "Checking if round-trip matches input file:"
unless (reJam == pillBytes) $ do
putStrLn " Our jam does not match the file...\n"
putStrLn " This is surprising, but it is probably okay."
when showPil $ do
putStrLn "\n\n== Pill ==\n"
io $ pPrint pill
2019-08-22 02:49:08 +03:00
when showSeq $ do
putStrLn "\n\n== Boot Sequence ==\n"
io $ pPrint bootSeq
2019-08-22 02:49:08 +03:00
validateNounVal :: (HasLogFunc e, Eq a, ToNoun a, FromNoun a)
=> a -> RIO e ByteString
2019-08-22 02:49:08 +03:00
validateNounVal inpVal = do
putStrLn " jam"
inpByt <- evaluate $ jamBS $ toNoun inpVal
putStrLn " cue"
outNon <- cueBS inpByt & either throwIO pure
putStrLn " fromNoun"
outVal <- fromNounErr outNon & either (throwIO . uncurry ParseErr) pure
putStrLn " toNoun"
outNon <- evaluate (toNoun outVal)
putStrLn " jam"
outByt <- evaluate $ jamBS outNon
putStrLn "Checking if: x == cue (jam x)"
unless (inpVal == outVal) $
error "Value fails test: x == cue (jam x)"
putStrLn "Checking if: jam x == jam (cue (jam x))"
unless (inpByt == outByt) $
error "Value fails test: jam x == jam (cue (jam x))"
pure outByt
--------------------------------------------------------------------------------
newShip :: HasLogFunc e => CLI.New -> CLI.Opts -> RIO e ()
2019-08-13 08:56:31 +03:00
newShip CLI.New{..} _ = do
2019-08-15 05:42:48 +03:00
tryBootFromPill nPillPath pierPath (Ship 0)
where
pierPath = fromMaybe ("./" <> unpack nShipAddr) nPierPath
2019-08-13 08:56:31 +03:00
runShip :: HasLogFunc e => CLI.Run -> CLI.Opts -> RIO e ()
2019-08-13 08:56:31 +03:00
runShip (CLI.Run pierPath) _ = tryPlayShip pierPath
startBrowser :: HasLogFunc e => FilePath -> RIO e ()
startBrowser pierPath = runRAcquire $ do
lockFile pierPath
log <- Log.existing (pierPath <> "/.urb/log")
rio $ EventBrowser.run log
2019-08-13 07:57:30 +03:00
main :: IO ()
main = do
mainTid <- myThreadId
let onTermSig = throwTo mainTid UserInterrupt
installHandler sigTERM (Catch onTermSig) Nothing
CLI.parseArgs >>= runApp . \case
CLI.CmdRun r o -> runShip r o
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 seq) -> testPill pax pil seq
CLI.CmdBug (CLI.ValidateEvents pax f l) -> checkEvs pax f l
CLI.CmdBug (CLI.ValidateFX pax f l) -> checkFx pax f l
2019-08-13 07:57:30 +03:00
--------------------------------------------------------------------------------
checkFx :: HasLogFunc e
=> FilePath -> Word64 -> Word64 -> RIO e ()
2019-08-22 03:54:00 +03:00
checkFx pierPath first last =
2019-08-29 03:26:59 +03:00
rwith (Log.existing logPath) $ \log ->
2019-08-22 03:54:00 +03:00
runConduit $ streamFX log first last
.| tryParseFXStream
where
logPath = pierPath <> "/.urb/log"
2019-08-29 03:26:59 +03:00
streamFX :: HasLogFunc e
=> Log.EventLog -> Word64 -> Word64
-> ConduitT () ByteString (RIO e) ()
2019-08-22 03:54:00 +03:00
streamFX log first last = do
Log.streamEffectsRows log first .| loop
2019-07-23 00:46:14 +03:00
where
2019-08-22 03:54:00 +03:00
loop = await >>= \case Nothing -> pure ()
Just (eId, bs) | eId > last -> pure ()
Just (eId, bs) -> yield bs >> loop
2019-07-23 00:46:14 +03:00
tryParseFXStream :: HasLogFunc e => ConduitT ByteString Void (RIO e) ()
tryParseFXStream = loop
2019-07-23 03:46:06 +03:00
where
loop = await >>= \case
Nothing -> pure ()
2019-07-23 03:46:06 +03:00
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/"
2019-07-20 06:00:23 +03:00
persistQ <- newTQueueIO
releaseQ <- newTQueueIO
(ident, nextEv, events) <-
2019-07-20 06:00:23 +03:00
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)
2019-07-20 06:00:23 +03:00
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"
-}