哪里出错了:Rust中的错误处理和错误消息

2021-02-19 17:04:27

这太烦人了。你已经写了一个铁锈箱,现在你想第一次测试它,它不起作用!

来吧,生锈!你怎么敢?你答应过,一旦获得了编译器。只是。作品!现在这个!

好的好的。你让自己平静下来。让我们从头开始。你想创造所谓的minidumps。这是一个文件,包含有关崩溃程序的信息(如堆栈的所有线程,CPU寄存器,系统信息等)。MiniDump由各种部分组成,例如MiniDump标题(包括一天时间,版本,基本上是一个目录),一个线程部分(包括过程的所有线程及其堆栈),内存映射和库等。[只是为了给出一些上下文,因为所有这些都实际上并不重要。]

为此,您创建了一个箱子。一个部分之后写入另一个部分,而从系统中检索有关目标进程的信息。你甚至创造了一个很好的简单API。您可以在进程ID和一个开放文件中携手,其中应该写入miniDump。像这样:

您还可以在用户中包含在转储中的用户指定的内存区域,如:

让app_memory = appmemory {ptr:some_address,length:memory_size,}; MiniDumpWriter :: New(PID,PID).set_app_memory(Vec![app_memory]).dump(& mut tmpfile).expect("倾倒失败");

但是当您在应用程序中运行漂亮的库代码时,您将获得'倾倒失败:"在Ptrace中失败:: read:sys(Eio)"'

好的,也许您可​​以稍微增强一下库错误处理能力。通过增强,您的意思是“首先实现一个”。

并使用结果< T>在所有函数中作为返回值,并使用?将它们全部移交给父函数。这样,原始错误就像飞镖一样穿过….jelly刺穿了您的调用堆栈(是的,您擅长单词并且您知道它。)。

通常,您只是从使用的库中冒出错误,但是对于必须定义自己的罕见错误,您目前只是在做

好吧,现在您至少知道存在错误。而且这与您使用ptrace有关。但是您不知道发生在哪里。您可以在各个地方使用该功能。是在初始化阶段吗?在其中一节中?如果是这样,哪一个呢?你想读什么?从哪里来的?或简而言之:这是怎么回事?

好吧,Rust已经存在了很长一段时间了,他们总是吹嘘错误处理如何成为一流的公民。所以错误处理应该已经完成​​了,对吧?以规范的方式处理错误,已正式记录下来,所有正确的地方,对吗?

事实证明,这是一个非常活跃的……嗯……实验领域。最近进行了一项调查,列出并快速描述了出现,失宠,分叉,无论如何死亡,被取代,再次失宠等各种错误处理库和错误处理的大多数方式。如果您应该使用错误链或失败,错误或失败,此错误或无论如何,或错误,则应经常更改

然后,您找到了这颗宝石,不知道您应该笑还是哭。在Rust达到1.0版本将近六年之后,一个错误处理项目组就形成了。六。年。 (沉重的呼吸)

哦,那好吧。至少他们现在正在解决它。问题是,您需要……。六年?您是认真的吗?……哎呀,抱歉……问题是,您现在需要有用的错误消息。

在阅读了有关该主题的一些不错的博客之后(至少是这样),似乎已经形成了共识,至少对于库而言:返回源自std :: error :: Error的内容。使用宏魔术,可以手动实现它们,也可以使用为您完成任务的板条箱。像这个错误。使用哪种方法取决于您的懒惰程度以及您对编译时间的耐心。

不幸的是,所有文章都倾向于使用非常简单的示例代码来进行说明,这是可以理解的,但很烦人。您甚至可能会说,不切实际的简单。他们具有深度为1的调用栈,其API中总共仅返回三种错误,并且它们的错误是显而易见的,并且易于描述(例如,“在“单词计数”程序中找不到输入文件XY”)。

您有一个更复杂的调用栈,在不同的地方有成千上万种不同的错误和代码重用。例如,您认为造成上述错误的函数是copy_from_process(),该函数调用ptrace :: read(),该函数可能在ptrace :: read:Sys(EIO)中返回类似Failed的错误信息。代码中的多个位置,例如:

