2018年7月

本周工作中,我花了整整一周的时间来尝试调试一个段错误。我以前从来没有这样做过,我花了很长时间才弄清楚其中涉及的一些基本事情(获得核心转储、找到导致段错误的行号)。于是便有了这篇博客来解释如何做那些事情!

在看完这篇博客后,你应该知道如何从“哦,我的程序出现段错误,但我不知道正在发生什么”到“我知道它出现段错误时的堆栈、行号了! ”。

什么是段错误?

段错误 segmentation fault ”是指你的程序尝试访问不允许访问的内存地址的情况。这可能是由于:

  • 试图解引用空指针(你不被允许访问内存地址 0);
  • 试图解引用其他一些不在你内存(LCTT 译注:指不在合法的内存地址区间内)中的指针;
  • 一个已被破坏并且指向错误的地方的 C++ 虚表指针 C++ vtable pointer ,这导致程序尝试执行没有执行权限的内存中的指令;
  • 其他一些我不明白的事情,比如我认为访问未对齐的内存地址也可能会导致段错误(LCTT 译注:在要求自然边界对齐的体系结构,如 MIPS、ARM 中更容易因非对齐访问产生段错误)。

这个“C++ 虚表指针”是我的程序发生段错误的情况。我可能会在未来的博客中解释这个,因为我最初并不知道任何关于 C++ 的知识,并且这种虚表查找导致程序段错误的情况也是我所不了解的。

但是!这篇博客后不是关于 C++ 问题的。让我们谈论的基本的东西,比如,我们如何得到一个核心转储?

步骤1:运行 valgrind

我发现找出为什么我的程序出现段错误的最简单的方式是使用 valgrind:我运行

valgrind -v your-program

这给了我一个故障时的堆栈调用序列。 简洁!

但我想也希望做一个更深入调查,并找出些 valgrind 没告诉我的信息! 所以我想获得一个核心转储并探索它。

如何获得一个核心转储

核心转储 core dump 是您的程序内存的一个副本,并且当您试图调试您的有问题的程序哪里出错的时候它非常有用。

当您的程序出现段错误,Linux 的内核有时会把一个核心转储写到磁盘。 当我最初试图获得一个核心转储时,我很长一段时间非常沮丧,因为 - Linux 没有生成核心转储!我的核心转储在哪里?

这就是我最终做的事情:

  1. 在启动我的程序之前运行 ulimit -c unlimited
  2. 运行 sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t

ulimit:设置核心转储的最大尺寸

ulimit -c 设置核心转储的最大尺寸。 它往往设置为 0,这意味着内核根本不会写核心转储。 它以千字节为单位。 ulimit 是按每个进程分别设置的 —— 你可以通过运行 cat /proc/PID/limit 看到一个进程的各种资源限制。

例如这些是我的系统上一个随便一个 Firefox 进程的资源限制:

$ cat /proc/6309/limits 
Limit                     Soft Limit           Hard Limit           Units     
Max cpu time              unlimited            unlimited            seconds   
Max file size             unlimited            unlimited            bytes     
Max data size             unlimited            unlimited            bytes     
Max stack size            8388608              unlimited            bytes     
Max core file size        0                    unlimited            bytes     
Max resident set          unlimited            unlimited            bytes     
Max processes             30571                30571                processes 
Max open files            1024                 1048576              files     
Max locked memory         65536                65536                bytes     
Max address space         unlimited            unlimited            bytes     
Max file locks            unlimited            unlimited            locks     
Max pending signals       30571                30571                signals   
Max msgqueue size         819200               819200               bytes     
Max nice priority         0                    0                    
Max realtime priority     0                    0                    
Max realtime timeout      unlimited            unlimited            us   

内核在决定写入多大的核心转储文件时使用 软限制 soft limit (在这种情况下,max core file size = 0)。 您可以使用 shell 内置命令 ulimitulimit -c unlimited) 将软限制增加到 硬限制 hard limit

kernel.core\_pattern:核心转储保存在哪里

kernel.core_pattern 是一个内核参数,或者叫 “sysctl 设置”,它控制 Linux 内核将核心转储文件写到磁盘的哪里。

内核参数是一种设定您的系统全局设置的方法。您可以通过运行 sysctl -a 得到一个包含每个内核参数的列表,或使用 sysctl kernel.core_pattern 来专门查看 kernel.core_pattern 设置。

所以 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t 将核心转储保存到目录 /tmp 下,并以 core 加上一系列能够标识(出故障的)进程的参数构成的后缀为文件名。

如果你想知道这些形如 %e%p 的参数都表示什么,请参考 man core

有一点很重要,kernel.core_pattern 是一个全局设置 —— 修改它的时候最好小心一点,因为有可能其它系统功能依赖于把它被设置为一个特定的方式(才能正常工作)。

