test_manager_lib/
debug_agent.rs

1// Copyright 2024 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::constants::TEST_ROOT_REALM_NAME;
6use crate::error::DebugAgentError;
7use crate::run_events::SuiteEvents;
8use ftest_manager::LaunchError;
9use fuchsia_component::client::connect_to_protocol;
10use futures::channel::mpsc;
11use futures::future::RemoteHandle;
12use futures::prelude::*;
13use {
14    fidl_fuchsia_debugger as fdbg, fidl_fuchsia_test_manager as ftest_manager,
15    fuchsia_async as fasync,
16};
17
18pub(crate) struct ThreadBacktraceInfo {
19    pub thread: u64,
20    pub backtrace: String,
21}
22
23pub(crate) struct DebugAgent {
24    proxy: fdbg::DebugAgentProxy,
25    event_stream_processor_handle: Option<futures::future::RemoteHandle<()>>,
26    takeable_fatal_exception_recevier: Option<mpsc::Receiver<ThreadBacktraceInfo>>,
27}
28
29impl DebugAgent {
30    // Creates a new `DebugAgent` and starts processing events received via the connection.
31    // The new `DebugAgent` maintains the connection until dropped.
32    pub(crate) async fn new() -> Result<Self, DebugAgentError> {
33        Self::from_launcher_proxy(
34            connect_to_protocol::<fdbg::LauncherMarker>()
35                .map_err(|e| DebugAgentError::ConnectToLauncher(e))?,
36        )
37        .await
38    }
39
40    // Creates a new `DebugAgent` from a launcher proxy and starts processing events received
41    // via the connection. The new `DebugAgent` maintains the connection until dropped.
42    async fn from_launcher_proxy(
43        launcher_proxy: fdbg::LauncherProxy,
44    ) -> Result<Self, DebugAgentError> {
45        let (proxy, agent_server_end) = fidl::endpoints::create_proxy();
46
47        launcher_proxy
48            .launch(agent_server_end)
49            .await
50            .map_err(|e| DebugAgentError::LaunchLocal(e))?
51            .map_err(|zx_status| DebugAgentError::LaunchResponse(zx_status))?;
52
53        // Attach to all test realms using a recursive, job-only filter. This will instruct
54        // debug_agent to monitor each test realm for unhandled exceptions, and will also mean that
55        // `get_process_info` will iterate over all processes in all active test realms unless
56        // further filtered. debug_agent will **not** create exception_channels on any entity other
57        // than the root job of each test realm. See the DebugAgent protocol documentation for
58        // further details.
59        let _ = proxy
60            .attach_to(
61                TEST_ROOT_REALM_NAME,
62                fdbg::FilterType::MonikerSuffix,
63                &fdbg::FilterOptions {
64                    job_only: Some(true),
65                    recursive: Some(true),
66                    ..Default::default()
67                },
68            )
69            .await
70            .map_err(|e| DebugAgentError::AttachToTestsLocal(e))?
71            .map_err(|e| DebugAgentError::AttachToTestsResponse(e))?;
72
73        let (fatal_exception_sender, fatal_exception_receiver) = mpsc::channel(1024);
74        let event_stream_processor_handle =
75            Self::begin_process_event_stream(proxy.take_event_stream(), fatal_exception_sender);
76
77        Ok(Self {
78            proxy,
79            event_stream_processor_handle: Some(event_stream_processor_handle),
80            takeable_fatal_exception_recevier: Some(fatal_exception_receiver),
81        })
82    }
83
84    // Takes the fatal exception receiver. This function panics if the receiver has already
85    // been taken.
86    pub(crate) fn take_fatal_exception_receiver(&mut self) -> mpsc::Receiver<ThreadBacktraceInfo> {
87        self.takeable_fatal_exception_recevier.take().unwrap()
88    }
89
90    // Stops sending fatal exceptions and closes the channel of fatal exceptions.
91    pub(crate) fn stop_sending_fatal_exceptions(&mut self) {
92        self.event_stream_processor_handle = None;
93    }
94
95    // Reports backtraces from all attached processes. This function executes in non-trivial
96    // time, but won't block long-term if the debug agent server is working properly.
97    pub(crate) async fn report_all_backtraces(
98        &self,
99        test_url: Option<&str>,
100        mut event_sender: mpsc::Sender<Result<SuiteEvents, LaunchError>>,
101    ) {
102        let (client_end, mut server_end) = fidl::handle::fuchsia_handles::Socket::create_stream();
103        event_sender.send(Ok(SuiteEvents::suite_stderr(client_end).into())).await.unwrap();
104
105        let mut maybe_filter: Option<fdbg::Filter> = None;
106        if let Some(test_url) = test_url {
107            // In the case of timeouts, we get the test's root realm URL.
108            //
109            // TODO(https://fxbug.dev/454904881): This won't handle concurrent runs of the same
110            // test suite very well, since they share the same package name, the filter will match
111            // all of them even if only one timed out. We'll need a more fine grained filter
112            // mechanism (job, moniker, etc) to filter to just the test realm we care about.
113            maybe_filter = Some(fdbg::Filter {
114                pattern: test_url.to_string(),
115                type_: fdbg::FilterType::Url,
116                options: fdbg::FilterOptions { recursive: Some(true), ..Default::default() },
117            });
118        }
119
120        let (iterator_proxy, iterator_server_end) = fidl::endpoints::create_proxy();
121        if let Err(_) = self
122            .proxy
123            .get_process_info(
124                &fdbg::GetProcessInfoOptions {
125                    filter: maybe_filter,
126                    interest: Some(fdbg::ThreadDetailsInterest {
127                        backtrace: Some(true),
128                        ..Default::default()
129                    }),
130                    ..Default::default()
131                },
132                iterator_server_end,
133            )
134            .await
135        {
136            // report error?
137            return;
138        }
139
140        loop {
141            match iterator_proxy.get_next().await {
142                Ok(result) => match result {
143                    Ok(infos) => {
144                        if infos.len() == 0 {
145                            break;
146                        }
147
148                        for info in infos {
149                            let _ = server_end.write(
150                                format!(
151                                    "Component: {}\nProcess: {} Thread: {}\n",
152                                    info.moniker, info.process, info.thread
153                                )
154                                .as_bytes(),
155                            );
156                            DebugAgent::write_backtrace_info(
157                                &ThreadBacktraceInfo {
158                                    thread: info.thread,
159                                    backtrace: info.details.backtrace.unwrap(),
160                                },
161                                &mut server_end,
162                            )
163                            .await;
164
165                            let _ = server_end.write("\n".as_bytes());
166                        }
167                    }
168                    Err(_e) => {
169                        break;
170                    }
171                },
172                Err(_e) => {
173                    break;
174                }
175            }
176        }
177    }
178
179    // Begins processing events sent by the debug agent server. Event processing runs in a
180    // spawned task and termines when the returned `RemoteHandle` is dropped.
181    fn begin_process_event_stream(
182        mut event_stream: fdbg::DebugAgentEventStream,
183        mut sender: mpsc::Sender<ThreadBacktraceInfo>,
184    ) -> RemoteHandle<()> {
185        let (event_stream_receiver, event_stream_receiver_handle) = async move {
186            loop {
187                match event_stream.next().await {
188                    Some(Ok(fdbg::DebugAgentEvent::OnFatalException {
189                        payload:
190                            fdbg::DebugAgentOnFatalExceptionRequest {
191                                thread: Some(thread),
192                                backtrace: Some(backtrace),
193                                ..
194                            },
195                    })) => {
196                        sender.send(ThreadBacktraceInfo { thread, backtrace }).await.unwrap();
197                    }
198                    Some(_) => {}
199                    None => break,
200                }
201            }
202        }
203        .remote_handle();
204
205        fasync::Task::spawn(event_stream_receiver).detach();
206
207        event_stream_receiver_handle
208    }
209
210    pub(crate) async fn write_backtrace_info(
211        backtrace_info: &ThreadBacktraceInfo,
212        socket_server_end: &mut fidl::Socket,
213    ) {
214        for line in backtrace_info.backtrace.split('\n') {
215            if line.starts_with("{{{reset:begin}}}") {
216                let _ = socket_server_end.write(line[..17].as_bytes());
217                let _ = socket_server_end.write(b"\n");
218                let _ = socket_server_end.write(line[17..].as_bytes());
219            } else {
220                let _ = socket_server_end.write(line.as_bytes());
221            }
222
223            let _ = socket_server_end.write(b"\n");
224        }
225    }
226}
227
228#[cfg(test)]
229mod test {
230    use crate::run_events::SuiteEventPayload;
231
232    use super::*;
233
234    const TEST_THREAD_1: u64 = 1234;
235    const TEST_THREAD_2: u64 = 5678;
236    const TEST_PROCESS_1: u64 = 4321;
237    const TEST_PROCESS_2: u64 = 8765;
238    const TEST_BACKTRACE_1: &str = "{{{reset:begin}}}test backtrace 1, with reset:begin prefix";
239    const TEST_BACKTRACE_2: &str = "test backtrace 2, no special prefix";
240    const TEST_MONIKER_1: &str = "test moniker 1";
241    const TEST_MONIKER_2: &str = "test moniker 2";
242    const TEST_STDERR_TEXT: &str = "Component: test moniker 1\nProcess: 4321 Thread: 1234\n\
243        {{{reset:begin}}}\ntest backtrace 1, with reset:begin prefix\n\nComponent: test moniker 2\n\
244        Process: 8765 Thread: 5678\ntest backtrace 2, no special prefix\n\n";
245    const TEST_STDERR_SIZE: usize = 204;
246
247    #[fuchsia::test]
248    async fn fatal_exceptions() {
249        let (mut under_test, mut debug_agent_service) = start_agent().await;
250
251        // Pretend a fatal exception has occurred.
252        debug_agent_service.send_on_fatal_exception_event(TEST_THREAD_1, TEST_BACKTRACE_1);
253
254        // Expect to receive the fatal exception.
255        let mut receiver = under_test.take_fatal_exception_receiver();
256        let thread_backtrace_info = receiver.next().await.expect("receive fatal exception");
257        assert_eq!(TEST_THREAD_1, thread_backtrace_info.thread);
258        assert_eq!(TEST_BACKTRACE_1, thread_backtrace_info.backtrace);
259
260        debug_agent_service.expect_nothing_more();
261    }
262
263    #[fuchsia::test]
264    async fn all_backtraces() {
265        let (under_test, mut debug_agent_service) = start_agent().await;
266
267        let (event_sender, mut event_receiver) =
268            mpsc::channel::<Result<SuiteEvents, LaunchError>>(16);
269
270        // Call `report_all_backtraces` while expecting a `GetProcessInfo` request and serving
271        // the process info iterator that is returned.
272        futures::future::join(under_test.report_all_backtraces(None, event_sender), async {
273            let iterator_server_end = debug_agent_service
274                .expect_get_process_info(&fdbg::GetProcessInfoOptions {
275                    filter: None,
276                    interest: Some(fdbg::ThreadDetailsInterest {
277                        backtrace: Some(true),
278                        ..Default::default()
279                    }),
280                    ..Default::default()
281                })
282                .await;
283            serve_process_info_iterator(iterator_server_end).await;
284        })
285        .await;
286
287        // Expect to receive a stderr event.
288        match event_receiver.try_next() {
289            Ok(Some(Ok(SuiteEvents {
290                timestamp: _,
291                payload: SuiteEventPayload::SuiteStderr(socket),
292            }))) => {
293                let mut buffer: [u8; 256] = [0; 256];
294                let size = socket.read(&mut buffer).expect("read from socket");
295                assert_eq!(TEST_STDERR_SIZE, size);
296                let stderr_string =
297                    std::str::from_utf8(&buffer[0..TEST_STDERR_SIZE]).expect("convert [u8] to str");
298                assert_eq!(TEST_STDERR_TEXT, stderr_string);
299            }
300            Ok(Some(Ok(_))) => {
301                assert!(false, "Expected SuiteStderr event, got other event");
302            }
303            Ok(Some(Err(e))) => {
304                assert!(false, "Expected event, got error {:?}", e);
305            }
306            Ok(None) => {
307                assert!(false, "Expected event, got channel closed");
308            }
309            Err(e) => {
310                assert!(false, "Expected event, got channel error {:?}", e);
311            }
312        }
313    }
314
315    // Fake fuchsia.debugger.Launcher service.
316    struct FakeLauncherService {
317        request_stream: fdbg::LauncherRequestStream,
318    }
319
320    impl FakeLauncherService {
321        // Creates a new FakeLauncherService.
322        fn new() -> (fdbg::LauncherProxy, Self) {
323            let (proxy, request_stream) =
324                fidl::endpoints::create_proxy_and_stream::<fdbg::LauncherMarker>();
325
326            (proxy, Self { request_stream })
327        }
328
329        // Expects a launch request and returns the resulting FakeDebugAgentService.
330        async fn expect_launch_request(&mut self) -> FakeDebugAgentService {
331            match self.next_request().await {
332                fdbg::LauncherRequest::Launch { agent, responder } => {
333                    responder.send(Ok(())).expect("send response to Launcher::Launch");
334                    return FakeDebugAgentService::new(agent);
335                }
336                _ => panic!("Call to unexpected method."),
337            }
338        }
339
340        async fn expect_connection_closed(&mut self) {
341            if let Some(request) = self.request_stream.try_next().await.expect("get request") {
342                assert!(false, "Expected connection closed, got request {:?}", request);
343            }
344        }
345
346        // Expects that no more requests have arrived and the client connection has not closed.
347        fn expect_nothing_more(&mut self) {
348            match self.request_stream.try_next().now_or_never() {
349                Some(Ok(None)) => {
350                    assert!(false, "Expected nothing more, got connection closed");
351                }
352                Some(Ok(v)) => {
353                    assert!(false, "Expected nothing more, got request {:?}", v);
354                }
355                Some(Err(e)) => {
356                    assert!(false, "Expected nothing more, got stream error {:?}", e);
357                }
358                None => {}
359            }
360        }
361
362        // Returns the next request.
363        async fn next_request(&mut self) -> fdbg::LauncherRequest {
364            self.request_stream
365                .try_next()
366                .await
367                .expect("get request")
368                .expect("connection not closed")
369        }
370    }
371
372    // Fake fuchsia.debugger.DebugAgent service.
373    struct FakeDebugAgentService {
374        request_stream: fdbg::DebugAgentRequestStream,
375        control_handle: fdbg::DebugAgentControlHandle,
376    }
377
378    impl FakeDebugAgentService {
379        // Creates a new FakeDebugAgentService.
380        fn new(server_end: fidl::endpoints::ServerEnd<fdbg::DebugAgentMarker>) -> Self {
381            let (request_stream, control_handle) = server_end.into_stream_and_control_handle();
382            FakeDebugAgentService { request_stream, control_handle }
383        }
384
385        // Expects an AttachTo request with the expected parameters and responds with `num_matches`.
386        async fn expect_attach_to(
387            &mut self,
388            expected_pattern: &str,
389            expected_type: fdbg::FilterType,
390            expected_options: fdbg::FilterOptions,
391            num_matches: u32,
392        ) {
393            match self.next_request().await {
394                fdbg::DebugAgentRequest::AttachTo { pattern, type_, options, responder } => {
395                    responder.send(Ok(num_matches)).expect("send response to DebugAgent::AttachTo");
396                    assert_eq!(expected_pattern, pattern);
397                    assert_eq!(expected_type, type_);
398                    assert_eq!(expected_options, options);
399                }
400                _ => panic!("Call to unexpected method."),
401            }
402        }
403
404        // Expects a GetProcessInfo request with the expected parameters and returns the server
405        // end of the process info iterator.
406        async fn expect_get_process_info(
407            &mut self,
408            expected_options: &fdbg::GetProcessInfoOptions,
409        ) -> fidl::endpoints::ServerEnd<fdbg::ProcessInfoIteratorMarker> {
410            match self.next_request().await {
411                fdbg::DebugAgentRequest::GetProcessInfo { options, iterator, responder } => {
412                    responder.send(Ok(())).expect("send response to DebugAgent::GetProcessInfo");
413                    assert_eq!(expected_options, &options);
414                    iterator
415                }
416                _ => panic!("Call to unexpected method."),
417            }
418        }
419
420        // Sends an OnFatalException event to the client.
421        fn send_on_fatal_exception_event(&mut self, thread: u64, backtrace: &str) {
422            self.control_handle
423                .send_on_fatal_exception(&fdbg::DebugAgentOnFatalExceptionRequest {
424                    thread: Some(thread),
425                    backtrace: Some(backtrace.to_string()),
426                    ..Default::default()
427                })
428                .expect("send OnFatalException event");
429        }
430
431        // Expects that no more requests have arrived and the client connection has not closed.
432        fn expect_nothing_more(&mut self) {
433            match self.request_stream.try_next().now_or_never() {
434                Some(Ok(None)) => {
435                    assert!(false, "Expected nothing more, got connection closed");
436                }
437                Some(Ok(v)) => {
438                    assert!(false, "Expected nothing more, got request {:?}", v);
439                }
440                Some(Err(e)) => {
441                    assert!(false, "Expected nothing more, got stream error {:?}", e);
442                }
443                None => {}
444            }
445        }
446
447        // Returns the next request.
448        async fn next_request(&mut self) -> fdbg::DebugAgentRequest {
449            self.request_stream
450                .try_next()
451                .await
452                .expect("get request")
453                .expect("connection not closed")
454        }
455    }
456
457    async fn start_agent() -> (DebugAgent, FakeDebugAgentService) {
458        // Create the fake launcher service.
459        let (launcher_proxy, mut launcher_service) = FakeLauncherService::new();
460        launcher_service.expect_nothing_more();
461
462        // Create a DebugAgent and the fake debug agent service.
463        let (under_test, mut debug_agent_service) =
464            futures::future::join(DebugAgent::from_launcher_proxy(launcher_proxy), async {
465                let mut debug_agent_service = launcher_service.expect_launch_request().await;
466                debug_agent_service
467                    .expect_attach_to(
468                        TEST_ROOT_REALM_NAME,
469                        fdbg::FilterType::MonikerSuffix,
470                        fdbg::FilterOptions {
471                            recursive: Some(true),
472                            job_only: Some(true),
473                            ..Default::default()
474                        },
475                        0,
476                    )
477                    .await;
478                debug_agent_service
479            })
480            .await;
481
482        let under_test = under_test.expect("create launcher from proxy");
483
484        // from_launcher_proxy consumes the proxy, so the launcher connection should close.
485        launcher_service.expect_connection_closed().await;
486        debug_agent_service.expect_nothing_more();
487
488        (under_test, debug_agent_service)
489    }
490
491    // Serves a `ProcessInfoIterator` producing two process infos.
492    async fn serve_process_info_iterator(
493        server_end: fidl::endpoints::ServerEnd<fdbg::ProcessInfoIteratorMarker>,
494    ) {
495        let mut request_stream = server_end.into_stream();
496
497        // Expect a request and respond with two process infos.
498        let request =
499            request_stream.try_next().await.expect("get request").expect("connection not closed");
500        match request {
501            fdbg::ProcessInfoIteratorRequest::GetNext { responder } => {
502                responder
503                    .send(Ok(&vec![
504                        fdbg::ProcessInfo {
505                            process: TEST_PROCESS_1,
506                            moniker: TEST_MONIKER_1.to_string(),
507                            thread: TEST_THREAD_1,
508                            details: fdbg::ThreadDetails {
509                                backtrace: Some(TEST_BACKTRACE_1.to_string()),
510                                ..Default::default()
511                            },
512                        },
513                        fdbg::ProcessInfo {
514                            process: TEST_PROCESS_2,
515                            moniker: TEST_MONIKER_2.to_string(),
516                            thread: TEST_THREAD_2,
517                            details: fdbg::ThreadDetails {
518                                backtrace: Some(TEST_BACKTRACE_2.to_string()),
519                                ..Default::default()
520                            },
521                        },
522                    ]))
523                    .expect("send response");
524            }
525        }
526
527        // Expect another request and respond with an empty vector.
528        let request =
529            request_stream.try_next().await.expect("get request").expect("connection not closed");
530        match request {
531            fdbg::ProcessInfoIteratorRequest::GetNext { responder } => {
532                responder.send(Ok(&vec![])).expect("send response");
533            }
534        }
535
536        // Expect the client to close the connection.
537        if let Some(request) = request_stream.try_next().await.expect("get request") {
538            assert!(false, "Expected connection closed, got request {:?}", request);
539        }
540    }
541}