├─init()│├─read_auxv()││├─open(format!(" / proc / {} / auxv&#34 ;, self.pid))││└─some_parsing()│├─ ...│├─enumerate_mappings()││├─open(format!(" / proc / {} / maps&#34 ;, self.pid))││└─some_parsing()│││└─ some_more_checks()│└─copy_from_process()│└─dump()│├─节:: header :: write()│├─节:: thread_list_stream :: write()│└─copy_from_process()│├─ :mappings :: write()│└─elf_identifier_for_mapping()│└─copy_from_process()│├─部分:: app_memory :: write()│└─copy_from_process()│└─...

打开文件的情况也是如此,它发生在多个地方(init()中显示了两个示例),因此在没有上下文的情况下获取FileNotFound同样会很有趣,依此类推。

包裹错误听起来仍然是一个不错的主意,但是仅一层不会对其进行包裹。以copy_from_process()为例,您看到了几种可能性:

将ptrace错误包装到副本中,但这为您提供任何东西(可能是某些上下文除外,如果添加一些)

使用initerrors和dumpingerrors包装ptrace错误,您仍然不知道哪个部分失败,为什么,但知道它是否在init()期间。

您可能会将上下文添加到选项2(请参阅以下方法),但每个部分都有各种原因可能会失败。部分有些独一无二的部分,有些人在几个中分享,其中一些。

使用这架子和神话般的#[来自]宏,您可以快速定义一个过多的错误和包装,从您的Callstack中的最深,最黑暗的地方开始,包裹起来:

#[派生(调试,错误)] Pub枚举ptracedumpererror {#[错误(" nix :: ptrace()错误")ptraceError(#[来自] nix ::错误),...}# [派生(调试,错误)] pub enum sectionAppMemoryError {#[错误("从进程中复制内存和#34;来自process"#[来自] ptracedumpererror),...}#[派生(调试,错误)] Pub枚举Dumperror {#[错误(" init阶段&#34期间的错误)] initerror(#[from] initerror),#[错误(透明)] ptracedumpererror(#[from] ptracedumpererror),# [错误("写作部分appMemory&#34失败)] SectionAppMemoryError(#[来自] SectionAppMemoryError),...

有趣的部分是:您必须触摸现有的代码很少,这归功于从一个错误到另一个错误的自动转换,方便地由#[来自]提供:

- PUB FN init(& mut self) - >结果<()> {+ PUB FN init(& mut self) - >结果<(),initerror> {self.read_auxv()?; self.enumerate_threads()?; self.enumerate_mappings()?好的(()) }

-pub fn get_stack_info(& self,int_stack_pointer:usize)->结果<(使用,使用)> {+ pub fn get_stack_info(& self,int_stack_pointer:usize)->结果<(使用,使用),DumperError> {//剪接let映射= self .find_mapping(stack_pointer)-.ok_or("未找到堆栈指针的映射")? + .ok_or(DumperError :: NoStackPointerMapping)?;让偏移量= stack_pointer-mapping.start_address;让distance_to_end = mapping.size-偏移量; //剪断

是… (从桌子上扔出一堆纸)……简短。实际上太短了,没有什么帮助。好吧,您知道哪个部分失败了。那挺好的。但是,您在错误中指定的所有错误消息都在哪里?

啊哈!现在您到了某个地方!微小,微小,痛苦的微小步骤,但是您到了某个地方!没有错误文本,但至少有一个链!

正常的打印似乎不会递归地解决所有错误,但会停在最顶端。为此,您需要自己亲自解决所有错误,或者使用可为您完成此任务的板条箱。有很多提供此功能的工具,但是无论如何都会做到这一点(与thiserror由同一作者撰写,因此互操作性不应该成为问题)。

写入部分AppMemory时失败:无法从进程复制内存:nix :: ptrace()错误:EIO:I / O错误

有了那个甜美,甜美的错误链以及由此产生的错误消息,您现在知道了哪里出错了。不过,不是真的为什么。你缺乏背景。幸运的是,您不会脱离上下文(请注意:您需要一个鼓组,以声学方式突出双关语)。

您需要添加大量上下文,添加起来非常容易。而不是使用#[from],而是使用#[source],它将不再实现自动转换功能,但保持错误链完整:

#[派生(调试,错误)] Pub枚举PtracedumpererRorRor {#[错误("从过程{0}失败的副本(源:0x {1:x},offset:{2},长度:{3}) ")] PtraceError(PID,USIZIZE,USIZIZE,USIZIZE,#[源] NIX :: ERROR),

这可以在错误链的所有层上完成。对于这个例子,我们只在最后一个链接中执行它.NO,你现在做的地方,你现在必须映射你的错误(yse可以更加符合人体工程学,但你想要保持你的依赖列表小):

让Word = ptrace :: read(pid,(src + idx)为* mut c_void).map_err(| e | ptraceError(src,Idx,num_of_bytes,e)?

写入部分AppMemory时失败:无法从进程复制内存:从过程12111复制失败(源:0x0,偏移:0,长度:4096):EIO:I / O错误

嗯,您的错误消息,包括许多上下文,现在是人类的信息。它非常适合记录(例如,放入伴随您的MiniDump的JSON文件)。但是,通过应用程序开发人员使用以编程方式使用吗?

你认为这可能是。您想要的方案,如“无法找到文件xy,因为尚未挂载文件系统”等。这些可以通过使用错误链中的最后一个链接在应用程序代码中轻松匹配,这无论如何都提供root_cause()函数.Oor您只能匹配顶部最错误并根据自己的迷你型部分重做,如果库代码失败了。