packages feed

haskell-debugger 0.8.0.0 → 0.9.0.0

raw patch · 26 files changed

+1488/−598 lines, 26 filesdep +aeson-prettydep +networkdep +network-rundep ~aesondep ~asyncdep ~bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: aeson-pretty, network, network-run, random, regex, tasty, tasty-golden, tasty-hunit, temporary, unordered-containers

Dependency ranges changed: aeson, async, bytestring, containers, dap, filepath, process, text

API changes (from Hackage documentation)

- GHC.Debugger.Interface.Messages: instance Data.Aeson.Types.FromJSON.FromJSON GHC.Types.Tickish.BreakpointId
- GHC.Debugger.Interface.Messages: instance Data.Aeson.Types.ToJSON.ToJSON GHC.Types.Tickish.BreakpointId
- GHC.Debugger.Interface.Messages: instance GHC.Internal.Show.Show GHC.Types.Tickish.BreakpointId
+ GHC.Debugger.Interface.Messages: instance Data.Aeson.Types.FromJSON.FromJSON GHC.ByteCode.Breakpoints.InternalBreakpointId
+ GHC.Debugger.Interface.Messages: instance Data.Aeson.Types.ToJSON.ToJSON GHC.ByteCode.Breakpoints.InternalBreakpointId
+ GHC.Debugger.Interface.Messages: instance GHC.Internal.Show.Show GHC.ByteCode.Breakpoints.InternalBreakpointId
+ GHC.Debugger.Logger: Verbosity :: Severity -> Verbosity
+ GHC.Debugger.Logger: [threshold] :: Verbosity -> Severity
+ GHC.Debugger.Logger: applyVerbosity :: Verbosity -> Recorder (WithSeverity a) -> Recorder (WithSeverity a)
+ GHC.Debugger.Logger: newtype Verbosity
+ GHC.Debugger.Monad: getInternalBreaksOf :: BreakpointId -> Debugger [InternalBreakpointId]
+ GHC.Debugger.Runtime: expandTerm :: HscEnv -> Term -> IO Term
- GHC.Debugger.Interface.Messages: BreakFound :: Bool -> BreakpointId -> SourceSpan -> BreakFound
+ GHC.Debugger.Interface.Messages: BreakFound :: Bool -> [InternalBreakpointId] -> SourceSpan -> BreakFound
- GHC.Debugger.Interface.Messages: EvalStopped :: Maybe BreakpointId -> EvalResult
+ GHC.Debugger.Interface.Messages: EvalStopped :: Maybe InternalBreakpointId -> EvalResult
- GHC.Debugger.Interface.Messages: [breakId] :: EvalResult -> Maybe BreakpointId
+ GHC.Debugger.Interface.Messages: [breakId] :: EvalResult -> Maybe InternalBreakpointId
- GHC.Debugger.Monad: getActiveBreakpoints :: Maybe FilePath -> Debugger [BreakpointId]
+ GHC.Debugger.Monad: getActiveBreakpoints :: Maybe FilePath -> Debugger [InternalBreakpointId]
- GHC.Debugger.Monad: registerBreakpoint :: BreakpointId -> BreakpointStatus -> BreakpointKind -> Debugger Bool
+ GHC.Debugger.Monad: registerBreakpoint :: BreakpointId -> BreakpointStatus -> BreakpointKind -> Debugger (Bool, [InternalBreakpointId])

Files