kernel.core\_pattern 和 Ubuntu

默认情况下在 ubuntu 系统中,kernel.core_pattern 被设置为下面的值:

$ sysctl kernel.core_pattern
kernel.core_pattern = |/usr/share/apport/apport %p %s %c %d %P

这引起了我的迷惑(这 apport 是干什么的,它对我的核心转储做了什么?)。以下关于这个我了解到的:

  • Ubuntu 使用一种叫做 apport 的系统来报告 apt 包有关的崩溃信息。
  • 设定 kernel.core_pattern=|/usr/share/apport/apport %p %s %c %d %P 意味着核心转储将被通过管道送给 apport 程序。
  • apport 的日志保存在文件 /var/log/apport.log 中。
  • apport 默认会忽略来自不属于 Ubuntu 软件包一部分的二进制文件的崩溃信息

我最终只是跳过了 apport,并把 kernel.core_pattern 重新设置为 sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t,因为我在一台开发机上,我不在乎 apport 是否工作,我也不想尝试让 apport 把我的核心转储留在磁盘上。

现在你有了核心转储,接下来干什么?

好的,现在我们了解了 ulimitkernel.core_pattern ,并且实际上在磁盘的 /tmp 目录中有了一个核心转储文件。太好了!接下来干什么?我们仍然不知道该程序为什么会出现段错误!

下一步将使用 gdb 打开核心转储文件并获取堆栈调用序列。

从 gdb 中得到堆栈调用序列

你可以像这样用 gdb 打开一个核心转储文件:

$ gdb -c my_core_file

接下来,我们想知道程序崩溃时的堆栈是什么样的。在 gdb 提示符下运行 bt 会给你一个 调用序列 backtrace 。在我的例子里,gdb 没有为二进制文件加载符号信息,所以这些函数名就像 “??????”。幸运的是,(我们通过)加载符号修复了它。

下面是如何加载调试符号。

symbol-file /path/to/my/binary
sharedlibrary

这从二进制文件及其引用的任何共享库中加载符号。一旦我这样做了,当我执行 bt 时,gdb 给了我一个带有行号的漂亮的堆栈跟踪!

如果你想它能工作,二进制文件应该以带有调试符号信息的方式被编译。在试图找出程序崩溃的原因时,堆栈跟踪中的行号非常有帮助。:)

查看每个线程的堆栈

通过以下方式在 gdb 中获取每个线程的调用栈!

thread apply all bt full

gdb + 核心转储 = 惊喜

如果你有一个带调试符号的核心转储以及 gdb,那太棒了!您可以上下查看调用堆栈(LCTT 译注:指跳进调用序列不同的函数中以便于查看局部变量),打印变量,并查看内存来得知发生了什么。这是最好的。

如果您仍然正在基于 gdb 向导来工作上,只打印出栈跟踪与bt也可以。 :)

ASAN

另一种搞清楚您的段错误的方法是使用 AddressSanitizer 选项编译程序(“ASAN”,即 $CC -fsanitize=address)然后运行它。 本文中我不准备讨论那个,因为本文已经相当长了,并且在我的例子中打开 ASAN 后段错误消失了,可能是因为 ASAN 使用了一个不同的内存分配器(系统内存分配器,而不是 tcmalloc)。

在未来如果我能让 ASAN 工作,我可能会多写点有关它的东西。(LCTT 译注:这里指使用 ASAN 也能复现段错误)

从一个核心转储得到一个堆栈跟踪真的很亲切!

这个博客听起来很多,当我做这些的时候很困惑,但说真的,从一个段错误的程序中获得一个堆栈调用序列不需要那么多步骤:

  1. 试试用 valgrind

如果那没用,或者你想要拿到一个核心转储来调查:

  1. 确保二进制文件编译时带有调试符号信息;
  2. 正确的设置 ulimitkernel.core_pattern
  3. 运行程序;
  4. 一旦你用 gdb 调试核心转储了,加载符号并运行 bt
  5. 尝试找出发生了什么!

我可以使用 gdb 弄清楚有个 C++ 的虚表条目指向一些被破坏的内存,这有点帮助,并且使我感觉好像更懂了 C++ 一点。也许有一天我们会更多地讨论如何使用 gdb 来查找问题!


via: https://jvns.ca/blog/2018/04/28/debugging-a-segfault-on-linux/

作者:Julia Evans 译者:stephenxs 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

Open edX 为各种规模和类型的组织提供了一个强大而多功能的开源课程管理的解决方案。要不要了解一下。

Open edX 平台 是一个自由开源的课程管理系统,它是 全世界 都在使用的大规模网络公开课(MOOC)以及小型课程和培训模块的托管平台。在 Open edX 的 第七个主要发行版 中,到现在为止,它已经提供了超过 8,000 个原创课程和 5000 万个课程注册数。你可以使用你自己的本地设备或者任何行业领先的云基础设施服务提供商来安装这个平台,而且,随着项目的服务提供商名单越来越长,来自它们中的软件即服务(SaaS)的可用模型也越来越多了。

