<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Tech on The Great Garage</title><link>https://blog.hgao.net/categories/tech/</link><description>Recent content in Tech on The Great Garage</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Tue, 18 Apr 2023 +0000</lastBuildDate><atom:link href="/categories/tech/" rel="self" type="application/rss+xml"/><item><title>NAS 视频串流解决方案</title><link>https://blog.hgao.net/post/nas-streaming-solution/</link><pubDate>Tue, 18 Apr 2023 +0000</pubDate><guid>https://blog.hgao.net/post/nas-streaming-solution/</guid><description>&lt;p&gt;起笔于二〇二二年十一月，发表于二〇二三年四月。&lt;/p&gt;
&lt;p&gt;某个周五的晚上，我和鬼鬼结束了一周的工作，坐在电视机前，满怀期待地打开 NAS 上保存的某热门电影宇宙的最新一部作品。然而，视频画面却十分不流畅，几秒中卡顿一次，完全无法正常观看。&lt;/p&gt;
&lt;p&gt;就在几个小时前，我还得意地告诉鬼鬼，我搞到了这部作品的 4K HDR 高清资源，盛情邀请她一同观赏，来度过这个愉快的夜晚。如今这个局面，可谓十分尴尬——这不是翻车了嘛！&lt;/p&gt;
&lt;p&gt;我的 NAS 是群晖入门级的 DS220j，购于两年多以前。不仅盘位只有两个，CPU 和内存也是捉襟见肘，只能跑跑自带的下载、文件传输、视频监控等功能，不支持 Docker。NAS 里存放着一些电影和剧集，通过海外版小米盒子（运行安卓系统）上的 VLC 播放。大部分资源都能流畅播放，也支持字幕。虽然没有任何的媒体管理功能，界面也十分粗糙，但考虑到播放效果过得去，我一直对这套挂壁方案还算满意，直到这一次。&lt;/p&gt;
&lt;p&gt;众所周知，一个垃圾佬在捣鼓自己的东西时可以尽情挂壁，但绝不能牺牲领导的体验。所以，虽然得到了鬼鬼的暖心安慰，我仍下定决心，好好升级一下家里的视频串流方案。当然，在满足要求的前提下，尽可能少花钱。&lt;/p&gt;
&lt;p&gt;首先要做的，是确定播放卡顿的原因。从 NAS 串流视频需要经过以下几个步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;NAS 读取本地硬盘的视频文件&lt;/li&gt;
&lt;li&gt;通过局域网串流至播放设备&lt;/li&gt;
&lt;li&gt;播放设备解码&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;前两步理论上不会有问题。NAS 的机械硬盘虽慢，CPU 也拉跨，然而不至于连串流所需的读取速度都满足不了。串流使用的是 SMB 协议，在我家局域网的文件传输速度大约是 30MB/s，而一个时长两小时的 40GB 视频文件所需带宽仅为 5.5MB/s，比这低得多。&lt;/p&gt;
&lt;p&gt;看来问题大概率出在播放设备，也就小米盒子。这个好测试，换一个播放设备嘛。我偶尔也会在 iPhone 手机上播放 NAS 上的视频，所以上面也装了 VLC。用它播放 40GB 的文件，画面流畅，但没有声音，而且发热严重。我又换了一个类似码率的高清视频播放，结果依然流畅，声音也正常，但是仍旧发热。这说明了两个问题：一，之前的卡顿的确是播放端的问题，与 NAS 和网络无关。这无疑是个好消息，因为升级这两样的成本可比升级播放端高多了。二，VLC 或许兼容性不佳，卡顿和发热应该都是硬解不可用，只能软解造成的。iPhone 的芯片足够强大，即使软解也能流畅播放，但是这种高强度任务难免会增加功率，产生热量。至于小米盒子，连流畅播放也不能指望了。&lt;/p&gt;
&lt;p&gt;为了验证 VLC 不行，我决定换一个播放器 app 试试，于是选择了大名鼎鼎的 Infuse。Infuse 是苹果独占的播放器，支持 iPhone，iPad，Mac 和 Apple TV，尤其在 Apple TV 上出镜率很高。&lt;/p&gt;
&lt;p&gt;我用 Infuse 打开了这个 40GB 的视频。嗯，怎么说呢，真是如沐春风啊。画面、声音全部正常，播放流畅无卡顿，并且自动开启 HDR、加载字幕。界面美观易用，如同 iOS 的第一方 app，和 VLC 那明显从 PC 移植过来的界面相比，简直是天壤之别。它甚至还有海报和字幕搜刮功能。原来这才是专业的影视播放器啊！那一刻，我就知道我再也回不去 VLC 了。就这样，串流播放在手机上完美实现了。&lt;/p&gt;
&lt;p&gt;现在回头看看小米盒子的问题。小米盒子离路由器很近，WiFi 信号不会比 iPhone差。硬件方面，根据说明书，它支持硬件解 H.265。看来换一个播放器 app 就有希望解决问题。安卓上对标 Infuse 的播放器应该是 Kodi，功能同样强大，但界面是满满的工程师和极客味，能感受到两者社区的巨大差异。Kodi 可以流畅播放高清视频，声音也正常。&lt;/p&gt;
&lt;p&gt;然而，我随即意识到小米盒子本身有些问题。它不能开启 HDR，而且分辨率被锁在 1080p，无法设置为 2160p。这些功能在说明书中是支持的。尝试了更换 HDMI 线，无果。似乎小米盒子是不堪大用了。那么电视自带的 Roku 系统呢？Roku 的软件生态不如安卓，只能用它自带的播放器。虽然也可以流畅播放高清视频，也支持 HDR，但不支持字幕，界面功能也很简陋。&lt;/p&gt;
&lt;p&gt;事已至此，看来只能花点钱升级一下播放设备了（&lt;del&gt;太好了，有理由买新玩具了&lt;/del&gt;）。&lt;/p&gt;
&lt;p&gt;市面上主流的产品有 Apple TV，Google Chromecast with Google TV （名字真长啊） 和 NVIDIA Shield。Shield 对家庭影院和外接音响的支持最好，但是我短期内并没有组建家庭影院的计划。而且有了小米盒子的教训，第三方设备不在首选之列，更何况它既贵又丑。Apple TV 和 Chromecast 都是我很喜欢的产品。安卓系统还有一个优势，那就是我们在用的 K 歌 app 只有安卓版本。如果选择了 Apple TV，意味着需要为了 K 歌保留安卓系统的小米盒子。然而这些都不重要，我毫不犹豫地选择了 Apple TV。这在我第一次使用 Infuse 的时候，就已经注定了。&lt;/p&gt;
&lt;p&gt;此时 Apple TV 2022 发售在即，2021 版获得了不错的折扣，果断入手。2022 有一些不错的升级，不过对于我的使用场景，2021 已经完全够用。&lt;/p&gt;
&lt;p&gt;两天后，亚马逊准时送达。设置好新机，第一件事就是安装 Infuse。果然没有令我失望，Apple TV 的 Infuse 采用和 iOS 同样的交互逻辑和设计语言，配合遥控器的触摸功能，使用体验十分愉悦，在大小屏幕上达成了令人满足的统一。不出意外，Apple TV 通过 Infuse 也能流畅播放 NAS 所有的高清视频资源，各方面效果都无可挑剔。&lt;/p&gt;
&lt;p&gt;至此，最初的目标圆满达成。但是在见识了 Infuse 搜刮海报的能力后，我想更进一步，把所有的资源整合进媒体库中。Infuse 可以把一个 NAS 目录下的视频文件自动整理成媒体库，但是前提是通过 SMB 协议连接，不支持速度更快的 UPnP。速度的区别在播放时感觉不到，但如果大范围拖拽跳转，就能感知到 SMB 的延迟更加明显。有没有一种协议，可以像 SMB 那样支持媒体库，又有 UPnP 的速度呢？还真有，那就是 Plex 协议。&lt;a href="https://www.plex.tv/"&gt;Plex&lt;/a&gt; 是一个多媒体管理平台，由 server 和客户端两部分组成，两部分都支持所有主流平台。其中 server 可以安装到群晖上（是的，孱弱如 DS220j 也是可以的），这样客户端就可以通过 Plex 协议访问NAS 上的资源。客户端可以是 Plex 自己的播放器，也可以是 Infuse 这种支持 Plex 协议的第三方播放器。Plex server 本身就支持海报搜刮和影片整理等功能（媒体库嘛），播放器只需要从 server 加载 metadata 即可。这样的好处是，只需要在 server 上配置一遍，各个客户端的行为便能统一。&lt;/p&gt;
&lt;p&gt;这里多说一句，Plex 播放器本身十分优秀，试用了一下，觉得播放效果不逊于 Infuse。但是如果想解锁 Plex 播放器的全部功能，需要订阅或购买 Plex 的会员。会员还包含其他强大的功能，比如从互联网访问 Plex 中的资源、根据网络情况将资源在服务器转码成指定的分辨率后再串流等等。由于这些功能暂时用不到，我选择了免费版 Plex + Infuse 订阅的方式，价格比 Plex 订阅要便宜。&lt;/p&gt;
&lt;p&gt;最后，为了进一步提升传输速度，也为了不浪费 Apple TV 的以太网接口，我买了一个入门千兆交换机，将 Apple TV 通过以太网接入。测试下来，一段时长两小时、大小为 40GB 的视频，可以在一秒钟之内从任意进度加载。&lt;/p&gt;
&lt;p&gt;串流方案的升级到此完成。与之前相比，新的方案可以流畅播放、跳转高码率的高清视频，UI 易用性获得了极大提升，支持酷炫的海报墙、影片剧集归类、内容简介、演员表、字幕搜刮。电视盒子的流畅度、遥控器手感也大幅提升，还获得了原生 AirPlay 功能。花费的成本如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Apple TV 2021：$100&lt;/li&gt;
&lt;li&gt;Netgear 五口千兆交换机：$20&lt;/li&gt;
&lt;li&gt;Infuse Pro 订阅：$10/年&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;总体来说，这次改造相当成功，我自己也很满意。不仅因为最后的结果理想，也因为研究的过程很有趣，每一点 progress 都能带来成就感。利用自己的知识、双手和学习能力，让生活变得更好一点，可能这就是所谓的「热爱生活」吧。&lt;/p&gt;</description></item><item><title>2023 年，再试一试使用 Notion 随时随地写博客</title><link>https://blog.hgao.net/post/notion-blog-anywhere-2023/</link><pubDate>Sat, 25 Mar 2023 +0000</pubDate><guid>https://blog.hgao.net/post/notion-blog-anywhere-2023/</guid><description>&lt;p&gt;&lt;img src="https://live.staticflickr.com/65535/52767786944_e6ab7435ae_o.jpg" alt="IMAGE"&gt;&lt;/p&gt;
&lt;p&gt;两年前写过一篇&lt;a href="%7B%7B%3C%20ref%20%22notion-blog-anywhere%22%20%3E%7D%7D"&gt;博文&lt;/a&gt;，讨论如何将 Notion 上的文章转换成 Markdown，并部署到自己的博客上。当时使用的是非官方的 Notion API，需要经常更新 token 才能获取数据，不稳定也不够自动化。发完那篇文章不久，我的 token 就挂了，自此再也没管它，也再没有发过文章。后来，&lt;a href="https://developers.notion.com/"&gt;Notion 发布了官方的 API&lt;/a&gt;，最近终于抽空玩了玩。登上了一年半未曾登录的 VPS，写一写这个两年没有更新的博客，顺便捡起阔别六年的 JavaScript。&lt;/p&gt;
&lt;p&gt;Notion API 本身是可以通过 HTTP 协议直接调用的，没有语言的限制。与此同时，Notion 也发布了一个&lt;a href="https://github.com/makenotion/notion-sdk-js"&gt;基于 Node.js 的 SDK&lt;/a&gt;，相当于给 API 套了一层壳，可以简化一些调用。我当时脑子没转过来这个弯，误以为一定要用 Node 才能调用 API，于是选择了 JavaScript 作为开发语言，而不是我更熟悉的 Python。对于这个决定我不后悔，因为学一学 JavaScript 不是坏事，无非多踩点坑（JS 坑之多可谓臭名昭著），开发速度慢一些。但另一方面，这也说明了不写需求文档的坏处🙃。&lt;/p&gt;
&lt;p&gt;通过 API 拿到数据后，下一步是把文章转换成 Markdown。其中比较复杂的是嵌套列表的转换，需要递归调用一些函数。我觉得这比我在工作中写的业务逻辑都复杂，可以当作一个不错的算法面试题。&lt;/p&gt;
&lt;p&gt;其余部分没有什么难度，主要时间都花在 debug 上。这个过程中，ChatGPT 多次解答了我的疑问，从 Node 里的常见函数调用，到复杂一些的 Promise 概念，再到帮我逐步分析解决了一个我毫无头绪的 bug - 这是 ChatGPT 横空出世半年来，我第一次高密度地与它互动，切身感受到了它的便捷。&lt;/p&gt;
&lt;p&gt;现在，这段两百行的小程序基本已经开发完成，正作为一个 cron job 定期运行，自动发布 Notion 上新增的文章。它的代码在 &lt;a href="https://github.com/hikoship/notion2hugo"&gt;GitHub&lt;/a&gt; 上。对了，这也是我时隔多年后再次使用 GitHub。&lt;/p&gt;
&lt;p&gt;从上周五到现在，我每天晚上都花几个小时，投入在这上面。我的业余时间变得久违地快乐、激动和充实。鬼鬼有点困惑不解：为什么我这个一年写不了两篇文章的博客，会值得投入这么多时间精力，手动发布不也挺简单的吗？&lt;/p&gt;
&lt;p&gt;我也说不准。或许首先，这个网站寄托着我的一部分精神和历史，像是我的一个「魂器」。我很珍惜它。个人网站是开放互联网最后的几颗遗珠。在这里，我尚有一些掌控权和自由度。任何人都能无门槛访问它，但又只能主动来访。它不需要注册登录，但也永远不会自动出现在时间线推送里。这种特质让我觉得安全又不孤独。因为珍惜，我愿意付出十分的努力，多打磨出一分的改进。&lt;/p&gt;
&lt;p&gt;另一方面，我单纯喜欢写代码，喜欢用代码作为工具发明创造，尤其是在写代码时间不多的今天。&lt;/p&gt;
&lt;p&gt;最后，我也是真的希望，可以随时随地写博客。&lt;/p&gt;</description></item><item><title>使用 Notion 随时随地写博客</title><link>https://blog.hgao.net/post/notion-blog-anywhere/</link><pubDate>Wed, 27 Jan 2021 +0000</pubDate><guid>https://blog.hgao.net/post/notion-blog-anywhere/</guid><description>&lt;p&gt;&lt;img src="https://live.staticflickr.com/65535/52767786944_e6ab7435ae_o.jpg" alt="IMAGE"&gt;&lt;/p&gt;
&lt;h2 id="notion-简介"&gt;Notion 简介&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://notion.so/"&gt;Notion&lt;/a&gt; 是一个跨平台笔记工具，类似印象笔记。它支持在笔记中使用 Markdown 语法，并且可以通过非官方的 API 将笔记批量导出。这篇文章将介绍如何通过脚本将 Notion 中的文章导出成 Markdown，并部署到自己已有的静态博客上。&lt;/p&gt;
&lt;p&gt;这篇文章就是通过这个方法完成并发布的。&lt;/p&gt;
&lt;h2 id="自建博客的痛点"&gt;自建博客的痛点&lt;/h2&gt;
&lt;p&gt;自己搭建博客的好处无须赘述。我从 2015 年开始用 Hugo 搭建自己的博客，也写了不少文章。但是近两年来发文显著减少，究其原因，是工作以后主要使用工作电脑，不常用个人电脑，而后者是我在 Hugo 上发表文章的唯一途径。每次想写点什么，只能记在手机或者工作电脑上。就算写完了一篇文章，还需要在个人电脑上手动格式化成 Markdown，再部署到 Hugo 上，很是繁琐。如果能有一个 Markdown 编辑器可以跨平台同步，写完后还无需额外操作就能发布到博客上，无疑会大大降低写作的门槛。接触到 Notion 以后，我发现这是可行的。&lt;/p&gt;
&lt;p&gt;在写这篇文章之前，我搜索了其他使用 Notion 驱动博客的文章。大部分文章是直接把 Notion 笔记分享出去，小部分是用 Netlify 和 Gatsby来解决。似乎没有文章提到如果把 Notion 整合到自己已有的博客中，所以希望这篇文章能弥补这一块的空白。&lt;/p&gt;
&lt;h2 id="需要用到的东西"&gt;需要用到的东西&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一个 Notion 账户（免费账户即可）&lt;/li&gt;
&lt;li&gt;一个使用 Markdown 的静态博客（如 Hugo，Hexo，Jekyll）&lt;/li&gt;
&lt;li&gt;一个能运行 Python 脚本，并且能访问 Notion 的服务器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="在-notion-中写作"&gt;在 Notion 中写作&lt;/h2&gt;
&lt;p&gt;首先，需要在 Notion 中新建一个数据库形式的页面（笔记本），用于放置所有要发表文章。给此数据库中的文章添加属性，比如发布日期、类别、标签、摘要等。凡是你在博客中使用到的属性都可以添加进来。另外，添加一个叫作「已发表」的 checkbox 用于区分草稿和已发表的文章。这样，当一篇文章可以发表时，只需要勾选 checkbox 即可，其余工作全部交给脚本自动化完成。&lt;/p&gt;
&lt;p&gt;关于撰写文章，Notion 支持 Markdown 语法，并且所见即所得。比如在新一行中输入 &lt;code&gt;# H1&lt;/code&gt;，会自动转换成大号加粗的 &lt;code&gt;H1&lt;/code&gt;。副标题、列表、粗体、斜体、代码块也是同理。如果在手机端不方便输入 Markdown 中的各种符号，也可以用 Notion 自己的 UI 来进行设置标题、加粗等操作，效果是一样的。&lt;/p&gt;
&lt;h2 id="文章提取及转化"&gt;文章提取及转化&lt;/h2&gt;
&lt;p&gt;这一段介绍如何通过代码，把写好的文章变成博客需要的 Markdown 文件。这里用到了 &lt;a href="https://github.com/jamalex/notion-py"&gt;notion-py&lt;/a&gt; 这个&lt;strong&gt;非官方&lt;/strong&gt;的 Python API，它支持对一个用户的 Notion 笔记进行增删查改，但我们只需要其中的「查」，所以步骤相对简单。具体的使用教程请各位参考 API 官方文档，不赘述了。&lt;/p&gt;
&lt;p&gt;文章存储在 Notion 自定义的类型中，由一个个 block 组成。通过一些简单的遍历和分支语句，就能够将大部分的 block 转换成 markdown 文本。博客的元数据保存在文章属性中，可以提取出来，用于生成 markdown 的 header。&lt;/p&gt;
&lt;h2 id="输出和部署"&gt;输出和部署&lt;/h2&gt;
&lt;p&gt;将每篇文章的 markdown 文本输出到静态博客生成器的文章目录下。通过 &lt;code&gt;diff&lt;/code&gt; 命令比较生成的文章，可以避免重复部署。如果博客在 Git 仓库中，也可以用 Git 比较文章的前后差异。有差异的话，运行相应的部署命令即可。&lt;/p&gt;
&lt;p&gt;我将我的脚本设置成一个 Linux 服务，每小时运行一次，这样就能够自动将 Notion 里的新文章部署到博客上了，还可以使用 Linux 的 &lt;code&gt;journalctl&lt;/code&gt; 命令可以查看服务日志。&lt;/p&gt;
&lt;h2 id="缺点和风险"&gt;缺点和风险&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;据说 Notion 无法从中国大陆访问，所以对脚本所在服务器有要求。&lt;/li&gt;
&lt;li&gt;无法预览文章在博客上的显示效果。&lt;/li&gt;
&lt;li&gt;脚本需要主动查询文章改动，所以时效性和节省带宽只能二者择一。目前还没有研究 API 如何监听 Notion 改动。&lt;/li&gt;
&lt;li&gt;在 Notion 中直接插入的图片可以在博客显示，但会压缩，并且 URL 只有一天内有效。每次生成 markdown 都会更新图片的 URL，导致 &lt;code&gt;diff&lt;/code&gt; 命令失去意义。&lt;/li&gt;
&lt;li&gt;API 不受官方支持，况且不知道 Notion 自己能存活多久。&lt;/li&gt;
&lt;li&gt;不支持 Vim 模式。&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>照片处理工作流</title><link>https://blog.hgao.net/post/photo-postprocessing-workflow/</link><pubDate>Sat, 22 Feb 2020 +0000</pubDate><guid>https://blog.hgao.net/post/photo-postprocessing-workflow/</guid><description>&lt;p&gt;从二〇一七年接触摄影开始，自己的摄影知识一直停留在快门、光圈、ISO 而已，照片也只用 JPEG 存储。去年九月，阿炜帮我把相机升级成了索尼 A7III，于是下决心精进一下拍照技术。这期间切身体会到了 RAW 格式之于 JPEG 的优势（尤其在索尼 JPEG 直出并不出众的情况下），并形成了使用 RAW 拍照的习惯。由此也发展出自己的一套以 RAW 为基础的照片工作流。&lt;/p&gt;
&lt;h3 id="导入"&gt;导入&lt;/h3&gt;
&lt;p&gt;我用 Lightroom 作为处理工具，所以直接把照片通过电脑读卡器导入到 Lightroom 中。在此之前不进行照片筛选，因为相机自己的显示屏素质平庸，MacBook 的 Finder 预览 RAW 的速度又不尽如人意。&lt;/p&gt;
&lt;h3 id="筛选"&gt;筛选&lt;/h3&gt;
&lt;p&gt;在 Lightroom 里结合使用旗帜标记（flag）和星级来实现筛选的功能。在第一遍筛选中，将明显的废片标记为拒绝（rejected，快捷键 &lt;code&gt;x&lt;/code&gt;）。其他照片则按照如下标准评定星级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一星：还凑合的照片。没有拍糊，但也没有太多反复观看的价值。&lt;/li&gt;
&lt;li&gt;二星：后期难度大，但有潜力的照片。这个类别比较难定义。举个例子，在拍摄大光比的风光照片时，经常会拍好多张，难以在短时间内比较其优劣，于是就挑一张还不错的重点后期处理，并标为五星，其他的归到二星。实际操作中，很少有照片是二星。&lt;/li&gt;
&lt;li&gt;三星：优秀的照片。或是具有一定的美感，或是有所叙事，能勾起我拍照时的回忆，可能会在以后时不时拿出来观看回味。&lt;/li&gt;
&lt;li&gt;四星：家人朋友的露脸照片。因为涉及到隐私，所以单独处理。&lt;/li&gt;
&lt;li&gt;五星：非常好的照片，或者需要复杂后期的优秀照片。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在第一遍筛选过程中，如果在两个星级之间摇摆，则尽量向高分靠拢。后面处理时候会根据需要对照片降级。在每一百张未被拒绝的照片中，五星比例大概在百分之五，三星百分之二十，其余大部分一星。当然，这个比例会根据拍摄的题材和场景变化。&lt;/p&gt;
&lt;h3 id="处理"&gt;处理&lt;/h3&gt;
&lt;p&gt;第一步，先删除被拒绝的照片。然后根据星级，按从高到低的顺序处理，同时，如果有需要，对照片进行降级。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;五星：逐张精细处理，包括曝光，色彩，裁剪等等。这其中的技巧和习惯因人而异，就不展开说了。如果有多张重复，考虑将重复照片降级为二星；如果处理空间不大，或者觉得照片没有那么好看，考虑降级为三星。&lt;/li&gt;
&lt;li&gt;四星：原封不动&lt;/li&gt;
&lt;li&gt;三星：与五星类似，逐一处理。有重复或者没那么好看的，降级成一星&lt;/li&gt;
&lt;li&gt;二星：原封不动&lt;/li&gt;
&lt;li&gt;一星：使用 Lightroom 的自动处理功能（快捷键 &lt;code&gt;Shift&lt;/code&gt; + &lt;code&gt;A&lt;/code&gt;）快速处理。如果觉得有的照片实在没有价值，考虑删除。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在每一星级的处理之后，确保被拒绝的照片均已删除，从而减少无效劳动。&lt;/p&gt;
&lt;h3 id="导出和存储"&gt;导出和存储&lt;/h3&gt;
&lt;p&gt;根据星级不同，导出 RAW 和 JPEG 两种格式，存储在移动硬盘、Flickr、Google Photos 三个位置。JPEG 我会按百分之九十的品质导出，追求极限的话可以用百分之百，但我没仔细比较过两者差别，我也没有强迫症，反正乍一看看不出什么不同。心里不膈应就行了。&lt;/p&gt;
&lt;p&gt;首先，在电脑的本地硬盘建立三个目录：&lt;code&gt;google_photos&lt;/code&gt;，&lt;code&gt;flickr&lt;/code&gt; 和 &lt;code&gt;raw&lt;/code&gt;。接下来分别进行如下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;五星：导出 RAW 格式到 &lt;code&gt;raw&lt;/code&gt; 目录，同时导出 JPEG 格式到 &lt;code&gt;flickr&lt;/code&gt; 目录&lt;/li&gt;
&lt;li&gt;四星、二星：导出 RAW 格式到 &lt;code&gt;raw&lt;/code&gt; 目录&lt;/li&gt;
&lt;li&gt;三星：导出 JPEG 格式到 &lt;code&gt;flickr&lt;/code&gt; 目录&lt;/li&gt;
&lt;li&gt;一星：导出 JPEG 格式到 &lt;code&gt;google_photos&lt;/code&gt; 目录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;导出完毕后，将 &lt;code&gt;flickr&lt;/code&gt; 目录中的所有照片复制到 &lt;code&gt;google_photos&lt;/code&gt; 目录下，使 &lt;code&gt;flickr&lt;/code&gt; 成为 &lt;code&gt;google_photos&lt;/code&gt; 的一个子集。然后，在移动硬盘（或 NAS，或其他永久存储资料的地方）建立一个目录，用于存放所有本次导出的照片。将 &lt;code&gt;google_photos&lt;/code&gt; 和 &lt;code&gt;raw&lt;/code&gt; 文件夹备份进移动硬盘。同时进行在线备份，将 &lt;code&gt;google_photos&lt;/code&gt; 上传到 Google Photos。将 &lt;code&gt;flickr&lt;/code&gt; 上传到 Flickr。上传 Flickr 时可以根据情况标记相册、标签，不强求完美。&lt;/p&gt;
&lt;h3 id="体验"&gt;体验&lt;/h3&gt;
&lt;p&gt;这种策略实行了几个月，处理过五六次照片，总体比较满意，修出了一些好看的照片，理论上也能很大程度保证照片的安全。比起以前无脑直出，体会到了照片处理软件和 RAW 格式的强大魔力。统计了某一次拍摄，共两百张照片使用 Lightroom 的处理时间，其中没有高难度的风景照或是需要精修的照片，大概花费了一个小时，还是可以接受的。保持效率的重要因素就是，学会取舍。一张照片如果看着不那么顺眼，就快修然后 move on（怎么翻译？）。&lt;/p&gt;
&lt;p&gt;这个工作流也有一些欠缺的地方，比如二星和四星的处理方式其实一模一样、硬盘照片如何检索、如何保证安全等等。同样的道理，这些都是要取舍的。如果今后有更好的点子或&lt;strong&gt;非常多&lt;/strong&gt;的闲钱（用来搭 NAS、组 RAID、或者订阅更大的网盘）再说吧。努力拒绝完美主义，拒绝强迫症，除非因为它们而做不到：）。&lt;/p&gt;</description></item><item><title>An Optimization of QMK Mod-tap (Layer-tap) for Fast Typists</title><link>https://blog.hgao.net/post/qmk-mod-key/</link><pubDate>Wed, 22 May 2019 +0000</pubDate><guid>https://blog.hgao.net/post/qmk-mod-key/</guid><description>&lt;p&gt;&lt;strong&gt;The code doesn&amp;rsquo; work as expected. Please refer to &lt;code&gt;IGNORE_MOD_TAP_INTERRUPT&lt;/code&gt; and &lt;code&gt;PERMISSIVE_HOLD&lt;/code&gt; &lt;a href="https://github.com/qmk/qmk_firmware/blob/master/docs/config_options.md"&gt;here&lt;/a&gt; for possible workarounds.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="http://qmk.fm"&gt;QMK&lt;/a&gt; is the most powerful and popular keyboard firmware in custimzed mechanical keyboard community. It allows you to can write C code to set macros to any key, which makes it highly playable.&lt;/p&gt;
&lt;p&gt;Apart from programming, QMK has quite a lot of pre-set quantum keys. An important category of them is mod-tap keys. That means, when you tap the key, it prints a normal output, such as a letter, digit or any other key on the keyboard; when you hold it, it works like a mod key, such as Control, Alt, Shift or Win (Command/Super). That&amp;rsquo;s particually useful for mini keyboards (40% layout, for example), where keys are highly reused.&lt;/p&gt;
&lt;p&gt;However, the performance of mod-tap may not meet the need of fast typing. For example, if the mod-tap key is Control(A), and you press Control(A) and C fast, it actually prints &amp;ldquo;AC&amp;rdquo; on the screen, but not sends a Control + C combination. The same thing happens on layer-tap key.&lt;/p&gt;
&lt;p&gt;I haven&amp;rsquo;t read the source code of mod-tap keys, but with programming, we can avoid this issue completely.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;QMK_KEYBOARD_H&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 2&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 3&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;lctl_other_key_pressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 4&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="n"&gt;lctl_hold_timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 5&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 6&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;process_record_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="n"&gt;keycode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;keyrecord_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 7&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keycode&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 8&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Left control. Can be changed to any modifier, or MO({LAYER_NUM}) for layer-taps.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 9&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nl"&gt;KC_LCTL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;10&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pressed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;11&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Records press time.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;12&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="n"&gt;lctl_hold_timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timer_read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;13&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// At the beginning, no other key is pressed.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;14&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="n"&gt;lctl_other_key_pressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;15&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;timer_elapsed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lctl_timer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;space_other_key_pressed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;16&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Sends out &amp;#39;A&amp;#39; if the key is held for less than 0.5s and no other key was pressed during the period.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;17&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="nf"&gt;tap_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KC_A&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;18&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;19&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;20&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;21&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// Another key is pressed.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;22&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;&lt;/span&gt; &lt;span class="n"&gt;lctl_other_key_pressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;23&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;24&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;25&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this piece of code, we set the keycode of the mod-tap key to a normal Control key. It ensures it works perfectly as a mod key. Then we attach another event to this key when it is released - we sends a normal letter, which is the &amp;ldquo;tap&amp;rdquo; part of mod-tap. Tap is only triggerred when the key was held shortly and no other key is pressed before release. This way, even fast typists can enjoy mod-tap keys happily.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not sure if the current behavior of mod-tap is expected by QMK developer, but I will do some investigation on it and see if I can make the solution general to the default mod-tap implementation.&lt;/p&gt;</description></item><item><title>Google Chromecast VS Amazon Fire TV, And Echo Dot</title><link>https://blog.hgao.net/draft/chromecast-firetv-echodot/</link><pubDate>Sat, 28 Jul 2018 -0700</pubDate><guid>https://blog.hgao.net/draft/chromecast-firetv-echodot/</guid><description>&lt;p&gt;I haven&amp;rsquo;t written blog since the end of last year, which is a really long period of time with many stories happend. Nor have I read any books, if I remember correctly, and that&amp;rsquo;s another story. The fact is, I moved to Bay Area last month together with Miss Gui, and started our career at respective companies.&lt;/p&gt;
&lt;p&gt;A new home should have a new TV, so here appears a TV. It&amp;rsquo;s a FHD 49-inch Insignia TV with only 200 bucks. I can&amp;rsquo;t tell the details of color display. I know it&amp;rsquo;s good enough for me, though. The TV is dumb (not smart), so in the first a few days, I had to connect the TV to a laptop to play videos on it. You know, it was far from the best solution. What I want is to insert a tool in the TV, and control it to play YouTube videos with my phone. Basically, I have the three possible solutions. Apple TV works best with my Macbook Pro and iPhone, but it&amp;rsquo;s way more pricer than other two competitors. Fire TV has a remote, and looks like it can satisfy my requirements. Chromecast is the cheapest one (20 bucks), and is totally controlled by Phone, which is also good to me. Therefore, I choosed Chromecast as my first smart TV device.&lt;/p&gt;</description></item><item><title>我的手机图鉴</title><link>https://blog.hgao.net/post/my-cellphones/</link><pubDate>Sun, 18 Dec 2016 -0800</pubDate><guid>https://blog.hgao.net/post/my-cellphones/</guid><description>&lt;p&gt;（写这篇博客之前读了 &lt;a href="http://dlyang.me"&gt;LanternD&lt;/a&gt; 同学的&lt;a href="http://dlyang.me/cellphone-evolution-1/"&gt;《手机进化史 · 上篇》&lt;/a&gt;和&lt;a href="http://dlyang.me/cellphone-evolution-2/"&gt;《手机进化史 · 下篇》&lt;/a&gt;，行文之中会下意识地借鉴，在此表示感谢。）&lt;/p&gt;
&lt;h2 id="功能机时代"&gt;功能机时代&lt;/h2&gt;
&lt;h3 id="联想型号不可考二五年九月-至-约二六下半年"&gt;联想（型号不可考，二〇〇五年九月 至 约二〇〇六下半年）&lt;/h3&gt;
&lt;p&gt;小学六年级，我转学到了上海。人生地不熟，为了方便与家人联络，在开学前几天，老爸带我去牡丹江路的电器商场（是国美永乐还是苏宁呢？）兜了一圈，带回了我的第一部手机。我不记得它的型号，连样子也有些模糊。能够确定的，那是一部一千多块的联想翻盖拍照手机，小巧的银色机身，尚可盈盈一握。这对于小学生来说已经相当奢侈了。拿到它的第一天晚上，和老爸在北翼商业街的大排档附近漫步，给老妈打了第一个电话，当时老妈用的还是几百块的小灵通呢，真是不好意思（笑）。&lt;/p&gt;
&lt;p&gt;除了那寒碜的摄像头，这部手机没什么可玩性。只是当时的需求也高不到哪去，一个俄罗斯方块足以让我玩到半夜，还会因为在将死之际化险为夷而心跳加速睡不着觉。&lt;/p&gt;
&lt;p&gt;至少在〇六年德国世界杯期间，它的表现还是很正常的。我清楚记得决赛夜里，正是它的闹钟把我准时唤醒。然而由于翻盖设计对排线的损耗，这部手机在之后经常点不亮屏幕，再加上当时有了老爸淘汰下来的备用机，我用它的频率日渐减少。终于，从某一天开始，它再也没有点亮过。如今，经过几次搬家，我已不知它现在委身于何处，或许仍在上海的家中吧。按照王国维的二重证据法，我既找不到实物，又没有相关的「史料」，恐怕是难以对它有更清楚的印象了。&lt;/p&gt;
&lt;p&gt;它的另一个重要意义是：&lt;strong&gt;从那时候起，我的身边就一直伴随着一个电子设备&lt;/strong&gt;。而这一点，可能永远都不会再改变。&lt;/p&gt;
&lt;h3 id="摩托罗拉-e360约二六下半年-至-二七年五月"&gt;摩托罗拉 E360（约二〇〇六下半年 至 二〇〇七年五月）&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.hgao.net/images/post/cellphone/moto-e360.jpg" alt="摩托罗拉 E360"&gt;&lt;/p&gt;
&lt;p&gt;之前提到老爸淘汰了一部备用机，便是这摩托罗拉 E360。这是我们全家的第一部手机，大概是老爸在〇二、〇三年购买的。有意思的是，我不仅记得它的型号，还记得当时的价格是一千六百八十元（题外话：一段时间后我在手机店里看到了它，价格变成了八百八十元）。&lt;/p&gt;
&lt;p&gt;由于年代的关系，它的各方面性能都不如我的联想。屏幕不够绚丽，响应不够迅速，更没有摄像头。然而当时的摩托罗拉是一线大厂，质量可靠，所以在我的联想罢工之后，它忠实地承担起了我的通讯任务。可惜的是，里面的游戏实在是不好玩。&lt;/p&gt;
&lt;p&gt;直到最后，它似乎都没出现什么故障，如今也不知身在何处。&lt;/p&gt;
&lt;h2 id="塞班王朝的兴衰"&gt;塞班王朝的兴衰&lt;/h2&gt;
&lt;h3 id="诺基亚-3230二七年五月-至-约二九上半年"&gt;诺基亚 3230（二〇〇七年五月 至 约二〇〇九上半年）&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.hgao.net/images/post/cellphone/nokia-3230.jpg" alt="诺基亚 3230"&gt;&lt;/p&gt;
&lt;p&gt;话说两朝老臣 E360 一直兢兢业业，虽说无功但也无过，对于一个中学生来说，还能有什么苛求呢？然而出于一些我不记得的原因，爸妈还是同意在二〇〇七年的五一长假，带我去牡丹江路换一部新手机。琳琅满目的柜台中，我一眼就看到了造型独特的诺基亚 3230。&lt;/p&gt;
&lt;p&gt;我很早就知道 3230。它是一部经典街机，在各种电视广告、海报中都能见到它酒红色的机身。有一次独身一人乘飞机回青岛，身边的时髦女性用着 3230，给年幼的我一种难以名状的感觉，而且我以为这种感觉是如今的 iPhone 所传达不出来的。&lt;/p&gt;
&lt;p&gt;时过境迁，这部曾经的中高端手机价格不断跳水，来到了一千五百元的价位；而看到我望眼欲穿的样子，爸妈也把预算从一千提高到了一千五——两个数字相遇，我满心欢喜地带着诺基亚 3230 回家了。&lt;/p&gt;
&lt;p&gt;对我而言，诺基亚 3230 最大的价值是它的 Symbian S60 V2 操作系统，让我从功能机用户一跃成为智能手机玩家。最初，由于系统的故障，第三方软件包一直无法安装，但其自带的 FIFA 2005 已经能让我玩得不亦乐乎。后来去家门口的手机店刷了下系统，开始安装各种各样的软件、游戏、主题和字体。再加上国庆长假开通了 GPRS 网络（大概是 10 元 / 50 MB，还记得那天从联通营业厅回家时，走到宝山文化中心，看到屏幕左上角跳出来的「G」时兴奋的心情），从此打开了一个潘多拉魔盒。&lt;/p&gt;
&lt;p&gt;当时的 S60 平台软件丰富，可玩性高，不乏佳作。UCWeb 5.1（UC 浏览器的前身）提供了标签浏览功能，在现在看来仍十分人性化；GameLoft 公司出品的 Java 运动游戏只有一百多 KB 的大小，却五脏俱全，无论是网球还是篮球，其成熟度都超过很多如今 iOS / Android 平台的游戏；QQ 2007 简洁而强大，已经具备了日常所需的基本功能。&lt;/p&gt;
&lt;p&gt;最令我兴奋的是，S60 上有一个叫做 vBag 的 GBA 模拟器。最初尝试了一个马里奥游戏，十分卡顿。但谢天谢地我没有放弃，重启之后，清空了内存（大概一共只有十几兆），3230 成功运行了《精灵宝可梦：红宝石》。我立即意识到，手里的这部设备可以化身一个无所不能的游戏机，而且它全天二十四小时属于我，意味着我再也不用眼巴巴盼望着每周几小时的玩电脑时间。爸妈或许从未想到，这个超出预算的手机竟能有如此大的魔力，还好我没有因此（太）荒废学业。&lt;/p&gt;
&lt;p&gt;题外话，当时还没有响应式设计（Responsive Design）的概念，手机上网大多数访问的是 Wap 网页，只有寥寥的文字连接，却能在 GPRS 网络下高速加载。当时常去的几个网站：一个是新浪体育；一个是尚且十分简洁清爽的百度贴吧；一个是&lt;a href="http://www.d.cn"&gt;当乐网&lt;/a&gt;，游戏资源丰富，如今仍在运作；还有一个是天网手机论坛（waptw.com)，从游戏到各种改版、美化、破解，无所不包。对我影响深刻的《超级机器人大战 OG2》游戏 ROM，就是在天网论坛下载到的。可惜天网现在似乎已经无法访问了。&lt;/p&gt;
&lt;p&gt;3230 还有一定的多媒体功能。虽然屏幕分辨率只有 176 * 208，存储卡也只有 128MB，但好歹能放几首歌或者短视频进去。那些年用它听过《千里之外》，《秋天不回来》，看过麦蒂三十五秒十三分的视频。哎，暴露年龄系列。&lt;/p&gt;
&lt;p&gt;诺基亚 3230 &lt;strong&gt;从来没有让我感到无聊过&lt;/strong&gt;。这是一个很高的评价。然而它的寿命不长，由于 GBA 游戏玩得太多，它娇小脆弱的摇杆不再灵敏，逐渐被我搁置，如今可能在家中的某个角落颐养天年吧。与此同时，老爸的又一部备用机出现在我的眼前，它搭载了最新的 S60 V3 系统，成功吸引了我的注意。&lt;/p&gt;
&lt;h3 id="诺基亚-e50约二九上半年-至-二一年四月"&gt;诺基亚 E50（约二〇〇九上半年 至 二〇一〇年四月）&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.hgao.net/images/post/cellphone/nokia-e50.jpg" alt="诺基亚 E50"&gt;&lt;/p&gt;
&lt;p&gt;这部诺基亚 E50 是老爸公司配发的手机，然而用了一阵之后，老爸显然不能体会到智能手机的魅力，换回了他之前的诺基亚 6270。于是，失宠的 E50 只能轮到我来疼爱了。&lt;/p&gt;
&lt;p&gt;E50 采用 S60 V3 操作系统，虽然屏幕尺寸比 3230 小，但是分辨率提升到 240 * 320，显示效果相当细腻。其运行内存大概是整个 S60 V3 家族中最小的（价格可能也是最便宜的），但比 3230 强不少，这意味着我可以使用模拟器加速进行 GBA 游戏了！（这很重要！）&lt;/p&gt;
&lt;p&gt;虽然没有统计过，但我相信自己在 E50 上花费的游戏时间相当可观。除了《精灵宝可梦》和《超级机器人大战》，我还打穿了《逆转裁判》和《火焰之纹章》两个经典系列，至今仍保留着很多美好的回忆。&lt;/p&gt;
&lt;p&gt;至于其他游戏，除了依然令人爱不释手的 GameLoft 作品之外，另一个神奇的游戏是《冠军足球经理》。几百 KB 的程序包括了联赛杯赛、球员买卖、战术布置等核心要素，其中的一些妖人更是几年后在现实中崭露头角。诺伊尔、拉姆塞都是我在这款游戏中认识的。&lt;/p&gt;
&lt;p&gt;除了显示和计算性能的提升，S60 V3 的软件资源也更为丰富。V3 和 V2 的应用程序互不兼容，有些 V2 上的好软件（比如内存管理）到 V3 中就找不到了。但同时，也有更多的 V3 独占应用可供选择。V3 的一大特点（缺点）是，安装软件需要认证签名，这算是我第一次真正意义上地「折腾」手机。&lt;/p&gt;
&lt;p&gt;由于大量的游戏操作，E50 的「7」键和「*」键（分别充当 GBA 的「B」键和「A」键）被我摧残至掉漆，摇杆也越发不灵敏；金属外壳在屡次摔碰之下已经变形，被我贴上胶带继续使用（质量真是好）。终于有一天，它的摇杆完全没有反应，我却不愿放弃，从淘宝买了一个替换摇杆自己拆机，却再也点不亮它。&lt;/p&gt;
&lt;p&gt;和前任们一样，E50 的遗体也不知在何方。大概越是喜爱，就越容易过度使用，从而结局也变得越发悲惨吧。&lt;/p&gt;
&lt;h3 id="诺基亚-5233二一年四月-至-二一三年一月"&gt;诺基亚 5233（二〇一〇年四月 至 二〇一三年一月）&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.hgao.net/images/post/cellphone/nokia-5233.jpg" alt="诺基亚 5233"&gt;&lt;/p&gt;
&lt;p&gt;历史的车轮滚滚向前。塞班系统虽然普及，但随着苹果和 Android 阵营的发力，中高端市场已经被全触屏手机所占据。作为应对，诺基亚发布了搭载 S60 V5 触屏专用系统的 N97、5800XM、5530XM、5230、5233。虽然价格上拉开了很大差距，但它们的 CPU、内存、分辨率却完全相同（5530XM 作为其中的次高端手机，屏幕竟然比低端 5230、5233 还小，不能理解）。5230 比 5233 多了 3G 网络，对我也没多大用处，于是我自然选择了其中最便宜、一千块出头的 5233。&lt;/p&gt;
&lt;p&gt;相比 E50，5233 的性能更上一个台阶（毕竟和旗舰 N97 持平），玩 GBA 游戏已经是毫无压力。但去掉了键盘，初期打字还是有些不便，《冠军足球经理》这样的优秀游戏也玩不到了，好在 GBA 模拟器永远都能满足我啊。&lt;/p&gt;
&lt;p&gt;5233 的真正优势是多媒体能力。640 * 360 的 3.2 寸屏幕能够以（在当时看来）相当不错的效果观看视频，我用它看完了《魔法禁书目录》、《某科学的超电磁炮》、《Clannad》等经典动画。为了节省空间而不断用格式工厂调整码率，恐怕是那个时代特有的回忆。&lt;/p&gt;
&lt;p&gt;那是塞班最后的巅峰，高中的班里有不少塞班用户。作为第一个 S60 V5 玩家，我为此还给大家写过破解和签名的教程。大概那时候开始，我可能给同学们一种「资深手机玩家」的印象，大家买手机有时候会找我咨询。&lt;/p&gt;
&lt;p&gt;当时间来到二〇一三年的时候，塞班无论从性能还是应用生态上，都已经完全无法抗衡 iOS 和 Android。于是，以「不能收邮件、不能与 Google 帐号同步、不能用 Dropbox 移动办公」为由，5233 被我强制退休了。&lt;/p&gt;
&lt;p&gt;5233 是我用过最长寿、最耐操的的手机。我不记得在哪家店、用什么价格买回了它，但它的结局最为安详：完好无损地躺在我家中的抽屉里，无疾而终。&lt;/p&gt;
&lt;p&gt;如果说移动电话发展至今，把它前后分为两段，那么切分点毫无疑问是二〇〇七年——第一代 iPhone 发布。此后的近十年间，手机从一个通讯工具，变成一个和电脑一样、几乎全能的数字计算设备。塞班系统作为前 iPhone 时代的智能手机，没有出众的计算能力，没有复杂的社交功能，但其中那些纯粹的应用让我始终难以忘怀。我总能明白，自己在用手机做什么，自己的时间花在了什么地方。&lt;/p&gt;
&lt;h2 id="重新发明手机"&gt;「重新发明手机」&lt;/h2&gt;
&lt;h3 id="索尼-xperia-acro-slt26w二一三年一月-至-二一四年九月"&gt;索尼 Xperia Acro S（LT26W，二〇一三年一月 至 二〇一四年九月）&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.hgao.net/images/post/cellphone/sony-lt26w.jpg" alt="索尼 Xperia Acro S (LT26W)"&gt;&lt;/p&gt;
&lt;p&gt;Acro S 是我的第一部 Android 手机，也是第一部水货手机。二〇一三年的寒假，我在上海火车站不夜城，从 Jack（我的高中同桌，全班第一位 Android 用户）推荐的店里带回了它，价格是两千两百八十元。作为索尼上一代主力机型 Xperia S (LT26i) 的防水版，算是两千元档性价比不错的选择。它的性能与 5233 不可同日而语，GBA 游戏八倍速运行毫无压力。更直观的性能反映是，它可以使用 NDS 模拟器了！虽然模拟速度不快，但足以让我体验无穷丰富的 NDS 游戏资源。&lt;/p&gt;
&lt;p&gt;Acro S 配备一块 720P 的 4.3 寸屏幕，像素密度达到 Retina 标准，但因为 iPad 的存在，我并没有用它看很多视频，同时 Android 原生游戏也没有深入体验。&lt;/p&gt;
&lt;p&gt;那段时间，我很注重个人数据的云同步，Android 在这方面提供了一个优秀的平台。Dropbox、Gmail、Google Calendar、Google Contacts 这些需求都可以通过 Android 管理，再也不必担心数据丢失。Google Now 更是一次次带来惊喜，也经常被我用于向父母、同学展示大数据的魅力。大概从这时开始，手机从一个通讯、娱乐工具，变成了我的个人数据管理终端。&lt;/p&gt;
&lt;p&gt;Acro S 没少被我折腾。用了一阵原生系统后，开始着手对一些不如意的地方进行改造。先是 root 之后删了些内置应用，后来直接刷各种各样的 ROM。索尼的官方系统只支持到 Android 4.0，但我把 4.1 到 4.4 的系统都刷过一遍。无奈 Acro S 属于小众，ROM 资源相比 Xperia S 差距甚远，即使偶尔能找到新版本的系统，其稳定性也难以令人满意。&lt;/p&gt;
&lt;p&gt;之前提到 Acro S 是一部防水手机，没想到这竟成了它的命门。二〇一四年九月，和两位朋友玩漂流，防水袋漏了，三人手机均进水。另两位同学无法开机，我的 Acro S 却不屈不挠正常工作，带我们导航回酒店，立下汗马功劳。然而回去之后，手机开始发烫、屏幕变白，最终无法启动了。其实我觉得这点进水程度不应该对一部「防水」手机造成致命伤，但事实就是如此。可能是我没把防水口塞紧，也可能是索尼大法并没有那么好吧。&lt;/p&gt;
&lt;p&gt;Acro S 后来被我拆了一下，又装回去（了一部分），现在安然躺在爸妈房间的抽屉里。&lt;/p&gt;
&lt;h3 id="lg-nexus-5二一四年九月-至今"&gt;LG Nexus 5（二〇一四年九月 至今）&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://blog.hgao.net/images/post/cellphone/lg-nexus5.jpg" alt="LG Nexus 5"&gt;&lt;/p&gt;
&lt;p&gt;自从被 Jack 安利了 Nexus 系列之后，我心里就一直对「原生」有所牵挂。5233 退役时，曾试过海淘性价比超群的 Nexus 4，但因为供不应求而作罢；等到 Acro S 需要接班人的时候，我不再犹豫，毅然买下发布十个月之久、价格依旧坚挺的 Nexus 5。&lt;/p&gt;
&lt;p&gt;这是我第一次买一部「系统和前任相同」的手机，按理说不容易有新鲜感。但是 Nexus 5 更轻便、速度更快、屏幕更大、分辨率更高、续航更久。总而言之，这是我第一次感觉到，自己&lt;strong&gt;买了一部「完美」的手机&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;型号带着「Nexus」，就意味着海量的刷机资源。然而折腾一轮、过了把瘾之后，还是换回了无 root 的原生系统。这一方面归功于硬件性能的提升，让用户不再需要定制以换取更快的响应速度；另一方面也归功于 Android 5.0 系统的长足进步。&lt;/p&gt;
&lt;p&gt;我的 Nexus 5 一直都是无套无膜裸奔，以致为它换过两次屏幕。因为屏占比太高，一旦磕碰就会碎得稀里哗啦。后来贴了钢化膜保护，倒是再也没摔过。&lt;/p&gt;
&lt;p&gt;今年感恩节期间，Nexus 5 的振动失灵。加上电池老化、摄像头进灰等原因，我在 eBay 上买了一部打折的 iPhone 7。未曾想，这 iPhone 有硬件故障，而 Nexus 5 的振动几天后又恢复正常，竟得以继续服役了。&lt;/p&gt;
&lt;h2 id="后记"&gt;后记&lt;/h2&gt;
&lt;p&gt;十多年前，看过一个 Windows Mobile 手机的广告，上面运行着一个微缩的 Windows 98。那时我以为，这是手机的最终形态。现在，对比我的第一部联想，市面上的手机已经变得完全不同。那是一个中学生所不能想像的。十年后的手机会是什么样子？或许会有新的介质、新的交互方式；当然，也可能因为一些外在原因，停滞不前。它越来越重要，因为人与人的联络日益紧密；它也越来越不重要，因为计算设备间的界限正逐渐模糊。&lt;/p&gt;</description></item><item><title>Hugo, GitHub, VPS (2): Write a Hook Listener and Protect It Using Supervisor</title><link>https://blog.hgao.net/post/hugo-github-vps-2/</link><pubDate>Fri, 29 Jan 2016 +0800</pubDate><guid>https://blog.hgao.net/post/hugo-github-vps-2/</guid><description>&lt;p&gt;The article will talk about the first part mentioned in &lt;em&gt;&lt;a href="https://blog.hgao.net/post/hugo-github-vps-1/"&gt;Hugo, GitHub, VPS (1): A Work Flow for static sites&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="create-github-webhooks"&gt;Create GitHub webhooks&lt;/h2&gt;
&lt;p&gt;GitHub has an &lt;a href="https://developer.github.com/webhooks/"&gt;official tutorial&lt;/a&gt; for webhooks, you can follow it and create one easily. The tutorial sets the payload URL to &lt;code&gt;http://localhost:4567/payload&lt;/code&gt;, but since my hook is deployed on the VPS, I just fill in &lt;code&gt;http://my.domain:18001&lt;/code&gt;. The port &lt;code&gt;:4567&lt;/code&gt; and the subfolder &lt;code&gt;payload&lt;/code&gt; doesn&amp;rsquo;t matter. That&amp;rsquo;s where the &lt;em&gt;POST&lt;/em&gt; request is sent, and you just need to match it with the port and location of your hook. I preserve the whole &lt;code&gt;:18001&lt;/code&gt; port for the hook, so I let the request sent to the root path directly.&lt;/p&gt;
&lt;p&gt;The content type is &lt;code&gt;application/json&lt;/code&gt;, and we only trigger the hook for push events.&lt;/p&gt;
&lt;h2 id="write-a-hook-listener"&gt;Write a hook listener&lt;/h2&gt;
&lt;p&gt;The tutorial offers a sample listener app written in &lt;a href="http://www.sinatrarb.com"&gt;Sinatra&lt;/a&gt;, which is a Ruby micro-framework. Unfortunately, I know little about Ruby as well as Sinatra, so I choose to use &lt;a href="http://www.tornadoweb.org"&gt;Tornado&lt;/a&gt;, a Python framework.&lt;/p&gt;
&lt;p&gt;There is a &lt;a href="http://www.tornadoweb.org/en/stable/#hello-world"&gt;&amp;ldquo;Hello, world&amp;rdquo; example&lt;/a&gt; in Tornado&amp;rsquo;s documents. It has a &lt;code&gt;get&lt;/code&gt; method in &lt;code&gt;MainHandler&lt;/code&gt;, and what we want is a &lt;code&gt;post&lt;/code&gt; method. So delete &lt;code&gt;get&lt;/code&gt; and write a &lt;code&gt;post&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;2&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;3&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;4&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;5&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# do something&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;6&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;New POST Received.&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;7&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/generate.sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, you can run the app and push something to your blog for which a hook has been set, and the terminal will show &lt;code&gt;New POST Received.&lt;/code&gt;. Then, it executes a shell script called &lt;code&gt;generate.sh&lt;/code&gt;, which is located in the same folder as the tornado python file. Since we want our VPS to pull down the new source codes immediately and regenerate the static site, so we embed these operations in the script file and make it look like this:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;#! /bin/sh
cd path/to/blog-repo
git pull
rm -rf public # remove previous output files
hugo # generate the site in public/
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then change its authority:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;chmod +x generate.sh
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we judge whether the &lt;em&gt;POST&lt;/em&gt; request is from our GitHub repository. Perhaps the hook is listening to several updates from different users, branches and repos, so let&amp;rsquo;s parse the &lt;em&gt;json&lt;/em&gt; file and read its information.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 2&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 3&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 4&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 5&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 6&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 7&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 8&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repository&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;full_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 9&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;10&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Not a GitHub webhook post&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;11&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;12&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;yourusername/yourreponame&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;13&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;POST from my blog&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;14&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/generate.sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;15&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;user2/repo2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;16&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# do something...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;17&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;user3/repo3&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;18&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# do something...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;19&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;user4/repo4&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;20&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# do something&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;21&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;22&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;const&lt;/span&gt; &lt;span class="n"&gt;uint16_t&lt;/span&gt; &lt;span class="n"&gt;PROGMEM&lt;/span&gt; &lt;span class="n"&gt;keymaps&lt;/span&gt;&lt;span class="p"&gt;[][&lt;/span&gt;&lt;span class="n"&gt;MATRIX_ROWS&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;MATRIX_COLS&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;23&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;Your&lt;/span&gt; &lt;span class="n"&gt;configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;24&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, we add logging module and filter IP addresses outside GitHub to prevent potentially harmful pseudo &lt;em&gt;POST&lt;/em&gt; requests. Luckily, GitHub has &lt;a href="https://help.github.com/articles/what-ip-addresses-does-github-use-that-i-should-whitelist/"&gt;a whitelist&lt;/a&gt;, so we only allow addresses from &lt;code&gt;192.30.252/22&lt;/code&gt;. Now we&amp;rsquo;ve got the whole program:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 1&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tornado.ioloop&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;tornado.web&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 2&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 3&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;subprocess&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 4&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 5&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 6&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 7&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 8&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remote_ip&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt; 9&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;192&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;30&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;252&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;10&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;## GitHub IP range&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;11&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;12&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;repository&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;full_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;13&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;14&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Not a GitHub webhook post&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;15&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;16&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;yourusername/yourreponame&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;17&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;POST from my blog&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;18&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/generate.sh&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;19&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;20&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Unknown repo: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;21&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;22&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Not from GitHub. IP [&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;]: &lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;23&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;24&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_app&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;25&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;web&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;26&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MainHandler&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;# match your payload URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;27&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;28&lt;/span&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;29&lt;/span&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;30&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%(asctime)s&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="si"&gt;%(message)s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datefmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%m/&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s1"&gt;/%Y, &lt;/span&gt;&lt;span class="si"&gt;%a&lt;/span&gt;&lt;span class="s1"&gt;, %H:%M:%S&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;/post.log&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;31&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_app&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;32&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;18001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# match your payload URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="ln"&gt;33&lt;/span&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tornado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ioloop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IOLoop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="protect-the-listener-process"&gt;Protect the listener process&lt;/h2&gt;
&lt;p&gt;In order to let our listener process run consistently in background and reboot after crash, we use &lt;a href="http://supervisord.org/"&gt;Supervisor&lt;/a&gt; as a daemon program. Besides, it can manage multiple subprocesses of a tornado app and balance the load with the help of &lt;a href="http://nginx.org/"&gt;nginx&lt;/a&gt;, but it&amp;rsquo;s unnecessary for a simple hook.&lt;/p&gt;
&lt;p&gt;Follow &lt;a href="http://supervisord.org/configuration.html"&gt;this direction&lt;/a&gt; to put your &lt;code&gt;supervisord.conf&lt;/code&gt; file in proper path, and add codes like the following:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[program:mylistener]
command=python /path/to/mylistener.py
redirect_stderr=true
stdout_logfile=/path/to/log.log
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, run &lt;code&gt;supervisorctl reload&lt;/code&gt; to check if &lt;code&gt;mylistener&lt;/code&gt; has been started.&lt;/p&gt;
&lt;h2 id="host-the-blog"&gt;Host the blog&lt;/h2&gt;
&lt;p&gt;Almost all static site generators have their internal server, but you can also use more dedicated web servers such as Apache or nginx for advanced performance and stability, just setting the host path to your output folder in the configuration file.&lt;/p&gt;
&lt;p&gt;We are done! Now try pushing something on GitHub, and you should see changes on your VPS.&lt;/p&gt;</description></item><item><title>Hugo, GitHub, VPS (1): A Work Flow for static sites</title><link>https://blog.hgao.net/post/hugo-github-vps-1/</link><pubDate>Wed, 27 Jan 2016 +0800</pubDate><guid>https://blog.hgao.net/post/hugo-github-vps-1/</guid><description>&lt;p&gt;I changed my username (See &lt;a href="https://blog.hgao.net/about"&gt;About&lt;/a&gt;) on Twitter and GitHub recently. Then, my updates to the blog won&amp;rsquo;t be examined by GitHub Pages. Worse is better, maybe it&amp;rsquo;s a chance for me to migrate the site on my DigitalOcean VPS. However, I still want to manage my source code on GitHub, tracing the change log and not necessary to worry about data loss.&lt;/p&gt;
&lt;p&gt;The tasked is divided into three sections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Listen to the changes (Git Push) on GitHub.&lt;/li&gt;
&lt;li&gt;Pull new changes from GitHub to VPS.&lt;/li&gt;
&lt;li&gt;Generate the site from source codes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For step 3, since my blog has to be generated by Jekyll and the repository on GitHub is just source code, the VPS has to execute &lt;code&gt;jekyll build&lt;/code&gt; command after receiving git updates. But I met with some problems here due to Ruby and Jekyll. Again, worse is better. I thought about &lt;a href="http://gohugo.io/"&gt;Hugo&lt;/a&gt;, which is a much faster generator written in Go. So I replaced Jekyll and refactored my blog, but it&amp;rsquo;s a painful process because it&amp;rsquo;s unfriendly to implement some logic with Hugo. Regardless, the blog you are reading is made by Hugo now.&lt;/p&gt;
&lt;p&gt;In the next article, I will write about how to build a hook program on the VPS with &lt;a href="http://www.tornadoweb.org/"&gt;Tornado&lt;/a&gt; and &lt;a href="http://supervisord.org"&gt;Supervisor&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>关于个人网络帐号安全的解决方案</title><link>https://blog.hgao.net/post/a-solution-to-online-security/</link><pubDate>Mon, 19 Oct 2015 +0000</pubDate><guid>https://blog.hgao.net/post/a-solution-to-online-security/</guid><description>&lt;p&gt;10 月 19 日下午，据漏洞报告平台&lt;a href="http://www.wooyun.org/"&gt;乌云网&lt;/a&gt;披露，「&lt;a href="http://www.wooyun.org/bugs/wooyun-2015-0147763"&gt;某邮箱过亿数据泄漏&lt;/a&gt;」。与此同时，各个论坛中均有使用网易邮箱的用户表示 Apple ID 被冻结，引发很多人修改淘宝、支付宝等网站绑定的网易邮箱。各种消息都指向这是来自网易的一起拖库事件。所谓拖库，是指获取网站中用于存放用户名、密码、安全问题等帐号信息的数据库。拖库无法从根本上避免，因为你无法保证注册的每个网站都有足够的安全保护。在这里想讨论的就是如何应付拖库，以及由此出发的、一套相对安全的个人网络帐号解决方案。&lt;/p&gt;
&lt;h2 id="定期更换密码"&gt;定期更换密码&lt;/h2&gt;
&lt;p&gt;只要是稍微正规些的网站，都会对数据库中的用户名和密码进行加密存储，即使黑客获得数据库，也得花好一阵子才&lt;strong&gt;可能&lt;/strong&gt;破解。这就是要定期更换密码的原因。如果在数据库破解之前就更改了密码，破解出的内容自然也就没有价值。这实际上是通过缩短密文的有效生命周期来达到计算安全。&lt;/p&gt;
&lt;h2 id="密码管理"&gt;密码管理&lt;/h2&gt;
&lt;p&gt;总有那么一些不正规的网站（比如各种小论坛），因为某些原因采用了不安全的存储方式（最直接的就是明文存储），让黑客得到了你得密码。但这些小网站往往也没有什么重要信息，为什么会造成损失呢？其关键在于，很多用户在多个网站上使用同一套用户名密码。黑客通过撞库，便能登录该用户在同一密码体系下的全部站点。为了避免这种损失，我们可以根据自身的条件和需求，采用以下几个级别的安全措施，安全程度递增。它们的共同特点是，&lt;strong&gt;在重要的网站上都采用了各不相同的密码&lt;/strong&gt;。一般情况下，做到第一点已经能保证自己不会因为密码问题而发生巨大并不可挽回的损失。&lt;/p&gt;
&lt;h3 id="1-多级别密码"&gt;1. 多级别密码&lt;/h3&gt;
&lt;p&gt;考虑到人脑的记忆和有限，很难要求一个人把各种乱七八糟的网站密码记得一清二楚。于是我们退而求其次，将一些重要的密码分开，而对小网站则可以放松要求，使用统一、常年不变的弱密码。这方面的心得可以参考&lt;a href="http://www.zhihu.com/question/19695004/answer/12976049"&gt;吴涛在知乎的回答&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;如果 A 和 B 网站的密码不同，黑客拿到用户在 A 网站的用户名和密码，尝试去撞 B 网站。尽管用户名已经在库中，但由于密码不同，撞库不会成功，最多会因为失败尝试过多而导致冻结。&lt;/p&gt;
&lt;h3 id="2-各不相同的密码"&gt;2. 各不相同的密码&lt;/h3&gt;
&lt;p&gt;这一步开始，建议大家使用密码管理器，这是一种协助生成、管理密码的工具，能够方便地使用生成的强密码自动登录各个网站，并减小记忆成本。缺点则是一旦被攻破，所有的密码都被暴露了。Chrome 和苹果都有自带的管理工具，也可以使用常见的第三方产品，比如 LastPass、1Password、Keepass。具体请自行搜索。有了密码管理器，即使小网站拖库也不会波及其他小网站了。&lt;/p&gt;
&lt;h3 id="3-前缀--网站识别符"&gt;3. 前缀 + 网站识别符&lt;/h3&gt;
&lt;p&gt;Gmail 邮箱有一个很棒的特点，可以在用户名后添加「+」+ 任意字符串，系统会自动过滤加号以后的内容，从而把邮件发到原邮箱中。比如一个 &lt;a href="mailto:user@gmail.com"&gt;user@gmail.com&lt;/a&gt; 的邮箱，在 foo 网站使用了 &lt;a href="mailto:user+foo@gmail.com"&gt;user+foo@gmail.com&lt;/a&gt; 作为注册邮箱，则相关的邮件仍会发到 &lt;a href="mailto:user@gmail.com"&gt;user@gmail.com&lt;/a&gt; 中。同理，在 bar 网站，可用 &lt;a href="mailto:user+bar@gmail.com"&gt;user+bar@gmail.com&lt;/a&gt; 作为注册邮箱。如果 foo 网站发生拖库，黑客使用电脑自动撞库，会因为 bar 网站不存在 &lt;a href="mailto:user+foo@gmail.com"&gt;user+foo@gmail.com&lt;/a&gt; 这个帐号而失败。但是倘若黑客有意对该用户进行攻击，则可以通过 foo 网站的特征，猜出其在 bar 网站的帐号为 &lt;a href="mailto:user+bar@gmail.com"&gt;user+bar@gmail.com&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;这种方法同样适用于注册各种小号、苹果的多国帐号等。&lt;/p&gt;
&lt;h3 id="4-前缀--乱码"&gt;4. 前缀 + 乱码&lt;/h3&gt;
&lt;p&gt;如何不让猜出 bar 网站的用户名呢？只要给各个网站随机分配后缀就可以了。让 foo 网站的邮箱为 &lt;a href="mailto:user+wjv2@gmail.com"&gt;user+wjv2@gmail.com&lt;/a&gt; ，bar 网站的邮箱为 &lt;a href="mailto:user+9m2q@gmail.com"&gt;user+9m2q@gmail.com&lt;/a&gt;，便极大程度上杜绝了猜出用户名的可能性。&lt;/p&gt;
&lt;h3 id="5-完全乱码"&gt;5. 完全乱码*&lt;/h3&gt;
&lt;p&gt;&lt;a href="mailto:sdi234mv@gmail.com"&gt;sdi234mv@gmail.com&lt;/a&gt; 和 &lt;a href="mailto:jn02mb@gmail.com"&gt;jn02mb@gmail.com&lt;/a&gt;，显然不存在任何联系，耶！但是这意味着要为每个帐号单独注册邮箱！你可以把它们都转发到你的主邮箱，但总体而言这种做法仍是牺牲了极大的便利性，换取一点几乎用不上的安全性提升。当然也可以通过设置个人域名邮箱来简化操作，但不是每个人都有自己的域名，而且独特的域名已经在昭示这些帐号拥有共同的主人。因此，我为这个方法加上了星号。它一点都不实用。&lt;/p&gt;
&lt;p&gt;然而很可惜，这些方法实际上很少能派上用场。有很多网站支持用户名（一个独一无二的字符串）登录，而且该字符串会被网站的其他用户看到（比如 Twitter 、 V2EX）。这样一来，以上 3、4 点都将无意义（你总不希望为了防止撞库，而在各个网站被人看到不同的名字吧？）。如果你想把淘宝的用户名改成一个乱码防止被撞，值得表扬，但淘宝支持手机号登录。别灰心，实际上第一点已经能够阻止绝大部分的拖库了。况且我们还有其他的措施。&lt;/p&gt;
&lt;h2 id="两步验证"&gt;两步验证&lt;/h2&gt;
&lt;p&gt;不夸张地说，&lt;a href="https://en.wikipedia.org/wiki/Two-factor_authentication"&gt;两步验证（two-factor authentication）&lt;/a&gt;已经成为一个合格重要网站的标配。用户在使用密码登录的同时，还需要输入手边的密码生成器中的随机验证码，从而防止因密码被盗而被别人异地登录。除非获知验证码的随机种子，否则难以破解。尽管增加了一道步骤，但很多网站的两步验证机制都支持在常用设备上免验证登录，大大简化了登录过程。建议在所有重要且支持两步验证的网站上开启。&lt;/p&gt;
&lt;p&gt;常见的两步验证包括手机短信、手机验证器、密码器（网银常用）等。其中采用最多、也最简便的方法是手机验证器，通过扫描网站二维码添加验证。这些在开启两步验证时会有介绍，不再赘述。&lt;/p&gt;
&lt;p&gt;开启后也并非万无一失，毕竟手机有丢失的风险，即使远程锁定，也会因为无法获取验证码，影响自己登录。对此，网站一般也提供了多种应急机制，比如备用手机号和恢复码。备用手机号很容易理解。恢复码是一系列用于代替两步验证的密码，网站通常会建议用户将其打印下来，保存在钱包或其他安全地点。个人建议保存多份，分别放在家中、工作单位、随身携带。如果不提供恢复码，也可以将开启两步验证时扫描的二维码打印。如果不方便打印，可以保存为图片，并加密压缩，存储在多个安全地点，比如 Dropbox、移动硬盘中。&lt;/p&gt;
&lt;p&gt;总而言之，&lt;strong&gt;两步验证是密码失窃后最重要最有效的屏障&lt;/strong&gt;（比密码保护问题靠谱得多），应尽量开启，并且&lt;strong&gt;一定要做好手机丢失的应急预案&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="安全性分析"&gt;安全性分析&lt;/h2&gt;
&lt;p&gt;下图展示了一位使用密码管理器的用户的安全分析。&lt;/p&gt;
&lt;img align="center" class="img-responsive" src="https://blog.hgao.net/images/post/security_analysis.svg" alt="安全性分析图"&gt;
&lt;p&gt;在开启了两步验证、以及各个网站使用不同密码后，我们的威胁主要来自三个方面：&lt;/p&gt;
&lt;h3 id="1-密码管理器拖库"&gt;1. 密码管理器拖库&lt;/h3&gt;
&lt;p&gt;意味着所有密码都暴露了。但实际上一家做密码管理器的公司都有这种觉悟，它们或是有特别的加密技巧，或是干脆把数据库放到用户本地。然而话说回来，永远不要把希望寄托在他们身上，我们还是得做最坏的打算。首先尽快修改密码管理器的主密码，并按重要性排序，依次修改其他密码。如果数据库存在用户本地，请将他们放在安全的地方，比如多个移动硬盘，以及 Dropbox、Google Drive，并进行加密处理。&lt;/p&gt;
&lt;h3 id="2-电脑丢失"&gt;2. 电脑丢失&lt;/h3&gt;
&lt;p&gt;首先请保证离开电脑随时锁屏。这样即使你去上厕所，别人也无法趁机获取信息。假设电脑被盗，尽快远程抹除。如果没有开启抹除，请登录密码管理器和其他重要网站，将丢失设备的远程下线。如果真的在此之前锁屏密码就被破解（只要密码不太弱，可能性就很低），那入侵者就可能获取你的密码管理器内容。不放心的话，每次使用密码管理器时都手输主密码吧。同理，Dropbox 中的内容也可以直接从解开锁屏的手机访问，所以不应该在其中存放未经加密的敏感信息。&lt;/p&gt;
&lt;h3 id="3-手机丢失"&gt;3. 手机丢失&lt;/h3&gt;
&lt;p&gt;和电脑类似，同样请做好备份（最好是同步），尤其是通讯录和照片。锁屏也是必须的，如果有指纹识别，请使用指纹配合强密码，否则可选用不易被猜出的 PIN 或锁屏图案。同样，丢失后应尽快远程抹除或下线。如果已被入侵，由于手机是两步验证工具，所以比电脑失窃更加危险，一定要确保在每次使用密码管理器前都要求输入密码，或者指纹解锁。如果没有指纹，同样选可用 PIN 或锁屏图案。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;信息安全没有最好，只有更好。McAfee 说自己几乎不用只能手机，如果迫不得已，也会两周换一部。这对一般人并不现实，但或许我可以冒着政治不正确的风险说，一般人的信息安全也并没有那么重要。能够做到以上这些，起码能够在可接受的操作复杂度内，降低财产、社交、数据的损失。至于日常的通话是否被监听，毫无疑问是杞人忧天了。&lt;/p&gt;</description></item><item><title>Install Destor on Ubuntu 14.04</title><link>https://blog.hgao.net/post/install-destor-on-ubuntu/</link><pubDate>Sun, 11 Oct 2015 +0000</pubDate><guid>https://blog.hgao.net/post/install-destor-on-ubuntu/</guid><description>&lt;p&gt;&lt;a href="https://github.com/fomy/destor"&gt;&lt;em&gt;Destor&lt;/em&gt;&lt;/a&gt; is a platform for data deduplication evaluation developed by &lt;a href="https://github.com/fomy"&gt;Min Fu&lt;/a&gt;. It runs on 64-bit Linux. I wanted to use it to compare the performance of existing deduplication schemes and those of my own. However, the installation process is a little bit complicated, which took me two days to make it work. My platform is 64-bit Ubuntu 14.04 with kernel 3.13.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Download glib. Extract it to &lt;code&gt;[PATH_TO_GLIB]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Run the following commands.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;sudo apt-get install zlib1g-dev
sudo apt-get install libffi-dev
cd [PATH_TO_GLIB]
./configure
make
sudo make install
cd /usr/local/
sudo cp include/glib-2.0/* include/
sudo cp lib/glib-2.0/include/glibconfig.h include/
cd lib
sudo link libglib-2.0.so libglib.so
sudo apt-get install libssl-dev
sudo apt-get install autotools-dev
sudo apt-get install automake
cd [DIR_OF_DESTOR]
./configure
automake --add-missing
make
sudo make install
&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>在 Android 6.0 Marshmallow 上开启 Google Now</title><link>https://blog.hgao.net/post/enable-google-now-on-android-6.0/</link><pubDate>Tue, 06 Oct 2015 +0000</pubDate><guid>https://blog.hgao.net/post/enable-google-now-on-android-6.0/</guid><description>&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;感谢 &lt;a href="https://disqus.com/by/disqus_6awjvNqfHQ/"&gt;Tiany&lt;/a&gt; 提醒，使用此法将导致 Google Play Store 不能使用。目前我通过登录另一个 Google 帐号解决该问题，参考了 &lt;a href="https://disqus.com/by/chammaxium/"&gt;Cham Maxium&lt;/a&gt; 的方法：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Account 里删除原有 Google 账号，再重新登入后，方可解决该问题。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;以及 V2EX 上 &lt;a href="http://www.v2ex.com/member/ssenkrad"&gt;ssenkrad&lt;/a&gt; 的方法:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;@lonelinsky 5.0 时候发现的， 6.0 是否还适用不得而知，手机上登陆两个 google 账号，开启 google now 之后退出重新登录任意一个，两个账号就都能用 play 了。&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;感谢二位。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;总体步骤和之前 5.1 类似，只是有些选项的位置发生了改变，需要稍加寻找。Nexus 5 刷原厂镜像（MRA58K）测试通过。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更改系统语言为英语；&lt;/li&gt;
&lt;li&gt;关闭 &lt;code&gt;Location&lt;/code&gt;，开启飞行模式，打开 Wi-Fi；&lt;/li&gt;
&lt;li&gt;进入 &lt;code&gt;Settings -&amp;gt; Apps&lt;/code&gt;，&lt;strong&gt;点击右上角三个点，选择 &lt;code&gt;Show System&lt;/code&gt;&lt;/strong&gt;；&lt;/li&gt;
&lt;li&gt;在列表中找到 &lt;code&gt;Google App&lt;/code&gt;，点击 &lt;code&gt;Storage&lt;/code&gt;，点击 &lt;code&gt;MANAGE SPACE&lt;/code&gt;，点击&lt;code&gt;CLEAR ALL DATA&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;对 &lt;code&gt;Google Play services&lt;/code&gt;、&lt;code&gt;Google Play Store&lt;/code&gt;、&lt;code&gt;Google Services Framework&lt;/code&gt; 进行同样操作；&lt;/li&gt;
&lt;li&gt;回到&lt;code&gt;Settings&lt;/code&gt;，选择 &lt;code&gt;Google&lt;/code&gt;，点击 &lt;code&gt;Search &amp;amp; Now&lt;/code&gt;，点击 &lt;code&gt;Accounts &amp;amp; privacy&lt;/code&gt;，点击 &lt;code&gt;Google Account&lt;/code&gt;，选择 &lt;code&gt;Sign out&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;回到刚才的界面，再次点击 &lt;code&gt;Google Account&lt;/code&gt;，选择之前登录过的帐号。&lt;/li&gt;
&lt;/ol&gt;</description></item></channel></rss>