mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
f8c0a35940
* 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
383 lines
16 KiB
Haskell
383 lines
16 KiB
Haskell
-- 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 won’t 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 it’s < 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
|