Open edX 平台被来自世界各地的顶尖教育机构、私人公司、公共机构、非政府组织、非营利机构,以及教育技术初创企业广泛地使用,并且该项目的服务提供商全球社区不断地让甚至更小的组织也可以访问这个平台。如果你打算向广大的读者设计和提供教育内容,你应该考虑去使用 Open edX 平台。

安装

安装这个软件有多种方式,这可能有点让你难以选择,至少刚开始是这样。但是不管你是以何种方式 安装 Open edX,最终你都得到的是有相同功能的应用程序。默认安装包含一个为在线学习者提供的、全功能的学习管理系统(LMS),和一个全功能的课程管理工作室(CMS),CMS 可以让你的讲师团队用它来编写原创课程内容。你可以把 CMS 当做是课程内容设计和管理的 “Wordpress”,把 LMS 当做是课程销售、分发、和消费的 “Magento”。

Open edX 是设备无关的、完全响应式的应用软件,并且不用花费很多的努力就可发布一个原生的 iOS 和 Android 应用,它可以无缝地集成到你的实例后端。Open edX 平台的代码库、原生移动应用、以及安装脚本都发布在 GitHub 上。

有何期望

Open edX 平台的 GitHub 仓库 包含适用于各种类型的组织的、性能很好的、产品级的代码。来自数百个机构的数千名程序员经常为 edX 仓库做贡献,并且这个平台是一个名副其实的、研究如何去构建和管理一个复杂的企业级应用的好案例。因此,尽管你可能会遇到大量的类似“如何将平台迁移到生产环境中”的问题,但是你无需对 Open edX 平台代码库本身的质量和健状性担忧。

通过少量的培训,你的讲师就可以去设计不错的在线课程。但是请记住,Open edX 是通过它的 XBlock 组件架构进行扩展的,因此,通过他们和你的努力,你的讲师将有可能将不错的课程变成精品课程。

这个平台在单服务器环境下也运行的很好,并且它是高度模块化的,几乎可以进行无限地水平扩展。它也是主题化的和本地化的,平台的功能和外观可以根据你的需要进行几乎无限制地调整。平台在你的设备上可以按需安装并可靠地运行。

需要一些封装

请记住,有大量的 edX 软件模块是不包含在默认安装中的,并且这些模块提供的经常都是各种组织所需要的功能。比如,分析模块、电商模块,以及课程的通知/公告模块都是不包含在默认安装中的,并且这些单独的模块都是值得安装的。另外,在数据备份/恢复和系统管理方面要完全依赖你自己去处理。幸运的是,有关这方面的内容,社区有越来越多的文档和如何去做的文章。你可以通过 Google 和 Bing 去搜索,以帮助你在生产环境中安装它们。

虽然有很多文档良好的程序,但是根据你的技能水平,配置 oAuthSSL/TLS,以及使用平台的 REST API 可能对你是一个挑战。另外,一些组织要求将 MySQL 和/或 MongoDB 数据库在中心化环境中管理,如果你正好是这种情况,你还需要将这些服务从默认平台安装中分离出来。edX 设计团队已经尽可能地为你做了简化,但是由于它是一个非常重大的更改,因此可能需要一些时间去实现。

如果你面临资源和/或技术上的困难 —— 不要气馁,Open edX 社区 SaaS 提供商,像 appsemblereduNEXT,提供了引人入胜的替代方案去进行 DIY 安装,尤其是如果你只想简单购买就行。

技术栈

在 Open edX 平台的安装上探索是件令人兴奋的事情,从架构的角度来说,这个项目是一个典范。应用程序模块是 Django 应用,它利用了大量的开源社区的顶尖项目,包括 UbuntuMySQLMongoDBRabbitMQElasticsearchHadoop、等等。

 title=

Open edX 技术栈(CC BY,来自 edX)

将这些组件安装并配置好本身就是一件非常不容易的事情,但是以这样的一种方式将所有的组件去打包,并适合于任意规模和复杂性的组织,并且能够按他们的需要进行任意调整搭配而无需在代码上做重大改动,看起来似乎是不可能的事情 —— 它就是这种情况,直到你看到主要的平台配置参数安排和命名是多少的巧妙和直观。请注意,平台的组织结构有一个学习曲线,但是,你所学习的一切都是值的去学习的,不仅是对这个项目,对一般意义上的大型 IT 项目都是如此。

提醒一点:这个平台的 UI 是在不断变动的,最终的目标是在 ReactBootstrap 上实现标准化。与此同时,你将会发现基本主题有多个实现的样式,这可能会让你感到困惑。

