1use aya::{
19 maps::{perf::PerfEventArray, MapData, RingBuf},
20 programs::{uprobe::UProbeLinkId, ProgramError, UProbe},
21 Ebpf, EbpfLoader, VerifierLogLevel,
22};
23use ghostscope_protocol::{ParsedTraceEvent, StreamingTraceParser, TraceContext};
24use log::log_enabled;
25use log::Level as LogLevel;
26use std::convert::TryInto;
27use std::future::poll_fn;
28use std::os::unix::io::AsRawFd;
29use std::os::unix::io::RawFd;
30use std::path::Path;
31use std::task::Poll;
32use tokio::io::unix::AsyncFd;
33use tokio::io::Interest;
34use tracing::{debug, error, info, warn};
35
36mod kernel_caps;
38pub use kernel_caps::{KernelCapabilities, KernelCapabilityError};
39
40mod error;
42pub use error::{LoaderError, Result};
43
44mod uprobe;
46use uprobe::UprobeAttachmentParams;
47
48use ghostscope_process::pinned_bpf_maps::{
50 bpffs_mount_hint_for_pin_path, pid_aliases_pin_path, proc_offsets_pin_dir,
51 proc_offsets_pin_path,
52};
53
54enum EventMap {
56 RingBuf(RingBuf<MapData>),
57 PerfEventArray {
58 _map: PerfEventArray<MapData>,
59 cpu_buffers: Vec<PerfEventCpuBuffer>,
60 },
61}
62
63#[derive(Clone, Copy, Debug)]
64struct PerfBufferFd(RawFd);
65
66impl AsRawFd for PerfBufferFd {
67 fn as_raw_fd(&self) -> RawFd {
68 self.0
69 }
70}
71
72struct PerfEventCpuBuffer {
73 cpu_id: u32,
74 buffer: aya::maps::perf::PerfEventArrayBuffer<MapData>,
75 readiness: AsyncFd<PerfBufferFd>,
76}
77
78enum UProbeAttachLocation<'a> {
81 AbsoluteOffset(u64),
82 Function(&'a str),
83}
84
85impl<'a> UProbeAttachLocation<'a> {
86 fn attach<T: AsRef<Path>>(
87 self,
88 program: &mut UProbe,
89 target: T,
90 pid: Option<i32>,
91 ) -> std::result::Result<UProbeLinkId, ProgramError> {
92 match self {
93 Self::AbsoluteOffset(offset) => program.attach(None, offset, target, pid),
94 Self::Function(fn_name) => program.attach(Some(fn_name), 0, target, pid),
95 }
96 }
97}
98
99pub fn hello() -> String {
100 format!("Loader: {}", ghostscope_compiler::hello())
101}
102
103pub struct GhostScopeLoader {
111 bpf: Ebpf,
113 event_map: Option<EventMap>,
115 uprobe_link: Option<UProbeLinkId>,
117 attachment_params: Option<UprobeAttachmentParams>,
119 parser: StreamingTraceParser,
121 trace_context: Option<TraceContext>,
123 perf_page_count: Option<usize>,
125}
126
127impl std::fmt::Debug for GhostScopeLoader {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.debug_struct("GhostScopeLoader")
130 .field("bpf", &"<eBPF object>")
131 .field("event_map", &self.event_map.is_some())
132 .field("uprobe_attached", &self.uprobe_link.is_some())
133 .field("attachment_params", &self.attachment_params.is_some())
134 .finish()
135 }
136}
137
138impl GhostScopeLoader {
139 pub fn new(bytecode: &[u8]) -> Result<Self> {
145 info!(
146 "Loading eBPF program from bytecode ({} bytes)",
147 bytecode.len()
148 );
149
150 let pin_path = proc_offsets_pin_path()
152 .map_err(|e| LoaderError::Generic(format!("Failed to resolve pinned map path: {e}")))?;
153 if !pin_path.exists() {
154 let hint = bpffs_mount_hint_for_pin_path(&pin_path)
155 .map(|hint| format!(" {hint}"))
156 .unwrap_or_default();
157 return Err(LoaderError::Generic(format!(
158 "Pinned map '{}' not found. Please call ghostscope-process to create it first.{}",
159 pin_path.display(),
160 hint
161 )));
162 }
163 let alias_pin_path = pid_aliases_pin_path().map_err(|e| {
164 LoaderError::Generic(format!("Failed to resolve pinned alias map path: {e}"))
165 })?;
166 if !alias_pin_path.exists() {
167 let hint = bpffs_mount_hint_for_pin_path(&alias_pin_path)
168 .map(|hint| format!(" {hint}"))
169 .unwrap_or_default();
170 return Err(LoaderError::Generic(format!(
171 "Pinned map '{}' not found. Please call ghostscope-process to create it first.{}",
172 alias_pin_path.display(),
173 hint
174 )));
175 }
176
177 let mut loader = EbpfLoader::new();
178 let use_verbose = cfg!(debug_assertions)
179 || log_enabled!(LogLevel::Trace)
180 || log_enabled!(LogLevel::Debug);
181 if use_verbose {
182 loader.verifier_log_level(VerifierLogLevel::VERBOSE | VerifierLogLevel::STATS);
183 tracing::info!("BPF verifier logs: VERBOSE (debug build/log)");
184 } else {
185 loader.verifier_log_level(VerifierLogLevel::DEBUG | VerifierLogLevel::STATS);
186 tracing::info!("BPF verifier logs: DEBUG (release/info)");
187 }
188 let pin_dir = proc_offsets_pin_dir().map_err(|e| {
192 LoaderError::Generic(format!("Failed to resolve pinned map directory: {e}"))
193 })?;
194 if pin_dir.exists() {
195 loader.map_pin_path(&pin_dir);
196 tracing::info!(
197 "Configured map pin directory for reuse: {}",
198 pin_dir.display()
199 );
200 }
201 match loader.load(bytecode) {
202 Ok(bpf) => {
203 info!("Successfully loaded eBPF program");
204 Ok(Self {
205 bpf,
206 event_map: None,
207 uprobe_link: None,
208 attachment_params: None,
209 parser: StreamingTraceParser::new(),
210 trace_context: None,
211 perf_page_count: None,
212 })
213 }
214 Err(e) => {
215 error!("Failed to load BPF program: {:?}", e);
216 match &e {
218 aya::EbpfError::ParseError(parse_err) => {
219 error!("Parse error details: {:?}", parse_err);
220 }
221 aya::EbpfError::BtfError(btf_err) => {
222 error!("BTF error details: {:?}", btf_err);
223 }
224 _ => {
225 error!("Other BPF error: {:?}", e);
226 }
227 }
228 Err(LoaderError::Aya(e))
229 }
230 }
231 }
232
233 pub fn attach_uprobe(
239 &mut self,
240 target_binary: &str,
241 function_name: &str,
242 offset: Option<u64>,
243 pid: Option<i32>,
244 ) -> Result<()> {
245 self.attach_uprobe_with_program_name(target_binary, function_name, offset, pid, None)
246 }
247
248 pub fn set_perf_page_count(&mut self, pages: u32) {
250 self.perf_page_count = Some(pages as usize);
251 }
252
253 pub fn attach_uprobe_with_program_name(
255 &mut self,
256 target_binary: &str,
257 function_name: &str,
258 offset: Option<u64>,
259 pid: Option<i32>,
260 program_name: Option<&str>,
261 ) -> Result<()> {
262 info!("attach_uprobe called with offset: {:?}", offset);
263 if let Some(offset) = offset {
264 info!(
265 "Using offset-based attachment: {} at 0x{:x} ({}) (pid: {:?})",
266 target_binary, offset, function_name, pid
267 );
268 } else {
269 info!(
270 "Using function name-based attachment: {}:{} (pid: {:?})",
271 target_binary, function_name, pid
272 );
273 }
274
275 let available_programs: Vec<String> = self
277 .bpf
278 .programs()
279 .map(|(name, _)| name.to_string())
280 .collect();
281
282 info!("Available programs:");
284 for name in &available_programs {
285 info!(" - {}", name);
286 }
287
288 let program_name: String = if let Some(name) = program_name {
290 info!("Using specified program name: {}", name);
292 if available_programs.contains(&name.to_string()) {
293 name.to_string()
294 } else {
295 return Err(LoaderError::Generic(format!(
296 "Specified program '{name}' not found in eBPF object"
297 )));
298 }
299 } else {
300 let program_names = ["uprobe", "main"];
302 let mut found_program_name: Option<String> = None;
303
304 for name in &program_names {
305 info!("Checking if program exists: {}", name);
306 if available_programs.contains(&name.to_string()) {
307 info!("Found program: {}", name);
308 found_program_name = Some(name.to_string());
309 break;
310 }
311 }
312
313 if found_program_name.is_none() {
315 if let Some(first_name) = available_programs.first() {
316 info!(
317 "No standard program names found, using first available: {}",
318 first_name
319 );
320 found_program_name = Some(first_name.clone());
321 }
322 }
323
324 found_program_name
325 .ok_or_else(|| LoaderError::Generic("No suitable program found".to_string()))?
326 };
327
328 info!("Attempting to load program: {}", program_name);
329
330 let program_ref = self
331 .bpf
332 .program_mut(&program_name)
333 .ok_or_else(|| LoaderError::Generic(format!("Program '{program_name}' not found")))?;
334
335 info!("Found program, attempting to convert to UProbe");
336 info!("Program type: {:?}", program_ref.prog_type());
337
338 match program_ref {
340 aya::programs::Program::UProbe(_) => {
341 info!("Program is correctly recognized as UProbe");
342 }
343 aya::programs::Program::KProbe(_) => {
344 error!("Program is incorrectly recognized as KProbe, should be UProbe");
345 }
346 ref _other => {
347 error!("Program is unexpected type (not UProbe or KProbe)");
348 }
349 }
350
351 let program: &mut UProbe = program_ref.try_into().map_err(|e| {
352 LoaderError::Generic(format!("Program '{program_name}' is not a UProbe: {e:?}"))
353 })?;
354
355 info!("About to load eBPF program");
357 match program.load() {
358 Ok(()) => {
359 info!("Program loaded successfully");
360 }
361 Err(e) => {
362 error!("eBPF program load failed: {}", e);
363 error!("This typically indicates eBPF verifier rejection");
364
365 if let ProgramError::SyscallError(syscall_error) = &e {
367 error!(
368 "Syscall '{}' failed: {}",
369 syscall_error.call, syscall_error.io_error
370 );
371
372 if let Some(errno) = syscall_error.io_error.raw_os_error() {
374 match errno {
375 22 => error!(
376 "EINVAL (22): Invalid argument - likely eBPF verifier rejection"
377 ),
378 7 => error!("E2BIG (7): Program too large"),
379 13 => error!("EACCES (13): Permission denied"),
380 95 => error!("EOPNOTSUPP (95): Operation not supported"),
381 _ => error!("Unknown errno: {}", errno),
382 }
383 }
384 }
385
386 error!("Program name: {}", program_name);
388 error!("Program type: {:?}", program_ref.prog_type());
389
390 return Err(LoaderError::Program(e));
391 }
392 }
393
394 let attach_location = match offset {
397 Some(offset) => UProbeAttachLocation::AbsoluteOffset(offset),
398 None => UProbeAttachLocation::Function(function_name),
399 };
400 let attach_result = attach_location.attach(program, target_binary, pid);
401
402 match attach_result {
403 Ok(link) => {
404 if let Some(offset) = offset {
405 info!(
406 "Uprobe attached successfully to {} at offset 0x{:x}",
407 target_binary, offset
408 );
409 } else {
410 info!(
411 "Uprobe attached successfully to {}:{}",
412 target_binary, function_name
413 );
414 }
415
416 self.uprobe_link = Some(link);
418 self.attachment_params = Some(UprobeAttachmentParams {
419 target_binary: target_binary.to_string(),
420 function_name: function_name.to_string(),
421 offset,
422 pid,
423 program_name,
424 });
425 }
426 Err(e) => {
427 if let Some(offset) = offset {
428 error!(
429 "Failed to attach uprobe to {} at offset 0x{:x}: {}",
430 target_binary, offset, e
431 );
432 error!("Detailed error: {:#?}", e);
433 } else {
434 error!(
435 "Failed to attach uprobe to {}:{}: {}",
436 target_binary, function_name, e
437 );
438 error!("Detailed error: {:#?}", e);
439 }
440
441 if let ProgramError::SyscallError(syscall_error) = &e {
443 error!(
444 "Syscall '{}' failed: {}",
445 syscall_error.call, syscall_error.io_error
446 );
447 if let Some(13) = syscall_error.io_error.raw_os_error() {
448 error!("Permission denied - make sure to run with sudo");
449 }
450 }
451
452 return Err(LoaderError::Program(e));
453 }
454 }
455
456 let event_map = if let Some(map) = self.bpf.take_map("ringbuf") {
459 info!("Initializing RingBuf event map");
460 let ringbuf: RingBuf<_> = map
461 .try_into()
462 .map_err(|e| LoaderError::Generic(format!("Failed to convert ringbuf map: {e}")))?;
463 EventMap::RingBuf(ringbuf)
464 } else if let Some(map) = self.bpf.take_map("events") {
465 info!("Initializing PerfEventArray event map");
466 let mut perf_array: PerfEventArray<_> = map.try_into().map_err(|e| {
467 LoaderError::Generic(format!("Failed to convert perf event array map: {e}"))
468 })?;
469
470 let online_cpus = aya::util::online_cpus().map_err(|(_, e)| {
472 LoaderError::Generic(format!("Failed to get online CPUs: {e}"))
473 })?;
474
475 info!(
476 "Opening PerfEventArray buffers for {} online CPUs",
477 online_cpus.len()
478 );
479
480 let mut cpu_buffers = Vec::new();
482
483 for cpu_id in online_cpus {
484 let pages = self.perf_page_count;
485 match perf_array.open(cpu_id, pages) {
486 Ok(buffer) => {
487 if let Some(p) = pages {
488 info!(
489 "Opened PerfEventArray buffer for CPU {} with {} pages",
490 cpu_id, p
491 );
492 } else {
493 info!(
494 "Opened PerfEventArray buffer for CPU {} (default pages)",
495 cpu_id
496 );
497 }
498 let fd = buffer.as_raw_fd();
499 let readiness =
500 AsyncFd::with_interest(PerfBufferFd(fd), Interest::READABLE).map_err(
501 |err| {
502 LoaderError::Generic(format!(
503 "Failed to register perf buffer fd for CPU {cpu_id}: {err}"
504 ))
505 },
506 )?;
507 cpu_buffers.push(PerfEventCpuBuffer {
508 cpu_id,
509 buffer,
510 readiness,
511 });
512 }
513 Err(e) => {
514 warn!("Failed to open perf buffer for CPU {}: {}", cpu_id, e);
515 }
516 }
517 }
518
519 if cpu_buffers.is_empty() {
520 return Err(LoaderError::Generic(
521 "Failed to open any perf event buffers".to_string(),
522 ));
523 }
524
525 EventMap::PerfEventArray {
526 _map: perf_array,
527 cpu_buffers,
528 }
529 } else {
530 return Err(LoaderError::MapNotFound(
531 "Neither 'ringbuf' nor 'events' map found".to_string(),
532 ));
533 };
534
535 let event_source = match &event_map {
537 EventMap::RingBuf(_) => {
538 info!("Using RingBuf mode for parser");
539 ghostscope_protocol::EventSource::RingBuf
540 }
541 EventMap::PerfEventArray { .. } => {
542 info!("Using PerfEventArray mode for parser");
543 ghostscope_protocol::EventSource::PerfEventArray
544 }
545 };
546 self.parser = StreamingTraceParser::with_event_source(event_source);
547
548 self.event_map = Some(event_map);
549 info!("Event map initialized");
550
551 Ok(())
552 }
553
554 pub fn detach_uprobe(&mut self) -> Result<()> {
557 if let Some(link_id) = self.uprobe_link.take() {
558 if let Some(params) = &self.attachment_params {
559 info!("Detaching uprobe...");
560
561 let program_ref = self.bpf.program_mut(¶ms.program_name).ok_or_else(|| {
563 let program_name = ¶ms.program_name;
564 LoaderError::Generic(format!("Program '{program_name}' not found"))
565 })?;
566
567 let program: &mut UProbe = program_ref.try_into().map_err(|e| {
568 let program_name = ¶ms.program_name;
569 LoaderError::Generic(format!("Program '{program_name}' is not a UProbe: {e:?}"))
570 })?;
571
572 program.detach(link_id).map_err(LoaderError::Program)?;
574
575 info!("Uprobe detached successfully");
576 Ok(())
577 } else {
578 error!("No attachment parameters stored");
579 Err(LoaderError::Generic(
580 "No attachment parameters stored".to_string(),
581 ))
582 }
583 } else {
584 warn!("No uprobe attached, nothing to detach");
585 Ok(())
586 }
587 }
588
589 pub fn reattach_uprobe(&mut self) -> Result<()> {
592 if self.uprobe_link.is_some() {
593 info!("Uprobe already attached");
594 return Ok(());
595 }
596
597 let params = self
598 .attachment_params
599 .as_ref()
600 .ok_or_else(|| {
601 LoaderError::Generic(
602 "No attachment parameters stored. Call attach_uprobe first.".to_string(),
603 )
604 })?
605 .clone();
606
607 info!("Reattaching uprobe with stored parameters...");
608
609 let program_ref = self.bpf.program_mut(¶ms.program_name).ok_or_else(|| {
611 LoaderError::Generic(format!("Program '{}' not found", params.program_name))
612 })?;
613
614 let program: &mut UProbe = program_ref.try_into().map_err(|e| {
615 LoaderError::Generic(format!(
616 "Program '{}' is not a UProbe: {:?}",
617 params.program_name, e
618 ))
619 })?;
620
621 let attach_location = match params.offset {
623 Some(offset) => UProbeAttachLocation::AbsoluteOffset(offset),
624 None => UProbeAttachLocation::Function(params.function_name.as_str()),
625 };
626 let attach_result = attach_location.attach(program, ¶ms.target_binary, params.pid);
627
628 match attach_result {
629 Ok(link) => {
630 if let Some(offset) = params.offset {
631 info!(
632 "Uprobe reattached successfully to {} at offset 0x{:x}",
633 params.target_binary, offset
634 );
635 } else {
636 info!(
637 "Uprobe reattached successfully to {}:{}",
638 params.target_binary, params.function_name
639 );
640 }
641
642 self.uprobe_link = Some(link);
644 Ok(())
645 }
646 Err(e) => {
647 error!("Failed to reattach uprobe: {:?}", e);
648 Err(LoaderError::Program(e))
649 }
650 }
651 }
652
653 pub fn is_uprobe_attached(&self) -> bool {
655 self.uprobe_link.is_some()
656 }
657
658 pub fn destroy(&mut self) -> Result<()> {
662 info!("Destroying GhostScopeLoader and all associated resources");
663
664 if self.uprobe_link.is_some() {
666 if let Err(e) = self.detach_uprobe() {
667 warn!("Failed to detach uprobe during destroy: {}", e);
668 }
670 }
671
672 self.attachment_params = None;
674
675 self.event_map = None;
678
679 info!("GhostScopeLoader destroyed successfully");
683 Ok(())
684 }
685
686 pub fn get_attachment_info(&self) -> Option<String> {
688 if let Some(params) = &self.attachment_params {
689 if let Some(offset) = params.offset {
690 Some(format!(
691 "{}:{} (offset: 0x{:x}, pid: {:?}) - {}",
692 params.target_binary,
693 params.function_name,
694 offset,
695 params.pid,
696 if self.is_uprobe_attached() {
697 "attached"
698 } else {
699 "detached"
700 }
701 ))
702 } else {
703 Some(format!(
704 "{}:{} (pid: {:?}) - {}",
705 params.target_binary,
706 params.function_name,
707 params.pid,
708 if self.is_uprobe_attached() {
709 "attached"
710 } else {
711 "detached"
712 }
713 ))
714 }
715 } else {
716 None
717 }
718 }
719
720 pub async fn wait_for_events_async(&mut self) -> Result<Vec<ParsedTraceEvent>> {
726 let trace_context = self.trace_context.as_ref().ok_or_else(|| {
727 LoaderError::Generic(
728 "No trace context available - cannot parse trace events".to_string(),
729 )
730 })?;
731
732 let event_map = self.event_map.as_mut().ok_or_else(|| {
733 LoaderError::Generic("Event map not initialized. Call attach_uprobe first.".to_string())
734 })?;
735
736 let mut events = Vec::new();
737
738 match event_map {
739 EventMap::RingBuf(ringbuf) => {
740 let async_fd = AsyncFd::new(ringbuf.as_raw_fd())
742 .map_err(|e| LoaderError::Generic(format!("Failed to create AsyncFd: {e}")))?;
743 let mut guard = async_fd
744 .readable()
745 .await
746 .map_err(|e| LoaderError::Generic(format!("AsyncFd error: {e}")))?;
747 guard.clear_ready();
748
749 while let Some(item) = ringbuf.next() {
751 match self.parser.process_segment(&item, trace_context) {
752 Ok(Some(parsed_event)) => events.push(parsed_event),
753 Ok(None) => {}
754 Err(e) => {
755 return Err(LoaderError::Generic(format!(
756 "Fatal: Failed to parse trace event from RingBuf (async): {e}"
757 )));
758 }
759 }
760 }
761 }
762 EventMap::PerfEventArray { cpu_buffers, .. } => {
763 use bytes::BytesMut;
764
765 let parser = &mut self.parser;
766
767 let mut drain_buffer = |entry: &mut PerfEventCpuBuffer| -> Result<bool> {
768 let mut produced = false;
769 let mut read_bufs = vec![BytesMut::with_capacity(4096)];
770
771 match entry.buffer.read_events(&mut read_bufs) {
772 Ok(result) => {
773 if result.read > 0 {
774 produced = true;
775 info!(
776 "Read {} events from CPU {} buffer",
777 result.read, entry.cpu_id
778 );
779 }
780 if result.lost > 0 {
781 warn!(
782 "Lost {} events from CPU {} buffer",
783 result.lost, entry.cpu_id
784 );
785 }
786
787 for (i, data) in read_bufs.iter().enumerate().take(result.read) {
788 debug!(
789 "PerfEvent {}: {} bytes - {:02x?}",
790 i,
791 data.len(),
792 &data[..data.len().min(32)]
793 );
794
795 match parser.process_segment(data, trace_context) {
796 Ok(Some(parsed_event)) => events.push(parsed_event),
797 Ok(None) => {}
798 Err(e) => {
799 let cpu = entry.cpu_id;
800 return Err(LoaderError::Generic(format!(
801 "Fatal: Failed to parse trace event from PerfEventArray CPU {cpu}: {e}"
802 )));
803 }
804 }
805 }
806 }
807 Err(e) => {
808 warn!("Failed to read from CPU {} buffer: {}", entry.cpu_id, e);
809 }
810 }
811
812 Ok(produced)
813 };
814
815 loop {
816 let mut made_progress = false;
818 for entry in cpu_buffers.iter_mut() {
819 if entry.buffer.readable() {
820 made_progress |= drain_buffer(entry)?;
821 }
822 }
823
824 if made_progress {
825 break;
826 }
827
828 let ready_idx = poll_fn(|cx| {
830 for (idx, entry) in cpu_buffers.iter().enumerate() {
831 match entry.readiness.poll_read_ready(cx) {
832 Poll::Ready(Ok(mut guard)) => {
833 guard.clear_ready();
834 return Poll::Ready(Ok(idx));
835 }
836 Poll::Ready(Err(e)) => return Poll::Ready(Err(e)),
837 Poll::Pending => {}
838 }
839 }
840 Poll::Pending
841 })
842 .await
843 .map_err(|e| {
844 LoaderError::Generic(format!(
845 "AsyncFd error while waiting for perf events: {e}"
846 ))
847 })?;
848
849 made_progress |= drain_buffer(
851 cpu_buffers
852 .get_mut(ready_idx)
853 .expect("ready index should be valid"),
854 )?;
855
856 for (idx, entry) in cpu_buffers.iter_mut().enumerate() {
858 if idx == ready_idx || !entry.buffer.readable() {
859 continue;
860 }
861 made_progress |= drain_buffer(entry)?;
862 }
863
864 if made_progress {
865 break;
866 }
867 }
870 }
871 }
872
873 Ok(events)
874 }
875
876 pub fn set_trace_context(&mut self, trace_context: TraceContext) {
878 info!("Setting trace context for trace event parsing");
879 self.trace_context = Some(trace_context);
880 }
881
882 pub fn get_map_info(&self) -> Vec<String> {
888 self.bpf
889 .maps()
890 .map(|(name, _map)| format!("Map: {name}"))
891 .collect()
892 }
893
894 pub fn get_program_info(&self) -> Vec<String> {
896 self.bpf
897 .programs()
898 .map(|(name, _prog)| format!("Program: {name}"))
899 .collect()
900 }
901}