forc_debug/server/
mod.rs

1mod handlers;
2mod state;
3mod util;
4
5use crate::{
6    error::{self, AdapterError, Error},
7    server::{state::ServerState, util::IdGenerator},
8    types::{ExitCode, Instruction},
9};
10use dap::{
11    events::{ExitedEventBody, OutputEventBody, StoppedEventBody},
12    prelude::*,
13    types::StartDebuggingRequestKind,
14};
15use forc_pkg::{
16    manifest::GenericManifestFile,
17    source::IPFSNode,
18    {self, BuildProfile, Built, BuiltPackage, PackageManifestFile},
19};
20use forc_test::{
21    execute::{DebugResult, TestExecutor},
22    setup::TestSetup,
23    BuiltTests,
24};
25use fuel_tx::GasCostsValues;
26use serde::{Deserialize, Serialize};
27use std::{
28    io::{BufReader, BufWriter, Read, Write},
29    process,
30    sync::Arc,
31};
32use sway_core::BuildTarget;
33
34pub const THREAD_ID: i64 = 0;
35pub const REGISTERS_VARIABLE_REF: i64 = 1;
36pub const INSTRUCTIONS_VARIABLE_REF: i64 = 2;
37
38#[derive(Debug, Clone, Default, Serialize, Deserialize)]
39pub struct AdditionalData {
40    pub program: String,
41}
42
43/// This struct is a stateful representation of a Debug Adapter Protocol (DAP) server. It holds everything
44/// needed to implement (DAP)[https://microsoft.github.io/debug-adapter-protocol/].
45///
46/// It is responsible for handling requests and sending responses and events to the client. It manages
47/// the state of the server and the underlying VM instances used for debugging sway tests. It builds sway code
48/// and generates source maps for debugging. It also manages the test setup and reports results back to the client.
49pub struct DapServer {
50    /// The DAP server transport.
51    server: Server<Box<dyn Read>, Box<dyn Write>>,
52    /// Used to generate unique breakpoint IDs.
53    breakpoint_id_gen: IdGenerator,
54    /// The server state.
55    pub state: ServerState,
56}
57
58impl Default for DapServer {
59    fn default() -> Self {
60        Self::new(Box::new(std::io::stdin()), Box::new(std::io::stdout()))
61    }
62}
63
64impl DapServer {
65    /// Creates a new DAP server with custom input and output streams.
66    ///
67    /// # Arguments
68    /// * `input` - Source of DAP protocol messages (usually stdin)
69    /// * `output` - Destination for DAP protocol messages (usually stdout)
70    pub fn new(input: Box<dyn Read>, output: Box<dyn Write>) -> Self {
71        let server = Server::new(BufReader::new(input), BufWriter::new(output));
72        DapServer {
73            server,
74            state: ServerState::default(),
75            breakpoint_id_gen: IdGenerator::default(),
76        }
77    }
78
79    /// Runs the debug server event loop, handling client requests until completion or error.
80    pub fn start(&mut self) -> error::Result<()> {
81        loop {
82            let req = match self.server.poll_request()? {
83                Some(req) => req,
84                None => return Err(Error::AdapterError(AdapterError::MissingCommand)),
85            };
86
87            // Handle the request and send response
88            let response = self.handle_request(req)?;
89            self.server.respond(response)?;
90
91            // Handle one-time initialization
92            if !self.state.initialized_event_sent {
93                let _ = self.server.send_event(Event::Initialized);
94                self.state.initialized_event_sent = true;
95            }
96
97            // Handle launch after configuration is complete
98            if self.should_launch() {
99                self.state.started_debugging = true;
100                match self.launch() {
101                    Ok(true) => continue,
102                    Ok(false) => self.exit(0), // The tests finished executing
103                    Err(e) => {
104                        self.error(format!("Launch error: {e:?}"));
105                        self.exit(1);
106                    }
107                }
108            }
109        }
110    }
111
112    /// Processes a debug adapter request and generates appropriate response.
113    fn handle_request(&mut self, req: Request) -> error::Result<Response> {
114        let (result, exit_code) = self.handle_command(&req.command).into_tuple();
115        let response = match result {
116            Ok(rsp) => Ok(req.success(rsp)),
117            Err(e) => {
118                self.error(format!("{e:?}"));
119                Ok(req.error(&format!("{e:?}")))
120            }
121        };
122        if let Some(exit_code) = exit_code {
123            self.exit(exit_code);
124        }
125        response
126    }
127
128    /// Handles a command and returns the result and exit code, if any.
129    pub fn handle_command(&mut self, command: &Command) -> HandlerResult {
130        match command {
131            Command::Attach(_) => self.handle_attach(),
132            Command::BreakpointLocations(ref args) => {
133                self.handle_breakpoint_locations_command(args)
134            }
135            Command::ConfigurationDone => self.handle_configuration_done(),
136            Command::Continue(_) => self.handle_continue(),
137            Command::Disconnect(_) => HandlerResult::ok_with_exit(ResponseBody::Disconnect, 0),
138            Command::Evaluate(args) => self.handle_evaluate(args),
139            Command::Initialize(_) => self.handle_initialize(),
140            Command::Launch(ref args) => self.handle_launch(args),
141            Command::Next(_) => self.handle_next(),
142            Command::Pause(_) => self.handle_pause(),
143            Command::Restart(_) => self.handle_restart(),
144            Command::Scopes(_) => self.handle_scopes(),
145            Command::SetBreakpoints(ref args) => self.handle_set_breakpoints_command(args),
146            Command::StackTrace(_) => self.handle_stack_trace_command(),
147            Command::StepIn(_) => {
148                self.error("This feature is not currently supported.".into());
149                HandlerResult::ok(ResponseBody::StepIn)
150            }
151            Command::StepOut(_) => {
152                self.error("This feature is not currently supported.".into());
153                HandlerResult::ok(ResponseBody::StepOut)
154            }
155            Command::Terminate(_) => HandlerResult::ok_with_exit(ResponseBody::Terminate, 0),
156            Command::TerminateThreads(_) => {
157                HandlerResult::ok_with_exit(ResponseBody::TerminateThreads, 0)
158            }
159            Command::Threads => self.handle_threads(),
160            Command::Variables(ref args) => self.handle_variables_command(args),
161            _ => HandlerResult::err(AdapterError::UnhandledCommand {
162                command: command.clone(),
163            }),
164        }
165    }
166
167    /// Checks whether debug session is ready to begin launching tests.
168    fn should_launch(&self) -> bool {
169        self.state.configuration_done
170            && !self.state.started_debugging
171            && matches!(self.state.mode, Some(StartDebuggingRequestKind::Launch))
172    }
173
174    /// Logs a message to the client's debugger console output.
175    fn log(&mut self, output: String) {
176        let _ = self.server.send_event(Event::Output(OutputEventBody {
177            output,
178            ..Default::default()
179        }));
180    }
181
182    /// Logs an error message to the client's debugger console output.
183    fn error(&mut self, output: String) {
184        let _ = self.server.send_event(Event::Output(OutputEventBody {
185            output,
186            category: Some(types::OutputEventCategory::Stderr),
187            ..Default::default()
188        }));
189    }
190
191    /// Logs test execution results in a cargo-test-like format, showing duration and gas usage for each test.
192    fn log_test_results(&mut self) {
193        if !self.state.executors.is_empty() {
194            return;
195        }
196        let test_results = &self.state.test_results;
197        let test_lines = test_results
198            .iter()
199            .map(|r| {
200                let outcome = if r.passed() { "ok" } else { "failed" };
201                format!(
202                    "test {} ... {} ({}ms, {} gas)",
203                    r.name,
204                    outcome,
205                    r.duration.as_millis(),
206                    r.gas_used
207                )
208            })
209            .collect::<Vec<_>>()
210            .join("\n");
211
212        let passed = test_results.iter().filter(|r| r.passed()).count();
213        let final_outcome = if passed == test_results.len() {
214            "OK"
215        } else {
216            "FAILED"
217        };
218
219        self.log(format!(
220            "{test_lines}\nResult: {final_outcome}. {passed} passed. {} failed.\n",
221            test_results.len() - passed
222        ));
223    }
224
225    /// Handles a `launch` request. Returns true if the server should continue running.
226    pub fn launch(&mut self) -> Result<bool, AdapterError> {
227        // Build tests for the given path.
228        let (pkg_to_debug, test_setup) = self.build_tests()?;
229        let entries = pkg_to_debug.bytecode.entries.iter().filter_map(|entry| {
230            if let Some(test_entry) = entry.kind.test() {
231                return Some((entry, test_entry));
232            }
233            None
234        });
235
236        // Construct a TestExecutor for each test and store it
237        let executors: Vec<TestExecutor> = entries
238            .filter_map(|(entry, test_entry)| {
239                let offset = u32::try_from(entry.finalized.imm)
240                    .expect("test instruction offset out of range");
241                let name = entry.finalized.fn_name.clone();
242                if test_entry.file_path.as_path() != self.state.program_path.as_path() {
243                    return None;
244                }
245
246                TestExecutor::build(
247                    &pkg_to_debug.bytecode.bytes,
248                    offset,
249                    test_setup.clone(),
250                    test_entry,
251                    name.clone(),
252                    // TODO: (GAS-COSTS) Provide gas costs values here, similar like in `forc test`.
253                    //       See: https://github.com/FuelLabs/sway/issues/7472
254                    GasCostsValues::default(),
255                )
256                .ok()
257            })
258            .collect();
259        self.state.init_executors(executors);
260
261        // Start debugging
262        self.start_debugging_tests(false)
263    }
264
265    /// Builds the tests at the given [PathBuf] and stores the source maps.
266    pub fn build_tests(&mut self) -> Result<(BuiltPackage, TestSetup), AdapterError> {
267        if let Some(pkg) = &self.state.built_package {
268            if let Some(setup) = &self.state.test_setup {
269                return Ok((pkg.clone(), setup.clone()));
270            }
271        }
272
273        // 1. Build the packages
274        let manifest_file = forc_pkg::manifest::ManifestFile::from_dir(&self.state.program_path)
275            .map_err(|err| AdapterError::BuildFailed {
276                reason: format!("read manifest file: {err:?}"),
277            })?;
278        let pkg_manifest: PackageManifestFile =
279            manifest_file
280                .clone()
281                .try_into()
282                .map_err(|err: anyhow::Error| AdapterError::BuildFailed {
283                    reason: format!("package manifest: {err:?}"),
284                })?;
285        let member_manifests =
286            manifest_file
287                .member_manifests()
288                .map_err(|err| AdapterError::BuildFailed {
289                    reason: format!("member manifests: {err:?}"),
290                })?;
291        let lock_path = manifest_file
292            .lock_path()
293            .map_err(|err| AdapterError::BuildFailed {
294                reason: format!("lock path: {err:?}"),
295            })?;
296        let build_plan = forc_pkg::BuildPlan::from_lock_and_manifests(
297            &lock_path,
298            &member_manifests,
299            false,
300            false,
301            &IPFSNode::default(),
302        )
303        .map_err(|err| AdapterError::BuildFailed {
304            reason: format!("build plan: {err:?}"),
305        })?;
306
307        let project_name = pkg_manifest.project_name();
308
309        let outputs = std::iter::once(build_plan.find_member_index(project_name).ok_or(
310            AdapterError::BuildFailed {
311                reason: format!("find built project: {project_name}"),
312            },
313        )?)
314        .collect();
315
316        let built_packages = forc_pkg::build(
317            &build_plan,
318            BuildTarget::default(),
319            &BuildProfile {
320                optimization_level: sway_core::OptLevel::Opt0,
321                include_tests: true,
322                ..Default::default()
323            },
324            &outputs,
325            &[],
326            &[],
327            None,
328        )
329        .map_err(|err| AdapterError::BuildFailed {
330            reason: format!("build packages: {err:?}"),
331        })?;
332
333        // 2. Store the source maps and find debug package
334        let pkg_to_debug = built_packages
335            .iter()
336            .find(|(_, pkg)| pkg.descriptor.manifest_file == pkg_manifest)
337            .map(|(_, pkg)| pkg)
338            .ok_or(AdapterError::BuildFailed {
339                reason: format!("find package: {project_name}"),
340            })?;
341
342        self.state.source_map = pkg_to_debug.source_map.clone();
343
344        // 3. Build the tests
345        let built = Built::Package(Arc::from(pkg_to_debug.clone()));
346
347        let built_tests = BuiltTests::from_built(built, &build_plan).map_err(|err| {
348            AdapterError::BuildFailed {
349                reason: format!("build tests: {err:?}"),
350            }
351        })?;
352
353        let pkg_tests = match built_tests {
354            BuiltTests::Package(pkg_tests) => pkg_tests,
355            BuiltTests::Workspace(_) => {
356                return Err(AdapterError::BuildFailed {
357                    reason: "package tests: workspace tests not supported".into(),
358                })
359            }
360        };
361        let test_setup = pkg_tests.setup().map_err(|err| AdapterError::BuildFailed {
362            reason: format!("test setup: {err:?}"),
363        })?;
364        self.state.built_package = Some(pkg_to_debug.clone());
365        self.state.test_setup = Some(test_setup.clone());
366        Ok((pkg_to_debug.clone(), test_setup))
367    }
368
369    /// Sends the 'exited' event to the client and kills the server process.
370    fn exit(&mut self, exit_code: i64) {
371        let _ = self
372            .server
373            .send_event(Event::Exited(ExitedEventBody { exit_code }));
374        process::exit(exit_code as i32);
375    }
376
377    fn stop(&mut self, pc: Instruction) -> Result<bool, AdapterError> {
378        let (hit_breakpoint_ids, reason) =
379            if let Ok(breakpoint_id) = self.state.vm_pc_to_breakpoint_id(pc) {
380                self.state.stopped_on_breakpoint_id = Some(breakpoint_id);
381                (
382                    Some(vec![breakpoint_id]),
383                    types::StoppedEventReason::Breakpoint,
384                )
385            } else {
386                self.state.stopped_on_breakpoint_id = None;
387                (None, types::StoppedEventReason::Step)
388            };
389
390        let _ = self.server.send_event(Event::Stopped(StoppedEventBody {
391            reason,
392            hit_breakpoint_ids,
393            description: None,
394            thread_id: Some(THREAD_ID),
395            preserve_focus_hint: None,
396            text: None,
397            all_threads_stopped: None,
398        }));
399        Ok(true)
400    }
401
402    /// Starts debugging all tests.
403    /// `single_stepping` indicates whether the VM should break after one instruction.
404    ///
405    /// Returns true if it has stopped on a breakpoint or false if all tests have finished.
406    fn start_debugging_tests(&mut self, single_stepping: bool) -> Result<bool, AdapterError> {
407        self.state.update_vm_breakpoints();
408
409        while let Some(executor) = self.state.executors.first_mut() {
410            executor.interpreter.set_single_stepping(single_stepping);
411            match executor.start_debugging()? {
412                DebugResult::TestComplete(result) => {
413                    self.state.test_complete(result);
414                }
415                DebugResult::Breakpoint(pc) => {
416                    executor.interpreter.set_single_stepping(false);
417                    return self.stop(pc);
418                }
419            };
420        }
421        self.log_test_results();
422        Ok(false)
423    }
424
425    /// Continues debugging the current test and starts the next one if no breakpoint is hit.
426    /// `single_stepping` indicates whether the VM should break after one instruction.
427    ///
428    /// Returns true if it has stopped on a breakpoint or false if all tests have finished.
429    fn continue_debugging_tests(&mut self, single_stepping: bool) -> Result<bool, AdapterError> {
430        self.state.update_vm_breakpoints();
431
432        if let Some(executor) = self.state.executors.first_mut() {
433            executor.interpreter.set_single_stepping(single_stepping);
434            match executor.continue_debugging()? {
435                DebugResult::TestComplete(result) => {
436                    self.state.test_complete(result);
437                    // The current test has finished, but there could be more tests to run. Start debugging the
438                    // remaining tests.
439                    return self.start_debugging_tests(single_stepping);
440                }
441                DebugResult::Breakpoint(pc) => {
442                    executor.interpreter.set_single_stepping(false);
443                    return self.stop(pc);
444                }
445            }
446        }
447        self.log_test_results();
448        Ok(false)
449    }
450}
451
452/// Represents the result of a DAP handler operation, combining the response/error and an optional exit code
453#[derive(Debug)]
454pub struct HandlerResult {
455    response: Result<ResponseBody, AdapterError>,
456    exit_code: Option<ExitCode>,
457}
458
459impl HandlerResult {
460    /// Creates a new successful result with no exit code
461    pub fn ok(response: ResponseBody) -> Self {
462        Self {
463            response: Ok(response),
464            exit_code: None,
465        }
466    }
467
468    /// Creates a new successful result with an exit code
469    pub fn ok_with_exit(response: ResponseBody, code: ExitCode) -> Self {
470        Self {
471            response: Ok(response),
472            exit_code: Some(code),
473        }
474    }
475
476    /// Creates a new error result with an exit code
477    pub fn err_with_exit(error: AdapterError, code: ExitCode) -> Self {
478        Self {
479            response: Err(error),
480            exit_code: Some(code),
481        }
482    }
483
484    /// Creates a new error result with no exit code
485    pub fn err(error: AdapterError) -> Self {
486        Self {
487            response: Err(error),
488            exit_code: None,
489        }
490    }
491
492    /// Deconstructs the result into its original tuple form
493    pub fn into_tuple(self) -> (Result<ResponseBody, AdapterError>, Option<ExitCode>) {
494        (self.response, self.exit_code)
495    }
496}