Skip to content

Commit 6e6c0fb

Browse files
committed
Improve launch defaults and failure message
Make all launch settings have a sensible default except for "entryFile" which is always required. When "entryFile" is not provided, the error should be correctly reported via an ErrorResponse message. Fixes #71
1 parent 029998e commit 6e6c0fb

File tree

5 files changed

+133
-80
lines changed

5 files changed

+133
-80
lines changed

hdb/Development/Debug/Adapter/Exit.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ exitCleanupWithMsg
7171
-- killing the output thread with @destroyDebugSession@)
7272
-> String
7373
-- ^ Error message, logged with notification
74-
-> DebugAdaptor ()
74+
-> DebugAdaptor a
7575
exitCleanupWithMsg final_handle msg = do
7676
destroyDebugSession -- kill all session threads (including the output thread)
7777
do -- flush buffer and get all pending output from GHC

hdb/Development/Debug/Adapter/Init.hs

Lines changed: 41 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44
{-# LANGUAGE LambdaCase #-}
55
{-# LANGUAGE OverloadedStrings #-}
66
{-# LANGUAGE RecordWildCards #-}
7+
{-# LANGUAGE ViewPatterns #-}
78

89
-- | TODO: This module should be called Launch.
910
module Development.Debug.Adapter.Init where
1011

1112
import qualified Data.Text as T
1213
import qualified Data.Text.IO as T
1314
import qualified System.Process as P
15+
import Control.Monad.Except
16+
import Control.Monad.Trans
1417
import Data.Function
1518
import Data.Functor
19+
import Data.Maybe
1620
import Data.Version (Version(..), showVersion, makeVersion)
1721
import Control.Monad.IO.Class
1822
import System.IO
@@ -62,16 +66,16 @@ data LaunchArgs
6266
= LaunchArgs
6367
{ __sessionId :: Maybe String
6468
-- ^ SessionID, set by VSCode client
65-
, projectRoot :: FilePath
69+
, projectRoot :: Maybe FilePath
6670
-- ^ Absolute path to the project root
67-
, entryFile :: FilePath
71+
, entryFile :: Maybe FilePath
6872
-- ^ The file with the entry point e.g. @app/Main.hs@
69-
, entryPoint :: String
73+
, entryPoint :: Maybe String
7074
-- ^ Either @main@ or a function name
71-
, entryArgs :: [String]
75+
, entryArgs :: Maybe [String]
7276
-- ^ The arguments to either set as environment arguments when @entryPoint = "main"@
7377
-- or function arguments otherwise.
74-
, extraGhcArgs :: [String]
78+
, extraGhcArgs :: Maybe [String]
7579
-- ^ Additional arguments to pass to the GHC invocation inferred by hie-bios for this project
7680
} deriving stock (Show, Eq, Generic)
7781
deriving anyclass FromJSON
@@ -80,39 +84,53 @@ data LaunchArgs
8084
-- * Launch Debugger
8185
--------------------------------------------------------------------------------
8286

87+
-- | Exception type for when initialization fails
88+
newtype InitFailed = InitFailed String deriving Show
89+
8390
-- | Initialize debugger
8491
--
85-
-- Returns @True@ if successful.
86-
initDebugger :: Recorder (WithSeverity InitLog) -> LaunchArgs -> DebugAdaptor Bool
87-
initDebugger l LaunchArgs{__sessionId, projectRoot, entryFile = entryFile, entryPoint, entryArgs, extraGhcArgs} = do
92+
-- Returns @()@ if successful, throws @InitFailed@ otherwise
93+
initDebugger :: Recorder (WithSeverity InitLog) -> LaunchArgs -> ExceptT InitFailed DebugAdaptor ()
94+
initDebugger l LaunchArgs{ __sessionId
95+
, projectRoot = givenRoot
96+
, entryFile = entryFileMaybe
97+
, entryPoint = fromMaybe "main" -> entryPoint
98+
, entryArgs = fromMaybe [] -> entryArgs
99+
, extraGhcArgs = fromMaybe [] -> extraGhcArgs
100+
} = do
88101
syncRequests <- liftIO newEmptyMVar
89102
syncResponses <- liftIO newEmptyMVar
103+
104+
entryFile <- case entryFileMaybe of
105+
Nothing -> throwError $ InitFailed "Missing \"entryFile\" key in debugger configuration"
106+
Just ef -> pure ef
107+
108+
projectRoot <- maybe (liftIO getCurrentDirectory) pure givenRoot
109+
90110
let hieBiosLogger = cmapWithSev FlagsLog l
91111
cradle <- liftIO (hieBiosCradle hieBiosLogger projectRoot entryFile) >>=
92112
\ case
93-
Left e ->
94-
exitWithMsg e
95-
113+
Left e -> throwError $ InitFailed e
96114
Right c -> pure c
97115

98-
Output.console $ T.pack "Checking GHC version against debugger version..."
116+
lift $ Output.console $ T.pack "Checking GHC version against debugger version..."
99117
-- GHC is found in PATH (by hie-bios as well).
100118
actualVersion <- liftIO (hieBiosRuntimeGhcVersion hieBiosLogger cradle) >>=
101119
\ case
102-
Left e ->
103-
exitWithMsg e
120+
Left e -> throwError $ InitFailed e
104121
Right c -> pure c
105122
-- Compare the GLASGOW_HASKELL version (e.g. 913) with the actualVersion (e.g. 9.13.1):
106123
when (compileTimeGhcWithoutPatchVersion /= forgetPatchVersion actualVersion) $ do
107-
exitWithMsg $ "Aborting...! The GHC version must be the same which " ++
108-
"ghc-debug-adapter was compiled against (" ++
109-
showVersion compileTimeGhcWithoutPatchVersion++
110-
"). Instead, got " ++ (showVersion actualVersion) ++ "."
124+
throwError $ InitFailed $
125+
"Aborting...! The GHC version must be the same which " ++
126+
"ghc-debug-adapter was compiled against (" ++
127+
showVersion compileTimeGhcWithoutPatchVersion++
128+
"). Instead, got " ++ (showVersion actualVersion) ++ "."
111129

112-
Output.console $ T.pack "Discovering session flags with hie-bios..."
130+
lift $ Output.console $ T.pack "Discovering session flags with hie-bios..."
113131
mflags <- liftIO (hieBiosFlags hieBiosLogger cradle projectRoot entryFile)
114132
case mflags of
115-
Left e -> exitWithMsg e
133+
Left e -> throwError $ InitFailed e
116134
Right flags -> do
117135

118136
let nextFreshBreakpointId = 0
@@ -138,7 +156,7 @@ initDebugger l LaunchArgs{__sessionId, projectRoot, entryFile = entryFile, entry
138156
finished_init <- liftIO $ newEmptyMVar
139157

140158
let absEntryFile = normalise $ projectRoot </> entryFile
141-
registerNewDebugSession (maybe "debug-session" T.pack __sessionId) DAS{entryFile=absEntryFile,..}
159+
lift $ registerNewDebugSession (maybe "debug-session" T.pack __sessionId) DAS{entryFile=absEntryFile,..}
142160
[ debuggerThread l finished_init writeDebuggerOutput projectRoot flags extraGhcArgs absEntryFile defaultRunConf syncRequests syncResponses
143161
, handleDebuggerOutput readDebuggerOutput
144162
, stdoutCaptureThread
@@ -147,14 +165,13 @@ initDebugger l LaunchArgs{__sessionId, projectRoot, entryFile = entryFile, entry
147165

148166
-- Do not return until the initialization is finished
149167
liftIO (takeMVar finished_init) >>= \case
150-
Right () -> return True
168+
Right () -> pure ()
151169
Left e -> do
152170
-- The process terminates cleanly with an error code (probably exit failure = 1)
153171
-- This can happen if compilation fails and the compiler exits cleanly.
154172
--
155173
-- Instead of signalInitialized, respond with error and exit.
156-
exitCleanupWithMsg readDebuggerOutput e
157-
return False
174+
throwError $ InitFailed e
158175

159176
-- | This thread captures stdout from the debugger and sends it to the client.
160177
-- NOTE, redirecting the stdout handle is a process-global operation. So this thread

hdb/Main.hs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import System.Environment
55
import Data.Maybe
66
import Data.Version
77
import Text.Read
8+
import Control.Monad.Except
9+
import Control.Monad.IO.Class
810

911
import DAP
1012

@@ -209,14 +211,19 @@ talk l = \ case
209211
sendInitializeResponse
210212
--------------------------------------------------------------------------------
211213
CommandLaunch -> do
212-
success <- initDebugger (cmapWithSev InitLog l) =<< getArguments
213-
if success then do
214-
sendLaunchResponse -- ack
215-
sendInitializedEvent -- our debugger is only ready to be configured after it has launched the session
216-
else
217-
sendError ErrorMessageCancelled Nothing
214+
launch_args <- getArguments
215+
merror <- runExceptT $ initDebugger (cmapWithSev InitLog l) launch_args
216+
case merror of
217+
Right () -> do
218+
sendLaunchResponse -- ack
219+
sendInitializedEvent -- our debugger is only ready to be configured after it has launched the session
220+
Left (InitFailed err) -> do
221+
sendErrorResponse (ErrorMessage (T.pack err)) Nothing
222+
exitCleanly
218223
--------------------------------------------------------------------------------
219-
CommandAttach -> undefined
224+
CommandAttach -> do
225+
sendErrorResponse (ErrorMessage (T.pack "hdb does not support \"attach\" mode yet")) Nothing
226+
exitCleanly
220227
--------------------------------------------------------------------------------
221228
CommandBreakpointLocations -> commandBreakpointLocations
222229
CommandSetBreakpoints -> commandSetBreakpoints
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
main = do
2+
putStrLn "T71 is running!"

test/integration-tests/test/adapter.test.ts

Lines changed: 75 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -125,53 +125,46 @@ describe("Debug Adapter Tests", function () {
125125

126126
const basicTests = (launchCfg) => {
127127

128-
// The most basic functionality we test on various different
129-
// configurations (such as Cabal vs without project vs Stack)
130-
// The remaining tests are run only on one config since the set up
131-
// starts being more specific (e.g. multiple home units with Cabal)
132-
describe("Most basic functionality", function () {
133-
134-
describe(launchCfg.name, function () {
135-
136-
it('should run program to the end', () => {
137-
return Promise.all([
138-
dc.configurationSequence(),
139-
dc.launch(launchCfg.config),
140-
dc.waitForEvent('exited').then(e => new Promise((resolve, reject) => {
141-
if (e.body.exitCode == 0)
142-
resolve(e)
143-
else
144-
reject(new Error("Expecting ExitCode 1"))
145-
}))
146-
]);
147-
});
148-
149-
it('should stop on a breakpoint', () => {
150-
const expected = { path: launchCfg.config.projectRoot + "/" + launchCfg.config.entryFile, line: 6 }
151-
return dc.hitBreakpoint(launchCfg.config, { path: launchCfg.config.entryFile, line: 6 }, expected, expected);
152-
});
153-
154-
it('should stop on an exception', () => {
155-
156-
// const expected = { path: launchCfg.config.projectRoot + "/" + launchCfg.config.entryFile, line: 10 }
157-
// Currently, no information is provided when stopped at an exception:
158-
const expected = { }
159-
return Promise.all([
160-
161-
dc.waitForEvent('initialized').then(event => {
162-
return dc.setExceptionBreakpointsRequest({
163-
filters: [ 'break-on-exception' ]
164-
});
165-
}).then(response => {
166-
return dc.configurationDoneRequest();
167-
}),
168-
169-
dc.launch(launchCfg.config),
170-
171-
dc.assertStoppedLocation('exception', expected)
172-
]);
173-
});
174-
})
128+
describe(launchCfg.name, function () {
129+
130+
it('should run program to the end', () => {
131+
return Promise.all([
132+
dc.configurationSequence(),
133+
dc.launch(launchCfg.config),
134+
dc.waitForEvent('exited').then(e => new Promise((resolve, reject) => {
135+
if (e.body.exitCode == 0)
136+
resolve(e)
137+
else
138+
reject(new Error("Expecting ExitCode 1"))
139+
}))
140+
]);
141+
});
142+
143+
it('should stop on a breakpoint', () => {
144+
const expected = { path: launchCfg.config.projectRoot + "/" + launchCfg.config.entryFile, line: 6 }
145+
return dc.hitBreakpoint(launchCfg.config, { path: launchCfg.config.entryFile, line: 6 }, expected, expected);
146+
});
147+
148+
it('should stop on an exception', () => {
149+
150+
// const expected = { path: launchCfg.config.projectRoot + "/" + launchCfg.config.entryFile, line: 10 }
151+
// Currently, no information is provided when stopped at an exception:
152+
const expected = { }
153+
return Promise.all([
154+
155+
dc.waitForEvent('initialized').then(event => {
156+
return dc.setExceptionBreakpointsRequest({
157+
filters: [ 'break-on-exception' ]
158+
});
159+
}).then(response => {
160+
return dc.configurationDoneRequest();
161+
}),
162+
163+
dc.launch(launchCfg.config),
164+
165+
dc.assertStoppedLocation('exception', expected)
166+
]);
167+
});
175168
})
176169
}
177170

@@ -233,7 +226,41 @@ describe("Debug Adapter Tests", function () {
233226
assert.strictEqual(v.variablesReference, 0, `Variable ${v.name} should not be expandable (because it is a String)`);
234227
}
235228

236-
simpleLaunchConfigs.forEach(basicTests);
229+
// The most basic functionality we test on various different
230+
// configurations (such as Cabal vs without project vs Stack)
231+
// The remaining tests are run only on one config since the set up
232+
// starts being more specific (e.g. multiple home units with Cabal)
233+
describe("Most basic functionality", function () {
234+
simpleLaunchConfigs.forEach(basicTests);
235+
236+
describe("Other basic tests", function () {
237+
it('report error on missing "entryFile"', () => {
238+
let config = mkConfig({
239+
projectRoot: "/data/T71",
240+
})
241+
242+
const expected = { path: config.projectRoot + "/" + config.entryFile, line: 1 }
243+
// 'Launch' the debugger using this config and expect the response to the launch request to be a ResponseError with message "Missing \"entryFile\""
244+
return dc.launch(config).then(
245+
() => Promise.reject(new Error("Expecting launch to fail")),
246+
(err) => {
247+
assert.strictEqual(err.message, 'Missing "entryFile" key in debugger configuration');
248+
}
249+
)
250+
})
251+
252+
it('minimal configuration with just entryFile', async () => {
253+
let config = mkConfig({
254+
projectRoot: "/data/T71",
255+
entryFile: "Main.hs",
256+
})
257+
258+
const expected = { path: config.projectRoot + "/" + config.entryFile, line: 2 }
259+
return dc.hitBreakpoint(config, { path: config.entryFile, line: 2 }, expected, expected)
260+
})
261+
})
262+
263+
})
237264

238265
describe("Multiple main function tests", function () {
239266
const multiMainConfig = mkConfig({

0 commit comments

Comments
 (0)