daml/daml-assistant/exe/DA/Daml/Assistant.hs
Gary Verhaegen f8c0a35940
rewrite trigger docs to follow gsg (#10509)
* rewrite trigger docs to follow gsg

Per #10419 point 4, I've rewritten the Triggers section to build upon
the Getting Started Guide instead of inventing its own example.

Compared to #10395, this has a lot more explanations as this page must
now serve the dual purpose of being a possible "next step" from the GSG
and being the main reference page for triggers. It's also lost the "next
steps" section, which I think is a bit of a shame, but it doesn't really
make sense here.

There's also no easy way for people not interested in the GSG to follow
along; should we expose the "completed GSG" as a tempate?

CHANGELOG_BEGIN
CHANGELOG_END

* keep copy-trigger as a template

* fix copy-trigger project name

* make up gsg-trigger template

* remove awkward sentence, fix existing typo

* update code to use when{,Some}

* add  to

* swap emitCommands and getCommandsInFlight

* typo

* insist on state-correction perspective

* fix copy-trigger tests

* add back copy-trigger to whitelist

* add gsg-trigger to whitelist
2021-08-17 13:28:07 +00:00

383 lines
16 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- Copyright (c) 2021 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
-- SPDX-License-Identifier: Apache-2.0
module DA.Daml.Assistant
( main
) where
import DA.Signals
import qualified DA.Service.Logger as L
import qualified DA.Service.Logger.Impl.Pure as L
import qualified DA.Service.Logger.Impl.GCP as L
import DA.Daml.Project.Config
import DA.Daml.Project.Consts (sdkVersionEnvVar)
import DA.Daml.Assistant.Types
import DA.Daml.Assistant.Env
import DA.Daml.Assistant.Command
import DA.Daml.Assistant.Version
import DA.Daml.Assistant.Install
import DA.Daml.Assistant.Util
import System.Environment (getArgs, lookupEnv)
import System.FilePath
import System.Directory
import System.Process.Typed
import System.Exit
import System.IO
import Control.Exception.Safe
import qualified Data.Aeson as A
import qualified Data.HashMap.Strict as HM
import Data.Char
import Data.Maybe
import Data.List.Extra
import Data.Either.Extra
import qualified Data.Set as S
import qualified Data.Text as T
import Control.Monad.Extra
import Safe
-- | Run the assistant and exit.
main :: IO ()
-- Note that we do not close on stdin here.
-- The reason for this is that this would result in us terminating the child
-- process, e.g., daml-helper using TerminateProcess on Windows which does
-- not give it a chance to cleanup. Therefore, we only do this in daml-helper
-- which starts long-running server processes like sandbox via run-jar.
-- This means that closing stdin wont work for things like daml test
-- but that seems acceptable for now.
-- In theory, process groups or job control might provide a solution
-- but as Ben Gamari noticed, this is horribly unreliable
-- https://gitlab.haskell.org/ghc/ghc/issues/17777
-- so we are likely to make things worse rather than better.
main = do
damlPath <- handleErrors L.makeNopHandle getDamlPath
withLogger damlPath $ \logger -> handleErrors logger $ do
installSignalHandlers
builtinCommandM <- tryBuiltinCommand
case builtinCommandM of
Just builtinCommand -> do
env <- getDamlEnv damlPath (commandWantsProjectPath builtinCommand)
handleCommand env logger builtinCommand
Nothing -> do
env@Env{..} <- autoInstall =<< getDamlEnv damlPath (LookForProjectPath True)
-- We already know we can't parse the command without an installed SDK.
-- So if we can't find it, let the user know. This will happen whenever
-- auto-install is disabled and the project or environment specify a
-- missing SDK version.
case envSdkPath of
Nothing -> do
let installTarget
| Just v <- envSdkVersion = versionToString v
| otherwise = "latest"
hPutStr stderr . unlines $
[ "SDK not installed. Cannot run command without SDK."
, "To proceed, please install the SDK by running:"
, ""
, " daml install " <> installTarget
, ""
]
exitFailure
Just sdkPath -> do
sdkConfig <- readSdkConfig sdkPath
enriched <- hasEnrichedCompletion <$> getArgs
sdkCommands <- fromRightM throwIO (listSdkCommands sdkPath enriched sdkConfig)
userCommand <- getCommand sdkCommands
versionChecks env
handleCommand env logger userCommand
commandWantsProjectPath :: Command -> LookForProjectPath
commandWantsProjectPath cmd = LookForProjectPath $
case cmd of
Builtin (Install InstallOptions{..})
| Just RawInstallTarget_Project <- iTargetM -> True
| otherwise -> False
Builtin Uninstall{} -> False
_ -> True
-- | Perform version checks, i.e. warn user if project SDK version or assistant SDK
-- versions are out of date with the latest known release.
versionChecks :: Env -> IO ()
versionChecks Env{..} =
whenJust envLatestStableSdkVersion $ \latestVersion -> do
let isHead = maybe False isHeadVersion envSdkVersion
projectSdkVersionIsOld = isJust envProjectPath && envSdkVersion < Just latestVersion
assistantVersionIsOld = isJust envDamlAssistantSdkVersion &&
fmap unwrapDamlAssistantSdkVersion envDamlAssistantSdkVersion <
Just latestVersion
-- Project SDK version is outdated.
when (not isHead && projectSdkVersionIsOld) $ do
hPutStr stderr . unlines $
[ "SDK " <> versionToString latestVersion <> " has been released!"
, "See https://github.com/digital-asset/daml/releases/tag/v"
<> versionToString latestVersion <> " for details."
-- Carefully crafted wording to make sure its < 80 characters so
-- we do not get a line break.
, ""
]
-- Daml assistant is outdated.
when (not isHead && not projectSdkVersionIsOld && assistantVersionIsOld) $ do
hPutStr stderr . unlines $
[ "WARNING: Using an outdated version of the Daml assistant."
, "Please upgrade to the latest stable version by running:"
, ""
, " daml install latest"
, ""
]
-- | Perform auto-install if SDK version is given but SDK path is missing,
-- and auto-installs are not disabled in the $DAML_HOME/daml-config.yaml.
-- Returns the Env updated with the installed SdkPath.
autoInstall :: Env -> IO Env
autoInstall env@Env{..} = do
damlConfigE <- tryConfig $ readDamlConfig envDamlPath
let doAutoInstallE = queryDamlConfigRequired ["auto-install"] =<< damlConfigE
doAutoInstall = fromRight True doAutoInstallE
artifactoryApiKeyM = queryArtifactoryApiKey =<< eitherToMaybe damlConfigE
if doAutoInstall && isJust envSdkVersion && isNothing envSdkPath then do
-- sdk is missing, so let's install it!
let sdkVersion = fromJust envSdkVersion
options = InstallOptions
{ iTargetM = Nothing
, iSnapshots = False
, iQuiet = QuietInstall False
, iAssistant = InstallAssistant Auto
, iActivate = ActivateInstall False
, iForce = ForceInstall False
, iSetPath = SetPath Auto
, iBashCompletions = BashCompletions Auto
, iZshCompletions = ZshCompletions Auto
}
installEnv = InstallEnv
{ options = options
, damlPath = envDamlPath
, targetVersionM = Just sdkVersion
, missingAssistant = False
, installingFromOutside = False
, projectPathM = Nothing
, assistantVersion = envDamlAssistantSdkVersion
, artifactoryApiKeyM
, output = hPutStrLn stderr
-- Print install messages to stderr since the install
-- is only happening because of some other command,
-- and we don't want to mess up the other command's
-- output / have the install messages be gobbled
-- up by a pipe.
}
versionInstall installEnv sdkVersion
pure env { envSdkPath = Just (defaultSdkPath envDamlPath sdkVersion) }
else
pure env
handleCommand :: Env -> L.Handle IO -> Command -> IO ()
handleCommand env@Env{..} logger command = do
runCommand env command
args' <- anonimizeArgs
L.logJson logger L.Telemetry $ mkLogTable
[ ("event", "command")
, ("assistant-version", A.String (maybe ""
(versionToText . unwrapDamlAssistantSdkVersion)
envDamlAssistantSdkVersion))
, ("sdk-version", A.String (maybe "" versionToText envSdkVersion))
, ("args", A.toJSON args')
]
runCommand :: Env -> Command -> IO ()
runCommand env@Env{..} = \case
Builtin (Version VersionOptions{..}) -> do
installedVersionsE <- tryAssistant $ getInstalledSdkVersions envDamlPath
availableVersionsE <- tryAssistant $ refreshAvailableSdkVersions envCachePath
defaultVersionM <- tryAssistantM $ getDefaultSdkVersion envDamlPath
projectVersionM <- mapM getSdkVersionFromProjectPath envProjectPath
envSelectedVersionM <- lookupEnv sdkVersionEnvVar
snapshotVersionsE <- tryAssistant $
if vSnapshots
then getAvailableSdkSnapshotVersions
else pure []
let asstVersion = unwrapDamlAssistantSdkVersion <$> envDamlAssistantSdkVersion
envVersions = catMaybes
[ envSdkVersion
, envLatestStableSdkVersion
, guard vAssistant >> asstVersion
, projectVersionM
, defaultVersionM
]
latestVersionM = maximumMay $ concat
[ fromRight [] availableVersionsE
, fromRight [] installedVersionsE
, envVersions
]
isNotInstalled = -- defaults to False if "installed version" list is not available
case installedVersionsE of
Left _ -> const False
Right vs -> (`notElem` vs)
isAvailable = -- defaults to False if "available version" list is not available
case availableVersionsE of
Left _ -> const False
Right vs -> (`elem` vs)
versionAttrs v = catMaybes
[ ("selected by env var " <> pack sdkVersionEnvVar)
<$ guard (Just (unpack $ versionToText v) == envSelectedVersionM)
, "project SDK version from daml.yaml"
<$ guard (Just v == projectVersionM && isJust envProjectPath)
, "default SDK version for new projects"
<$ guard (Just v == defaultVersionM && isNothing envProjectPath)
, "daml assistant version"
<$ guard (Just v == asstVersion && vAssistant)
, "latest release"
<$ guard (Just v == latestVersionM && isNotInstalled v && isAvailable v)
, "not installed"
<$ guard (isNotInstalled v)
]
versions = nubSort . concat $
[ envVersions
, fromRight [] installedVersionsE
, if vAll then fromRight [] availableVersionsE else []
, fromRight [] snapshotVersionsE
]
versionTable = [ (versionToText v, versionAttrs v) | v <- versions ]
versionWidth = maximum (1 : map (T.length . fst) versionTable)
versionLines =
[ T.concat
[ " "
, v
, T.replicate (versionWidth - T.length v) " "
, if null attrs
then ""
else " (" <> T.intercalate ", " attrs <> ")"
]
| (v,attrs) <- versionTable ]
putStr . unpack $ T.unlines ("SDK versions:" : versionLines)
Builtin (Install options) -> wrapErr "Installing the SDK." $ do
install options envDamlPath envProjectPath envDamlAssistantSdkVersion
Builtin (Uninstall version) -> do
uninstallVersion env version
Builtin (Exec cmd args) -> do
wrapErr "Running executable in daml environment." $ do
path <- fromMaybe cmd <$> findExecutable cmd
dispatch env path args
Dispatch SdkCommandInfo{..} cmdArgs -> do
wrapErr ("Running " <> unwrapSdkCommandName sdkCommandName <> " command.") $ do
sdkPath <- required "Could not determine SDK path." envSdkPath
let path = unwrapSdkPath sdkPath </> unwrapSdkCommandPath sdkCommandPath
args = unwrapSdkCommandArgs sdkCommandArgs ++ unwrapUserCommandArgs cmdArgs
dispatch env path args
dispatch :: Env -> FilePath -> [String] -> IO ()
dispatch env path args = do
dispatchEnv <- getDispatchEnv env
requiredIO "Failed to spawn command subprocess." $
runProcess_ (setEnv dispatchEnv $ proc path args)
handleErrors :: forall t. L.Handle IO -> IO t -> IO t
handleErrors logger m = m `catches`
[ Handler (go . displayException @AssistantError)
, Handler (go . displayException @ConfigError)
]
where
go :: String -> IO t
go err = do
hPutStrLn stderr err
L.logJson logger L.Error $ mkLogTable
[ ("event", "error")
, ("message", A.String (T.pack err))
]
exitFailure
withLogger :: DamlPath -> (L.Handle IO -> IO ()) -> IO ()
withLogger (DamlPath damlPath) k = do
cache <- getCachePath
let cachePath = unwrapCachePath cache
let logOfInterest prio = prio `elem` [L.Telemetry, L.Warning, L.Error]
gcpConfig = L.GCPConfig
{ gcpConfigTag = "assistant"
, gcpConfigCachePath = Just cachePath
, gcpConfigDamlPath = Just damlPath
}
isOptedIn <- L.isOptedIn cachePath
isOptedOut <- L.isOptedOut cachePath
if isOptedIn && not isOptedOut
then
L.withGcpLogger gcpConfig logOfInterest L.makeNopHandle $ \gcpStateM logger -> do
whenJust gcpStateM $ \gcpState -> L.logMetaData gcpState
k logger
else
k L.makeNopHandle
-- | Get the arguments to `daml` and anonimize all but the first.
-- That way, the daml command doesn't get accidentally anonimized.
anonimizeArgs :: IO [T.Text]
anonimizeArgs = do
args <- map T.pack <$> getArgs
case args of
[] -> pure []
argsHead : argsTail -> do
argsTail' <- concatMapM anonimizeArg argsTail
pure (argsHead : argsTail')
-- | Anonimize an argument to `daml`.
anonimizeArg :: T.Text -> IO [T.Text]
anonimizeArg arg = do
forM (T.splitOn "=" arg) $ \part -> do
let partStr = T.unpack part
isPath <- doesPathExist partStr
pure $ if (part `S.member` argWhitelist) || (isFlag partStr && not isPath)
then part
else ""
where
isFlag :: [Char] -> Bool
isFlag ['-', _] = True
isFlag ('-':'-':cs) = all isFlagChar cs
isFlag _ = False
isFlagChar :: Char -> Bool
isFlagChar c = isAlphaNum c || c == '-'
argWhitelist :: S.Set T.Text
argWhitelist = S.fromList
[ "version", "yes", "no", "auto"
, "install", "latest", "project"
, "uninstall"
, "studio", "never", "always", "published"
, "new", "skeleton", "empty-skeleton", "quickstart-java", "quickstart-scala", "copy-trigger", "gsg-trigger"
, "daml-intro-1", "daml-intro-2", "daml-intro-3", "daml-intro-4"
, "daml-intro-5", "daml-intro-6", "daml-intro-7", "script-example"
, "migrate"
, "init"
, "build"
, "test"
, "start", "none"
, "clean"
, "damlc", "ide", "license", "package", "docs", "visual", "visual-web", "inspect-dar", "validate-dar", "doctest", "lint"
, "sandbox", "INFO", "TRACE", "DEBUG", "WARN", "ERROR"
, "navigator", "server", "console", "dump-graphql-schema", "create-config", "static", "simulated", "wallclock"
, "extractor", "prettyprint", "postgresql"
, "ledger", "list-parties", "allocate-parties", "upload-dar", "fetch-dar"
, "codegen", "java", "scala", "js"
, "deploy"
, "json-api"
, "trigger", "trigger-service", "list"
, "oauth2-middleware"
, "script"
, "test-script"
]
mkLogTable :: [(T.Text, A.Value)] -> A.Value
mkLogTable fields = A.Object . HM.fromList $
("source", A.String "daml-assistant") : fields