About

Blog

如你所见,如我所想。

Me

Microtaku -> ~(>_<~)

Yuujin

以下为各位线上/线下认识的小伙伴们的真·无序排列:

| Posted by frantic1048

给 Arch Linux 换个文件系统和启用全盘加密

近来组装好了一个新的小电脑,于是终于可以放心对之前的小本本做各种想做不敢做的大改动了。于是我就:

  • 分区表和引导方式从 MBR/Legacy 换到了 GPT/UEFI。
  • 文件系统从 ext4 换到了跨设备的 btrfs。
  • 启用了 btrfs on LUKS 的全盘加密。

当然,所有个过程里面是不包括装新系统的,前后的系统还是一模一样的系统~

总体流程如下:

  • 备份系统
  • 创建新分区表和文件系统
  • 还原系统
  • 配置启动与引导程序

最后的本本两块硬盘的存储结构是这样:

+---------------------------------------------------------------------+
|EFI System|Root                                  |Swap               |
|/boot/efi |/                                     |Swap               |
|FAT32     |Btrfs                                 |Swap!!!            |
|          +-------------------+------------------+-------------------+
|          |LUKS container     |LUKS container    |LUKS container     |
|          |/dev/mapper/cryroot|/dev/mapper/cryext|/dev/mapper/cryswap|
+----------+-------------------+------------------+-------------------+
|Partition |Partition          |Partition         |Partition          |
|/dev/sdb1 |/dev/sdb2          |/dev/sda1         |/dev/sda2          |
+----------+-------------------+------------------+-------------------+
|20GB SSD                      |500GB HDD                             |
|GPT                           |GPT                                   |
|/dev/sdb                      |/dev/sda                              |
+---------------------------------------------------------------------+

由于之前饱受 20GB 根目录(系统装在了那个 20G 的 SSD 上)的限制,随着系统更新,软件体积的缓慢变大,根目录的分区长期不足 1G 剩余空间,所以我使用 btrfs 将两块盘上的空间连起来做根目录了。

注意:在原味的 encrypt HOOK 支持引导时解密多个 LUKS container 之前,让系统分区横跨一个以上的 LUKS container 这种事情务必三思,后文有述因此造成的麻烦。

这并不是唯一的全盘加密(即包括系统本身也加密)方案,更多的可参考 dm-crypt/Encrypting an entire system - ArchWiki,我采用方案主体与其中介绍的 Btrfs subvolumes with swap 相似。

备份系统

既然不想重装系统,那肯定就先要备份系统咯,这整个过程其实相当于完成了一次系统的迁移,可以参考Arch Wiki 上关于迁移系统的介绍

我直接把原来本本的数据备份到了另一台电脑上,在局域网下通过网络来传输数据,确保一条尽量快的传输备份数据数据链路能够在这一步少花很多时间,让速度瓶颈卡在硬盘 IO 上是最好不过的~

先确保 sshd 允许 root 用户登录:/etc/ssh/sshd_config 中加入 PermitRootLogin yes。因为要备份整个系统需要 root 权限。

然后在待备份的电脑上启动 sshd:#systemctl start sshd

接着在另一台电脑上创建好用来保存备份的数据的目录,用 ssh 直接 dd 硬盘数据到本地,这样好处是不会受制于文件系统在小文件传输的时候奇慢无比的问题,只要另一头有足够的存储空间就行了:

ssh root@10.50.136.233 "dd if=/dev/sda" | dd status=progress of=~/onmybackup/sda.img
ssh root@10.50.136.233 "dd if=/dev/sdb" | dd status=progress of=~/onmybackup/sdb.img

