{-# OPTIONS_GHC -Werror -Wall #-} {-# LANGUAGE CPP #-} {-| Command line parsing. -} module Urbit.King.CLI where import ClassyPrelude hiding (log) import Options.Applicative import Options.Applicative.Help.Pretty import Data.Word (Word16) import RIO (LogLevel(..)) import System.Environment (getProgName) -------------------------------------------------------------------------------- data Host = Host { hSharedHttpPort :: Maybe Word16 , hSharedHttpsPort :: Maybe Word16 , hUseNatPmp :: Nat , hSerfExe :: Maybe Text } deriving (Show) -- | Options for each running pier. data Opts = Opts { oQuiet :: Bool , oHashless :: Bool , oExit :: Bool , oDryRun :: Bool , oDryFrom :: Maybe Word64 , oVerbose :: Bool , oAmesPort :: Maybe Word16 , oNoAmes :: Bool , oNoHttp :: Bool , oNoHttps :: Bool , oTrace :: Bool , oCollectFx :: Bool , oLocalhost :: Bool , oOffline :: Bool , oFullReplay :: Bool , oHttpPort :: Maybe Word16 , oHttpsPort :: Maybe Word16 , oLoopbackPort :: Maybe Word16 , oInjectEvents :: [Injection] } deriving (Show) -- | Options for the logging subsystem. data Log = Log { lTarget :: Maybe (LogTarget FilePath) , lLevel :: LogLevel } deriving (Show) data LogTarget a = LogOff | LogStderr | LogFile a deriving (Show) data BootType = BootComet | BootFake Text | BootFromKeyfile FilePath deriving (Show) data PillSource = PillSourceFile FilePath | PillSourceURL String deriving (Show) data Nat = NatAlways | NatWhenPrivateNetwork | NatNever deriving (Show) data Injection = InjectOneEvent FilePath | InjectManyEvents FilePath deriving (Show) data New = New { nPillSource :: PillSource , nPierPath :: Maybe FilePath -- Derived from ship name if not specified. , nArvoDir :: Maybe FilePath , nBootType :: BootType , nLite :: Bool , nSerfExe :: Maybe Text } deriving (Show) data Run = Run { rPierPath :: FilePath } deriving (Show) data Bug = ValidatePill { bPillPath :: FilePath , bPrintPil :: Bool , bPrintSeq :: Bool } | CollectAllFX { bPierPath :: FilePath } | EventBrowser { bPierPath :: FilePath } | ValidateEvents { bPierPath :: FilePath , bFirstEvt :: Word64 , bFinalEvt :: Word64 } | ValidateFX { bPierPath :: FilePath , bFirstEvt :: Word64 , bFinalEvt :: Word64 } | ReplayEvents { bPierPath :: FilePath , bFinalEvt :: Word64 } | CheckDawn { bKeyfilePath :: FilePath } | CheckComet deriving (Show) data Cmd = CmdNew New Opts | CmdRun Host [(Run, Opts, Bool)] | CmdBug Bug | CmdCon FilePath deriving (Show) -------------------------------------------------------------------------------- headNote :: String -> Doc headNote _version = string $ intercalate "\n" [ "Urbit: a personal server operating function" , "https://urbit.org" , "Version " <> VERSION_urbit_king ] -- | TODO This needs to be updated. footNote :: String -> Doc footNote exe = string $ intercalate "\n" [ "Development Usage:" , " To create a development ship, use a fakezod:" , " $ " <>exe<> " new zod /path/to/pill -F zod -A arvo/folder" , "" , "Simple Usage: " , " $ " <>exe<> " new pier to create a comet (anonymous urbit)" , " $ " <>exe<> " new pier -k if you own a planet" , " $ " <>exe<> " run to restart an existing urbit" , "" , "For more information about developing on urbit, see:" , " https://github.com/urbit/urbit/blob/master/CONTRIBUTING.md" ] -------------------------------------------------------------------------------- parseArgs :: IO (Cmd, Log) parseArgs = do nm <- getProgName let p = prefs $ showHelpOnError <> showHelpOnEmpty <> columns 80 let o = info (cmd <**> helper) $ progDesc "Start an existing Urbit or boot a new one." <> headerDoc (Just $ headNote "0.9001.0") <> footerDoc (Just $ footNote nm) <> fullDesc customExecParser p o -------------------------------------------------------------------------------- defaultPillURL :: String defaultPillURL = "https://bootstrap.urbit.org/urbit-v" <> ver <> ".pill" where ver = VERSION_urbit_king -------------------------------------------------------------------------------- newComet :: Parser BootType newComet = flag' BootComet ( long "comet" <> help "Boot a new comet" ) newFakeship :: Parser BootType newFakeship = BootFake <$> strOption (short 'F' <> long "fake" <> metavar "SHIP" <> help "Boot a fakeship") newFromKeyfile :: Parser BootType newFromKeyfile = BootFromKeyfile <$> strOption (short 'k' <> long "keyfile" <> metavar "KEYFILE" <> help "Boot from a keyfile") pillFromPath :: Parser PillSource pillFromPath = PillSourceFile <$> strOption ( short 'B' <> long "pill" <> metavar "PILL" <> help "Path to pill file") pillFromURL :: Parser PillSource pillFromURL = PillSourceURL <$> strOption ( short 'u' <> long "pill-url" <> metavar "URL" <> value defaultPillURL <> help "URL to pill file") pierPath :: Parser FilePath pierPath = strArgument (metavar "PIER" <> help "Path to pier") injectEvents :: Parser [Injection] injectEvents = many $ InjectOneEvent <$> strOption ( short 'I' <> long "inject-event" <> metavar "JAM" <> help "Path to a jammed event" <> hidden) <|> InjectManyEvents <$> strOption ( long "inject-event-list" <> metavar "JAM_LIST" <> help "Path to a jammed list of events" <> hidden) serfExe :: Parser (Maybe Text) serfExe = optional $ strOption $ metavar "PATH" <> long "serf" <> help "Path to serf binary to run ships in" <> hidden new :: Parser New new = do nPierPath <- optional pierPath nBootType <- newComet <|> newFakeship <|> newFromKeyfile nPillSource <- pillFromPath <|> pillFromURL nLite <- switch $ short 'l' <> long "lite" <> help "Boots ship in lite mode" nArvoDir <- option auto $ metavar "PATH" <> short 'A' <> long "arvo" <> value Nothing <> help "Replace initial clay filesys with contents of PATH" nSerfExe <- serfExe pure New{..} opts :: Parser Opts opts = do oAmesPort <- optional $ option auto $ metavar "PORT" <> short 'p' <> long "ames" <> help "Ames port" <> hidden oNoAmes <- switch $ long "no-ames" <> help "Run with Ames disabled." <> hidden oNoHttp <- switch $ long "no-http" <> help "Run with HTTP disabled." <> hidden oNoHttps <- switch $ long "no-https" <> help "Run with HTTPS disabled." <> hidden oHttpPort <- optional $ option auto $ metavar "PORT" <> long "http-port" <> help "HTTP port" <> hidden oHttpsPort <- optional $ option auto $ metavar "PORT" <> long "https-port" <> help "HTTPS port" <> hidden oLoopbackPort <- optional $ option auto $ metavar "PORT" <> long "loopback-port" <> help "Localhost-only HTTP port" <> hidden oInjectEvents <- injectEvents oHashless <- switch $ short 'S' <> long "hashless" <> help "Disable battery hashing (Ignored for now)" <> hidden oQuiet <- switch $ short 'q' <> long "quiet" <> help "Quiet" <> hidden oVerbose <- switch $ short 'v' <> long "verbose" <> help "Puts the serf and king into verbose mode" <> hidden oExit <- switch $ short 'x' <> long "exit" <> help "Exit immediately" <> hidden oDryRun <- switch $ long "dry-run" <> help "Persist no events and turn off Ames networking" <> hidden oDryFrom <- optional $ option auto $ metavar "EVENT" <> long "dry-from" <> help "Dry run from event number" <> hidden oTrace <- switch $ short 't' <> long "trace" <> help "Enable tracing" <> hidden oLocalhost <- switch $ short 'L' <> long "local" <> help "Localhost-only networking" <> hidden oCollectFx <- switch $ short 'f' <> long "collect-fx" <> help "Write effects to disk for debugging" <> hidden oOffline <- switch $ short 'O' <> long "offline" <> help "Run without any networking" <> hidden oFullReplay <- switch $ long "full-log-replay" <> help "Ignores snapshot and recomputes state from event log" <> hidden pure (Opts{..}) log :: Parser Log log = do lTarget <- optional $ ( flag' LogStderr $ long "log-to-stderr" <> long "stderr" <> help "Display logs on stderr" <> hidden ) <|> ( fmap LogFile . strOption $ long "log-to" <> metavar "LOG_FILE" <> help "Append logs to the given file" <> hidden ) <|> ( flag' LogOff $ long "no-logging" <> help "Disable logging entirely" <> hidden ) lLevel <- ( flag' LevelDebug $ long "log-debug" <> help "Log errors, warnings, info, and debug messages" <> hidden ) <|> ( flag' LevelInfo $ long "log-info" <> help "Log errors, warnings, and info" <> hidden ) <|> ( flag' LevelWarn $ long "log-warn" <> help "Log errors and warnings (default)" <> hidden ) <|> ( flag' LevelError $ long "log-error" <> help "Log errors only" <> hidden ) <|> pure LevelWarn pure (Log{..}) newShip :: Parser (Cmd, Log) newShip = (,) <$> (CmdNew <$> new <*> opts) <*> log runOneShip :: Parser (Run, Opts, Bool) runOneShip = (,,) <$> fmap Run pierPath <*> opts <*> df where df = switch (short 'd' <> long "daemon" <> help "Daemon mode" <> hidden) host :: Parser Host host = do hSharedHttpPort <- optional $ option auto $ metavar "PORT" <> long "shared-http-port" <> help "HTTP port" <> hidden hSharedHttpsPort <- optional $ option auto $ metavar "PORT" <> long "shared-https-port" <> help "HTTPS port" <> hidden hUseNatPmp <- ( flag' NatAlways $ long "port-forwarding" <> help "Always try to search for a router to forward ames ports" <> hidden ) <|> ( flag' NatNever $ long "no-port-forwarding" <> help "Disable trying to ask the router to forward ames ports" <> hidden ) <|> ( flag' NatWhenPrivateNetwork $ long "port-forwarding-when-internal" <> help ("Try asking the router to forward when ip is 192.168.0.0/16, " <> "172.16.0.0/12 or 10.0.0.0/8 (default).") <> hidden ) <|> (pure $ NatWhenPrivateNetwork) hSerfExe <- serfExe pure (Host{..}) runShip :: Parser (Cmd, Log) runShip = (,) <$> (CmdRun <$> host <*> some runOneShip) <*> log valPill :: Parser Bug valPill = do bPillPath <- strArgument (metavar "PILL" <> help "Path to pill") bPrintPil <- switch $ long "print-pill" <> help "Print pill" bPrintSeq <- switch $ long "print-boot" <> help "Print boot sequence" pure ValidatePill{..} keyfilePath :: Parser FilePath keyfilePath = strArgument (metavar "KEYFILE" <> help "Path to key file") firstEv :: Parser Word64 firstEv = option auto $ long "first" <> metavar "FST" <> help "starting from event FST" <> value 1 lastEv :: Parser Word64 lastEv = option auto $ long "last" <> metavar "LAS" <> help "ending with event LAS" <> value maxBound checkEvs :: Parser Bug checkEvs = ValidateEvents <$> pierPath <*> firstEv <*> lastEv checkFx :: Parser Bug checkFx = ValidateFX <$> pierPath <*> firstEv <*> lastEv replayEvs :: Parser Bug replayEvs = ReplayEvents <$> pierPath <*> lastEv browseEvs :: Parser Bug browseEvs = EventBrowser <$> pierPath checkDawn :: Parser Bug checkDawn = CheckDawn <$> keyfilePath bugCmd :: Parser (Cmd, Log) bugCmd = (flip (,) <$> log <*>) $ fmap CmdBug $ subparser $ command "validate-pill" ( info (valPill <**> helper) $ progDesc "Validate a pill file." ) <> command "collect-all-fx" ( info (allFx <**> helper) $ progDesc "Replay entire event log, collecting all effects" ) <> command "validate-events" ( info (checkEvs <**> helper) $ progDesc "Parse all data in event log" ) <> command "event-browser" ( info (browseEvs <**> helper) $ progDesc "Interactively view (and prune) event log" ) <> command "validate-effects" ( info (checkFx <**> helper) $ progDesc "Parse all data in event log" ) <> command "partial-replay" ( info (replayEvs <**> helper) $ progDesc "Replay up to N events" ) <> command "dawn" ( info (checkDawn <**> helper) $ progDesc "Test run dawn" ) <> command "comet" ( info (pure CheckComet) $ progDesc "Shows the list of stars accepting comets" ) conCmd :: Parser (Cmd, Log) conCmd = (,) <$> (CmdCon <$> pierPath) <*> log allFx :: Parser Bug allFx = do bPierPath <- strArgument (metavar "PIER" <> help "Path to pier") pure CollectAllFX{..} cmd :: Parser (Cmd, Log) cmd = subparser $ command "new" ( info (newShip <**> helper) $ progDesc "Boot a new ship." ) <> command "run" ( info (runShip <**> helper) $ progDesc "Run an existing ship." ) <> command "bug" ( info (bugCmd <**> helper) $ progDesc "Run a debugging sub-command." ) <> command "con" ( info (conCmd <**> helper) $ progDesc "Connect a terminal to a running urbit." )