采用

edX 项目能够迅速得到世界范围内的采纳,很大程度上取决于该软件的运行情况。这一点也不奇怪,这个项目成功地吸引了大量才华卓越的人参与其中,他们作为程序员、项目顾问、翻译者、技术作者、以及博客作者参与了项目的贡献。一年一次的 Open edX 会议官方的 edX Google Group、以及 Open edX 服务提供商名单 是了解这个多样化的、不断成长的生态系统的非常好的起点。我作为相对而言的新人,我发现参与和直接从事这个项目的各个方面是非常容易的。

祝你学习之旅一切顺利,并且当你构思你的项目时,你可以随时联系我。


via: https://opensource.com/article/18/6/getting-started-open-edx

作者:Lawrence Mc Daniel 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

自 2005 年搬到加拿大以来,我使用 Ledger CLI 来跟踪我的财务状况。我喜欢纯文本的方式,它支持虚拟信封意味着我可以同时将我的银行帐户余额和我的虚拟分配到不同的目录下。以下是我们如何使用这些虚拟信封分别管理我们的财务状况。

每个月,我都有一个条目将我生活开支分配到不同的目录中,包括家庭开支的分配。W- 不要求太多, 所以我要谨慎地处理这两者之间的差别和我自己的生活费用。我们处理它的方式是我支付固定金额,这是贷记我支付的杂货。由于我们的杂货总额通常低于我预算的家庭开支,因此任何差异都会留在标签上。我过去常常给他写支票,但最近我只是支付偶尔额外的大笔费用。

这是个示例信封分配:

2014.10.01 * Budget
 [Envelopes:Living]
 [Envelopes:Household] $500
 ;; More lines go here

这是设置的信封规则之一。它鼓励我正确地分类支出。所有支出都从我的 “Play” 信封中取出。

= /^Expenses/
 (Envelopes:Play) -1.0

这个为家庭支出报销 “Play” 信封,将金额从 “Household” 信封转移到 “Play” 信封。

= /^Expenses:House$/
 (Envelopes:Play) 1.0
 (Envelopes:Household) -1.0

我有一套定期的支出来模拟我的预算中的家庭开支。例如,这是 10 月份的。

2014.10.1 * House
 Expenses:House
 Assets:Household $-500

这是杂货交易的形式:

2014.09.28 * No Frills
 Assets:Household:Groceries $70.45
 Liabilities:MBNA:September $-70.45

接着 ledger bal Assets:Household 就会告诉我是否欠他钱(负余额)。如果我支付大笔费用(例如:机票、通管道),那么正常家庭开支预算会逐渐减少余额。

我从 W- 那找到了一个为我的信用卡交易添加一个月标签的技巧,他还使用 Ledger 跟踪他的交易。它允许我再次检查条目的余额,看看前一个条目是否已被正确清除。

这个资产分类使用有点奇怪,但它在精神上对我有用。

使用 Ledger 以这种方式跟踪它可以让我跟踪我们的杂货费用以及我实际支付费用和我预算费用之间的差额。如果我最终支出超出预期,我可以从更多可自由支配的信封中移动虚拟货币,因此我的预算始终保持平衡。

Ledger 是一个强大的工具。相当极客,但也许更多的工作流描述可能会帮助那些正在搞清楚它的人!


via: http://sachachua.com/blog/2014/11/keeping-financial-score-ledger/

作者:Sacha Chua 选题:lujun9972 译者:geekpi 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

人们经常用 x 相对于 y 这样的术语来考虑问题,但是它并不是一个技术对另一个技术的问题。Ben Hindman 在这里解释了 Mesos 是如何对另外一种技术进行补充的。

Mesos 的起源可以追溯到 2009 年,当时,Ben Hindman 还是加州大学伯克利分校研究并行编程的博士生。他们在 128 核的芯片上做大规模的并行计算,以尝试去解决多个问题,比如怎么让软件和库在这些芯片上运行更高效。他与同学们讨论能否借鉴并行处理和多线程的思想,并将它们应用到集群管理上。

Hindman 说 “最初,我们专注于大数据” 。那时,大数据非常热门,而 Hadoop 就是其中的一个热门技术。“我们发现,人们在集群上运行像 Hadoop 这样的程序与运行多线程应用及并行应用很相似。”Hindman 说。

但是,它们的效率并不高,因此,他们开始去思考,如何通过集群管理和资源管理让它们运行的更好。“我们查看了那个时期很多的各种技术” Hindman 回忆道。

然后,Hindman 和他的同事们决定去采用一种全新的方法。“我们决定对资源管理创建一个低级的抽象,然后在此之上运行调度服务和做其它的事情。” Hindman 说,“基本上,这就是 Mesos 的本质 —— 将资源管理部分从调度部分中分离出来。”