CHANGELOG.md view
@@ -1,8 +1,18 @@ # Revision history for haskell-debugger +## 0.8.0.0 -- 2025-09-19++* Allow defaults for all settings except `entryFile` and return a proper error in that case+* Fix bug that crashed debugger when attempting to load newtype constructor closure+* Fix bug that broke displaying newtype variables+* (Vscode) Allow the debugger to be run without a launch.json file+* Don't display functions as a forceable thunk, instead just the type+* Add `--version` flag+ ## 0.7.0.0 -- 2025-09-10  * Fix line buffering of debuggee output (thus, stepping through a print line, will indeed print it to the console now).+    * In fact, this was only caused by not building with `--enable-executable-dynamic`  ## 0.6.0.0 -- 2025-09-10 
haskell-debugger.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.12 name:               haskell-debugger-version:            0.8.0.0+version:            0.9.0.0 synopsis:     A step-through machine-interface debugger for GHC Haskell @@ -93,8 +93,7 @@ executable hdb     import:           warnings     main-is:          Main.hs-    other-modules:    Development.Debug.Adapter.Flags,-                      Development.Debug.Adapter.Breakpoints,+    other-modules:    Development.Debug.Adapter.Breakpoints,                       Development.Debug.Adapter.Stepping,                       Development.Debug.Adapter.Stopped,                       Development.Debug.Adapter.Evaluation,@@ -104,7 +103,16 @@                       Development.Debug.Adapter.Exit,                       Development.Debug.Adapter.Handles,                       Development.Debug.Adapter,++                      Development.Debug.Adapter.Proxy,+                       Development.Debug.Interactive,++                      Development.Debug.Session.Setup,++                      Development.Debug.Options,+                      Development.Debug.Options.Parser,+                       Paths_haskell_debugger     autogen-modules:  Paths_haskell_debugger     build-depends:@@ -112,6 +120,7 @@         exceptions, aeson, bytestring,         containers, filepath,         process, mtl, unix,+        unordered-containers >= 0.2.19 && < 0.3,          haskell-debugger,         hie-bios,@@ -122,9 +131,11 @@         prettyprinter,          directory >= 1.3.9 && < 1.4,+        network >= 3.2.8,+        network-run >= 0.4.4,         async >= 2.2.5 && < 2.3,         text >= 2.1 && < 2.3,-        dap >= 0.2 && < 1,+        dap >= 0.3 && < 1,          haskeline >= 0.8 && < 1,         optparse-applicative >= 0.18 && < 0.20@@ -136,11 +147,28 @@ test-suite haskell-debugger-test     import:           warnings     default-language: Haskell2010-    -- other-modules:     -- other-extensions:     type:             exitcode-stdio-1.0     hs-source-dirs:   test/haskell/     main-is:          Main.hs+    other-modules:    Test.DAP.RunInTerminal, Test.DAP, Test.Utils     build-depends:         base >=4.14,-        haskell-debugger+        haskell-debugger,+        bytestring, text,+        containers,+        filepath,+        process,+        temporary >= 1.3,++        unordered-containers,+        aeson-pretty >= 0.8.10, async >= 2.2.5,+        dap, network, aeson, network-run,+        random >= 1.3.1,++        tasty >= 1.5.3,+        tasty-golden >= 2.3.5,+        tasty-hunit >= 0.10.2,+        regex >= 1.1+    build-tool-depends: haskell-debugger:hdb+    ghc-options: -threaded
haskell-debugger/GHC/Debugger.hs view
@@ -5,7 +5,6 @@  import System.Exit import Control.Monad.IO.Class-import GHC.Types.Name.Occurrence (sizeOccEnv)  import GHC.Debugger.Breakpoint import GHC.Debugger.Evaluation
haskell-debugger/GHC/Debugger/Breakpoint.hs view
@@ -10,7 +10,6 @@ import Data.Bits (xor)  import GHC-import GHC.Types.Name.Occurrence (sizeOccEnv) import GHC.ByteCode.Breakpoints import GHC.Utils.Error (logOutput) import GHC.Driver.DynFlags as GHC@@ -76,42 +75,29 @@         Just (bix, spn) -> do           let bid = BreakpointId { bi_tick_mod = ms_mod modl                                  , bi_tick_index = bix }-#if MIN_VERSION_ghc(9,14,2)           (changed, ibis) <- registerBreakpoint bid bp_status ModuleBreakpointKind-#else-          changed <- registerBreakpoint bid bp_status ModuleBreakpointKind-#endif           return $ BreakFound             { changed = changed             , sourceSpan = realSrcSpanToSourceSpan spn-#if MIN_VERSION_ghc(9,14,2)             , breakId = ibis-#else-            , breakId = bid-#endif             } setBreakpoint FunctionBreak{function} bp_status = do   logger <- getLogger   resolveFunctionBreakpoint function >>= \case-    Left e -> error (showPprUnsafe e)+    Left e -> do+      liftIO $ logOutput logger $ text $+        "Failed to resolve function breakpoint " ++ function ++ ".\n" ++ showPprUnsafe e ++ "\nIgnoring..."+      return BreakNotFound     Right (modl, mod_info, fun_str) -> do       let modBreaks = GHC.modInfoModBreaks mod_info           applyBreak (bix, spn) = do             let bid = BreakpointId { bi_tick_mod = modl                                    , bi_tick_index = bix }-#if MIN_VERSION_ghc(9,14,2)             (changed, ibis) <- registerBreakpoint bid bp_status FunctionBreakpointKind-#else-            changed <- registerBreakpoint bid bp_status FunctionBreakpointKind-#endif             return $ BreakFound               { changed = changed               , sourceSpan = realSrcSpanToSourceSpan spn-#if MIN_VERSION_ghc(9,14,2)               , breakId = ibis-#else-              , breakId = bid-#endif               }       case maybe [] (findBreakForBind fun_str . imodBreaks_modBreaks) modBreaks of         []  -> do
haskell-debugger/GHC/Debugger/Evaluation.hs view
@@ -11,11 +11,13 @@ {-# LANGUAGE ViewPatterns #-} module GHC.Debugger.Evaluation where +import GHC.Utils.Outputable import Control.Monad.IO.Class import Control.Monad.Catch import qualified Data.List as List import Data.Maybe import System.FilePath+import System.Directory import qualified Prettyprinter as Pretty  import GHC@@ -27,7 +29,6 @@ import GHC.Driver.Env as GHC import GHC.Runtime.Debugger.Breakpoints as GHC import qualified GHC.Unit.Module.ModSummary as GHC-import GHC.ByteCode.Breakpoints import GHC.Types.Name.Occurrence (mkVarOccFS) import GHC.Types.Name.Reader as RdrName (mkOrig) import GHC.Utils.Outputable as GHC@@ -116,9 +117,11 @@      findUnitIdOfEntryFile :: GhcMonad m => FilePath -> m GHC.ModSummary     findUnitIdOfEntryFile fp = do+      afp <- normalise <$> liftIO (makeAbsolute fp)       modSums <- getAllLoadedModules-      case List.find ((Just fp ==) . fmap normalise . GHC.ml_hs_file . GHC.ms_location ) modSums of-        Nothing -> error $ "findUnitIdOfEntryFile: no unit id found for: " ++ fp+      let normalisedModLoc = fmap normalise . GHC.ml_hs_file . GHC.ms_location+      case List.find ((Just afp ==) . normalisedModLoc) modSums of+        Nothing -> error $ "findUnitIdOfEntryFile: no unit id found for: " ++ fp ++ "\nCandidates were:\n" ++ unlines (map (show . normalisedModLoc) modSums)         Just summary -> pure summary  -- | Resume execution of the stopped debuggee program@@ -196,11 +199,7 @@       -- TODO: force the exception to display string with Backtrace?       return EvalStopped{breakId = Nothing}     ExecBreak {breakNames = _, breakPointId} ->-#if MIN_VERSION_ghc(9,14,2)       return EvalStopped{breakId = breakPointId}-#else-      return EvalStopped{breakId = toBreakpointId <$> breakPointId}-#endif  -- | Get the value and type of a given 'Name' as rendered strings in 'VarInfo'. inspectName :: Name -> Debugger (Maybe VarInfo)
haskell-debugger/GHC/Debugger/Interface/Messages.hs view
@@ -210,11 +210,7 @@   = BreakFound     { changed :: !Bool     -- ^ Did the status of the found breakpoint change?-#if MIN_VERSION_ghc(9,14,2)     , breakId :: [GHC.InternalBreakpointId]-#else-    , breakId :: GHC.BreakpointId-#endif     -- ^ Internal breakpoint identifier (module + ix) (TODO: Don't expose GHC)     , sourceSpan :: SourceSpan     -- ^ Source span for interface@@ -233,11 +229,7 @@ data EvalResult   = EvalCompleted { resultVal :: String, resultType :: String }   | EvalException { resultVal :: String, resultType :: String }-#if MIN_VERSION_ghc(9,14,2)   | EvalStopped   { breakId :: Maybe GHC.InternalBreakpointId {-^ Did we stop at an exception (@Nothing@) or at a breakpoint (@Just@)? -} }-#else-  | EvalStopped   { breakId :: Maybe GHC.BreakpointId {-^ Did we stop at an exception (@Nothing@) or at a breakpoint (@Just@)? -} }-#endif   -- | Evaluation failed for some reason other than completed/completed-with-exception/stopped.   | EvalAbortedWith String   deriving (Show, Generic)@@ -291,8 +283,6 @@ instance FromJSON VarInfo instance FromJSON VarFields -#if MIN_VERSION_ghc(9,14,2)- instance Show GHC.InternalBreakpointId where   show (GHC.InternalBreakpointId m ix) = "InternalBreakpointId " ++ GHC.showPprUnsafe m ++ " " ++ show ix @@ -306,20 +296,3 @@   parseJSON = withObject "InternalBreakpointId" $ \v -> GHC.InternalBreakpointId         <$> (Module <$> (stringToUnit <$> v .: "module_unit") <*> (mkModuleName <$> v .: "module_name"))         <*> v .: "ix"-#else--instance Show GHC.BreakpointId where-  show (GHC.BreakpointId m ix) = "BreakpointId " ++ GHC.showPprUnsafe m ++ " " ++ show ix--instance ToJSON GHC.BreakpointId where-  toJSON (GHC.BreakpointId (Module unit mn) ix) =-    object [ "module_name" .= moduleNameString mn-           , "module_unit" .= unitString unit-           , "ix" .= ix-           ]-instance FromJSON GHC.BreakpointId where-  parseJSON = withObject "BreakpointId" $ \v -> GHC.BreakpointId-        <$> (Module <$> (stringToUnit <$> v .: "module_unit") <*> (mkModuleName <$> v .: "module_name"))-        <*> v .: "ix"--#endif
haskell-debugger/GHC/Debugger/Logger.hs view
@@ -22,6 +22,9 @@   cmap,   cmapIO,   cmapWithSev,+  -- * Verbosity+  Verbosity(..),+  applyVerbosity,    -- * Pretty printing of logs   renderPrettyWithSeverity,@@ -39,7 +42,7 @@ import Control.Monad.IO.Class import Control.Monad ((>=>)) -import Colog.Core (Severity(..), WithSeverity(..))+import Colog.Core (Severity(..), WithSeverity(..), filterBySeverity) import qualified Colog.Core as Colog import Data.Functor.Contravariant (Contravariant (contramap)) import Data.Text (Text)@@ -66,8 +69,16 @@     Recorder       { logger_ = \_ -> pure () } +-- | Logging verbosity, where all Severities matching or exceeding the threshold are printed+newtype Verbosity = Verbosity { threshold :: Severity }++-- | Make this logger never report messages whose severity is below the given verbosity severity.+applyVerbosity :: Verbosity -> Recorder (WithSeverity a) -> Recorder (WithSeverity a)+applyVerbosity Verbosity{threshold} rc+  = fromCologAction $ filterBySeverity threshold getSeverity (toCologAction rc)+ logWith :: (HasCallStack, MonadIO m) => Recorder (WithSeverity msg) -> Severity -> msg -> m ()-logWith (Recorder logger_) sev msg = logger_ $ WithSeverity msg sev+logWith Recorder{logger_} sev msg = logger_ $ WithSeverity msg sev  cmap :: (a -> b) -> Recorder b -> Recorder a cmap = contramap
haskell-debugger/GHC/Debugger/Monad.hs view
@@ -182,11 +182,7 @@ -- 'BreakpointStatus' being set. -- -- Returns @True@ when the breakpoint status is changed.-#if MIN_VERSION_ghc(9,14,2) registerBreakpoint :: GHC.BreakpointId -> BreakpointStatus -> BreakpointKind -> Debugger (Bool, [GHC.InternalBreakpointId])-#else-registerBreakpoint :: GHC.BreakpointId -> BreakpointStatus -> BreakpointKind -> Debugger Bool-#endif registerBreakpoint bp@GHC.BreakpointId                     { GHC.bi_tick_mod = mod                     , GHC.bi_tick_index = bid } status kind = do@@ -194,13 +190,9 @@   -- Set breakpoint in GHC session   let breakpoint_count = breakpointStatusInt status   hsc_env <- GHC.getSession-#if MIN_VERSION_ghc(9,14,2)   internal_break_ids <- getInternalBreaksOf bp   forM_ internal_break_ids $ \ibi -> do     GHC.setupBreakpoint (hscInterp hsc_env) ibi breakpoint_count-#else-  GHC.setupBreakpoint (hscInterp hsc_env) bp breakpoint_count-#endif    -- Register breakpoint in Debugger state   brksRef <- asks activeBreakpoints@@ -240,21 +232,13 @@    -- no races since the debugger execution is run in a single thread   liftIO $ writeIORef brksRef newBrks-#if MIN_VERSION_ghc(9,14,2)   return (changed, internal_break_ids)-#else-  return changed-#endif   -- | Get a list with all currently active breakpoints on the given module (by path) -- -- If the path argument is @Nothing@, get all active function breakpoints instead-#if MIN_VERSION_ghc(9,14,2) getActiveBreakpoints :: Maybe FilePath -> Debugger [GHC.InternalBreakpointId]-#else-getActiveBreakpoints :: Maybe FilePath -> Debugger [GHC.BreakpointId]-#endif getActiveBreakpoints mfile = do   m <- asks activeBreakpoints >>= liftIO . readIORef   case mfile of@@ -262,11 +246,7 @@       mms <- getModuleByPath file       case mms of         Right ms ->-#if MIN_VERSION_ghc(9,14,2)           concat <$> mapM getInternalBreaksOf-#else-          return-#endif             [ GHC.BreakpointId mod bix             | (mod, im) <- moduleEnvToList m             , mod == ms_mod ms@@ -277,11 +257,7 @@           displayWarnings [e]           return []     Nothing -> do-#if MIN_VERSION_ghc(9,14,2)       concat <$> mapM getInternalBreaksOf-#else-      return-#endif         [ GHC.BreakpointId mod bix         | (mod, im) <- moduleEnvToList m         , (bix, (status, kind)) <- IM.assocs im@@ -452,7 +428,6 @@  -------------------------------------------------------------------------------- -#if MIN_VERSION_ghc(9,14,2) -- | Find all the internal breakpoints that use the given source-level breakpoint id getInternalBreaksOf :: BreakpointId -> Debugger [InternalBreakpointId] getInternalBreaksOf bi = do@@ -460,4 +435,3 @@   return $     fromMaybe [] {- still not found after refresh -} $       lookupBreakpointOccurrences bs bi-#endif
haskell-debugger/GHC/Debugger/Runtime.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE GADTs, LambdaCase, NamedFieldPuns #-}+{-# LANGUAGE OrPatterns, GADTs, LambdaCase, NamedFieldPuns #-} module GHC.Debugger.Runtime where  import Data.IORef@@ -9,6 +9,7 @@ import GHC.Types.FieldLabel import GHC.Tc.Utils.TcType import GHC.Runtime.Eval+import GHC.Runtime.Heap.Inspect  import GHC.Debugger.Runtime.Term.Key import GHC.Debugger.Runtime.Term.Cache@@ -21,8 +22,9 @@ -- scratch and stored in the cache. obtainTerm :: TermKey -> Debugger Term obtainTerm key = do-  tc_ref <- asks termCache-  tc     <- liftIO $ readIORef tc_ref+  hsc_env <- getSession+  tc_ref  <- asks termCache+  tc      <- liftIO $ readIORef tc_ref   case lookupTermCache key tc of     -- cache miss: reconstruct, then store.     Nothing ->@@ -37,17 +39,8 @@         getTerm = \case           FromId i -> GHC.obtainTermFromId (depth i) False{-don't force-} i           FromPath k pf -> do-            term <- getTerm k >>= \case-              -- When the key points to a Suspension, the real thing should-              -- already be forced. It's just that the shallow depth meant we-              -- returned a Suspension nonetheless while recursing in `getTerm`.-              t@Suspension{} -> do-                t' <- seqTerm t-                -- update term cache with intermediate values?-                -- insertTermCache k t'-                return t'-              t -> return t-            return $ case term of+            term <- getTerm k+            liftIO $ expandTerm hsc_env $ case term of               Term{dc=Right dc, subTerms} -> case pf of                 PositionalIndex ix -> subTerms !! (ix-1)                 LabeledField fl    ->@@ -59,11 +52,24 @@               _ -> error "Unexpected term for the given TermKey"        in do         term <- getTerm key-        liftIO $ writeIORef tc_ref (insertTermCache key term tc)+        liftIO $ modifyIORef tc_ref (insertTermCache key term)         return term      -- cache hit     Just hit -> return hit++-- | Before returning a 'Term' we want to expand its heap representation up to the 'defaultDepth'+--+-- For 'Id's, this is done by 'GHC.obtainTermFromId'. For other 'TermKey's this+-- function should be used+expandTerm :: HscEnv -> Term -> IO Term+expandTerm hsc_env term = case term of+  Term{val, ty} -> cvObtainTerm hsc_env defaultDepth False ty val+  (NewtypeWrap{}; RefWrap{}) -> do+    -- TODO: we don't do anything clever here yet+    return term+  -- For other terms there's no point in trying to expand+  (Suspension{}; Prim{}) -> return term  -- | A boring type is one for which we don't care about the structure and would -- rather see "whole" when being inspected. Strings and literals are a good
haskell-debugger/GHC/Debugger/Session.hs view
@@ -29,7 +29,6 @@ import qualified Crypto.Hash.SHA1                    as H import qualified Data.ByteString.Base16              as B16 import qualified Data.ByteString.Char8               as B-import Data.Function import qualified Data.List.NonEmpty as NonEmpty import qualified Data.Map as Map import qualified Data.List as L@@ -87,7 +86,9 @@         -- If we don't end up with a target for the current file in the end, then         -- we will report it as an error for that file         let abs_fp = rootDir </> cfp-        let special_target = mkSimpleTarget df abs_fp+        -- Canonicalize! Why? Because the targets we get from the cradle are normalised and if we don't normalise the "special target" then they aren't deduplicated properly.+        canon_fp <- liftIO $ Directory.canonicalizePath abs_fp+        let special_target = mkSimpleTarget df canon_fp         pure $ (df, special_target : targets) NonEmpty.:| []     where       initMulti unitArgFiles =@@ -96,10 +97,11 @@           initOne args       initOne this_opts = do         (dflags', targets') <- addCmdOpts this_opts dflags-        let targets = HIE.makeTargetsAbsolute root targets'-            root = case workingDirectory dflags' of+        let root = case workingDirectory dflags' of               Nothing   -> compRoot               Just wdir -> compRoot </> wdir+        root_canon <- liftIO $ Directory.canonicalizePath root+        let targets = HIE.makeTargetsAbsolute root_canon targets'         cacheDirs <- liftIO $ getCacheDirs (takeFileName root) this_opts         let dflags'' =               setWorkingDirectory root $
haskell-debugger/GHC/Debugger/Stopped.hs view
@@ -196,12 +196,8 @@         case GHC.resumeBreakpointId r of           Nothing -> return []           Just ibi -> do-#if MIN_VERSION_ghc(9,14,2)             curr_modl <- liftIO $ bi_tick_mod . getBreakSourceId ibi <$>                           readIModBreaks (hsc_HUG hsc_env) ibi-#else-            let curr_modl = ibi_tick_mod ibi-#endif             things <- typeEnvElts <$> getTopEnv curr_modl             mapM (\tt -> do               nameStr <- display (getName tt)@@ -212,12 +208,8 @@         case GHC.resumeBreakpointId r of           Nothing -> return []           Just ibi -> do-#if MIN_VERSION_ghc(9,14,2)             curr_modl <- liftIO $ bi_tick_mod . getBreakSourceId ibi <$>                           readIModBreaks (hsc_HUG hsc_env) ibi-#else-            let curr_modl = ibi_tick_mod ibi-#endif             names <- map greName . globalRdrEnvElts <$> getTopImported curr_modl             mapM (\n-> do               nameStr <- display n
haskell-debugger/GHC/Debugger/Stopped/Variables.hs view
@@ -43,6 +43,9 @@     termToVarInfo key term  -- | Construct the VarInfos of the fields ('VarFields') of the given 'TermKey'/'Term'+--+-- This is used to come up with terms for the fields of an already `seq`ed+-- variable which was expanded. termVarFields :: TermKey -> Term -> Debugger VarFields termVarFields top_key top_term = 
hdb/Development/Debug/Adapter.hs view
@@ -1,7 +1,9 @@ module Development.Debug.Adapter where  import Control.Concurrent.MVar+import Control.Concurrent.Chan import qualified Data.IntSet as IS+import qualified Data.ByteString as BS import qualified Data.Map as Map import qualified Data.Text as T import System.FilePath@@ -24,15 +26,17 @@       { syncRequests :: MVar D.Command       , syncResponses :: MVar D.Response       , nextFreshBreakpointId :: !BreakpointId-#if MIN_VERSION_ghc(9,14,2)       , breakpointMap :: Map.Map GHC.InternalBreakpointId BreakpointSet-#else-      , breakpointMap :: Map.Map GHC.BreakpointId BreakpointSet-#endif       , entryFile :: FilePath       , entryPoint :: String       , entryArgs :: [String]       , projectRoot :: FilePath+      , syncProxyIn :: Chan BS.ByteString+        -- ^ Read input to the debuggee from the proxy+      , syncProxyOut :: Chan BS.ByteString+        -- ^ Write output from the debuggee to the proxy+      , syncProxyErr :: Chan BS.ByteString+        -- ^ Write stderr from the debuggee to the proxy       }  type BreakpointId = Int
hdb/Development/Debug/Adapter/Breakpoints.hs view
@@ -116,7 +116,6 @@     BreakFoundNoLoc _ch -> pure [ DAP.defaultBreakpoint { DAP.breakpointVerified = True } ]     BreakFound _ch iid ss -> do       source <- fileToSource ss.file-#if MIN_VERSION_ghc(9,14,2)       bids <- mapM registerNewBreakpoint iid       pure $ map (\bid -> DAP.defaultBreakpoint         { DAP.breakpointVerified = True@@ -127,25 +126,9 @@         , DAP.breakpointEndColumn = Just ss.endCol         , DAP.breakpointId = Just bid         }) bids-#else-      bid <- registerNewBreakpoint iid-      pure [ DAP.defaultBreakpoint-        { DAP.breakpointVerified = True-        , DAP.breakpointSource = Just source-        , DAP.breakpointLine = Just ss.startLine-        , DAP.breakpointEndLine = Just ss.endLine-        , DAP.breakpointColumn = Just ss.startCol-        , DAP.breakpointEndColumn = Just ss.endCol-        , DAP.breakpointId = Just bid-        } ]-#endif  -- | Adds new BreakpointId for a givent StgPoint-#if MIN_VERSION_ghc(9,14,2) registerNewBreakpoint :: GHC.InternalBreakpointId -> DebugAdaptor BreakpointId-#else-registerNewBreakpoint :: GHC.BreakpointId -> DebugAdaptor BreakpointId-#endif registerNewBreakpoint breakpoint = do   bkpId <- getFreshBreakpointId   updateDebugSession $ \das@DAS{..} -> das {breakpointMap = Map.insertWith mappend breakpoint (IS.singleton bkpId) breakpointMap}
− hdb/Development/Debug/Adapter/Flags.hs
@@ -1,281 +0,0 @@-{-# LANGUAGE CPP #-}-{-# LANGUAGE LambdaCase #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}--module Development.Debug.Adapter.Flags where--import Control.Applicative ((<|>))-import Control.Exception (handleJust)-import Control.Monad-import Control.Monad.Except-import Control.Monad.IO.Class-import Control.Monad.Trans.Maybe-import Data.Bifunctor-import Data.Function-import Data.Functor ((<&>))-import Data.Maybe-import Data.Version-import Data.Void-import System.Directory hiding (findFile)-import System.FilePath-import System.IO.Error-import Text.ParserCombinators.ReadP (readP_to_S)-import Prettyprinter--import qualified HIE.Bios as HIE-import qualified HIE.Bios.Config as Config-import qualified HIE.Bios.Cradle as HIE-import qualified HIE.Bios.Environment as HIE-import qualified HIE.Bios.Types as HIE-import qualified Hie.Cabal.Parser as Implicit-import qualified Hie.Locate as Implicit-import qualified Hie.Yaml as Implicit--import GHC.Debugger.Logger--data FlagsLog-  = HieBiosLog HIE.Log-  | LogCradle (HIE.Cradle Void)--instance Pretty FlagsLog where-  pretty = \ case-    HieBiosLog msg -> pretty msg-    LogCradle crdl -> "Determined Cradle:" <+> viaShow crdl---- | Flags inferred by @hie-bios@ to invoke GHC-data HieBiosFlags = HieBiosFlags-      { ghcInvocation :: [String]-      , libdir :: FilePath-      , units :: [String]-      , rootDir :: FilePath-      -- ^ Root dir as reported by the 'Cradle'-      , componentDir :: FilePath-      -- ^ Root dir of the loaded 'ComponentOptions'.-      -- Important for multi-package cabal projects, as packages are not in the-      -- root of the cradle, but in some sub-directory.-      }--hieBiosCradle :: Recorder (WithSeverity FlagsLog) {-^ Logger -}-              -> FilePath {-^ Project root -}-              -> FilePath {-^ Entry file relative to root -}-              -> IO (Either String (HIE.Cradle Void))-hieBiosCradle logger root relTarget = runExceptT $ do-  let target = root </> relTarget-  explicitCradle <- HIE.findCradle target & liftIO-  cradle <- maybe (loadImplicitCradle hieBiosLogger target)-                  (HIE.loadCradle hieBiosLogger) explicitCradle & liftIO-  logWith logger Info $ LogCradle cradle-  pure cradle-  where-    hieBiosLogger = toCologAction $ cmapWithSev HieBiosLog logger--hieBiosRuntimeGhcVersion :: Recorder (WithSeverity FlagsLog)-                         -> HIE.Cradle Void-                         -> IO (Either String Version)-hieBiosRuntimeGhcVersion _logger cradle = runExceptT $ do-  out <- liftIO (HIE.getRuntimeGhcVersion cradle) >>= unwrapCradleResult "Failed to get runtime GHC version"-  case versionMaybe out of-    Nothing -> throwError $ "Failed to parse GHC version: " <> out-    Just ver -> pure ver---- | Make 'HieBiosFlags' from the given target file-hieBiosFlags :: Recorder (WithSeverity FlagsLog) {-^ Logger -}-             -> HIE.Cradle Void {-^ Project cradle the entry file belongs to -}-             -> FilePath {-^ Project root -}-             -> FilePath {-^ Entry file relative to root -}-             -> IO (Either String HieBiosFlags)-hieBiosFlags _logger cradle root relTarget = runExceptT $ do-  let target = root </> relTarget-  libdir <- liftIO (HIE.getRuntimeGhcLibDir cradle) >>= unwrapCradleResult "Failed to get runtime GHC libdir"--  -- To determine the flags we MUST set the current directory to the root-  -- because hie.yaml may invoke programs relative to the root (e.g. GHC's hie.yaml does)-  -- (HIE.getCompilerOptions depends on CWD being the proper root dir)-  let compilerOpts = liftIO $ withCurrentDirectory root $-#if MIN_VERSION_hie_bios(0,14,0)-                          HIE.getCompilerOptions target (HIE.LoadWithContext [target]) cradle-#else-                          HIE.getCompilerOptions target [] cradle-#endif-  componentOpts <- compilerOpts >>= unwrapCradleResult "Failed to get compiler options using hie-bios cradle"-#if __GLASGOW_HASKELL__ >= 913-  -- fwrite-if-simplified-core requires a recent bug fix regarding GHCi loading-  -- ROMES:TODO: Re-enable as soon as I'm using Matthew's patch.-  -- ["-fwrite-if-simplified-core"] ++-#endif--  let (units', flags') = extractUnits (HIE.componentOptions componentOpts)-  return HieBiosFlags-    { ghcInvocation = flags' ++ ghcDebuggerFlags-    , libdir = libdir-    , units  = units'-    , rootDir = HIE.cradleRootDir cradle-    , componentDir = HIE.componentRoot componentOpts-    }--unwrapCradleResult :: MonadError String m => [Char] -> HIE.CradleLoadResult a -> m a-unwrapCradleResult m = \case-  HIE.CradleNone      -> throwError $ "HIE.CradleNone\n" ++ m-  HIE.CradleFail err  -> throwError $ unlines (HIE.cradleErrorStderr err) ++ "\n" ++ m-  HIE.CradleSuccess x -> return x--extractUnits :: [String] -> ([String], [String])-extractUnits = go [] []-  where-    -- TODO: we should likely use the 'processCmdLineP' instead-    go units rest ("-unit" : x : xs) = go (x : units) rest xs-    go units rest (x : xs)           = go units (x : rest) xs-    go units rest []                 = (reverse units, reverse rest)---- | Flags specific to haskell-debugger to append to all GHC invocations.-ghcDebuggerFlags :: [String]-ghcDebuggerFlags =-  [ "-fno-it" -- don't introduce @it@ after evaluating something at the prompt-  ]----- ------------------------------------------------------------------------------- Utilities--- ------------------------------------------------------------------------------versionMaybe :: String -> Maybe Version-versionMaybe xs = case reverse $ readP_to_S parseVersion xs of-  [] -> Nothing-  (x:_) -> Just (fst x)---- ------------------------------------------------------------------------------- Implicit cradle discovery logic mirroring the one used by HLS.--- The code itself is copy-pasted from HLS.--- Obviously, we want to reuse the logic without having to depend on HLS.--- We should factor out a common sub-component from HLS and use it here as well.--- ------------------------------------------------------------------------------loadImplicitCradle :: Show a => LogAction IO (WithSeverity HIE.Log) -> FilePath -> IO (HIE.Cradle a)-loadImplicitCradle l wfile = do-  is_dir <- doesDirectoryExist wfile-  let wdir | is_dir = wfile-           | otherwise = takeDirectory wfile-  cfg <- runMaybeT (implicitConfig wdir)-  case cfg of-    Just bc -> HIE.getCradle l absurd bc-    Nothing -> return $ HIE.defaultCradle l wdir---- | Wraps up the cradle inferred by @inferCradleTree@ as a @CradleConfig@ with no dependencies-implicitConfig :: FilePath -> MaybeT IO (Config.CradleConfig a, FilePath)-implicitConfig = (fmap . first) (Config.CradleConfig noDeps) . inferCradleTree-  where-  noDeps :: [FilePath]-  noDeps = []---inferCradleTree :: FilePath -> MaybeT IO (Config.CradleTree a, FilePath)-inferCradleTree start_dir =-       maybeItsBios-   -- If we have both a config file (cabal.project/stack.yaml) and a work dir-   -- (dist-newstyle/.stack-work), prefer that-   <|> (cabalExecutable >> cabalConfigDir start_dir >>= \dir -> cabalWorkDir dir >> pure (simpleCabalCradle dir))-   <|> (stackExecutable >> stackConfigDir start_dir >>= \dir -> stackWorkDir dir >> stackCradle dir)-   -- If we have a cabal.project OR we have a .cabal and dist-newstyle, prefer cabal-   <|> (cabalExecutable >> (cabalConfigDir start_dir <|> cabalFileAndWorkDir) <&> simpleCabalCradle)-   -- If we have a stack.yaml, use stack-   <|> (stackExecutable >> stackConfigDir start_dir >>= stackCradle)-   -- If we have a cabal file, use cabal-   <|> (cabalExecutable >> cabalFileDir start_dir <&> simpleCabalCradle)--  where-  maybeItsBios = (\wdir -> (Config.Bios (Config.Program $ wdir </> ".hie-bios") Nothing Nothing, wdir)) <$> biosWorkDir start_dir--  cabalFileAndWorkDir = cabalFileDir start_dir >>= (\dir -> cabalWorkDir dir >> pure dir)---- | Generate a stack cradle given a filepath.------ Since we assume there was proof that this file belongs to a stack cradle--- we look immediately for the relevant @*.cabal@ and @stack.yaml@ files.--- We do not look for package.yaml, as we assume the corresponding .cabal has--- been generated already.------ We parse the @stack.yaml@ to find relevant @*.cabal@ file locations, then--- we parse the @*.cabal@ files to generate a mapping from @hs-source-dirs@ to--- component names.-stackCradle :: FilePath -> MaybeT IO (Config.CradleTree a, FilePath)-stackCradle fp = do-  pkgs <- Implicit.stackYamlPkgs fp-  pkgsWithComps <- liftIO $ catMaybes <$> mapM (Implicit.nestedPkg fp) pkgs-  let yaml = fp </> "stack.yaml"-  pure $ (,fp) $ case pkgsWithComps of-    [] -> Config.Stack (Config.StackType Nothing (Just yaml))-    ps -> Config.StackMulti mempty $ do-      Implicit.Package n cs <- ps-      c <- cs-      let (prefix, comp) = Implicit.stackComponent n c-      pure (prefix, Config.StackType (Just comp) (Just yaml))---- | By default, we generate a simple cabal cradle which is equivalent to the--- following hie.yaml:------ @---   cradle:---     cabal:--- @------ Note, this only works reliable for reasonably modern cabal versions >= 3.2.-simpleCabalCradle :: FilePath -> (Config.CradleTree a, FilePath)-simpleCabalCradle fp = (Config.Cabal $ Config.CabalType Nothing Nothing, fp)--cabalExecutable :: MaybeT IO FilePath-cabalExecutable = MaybeT $ findExecutable "cabal"--stackExecutable :: MaybeT IO FilePath-stackExecutable = MaybeT $ findExecutable "stack"--biosWorkDir :: FilePath -> MaybeT IO FilePath-biosWorkDir = findFileUpwards (".hie-bios" ==)--cabalWorkDir :: FilePath -> MaybeT IO ()-cabalWorkDir wdir = do-  check <- liftIO $ doesDirectoryExist (wdir </> "dist-newstyle")-  unless check $ fail "No dist-newstyle"--stackWorkDir :: FilePath -> MaybeT IO ()-stackWorkDir wdir = do-  check <- liftIO $ doesDirectoryExist (wdir </> ".stack-work")-  unless check $ fail "No .stack-work"--cabalConfigDir :: FilePath -> MaybeT IO FilePath-cabalConfigDir = findFileUpwards (\fp -> fp == "cabal.project" || fp == "cabal.project.local")--cabalFileDir :: FilePath -> MaybeT IO FilePath-cabalFileDir = findFileUpwards (\fp -> takeExtension fp == ".cabal")--stackConfigDir :: FilePath -> MaybeT IO FilePath-stackConfigDir = findFileUpwards isStack-  where-    isStack name = name == "stack.yaml"---- | Searches upwards for the first directory containing a file to match--- the predicate.-findFileUpwards :: (FilePath -> Bool) -> FilePath -> MaybeT IO FilePath-findFileUpwards p dir = do-  cnts <--    liftIO-    $ handleJust-        -- Catch permission errors-        (\(e :: IOError) -> if isPermissionError e then Just [] else Nothing)-        pure-        (findFile p dir)--  case cnts of-    [] | dir' == dir -> fail "No cabal files"-            | otherwise   -> findFileUpwards p dir'-    _ : _ -> return dir-  where dir' = takeDirectory dir---- | Sees if any file in the directory matches the predicate-findFile :: (FilePath -> Bool) -> FilePath -> IO [FilePath]-findFile p dir = do-  b <- doesDirectoryExist dir-  if b then getFiles >>= filterM doesPredFileExist else return []-  where-    getFiles = filter p <$> getDirectoryContents dir-    doesPredFileExist file = doesFileExist $ dir </> file
hdb/Development/Debug/Adapter/Init.hs view
@@ -9,16 +9,18 @@ -- | TODO: This module should be called Launch. module Development.Debug.Adapter.Init where +import GHC.IO.Handle+import System.Process+import qualified Data.ByteString as BS import qualified Data.Text as T import qualified Data.Text.IO as T+import qualified Data.Text.Encoding as T import qualified System.Process as P import Control.Monad.Except import Control.Monad.Trans import Data.Function import Data.Functor import Data.Maybe-import Data.Version (Version(..), showVersion, makeVersion)-import Control.Monad.IO.Class import System.IO import GHC.IO.Encoding import Control.Monad.Catch@@ -32,7 +34,6 @@  import Development.Debug.Adapter import Development.Debug.Adapter.Exit-import Development.Debug.Adapter.Flags import GHC.Debugger.Logger import qualified Development.Debug.Adapter.Output as Output @@ -43,6 +44,7 @@  import DAP import Development.Debug.Adapter.Handles+import Development.Debug.Session.Setup  -------------------------------------------------------------------------------- -- * Logging@@ -84,14 +86,15 @@ -- * Launch Debugger -------------------------------------------------------------------------------- --- | Exception type for when initialization fails+-- | Exception type for when hie-bios initialization fails newtype InitFailed = InitFailed String deriving Show  -- | Initialize debugger -- -- Returns @()@ if successful, throws @InitFailed@ otherwise-initDebugger :: Recorder (WithSeverity InitLog) -> LaunchArgs -> ExceptT InitFailed DebugAdaptor ()-initDebugger l LaunchArgs{ __sessionId+initDebugger :: Recorder (WithSeverity InitLog) -> Bool -> LaunchArgs -> ExceptT InitFailed DebugAdaptor ()+initDebugger l supportsRunInTerminal+               LaunchArgs{ __sessionId                          , projectRoot = givenRoot                          , entryFile = entryFileMaybe                          , entryPoint = fromMaybe "main" -> entryPoint@@ -100,6 +103,9 @@                          } = do   syncRequests  <- liftIO newEmptyMVar   syncResponses <- liftIO newEmptyMVar+  syncProxyIn   <- liftIO newChan+  syncProxyOut  <- liftIO newChan+  syncProxyErr  <- liftIO newChan    entryFile <- case entryFileMaybe of     Nothing -> throwError $ InitFailed "Missing \"entryFile\" key in debugger configuration"@@ -108,31 +114,10 @@   projectRoot <- maybe (liftIO getCurrentDirectory) pure givenRoot    let hieBiosLogger = cmapWithSev FlagsLog l-  cradle <- liftIO (hieBiosCradle hieBiosLogger projectRoot entryFile) >>=-    \ case-      Left e  -> throwError $ InitFailed e-      Right c -> pure c--  lift $ Output.console $ T.pack "Checking GHC version against debugger version..."-  -- GHC is found in PATH (by hie-bios as well).-  actualVersion <- liftIO (hieBiosRuntimeGhcVersion hieBiosLogger cradle) >>=-    \ case-      Left e  -> throwError $ InitFailed e-      Right c -> pure c-  -- Compare the GLASGOW_HASKELL version (e.g. 913) with the actualVersion (e.g. 9.13.1):-  when (compileTimeGhcWithoutPatchVersion /= forgetPatchVersion actualVersion) $ do-    throwError $ InitFailed $-      "Aborting...! The GHC version must be the same which " ++-        "ghc-debug-adapter was compiled against (" ++-          showVersion compileTimeGhcWithoutPatchVersion++-            "). Instead, got " ++ (showVersion actualVersion) ++ "."--  lift $ Output.console $ T.pack "Discovering session flags with hie-bios..."-  mflags <- liftIO (hieBiosFlags hieBiosLogger cradle projectRoot entryFile)-  case mflags of-    Left e -> throwError $ InitFailed e-    Right flags -> do-+  liftIO (runExceptT (hieBiosSetup hieBiosLogger projectRoot entryFile)) >>= \case+    Left e              -> throwError $ InitFailed e+    Right (Left e)      -> lift       $ exitWithMsg e+    Right (Right flags) -> do       let nextFreshBreakpointId = 0           breakpointMap = mempty           defaultRunConf = Debugger.RunDebuggerSettings@@ -157,10 +142,12 @@        let absEntryFile = normalise $ projectRoot </> entryFile       lift $ registerNewDebugSession (maybe "debug-session" T.pack __sessionId) DAS{entryFile=absEntryFile,..}-        [ debuggerThread l finished_init writeDebuggerOutput projectRoot flags extraGhcArgs absEntryFile defaultRunConf syncRequests syncResponses+        [ debuggerThread l finished_init writeDebuggerOutput projectRoot flags+            extraGhcArgs absEntryFile defaultRunConf syncRequests syncResponses         , handleDebuggerOutput readDebuggerOutput-        , stdoutCaptureThread-        , stderrCaptureThread+        , stdinForwardThread  supportsRunInTerminal syncProxyIn+        , stdoutCaptureThread supportsRunInTerminal syncProxyOut+        , stderrCaptureThread supportsRunInTerminal syncProxyErr         ]        -- Do not return until the initialization is finished@@ -171,27 +158,52 @@           -- This can happen if compilation fails and the compiler exits cleanly.           --           -- Instead of signalInitialized, respond with error and exit.-          throwError $ InitFailed e+          lift $ exitCleanupWithMsg readDebuggerOutput e --- | This thread captures stdout from the debugger and sends it to the client.+-- | This thread captures stdout from the debuggee and sends it to the client. -- NOTE, redirecting the stdout handle is a process-global operation. So this thread--- will capture ANY stdout the debugger emits. Therefore you should never directly+-- will capture ANY stdout the debuggee emits. Therefore you should never directly -- write to stdout, but always write to the appropiate handle.-stdoutCaptureThread :: (DebugAdaptorCont () -> IO ()) -> IO ()-stdoutCaptureThread withAdaptor = do+stdoutCaptureThread :: Bool -> Chan BS.ByteString -> (DebugAdaptorCont () -> IO ()) -> IO ()+stdoutCaptureThread runInTerminal syncOut withAdaptor = do   withInterceptedStdout $ \_ interceptedStdout -> do     forever $ do       line <- liftIO $ T.hGetLine interceptedStdout+      when runInTerminal $+        writeChan syncOut $ T.encodeUtf8 (line <> T.pack "\n")++      -- Always output to Debug Console       withAdaptor $ Output.stdout line  -- | Like 'stdoutCaptureThread' but for stderr-stderrCaptureThread :: (DebugAdaptorCont () -> IO ()) -> IO ()-stderrCaptureThread withAdaptor = do+stderrCaptureThread :: Bool -> Chan BS.ByteString -> (DebugAdaptorCont () -> IO ()) -> IO ()+stderrCaptureThread runInTerminal syncErr withAdaptor = do   withInterceptedStderr $ \_ interceptedStderr -> do     forever $ do       line <- liftIO $ T.hGetLine interceptedStderr+      when runInTerminal $+        writeChan syncErr $ T.encodeUtf8 (line <> "\n")++      -- Always output to Debug Console       withAdaptor $ Output.stderr line +stdinForwardThread :: Bool -> Chan BS.ByteString -> (DebugAdaptorCont () -> IO ()) -> IO ()+stdinForwardThread runInTerminal syncIn _withAdaptor = do+  when runInTerminal $ do+    -- We need to hijack stdin to write to it++    -- 1. Create a new pipe from writeEnd->readEnd+    (readEnd, writeEnd) <- createPipe++    -- 2. Substitute the read-end of the pipe by stdin+    _ <- hDuplicateTo readEnd stdin+    hClose readEnd -- we'll never need to read from readEnd++    forever $ do+      i <- readChan syncIn+      -- 3. Write to write-end of the pipe+      BS.hPut writeEnd i >> hFlush writeEnd+ -- | The main debugger thread launches a GHC.Debugger session. -- -- Then, forever:@@ -278,15 +290,3 @@           -- Cleanly exit when readDebuggerOutput is closed or thread is killed.           return () -compileTimeGhcWithoutPatchVersion :: Version-compileTimeGhcWithoutPatchVersion =-  let-    versionNumber = __GLASGOW_HASKELL__ :: Int-    (major, minor) = divMod versionNumber 100-  in-    makeVersion [major, minor]--forgetPatchVersion :: Version -> Version-forgetPatchVersion v = case versionBranch v of-  (major:minor:_patches) -> makeVersion [major, minor]-  _ -> v
+ hdb/Development/Debug/Adapter/Proxy.hs view
@@ -0,0 +1,161 @@+{-# LANGUAGE OverloadedStrings, DerivingStrategies #-}+-- | Run the proxy mode, which forwards stdin/stdout to/from the DAP server and+-- is displayed in a terminal in the DAP client using 'runInTerminal'+module Development.Debug.Adapter.Proxy+  ( serverSideHdbProxy+  , runInTerminalHdbProxy+  , ProxyLog(..)+  ) where++import DAP++import System.IO+import System.Exit (exitSuccess)+import System.Environment+import System.FilePath+import Control.Exception.Base+import Control.Monad+import Control.Monad.IO.Class+import Control.Concurrent+import qualified Data.List.NonEmpty as NE++import qualified Data.Text as T+import Network.Socket hiding (Debug)+import Network.Run.TCP+import qualified Network.Socket.ByteString as NBS+import qualified Data.ByteString.Char8 as BS8+import qualified Data.HashMap.Strict as H++import GHC.Debugger.Logger+import Development.Debug.Adapter++newtype ProxyLog = ProxyLog T.Text+  deriving newtype Pretty++-- | Connect to a running @hdb proxy@ process on the given port+-- connectToHdbProxy :: Recorder (WithVerbosity x) -> Int -> DebugAdaptor ()+-- connectToHdbProxy = _++-- | Fork a new thread to run the server-side of the proxy.+--+-- 1. To setup:+-- Ask the DAP client to launch a process running @hdb proxy --port <port>@+-- by sending a 'runInTerminal' DAP reverse request. This is done outside of+-- this function by signaling the given MVar (this is the case because we cannot use `network` with `DebugAdaptor`+--+-- 2. In a loop,+-- 2.1 Read stdin from the socket and push it to a Chan+-- 2.1 Read from a stdout Chan and write to the socket+serverSideHdbProxy :: Recorder (WithSeverity ProxyLog)+                   -> MVar ()+                   -> DebugAdaptor ()+serverSideHdbProxy l client_conn_signal = do+  DAS { syncProxyIn = dbIn+      , syncProxyOut = dbOut+      , syncProxyErr = dbErr } <- getDebugSession++  sock <- liftIO $ do+    let hints = defaultHints { addrFlags = [AI_NUMERICHOST, AI_NUMERICSERV], addrSocketType = Stream }+    addr <- NE.head <$> getAddrInfo (Just hints) (Just "127.0.0.1") (Just "0")+    -- Bind on "0" to let the OS pick a free port+    openTCPServerSocket addr++  port <- liftIO $ socketPort sock++  _ <- liftIO $ forkIO $ ignoreIOException $ do+    runTCPServerWithSocket sock $ \scket -> do++      logWith l Info $ ProxyLog $ T.pack $ "Connected to client on port " ++ show port ++ "...!"+      putMVar client_conn_signal () -- signal ready (see #95)++      -- -- Read stdout from chan and write to socket+      _ <- forkIO $ ignoreIOException $ do+        forever $ do+          bs <- readChan dbOut+          logWith l Debug $ ProxyLog $ T.pack $ "Writing to socket: " ++ BS8.unpack bs+          NBS.sendAll scket bs++      -- Read stderr from chan and write to socket+      _ <- forkIO $ ignoreIOException $ do+        forever $ do+          bs <- readChan dbErr+          logWith l Debug $ ProxyLog $ T.pack $ "Writing to socket (from stderr): " ++ BS8.unpack bs+          NBS.sendAll scket bs++      -- Read stdin from socket and write to chan+      let loop = do+            bs <- NBS.recv scket 4096+            if BS8.null bs+              then do+                logWith l Debug $ ProxyLog $ T.pack "Connection to client was closed."+                close scket+              else do+                logWith l Debug $ ProxyLog $ T.pack $ "Read from socket: " ++ BS8.unpack bs+                writeChan dbIn bs >> loop+       in ignoreIOException loop++  sendRunProxyInTerminal port++  where+    ignoreIOException a = catch a $ \(e::IOException) ->+      logWith l Info $ ProxyLog $ T.pack $ "Ignoring connection broken to proxy client: " ++ show e++-- | The proxy code running on the terminal in which the @hdb proxy@ process is launched.+--+-- This client-side proxy is responsible for+-- 1. Connecting to the given proxy-server port+-- 2. Forwarding stdin to the port it is connected to+-- 3. Read from the network the output and write it to stdout+runInTerminalHdbProxy :: Recorder (WithSeverity ProxyLog) -> Int -> IO ()+runInTerminalHdbProxy l port = do+  logWith l Info $ ProxyLog $ T.pack $ "Running in terminal on port " ++ show port ++ "...!"+  hSetBuffering stdin LineBuffering++  dbg_inv <- lookupEnv "DEBUGGEE_INVOCATION"+  case dbg_inv of+    Nothing  -> pure ()+    Just inv ->+      putStrLn $ "Running the debugger input/output proxy for the following debuggee execution:\n\n\n    " ++ inv ++ "\n\n"++  catch (+    runTCPClient "127.0.0.1" (show port) $ \sock -> do+      -- Forward stdin to sock+      _ <- forkIO $+        catch (forever $ do+          str <- BS8.hGetLine stdin+          NBS.sendAll sock (str <> BS8.pack "\n")+          ) $ \(e::IOException) -> return () -- connection dropped, just exit.++      -- Forward stdout from sock+      catch (forever $ do+        msg <- NBS.recv sock 4096+        if BS8.null msg+          then do+            logWith l Info $ ProxyLog $ T.pack "Exiting..."+            close sock+            exitSuccess+          else BS8.hPut stdout msg >> hFlush stdout+        ) $ \(e::IOException) -> return () -- connection dropped, just exit.++    ) $ \(e::IOException) -> do+      hPutStrLn stderr "Failed to connect to debugger server proxy -- did the debuggee compile and start running successfully?"++-- | Send a 'runInTerminal' reverse request to the DAP client+-- with the @hdb proxy@ invocation+sendRunProxyInTerminal :: PortNumber -> DebugAdaptor ()+sendRunProxyInTerminal port = do+  DAS { entryFile+      , entryPoint+      , entryArgs+      , projectRoot } <- getDebugSession+  let debuggee_inv = T.pack $ makeRelative projectRoot entryFile ++ ":" ++ entryPoint +++                              (if null entryArgs then "" else " ") ++ unwords entryArgs+  sendRunInTerminalReverseRequest+    RunInTerminalRequestArguments+      { runInTerminalRequestArgumentsKind = Just RunInTerminalRequestArgumentsKindIntegrated+      , runInTerminalRequestArgumentsTitle = Just debuggee_inv+      , runInTerminalRequestArgumentsCwd = ""+      , runInTerminalRequestArgumentsArgs = ["hdb", "proxy", "--port", T.pack (show port)]+      , runInTerminalRequestArgumentsEnv = Just (H.singleton "DEBUGGEE_INVOCATION" debuggee_inv)+      , runInTerminalRequestArgumentsArgsCanBeInterpretedByShell = False+      }
hdb/Development/Debug/Interactive.hs view
@@ -6,14 +6,15 @@ import System.Directory import System.Console.Haskeline import System.Console.Haskeline.Completion+import System.FilePath+import Control.Monad.Except import Control.Monad.State import Control.Monad.Reader import Control.Monad.RWS import Options.Applicative import Options.Applicative.BashCompletion -import Development.Debug.Adapter.Flags  -- use different namespace for common things-import Development.Debug.Adapter.Handles+import Development.Debug.Session.Setup  import GHC.Debugger.Logger import GHC.Debugger.Interface.Messages@@ -34,38 +35,39 @@     FlagsLog msg -> pretty msg  -- | Run it-runIDM :: String   -- ^ entryPoint+runIDM :: Recorder (WithSeverity InteractiveLog)+       -> String   -- ^ entryPoint        -> FilePath -- ^ entryFile        -> [String] -- ^ entryArgs        -> [String] -- ^ extraGhcArgs        -> InteractiveDM a        -> IO a-runIDM entryPoint entryFile entryArgs extraGhcArgs act = do+runIDM logger entryPoint entryFile entryArgs extraGhcArgs act = do   projectRoot <- getCurrentDirectory-  l           <- handleLogger stdout-  let-    loggerWithSev = cmap renderPrettyWithSeverity (fromCologAction l)-  let hieBiosLogger = cmapWithSev FlagsLog loggerWithSev-  cradle <- hieBiosCradle hieBiosLogger projectRoot entryFile >>=-    \case-      Left e -> exitWithMsg e-      Right c -> pure c-  mflags <- hieBiosFlags hieBiosLogger cradle projectRoot entryFile-  case mflags of-    Left e -> exitWithMsg e-    Right HieBiosFlags{..} -> do++  let hieBiosLogger = cmapWithSev FlagsLog logger+  runExceptT (hieBiosSetup hieBiosLogger projectRoot entryFile) >>= \case+    Left e               -> exitWithMsg e+    Right (Left e)       -> exitWithMsg e+    Right (Right flags)+      | HieBiosFlags{..} <- flags+                         -> do+       let defaultRunConf = RunDebuggerSettings-            { supportsANSIStyling = True+            { supportsANSIStyling = True -- todo: check!!             , supportsANSIHyperlinks = False             }+       let finalGhcInvocation = ghcInvocation ++ extraGhcArgs-      runDebugger stdout rootDir componentDir libdir units finalGhcInvocation entryFile defaultRunConf $+      let absEntryFile = normalise $ projectRoot </> entryFile++      runDebugger stdout rootDir componentDir libdir units finalGhcInvocation absEntryFile defaultRunConf $         fmap fst $           evalRWST (runInputT (setComplete noCompletion defaultSettings) act)                    (entryFile, entryPoint, entryArgs) Nothing   where-    exitWithMsg str = do-      putStrLn str+    exitWithMsg txt = do+      putStrLn txt       exitWith (ExitFailure 33)    --   completeF = completeWordWithPrev Nothing filenameWordBreakChars $@@ -223,6 +225,10 @@     ( info (DoEval . unwords <$> many (argument str ( metavar "EXPRESSION"      <> help "Expression to evaluate in the current context" )))       ( progDesc "Evaluate an expression in the current context" ) )+  <>+    Options.Applicative.command "exit"+    ( info (pure TerminateProcess)+      ( progDesc "Terminate and exit the debugger session" ) )   )  -- | Main parser info
+ hdb/Development/Debug/Options.hs view
@@ -0,0 +1,42 @@+-- | Options supported by the debugger+--+-- For parsing see 'Development.Debug.Options.Parser'+module Development.Debug.Options+  ( HdbOptions(..) ) where++import GHC.Debugger.Logger++-- | The options `hdb` is invoked in the command line with+data HdbOptions+  -- | @server --port <port>@+  = HdbDAPServer+    { port :: Int+    , verbosity :: Verbosity+    }+  -- | @cli [--entry-point=<entryPoint>] [--extra-ghc-args="<args>"] [<entryFile>] -- [<entryArgs>]@+  | HdbCLI+    { entryPoint :: String+    , entryFile :: FilePath+    , entryArgs :: [String]+    , extraGhcArgs :: [String]+    , verbosity :: Verbosity+    }++  -- | @proxy --port <port>@+  --+  -- The proxy command serves as a middle man between the user and the debugger.+  -- It is used implicitly by DAP mode: upon initialization, the debugger+  -- server asks the DAP client to @runInTerminal@ the @hdb proxy --port ...@+  -- command at a port determined by the debugger server.+  --+  -- The proxy mode will forward stdin to the debugger server and will be+  -- forwarded the debuggee's stdout. This essentially enables the user to+  -- observe and interact with the execution of the debugger, in a standalone+  -- terminal.+  --+  -- See #44 for the original ticket+  | HdbProxy+    { port :: Int+    , verbosity :: Verbosity+    }+
+ hdb/Development/Debug/Options/Parser.hs view
@@ -0,0 +1,117 @@+-- | Options parser using optparse-applicative for the debugger options in+-- 'Development.Debug.Options'+module Development.Debug.Options.Parser+  ( -- * Command line options parsing+    parseHdbOptions+  ) where++import Options.Applicative hiding (command)++import Data.Version+import qualified Options.Applicative+import qualified Paths_haskell_debugger as P++import GHC.Debugger.Logger+import Development.Debug.Options++--------------------------------------------------------------------------------+-- Options parser+--------------------------------------------------------------------------------++-- | Parser for 'HdbDAPServer' options+serverParser :: Parser HdbOptions+serverParser = HdbDAPServer+  <$> option auto+      ( long "port"+     <> short 'p'+     <> metavar "PORT"+     <> help "DAP server port" )+  <*> verbosityParser (Verbosity Debug)++-- | Parser for 'HdbCLI' options+cliParser :: Parser HdbOptions+cliParser = HdbCLI+  <$> strOption+      ( long "entry-point"+     <> short 'e'+     <> metavar "ENTRY_POINT"+     <> value "main"+     <> help "The name of the function that is called to start execution (default: main)" )+  <*> argument str+      ( metavar "ENTRY_FILE"+     <> help "The relative path from the project root to the file with the entry point for execution" )+  <*> many+        ( argument str+          ( metavar "ENTRY_ARGS..."+         <> help "The arguments passed to the entryPoint. If the entryPoint is main, these arguments are passed as environment arguments (as in getArgs) rather than direct function arguments."+          )+        )+  <*> option (words <$> str)+      ( long "extra-ghc-args"+     <> metavar "GHC_ARGS"+     <> value []+     <> help "Additional flags to pass to the ghc invocation that loads the program for debugging" )+  <*> verbosityParser (Verbosity Warning)++-- | Parser for 'HdbProxy' options+proxyParser :: Parser HdbOptions+proxyParser = HdbProxy+  <$> option auto+      ( long "port"+     <> short 'p'+     <> metavar "PORT"+     <> help "proxy port to which the debugger connects" )+  <*> verbosityParser (Verbosity Warning)++-- | Combined parser for HdbOptions+hdbOptionsParser :: Parser HdbOptions+hdbOptionsParser = hsubparser+  ( Options.Applicative.command "server"+    ( info serverParser+      ( progDesc "Start the Haskell debugger in DAP server mode" ) )+ <> Options.Applicative.command "cli"+    ( info cliParser+      ( progDesc "Debug a Haskell program in CLI mode" ) )+ <> Options.Applicative.command "proxy"+    ( info proxyParser+      ( progDesc "Internal mode used by the DAP server to proxy the stdin/stdout to the DAP client's terminal" ) )+  )+  <|> cliParser  -- Default to CLI mode if no subcommand++-- | Parser for --version flag+versioner :: Parser (a -> a)+versioner = simpleVersioner $ "Haskell Debugger, version " ++ showVersion P.version++-- | Parser for --verbosity 0+--+-- The default verbosity differs by mode (#86):+-- - DAP server mode: DEBUG+-- - CLI mode: WARNING+verbosityParser :: Verbosity -> Parser Verbosity+verbosityParser vdef = option verb+    ( long "verbosity"+   <> short 'v'+   <> metavar "VERBOSITY"+   <> value vdef+   <> help "Logger verbosity in [0..3] interval, where 0 is silent and 3 is debug"+    )+  where+    verb = Verbosity <$> (verbNum =<< auto)+    verbNum n = case n :: Int of+      0 -> pure Error+      1 -> pure Warning+      2 -> pure Info+      3 -> pure Debug+      _ -> readerAbort (ErrorMsg "Verbosity must be a value in [0..3]")++-- | Main parser info+hdbParserInfo :: ParserInfo HdbOptions+hdbParserInfo = info (hdbOptionsParser <**> versioner <**> helper)+  ( fullDesc+ <> header "Haskell debugger supporting both CLI and DAP modes" )++-- | Parse command line arguments+parseHdbOptions :: IO HdbOptions+parseHdbOptions = customExecParser+  defaultPrefs{prefShowHelpOnError = True, prefShowHelpOnEmpty = True}+  hdbParserInfo
+ hdb/Development/Debug/Session/Setup.hs view
@@ -0,0 +1,344 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++module Development.Debug.Session.Setup+  (+  -- * Setting up a hie-bios session+    HieBiosFlags(..)+  , hieBiosSetup++  -- * Logging+  , FlagsLog(..)+  ) where++import Control.Applicative ((<|>))+import Control.Exception (handleJust)+import Control.Monad+import Control.Monad.Except+import Control.Monad.IO.Class+import Control.Monad.Trans.Maybe+import Data.Bifunctor+import Data.Function+import Data.Functor ((<&>))+import Data.Maybe+import Data.Version+import Data.Void+import System.Directory hiding (findFile)+import System.FilePath+import System.IO.Error+import Text.ParserCombinators.ReadP (readP_to_S)+import Prettyprinter++import qualified Data.Text as T++import qualified HIE.Bios as HIE+import qualified HIE.Bios.Config as Config+import qualified HIE.Bios.Cradle as HIE+import qualified HIE.Bios.Environment as HIE+import qualified HIE.Bios.Types as HIE+import qualified Hie.Cabal.Parser as Implicit+import qualified Hie.Locate as Implicit+import qualified Hie.Yaml as Implicit++import GHC.Debugger.Logger++data FlagsLog+  = HieBiosLog HIE.Log+  | LogCradle (HIE.Cradle Void)+  | LogSetupMsg T.Text++instance Pretty FlagsLog where+  pretty = \ case+    HieBiosLog msg -> pretty msg+    LogCradle crdl -> "Determined Cradle:" <+> viaShow crdl+    LogSetupMsg txt -> pretty txt++-- | Flags inferred by @hie-bios@ to invoke GHC+data HieBiosFlags = HieBiosFlags+      { ghcInvocation :: [String]+      , libdir :: FilePath+      , units :: [String]+      , rootDir :: FilePath+      -- ^ Root dir as reported by the 'Cradle'+      , componentDir :: FilePath+      -- ^ Root dir of the loaded 'ComponentOptions'.+      -- Important for multi-package cabal projects, as packages are not in the+      -- root of the cradle, but in some sub-directory.+      }++-- | Prepare a GHC session using hie-bios from scratch+hieBiosSetup :: Recorder (WithSeverity FlagsLog)+             -> FilePath -- ^ project root+             -> FilePath -- ^ entry file+             -> ExceptT String IO (Either String HieBiosFlags)+hieBiosSetup logger projectRoot entryFile = do++  cradle <- hieBiosCradle logger projectRoot entryFile & ExceptT++  -- GHC is found in PATH (by hie-bios as well).+  logT "Checking GHC version against debugger version..."+  _version <- hieBiosRuntimeGhcVersion logger cradle++  logT "Discovering session flags with hie-bios..."+  r <- hieBiosFlags logger cradle projectRoot entryFile     & liftIO++  logT "Session setup with hie-bios was successful."+  return r++  where+    logT = logWith logger Info . LogSetupMsg . T.pack++-- | Try implicit-hie and the builtin search to come up with a @'HIE.Cradle'@+hieBiosCradle :: Recorder (WithSeverity FlagsLog) {-^ Logger -}+              -> FilePath {-^ Project root -}+              -> FilePath {-^ Entry file relative to root -}+              -> IO (Either String (HIE.Cradle Void))+hieBiosCradle logger root relTarget = runExceptT $ do+  let target = root </> relTarget+  explicitCradle <- HIE.findCradle target & liftIO+  cradle <- maybe (loadImplicitCradle hieBiosLogger target)+                  (HIE.loadCradle hieBiosLogger) explicitCradle & liftIO+  logWith logger Info $ LogCradle cradle+  pure cradle+  where+    hieBiosLogger = toCologAction $ cmapWithSev HieBiosLog logger++-- | Fetch the runtime GHC version, according to hie-bios, and check it is the+-- same as the compile time GHC version+hieBiosRuntimeGhcVersion :: Recorder (WithSeverity FlagsLog)+                         -> HIE.Cradle Void+                         -> ExceptT String IO Version+hieBiosRuntimeGhcVersion _logger cradle = do+  out <- liftIO (HIE.getRuntimeGhcVersion cradle) >>= unwrapCradleResult "Failed to get runtime GHC version"++  case versionMaybe out of+    Nothing -> throwError $ "Failed to parse GHC version: " <> out+    Just actualVersion -> do++      -- Compare the GLASGOW_HASKELL version (e.g. 913) with the actualVersion (e.g. 9.13.1):+      when (compileTimeGhcWithoutPatchVersion /= forgetPatchVersion actualVersion) $ do+        throwError $+          "Aborting...! The GHC version must be the same which " +++            "ghc-debug-adapter was compiled against (" +++              showVersion compileTimeGhcWithoutPatchVersion+++                "). Instead, got " ++ (showVersion actualVersion) ++ "."++      pure actualVersion++-- | Make 'HieBiosFlags' from the given target file+hieBiosFlags :: Recorder (WithSeverity FlagsLog) {-^ Logger -}+             -> HIE.Cradle Void {-^ Project cradle the entry file belongs to -}+             -> FilePath {-^ Project root -}+             -> FilePath {-^ Entry file relative to root -}+             -> IO (Either String HieBiosFlags)+hieBiosFlags _logger cradle root relTarget = runExceptT $ do+  let target = root </> relTarget+  libdir <- liftIO (HIE.getRuntimeGhcLibDir cradle) >>= unwrapCradleResult "Failed to get runtime GHC libdir"++  -- To determine the flags we MUST set the current directory to the root+  -- because hie.yaml may invoke programs relative to the root (e.g. GHC's hie.yaml does)+  -- (HIE.getCompilerOptions depends on CWD being the proper root dir)+  let compilerOpts = liftIO $ withCurrentDirectory root $+#if MIN_VERSION_hie_bios(0,14,0)+                          HIE.getCompilerOptions target (HIE.LoadWithContext [target]) cradle+#else+                          HIE.getCompilerOptions target [] cradle+#endif+  componentOpts <- compilerOpts >>= unwrapCradleResult "Failed to get compiler options using hie-bios cradle"+#if __GLASGOW_HASKELL__ >= 913+  -- fwrite-if-simplified-core requires a recent bug fix regarding GHCi loading+  -- ROMES:TODO: Re-enable as soon as I'm using Matthew's patch.+  -- ["-fwrite-if-simplified-core"] +++#endif++  let (units', flags') = extractUnits (HIE.componentOptions componentOpts)+  return HieBiosFlags+    { ghcInvocation = flags' ++ ghcDebuggerFlags+    , libdir = libdir+    , units  = units'+    , rootDir = HIE.cradleRootDir cradle+    , componentDir = HIE.componentRoot componentOpts+    }++unwrapCradleResult :: MonadError String m => [Char] -> HIE.CradleLoadResult a -> m a+unwrapCradleResult m = \case+  HIE.CradleNone      -> throwError $ "HIE.CradleNone\n" ++ m+  HIE.CradleFail err  -> throwError $ unlines (HIE.cradleErrorStderr err) ++ "\n" ++ m+  HIE.CradleSuccess x -> return x++extractUnits :: [String] -> ([String], [String])+extractUnits = go [] []+  where+    -- TODO: we should likely use the 'processCmdLineP' instead+    go units rest ("-unit" : x : xs) = go (x : units) rest xs+    go units rest (x : xs)           = go units (x : rest) xs+    go units rest []                 = (reverse units, reverse rest)++-- | Flags specific to haskell-debugger to append to all GHC invocations.+ghcDebuggerFlags :: [String]+ghcDebuggerFlags =+  [ "-fno-it" -- don't introduce @it@ after evaluating something at the prompt+  ]+++-- ----------------------------------------------------------------------------+-- Utilities+-- ----------------------------------------------------------------------------++versionMaybe :: String -> Maybe Version+versionMaybe xs = case reverse $ readP_to_S parseVersion xs of+  [] -> Nothing+  (x:_) -> Just (fst x)++-- ----------------------------------------------------------------------------+-- Implicit cradle discovery logic mirroring the one used by HLS.+-- The code itself is copy-pasted from HLS.+-- Obviously, we want to reuse the logic without having to depend on HLS.+-- We should factor out a common sub-component from HLS and use it here as well.+-- ----------------------------------------------------------------------------++loadImplicitCradle :: Show a => LogAction IO (WithSeverity HIE.Log) -> FilePath -> IO (HIE.Cradle a)+loadImplicitCradle l wfile = do+  is_dir <- doesDirectoryExist wfile+  let wdir | is_dir = wfile+           | otherwise = takeDirectory wfile+  cfg <- runMaybeT (implicitConfig wdir)+  case cfg of+    Just bc -> HIE.getCradle l absurd bc+    Nothing -> return $ HIE.defaultCradle l wdir++-- | Wraps up the cradle inferred by @inferCradleTree@ as a @CradleConfig@ with no dependencies+implicitConfig :: FilePath -> MaybeT IO (Config.CradleConfig a, FilePath)+implicitConfig = (fmap . first) (Config.CradleConfig noDeps) . inferCradleTree+  where+  noDeps :: [FilePath]+  noDeps = []+++inferCradleTree :: FilePath -> MaybeT IO (Config.CradleTree a, FilePath)+inferCradleTree start_dir =+       maybeItsBios+   -- If we have both a config file (cabal.project/stack.yaml) and a work dir+   -- (dist-newstyle/.stack-work), prefer that+   <|> (cabalExecutable >> cabalConfigDir start_dir >>= \dir -> cabalWorkDir dir >> pure (simpleCabalCradle dir))+   <|> (stackExecutable >> stackConfigDir start_dir >>= \dir -> stackWorkDir dir >> stackCradle dir)+   -- If we have a cabal.project OR we have a .cabal and dist-newstyle, prefer cabal+   <|> (cabalExecutable >> (cabalConfigDir start_dir <|> cabalFileAndWorkDir) <&> simpleCabalCradle)+   -- If we have a stack.yaml, use stack+   <|> (stackExecutable >> stackConfigDir start_dir >>= stackCradle)+   -- If we have a cabal file, use cabal+   <|> (cabalExecutable >> cabalFileDir start_dir <&> simpleCabalCradle)++  where+  maybeItsBios = (\wdir -> (Config.Bios (Config.Program $ wdir </> ".hie-bios") Nothing Nothing, wdir)) <$> biosWorkDir start_dir++  cabalFileAndWorkDir = cabalFileDir start_dir >>= (\dir -> cabalWorkDir dir >> pure dir)++-- | Generate a stack cradle given a filepath.+--+-- Since we assume there was proof that this file belongs to a stack cradle+-- we look immediately for the relevant @*.cabal@ and @stack.yaml@ files.+-- We do not look for package.yaml, as we assume the corresponding .cabal has+-- been generated already.+--+-- We parse the @stack.yaml@ to find relevant @*.cabal@ file locations, then+-- we parse the @*.cabal@ files to generate a mapping from @hs-source-dirs@ to+-- component names.+stackCradle :: FilePath -> MaybeT IO (Config.CradleTree a, FilePath)+stackCradle fp = do+  pkgs <- Implicit.stackYamlPkgs fp+  pkgsWithComps <- liftIO $ catMaybes <$> mapM (Implicit.nestedPkg fp) pkgs+  let yaml = fp </> "stack.yaml"+  pure $ (,fp) $ case pkgsWithComps of+    [] -> Config.Stack (Config.StackType Nothing (Just yaml))+    ps -> Config.StackMulti mempty $ do+      Implicit.Package n cs <- ps+      c <- cs+      let (prefix, comp) = Implicit.stackComponent n c+      pure (prefix, Config.StackType (Just comp) (Just yaml))++-- | By default, we generate a simple cabal cradle which is equivalent to the+-- following hie.yaml:+--+-- @+--   cradle:+--     cabal:+-- @+--+-- Note, this only works reliable for reasonably modern cabal versions >= 3.2.+simpleCabalCradle :: FilePath -> (Config.CradleTree a, FilePath)+simpleCabalCradle fp = (Config.Cabal $ Config.CabalType Nothing Nothing, fp)++cabalExecutable :: MaybeT IO FilePath+cabalExecutable = MaybeT $ findExecutable "cabal"++stackExecutable :: MaybeT IO FilePath+stackExecutable = MaybeT $ findExecutable "stack"++biosWorkDir :: FilePath -> MaybeT IO FilePath+biosWorkDir = findFileUpwards (".hie-bios" ==)++cabalWorkDir :: FilePath -> MaybeT IO ()+cabalWorkDir wdir = do+  check <- liftIO $ doesDirectoryExist (wdir </> "dist-newstyle")+  unless check $ fail "No dist-newstyle"++stackWorkDir :: FilePath -> MaybeT IO ()+stackWorkDir wdir = do+  check <- liftIO $ doesDirectoryExist (wdir </> ".stack-work")+  unless check $ fail "No .stack-work"++cabalConfigDir :: FilePath -> MaybeT IO FilePath+cabalConfigDir = findFileUpwards (\fp -> fp == "cabal.project" || fp == "cabal.project.local")++cabalFileDir :: FilePath -> MaybeT IO FilePath+cabalFileDir = findFileUpwards (\fp -> takeExtension fp == ".cabal")++stackConfigDir :: FilePath -> MaybeT IO FilePath+stackConfigDir = findFileUpwards isStack+  where+    isStack name = name == "stack.yaml"++-- | Searches upwards for the first directory containing a file to match+-- the predicate.+findFileUpwards :: (FilePath -> Bool) -> FilePath -> MaybeT IO FilePath+findFileUpwards p dir = do+  cnts <-+    liftIO+    $ handleJust+        -- Catch permission errors+        (\(e :: IOError) -> if isPermissionError e then Just [] else Nothing)+        pure+        (findFile p dir)++  case cnts of+    [] | dir' == dir -> fail "No cabal files"+            | otherwise   -> findFileUpwards p dir'+    _ : _ -> return dir+  where dir' = takeDirectory dir++-- | Sees if any file in the directory matches the predicate+findFile :: (FilePath -> Bool) -> FilePath -> IO [FilePath]+findFile p dir = do+  b <- doesDirectoryExist dir+  if b then getFiles >>= filterM doesPredFileExist else return []+  where+    getFiles = filter p <$> getDirectoryContents dir+    doesPredFileExist file = doesFileExist $ dir </> file++--------------------------------------------------------------------------------++compileTimeGhcWithoutPatchVersion :: Version+compileTimeGhcWithoutPatchVersion =+  let+    versionNumber = __GLASGOW_HASKELL__ :: Int+    (major, minor) = divMod versionNumber 100+  in+    makeVersion [major, minor]++forgetPatchVersion :: Version -> Version+forgetPatchVersion v = case versionBranch v of+  (major:minor:_patches) -> makeVersion [major, minor]+  _ -> v
hdb/Main.hs view
@@ -2,11 +2,15 @@ module Main where  import System.Environment+import System.Process import Data.Maybe-import Data.Version+import Data.Aeson+import Data.IORef import Text.Read-import Control.Monad.Except+import Control.Concurrent+import Control.Monad import Control.Monad.IO.Class+import Control.Monad.Except  import DAP @@ -18,98 +22,19 @@ import Development.Debug.Adapter.Exit import Development.Debug.Adapter.Handles import GHC.Debugger.Logger-import Development.Debug.Adapter--import Development.Debug.Interactive+import Prettyprinter  import System.IO (hSetBuffering, BufferMode(LineBuffering)) import qualified DAP.Log as DAP import qualified Data.Text as T import qualified Data.Text.IO as T import GHC.IO.Handle.FD-import Options.Applicative hiding (command)-import qualified Options.Applicative -import qualified Paths_haskell_debugger as P---- | The options `hdb` is invoked in the command line with-data HdbOptions-  -- | @server --port <port>@-  = HdbDAPServer-    { port :: Int-    }-  -- | @[--entry-point=<entryPoint>] [--extra-ghc-args="<args>"] [<entryFile>] -- [<entryArgs>]@-  | HdbCLI-    { entryPoint :: String-    , entryFile :: FilePath-    , entryArgs :: [String]-    , extraGhcArgs :: [String]-    }------------------------------------------------------------------------------------- Options parser------------------------------------------------------------------------------------- | Parser for HdbDAPServer options-serverParser :: Parser HdbOptions-serverParser = HdbDAPServer-  <$> option auto-      ( long "port"-     <> short 'p'-     <> metavar "PORT"-     <> help "DAP server port" )---- | Parser for HdbCLI options-cliParser :: Parser HdbOptions-cliParser = HdbCLI-  <$> strOption-      ( long "entry-point"-     <> short 'e'-     <> metavar "ENTRY_POINT"-     <> value "main"-     <> help "The name of the function that is called to start execution (default: main)" )-  <*> argument str-      ( metavar "ENTRY_FILE"-     <> help "The relative path from the project root to the file with the entry point for execution" )-  <*> many-        ( argument str-          ( metavar "ENTRY_ARGS..."-         <> help "The arguments passed to the entryPoint. If the entryPoint is main, these arguments are passed as environment arguments (as in getArgs) rather than direct function arguments."-          )-        )-  <*> option (words <$> str)-      ( long "extra-ghc-args"-     <> metavar "GHC_ARGS"-     <> value []-     <> help "Additional flags to pass to the ghc invocation that loads the program for debugging" )---- | Combined parser for HdbOptions-hdbOptionsParser :: Parser HdbOptions-hdbOptionsParser = hsubparser-  ( Options.Applicative.command "server"-    ( info serverParser-      ( progDesc "Start the Haskell debugger in DAP server mode" ) )- <> Options.Applicative.command "cli"-    ( info cliParser-      ( progDesc "Debug a Haskell program in CLI mode" ) )-  )-  <|> cliParser  -- Default to CLI mode if no subcommand---- | Parser for --version flag-versioner :: Parser (a -> a)-versioner = simpleVersioner $ "Haskell Debugger, version " ++ showVersion P.version---- | Main parser info-hdbParserInfo :: ParserInfo HdbOptions-hdbParserInfo = info (hdbOptionsParser <**> versioner <**> helper)-  ( fullDesc- <> header "Haskell debugger supporting both CLI and DAP modes" )---- | Parse command line arguments-parseHdbOptions :: IO HdbOptions-parseHdbOptions = customExecParser-  defaultPrefs{prefShowHelpOnError = True, prefShowHelpOnEmpty = True}-  hdbParserInfo+import Development.Debug.Options (HdbOptions(..))+import Development.Debug.Options.Parser (parseHdbOptions)+import Development.Debug.Adapter+import Development.Debug.Adapter.Proxy+import Development.Debug.Interactive  -------------------------------------------------------------------------------- @@ -120,28 +45,33 @@ main :: IO () main = do   hdbOpts <- parseHdbOptions+  let+    timeStampLogger  = cmapIO renderWithTimestamp . fromCologAction+    loggerWithSev    = cmap renderPrettyWithSeverity+    loggerFinal opts = applyVerbosity opts.verbosity . loggerWithSev . timeStampLogger   case hdbOpts of     HdbDAPServer{port} -> do       config <- getConfig port       withInterceptedStdoutForwarding defaultStdoutForwardingAction $ \realStdout -> do         hSetBuffering realStdout LineBuffering         l <- handleLogger realStdout-        let-          timeStampLogger :: Recorder T.Text-          timeStampLogger = cmapIO renderWithTimestamp (fromCologAction l)-          loggerWithSev :: Recorder (WithSeverity MainLog)-          loggerWithSev = cmap renderPrettyWithSeverity timeStampLogger-        runDAPServerWithLogger (toCologAction $ cmap DAP.renderDAPLog timeStampLogger) config (talk loggerWithSev)+        let dapLogger = cmap DAP.renderDAPLog $ timeStampLogger l+        let runLogger = loggerFinal hdbOpts l+        init_var <- liftIO (newIORef False{-not supported by default-})+        pid_var  <- liftIO (newIORef Nothing)+        ccon_var <- liftIO newEmptyMVar+        runDAPServerWithLogger (toCologAction dapLogger) config+          (talk runLogger init_var pid_var ccon_var)+          (ack runLogger pid_var)     HdbCLI{..} -> do-      l <- handleLogger stdout-      let-        timeStampLogger :: Recorder T.Text-        timeStampLogger = cmapIO renderWithTimestamp (fromCologAction l)-        loggerWithSev :: Recorder (WithSeverity MainLog)-        loggerWithSev = cmap renderPrettyWithSeverity timeStampLogger-      runIDM entryPoint entryFile entryArgs extraGhcArgs $-        debugInteractive (cmapWithSev InteractiveLog loggerWithSev)-+        l <- handleLogger stdout+        let runLogger = cmapWithSev InteractiveLog $ loggerFinal hdbOpts l+        runIDM runLogger entryPoint entryFile entryArgs extraGhcArgs $+          debugInteractive runLogger+    HdbProxy{port} -> do+        l <- handleLogger stdout+        let runLogger = cmapWithSev RunProxyClientLog $ loggerFinal hdbOpts l+        runInTerminalHdbProxy runLogger port  -- | Fetch config from environment, fallback to sane defaults getConfig :: Int -> IO ServerConfig@@ -194,29 +124,64 @@  data MainLog   = InitLog InitLog+  | LaunchLog T.Text   | InteractiveLog InteractiveLog+  | RunProxyServerLog ProxyLog+  | RunProxyClientLog ProxyLog  instance Pretty MainLog where   pretty = \ case     InitLog msg -> pretty msg+    LaunchLog msg -> pretty msg+    InteractiveLog msg -> pretty msg+    RunProxyServerLog msg -> pretty ("Proxy Server:" :: String) <+> pretty msg+    RunProxyClientLog msg -> pretty ("Proxy Client:" :: String) <+> pretty msg  -- | Main function where requests are received and Events + Responses are returned. -- The core logic of communicating between the client <-> adaptor <-> debugger -- is implemented in this function.-talk :: Recorder (WithSeverity MainLog) -> Command -> DebugAdaptor ()+talk :: Recorder (WithSeverity MainLog)+     -> IORef Bool+     -- ^ Whether the client supports runInTerminal+     -> IORef (Maybe Int)+     -- ^ The PID of the runInTerminal proxy process+     -> MVar ()+     -- ^ A var to block on waiting for the proxy client to connect, if a proxy+     -- connection is expected. See #95.+     -> Command -> DebugAdaptor () ---------------------------------------------------------------------------------talk l = \ case+talk l support_rit_var pid_var client_proxy_signal = \ case   CommandInitialize -> do-    -- InitializeRequestArguments{..} <- getArguments+    InitializeRequestArguments{supportsRunInTerminalRequest} <- getArguments+    let runInTerminal = fromMaybe False supportsRunInTerminalRequest+    liftIO $ writeIORef support_rit_var runInTerminal     sendInitializeResponse++    -- If runInTerminal is not supported by the client, signal readiness right away+    when (not runInTerminal) $+      liftIO $ putMVar client_proxy_signal ()+ --------------------------------------------------------------------------------   CommandLaunch -> do     launch_args <- getArguments-    merror <- runExceptT $ initDebugger (cmapWithSev InitLog l) launch_args++    supportsRunInTerminalRequest <- liftIO $ readIORef support_rit_var++    merror <- runExceptT $ initDebugger (cmapWithSev InitLog l) supportsRunInTerminalRequest launch_args     case merror of       Right () -> do         sendLaunchResponse   -- ack         sendInitializedEvent -- our debugger is only ready to be configured after it has launched the session++        -- Run the proxy in a separate terminal to accept stdin / forward stdout+        -- if it is supported+        when supportsRunInTerminalRequest $ do+          -- Run proxy thread, server side, and+          -- send the 'runInTerminal' request+          serverSideHdbProxy (cmapWithSev RunProxyServerLog l) client_proxy_signal++        logWith l Info $ LaunchLog $ T.pack "Debugger launched successfully."+       Left (InitFailed err) -> do         sendErrorResponse (ErrorMessage (T.pack err)) Nothing         exitCleanly@@ -237,6 +202,9 @@   CommandConfigurationDone -> do     sendConfigurationDoneResponse     -- now that it has been configured, start executing until it halts, then send an event++    -- wait for the proxy client to connect before starting the execution (#95)+    () <- liftIO $ takeMVar client_proxy_signal     startExecution >>= handleEvalResult False ----------------------------------------------------------------------------   CommandThreads    -> commandThreads@@ -253,13 +221,33 @@ ----------------------------------------------------------------------------   CommandEvaluate   -> commandEvaluate -----------------------------------------------------------------------------  CommandTerminate  -> commandTerminate+  CommandTerminate  -> do+    commandTerminate   CommandDisconnect -> commandDisconnect ----------------------------------------------------------------------------   CommandModules -> sendModulesResponse (ModulesResponse [] Nothing)   CommandSource -> undefined-  CommandPause -> undefined+  CommandPause -> pure () -- TODO   (CustomCommand "mycustomcommand") -> undefined+  (CustomCommand "runInTerminal") -> do+    -- Ignore result of runInTerminal (reverse request) response.+    -- If it fails, we simply continue without that functionality.+    pure ()+  other -> do+    sendErrorResponse (ErrorMessage (T.pack ("Unsupported command: " <> show other))) Nothing+    exitCleanly ---------------------------------------------------------------------------- -- talk cmd = logInfo $ BL8.pack ("GOT cmd " <> show cmd) ----------------------------------------------------------------------------++-- | Receive reverse request responses (such as runInTerminal response)+ack :: Recorder (WithSeverity MainLog)+    -> IORef (Maybe Int)+    -- ^ Reference to PID of runInTerminal proxy process running+    -> ReverseRequestResponse -> DebugAdaptorCont ()+ack l ref rrr = case rrr.reverseRequestCommand of+  ReverseCommandRunInTerminal -> do+    when rrr.success $ do+      logWith l Info $ LaunchLog $ T.pack "RunInTerminal was successful"+  _ -> pure ()+
test/haskell/Main.hs view
@@ -1,4 +1,121 @@+{-# LANGUAGE LambdaCase, OverloadedStrings, ViewPatterns, QuasiQuotes #-} module Main (main) where +import Text.RE.TDFA.Text.Lazy+import Text.Printf+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Lazy.Encoding as LT+import qualified Data.Text.Lazy.IO as LT+import qualified Data.ByteString.Lazy.Char8 as LBS+import qualified System.Process as P+import System.FilePath+import System.IO.Temp+import System.Exit+import System.IO+import Control.Exception++import Test.Tasty+import Test.Tasty.Golden as G+import Test.Tasty.Golden.Advanced as G++import Test.DAP.RunInTerminal+import Test.Utils+ main :: IO ()-main = putStrLn "Test suite not yet implemented."+main = do+  goldens <- mapM (mkGoldenTest False) =<< findByExtension [".hdb-test"] "test/golden"+  defaultMain $+    testGroup "Tests"+      [ testGroup "Golden tests" goldens+      , testGroup "Unit tests" unitTests+      ]++unitTests =+  [ runInTerminalTests+  ]++-- | Receives as an argument the path to the @*.hdb-test@ which contains the+-- shell invocation for running +mkGoldenTest :: Bool -> FilePath -> IO TestTree+mkGoldenTest keepTmpDirs path = do+  let testName   = takeBaseName     path+  let goldenPath = replaceExtension path ".hdb-stdout"+  return (goldenVsStringComparing testName goldenPath action)+  where+    action :: IO LBS.ByteString+    action = do+      script <- readFile path+      withHermeticDir keepTmpDirs (takeDirectory path) $ \test_dir -> do+        (_, Just hout, _, p)+          <- P.createProcess (P.shell script){P.cwd = Just test_dir, P.std_out = P.CreatePipe}+        P.waitForProcess p >>= \case+          ExitSuccess   -> LBS.hGetContents hout+          ExitFailure c -> error $ "Test script in " ++ test_dir ++ " failed with exit code: " ++ show c+        +--------------------------------------------------------------------------------+-- Tasty Golden Advanced wrapper+--------------------------------------------------------------------------------++-- | Compare a given string against the golden file's contents using the given normalising function+-- This is inlined from 'goldenVsString' and the accompanying functions. We+-- wanted the same but with a normalising function.+goldenVsStringComparing+  :: TestName -- ^ test name+  -> FilePath -- ^ path to the «golden» file (the file that contains correct output)+  -> IO LBS.ByteString+  -- ^ action that returns a string+  -> TestTree+  -- ^ the test verifies that the returned string is the same as the golden file contents+goldenVsStringComparing name ref act = do++  -- Normalise the output. The test file should already be saved normalised.+  goldenTest name (LT.decodeUtf8 <$> readFileStrict ref) normalisingAct cmpNormalising upd++  where+  upd = createDirectoriesAndWriteFile ref . LT.encodeUtf8++  -- Normalise the action producing the output+  normalisingAct = do+    tmpDir <- getCanonicalTemporaryDirectory+    replaceRE <- compileSearchReplace (tmpDir ++ ".*" ++ takeBaseName (takeDirectory ref) {- the folder in which the test is run, inside the canonical temp dir-})+                                      "<TEMPORARY-DIRECTORY>"++    let normalising (LT.decodeUtf8 -> txt) = txt *=~/ replaceRE++    normalising <$> act++  readFileStrict :: FilePath -> IO LBS.ByteString+  readFileStrict path = do+    s <- LBS.readFile path+    evaluate $ forceLbs s+    return s++  forceLbs :: LBS.ByteString -> ()+  forceLbs = LBS.foldr seq ()++--------------------------------------------------------------------------------+-- Normalisation+--------------------------------------------------------------------------------++  -- | Compare the golden test against the actual output after normalisation+  cmpNormalising :: LT.Text -> LT.Text -> IO (Maybe String)+  cmpNormalising x y = do++    let+      msg = printf "Test output was different from '%s'. It was:\n" ref <> (LT.unpack y)++    if x == y+      then return Nothing+      else do+        -- Call diff to show the difference+        withSystemTempFile "x.txt" $ \xf xH -> do+          withSystemTempFile "y.txt" $ \yf yH -> do+            LT.hPutStr xH x+            LT.hPutStr yH y+            hFlush xH+            hFlush yH+            hClose xH+            hClose yH+            (_exitCode, out, err) <- P.readProcessWithExitCode "diff" ["-u", xf, yf] ""+            return $ Just $ msg ++ "\nDiff output:\n" ++ out ++ err+
+ test/haskell/Test/DAP.hs view
@@ -0,0 +1,122 @@+-- | Utils essentially copied from `dap`'s test suite+{-# LANGUAGE NumericUnderscores #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE LambdaCase #-}+module Test.DAP where++----------------------------------------------------------------------------+import           Control.Applicative+import           Control.Concurrent+import           Data.Aeson.Encode.Pretty+import           Data.Aeson.Types+import           Data.Aeson.KeyMap+import           Control.Exception hiding (handle)+import qualified Data.ByteString.Lazy.Char8 as BL8 ( hPutStrLn )+import qualified Data.ByteString.Lazy       as LBS+import qualified Data.ByteString            as BS+import qualified Data.ByteString.Char8      as B8 ( hPutStrLn )+import qualified Data.HashMap.Strict as H+import           Network.Run.TCP+import           Network.Socket             (socketToHandle)+import           System.IO+import           System.Exit+import           Data.IORef+----------------------------------------------------------------------------+import           DAP.Utils+import           DAP.Server+import           DAP.Types (Command)+----------------------------------------------------------------------------+import           Test.Tasty.HUnit++-- | Sends a DAP request to the given handle+sendDAPRequest :: ToJSON a => Handle -> Command -> a -> IO ()+sendDAPRequest handle cmd args = do+  send handle+    [ "seq"      .= (1 :: Int)+    , "type"     .= ("request" :: String)+    , "command"  .= cmd+    , "arguments".= args+    ]++-- | Receive and decode DAP message+recvDAPResponse+  :: FromJSON a+  => Handle+  -- ^ Handle to receive bytes from+  -> IO a+  -- ^ The decoded DAP message+recvDAPResponse h = do+  readPayload h >>= \case+    Left e -> fail e+    Right actual ->+      -- Read field "body" or "arguments" (for reverse requests)+      case parseMaybe (withObject "response/rev request" $ \r -> (do+              ("response" :: String) <- r .: "type"+              True                   <- r .: "success"+              r .: "body") <|> (do+                ("request" :: String) <- r .: "type"+                r .: "arguments")+            ) actual of+        Nothing -> fail $ "Failed to parse DAP response body: " ++ show actual+        Just body ->+          case parseMaybe parseJSON body of+            Nothing -> fail $ "Failed to parse DAP response body content: " ++ show body+            Just res -> pure res++--------------------------------------------------------------------------------++-- | Sample host shared amongst client and server+--+testHost :: String+testHost = "127.0.0.1"++-- | Spawns a new mock client that connects to the mock server.+--+withNewClient :: Int -- ^ Port+              -> IORef Bool+              -- ^ True if we've already connected once and therefore should no longer retry+              -> (Handle -> IO ())+              -> IO ()+withNewClient port retryVar continue = flip catch exceptionHandler $+  runTCPClient testHost (show port) $ \socket -> do+    h <- socketToHandle socket ReadWriteMode+    hSetNewlineMode h NewlineMode { inputNL = CRLF, outputNL = CRLF }+    continue h `finally` hClose h+      where+        exceptionHandler :: SomeException -> IO ()+        exceptionHandler e = do+          do_retry <- readIORef retryVar+          if do_retry then do+            threadDelay 100_000 -- 0.1s+            -- Do it silently:+            -- putStrLn "Retrying connection..."+            withNewClient port retryVar continue+          else do+            putStrLn $ displayException e+            exitWith (ExitFailure 22)++-- | Helper to send JSON payloads to the server+--+send :: Handle -> [Pair] -> IO ()+send h message+  = BS.hPutStr h $ encodeBaseProtocolMessage (object message)++-- | Helper to receive JSON payloads to the client+-- checks if 'Handle' returns a subset expected payload+--+shouldReceive+  :: Handle+  -- ^ Handle to receive bytes from+  -> [Pair]+  -- ^ Subset of JSON values that should be present in the payload+  -> IO ()+shouldReceive h expected = do+  case object expected of+    Object ex ->+      readPayload h >>= \case+        Left e -> fail e+        Right actual+          | toHashMapText ex `H.isSubmapOf` toHashMapText actual -> pure ()+          | otherwise -> encodePretty actual @=? encodePretty ex+    _ -> fail "Invalid JSON"
+ test/haskell/Test/DAP/RunInTerminal.hs view
@@ -0,0 +1,279 @@+-- | 'runInTerminal' tests+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedRecordDot #-}+{-# LANGUAGE LambdaCase #-}+module Test.DAP.RunInTerminal (runInTerminalTests) where++import Control.Concurrent+import DAP.Types+import DAP.Utils+import Data.Aeson+import Data.IORef+import Data.List (isInfixOf)+import System.FilePath+import System.IO+import System.Random+import Test.DAP+import Test.Tasty+import Test.Tasty.HUnit+import Test.Utils+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Lazy.Char8 as LB8+import qualified Data.HashMap.Strict as H+import qualified Data.Text as T+import qualified System.Process as P++runInTerminalTests =+  testGroup "DAP.RunInTerminal"+    [ testCase "runInTerminal: proxy forwards stdin correctly" runInTerminal1+    ]++rit_keep_tmp_dirs :: Bool+rit_keep_tmp_dirs = False++runInTerminal1 = do+  withHermeticDir rit_keep_tmp_dirs "test/unit/T44" $ \test_dir -> do++    -- Come up with a random port+    testPort <- randomRIO (49152, 65534) :: IO Int++    -- Launch server process+    (Just hin, Just hout, _, p)+      <- P.createProcess (P.shell $ "hdb server --port " ++ show testPort)+          {P.cwd = Just test_dir, P.std_out = P.CreatePipe, P.std_in = P.CreatePipe}++    -- Fork thread to print out output of server process+    -- This is surprisingly needed, otherwise the server process+    -- will be broken, perhaps because it blocks trying to write to stdout/stderr if the buffer is full?+    forkIO $ do+      hSetBuffering hout LineBuffering+      let loop = do+            eof <- hIsEOF hout+            if eof+              then return ()+              else do+                _l <- hGetLine hout+                -- putStrLn ("[server] " ++ l)+                loop+      loop++    retryVar <- newIORef True+    -- Connect to the DAP server+    withNewClient testPort retryVar $ \handle -> do+      -- As soon as we get a connection, stop retrying+      writeIORef retryVar False++      -- Initialize+      sendDAPRequest handle CommandInitialize InitializeRequestArguments+        { adapterID = "haskell-debugger"+        , clientID = Just "mock-client"+        , clientName = Just "Mock Client"+        , columnsStartAt1 = Just True+        , linesStartAt1 = Just True+        , locale = Just "en"+        , pathFormat = Just Path+        , supportsArgsCanBeInterpretedByShell = Nothing+        , supportsInvalidatedEvent = Nothing+        , supportsMemoryEvent = Nothing+        , supportsMemoryReferences = Nothing+        , supportsProgressReporting = Nothing+        , supportsRunInTerminalRequest = Just True+        , supportsStartDebuggingRequest = Nothing+        , supportsVariablePaging = Nothing+        , supportsVariableType = Nothing+        }++      -- Recv initalize response+      _ <- shouldReceive handle []++      -- Send launch request+      send handle+        [ "command"  .= ("launch" :: String)+        , "seq"      .= (2 :: Int)+        , "type"     .= ("request" :: String)+        , "arguments".= object+            [ "entryFile" .= (test_dir </> "Main.hs" :: String)+            , "entryPoint" .= ("main" :: String)+            , "projectRoot" .= (test_dir :: String)+            , "extraGhcArgs" .= ([] :: [String])+            , "entryArgs" .= ([] :: [String])+            , "request" .= ("launch" :: String)+            ]+        ]++      _ <- shouldReceive handle+            ["type" .= ("event" :: String), "event" .= ("output" :: String)]+      _ <- shouldReceive handle+            ["type" .= ("event" :: String), "event" .= ("output" :: String)]+      _ <- shouldReceive handle+            [ "command" .= ("launch" :: String)+            , "success" .= True]+      _ <- shouldReceive handle+            [ "event" .= ("initialized" :: String)+            , "type"  .= ("event" :: String)+            ]++      -- Receive a runInTerminal request!!+      r@RunInTerminalRequestArguments{} <- recvDAPResponse handle+      (Just rit_in, Just rit_out, _, rit_p)+        <- P.createProcess+          (P.shell $ T.unpack $+              "/usr/bin/env " <> addRITEnv r.runInTerminalRequestArgumentsEnv <> " " <> T.unwords (r.runInTerminalRequestArgumentsArgs))+            {P.cwd = Just test_dir, P.std_in = P.CreatePipe, P.std_out = P.CreatePipe}++      -- Send a breakpoint request+      sendDAPRequest handle CommandSetBreakpoints+        SetBreakpointsArguments+          { setBreakpointsArgumentsSource = Source+              { sourceName = Just "Main.hs"+              , sourcePath = Just $ T.pack $ test_dir </> "Main.hs"+              , sourceSourceReference = Nothing+              , sourcePresentationHint = Nothing+              , sourceOrigin = Nothing+              , sourceAdapterData = Nothing+              , sourceChecksums = Nothing+              , sourceSources = Nothing+              }+          , setBreakpointsArgumentsBreakpoints = Just [SourceBreakpoint {sourceBreakpointLine = 6, sourceBreakpointColumn = Nothing, sourceBreakpointCondition = Nothing, sourceBreakpointHitCondition = Nothing, sourceBreakpointLogMessage = Nothing}]+          , setBreakpointsArgumentsLines = Just [6]+          , setBreakpointsArgumentsSourceModified = Just False+          }++      _ <- shouldReceive handle+            [ "command" .= ("setBreakpoints" :: String)+            , "success" .= True+            , "type"    .= ("response" :: String)+            ]++      -- Send runInTerminal response+      Just rit_pid <- P.getPid rit_p+      send handle+        [ "command" .= ("runInTerminal" :: String)+        , "seq"     .= (6 :: Int)+        , "type"    .= ("response" :: String)+        , "success" .= True+        , "body"    .= object+            [ "shellProcessId" .= (fromIntegral rit_pid :: Int)+            ]+        ]++      send handle+        [ "seq"      .= (1 :: Int)+        , "type"     .= ("request" :: String)+        , "command"  .= CommandConfigurationDone+        ]++      _ <- shouldReceive handle+            [ "command" .= ("configurationDone" :: String)+            , "success" .= True+            , "type"    .= ("response" :: String)+            ]++      -- The program should start running now, and hit the breakpoint.+      -- It will also print "Hello". Since the order is not important, we just+      -- match on the type == event and ignore if it's "stopped" or "output"+      _ <- shouldReceive handle+            [ "type"  .= ("event" :: String) ]+      _ <- shouldReceive handle+            [ "type"  .= ("event" :: String) ]++      -- Continue from "getLine" which will block waiting for input+      goToNextLine handle++      let secret_in = "SOMETHING_SECRET"++      -- Time to write to the stdin of the rit process+      hSetBuffering rit_in LineBuffering+      hPutStrLn rit_in secret_in++      -- Only after writing should we receive the next "stopped" event+      _ <- shouldReceive handle+            ["type" .= ("event" :: String), "event" .= ("stopped" :: String)]++      -- To next line, which should be the "putStrLn" after the "getLine"+      goToNextLine handle+      -- It's both stopped and we receive the SOMETHING_SECRET printed out. Order not important.+      _ <- shouldReceive handle+            ["type" .= ("event" :: String)]+      _ <- shouldReceive handle+            ["type" .= ("event" :: String)]++      -- The contents of the rit_output should contain "hello" plus printing of what we wrote+      out <- LBS.hGetContents rit_out+      let out_str = LB8.unpack out+      assertBool ("Expected output to contain 'hello', got: " ++ out_str)+                 ("hello" `isInfixOf` out_str)+      assertBool ("Expected output to contain '" ++ secret_in ++ "' , got: " ++ out_str)+                 (secret_in `isInfixOf` out_str)++      -- Send disconnect+      sendDAPRequest handle CommandDisconnect (DisconnectArguments {disconnectArgumentsRestart = False, disconnectArgumentsTerminateDebuggee = True, disconnectArgumentsSuspendDebuggee = False})+      _ <- shouldReceive handle+            [ "command" .= ("disconnect" :: String)+            , "success" .= True+            , "type"    .= ("response" :: String)+            ]+      _ <- shouldReceive handle+            [ "event" .= ("terminated" :: String)+            , "type"  .= ("event" :: String)+            ]+      -- Kill the processes if they're still running+      P.terminateProcess rit_p+      P.terminateProcess p++  where+    goToNextLine handle = do+      _ <- sendDAPRequest handle CommandNext (Just (NextArguments {nextArgumentsThreadId = 0, nextArgumentsSingleThread = Nothing, nextArgumentsGranularity = Nothing}))+      _ <- shouldReceive handle+            [ "command" .= ("next" :: String)+            , "success" .= True+            , "type"    .= ("response" :: String)+            ]+      return ()++    addRITEnv :: Maybe (H.HashMap T.Text T.Text) -> T.Text+    addRITEnv env =+      case env of+        Nothing -> ""+        Just env -> T.unwords [k{-todo: escape-} <> "=" <> v | (k,v) <- H.toList env]+--------------------------------------------------------------------------------+instance ToJSON InitializeRequestArguments where+  toJSON InitializeRequestArguments{..} = object+    [ "adapterID" .= adapterID+    , "clientID" .= clientID+    , "clientName" .= clientName+    , "columnsStartAt1" .= columnsStartAt1+    , "linesStartAt1" .= linesStartAt1+    , "locale" .= locale+    , "pathFormat" .= pathFormat+    , "supportsArgsCanBeInterpretedByShell" .= supportsArgsCanBeInterpretedByShell+    , "supportsInvalidatedEvent" .= supportsInvalidatedEvent+    , "supportsMemoryEvent" .= supportsMemoryEvent+    , "supportsMemoryReferences" .= supportsMemoryReferences+    , "supportsProgressReporting" .= supportsProgressReporting+    , "supportsRunInTerminalRequest" .= supportsRunInTerminalRequest+    , "supportsStartDebuggingRequest" .= supportsStartDebuggingRequest+    , "supportsVariablePaging" .= supportsVariablePaging+    , "supportsVariableType" .= supportsVariableType+    ]+instance ToJSON PathFormat where+  toJSON Path = String "path"+  toJSON URI  = String "uri"+  toJSON (PathFormat x) = String x+instance ToJSON SetBreakpointsArguments where+  toJSON = genericToJSONWithModifier+instance ToJSON NextArguments where+  toJSON = genericToJSONWithModifier+instance ToJSON DisconnectArguments where+  toJSON = genericToJSONWithModifier+instance ToJSON SourceBreakpoint where+  toJSON = genericToJSONWithModifier+instance ToJSON SteppingGranularity where+  toJSON = genericToJSONWithModifier+instance FromJSON bps => FromJSON (Breakpoints bps) where+  parseJSON = withObject "bkrps" $ \o ->+    Breakpoints <$> o .: "breakpoints"+instance FromJSON Breakpoint where+  parseJSON = genericParseJSONWithModifier+
+ test/haskell/Test/Utils.hs view
@@ -0,0 +1,25 @@+module Test.Utils where++import System.FilePath+import System.IO.Temp+import qualified System.Process as P++-- | Copy the contents of a test directory (with a `*.hdb-test` in the root) to+-- a temporary location and return the path to the new location.+withHermeticDir :: Bool               -- ^ Whether to keep the temp dir around for inspection+                -> FilePath           -- ^ Test dir+                -> (FilePath -> IO r) -- ^ Continuation receives hermetic test dir (in temporary dir)+                -> IO r+withHermeticDir keep src k = do+  withTmpDir "hdb-test" $ \dest -> do+    P.callCommand $ "cp -r " ++ src ++ " " ++ dest+    k (dest </> takeBaseName src)+  where+    withTmpDir | keep      = withPersistentSystemTempDirectory+               | otherwise = withSystemTempDirectory++    withPersistentSystemTempDirectory :: String -> (FilePath -> IO r) -> IO r+    withPersistentSystemTempDirectory template k' = do+      dir <- flip createTempDirectory template =<< getCanonicalTemporaryDirectory+      k' dir+