备份完数据之后,就可以关掉本本,把启动改成 UEFI,然后插个 Arch 的 liveusb 上去,然后继续开着 sshd 待命就可以了,使用 ssh 可以避免在两个机器的输入设备之间来回切换,何况我现在用着 Colemak,本本那个 qwerty 光是开机敲敲 dhcpcd 都已经很抓狂了(

创建新分区表和文件系统

现在准备开始硬盘擦除,在两块硬盘上开俩随机密钥的 LUKS container

cryptsetup open --type plain /dev/sda containera --key-file /dev/random
cryptsetup open --type plain /dev/sdb containerb --key-file /dev/random

通过 fdisk -l 应该可以看到 /dev/mapper/ 下多了 containeracontainerb,现在的存储结构是这样:

+----------+-------------------+------------------+-------------------+
|LUKS container                |LUKS container                        |
|/dev/mapper/containera        |/dev/mapper/containerb                |
+----------+-------------------+------------------+-------------------+
|20GB SSD                      |500GB HDD                             |
|GPT                           |GPT                                   |
|/dev/sdb                      |/dev/sda                              |
+---------------------------------------------------------------------+

接下来直接向刚创建的两个 LUKS container 用 0 填充,就能够完成擦除硬盘了,因为 LUKS 容器的密钥是 /dev/random ,所以对其写 0 的时候实际到硬盘上是随机的数据。

dd if=/dev/zero of=/dev/mapper/containera status=progress
dd if=/dev/zero of=/dev/mapper/containerb status=progress

完成之后关闭刚刚开启的两个 LUKS container。

cryptsetup close containera
cryptsetup close containerb

接下来就是用 fdisk/dev/sda/dev/sdb 上创建新的分区表和分区了。这样就有了:

+----------+-------------------+------------------+-------------------+
|Partition |Partition          |Partition         |Partition          |
|/dev/sdb1 |/dev/sdb2          |/dev/sda1         |/dev/sda2          |
+----------+-------------------+------------------+-------------------+
|20GB SSD                      |500GB HDD                             |
|GPT                           |GPT                                   |
|/dev/sdb                      |/dev/sda                              |
+---------------------------------------------------------------------+

除了 /dev/sdb1 是给 EFI 用之外,剩下的分区全都是要加密的,这次给要加密的分区创建 LUKS 头和密钥:

cryptsetup luksFormat /dev/sdb2
cryptsetup luksFormat /dev/sda1
cryptsetup luksFormat /dev/sda2

更多关于 luksFormat 的选项参见 dm-crypt/Device encryption - ArchWiki#Encryption options for LUKS mode, 我的本本只有俩 USB 口,一个插着网卡,一个插着 U 盘,所以就用默认的手输密码作为密钥了 Q_Q

接下来“打开”三块加密的分区,期间会被要求输入刚刚设置 LUKS 的密码:

cryptsetup open --type luks /dev/sdb2 cryroot
cryptsetup open --type luks /dev/sda1 cryext
cryptsetup open --type luks /dev/sda2 cryswap

现在存储结构就变成了下面这样,再创建好文件系统就可以开始用啦~

+----------+-------------------+------------------+-------------------+
|          |LUKS container     |LUKS container    |LUKS container     |
|          |/dev/mapper/cryroot|/dev/mapper/cryext|/dev/mapper/cryswap|
+----------+-------------------+------------------+-------------------+
|Partition |Partition          |Partition         |Partition          |
|/dev/sdb1 |/dev/sdb2          |/dev/sda1         |/dev/sda2          |
+----------+-------------------+------------------+-------------------+
|20GB SSD                      |500GB HDD                             |
|GPT                           |GPT                                   |
|/dev/sdb                      |/dev/sda                              |
+---------------------------------------------------------------------+

首先是 EFI 用的分区:mkfs.fat -F32 /dev/sdb1

然后在 cryroot 和 cryext 上创建一个 btrfs 分区,-d single 会让后面的设备的存储空间跟纸带一样连在一起,创建出来的分区容量是简单的相加:

mkfs.btrfs -d single /dev/mapper/cryroot /dev/mapper/cryext

swap 就简单啦:mkswap /dev/sda2

接下来就是挂载上这些分区了,由于 cryroot 和 cryext 是一个分区,所以随便挂哪个都行。

mount /dev/mapper/cryroot /mnt
mkdir /mnt/boot/efi
mount /dev/sdb1 /mnt/boot/efi

这样最终需要的存储结构就已经完成,可以开始往里面写数据啦~

还原系统

回到刚刚存了备份数据的系统,打开刚备份的硬盘镜像,(需要 root 用户来执行),然后在 Dolphin 上戳一下新出现的一个个分区就挂载上了。

losetup -P /dev/loop0 ~/onmybackup/sda.img
losetup -P /dev/loop1 ~/onmybackup/sdb.img

接着记得保持用 root 用户,从根目录开始,到每一个分区的根目录去,把分区的内容 rsync 回本本系统的对于目录。

可以通过 rsync 的 --exclude 排除一些没必要恢复的目录,比如缓存目录什么的,这样可以减少一点传输时间。

rsync -SPAaXr PATH_TO_ROOT root@10.50.136.233:/mnt/
rsync -SPAaXr PATH_TO_HOME root@10.50.136.233:/mnt/home/
#...

配置启动与引导程序

现在系统已经恢复了,在本本上,直接 chroot 到恢复的系统:

arch-chroot /mnt

相比与原来的系统,现在的变化就是分区变了,以及启动方式改变了,引导也没了。

由于是系统自己都被加密了,所以需要额外的步骤来让引导程序启动系统。

由于开头提到在系统分区跨多个 LUKS container 未受到 encrypt HOOK 直接支持的问题。这里我使用了 aur/mkinitcpio-multiencrypt 提供的 HOOK 来解决这个问题,缺点是,目前只支持手打密码的认证方式 Q_Q 系统分区没有跨越多个 LUKS container 的话,就直接用 encrypt 这个 HOOK 就行了。

/etc/mkinitcpio.conf 的 HOOKS 行中,加入 multiencrypt ,以及为了能用键盘输密码,确保 keyboard 在它前面。

在 Grub 的配置 /etc/default/grub 中添加 HOOK 对应参数,如果 GRUB_CMDLINE_LINUX 已经有别的参数,则用空格与其隔开即可,如下可见参数格式为 "<device>:<device mapper name>;<device>:<device mapper name>;...",因为这个 HOOK 支持多设备,所以我就顺便把全部用到的设备都写上了,这样之后就不用单独创建一个 crypttab 来做这个事情了 (っ'ω'c)

GRUB_CMDLINE_LINUX="cryptdevices=/dev/sdb2:cryroot;/dev/sda2:cryext;/dev/sda1:cryswap"

配置搞定之后,就可以生成启动镜像和引导配置咯,顺便装上引导程序,mkinitcpio 的内核名字参照自己用的就好:

mkinitcpio -p linux-zen
grub-mkconfig -o /boot/grub/grub.cfg
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=grub

关于处理分区格局发生变化的最后一件事,就是生成新的 fstab 让系统启动的时候正确挂载分区咯,先退出 chroot 环境,然后:

genfstab /mnt >> /mnt/etc/fstab

再检查一下生成的 fstab 里面如果有 live 系统的 U 盘的话将其删掉就好了。

最后重启电脑,拔掉 U 盘,原味的系统又回来了,只是现在启动要多输点密码 ( ⚆ _ ⚆ )

参考

| Posted by frantic1048

测试 Redux 应用中使用 window.fetch 的 API 请求

近来写博客程序的时候需做到请求文章的功能,用的是新的 Fetch API 做的请求,用了好几个工具要么不能用要么尚只支持 XMLHttpRequest (´_`),如果你还不知道 Fetch API,可以通过这篇 Introduction to fetch() 品尝一下 (っ╹ ◡ ╹ )っ

测试环境是 Karma/Jasmine 的组合,尝试了如下几种工具/方法:

当然,不能因为这点问题就放弃治疗 ~(>_<~),后来翻到 RJ Zaworski 的Testing API requests from window.fetch 这篇文章,直接用 sinon.stub 来吃掉 window.fetch,还是蛮好用的。RJ Zaworski 已经介绍了最小化的测试写法,下面就搭着 Redux 一起上啦。

首先,就着 Redux 文档对异步 Action 的介绍写获取文章的异步 Action Creator:

actions.js

// 引入 Fetch API 的 polyfill,

// 确保在遇到不支持的浏览器上一切正常运行。

// https://github.com/matthew-andrews/isomorphic-fetch/

import 'isomorphic-fetch';

export const types = {
  FETCH_POST_REQUEST: 'FETCH_POST_REQUEST',
  FETCH_POST_SUCCESS: 'FETCH_POST_SUCCESS',
  FETCH_POST_FAILURE: 'FETCH_POST_FAILURE',
};

// 发起文章请求的 action,

// 包含一个 postId 标识准备请求哪篇文章,

// 可用于在 store 中标识拉取状态。

const fetchPostRequest = (postId) => ({
  type: types.FETCH_POST_REQUEST,
  payload: postId,
});

// 成功接收文章的 action,

// 包含一个 post 属性,里面存储了接收的文章内容以及 postId。

const fetchPostSuccess = ({id, content}) => ({
  type: types.FETCH_POST_SUCCESS,
  payload: {id, content},
});

const fetchPostFailure = ({id, failedResponse}) => ({
  type: types.FETCH_POST_FAILURE,
  payload: {id, failedResponse},
});

// 用来发请求的 action creator 返回一个函数,

// 称之为 thunk action,会被 Redux Thunk 中间件接手

// 使用的时候和普通 action 一样:

// store.dispatch(fetchPost(postId))

export const fetchPost = (postId) => (dispatch) => {

  // 这个返回的函数会被 Redux Thunk 中间件执行,

  // 同时会在第一个参数接收到 redux store 的 `dispatch` 方法,

  // 从而可以在这里面自己触发 action。


  // 先触发一个准备发请求的 action,

  dispatch(fetchPostRequest(postId));

  return fetch(`/posts/${postId}.md`)
    .then(response => response.text())
    .then(content => fetchPostSuccess({id: postId, content}))
    .catch(failedResponse => fetchPostFailure({id: postId, failedResponse}));
}

Action Creator 有了之后,就可以来写 Jasmine 中的测试了:

actionTest.js

// 用 redux-mock-store 来模拟一个 store 并检查是否触发了预期的 action

// https://github.com/arnaudbenard/redux-mock-store

import configureMockStore from 'redux-mock-store';

// 用来处理我们的 thunk action 的中间件

// https://github.com/gaearon/redux-thunk

import thunk from 'redux-thunk';

// 用 sinon 来伪造 fetch API。

// 如果在 Karma 配置中设定了 frameworks: [... ,'sinon'],

// 则不需要再写这句 import 了。

// http://sinonjs.org/

import sinon from 'sinon';

// 上面写的 actions.js

import {types, fetchPost} from '../actions';

const middlewares = [ thunk ];
const mockStore = configureMockStore(middlewares);

describe('Post fetching', () => {
  // 用作测试的文章 id 及其内容

  const testPostId = 223;
  const testPostContent = 'Gochuumon wa usagi desu ka ???';

  describe('succeed', () => {
    // 创建一个包含文章内容的成功响应

    const res = new window.Response(testPostContent, {
      status: 200,
      headers: { 'Content-type': 'text/plain' },
    });

    beforeAll(() => {
      // 把原来的 fetch 包成 stub,

      // 这样做原来的 window.fetch 就不会被调用了

      sinon.stub(window, 'fetch');

      // 这里调用的 window.fetch 已经是 sinon 的 stub function 了,

      // 可以通过 withArgs 与 returns 方法

      // 来指定函数在接受到什么参数的时候返回什么。

      // 这里指定请求正确的文章 URL 的时候

      // 返回对应的请求成功的响应

      window.fetch
        .withArgs(`/posts/${testPostId}.md`)
        .returns(Promise.resolve(res));
    })

    afterAll(() => {
      // 执行完这组用例后恢复原来的 window.fetch 函数。

      window.fetch.restore();
    })

    it('should FETCH_POST_SUCCESS with post content', (done) => {
      // 期望的发起请求的 action

      const actRequest = {
        type: types.FETCH_POST_REQUEST,
        payload: testPostId,
      };

      // 期望的请求成功的 action

      const actSuccess = {
        type: types.FETCH_POST_SUCCESS,
        payload: {id: testPostId, content: testPostContent},
      };

      const expectedActions = [
        actRequest,
        actSuccess,
      ];

      // store 的初始状态

      const initialState = {};

      // 如果触发了期望的 action 的话,

      // done 会被调用,表明这个测试用例通过了。

      const store = mockStore(initialState);

      // 准备好模拟的 store 后,

      // 触发请求文章的 action ~

      store.dispatch(fetchPost(testPostId))
        .then(() => {
          expect(store.getActions())
            .toEqual(expectedActions);
        })
        .then(done)
        .catch(done.fail);
    });
  });

  describe('failed', () => {
    // 和请求成功的情况类似,先创建一个失败的响应比如 404。

    const res = new window.Response('', { status: 404 });

    beforeAll(() => {
      sinon.stub(window, 'fetch');
      window.fetch
        .withArgs(`/posts/${testPostId}.md`)
        .returns(Promise.reject(res)); // 失败的请求应该用 Promise.reject()

    });

    afterAll(() => {
      window.fetch.restore();
    });

    it('should FETCH_POST_FAILURE with errored response', (done) => {
      const actRequest = {
        type: types.FETCH_POST_REQUEST,
        payload: testPostId,
      };

      const actFailure = {
        type: types.FETCH_POST_FAILURE,
        payload: {id: testPostId, failedResponse: res},
      };

      const expectedActions = [
        actRequest,
        actFailure,
      ];

      const store = mockStore({});
      store.dispatch(fetchPost(testPostId))
        .then(() => {
          expect(store.getActions())
            .toEqual(expectedActions);
        })
        .then(done)
        .catch(done.fail);
    });
  });
});

就酱 ~(>_<~)

| Posted by frantic1048

解决 Linux 4.5 接上 Wacom CTL 471 冻结系统的问题

在 Linux 4.3.3 升级 4.4 的时候就遇到这个问题了,系统只要碰着 Wacom CTL 471 板子就立马冻住,或者是提笔就冻,这时候只能强制按电源重启。

当时太懒就一时把内核滚回了 4.3.3 好长时间,最近滚上 4.5 了想着问题大概没有了,然而还是太天真,开机就给我冻住……于是终于不能忍了,得搞定一下才行。顺带记下打内核补丁的姿势。

  • 发行版: Arch Linux
  • 出现问题对应的 linux 版本:linux-4.5.1-1-x86_64
  • Wacom 驱动版本:xf86-input-wacom-0.32.0-1-x86_64
  • Wacom 数位板型号:Wacom CTL-471

先顺着 Wacom Tablet - Arch Wiki 上关于 System Freeze 的解决方案,说是打个内核补丁就好,于是我找到了对应的内核补丁:[PATCH v2] hid: usbhid: hid-core: fix recursive deadlock,然后到后面我准备好源码的时候却发现 4.5 内核已经有这个 patch 了,也就是解决问题这个 patch 还不够。

一路搜索找到 #311 Kernel panic, system freeze since kernel4.4 til 4.5rc6
,问题描述和我的问题非常一致,仔细读下去后闻到了到问题解决的味道,顺着回复跳到了 Bug 1317116 - System freeze when Wacom One Tablet (CTL-471) is attached,读下去看来那个 [PATCH] HID: wacom: fix Bamboo ONE oops 就是解决问题的 patch 了,于是一把复制下来:

fix-Bamboo-ONE-oops.patch
--- a/drivers/hid/wacom_wac.c
+++ b/drivers/hid/wacom_wac.c
@@ -2426,6 +2426,17 @@ void wacom_setup_device_quirks(struct wacom *wacom)
     }
 
    /*
+   * Hack for the Bamboo One:
+    * the device presents a PAD/Touch interface as most Bamboos and even
+    * sends ghosts PAD data on it. However, later, we must disable this
+    * ghost interface, and we can not detect it unless we set it here
+    * to WACOM_DEVICETYPE_PAD or WACOM_DEVICETYPE_TOUCH.
+    */
+   if (features->type == BAMBOO_PEN &&
+       features->pktlen == WACOM_PKGLEN_BBTOUCH3)
+       features->device_type |= WACOM_DEVICETYPE_PAD;
+
+   /*
      * Raw Wacom-mode pen and touch events both come from interface
     * 0, whose HID descriptor has an application usage of 0xFF0D
     * (i.e., WACOM_VENDORDEFINED_PEN). We route pen packets back
-- 

接着装个 abs,它说用来获取 ABS tree 的脚本,ABS tree 是一个树状的包含了 Arch 所有软件包源文件的目录(总之还是看 Arch Build System - Arch Wiki 吧),用它来获取构建 linux 包所需的源文件(像 PKGBUILD 之类的),然后从 abs 同步下来的 /var/abs/ 里面取出 linux 包的目录,复制到准备对 Linux 进行 魔改 打补丁的目录:

sudo pacman -S abs
sudo abs
sudo abs linux
cp -r /var/abs/core/linux ~/arch_packages/

为了确认一下 patch 是没被打上的想先看看源码,先不急着开始构建,把所有源代码先准备好:

makepkg -o # 这个选项下 makepkg 只会下载校验解压好所有源代码/资源文件

结果遇到了 gpg 验证过不去,顺着错误原因搜了一下,将这俩人(Linus TorvaldsGreg Kroah-Hartman)的 gpg 公钥加入自己的 gpg 配置里面就好了,比较懒的话可以直接用带 GUI 的 Kleopatra 完成这一步,在其 Directory Services 的设置里面加入 http://pgp.mit.edu 这个服务器(确保能搜到那俩 key),然后在主界面戳 Lookup Certificates on Server,搜索 key 的后八位(Key ID),比如 00411886,就能够找到对应的证书啦,一键 import 之。

==> Verifying source file signatures with gpg...
    linux-4.5.tar ... FAILED (unknown public key 79BE3E4300411886)
    patch-4.5.1 ... FAILED (unknown public key 38DBBDC86092693E)

然后翻开看了一下 /drivers/hid/wacom_wac.c 对应的 2426 行附近,嗯,patch 没被打上的。把从 bugzilla 那边扒下来的 fix-Bamboo-ONE-oops.patch 放到和 PKGBUILD 相同目录下,然后在 PKGBUILD 里面添加上这个文件和对应的 shasum,并在 prepare() 里面 patch 它,由于个人口味顺带自定义了一下内核名字(修改 pkgbase 的值)。PKGBUILD 的开头看起来像这样(这不是完整的 PKGBUILD!,需要完整的源文件请跳到文末):

PKGBUILD
# $Id: PKGBUILD 265148 2016-04-19 06:55:26Z tpowa $

# Maintainer: Tobias Powalowski <tpowa@archlinux.org>

# Maintainer: Thomas Baechler <thomas@archlinux.org>


#pkgbase=linux               # Build stock -ARCH kernel

pkgbase=linux-Kafuu       # Build kernel with a different name

_srcname=linux-4.5
pkgver=4.5.1
pkgrel=1
arch=('i686' 'x86_64')
url="http://www.kernel.org/"
license=('GPL2')
makedepends=('xmlto' 'docbook-xsl' 'kmod' 'inetutils' 'bc')
options=('!strip')
source=("https://www.kernel.org/pub/linux/kernel/v4.x/${_srcname}.tar.xz"
        "https://www.kernel.org/pub/linux/kernel/v4.x/${_srcname}.tar.sign"
        "https://www.kernel.org/pub/linux/kernel/v4.x/patch-${pkgver}.xz"
        "https://www.kernel.org/pub/linux/kernel/v4.x/patch-${pkgver}.sign"
        # the main kernel config files

        'config' 'config.x86_64'
        # standard config files for mkinitcpio ramdisk

        'linux.preset'
        'change-default-console-loglevel.patch'
        'fix-Bamboo-ONE-oops.patch')

sha256sums=('a40defb401e01b37d6b8c8ad5c1bbab665be6ac6310cdeed59950c96b31a519c'
            'SKIP'
            '060ad091ebfa2b63d62e86beaf68c3a5d4638c506c3ac941c1825ba756e830b1'
            'SKIP'
            '8a8a955f015ee8342701231a63836cec0e300fd7e96d30e8696fde8a383fcdc9'
            '8b60911aad591306336e300e27071f2d108c5016e66a04327b82ac69acbfefff'
            'f0d90e756f14533ee67afda280500511a62465b4f76adcc5effa95a40045179c'
            '1256b241cd477b265a3c2d64bdc19ffe3c9bbcee82ea3994c590c2c76e767d99'
            '7dec753db812bd8a268e50d97cb1704b4ed097746e423720f6872ecf4a14e78c')
validpgpkeys=(
              'ABAF11C65A2970B130ABE3C479BE3E4300411886' # Linus Torvalds

              '647F28654894E3BD457199BE38DBBDC86092693E' # Greg Kroah-Hartman

             )

_kernelname=${pkgbase#linux}

prepare() {
  cd "${srcdir}/${_srcname}"

  # add upstream patch

  patch -p1 -i "${srcdir}/patch-${pkgver}"

  # add latest fixes from stable queue, if needed

  # http://git.kernel.org/?p=linux/kernel/git/stable/stable-queue.git


  # set DEFAULT_CONSOLE_LOGLEVEL to 4 (same value as the 'quiet' kernel param)

  # remove this when a Kconfig knob is made available by upstream

  # (relevant patch sent upstream: https://lkml.org/lkml/2011/7/26/227)

  patch -p1 -i "${srcdir}/change-default-console-loglevel.patch"

  # Fix System freeze with Wacom Bamboo One Tablet

  # Relevant patch: https://bugzilla.redhat.com/show_bug.cgi?id=1317116#c11

  patch -p1 -i "${srcdir}/fix-Bamboo-ONE-oops.patch"

  if [ "${CARCH}" = "x86_64" ]; then
    cat "${srcdir}/config.x86_64" > ./.config
  else
    cat "${srcdir}/config" > ./.config
  fi

  if [ "${_kernelname}" != "" ]; then
    sed -i "s|CONFIG_LOCALVERSION=.*|CONFIG_LOCALVERSION=\"${_kernelname}\"|g" ./.config
    sed -i "s|CONFIG_LOCALVERSION_AUTO=.*|CONFIG_LOCALVERSION_AUTO=n|" ./.config
  fi

  # set extraversion to pkgrel

  sed -ri "s|^(EXTRAVERSION =).*|\1 -${pkgrel}|" Makefile

  # don't run depmod on 'make install'. We'll do this ourselves in packaging

  sed -i '2iexit 0' scripts/depmod.sh

  # get kernel version

  make prepare

  # load configuration

  # Configure the kernel. Replace the line below with one of your choice.

  #make menuconfig # CLI menu for configuration

  #make nconfig # new CLI menu for configuration

  #make xconfig # X-based configuration

  #make oldconfig # using old config from previous kernel version

  # ... or manually edit .config


  # rewrite configuration

  yes "" | make config >/dev/null
}

接下来就可以开始构建内核了,为了更快一点,可以开个 -j8 什么的:

MAKEFLAGS="-j8" makepkg

我可怜的小本本(CPU i5 3337U)跑了有四个小时才跑完,如果你的处理器不是很厉害的话,最好确保一下电源的稳定确保跑过这场马拉松。

跑完之后会构建出三个包,根据设置的 pkgbase 不同,包名也会有所不同,一把安装之即可:

sudo pacman -U linux-Kafuu-4.5.1-1-x86_64.pkg.tar.xz linux-Kafuu-docs-4.5.1-1-x86_64.pkg.tar.xz linux-Kafuu-headers-4.5.1-1-x86_64.pkg.tar.xz

接着别急着重启,先刷新一下引导,在启动项中添加新内核的入口,grub 会自动搞定这件事情:

sudo grub-mkconfig

然后戳开 /boot/grub/grub.cfg 瞅瞅刚添加的内核是不是在启动菜单的第一个选项(看下第一个 menuentry 下是不是有 linux-Kafuu 即可),这样开机的时候直接回车进第一个就是刚刚新鲜的内核了,如果不是的话,那么应该可以在下面的 submenu 里面找到~

最后重启电脑,启动新的内核,接上 Wacom CTL 471 的数据线,世界和平!系统没有再被冻结啦,撒花!

关于打了 patch 的 linux 包源文件戳 gist 链接,你可以下下来 makepkg 直接构建出文中所述没有问题的内核。

另一消息是那个 patch 会并入 linux 4.6,我检查了一下 Arch Linux 中文社区源的 linux-mainline (当前版本是 4.6-rc5)之后发现果然那个 patch 已经打进去了。如果不想自己编译内核的话,可以尝试一下这个包。

| Posted by frantic1048

Mangekyou 开发小记

最近搞数字图像处理作业,需要图形界面,遂又用 Electron 搞了一发,可桌面可浏览器的感觉真的很爽呢。稍微记录下经验吧。

因为是数字图像,想着想着就脑洞到了万花筒写轮眼,于是项目就用 Mangekyou 做名字了(取个名字真是艰难)。

总览:

  • 源代码:100% ES6
  • 构建控制:Gulp
  • 模块绑定:Webpack
  • 架构:Flux(Facebook 的实现)
  • 界面库:React
  • 界面组件库:Material UI
  • 测试:人形自走测试框架

读取和存放图片数据

为了让程序能够不用修改跑浏览器上,我没用 Node.js 的文件接口,而是用浏览器的 <input> 元素来做输入,很方便啊!自带 MIME 过滤,自带系统的文件对话框,省了好多事情,监听一下 Change 事件,然后用 FileReader 把图片数据读进来这个工作就完成啦~

关于存放数据,我最开始是解析成 ImageData 存放的,结果后面发现这玩意儿怎么用怎么别扭,而且 canvas 的 putImageData() 竟然比 drawImage() 少了俩参数,没了自动缩放的支持,而且绘制还慢。而 canvas 很方便转换不说,还没那么慢,所以就开心用 canvas 来存放图像咯。

实现起来像这样子:

<input
  type="file"
  multiple
  accept="image/*"
  onchange={handleFile}
/>
function handleFile() {
  function extractDataAndDoSomething(f) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const img = new Image();
    const fr = new FileReader();
    img.onload = () => {
      canvas.setAttribute('width', img.width);
      canvas.setAttribute('height', img.height);
      ctx.drawImage(img, 0, 0);

      // store loaded image.

      storeMyImage(canvas);
    };
    fr.onload = () => { img.src = fr.result; };
    fr.readAsDataURL(f);
  }
  for (const eachFile of new Array(...this.refs.fileInput.files)) {
    extractDataAndDoSomething(eachFile);
  }
}

用 canvas 而非 <img> 绘图

因为有实现一个历史列表,每项都有历史记录的小图,结果我就干出了用 <img> 去展示那堆小图的蠢事,我把 canvas 的数据转成 DataURL 赋值给 <img> 的 src 属性,因为 <img> 有自动调整图像显示大小的功能,然后发现历史列表更新时真是卡的可以,录了一下性能信息,发现更新历史列表要花个几千毫秒,其中 toDataURL() 耗费了巨量时间,结果后来手写缩放用 canvas 来绘图耗时直接缩短到几十毫秒 (´_`)。

正确使用 React 组件的 key 属性

用数组之类的东西动态生成一堆组件的时候,React 会提示要你提供一个 key 属性,这个是 React 用来标记每个组件谁是谁从而能正确处理更新的,这玩意儿没正确使用的话,就会有类似该更新的元素不更新一类的事情发生。另外得确保提供的 key 是和数据项一对一的,像一个变动的数组的下标就不适合做 key,因为不同的时候同一个下标值可能是不同的数据,结果就会造成界面那边更新的时候看起来和数据不一致,最好在存放成堆的数据的时候就给它们顺带打上个 key 属性,如果懒得想 key 怎么生成的问题,用 performance.now() 这家伙吧,它能在同一次会话输出增序的时间戳。

用 Generator 帮助遍历 ImageData

处理图像数据的时候经常有需要遍历像素的操作,时不时又跟坐标值相关,而 ImageData 里面的像素是个一维数组不说,还是 r, g, b, a 展开排列的,每次手写二重循环一点很是麻烦,这时 ES6 的 Generator 就派上用场啦~

比如写个获取图片所有像素的坐标和 ImageData 中的索引值的函数:

function getAllPositions(width, height) {
  return function* pos() {
    for (let y = 0; y < height; ++y) {
      for (let x = 0; x < width; ++x) {
        yield [x, y, y * width * 4 + x * 4];
      }
    }
  };
}

然后就可以用 for...of 直接遍历了:

const allPos = getAllpositions(imageData.width, imageData.height);
for (const [x, y, index] of allPos()) {
   imageData.data[index];     // Red

   imageData.data[index + 1]; // Green

   imageData.data[index + 2]; // Blue

   imageData.data[index + 3]; // Alpha

}

在 Web Worker 里面使用 ES6 Module

为了避免卡界面太厉害,我把关于图像计算工作丢给了 Web Workers 处理,使用的时候发现即使是 Electron 环境下,它也是没有 require 之类的模块相关功能,而只能用那个看起来很捉计的 importScripts() 来导入外部文件,不过有 webpack 在,把 worker 部分程序的入口文件交给 webpack 绑定一下,就可以在 worker 代码里面用 import 导入模块了,也避免了用 importScripts() 造成 ESLint 疯狂报变量未声明/未使用的警报。

具体的实现可参考 Mangekyou 的 gulpfile 中的配置与对应的 worker 的代码的组织方式。

用 transferable objects 更快地传递 worker 的数据

默认的 postMessage() 是用的结构化拷贝的方式创了一个数据的副本传递到 worker/主线程的,想想都会有点费时间,另外有种方法是可以直接移交数据的所有权到另一个线程,从而少了一步复制,这样会让数据传递更快一些,jsPerf 上有对于这两个方式的速度对比,提升还是挺大的。

postMessage() 第二个参数是要移交的变量的数组,对于数组的话,只能移交 ArrayBuffer(可以通过数组的 buffer 属性获得),所以以 transferable object 的方式传递 ImageData 的数据是这个样子:

self.postMessage({
  width: image.width,
  height: image.height,
  buffer: image.data.buffer,
},[image.data.buffer]);

然后在接收数据的那端将其重新包装成 ImageData 进行后续操作:

function onMessage({data}) {
  const imgd = new ImageData(
    new Uint8ClampedArray(data.image.buffer),
    data.image.width,
    data.image.height
  );

  // do somthing with recived imageData~

}

React 的 setState() 的奇怪的更新行为

原以为 setState() 是像 Object.assign() 类似的方式更新 state 的,结果又不完全是;最后发现是自己没仔细看 setState() 的文档shallow merge 在那儿摆着 (´_`)

比如 state 原本是 {kotori: 0, honoka: {x: 1, y: 2}}

this.setState({kotori: 3})没有什么问题,只有 kotori 被更新了,honoka 还是原来的值。

this.setState({honoka: {x: 2}}) 的话,奇怪的就来了,honoka被整个替换成 {x: 2}y 属性就这么飞了。

做这种更新的时候用 Object.assign() 之类的手段确保不会发生这样的意外,爆栈上也有介绍这个问题的解决方案

this.setState({
  honoka: {
    ...this.state.honoka,
    x: 2,
  },
})

canvas 用的颜色空间不像是 sRGB

在写 Rec. 709 Luma 的计算的时候,找不到资料关于 canvas 到底用的什么颜色空间,因为 sRGB 在网页上是如此的通用,所以先写了带 sRGB Gamma 校正的灰度化算法,然后丢进 Krita 里面去发现并不科学,然后去掉 Gamma 校正之后就正确了,尝试了 Chromium 47.0.2526.73 (64-bit)
,Firefox 44.0a2 (2015-12-06) 结果均是如此,目前来看,直接把 canvas 里面的颜色值当线性 RGB 值处理就可以了。

<a> 标签触发浏览器下载

经过一番折腾,发现如果新创建 <a> 标签不插到 document 里面去的话,各种调整都无法保证 Electron、Chromium、Firefox 里面都能成功触发下载,最后尝试了插入 document 再模拟点击,才终于获得大统一 _(:з」∠)_:

function handleExportImage() {
  const a = document.createElement('a');
  a.setAttribute('download', 'proceed.png');
  a.setAttribute('href', canvas.toDataURL());
  a.setAttribute('style', 'position: fixed; width: 0; height: 0;');

  const link = document.body.appendChild(a);
  link.click();
  document.body.removeChild(link);
}

噢对,Mangekyou 源代码传送:https://github.com/frantic1048/mangekyou

继续写 (ง •̀_•́)ง

| Posted by frantic1048

解决一个诡异的 MySQL Workbench 崩溃问题

用 MySQL Workbench 的时候戳了一下选项卡的关闭按钮,它就直接崩溃了。

之后启动 MySQL Workbench 的时候出现以下错误直接崩溃,无论重启动系统,或重新安装它均无法解决。

*** Error in `/usr/bin/mysql-workbench-bin': double free or corruption (out): 0x00000000011a5f00 ***

最后,网上搜到了这个神奇的操作一下子解决了问题,MySQL Workbench 能启动了。

rm ~/.mysql/workbench/wb_options.xml

然而发现之前做了半天的 .mwb 文件打不开了,各种姿势用 MySQL Workbench 打开各种姿势闪退,然后又得执行上面的删除操作才能启动。最后排查发现打开的文件路径全英文的话就没事儿了 (╯°□°)╯︵ ┻━┻

| Posted by frantic1048

打造一个基于 Node.js 的 web API 工程

最近被强上服务端,记录个创建过程(什么时候有个 IDE 能用(..•˘_˘•..))。流水帐警报。

此次目的是建立一个只提供 API 的服务器,所以前端什么的就不用管了,直接上 Node.js 搞。关于各项工具的进一步使用,建议顺着下面的链接查阅对应文档,总览如下:

  • 服务器:Express@4.12.3
  • 数据库:MongoDB@3.0.7
  • 测试框架:Jasmine@2.3.2
  • API 测试工具:hippie@0.4.0
  • 测试覆盖率报告:Istanbul@0.3.22
  • 工作流:Gulp@3.9.0
  • 版本管理:git@2.6.1(Github)
  • Lint:eslint@1.6.0
  • 编译(ES6 -> ES5):Babel@5.8.25
  • 源代码:
    • 程序:JavaScript(ES6)
    • 测试及其它:JavaScript(ES5)
  • 在线平台:

最终的项目目录结构看起来是这样子的:

.
├── build                  // ES6 代码编译输出目录
├── .codeclimate.yml       // Code Climate 配置
├── config.js              // 程序自己的配置
├── coverage               // Istanbul 生成的测试报告目录
├── .eslintrc              // ESLint 配置
├── .git                   // git 目录
├── gulpfile.js            // Gulp 配置
├── mongodb.conf           // MongoDB 配置
├── node_modules           // NPM 安装的依赖模块目录
├── package.json           // 程序自己的 NPM 包信息
├── data                   // MongoDB 数据库目录
│   └── mongodb           
├── src                    // 源代码目录
│   └── app.js            
├── test                   // 测试程序目录
│   └── spec              
│       └── yahaloSpec.js
└── .travis.yml            // Travis CI 配置

创建 NPM package

先在 Github 创建新的仓库。

写一个 Node.js 程序的例行,给项目创建 package.json 以管理依赖。按照命令提示填写即可。

npm init

用 gulp 管理程序所有入口,在 pakcage.json 中 scripts 字段如下填写。

"scripts": {
    "test": "gulp",
    "dev": "gulp dev",
    "run": "gulp run"
}

编写 gulpfile.js

共用变量

将一些常用目录写在变量中,会比较容易管理。

var appSrc = 'src/**/*.js'; // 程序源代码

var appDest = 'build/**/*.js'; // 编译输出的文件

var appDestPath = 'build'; // 编译输出目录

var testSrc = ['test/spec/*Spec.js']; // 测试程序源代码

var server = null; // 用来保存 http 服务器实例,在启动服务器测试的时候

Lint

本人是直接用了 AirBnB 的 JS 规范去掉了 JSX 部分。选择自己喜欢的 .eslintrc 放在根目录就可以啦。

接着是在根目录编写 gulpfile.js,先是完成 lint 工作

var gulp = require('gulp');
var eslint = require('gulp-eslint');

gulp.task('lint', function() {
  return gulp.src(appSrc)
    .pipe(eslint({ rulePaths: ['./'] }))
    .pipe(eslint.format());
});

编译 ES6 代码

使用 Babel 将 ES6 的源代码编译到 CommonJS 规范的 ES5 代码,输出到 build 目录。

var babel  = require('gulp-babel');
var newer = require('gulp-newer');

gulp.task('compile', function() {
  return gulp.src(appSrc)
    .pipe(newer(appDestPath))
    .pipe(babel({ modules: 'common' }))
    .pipe(gulp.dest(appDestPath));
});

启动/关闭服务器

通过 Gulp 来控制服务器的开关。这里利用前面创建的 server 这个变量储存服务器实例,保证只有一个实例运行。

gulp.task('serve', function(callback) {
  server = require('./build/app');
  callback();
});

gulp.task('end-serve', function(callback) {
  if (server) {
    server.close();
    server = null;
  }
  callback();
});

测试

由于测试的是服务端程序,需要测试前先启动服务器,根据 gulp-stanbul 的说明,将任务分成以下两部分。在 pre-tsettest 任务之间启动服务器即可。

var jasmine = require('gulp-jasmine');
var SpecReporter = require('jasmine-spec-reporter');
var istanbul = require('gulp-istanbul');

gulp.task('pre-test', function() {
  return gulp.src(appDest)
    .pipe(istanbul())
    .pipe(istanbul.hookRequire());
});

gulp.task('test', function() {
  return gulp.src(testSrc)
    .pipe(jasmine({ reporter: new SpecReporter() }))
    .on('end', function() {
      // 测试跑完关闭服务器

      server.close();
      server = null;
    })
    .pipe(istanbul.writeReports());
});

监视

在开发的时候,让源代码改变的时候自动重新编译运行。而在测试程序改变的时候,重跑一遍测试。这里利用 run-sequence 来让一次而不是并行地执行 gulp 任务,在 Gulp 4.0 (参见 Migrating to gulp 4 by example - We Are Wizards Blog)中已经自带了 gulp.series 与 gulp.parallel 来控制执行次序。

var runSequence = require('run-sequence');
gulp.task('watcher-appSrc', function(callback) {
  runSequence(
    'end-serve',
    'compile',
    'pre-test',
    'serve',
    'test',
    callback
  );
});

gulp.task('watcher-testSrc', function(callback) {
  runSequence(
    'pre-test',
    'test',
    callback
  );
});

gulp.task('watch', function(callback) {
  gulp.watch(appSrc, ['watcher-appSrc']);
  gulp.watch(testSrc, ['watcher-testSrc']);
  callback();
});

串接任务

将前面的任务串起来,分别创建用于 CI 的一次性测试、开发中持续监视与作为后端运行的三个最终使用的任务。

// once

gulp.task('default', function(callback) {
  runSequence(
    ['compile', 'lint'],
    'pre-test',
    'serve',
    'test',
    'end-serve',
    callback
  );});

// develop

gulp.task('dev', function(callback) {
  runSequence(
    'compile',
    'pre-test',
    'serve',
    'test',
    'watch',
    callback
  );
});

// run server

gulp.task('run', function(callback) {
  runSequence(
    'compile',
    'serve',
    callback
  );
});

安装依赖

根据前面用到的包,以及服务端的需求,安装并保存依赖到 package.json 中去。

npm install --save express gulp mongodb mongoskin run-sequence
npm install --save-dev babel-eslint gulp-babel gulp-eslint gulp-istanbul gulp-jasmine jasmine-spec-reporter hippie gulp-newer

配置文件

用一个配置文件来保存程序配置,比如服务器端口号,创建在根目录 config.js

var config = {
  serverPort: 2333, // 服务器端口

  databaseURI: 'mongodb://localhost:27017', // MongoDB 数据库 URI

  dev: true, // 开发模式标志

};

module.exports = config;

编写测试

先编写一个最简单的 GET 请求测试,文件为 test/spec/yahaloSpec.js,服务器端口就从配置中读取。

var hippie = require('hippie');
var port = require('../../config').serverPort;

describe('yahalo Spec !', function() {
  it('should get 200 yooo', function(done) {
    hippie()
      .base('http://localhost:' + port)
      .get('/')
      .expectStatus(200)
      .expectBody('yahalo! GET!')
      .end(function(err, res, body) {
        if (err) done.fail(err);
        else done();
      });
  });
});

创建 MongoDB 配置

先为 MongoDB 创建数据库目录 data/mongodb
在根目录添加 MongoDB 配置 mongodb.conf

# See http://www.mongodb.org/display/DOCS/File+Based+Configuration for format details
# Run mongod --help to see a list of options

port = 27017
bind_ip = 127.0.0.1
httpinterface = true
rest = true
quiet = false
dbpath = data/mongodb
logpath = data/mongod.log
logappend = true

编写服务端程序

写一个最简单的只会相应 GET 请求的程序(src/app.js),同时在启动的时候连接 MongoDB。

import express from 'express';
import mongoose from 'mongoose';
import config from '../config';

const app = express();

mongoose.connect(config.databaseURI, () => {
  if (config.dev) {
    // 在开发模式运行的时候,在一开始清空数据库

    mongoose.connection.db.dropDatabase();
  }
});

app.get('/', (req, res) => {
  res.send('yahalo! GET!');
});

const server = app.listen(config.serverPort, () => {
  console.log(`my app listening at http://localhost:${server.address().port}`);

});

server.on('close', () => {
  // 在关闭服务器的时候断开数据库连接

  mongoose.connection.close();
});

export default server;

接下来在根目录执行 mongod -f mongodb.conf 启动数据库,然后直接运行 gulp,就能够看到命令行下输出的测试报告了,以及 istanbul 在 coverage 目录下生成的各种格式的报告(包括 html)。

整合 Code Climate 和 Travis CI

首先在两个平台都将项目的 Github 仓库添加上。在 Code Climate 那边选择 Engine analysis,根据提示步骤在根目录编写 Code Climate 配置文件 .codeclimate.yml

# 启用 eslint

eslint:

  enabled: true


# 设定要进行评级的代码

ratings:

  paths:

  - src/**/*.js

之后在 Code Climate 当前项目旁边的 Set Up Coverage 按钮上戳一下,在页面最底部获得用于 Travis CI 连接 Code Climate 上该项目用的 repo_token,将其写入根目录的 Travis CI 的配置文件 .travis.yml

addons:

    code_climate:

        repo_token: balabalabalashaaa

最后是接着编辑 Travis CI 的配置文件 .travis.yml,让 Travis CI 自动跑测试,同时报告测试覆盖率到 Code Climate:

# 指定程序语言

language: node_js


# 指定 node 版本,“node” 为最新的稳定版本

node_js:

  - "node"


# 启用 MongoDB

services:

  - mongodb


# 跑之前先安装依赖

install:

  - npm install codeclimate-test-reporter

  - npm install


# 执行 gulp 直接开跑

script: gulp


# 跑完报告测试覆盖率

after_script:

  - codeclimate-test-reporter < coverage/lcov.info

至此,项目在 Github 上每遭到 push 一次,Travis CI 和 Code Climate 就会自动对你的代码进行测试并报告结果咯~关于 CI 还有很多的用途可以探索哟。

| Posted by frantic1048

Huawei P7-L07 解锁刷机小记

今天终于拿到了设备的解锁码,华为对于获取解锁码的限制(注册华为帐号,绑定设备两周以上)简直反人类,他们的客服也是我遇到过的最无用的客服,各种手段疯狂联系客服,得到的回应总是不停地背书,从来不谈解决问题。 (╯°Д°)╯︵/(.□ . )

本记录操作环境是在 Arch Linux 下,并已安装 android-tools,所以 adb 什么的直接敲终端就来了。

手机那边先启用开发人员选项,在 设置->关于手机 中,连戳⑨次手机版本那一项,开发人员选项就会显示在设置菜单中,此时再进入 设置->开发人员选项,启用 USB 调试

接下来直接用数据线连接手机与电脑,可以先用 adb 查看一下是否已经连上了。正常的话会列出至少一台设备。

adb devices

如果手机端出现连接提示,选择确认即可。接下来将手机重启到 fastboot 模式:

adb reboot-bootloader

完成后手机屏幕上会显示一个安卓小人,下面有红字提示 bootloader 的锁定状态。

再次查看一下设备是否连上电脑状态。

sudo fastboot devices

然后按照华为那边的提示,进行解锁操作:

sudo fastboot oem unlock <设备解锁码>

操作成功之后,手机屏幕上应该会显示 PHONE Unlocked。接下来刷入 Recovery,获取自 xda 的 TWRP 2.8.1

sudo fastboot flash recovery <从 xda 下载下来的 TWRP 镜像文件目录>

完成操作后,重启设备:

sudo fastboot reboot

然后将需要刷的 ROM 存到手机上。获取自 xda 的 material one 1.0,这个 ROM 要求原系统为 B126 的版本,如果像我一样手残升到 EMUI3 的话,需要手动降级回来,降级参见花粉俱乐部的说明

接下来重启到 Recovery:

adb reboot recovery

在 recovery 中选择 install,选择刚刚存到手机上的那个 ROM 文件,开始安装即可,大概需要越 10 分钟。完成之后在 recovery 上选择重启,接下来就能够看到没有华为一家子乱七八糟应用的新环境了 (。>ω<)。

| Posted by frantic1048

解决 ssh 密码正确却被拒绝连接

服务器( Arch Linux 系统)好久没滚过了,今天上去飞速滚完之后登出。再次 ssh 登录的时候,密码正确,却不断提示 Permission Denied,当时就奇怪了。明明刚刚还能上 (´_`)。

在 Super User 上看到了情形基本一致的问题的解答,遂用 Linode 提供的备胎 lish 登上了服务器,查看了 sshd 配置,果然是登录用到的几个选项没有打开,推测是更新 openssh 的时候配置文件顺带被刷新了。

按照解答操作 sshd 的配置文件 < /etc/ssh/sshd_config >,解注释(设定)了下面这些配置项:

启用密码认证

PasswordAuthentication yes

启用 root 用户登录

PermitRootLogin yes

启用 ssh 密钥登录

PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys

配置文件至此编辑完成。接下来重启一下 sshd 服务:

#systemctl daemon-reload

#systemctl restart sshd.service

再次尝试 ssh 登录,世界和平!

| Posted by frantic1048

川西小环线骑行记


D1

日期:2015-07-28

路线:成都王府井购物中心-都江堰市-漩口镇-映秀镇

交通:骑行

里程:约 110 km

路况:良好的柏油路

爬升:平路,最后小段上坡

住宿:某个小旅馆,映秀镇

早上喝点水,吃了两个包子,然后就在王府井购物中心门口集合一路往都江堰走,成都城里的交通简直挤得可以。

到了都江堰吃了个很日常的午餐,队友去车行调整变速。

下午继续上路,出了都江堰就开始各种上坡,太阳晒得厉害,路上遇到个带着俩孩子骑车的父亲,真是厉害。看到都汶高速的大桥还以为是我们走的路,经过的时候才发现那是高速公路,还翻了个小山最后到了映秀。

映秀城修地也是挺不错的,不过漩口中学的遗址现在成了类似旅游景点一样的地方,还弄了个很正式的门口收门票,让我感到非常不舒服。在映秀转了好多家旅馆最后才找到个有三人间的,也是不容易。队友说变速在车行弄过之后踩起来更不对劲了,我看了下发现曲柄竟然有松动 (´_`),然而没足够大的扳手,问了下旅馆老板说附近有个摩托车修理店,然而晚上已经关门,第二天早上起来去那店借大号内六角帮队友紧了下曲柄,微调了下变速大体没啥问题了。

D2

路线:映秀镇-耿达乡-卧龙镇-熊猫沟

交通:骑行

里程:173 km (+ 约 63 km)

路况:映秀到耿达路况非常差,有两公里多的没有灯的隧道。其余路段良好。

爬升:基本全是上坡

住宿:路边小旅馆,熊猫沟

早上因为等摩托车修理店开门,睡得略爽,出发就有点晚。离开映秀马上就是感人的完全破坏的路面,不过比起半年前牛背山之行天全县出去那条烂路和牛背山上山的路,可行性还是高不少,至少前进没那么变态般的费劲,磨磨蹭蹭,快到耿达的时候遇到了一个看起来不长的隧道(然而很长……),进去之后没灯不说,长度感人,路面更加感人,路边还有轮子插进去就肯定摔个爽的沟,整个隧道里面各种水在流,顺便下着小雨,取下风镜用小车灯勉强照着慢慢地磨出了隧道,后来过来的一个队友出来说灯支架坏了直接没用灯就穿过来了 ( ̄□ ̄),隧道里面的温度也基本冬天一样,刚开始很凉快,之后就冷得一笔了。

穿出隧道没多久,终于见到了耿达,这时候简直是饥寒交迫,看到个叫做鸿运饭店简直两眼金光,这个时候已经下午差不多 14 点了,老板说有饭之后欣喜若狂,然后吃了个爽。

开始朝卧龙走的路上渐渐变得凉爽,路面也终于变得平整,路上遇到了一些骑友,他们走得早,在路边玩了挺久。

到了卧龙补充了一些干粮以备第二天上巴郎山用,为了第二天稍微轻松一点,继续往前走了 10 公里到了熊猫沟住宿,刚开始那家老板还不让住宿他们那栋楼的房间,让我们去旁边个条件略差的,队友强行说服,然后住上了好点的那楼的房间,不得不佩服他俩的执着。

D3

路线:熊猫沟-邓生-巴郎山熊猫国之巅-巴郎山垭口-巴郎山猫鼻梁-日隆镇

交通:骑行,坐车

里程:260 km (+ 约 87 km)

   骑行:221 km (+ 约 48 km)

   坐车:约 39 km

路况:良好的柏油路

爬升:熊猫沟到邓生 10km 缓上

   邓生到巴郎山垭口 36 km 陡上坡

   巴郎山垭口到日隆 27 km 下坡

住宿:阿里本青年旅社,日隆镇

熊猫沟出来一路上风景不错,沿着一条河一直往上游前进,快到邓生的时候开始疯狂陡坡,一直到邓生,看到了巴郎山那 36 km 超级上坡的牌子,在这里休息,吃了点干粮。这时候山上下来一个自驾的大叔,拍照之后还给我们送了矿泉水,真是感动 T_T,接着又遇到一个骑摩托车来的大叔,交谈之后知道他是今天早上从成都出发的 ( ̄□ ̄),我们累死累活走了两天一大上午才到这里啊,给我加油之后我就出发了,走了大概十分钟听到他那轰鸣的摩托从我的身旁掠过,飞向远方……

上山的路是看正在走的前面几米觉得和平路一样,看远方觉得这部可能上去吧……然而脚才是判断上坡的唯一标准,一路龟速+大力艰难地前进,队友跑得比较快,到我前面很远的地方去了。一路上受到各种大叔青年小朋友的加油,坚持着缓慢前进,到了 3500m 的海拔的时候,基本没有炎热感了,明显感觉吃力,蹬一两公里都觉得跟摇车十分钟一样的累,这高度基本上看不到什么树了,代之的是满山直到山顶的草,远看是绿草,近看是五颜六色的各种花花草草,颜色也是很鲜艳。

离垭口还有 18 km 的时候,开始飘起若有若无的雨,不久就变成超大的雨,雾也飘起来,能见度已经只有 20m 左右,路过的车全都开上灯,还好骑行服能顶雨,要不我得冷死了,就是心疼小车被雨淋啊 (╥_╥),继续差不多的速度一直前进,走了好几公里,看到队友的车放在个小棚旁边,大概是在那里面躲雨吧,我也就骑过去了,人果然在那儿,把车刚放好的时候,忽然发现一米不到的距离的木门下面一只略大的藏獒正在看着我,顿时吓了个透,弱弱地后退,进入了队友在的那个房间,里面是一个老大爷,中间一个小火炉,我也凑过去烘干下手套,队友说这天气这样可能到不了垭口了,老大爷说离垭口还有 12km,我想着也就这 12km 了,下雨就下雨吧,磨过去算了,于是准备继续前进,队友执意要我告诉家人目前的情况 = =,纠缠半天,然后一个半路来的大叔帮我们截到一个货车,时间不早,我也就上车了……三个人两百给我们搭到了日隆,路上看到了熊猫国之巅的石碑,嘛不是骑过去的就看看就行了……离垭口还有不到 2km 的地方还看到一位骑行者在前进,垭口看到另外一位。过了垭口看到了网上经典的巴郎山日隆那面的盘山公路的壮观景象,接着左挤右挤到了日隆。

阿里本青年旅社条件简直整个旅途的天堂,条件非常棒,而且三人间订到特价只要 90(原价 150)。吃饭的时候点了个看起来很气派的红烧牦牛肉,然而我们基本上在吃胡萝卜和汤,肉没有几块,吃完老板问我们有没有什么意见,队友纷纷表示牦牛肉太少 ლ(・∀・ )ლ。

这天搭车还有个原因是队友膝盖炸了,难以前进,在阿里本老板那儿取了点云南白药处理了下,决定第二天休息一下。

D4

路线:日隆镇

里程:260 km

   骑行:221 km

   坐车:39 km

住宿:四姑娘山米兰青年酒店,日隆镇

换到了一家更便宜的旅馆,膝盖炸的那个队友去卫生院买了云南白药。下午他们想去猫鼻梁看看风景,我表示没兴趣就没去,另外俩队友就走路去了,据说走了两个半小时才走到猫鼻梁……不知道那位膝盖还好不好。我下午就精心打理了下之前弄得灰头土脑的小车,给后刹换了之前备上的新刹车皮,最初装车之后一直没换过,已经基本磨平了。

晚上去买了个当地的沙棘汁,味道,比较酸甜吧。晚饭直接烧烤解决,我顺带吃了一碗牛肉面。这里基本家家饭店都有烤全羊,生意还都不错。

D5

路线:日隆镇-达维乡-小金县-丹巴县

交通:骑行

里程:372 km (+ 112 km)

   骑行:333 km (+ 112 km)

   坐车:39 km

路况:良好的柏油路

爬升:全下坡

住宿:扎西卓康国际青年旅社,丹巴县

早上悠闲地出发了,因为丹巴很热,我就直接短裤短袖开跑了,一路下坡,冷得我受不了,只好减速,阳光更加强烈之后,终于感受到了太阳公公的温暖。到了夹金山的岔路口不久就是达维乡的那路边的红军会师纪念碑,留照之后继续前进,路过了猛固桥,是一座全是铁索构成的桥。

中午在小金县付水饺那家吃水饺,味道真的非常棒。

下午到了丹巴,热得不行,在附近一家邛崃特色的饭店吃晚饭,那个味道非常棒,而且菜都有点恰到好处的辣味。

路上看到有说丹巴的最大特色就是沙土多,的确是这样,整个城风沙挺大的。说起那路边介绍的美人谷,倒是从来没看到过,也不知道到底指什么。

D6

路线:丹巴县-八美镇

交通:骑行

里程:455.8 km (+ 83.8 km)

   骑行:416.8 km (+ 83.8 km)

   坐车:39 km

路况:少量坏掉的路,其余良好的柏油路

爬升:58 km 较陡上坡

   约 10 km 陡上坡,上疙瘩梁子

   18 km 下坡,下疙瘩梁子

   8 km 起伏路,至八美

住宿:遂州饭庄,八美镇

出丹巴不久遇到一小截坏掉的路,之后就一路好路,一直上坡不停,到了雅拉雪山的入口那里已经是下午了,幸好昨天备的干粮足,不至于饿肚子,问了问路边的人,说是翻上(疙瘩梁子)去竟然还有 12 km,我还有点不相信,因为走着走着就看到那边那个弯道过去车子都往下了,肯定是垭口嘛,这么近的能看到的一段路小菜一碟,于是磨到那个弯道过去发现,那路还是上坡,只是稍微缓一点而已,眼前几公里外又出现了一个新的“垭口”,心想这个肯定不会错了,看起来这么高,而且后边也没有山的样子,辛辛苦苦蹬上去,结果发现了更长的几公里的路,远方又是一个“垭口”,真是感觉不会再爱了,这个时候天色已经明显暗了下来,寒风直吹,这坡什么时候到顶啊!遇到后面来的两个骑马的大叔,问了下离顶的距离,说是还有几公里,坐在路边休息,把最后的干粮和水全部吃光了,准备一口气上去,然而实际上是走走停停快有一个小时才终于到达那里,那个时候真是持续一公里都非常吃力了,不得不多休息积蓄体力,最后到达之前看到的“垭口”,转入一条缓上坡的直路,前去的车辆都消失在地平线下,公路延伸到前方就像直直插进了映着暮色的云层,噢这肯定是到头了!狂踩十几下到了下坡起始的地方,旁边是一个寺庙,简单拍一张之后,坐下休息片刻就开始我最喜欢的下坡啦!连续下了 18 km,爽翻了,然而接着一个路牌告诉我八美还有 8 km,这个时候快天黑了,然而我此时心情是万分的愉悦,眼前一片高原风格的景象,力气也感觉全都回来了,大力踩了二十分钟直达八美,和队友一起在饭店吃了个爽,然而就是没水洗澡了 (╯°Д°)╯︵/(.□ . )。

D7

路线:八美镇-塔公镇-新都桥镇

交通:骑行

里程:532.9 km (+ 77.1 km)

   骑行:493.9 km (+ 77.1 km)

   坐车:39 km

路况:良好的柏油路

爬升:约 5km 上坡,上橡皮山

   缓下坡

住宿:山幺吧客栈,新都桥镇

一大清早出发,感觉天气有点冷就穿上了外套,结果走了几公里就热得不行脱掉了,然而风吹起来还是冷,这天气真是跨度大得一天就是一年四季。一路上风景非常棒。

然后没多久就到了塔公,看到了很大的寺庙,路边一个饭店里面队友一起每人来了一碗牦牛酸奶,这玩意儿味道略刺激,很酸,然后带着很大的牛油味道,老板端过来就直接帮我加了两勺白糖,用筷子可以搅起来,比一般商品包装的酸奶要黏得多,不过也算是体验了一下原生的牦牛酸奶……在塔公吃过饭后继续朝新都桥前进,途径布达拉圣山的时候,河里,路边,山坡上,各种地方的能看到的石头表面全被写上了文字,大片的就是直接被画上一些图,场面非常壮观,深深感受到了信仰之力 ˭̡̞(◞⁎˃ᆺ˂)◞*✰,之后路上晴转小雨转大雨,正感觉冷停下来躲雨穿上外套暖和一会儿,雨就停了 ( ̄□ ̄),于是又脱掉外套继续前进。

刚到新都桥镇还是大太阳,朝客栈走的路上直接转超大雨,为了快点吃到饭冲个澡,几个人苦逼在雨里面继续骑了几公里,快到客栈的时候走出了乌云的范围,又变成大晴天,看来这是先给我们洗澡了 (..•˘_˘•..)。

原本是预订的新都桥镇上的一个客栈,然而连洗澡的都没有,想着最后一天住个舒服的第二天翻折多山,奈何新都桥住宿非常紧张,最后找到了山幺吧,那客栈价格感人,三人间 200,老板竟然还说那房间还卖到过 500……然而配置很一般,wifi 木有,Umaru-chan 也没有,电视也没信号,室友都嚷嚷着早点睡明天五点起来翻山。不过这家客栈的菜倒是非常好吃,价格不像房间那么凶残,那个拌牛肉味道非常不错。

D8

路线:新都桥镇-康定县

交通:骑行

里程:596.9 km (+ 64 km)

   骑行:557.9 km (+ 64 km)

   坐车:39 km

路况:良好的柏油路

爬升:约 5km 上坡,上橡皮山

   缓下坡

于是我们就五点就起来了,吃了几个队友的早茶饼,然后烧了点水把瓶子都装满了,出去骑车发现我的车后轮完全没气了,带着深深的怨念打上气发现并没有漏气,脑子里想着是不是熊孩子来放气了什么的骑了一两公里,然后在我后面队友说我的后轮果然扎胎了(´_`)。停下把轮子拆下来,内胎取出打气后发现是以前补过的位置漏气了,然后又检查外胎,发现另外一个位置插上了一个很小的订书订般的铁丝,费很大劲才把那玩意儿弄出来,破俩口懒得补了,何况还有个上面还有一片橡胶不好打理,就直接掏出出门带的备胎换了,全程队友强力围观表示不会也要看看换胎是怎么弄的(っ- ‸ – ς)。还好之后一路就没有出过问题了。

上折多山路上很多车,特别是自驾游的,遇到不少不按规则出牌的司机,强行超车什么的吓得我都快跳下去了 。:゚(。ノω\。)゚・。。到了挺高的地方看到上方有个岔路,心想地图上没看到过有折多山上面的岔路啊这什么鬼,上去之后发现竟然是康定机场,这修建位置也是够高的。到了折多山垭口交通非常糟糕,很多车路边到处停,人更多,让我想起了旺季的峨眉山金顶,在折多山垭口那个石碑那儿很长一串人在排队拍照,我没管那么多,就直接在石碑下方拍了,在上面休息的时候遇到不少骑友,有从 G318 0 公里的位置开始从上海一路到这里的,说是已经骑了整整一月了;在另一面的山坡看到一哥们儿肩膀扛着车爬山坡直接上垭口来了,我目瞪口呆,上来之后问他为何如此,说是骑车好久了,想爬爬山,看到这么大的弯懒得转,就直接上了;还有骑小牙盘比我们中牙盘还大的公跑上来的,简直凶残。

下山的时候非常多的骑友,不少都在推车,不知道是不是第一次爬这种山被虐地失去希望了,我上巴郎山的时候一路收到不少鼓励,我满踩下山也一路给他们加油。下了 20 多公里天气从冬天变成了夏天的暴晒,看到康定城之后一路下坡到山沟里面,然后又是感人的陡上坡,蹬了好久才蹬上城里,因为预计是当天坐车回去,所以一路至奔车站,穿了好几公里的街道才到汽车站,分分钟有人上来问车卖不卖,说自行车带不上客车什么的,这些揽生意的也是丧心病狂。我们找到当天最后一趟发班的成都司机商量好带车的事宜,把三辆车塞到大巴行李柜后,才去找了个饭店吃午饭。接着坐车一坐就是7小时,半夜到达成都,第二天滚回家去。


最后,条河麻耶酱、大老师生日快乐 (。>ω<)。

| Posted by frantic1048

制作点兔的 Telegram 贴纸集小记

Telegram 刚出贴纸功能的时候就想给做一个关于 点兔 的贴纸集了,因为考试迟迟没有动工,上周终于从动画截图中挑选了 100 多张图准备来做贴纸,扣了大概 10 多张之后在群里问了问,幸运的是 Mika 来助力,三天帮我搞定了其中的 60 张,质量绝赞!前后快一个星期的时间最后出了 149 张贴纸 !最后看到自己扣了这么多图也是感到不可思议,大概这就是爱得深沉吧 _(:з」∠)_ 随后段时间我会把原尺寸的 png 图包发到 DA 上去。同时记个创贴纸的流程~

贴纸集链接:Gochiusa, Gochiusa2
DeviantArt 链接:Gochiusa - telegram stickers set

创建贴纸集

Stickers Bot 发送 /newstickerpack,随后它会提示你发送贴纸集的名字给它。

上传贴纸

先发送 1 个或者多个符合当前贴纸内容的 emoji 过去,bot 的建议是不超过两个 emoji。

然后以文件的形式发送对应贴纸的 png 图。多张贴纸重复此步骤即可。

注意

png 图片长宽需均小于等于 512px,且长边为 512px。

使用 imagemagick 可以一键批量搞定:mogrify -resize 512x512 -format png <your pictures>

批量操作之前一定要记得先备份!我就是因为这个操作导致原尺寸的文件全被原地 resize 到 512px 了,现在得手动重做出原尺寸的大图 ˊ_>ˋ

另外,单个 stickers set 的贴纸数量限制是 100(就这么轻易地触及上限了 _(:з」∠)_)

发布贴纸

向 bot 发送 /publish,随后它会提示你发送贴纸的链接名过去,就是一个贴纸链接 https://telegram.me/addstickers/ 之后连着的内容。

之后你可以继续 newstickerpack 上传新的贴纸集。