他成功了,并且 Mesos 从那时开始强大了起来。

将项目呈献给 Apache

这个项目发起于 2009 年。在 2010 年时,团队决定将这个项目捐献给 Apache 软件基金会(ASF)。它在 Apache 孵化,并于 2013 年成为顶级项目(TLP)。

为什么 Mesos 社区选择 Apache 软件基金会有很多的原因,比如,Apache 许可证,以及基金会已经拥有了一个充满活力的其它此类项目的社区。

与影响力也有关系。许多在 Mesos 上工作的人也参与了 Apache,并且许多人也致力于像 Hadoop 这样的项目。同时,来自 Mesos 社区的许多人也致力于其它大数据项目,比如 Spark。这种交叉工作使得这三个项目 —— Hadoop、Mesos,以及 Spark —— 成为 ASF 的项目。

与商业也有关系。许多公司对 Mesos 很感兴趣,并且开发者希望它能由一个中立的机构来维护它,而不是让它成为一个私有项目。

谁在用 Mesos?

更好的问题应该是,谁不在用 Mesos?从 Apple 到 Netflix 每个都在用 Mesos。但是,Mesos 也面临任何技术在早期所面对的挑战。“最初,我要说服人们,这是一个很有趣的新技术。它叫做‘容器’,因为它不需要使用虚拟机” Hindman 说。

从那以后,这个行业发生了许多变化,现在,只要与别人聊到基础设施,必然是从”容器“开始的 —— 感谢 Docker 所做出的工作。今天再也不需要做说服工作了,而在 Mesos 出现的早期,前面提到的像 Apple、Netflix,以及 PayPal 这样的公司。他们已经知道了容器替代虚拟机给他们带来的技术优势。“这些公司在容器成为一种现象之前,已经明白了容器的价值所在”, Hindman 说。

可以在这些公司中看到,他们有大量的容器而不是虚拟机。他们所做的全部工作只是去管理和运行这些容器,并且他们欣然接受了 Mesos。在 Mesos 早期就使用它的公司有 Apple、Netflix、PayPal、Yelp、OpenTable 和 Groupon。

“大多数组织使用 Mesos 来运行各种服务” Hindman 说,“但也有些公司用它做一些非常有趣的事情,比如,数据处理、数据流、分析任务和应用程序。“

这些公司采用 Mesos 的其中一个原因是,资源管理层之间有一个明晰的界线。当公司运营容器的时候,Mesos 为他们提供了很好的灵活性。

“我们尝试使用 Mesos 去做的一件事情是去创建一个层,以让使用者享受到我们的层带来的好处,当然也可以在它之上创建任何他们想要的东西,” Hindman 说。 “我认为这对一些像 Netflix 和 Apple 这样的大公司非常有用。”

但是,并不是每个公司都是技术型的公司;不是每个公司都有或者应该有这种专长。为帮助这样的组织,Hindman 联合创建了 Mesosphere 去围绕 Mesos 提供服务和解决方案。“我们最终决定,为这样的组织去构建 DC/OS,它不需要技术专长或者不想把时间花费在像构建这样的事情上。”

Mesos vs. Kubernetes?

人们经常用 x 相对于 y 这样的术语来考虑问题,但是它并不是一个技术对另一个技术的问题。大多数的技术在一些领域总是重叠的,并且它们可以是互补的。“我不喜欢将所有的这些东西都看做是竞争者。我认为它们中的一些与另一个在工作中是互补的,” Hindman 说。

“事实上,名字 Mesos 表示它处于 ‘中间’;它是一种中间的操作系统”, Hindman 说,“我们有一个容器调度器的概念,它能够运行在像 Mesos 这样的东西之上。当 Kubernetes 刚出现的时候,我们实际上在 Mesos 的生态系统中接受了它,并将它看做是在 Mesos 上的 DC/OS 中运行容器的另一种方式。”

Mesos 也复活了一个名为 Marathon(一个用于 Mesos 和 DC/OS 的容器编排器)的项目,它成为了 Mesos 生态系统中最重要的成员。但是,Marathon 确实无法与 Kubernetes 相比较。“Kubernetes 比 Marathon 做的更多,因此,你不能将它们简单地相互交换,” Hindman 说,“与此同时,我们在 Mesos 中做了许多 Kubernetes 中没有的东西。因此,这些技术之间是互补的。”

不要将这些技术视为相互之间是敌对的关系,它们应该被看做是对行业有益的技术。它们不是技术上的重复;它们是多样化的。据 Hindman 说,“对于开源领域的终端用户来说,这可能会让他们很困惑,因为他们很难去知道哪个技术适用于哪种任务,但这是这个被称之为开源的本质所在。“

这只是意味着有更多的选择,并且每个都是赢家。


via: https://www.linux.com/blog/2018/6/mesos-and-kubernetes-its-not-competition

作者:Swapnil Bhartiya 选题:lujun9972 译者:qhwdw 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我们继续 无密码验证 的文章。上一篇文章中,我们用 Go 写了一个 HTTP 服务,用这个服务来做无密码验证 API。今天,我们为它再写一个 JavaScript 客户端。

我们将使用 这里的 这个单页面应用程序(SPA)来展示使用的技术。如果你还没有读过它,请先读它。

记住流程:

  • 用户输入其 email。
  • 用户收到一个带有魔法链接的邮件。
  • 用户点击该链接、
  • 用户验证成功。

对于根 URL(/),我们将根据验证的状态分别使用两个不同的页面:一个是带有访问表单的页面,或者是已验证通过的用户的欢迎页面。另一个页面是验证回调的重定向页面。

伺服

我们将使用相同的 Go 服务器来为客户端提供服务,因此,在我们前面的 main.go 中添加一些路由:

router.Handle("GET", "/...", http.FileServer(SPAFileSystem{http.Dir("static")}))
type SPAFileSystem struct {
    fs http.FileSystem
}

func (spa SPAFileSystem) Open(name string) (http.File, error) {
    f, err := spa.fs.Open(name)
    if err != nil {
        return spa.fs.Open("index.html")
    }
    return f, nil
}

这个伺服文件放在 static 下,配合 static/index.html 作为回调。

你可以使用你自己的服务器,但是你得在服务器上启用 CORS

HTML

我们来看一下那个 static/index.html 文件。

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <title>Passwordless Demo</title>
 <link rel="shortcut icon" href="data:,">
 <script src="/js/main.js" type="module"></script>
</head>
<body></body>
</html>

单页面应用程序的所有渲染由 JavaScript 来完成,因此,我们使用了一个空的 body 部分和一个 main.js 文件。

我们将使用 上篇文章 中的 Router。

渲染

现在,我们使用下面的内容来创建一个 static/js/main.js 文件:

import Router from 'https://unpkg.com/@nicolasparada/router'
import { isAuthenticated } from './auth.js'

const router = new Router()

router.handle('/', guard(view('home')))
router.handle('/callback', view('callback'))
router.handle(/^\//, view('not-found'))

router.install(async resultPromise => {
    document.body.innerHTML = ''
    document.body.appendChild(await resultPromise)
})

function view(name) {
    return (...args) => import(`/js/pages/${name}-page.js`)
        .then(m => m.default(...args))
}

function guard(fn1, fn2 = view('welcome')) {
    return (...args) => isAuthenticated()
        ? fn1(...args)
        : fn2(...args)
}

与上篇文章不同的是,我们实现了一个 isAuthenticated() 函数和一个 guard() 函数,使用它去渲染两种验证状态的页面。因此,当用户访问 / 时,它将根据用户是否通过了验证来展示主页或者是欢迎页面。

验证

现在,我们来编写 isAuthenticated() 函数。使用下面的内容来创建一个 static/js/auth.js 文件:

export function getAuthUser() {
    const authUserItem = localStorage.getItem('auth_user')
    const expiresAtItem = localStorage.getItem('expires_at')

    if (authUserItem !== null && expiresAtItem !== null) {
        const expiresAt = new Date(expiresAtItem)

        if (!isNaN(expiresAt.valueOf()) && expiresAt > new Date()) {
            try {
                return JSON.parse(authUserItem)
            } catch (_) { }
        }
    }

    return null
}

export function isAuthenticated() {
    return localStorage.getItem('jwt') !== null && getAuthUser() !== null
}

当有人登入时,我们将保存 JSON 格式的 web 令牌、它的过期日期,以及在 localStorage 上的当前已验证用户。这个模块就是这个用处。

  • getAuthUser() 用于从 localStorage 获取已认证的用户,以确认 JSON 格式的 Web 令牌没有过期。
  • isAuthenticated() 在前面的函数中用于去检查它是否没有返回 null

获取

在继续这个页面之前,我将写一些与服务器 API 一起使用的 HTTP 工具。

我们使用以下的内容去创建一个 static/js/http.js 文件:

import { isAuthenticated } from './auth.js'

function get(url, headers) {
    return fetch(url, {
        headers: Object.assign(getAuthHeader(), headers),
    }).then(handleResponse)
}

function post(url, body, headers) {
    return fetch(url, {
        method: 'POST',
        headers: Object.assign(getAuthHeader(), { 'content-type': 'application/json' }, headers),
        body: JSON.stringify(body),
    }).then(handleResponse)
}

function getAuthHeader() {
    return isAuthenticated()
        ? { authorization: `Bearer ${localStorage.getItem('jwt')}` }
        : {}
}

export async function handleResponse(res) {
    const body = await res.clone().json().catch(() => res.text())
    const response = {
        statusCode: res.status,
        statusText: res.statusText,
        headers: res.headers,
        body,
    }
    if (!res.ok) {
        const message = typeof body === 'object' && body !== null && 'message' in body
            ? body.message
            : typeof body === 'string' && body !== ''
                ? body
                : res.statusText
        const err = new Error(message)
        throw Object.assign(err, response)
    }
    return response
}

export default {
    get,
    post,
}

这个模块导出了 get()post() 函数。它们是 fetch API 的封装。当用户是已验证的,这二个函数注入一个 Authorization: Bearer <token_here> 头到请求中;这样服务器就能对我们进行身份验证。

欢迎页

我们现在来到欢迎页面。用如下的内容创建一个 static/js/pages/welcome-page.js 文件:

const template = document.createElement('template')
template.innerHTML = `
    <h1>Passwordless Demo</h1>
    <h2>Access</h2>
    <form id="access-form">
        <input type="email" placeholder="Email" autofocus required>
        <button type="submit">Send Magic Link</button>
    </form>
`

export default function welcomePage() {
    const page = template.content.cloneNode(true)

    page.getElementById('access-form')
        .addEventListener('submit', onAccessFormSubmit)

    return page
}

这个页面使用一个 HTMLTemplateElement 作为视图。这只是一个输入用户 email 的简单表单。

为了避免干扰,我将跳过错误处理部分,只是将它们输出到控制台上。

现在,我们来写 onAccessFormSubmit() 函数。

import http from '../http.js'

function onAccessFormSubmit(ev) {
    ev.preventDefault()

    const form = ev.currentTarget
    const input = form.querySelector('input')
    const email = input.value

    sendMagicLink(email).catch(err => {
        console.error(err)
        if (err.statusCode === 404 && wantToCreateAccount()) {
            runCreateUserProgram(email)
        }
    })
}

function sendMagicLink(email) {
    return http.post('/api/passwordless/start', {
        email,
        redirectUri: location.origin + '/callback',
    }).then(() => {
        alert('Magic link sent. Go check your email inbox.')
    })
}

function wantToCreateAccount() {
    return prompt('No user found. Do you want to create an account?')
}

它对 /api/passwordless/start 发起了 POST 请求,请求体中包含 emailredirectUri。在本例中它返回 404 Not Found 状态码时,我们将创建一个用户。

function runCreateUserProgram(email) {
    const username = prompt("Enter username")
    if (username === null) return

    http.post('/api/users', { email, username })
        .then(res => res.body)
        .then(user => sendMagicLink(user.email))
        .catch(console.error)
}

这个用户创建程序,首先询问用户名,然后使用 email 和用户名做一个 POST 请求到 /api/users。成功之后,给创建的用户发送一个魔法链接。

回调页

这是访问表单的全部功能,现在我们来做回调页面。使用如下的内容来创建一个 static/js/pages/callback-page.js 文件:

import http from '../http.js'

const template = document.createElement('template')
template.innerHTML = `
    <h1>Authenticating you</h1>
`

export default function callbackPage() {
    const page = template.content.cloneNode(true)

    const hash = location.hash.substr(1)
    const fragment = new URLSearchParams(hash)
    for (const [k, v] of fragment.entries()) {
        fragment.set(decodeURIComponent(k), decodeURIComponent(v))
    }
    const jwt = fragment.get('jwt')
    const expiresAt = fragment.get('expires_at')

    http.get('/api/auth_user', { authorization: `Bearer ${jwt}` })
        .then(res => res.body)
        .then(authUser => {
            localStorage.setItem('jwt', jwt)
            localStorage.setItem('auth_user', JSON.stringify(authUser))
            localStorage.setItem('expires_at', expiresAt)

            location.replace('/')
        })
        .catch(console.error)

    return page
}

请记住……当点击魔法链接时,我们会来到 /api/passwordless/verify_redirect,它将把我们重定向到重定向 URI,我们将放在哈希中的 JWT 和过期日期传递给 /callback

回调页面解码 URL 中的哈希,提取这些参数去做一个 GET 请求到 /api/auth_user,用 JWT 保存所有数据到 localStorage 中。最后,重定向到主页面。

主页

创建如下内容的 static/pages/home-page.js 文件:

import { getAuthUser } from '../auth.js'

export default function homePage() {
    const authUser = getAuthUser()

    const template = document.createElement('template')
    template.innerHTML = `
        <h1>Passwordless Demo</h1>
        <p>Welcome back, ${authUser.username} 

在 Linux 命令行当中使用不同颜色以期提供一种根据文件类型来识别文件的简单方式。你可以修改这些颜色,但是在做之前应该对你做的事情有充分的理由。

如果你在 Linux 命令行上花费了大量的时间(如果没有,那么你可能不会读这篇文章),你无疑注意到了 ls 以多种不同的颜色显示文件。你可能也注意到了一些区别 —— 目录是一种颜色,可执行文件是另一种颜色等等。

这一切是如何发生的呢?以及,你可以选择哪些选项来改变颜色分配可能就不是很多人都知道的。

一种方法是运行 dircolors 命令得到一大堆展示了如何指定这些颜色的数据。它会显示以下这些东西:

$ dircolors
LS_COLORS='rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do
=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg
=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01
;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01
;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=0
1;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31
:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.
xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.t
bz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.j
ar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.a
lz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.r
z=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.
mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:
*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:
*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;3
5:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;
35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01
;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01
;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01
;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;3
5:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;3
5:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;3
6:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;
36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;
36:*.spx=00;36:*.xspf=00;36:';
export LS_COLORS

如果你擅长解析文件,那么你可能会注意到这个列表有一种 模式 patten 。用冒号分隔开,你会看到这样的东西:

$ dircolors | tr ":" "\n" | head -10
LS_COLORS='rs=0
di=01;34
ln=01;36
mh=00
pi=40;33
so=01;35
do=01;35
bd=40;33;01
cd=40;33;01
or=40;31;01

OK,这里有一个模式 —— 一系列定义,有一到三个数字组件。我们来看看其中的一个定义。

pi=40;33

有些人可能会问的第一个问题是“pi 是什么?”在这里,我们研究的是颜色和文件类型,所以这显然不是以 3.14 开头的那个有趣的数字。当然不是,这个 “pi” 代表 “pipe(管道)” —— Linux 系统上的一种特殊类型的文件,它可以将数据从一个程序传递给另一个程序。所以,让我们建立一个管道。

$ mknod /tmp/mypipe p
$ ls -l /tmp/mypipe
prw-rw-r-- 1 shs shs 0 May 1 14:00 /tmp/mypipe

当我们在终端窗口中查看我们的管道和其他几个文件时,颜色差异非常明显。

font colors

pi 的定义中(如上所示),“40” 使文件在终端(或 PuTTY)窗口中使用黑色背景显示,31 使字体颜色变红。管道是特殊的文件,这种特殊的处理使它们在目录列表中突出显示。

bdcd 定义是相同的 —— 40;33;01,它有一个额外的设置。这个设置会导致 块设备 block device (bd)和 字符设备 character device (cd)以黑色背景,橙色字体和另一种效果显示 —— 字符将以粗体显示。

以下列表显示由 文件类型 file type 所指定的颜色和字体分配:

setting         file type
=======         =========
rs=0            reset to no color
di=01;34        directory
ln=01;36        link
mh=00           multi-hard link
pi=40;33        pipe
so=01;35        socket
do=01;35        door
bd=40;33;01     block device
cd=40;33;01     character device
or=40;31;01     orphan
mi=00           missing?
su=37;41        setuid
sg=30;43        setgid
ca=30;41        file with capability
tw=30;42        directory with sticky bit and world writable
ow=34;42        directory that is world writable
st=37;44        directory with sticky bit
ex=01;93        executable

你可能已经注意到,在 dircolors 命令输出中,我们的大多数定义都以星号开头(例如,*.wav=00;36)。这些按 文件扩展名 file extension 而不是文件类型定义显示属性。这有一个示例:

$ dircolors | tr ":" "\n" | tail -10
*.mpc=00;36
*.ogg=00;36
*.ra=00;36
*.wav=00;36
*.oga=00;36
*.opus=00;36
*.spx=00;36
*.xspf=00;36
';
export LS_COLORS

这些设置(上面列表中所有的 00;36)将使这些文件名以青色显示。可用的颜色如下所示。

all colors

如何改变设置

你要使用 ls 的别名来打开颜色显示功能。这通常是 Linux 系统上的默认设置,看起来是这样的:

alias ls='ls --color=auto'

如果要关闭字体颜色,可以运行 unalias ls 命令,然后文件列表将仅以默认字体颜色显示。

你可以通过修改 $LS_COLORS 设置和导出修改后的设置来更改文本颜色。

$ export LS_COLORS='rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;...

注意:上面的命令由于太长被截断了。

如果希望文本颜色的修改是永久性的,则需要将修改后的 $LS_COLORS 定义添加到一个启动文件中,例如 .bashrc

更多关于命令行文本

你可以在 NetworkWorld 的 2016 年 11 月的帖子中找到有关文本颜色的其他信息。


via: https://www.networkworld.com/article/3269587/linux/customizing-your-text-colors-on-the-linux-command-line.html

作者:Sandra Henry-Stocker 选题:lujun9972 译者:MjSeven 校对:pityonline

本文由 LCTT 原创编译,Linux中国 荣誉推出