William's Blog with Octopress

Octopress is A blogging framework for hackers.

简单方法实现拖拽排序

| Comments

拖拽排序就是类似下面图的效果

要实现以上的效果,有很多的选择,最流行的是使用act_as_listgem,它的问题是要修改一条记录的位置,所有跟在它后面的记录都要修改位置。这样如果你有很多条数据,排序就会变的消耗极大和很慢

另一个办法是使用ranked-model这个gem,它只需要执行一条操作就可以了,它是每次给要排序的记录找一个前后两个数的中间值,它使用的数范围很宽,-8388607 to 8388607,使用的过程中也发现了问题,就是有时候排序是失败的,我没找到具体的原因,猜测可能是找的中间值有问题

后来我发现了一个更简单的方式实现这样的效果,甚至都不用第三方gem(jQuery都sortable还是要用都,前面两个也用)。它都原理是:任何两个浮点数中间都包括无数个浮点数,简单说就是浮点数可以一直二分下去:1 > 0.5 > 0.25 –> 0.125

看一下如何实现

首先再要排序都model里加一个字段用来记录位置,类型为float

db/migrate/20140908010519_add_position_to_things.rb
1
2
3
4
5
class AddRowOrderToThings < ActiveRecord::Migration
  def change
    add_column :things, :position, :float
  end
end

然后再该model都controller里添加更新都action

controllers/admin/things_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ThingsController < ApplicationController

def update
  @thing = Thing.find(thing_params[:thing_id])
  @thing.update thing_params

  render nothing: true # this is a POST action, updates sent via AJAX, no view rendered
end

private

def thing_params
  params.require(:thing).permit(:position)
end

后端都工作以及做完里,下面就是前端了,首先引入jquery-ui/sortable

assets/javascripts/admin/application.js
1
//= require jquery-ui/sortable

下来就是最重要都部分了,在客户断直接计算好要排记录它断位置值,传给后台更新

assets/javascripts/admin/update_things_position.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
if ($('#sortable').length > 0) {
  $('#sortable').sortable({
    axis: 'y',
    cursor: 'move',

    sort: function(e, ui) {
      ui.item.addClass('active-item-shadow');
    },

    update: function(e, ui) {
      itemId = ui.item.data('item-id');
      prevPosition = parseFloat(ui.item.prev().data('position'));
      nextPosition = parseFloat(ui.item.next().data('position'));

      if (isNaN(prevPosition)) {
        position = nextPosition - 1;
      } else if (isNaN(nextPosition)) {
        position = prevPosition + 1;
      } else {
        position = (nextPosition + prevPosition) / 2
      }

      $.ajax({
        type: 'POST',
        url: "/things/" + itemId,
        method: "PATCH",
        data: { thing: { position: position } },
        success: function() {
          ui.item.attr('data-position', position);
        }
      });
    }
  });
}

页面渲染断时候我把记录当前的position值放在attr里,当移动一条记录后,找到这条记录前后记录的position值,如果前面没有记录,就是移到第一个了,那么把它的position设为后面一条记录position值减1,如果后面没有记录了,就是移动到最后一个,那么把它到position设为前面一条记录position值加1,如果前后都要,前后记录position值相加除2

这样position值到变化好像是指数级的,每次都除以2,会不会除到小到没有呢?我们来看一下

如果每次都移动到最前面或最后吗,都是加1减1到操作,完全不用担心

看一下每次都移动到第二个位置到情况,假设有4条数据,它们到初始position值为0,1,2

第一次把第三个数移动到第二个位置,它到值就是(0+1)/2 = 0.5,现在前两个数到position为0,0.5

第二次把第三个数移动到第二个位置,它到值就是(0+0.5)/2 = 0.25, 现在前两个数到position为0,0.25

第三次把第三个数移动到第二个位置,它到值就是(0+0.25)/2 = 0.125, 现在前两个数到position为0,0.125

应该找到规律了,第n次移动之后第二个数为0.5的n次方,那么排序多少次会出现溢出的问题呢?我把这个问题留给读者 :)

Gitlab Shell如何工作

| Comments

这篇博客的目的是研究一下从git push到commit出现在gitlab页面上中间发生到什么

开始之前补充一点背景知识,ssh可以执行命令, Gitlab以及Gitolite这些操作远程仓库应该都有用这个技术

比如我们可以在~/.ssh/authorized_keys中加上自己当command

command="./cmd" ssh-rsa <my-rsa-key>

这样就没法用ssh登陆了,每次登陆只会执行cmd这个程序

就像上面文章里写的,我们还可以用$SSH_ORIGINAL_COMMAND变量取到客户端发来当命令

1
2
#!/bin/sh
echo $SSH_ORIGINAL_COMMAND
chmod +x ~/cmd

现在如果在登陆的时候后面参数,就可以回显里

$ ssh git@gitlab.williamherry.com a b c
# => a b c

Gitlab-shell就用的这个技术,~/.ssh/authorized_keys加一条类似这样当纪录

command="/home/git/gitlab-development-kit/gitlab-shell/bin/gitlab-shell key-15",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa <my-rsa-key>

看一下gitlab-shell的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env ruby

unless ENV['SSH_CONNECTION']
  puts "Only ssh allowed"
  exit
end

key_id = /key-[0-9]+/.match(ARGV.join).to_s
original_cmd = ENV['SSH_ORIGINAL_COMMAND']

require_relative '../lib/gitlab_init'

#
#
# GitLab shell, invoked from ~/.ssh/authorized_keys
#
#
require File.join(ROOT_PATH, 'lib', 'gitlab_shell')

if GitlabShell.new(key_id, original_cmd).exec
  exit 0
else
  exit 1
end

它从ARGV读到key_id,从环境变量SSH_ORIGINAL_COMMAND读到命令, 我们把它写到文件看看git push对应的命令是什么,在文件中加入下面一行代码

File.write('/tmp/sss', ENV['SSH_ORIGINAL_COMMAND'])

通过测试发现有下面的对应关系

git push  => git-receive-pack <repo>
git pull  => git-upload-pack <repo>
git fetch => git-upload-pack <repo>

现在key的id有了,命令有了,操作的repo也有了,我们看一下gitlab-shell怎么处理

GitlabShell.new(key_id, original_cmd).exec

他用参数创建了一个GitlabShell实例之后执行exec方法

1
2
3
4
5
6
def initialize(key_id, origin_cmd)
  @key_id = key_id
  @origin_cmd = origin_cmd
  @config = GitlabConfig.new
  @repos_path = @config.repos_path
end

初始化只是把一些变量保存到了实例变量中,我们重点追踪cmd,记住这里到origin_cmd是’git-receive-pack ‘(我们只追踪git push)

然后就是执行exec方法,该方法主要执行三个步骤

1
2
3
4
5
def exec
  parse_cmd
  verify_access
  process_cmd
end

parse_cmd解析了一个origin_cmd,把命令(git-receive-pack)赋给了实例变量@git_cmd和@git_access,把repo赋给了实例变量@repo_name

verify_access拿前面得到的数据调用gitlab的api去验证看合不合法

最后process_cmd调用exec_cmd然后再调用了这样一行代码

Kernel::exec({ 'PATH' => ENV['PATH'], 'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'], 'GL_ID' => @key_id }, *args, unsetenv_others: true)

把变量都替换成真实的数据得到

Kernel::exec({ 'path' => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games, 'LD_LIBRARY_PATH' => , 'GL_ID' => key-15}, ["git-receive-pack", "/home/git/gitlab-development-kit/repositories/williamherry/test2.git"], unsetenv_others: true })

这里更深入的分析再以后再搞,简单说就是git push的时候gitlab-shell再服务端打开一个接受历程等待接收pack,客户端发送pack,好像从这里就断掉了,前面说的要在gitlab网站中现实的数据是在什么什么创建的呢?

你猜对了,他就是利用对git的hooks,查看gitlab-shell目录下的hooks目录会发现有三个hooks, post-receive pre-receive update,push应该对应的是post-receive, 再里面添加打印信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/usr/bin/env ruby

# This file was placed here by GitLab. It makes sure that your pushed commits
# will be processed properly.

refs = ARGF.read
key_id  = ENV['GL_ID']
repo_path = Dir.pwd

puts refs
puts key_id
puts repo_path

# reset GL_ID env since we already got its value
ENV['GL_ID'] = nil

require_relative '../lib/gitlab_custom_hook'
require_relative '../lib/gitlab_post_receive'

if GitlabPostReceive.new(repo_path, key_id, refs).exec &&
    GitlabCustomHook.new.post_receive(refs, repo_path)
  exit 0
else
  exit 1
end

push的输出中包含了相关的信息,这里的hooks是成功后才执行的,如果你没有新commit,push就不会有这些输出

1
2
3
remote: refs: 8939f77074dc85ac4529fcec1245f7c4563e17cd a6cd629e9deefb0ab9638217bdd82ae68ff17099 refs/heads/master
remote: key_id: key-15
remote: repo_path: /home/git/gitlab-development-kit/repositories/williamherry/test2.git

再这个hooks中它执行了GitlabPostReceive的exec方法和GitlabCustomHook的post_receive方法。而后面这个只是负责去执行custom_hooks下面的hooks,所以我们只看一看前面一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GitlabPostReceive
  def initialize(repo_path, actor, changes)
    @config = GitlabConfig.new
    @repo_path, @actor = repo_path.strip, actor
    @changes = changes
  end

  def exec
    result = update_redis
    broadcast_message = GitlabNet.new.broadcast_message
  end

  protected

  def update_redis
    system(*config.redis_command, 'rpush', queue, msg, err: '/dev/null', out: '/dev/null')
  end
end

我把代码做了简化,去掉了不重要的步骤,简单说这个hooks做了两件是:往redis里塞里一条数据,查了下广播(不知道是干什么的,不过好像不是很重要的样子),用puts发现它是往队列resque:gitlab:queue:post_receive中添加里这样一条信息

  {"class":"PostReceive","args":["/home/git/gitlab-development-kit/repositories/williamherry/test2.git","key-15","YTZjZDYyOWU5ZGVlZmIwYWI5NjM4MjE3YmRkODJhZTY4ZmYxNzA5OSA1MjJj\nZDhhN2MwMDEzYWU0MGU3ZTVhZDFiM2I5ZjlhNGE2OWYzNmIyIHJlZnMvaGVh\nZHMvbWFzdGVyCg==\n"]}

那一串奇怪的字符是Base64加密后的@changes,而@changes就是前面的refs即:

8939f77074dc85ac4529fcec1245f7c4563e17cd a6cd629e9deefb0ab9638217bdd82ae68ff17099 refs/heads/master

好了,现在我们知道gitlab-shell和gitlag是通过redis共享数据的,接下来就是gitlab的app/workers/post_receive.rb中的代码做相应的处理了

Ruby元编程读书笔记

| Comments

由于C语言绝大多数编译时的信息在运行时都丢失了,所以C语言不支持元编程或內省

实例方法:在类中定义,在实例中使用

Ruby的class关键词更像一个作用域操作符而不是类型声明语句

一个对象仅仅包含它的实例变量以及一个对自身类的引用

方法存放在类中而不是实例中

类自身也是对象,作为对象它的类是Class

超类是指类之间的继承关系

任何以大写字母开头的引用(包括类名和模块名),都是常量

方法查找:如果不引入eigenclass, 方法查找被称为“向右一步,在向上”, 也就是说,先向右一步来到接收者所在的类中查找,然后沿着祖先链向上直到找到给定的方法。如果有eigenclass,那么最先会在接收者的eigenclass中查找

默认类是从Object类继承而来,即

class MyClass
end

class MyClass < Object
end

是一样一样的

类和模块的定义中(并且在任何方法之外),self的角色由这个类或模块担任

私有方法服从一个简单的规则:不能明确指定一个接收者调用一个私有方法

class A
  include B
end

include会把被引入模块B插入到引入模块A祖先链中该模块A的上方

A.ancestors # => [A, B, Object, Kernel, BasicObject]

没有明确指定接收者的调用都会作用于self

当调用一个方法时,实际上是给一个对象发送了一条消息

符号和字符串没有关系,并且它们属于完全不同的类

符号是不可变的

一些操作(比如比较操作)针对符号运行的更快

动态调用方法叫动态派发

动态定义方法叫动态方法

method_missing叫幽灵方法

什么事拟态方法?

什么事类宏?

只有在调用一个方法时才可以定义一个块,yield回调这个块,Kernel#block_given?()方法判断是否包含块

什么事闭包?

当定义一个块时,它会获取当时环境中的绑定,并且把它传给一个方法时,它会带着这些绑定一起进入该方法

块会覆盖具有相同名字的局部变量

类和模块定义中的代码会被立即执行,相反,方法定义中的代码只有在方法被调用时被执行

&操作符的真正含义:这是一个Proc对象,我想把它当成一个块再使用

可调用对象可以有一下方式:

  • 块(虽然它们不是真正的对象,但是它们是可调用的):在定义它们的作用域中执行
  • proc:Proc类的对象,跟块一样,它们在定义自身的作用域中执行
  • lambda:也是Proc类的对象,但是它跟普通的proc有细微的区别。它跟块和proc一样都是闭包,因此可以在定义自身的作用域中执行
  • 方法:绑定于对象,在所绑定对象的作用域中执行

可调用对象的区别:

  • 在方法和lambda中,return语句从可调用对象中返回
  • 在块和proc中,return语句从定义可调用对象的原始上下文中返回
  • 对传入参数的处理:方法最严格,lambda同样严格(它与方法相比,在某些极端情况下略为宽松),而proc和块则要宽松一些

类方法就是类的单件方法,它存在于该类的eigenclass中

eigenclass的超类就是超类的eigenclass(有什么用?)

class << obj 进入对象的eigenclass

当类包含模块时,它获得的是该模块的实例方法--而不是类方法。类方法存在于模块的eigenclass中

如果在对象的eigenclass中包含模块,就可以把该模块的实例方法导入到该对象的eigenclass中,从而成为该对象自己的方法

Object#extend只是在接收者eigenclass中包含模块的快捷方式

关于别名:

可以把方法看成有名字(name)和方法体(body)两部分组成,名字指向方法体

def m1
  # body
end

经过

alias m2 m1

后,m2也指向类m1的方法内容b1 这时候在重定义m1

def m1
  # body 2
end

m1就会指向一个新的方法体b2,原来的方法体只能通过m2来引用了

注意alias是一个关键字,后面两个参数中间没有逗号

环绕别名:先定义别名,再重定义方法,在其中使用别名

alias new_name old_name

class是保留词,除了使用klass替代外还可以用clazz

类扩展混入:

module MyMixin
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def x
      "x()"
    end
  end
end

使用Docker搭建一个Rails开发环境

| Comments

如果使用的fig的话那就太简单了,照着fig官方文档分分钟就可以搞定,但是它的方法存在一个问题,他是直接用的ruby,postgres,redis的image,整个加起来就上G了,国内的这破网速,虽然只需下载一次就好了,并且国内也有镜像加速的服务如daocloud,但实际使用发现任然无法接受.所以我的方法是使用一个基本的ubuntu镜像,其他镜像在此基础上构建而成

首先是多官方的ubuntu镜像做稍微修改,这个修改就是把源换成网易的,然后制作成一个镜像,其他的镜像都一次为基础

想看一下相关的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
# tree .
├── Dockerfile
├── docker
│   ├── gemrc
│   ├── postgres
│   │   ├── Dockerfile
│   │   ├── pg_hba.conf
│   │   ├── postgresql.conf
│   │   └── run
│   └── redis
│       ├── Dockerfile
│       └── redis.conf
└── fig.yml

我们使用fig来编排,所以fig.yml就是主要的入口了,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db:
  build: ./docker/postgres
redis:
  build: ./docker/redis
web:
  build: .
  command: bundle exec rails s -b 0.0.0.0 -p 3000
  volumes:
    - .:/myapp
  ports:
    - "3000:3000"
  links:
    - db
    - redis

这里有三个部分,前面两个都是知道从什么地方找Dockerfile来build镜像(fig官方的例子这里是用的image: postgres)

第三个就是实际跑rails的image了,它指定了一个image启动后运行的命令(其他两个镜像也有启动后要执行的命令,是在Dockerfile里指定),然后有一个volumes把代码目录镜像到images是,这样修改了代码不用做操作就可以看到效果

ports指令把image里的端口映射到宿主机上,宿主机并不一定就是我们的开发机,像Mac使用boot2docker安装的docker,那么开发机就是运行docker的虚拟机了,可以用boot2docker ip得到ip地址,等之后配置完成了就可以用这个地址加3000端口查看网页了

最后的links会在web的image执行的时候(就是container)在其的/etc/hosts里加一条到其他container地址的对应,要知道每个container运行IP都是不一样的,这样就可以在web去连db和redis了,相应的database.yml里把host写成db就可以了

我们一个一个来看他们是怎么build的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# cat docker/postgres/Dockerfile
FROM williamherry/ubuntu
MAINTAINER William Herry <WilliamHerryChina@Gmail.com>
ENV DOCKERFILE_VERSION 0.0.1.1

RUN locale-gen en_US.UTF-8
RUN update-locale LANG=en_US.UTF-8

RUN apt-get -qqy update && DEBIAN_FRONTEND=noninteractive apt-get install -qqy postgresql-9.3 postgresql-contrib-9.3 postgresql-9.3-postgis-2.1 libpq-dev sudo

# /etc/ssl/private can't be accessed from within container for some reason
# (@andrewgodwin says it's something AUFS related)
RUN mkdir /etc/ssl/private-copy; mv /etc/ssl/private/* /etc/ssl/private-copy/; rm -r /etc/ssl/private; mv /etc/ssl/private-copy /etc/ssl/private; chmod -R 0700 /etc/ssl/private; chown -R postgres /etc/ssl/private

ADD postgresql.conf /etc/postgresql/9.3/main/postgresql.conf
ADD pg_hba.conf /etc/postgresql/9.3/main/pg_hba.conf
RUN chown postgres:postgres /etc/postgresql/9.3/main/*.conf

ADD main /var/lib/postgresql/9.3/main
RUN chown -R postgres:postgres /var/lib/postgresql/9.3/main

ADD run /usr/local/bin/run
RUN chmod +x /usr/local/bin/run

EXPOSE 5432
CMD ["/usr/local/bin/run"]

这个是直接抄的https://github.com/orchardup/docker-postgresql, 刚发现他github项目主页就是这个已经是Deprecated,请使用官方镜像

简单说就是安装postgresql,把配置文件改改,让访问权限宽松一点,在启动起来

1
2
ADD main /var/lib/postgresql/9.3/main
RUN chown -R postgres:postgres /var/lib/postgresql/9.3/main

上面这两行是我添加的,实现把一些初始数据导入,把postgresql数据目录的文件整个放在docker/postgres/main下面build镜像,就可以一启动就有seed数据了

下面看看redis的Dockerfile

1
2
3
4
5
6
7
8
9
10
11
# cat docker/redis/Dockerfile
FROM williamherry/ubuntu
MAINTAINER William Herry <WilliamHerryChina@Gmail.com>
ENV DOCKERFILE_VERSION 0.0.1

RUN apt-get update -qqy && apt-get install -qqy redis-server

ADD redis.conf /etc/redis/redis.conf
CMD ["redis-server", "/etc/redis/redis.conf"]

EXPOSE 6379

这个就更简单了,安装,加个配置文件,启动

配置文件主要修改了两个地方:

1
2
daemonize no
bind 0.0.0.0

似乎docker在后台运行是不行的

最后我们看看web的Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# cat Dockerfile
FROM williamherry/ubuntu
MAINTAINER William Herry <WilliamHerryChina@Gmail.com>
ENV DOCKERFILE_VERSION 0.0.1

ENV RUBY_MAJOR 2.1
ENV RUBY_VERSION 2.1.5
ENV RUBY_SRC_DIR /usr/src/ruby

# Essentials
RUN apt-get update -qqy \
  && apt-get install -qqy \
    autoconf \
    build-essential \
    curl \
    git \
    imagemagick \
    libbz2-dev \
    libcurl4-openssl-dev \
    libevent-dev \
    libffi-dev \
    libglib2.0-dev \
    libjpeg-dev \
    libmagickcore-dev \
    libmagickwand-dev \
    libmysqlclient-dev \
    libncurses-dev \
    libpq-dev \
    libreadline-dev \
    libsqlite3-dev \
    libssl-dev \
    libxml2-dev \
    libxslt-dev \
    libyaml-dev \
    procps \
    zlib1g-dev \
  && rm -rf /var/lib/apt/lists/*

RUN apt-get update -qqy \
  && apt-get install -qqy locales \
  && rm -rf /var/lib/apt/lists/* \
  && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8
ENV LANG en_US.utf8

# Ruby specifics
RUN apt-get update -qqy \
  && apt-get install -qqy \
    bison \
    ruby \
  && rm -rf /var/lib/apt/lists/* \
  && mkdir -p $RUBY_SRC_DIR \
  && curl -s -SL "http://ruby.taobao.org/mirrors/ruby/2.1/ruby-$RUBY_VERSION.tar.bz2" \
    | tar -xjC $RUBY_SRC_DIR --strip-components=1

WORKDIR $RUBY_SRC_DIR

RUN autoconf \
  && ./configure \
    --disable-install-doc \
  && make -j"$(nproc)" \
  && apt-get purge -qqy --auto-remove bison ruby \
  && make install \
  && rm -r /usr/src/ruby

RUN mkdir /myapp
WORKDIR /myapp
ADD . /myapp
ADD config/database.yml.fig /myapp/config/database.yml
ADD config/secrets.yml.fig /myapp/config/secrets.yml
ADD docker/gemrc /root/.gemrc
RUN gem install bundler
RUN bundle install

前面那部分是网上找的安装ruby的配制方法,你可能不能理解为什么要自己安装ruby,你可以直接用官方ruby镜像,只要把FROM后面改成ruby就可以了,就是比较大而已,我发现下载安装好ruby的镜像还没从ubuntu镜像安装来的快

设置好之后就可以用下面的命令启动开发环境了(如果有数据放docker/postgres/main下,fig up就够了)

1
2
3
fig up -d db
fig run web rake db:setup
fig up

从mkdir开始才是正在起作用的,太简单就不用说了,这里的.gemrc里知道了taobao的源,这样安装bundler就会快一点

最后说说踩过的一个坑,你可能注意到了每一个Dockerfile前面都有ENV DOCKERFILE_VERSION 0.0.1这一行,刚开始是ENV VERSION 0.0.1,这样会有一个问题,就是在执行migration的时候干脆不跑了,花了我一天的时间debug,最后发现他会把这个变量传进去,rake db:migrate就相当于rake db:migrate VERSION=0.0.1

理解Docker镜像的存储原理

| Comments

一开始接触Docker就对他镜像的存储原理很感兴趣,有一个问题一直不明白,Commit一个镜像的时候到底把什么存下来了,只知道他是使用分层的AUFS,具体不是特别很清楚,直到看了这篇关于Linux AuFS的文章,才有一个自己的认识,对不对还不知道

如果你用过Photoshop,那么用类比的方法就很好理解了,PS里作图一般都是一层一层的,上层如果没有内容就会透过去显示下层的内容,如果有旧覆盖下层内容.假设从下到上有ABC三层,A层上有一个红点,B层上靠右的地方有一个绿点(没有和A层上的重叠),如果只有这两层,那我们看到的就是一个红点加几个绿点,现在在上面有加一层C,他上面有一个蓝点和A层上红点的位置重叠,那么他就会覆盖A层的红点,最后我们看到的就是一个蓝点一个绿点

类比到文件也是这样,假设一个目录/XXX由自底向上的ABC三层组成,他们每一层都有一个和他们名字对应的文件(A=>A.txt, ..),同时他们都有一个叫common.txt文件,里面的内容分别是A,B,C,那么最终的结果是/XXX目录共有4个文件,A.txt, B.txt, C.txtcommon.txt,而common.txt里的内容是C(最上层覆盖下层的)

我们可以用一个例子看看:

首先创建三个目录:

1
2
3
mkdir -p /tmp/aufs/root
mkdir -p /tmp/aufs/layer1
mkdir -p /tmp/aufs/layer2

创建相应的文件

1
2
3
4
5
6
7
echo 'root' > /tmp/aufs/root/root.txt
echo 'layer1' > /tmp/aufs/layer1/layer1.txt
echo 'layer2' > /tmp/aufs/layer2/layer2.txt

echo 'root' > /tmp/aufs/root/common.txt
echo 'layer1' > /tmp/aufs/root/common.txt
echo 'layer2' > /tmp/aufs/root/common.txt

然后我们把他们按顺序Mount起来

1
mount -t aufs -o br=/tmp/aufs/layer2:/tmp/aufs/layer1:/tmp/aufs/root none /tmp/aufs/root

br后面的参数顺序是越后面越底层

现在/tmp/aufs/root/下应该有4个文件,root.txt, layer1.txt, layer2.txtcommon.txt,并且common.txt里面的内容应该是layer2.同时由于现在layer2是最上层,那么/tmp/aufs/root里面做修改会反映到/tmp/aufs/layer2,在/tmp/aufs/root里创建文件,/tmp/aufs/layer2里也会出现

再回到前面的问题,Commit的时候到底保存的是什么呢,假设我们要基于前面的三层做修改在提交,只要再在上面加一层layer3,然后对/tmp/aufs/root做修改

1
2
3
mkdir -p /tmp/aufs/layer3
umount /tmp/aufs/root
mount -t aufs -o br=/tmp/aufs/layer3:/tmp/aufs/layer2:/tmp/aufs/layer1:/tmp/aufs/root none /tmp/aufs/root

Note: 没有找到可以一层一层加的方法,只能先umount在按顺序mount

然后再/tmp/aufs/root下做修改

1
2
echo 'layer3' > /tmp/aufs/root/layer3.txt
echo 'layer3' > /tmp/aufs/root/common.txt

这时候看/tmp/aufs/layer3目录,应该看到会有这两个文件,我们就可以把这个目录打个包实现Commit的目的

把这样的每一层保存下来,并想办法记录他们之间的关系(某一层的上层和下层是谁),差不多就可以实现Docker的镜像存储了,不过不一定对,具体怎么做得看了代码才知道

2014年总结 && 2015年展望

| Comments

2014年已经结束一周了,回顾过去这一年,同样又是收获颇多,很多方面都在持续地,稳定地,慢慢的提升,或许这就是幸福吧

2014年总结

骑行

骑行真是远远的超出了预期: 买了自行车,半年骑了差不多2000KM,参加了环萧山的比赛,完成了龙井十上十下的挑战.还记得第一次骑的时候就把一个电瓶车撞翻了,回来的时候领我骑的同事还摔了车,本以为这糟糕的开始会阻止我骑车的决心,结果出奇意外的之后都比较顺利,摔过几次车,都没什么大碍.希望2015年也可以平平安安的,并可以完成川藏线的挑战

工作

前面一直贪多求快,写了好多极低质量的代码,好多地方没有完全搞清楚,只是实现了基本的效果,就不去管了,导致后来出现各种诡异的,低级的Bug,有一段时间各种被同事吐槽,很不好过(不过我挺过去了,而且心里素质得到进一步提高,所以凡事都要辩证的区看). 所以之后一定要对自己的要求提高,争取做一个有追求的程序员

另外发现还是有好多技术理解的不够深刻,这可能都是习惯造成的,习惯上不喜欢去深究底层的东西,只有实现效果就不管了,这习惯一定得改,讲到习惯的地方在细谈

五笔 => 拼音

由于经常不写字,好多字不会写,导致用五笔打不出来,而且从长远看好像用拼音会更有优势,所以决定转到拼音去.转换过程并没有想象中的困难,只花了差不多一个月就已经到可用的状态了. 但是尴尬的是我发现小学时候拼音没学好,好多字还是打不出来:(, 不过不打算在转回五笔了.事实证明,很多看起来有难度的事情,只要有一个契机,加小小的坚持,还是可以完成的.

领悟

千里之行始于足下

只要功夫深铁杵磨成针

聪明在于勤奋,天才在于积累

有太多关于积累的名言警句,但大部分人都是听听就好了,没有更深刻的认识,因为这些都是别人总结出来的,是别人的不是自己的.人们都说长时间不吃饭会很饿,我们听了后会觉的, 好像很有道理哦,直到自己饿了好多天没吃才会想,长时间不吃饭TM会很饿呀; 刚上大学的时候就听到有人说不要挂科,不让会很惨,当时听到也觉的好像很有道理,直到我因为挂了太多课,拿不到学位证才发现,TM挂科真的很惨呀.所有很多大道理,鸡汤,自己不体会一下是不会有直观的体验的.

刚开始用百词斩背单词的时候并不会想到会坚持400多天,当时只是天真的想,等我坚持背100天的时候发朋友圈里,别人一定会觉得我很牛B.可是到坚持100多天的时候并不是很想分享到朋友圈,反倒是深刻的明白了一个道理:习惯真的太有用了,试想,如果我一直坚持背单词,最好形成习惯,那单词量增加不过是时间问题,在试想,如果我一年前,或五年前,十年前就有这个习惯,那我现在单词量岂不是已经很足了?

在把这个发现推广到其他地方,如果我很早就有每天运动的习惯,哪怕是最简单的俯卧撑,仰卧起坐之类的,坚持多年后体质也一定会有非常明显的,在比如有早睡早起,生活规律的习惯,那现在头发也应该不会掉的这么严重,如果很早就有做事认真的习惯,那今年就不会出这么多Bug了,为什么现在才明白过来?

突然想起上初中的时候有一个数学老师给我们讲了一个故事: 有一个人养了一头猪下了一窝小猪,其中有一只腿有缺陷站不起来,每只给猪喂食的时候这个人都是从耳朵上把这头小猪提过去吃,完了再提回去,等这头猪长到200多斤的时候,这个人可以单手提起200斤的重物. 当时听完后好像很有感触,然后就没有然后了.现在想想,如果当时能明白过来,从那时候就注重培养一些好习惯,现在岂不是已经很牛逼了?

不过迟点明白过来总比还没明白过来好,所以接下来的一年我回把养成好习惯放在最重要的位置;为此我还专门开发了一个网页程序: https://github.com/williamherry/habit, 部署在heroku上: http://habit.williamherry.com/, 现在除了背单词以为已经有几个习惯稳定下来了,像做仰卧起坐已经坚持了100多天了吧,从原来的一口气做30个到现在已经可以一口气做140个

习惯才是最大的财富

吉他 && 高音

练吉他和高音的任务远远没有完成,究其原因,之前一直习惯于做一件事想马上看到结果,看到提高,但实际上大多事情都是需要靠时间来磨的,积累足够的量变才会引起质变.所以要能学会弹一首曲子,首先得养成每天都弹弹的习惯,不过习惯也不是说想养成就可以养成的,也是需要磨时间的,鉴于已经有一些习惯正在进行中,这个只能排的后一点了,另外现在有室友了,练习会影响到他

2015年计划

2015年只有一件很重要的事情需要完成: 养成尽可能多的好习惯

Memcached Cache Analysis

| Comments

利用munin这样的工具可以查看到Memcached整个缓存的命中率,有时候可能会需要知道不同的缓存各自的命中率如何,这时候可以利用Rails的ActiveSupport::Notifications来帮助我们分析

1
2
3
4
5
CacheLogger = Logger.new("log/cache.log")

ActiveSupport::Notifications.subscribe /cache_\S*\.active_support/ do |name, start, finish, id, payload|
  CacheLogger.info "#{name} #{start} #{payload[:key]}"
end

上面的代码会把所有的缓存的请求记录到一个日志文件里面,可以用下面的脚本去生产分析报告(from @soffolk)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#! /bin/sh

# ./parse_cache cache.log courses

read=`cat $1 | grep $2 | grep cache_read.active_support | wc -l`
write=`cat $1 | grep $2 | grep cache_write.active_support | wc -l`
read_multi=`cat $1 | grep $2 | grep cache_read_multi.active_support | grep -o '[as|to]-json' | wc -l`
fetch_hit=`cat $1 | grep $2 | grep cache_fetch_hit.active_support | wc -l`
all=$((read+read_multi))
if [ $all = 0 ]; then
  ratio=0
else
  ratio=$(echo "scale=6; $(($all-$write))/$all*100" | bc -l)
fi

echo "Read:         $read"
echo "Write:        $write"
echo "Read Multi:   $read_multi"
echo "Fetch Hit:    $fetch_hit"
echo "All:          $all"
echo "Hit Ratio:    $ratio%"

另外你可能想看一下是不是分给Memcached的内存已经用光了,不断有少访问的item被挤出去

可以用memcdump命令dump出所有的keys

1
memcdump --servers=localhost | sort > keys1.dump

过几秒在dump一次

1
memcdump --servers=localhost | sort > keys2.dump

使用comm命令可以查看是否有变化

1
2
comm -23 keys1.dump keys2.dump > only_in_one.dump
comm -13 keys1.dump keys2.dump > only_in_two.dump

实际上从memcached的内置命令stats就可以看到内存已经不够用的,一直有item被挤出去

1
2
3
4
5
6
...
STAT hash_is_expanding 0
STAT expired_unfetched 4383
STAT evicted_unfetched 7335517 #已驱逐但未获取的对象数目 
STAT bytes 253815528
...

Login From Console

| Comments

最近有一个问题调试了好久,这个过程中学的了一些有意思的调试方法,在这里记录一下过程

问题是出现在staging环境上的,有一个form已提交就报错,只有那一条数据有这个错误, 但是单独肉眼看没发现有什么问题,查看日志只有很少的错误信息

1
2
3
ActiveRecord::RecordNotSaved (ActiveRecord::RecordNotSaved):
  app/forms/my_form.rb:51:in `submit'
  app/controllers/my_controller.rb:60:in `update'

app/forms/my_form.rb的51行是

1
my_object.save!

判断是没保存成功,由于不是在本地开发模式,不能用pry设置断点,只能打印出来了

1
2
3
4
5
begin
  my_object.save
rescue ActiveRecord::RecordNotSaved => e
  Rails.logger.info my_object.errors.full_messages
end

结果什么错误信息也没有,搜索了一下这个错误提示,有人说callback返回false会引起保存不成功并没有错误,例如有这样一个callback

1
2
3
def hide
  self.visible = false
end

但这个model并没有这样的Callback,把全部callback都删掉问题依旧,所以判断不是这个问题

同事支招把backtrace打出来会不会看出点眉目

1
2
3
4
5
begin
  my_object.save
rescue ActiveRecord::RecordNotSaved => e
  Rails.logger.info $!.inspect, $@
end

错误信息是多了,但任然看不出问题所在,由于经常用pry断点调试,现在已经没办法了,搜索怎么在非development环境用pry

这个过程中学会了怎么在console里发请求如登陆什么的

引用 http://stackoverflow.com/questions/151030/how-do-i-call-controller-view-methods-from-the-console-in-rails 的最后一个答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Start Rails console
rails console
# Disable forgery_protection
ApplicationController.allow_forgery_protection = false
# Get the login form
app.get '/community_members/sign_in'
# View the session
app.session.to_hash
# Copy the CSRF token "_csrf_token" and place it in the login request.
# Log in from the console to create a session
app.post '/community_members/login', {"authenticity_token"=>"gT7G17RNFaWUDLC6PJGapwHk/OEyYfI1V8yrlg0lHpM=",  "refinery_user[login]"=>'chloe', 'refinery_user[password]'=>'test'}
# View the session to verify CSRF token is the same
app.session.to_hash
# Copy the CSRF token "_csrf_token" and place it in the request. It's best to edit this in Notepad++
app.post '/refinery/blog/posts', {"authenticity_token"=>"gT7G17RNFaWUDLC6PJGapwHk/OEyYfI1V8yrlg0lHpM=", "switch_locale"=>"en", "post"=>{"title"=>"Test", "homepage"=>"0", "featured"=>"0", "magazine"=>"0", "refinery_category_ids"=>["1282"], "body"=>"Tests do a body good.", "custom_teaser"=>"", "draft"=>"0", "tag_list"=>"", "published_at(1i)"=>"2014", "published_at(2i)"=>"5", "published_at(3i)"=>"27", "published_at(4i)"=>"21", "published_at(5i)"=>"20", "custom_url"=>"", "source_url_title"=>"", "source_url"=>"", "user_id"=>"56", "browser_title"=>"", "meta_description"=>""}, "continue_editing"=>"false", "locale"=>:en}

登陆可以了但断点还是不行, 不过http://stackoverflow.com/questions/7744848/is-there-a-way-to-identify-why-the-database-rollsback-in-a-rails-application 的回答给了我灵感,保存不成功日志里会有rollback, 所以我修改了日志配置,果然发现了原因,原来是关联的model缺字段保存没成功

Chef Ohai源码学习

| Comments

接触Chef以来一直对Ohai很感兴趣,它收集的系统信息非常全面,很好奇它是怎么收集的,现在终于有时间学习了

目标

搞清楚Ohai是如何收集到某一项系统消息的,如IP地址

环境搭建

使用gem install ohai安装ohai和它的依赖,然后下载ohai的源码

git clone https://github.com/opscode/ohai.git

可以直接在源码目录通过./bin/ohai运行ohai,这样可以对源码做一些修改(添加调试代码)然后马上运行查看效果,而不用去找它的源码安装到系统的位置

还可以使用pry进行单步调试: gem install pry安装pry,的要调试的代码前面加上binding.pry,在该文件前面加上require 'pry'

查看源码过程

首先找到入口,当然就是bin/ohai文件了,我们需要关系的只有一行

bin/ohai
1
Ohai::Application.new.run

我们在看一下Ohai::Application的定义,我们感兴趣的是run方法的定义

lib/ohai/application.rb
1
2
3
4
5
def run
  configure_ohai
  configure_logging
  run_application
end

这里前两个方法我们目前不关心,看一下run_application方法的定义

lib/ohai/application.rb
1
2
3
4
5
6
7
8
9
10
11
12
def run_application
  ohai = Ohai::System.new
  ohai.all_plugins(@attributes)

  if @attributes
    @attributes.each do |a|
      puts ohai.attributes_print(a)
    end
  else
    puts ohai.json_pretty_print
  end
end

通过加调试代码发现这里的@attributes是nil,所以这里的代码可以简化为

1
2
3
ohai = Ohai::System.new
ohai.all_plugins(nil)
puts ohai.json_pretty_print

下来去看Ohai::System的定义

首先我们看一下json_pretty_print的定义

lib/ohai/system.rb
1
2
3
def json_pretty_print(item=nil)
  Yajl::Encoder.new(:pretty => true).encode(item || @data)
end

它只是把@data的数据格式化后输出,所以我们推测实际收集的动作是发生在all_plugins方法里

lib/ohai/system.rb
1
2
3
4
5
6
7
8
9
def all_plugins(attribute_filter=nil)
  # Reset the system when all_plugins is called since this function
  # can be run multiple times in order to pick up any changes in the
  # config or plugins with Chef.
  reset_system

  load_plugins
  run_plugins(true, attribute_filter)
end

reset_system方法里只是初始化了一些实例变量,其中包括@loader@runner

1
2
@loader = Ohai::Loader.new(self)
@runner = Ohai::Runner.new(self, true)

这里把self传了进去,这里的self就就Ohai::System的实例,我们看一下loader如何处理

lib/ohai/loader.rb
1
2
3
4
5
def initialize(controller)
  @controller = controller
  @v6_plugin_classes = []
  @v7_plugin_classes = []
end

loader把Ohai::System的实例保存到了实例变量@controller

load_plugins方法只有一行,调用了@loaderload_all方法,我们看一下这个方法

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
def load_all
  plugin_files_by_dir.each do |plugin_file|
    load_plugin_class(plugin_file.path, plugin_file.plugin_root)
  end

  collect_v6_plugins
  collect_v7_plugins
end

看一下plugin_files_by_dir的定义

lib/ohai/loader.rb
1
2
3
4
5
def plugin_files_by_dir
  Array(Ohai::Config[:plugin_path]).inject([]) do |plugin_files, plugin_path|
    plugin_files + PluginFile.find_all_in(plugin_path)
  end
end

注释中说它搜索所有的plugin路径并返回一个包含PluginFile对象的数组

在看一下PluginFile对象长什么样子

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
class PluginFile < Struct.new(:path, :plugin_root)

  def self.find_all_in(plugin_dir)
    Dir[File.join(plugin_dir, "**", "*.rb")].map do |file|
      new(file, plugin_dir)
    end
  end
end

可以看到它只是简单的从Struct继承而来,这里所起的作用就是定义了两个访问器pathplugin_root,打印出来类似这样

lib/ohai/loader.rb
1
2
3
#<struct Ohai::Loader::PluginFile
 path="/Users/william/Codes/ohai/lib/ohai/plugins/aix/cpu.rb",
 plugin_root="/Users/william/Codes/ohai/lib/ohai/plugins">

再回到load_all方法

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
def load_all
  plugin_files_by_dir.each do |plugin_file|
    load_plugin_class(plugin_file.path, plugin_file.plugin_root)
  end

  collect_v6_plugins
  collect_v7_plugins
end

对每一个PluginFile对象调用了load_plugin_class方法

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def load_plugin_class(plugin_path, plugin_dir_path=nil)
  # Read the contents of the plugin to understand if it's a V6 or V7 plugin.
  contents = ""
  begin
    contents << IO.read(plugin_path)
  rescue IOError, Errno::ENOENT
    Ohai::Log.warn("Unable to open or read plugin at #{plugin_path}")
    return nil
  end

  # We assume that a plugin is a V7 plugin if it contains Ohai.plugin in its contents.
  if contents.include?("Ohai.plugin")
    load_v7_plugin_class(contents, plugin_path)
  else
    Ohai::Log.warn("[DEPRECATION] Plugin at #{plugin_path} is a version 6 plugin. \
Version 6 plugins will not be supported in future releases of Ohai. \
Please upgrade your plugin to version 7 plugin syntax. \
For more information visit here: docs.opscode.com/ohai_custom.html")

    load_v6_plugin_class(contents, plugin_path, plugin_dir_path)
  end
end

他把文件里的内容读了进来,并根据有没有包含Ohai.plugin来有选择了调用load_v7_plugin_classload_v6_plugin_class,我大概看来一下基本上全都包含Ohai.plugin,所以我们从v7追

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def load_v7_plugin_class(contents, plugin_path)
  plugin_class = eval(contents, TOPLEVEL_BINDING, plugin_path)
  unless plugin_class.kind_of?(Class) and plugin_class < Ohai::DSL::Plugin
    raise Ohai::Exceptions::IllegalPluginDefinition, "Plugin file cannot contain any statements after the plugin definition"
  end
  plugin_class.sources << plugin_path
  @v7_plugin_classes << plugin_class unless @v7_plugin_classes.include?(plugin_class)
  plugin_class
rescue SystemExit, Interrupt
  raise
rescue Ohai::Exceptions::InvalidPluginName => e
  Ohai::Log.warn("Plugin Name Error: <#{plugin_path}>: #{e.message}")
rescue Ohai::Exceptions::IllegalPluginDefinition => e
  Ohai::Log.warn("Plugin Definition Error: <#{plugin_path}>: #{e.message}")
rescue NoMethodError => e
  Ohai::Log.warn("Plugin Method Error: <#{plugin_path}>: unsupported operation \'#{e.name}\'")
rescue SyntaxError => e
  # split on occurrences of
  #    <env>: syntax error,
  #    <env>:##: syntax error,
  # to remove from error message
  parts = e.message.split(/<.*>[:[0-9]+]*: syntax error, /)
  parts.each do |part|
    next if part.length == 0
    Ohai::Log.warn("Plugin Syntax Error: <#{plugin_path}>: #{part}")
  end
rescue Exception, Errno::ENOENT => e
  Ohai::Log.warn("Plugin Error: <#{plugin_path}>: #{e.message}")
  Ohai::Log.debug("Plugin Error: <#{plugin_path}>: #{e.inspect}, #{e.backtrace.join('\n')}")
end

异常处理我们不关心,直接删掉来看

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
9
def load_v7_plugin_class(contents, plugin_path)
  plugin_class = eval(contents, TOPLEVEL_BINDING, plugin_path)
  unless plugin_class.kind_of?(Class) and plugin_class < Ohai::DSL::Plugin
    raise Ohai::Exceptions::IllegalPluginDefinition, "Plugin file cannot contain any statements after the plugin definition"
  end
  plugin_class.sources << plugin_path
  @v7_plugin_classes << plugin_class unless @v7_plugin_classes.include?(plugin_class)
  plugin_class
end

这里第一行把插件文件里的代码执行,返回的结果赋给plugin_class,把plugin_path保存到其中,并把它收集到实例变量@v7_plugin_classes

我们找最简单了plugin看里面是什么

lib/ohai/plugins/command.rb
1
2
3
4
5
6
7
Ohai.plugin(:Command) do
  provides "command"

  collect_data do
    command Mash.new
  end
end

它是调用Ohaiplugin方法,还传给它一个block,看看这个方法的定义

lib/ohai/dsl/plugin.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def self.plugin(name, &block)
  raise Ohai::Exceptions::InvalidPluginName, "#{name} is not a valid plugin name. A valid plugin name is a symbol which begins with a capital letter and contains no underscores" unless NamedPlugin.valid_name?(name)

  plugin = nil

  if NamedPlugin.strict_const_defined?(name)
    plugin = NamedPlugin.const_get(name)
    plugin.class_eval(&block)
  else
    klass = Class.new(DSL::Plugin::VersionVII, &block)
    plugin = NamedPlugin.const_set(name, klass)
  end

  plugin
end

这个方法重点是这两行

lib/ohai/dsl/plugin.rb
1
2
klass = Class.new(DSL::Plugin::VersionVII, &block)
plugin = NamedPlugin.const_set(name, klass)

第一行用传进来了block创建了一个继承自DSL::Plugin::VersionVII的类,然后一传进来的name为常量名保存到模块NamedPlugin

我们在回到前面的plugin文件

lib/ohai/plugins/command.rb
1
2
3
4
5
6
7
Ohai.plugin(:Command) do
  provides "command"

  collect_data do
    command Mash.new
  end
end

这里还有两个方法providescollect_data,你没有猜错,它们就是定义在继承来的DSL::Plugin::VersionVII

lib/ohai/dsl/plugin/versionvii.rb
1
2
3
4
5
def self.provides(*attrs)
  attrs.each do |attr|
    provides_attrs << attr unless provides_attrs.include?(attr)
  end
end

它只是它传进来的参数收集到provides_atts,而provides_atts是前面生成的类的实例变量

lib/ohai/dsl/plugin/versionvii.rb
1
2
3
def self.provides_attrs
  @provides_attrs ||= []
end

在看collect_data

lib/ohai/dsl/plugin/versionvii.rb
1
2
3
4
5
6
7
8
9
def self.collect_data(platform = :default, *other_platforms, &block)
  [platform, other_platforms].flatten.each do |plat|
    if data_collector.has_key?(plat)
      raise Ohai::Exceptions::IllegalPluginDefinition, "collect_data already defined on platform #{plat}"
    else
      data_collector[plat] = block
    end
  end
end

由于前面的plugin中调用这个方法的时候只传了一个快进来,所从这个方法只是把传进来了块赋值给了以:default为key的Mash对象data_collector

lib/ohai/dsl/plugin/versionvii.rb
1
2
3
def self.data_collector
  @data_collector ||= Mash.new
end

我们再一次回到load_all方法中

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
def load_all
  plugin_files_by_dir.each do |plugin_file|
    load_plugin_class(plugin_file.path, plugin_file.plugin_root)
  end

  collect_v6_plugins
  collect_v7_plugins
end

还剩下两行代码,我们只看一下collect_v7_plugins

lib/ohai/loader.rb
1
2
3
4
5
def collect_v7_plugins
  @v7_plugin_classes.each do |plugin_class|
    load_v7_plugin(plugin_class)
  end
end

对收集的plugin_class调用load_v7_plugin方法

lib/ohai/loader.rb
1
2
3
4
5
def load_v7_plugin(plugin_class)
  plugin = plugin_class.new(@controller.data)
  collect_provides(plugin)
  plugin
end

这个方法把传进来的类实例化,并把Ohai::System实例的data传了进去,然后调用了collect_provides

lib/ohai/loader.rb
1
2
3
4
def collect_provides(plugin)
  plugin_provides = plugin.class.provides_attrs
  @controller.provides_map.set_providers_for(plugin, plugin_provides)
end

还记得前面最简单的plugin的代码吗,这里的plugin.class.provides_attrs就是provides后面的参数(“command”)

这里的provides_map是在Ohai::Systemreset_system赋值的,是ProvidesMap的实例

lib/ohai/loader.rb
1
@provides_map = ProvidesMap.new

然后它又调用了set_providers_for方法

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def set_providers_for(plugin, provided_attributes)
  unless plugin.kind_of?(Ohai::DSL::Plugin)
    raise ArgumentError, "set_providers_for only accepts Ohai Plugin classes (got: #{plugin})"
  end

  provided_attributes.each do |attribute|
    attrs = @map
    parts = normalize_and_validate(attribute)
    parts.each do |part|
      attrs[part] ||= Mash.new
      attrs = attrs[part]
    end
    attrs[:_plugins] ||= []
    attrs[:_plugins] << plugin
  end
end

这里把传来的keys它plugin收集到了providesmap的实例变量@map中,类似这样

lib/ohai/loader.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[2] pry(#<Ohai::ProvidesMap>)> @map
=> {"cpu"=>
     {"_plugins"=>
       [#<Ohai::NamedPlugin::CPU:0x00000101a6a4d0
         @data={},
         @has_run=false,
         @source=
           ["/Users/william/Codes/ohai/lib/ohai/plugins/aix/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/darwin/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/freebsd/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/linux/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/netbsd/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/openbsd/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/sigar/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/solaris2/cpu.rb",
             "/Users/william/Codes/ohai/lib/ohai/plugins/windows/cpu.rb"
           ],
         @version=:version7>
       ]
     }
   }

我们再回到all_plugins方法,前面是load_plugins方法的深入执行过程,现在来看run_plugins方法

lib/ohai/system.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def run_plugins(safe = false, attribute_filter = nil)
  # First run all the version 6 plugins
  @v6_dependency_solver.values.each do |v6plugin|
    @runner.run_plugin(v6plugin)
  end

  # Then run all the version 7 plugins
  begin
    @provides_map.all_plugins(attribute_filter).each { |plugin|
      @runner.run_plugin(plugin)
    }
  rescue Ohai::Exceptions::AttributeNotFound, Ohai::Exceptions::DependencyCycle => e
    Ohai::Log.error("Encountered error while running plugins: #{e.inspect}")
    raise
  end
end

我们只看v7的,这里比较简单,对每一个plugin调用@runnerrun_plugin方法(@runner)是在reset_system中定义的

lib/ohai/system.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def run_plugin(plugin)
  unless plugin.kind_of?(Ohai::DSL::Plugin)
    raise Ohai::Exceptions::InvalidPlugin, "Invalid plugin #{plugin} (must be an Ohai::DSL::Plugin or subclass)"
  end

  if Ohai::Config[:disabled_plugins].include?(plugin.name)
    Ohai::Log.debug("Skipping disabled plugin #{plugin.name}")
    return false
  end

  begin
    case plugin.version
    when :version7
      run_v7_plugin(plugin)
    when :version6
      run_v6_plugin(plugin)
    else
      raise Ohai::Exceptions::InvalidPlugin, "Invalid plugin version #{plugin.version} for plugin #{plugin}"
    end
  rescue Ohai::Exceptions::Error
    raise
  rescue Exception,Errno::ENOENT => e
    Ohai::Log.debug("Plugin #{plugin.name} threw exception #{e.inspect} #{e.backtrace.join("\n")}")
  end
end

简单说它就是转而去调用run_v7_plugin

lib/ohai/system.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def run_v7_plugin(plugin)
  visited = [ plugin ]
  while !visited.empty?
    next_plugin = visited.pop

    next if next_plugin.has_run?

    if visited.include?(next_plugin)
      raise Ohai::Exceptions::DependencyCycle, "Dependency cycle detected. Please refer to the following plugins: #{get_cycle(visited, plugin).join(", ") }"
    end

    dependency_providers = fetch_plugins(next_plugin.dependencies)

    # Remove the already ran plugins from dependencies if force is not set
    # Also remove the plugin that we are about to run from dependencies as well.
    dependency_providers.delete_if { |dep_plugin|
      dep_plugin.has_run? || dep_plugin.eql?(next_plugin)
    }

    if dependency_providers.empty?
      @safe_run ? next_plugin.safe_run : next_plugin.run
    else
      visited << next_plugin << dependency_providers.first
    end
  end
end

简单说就是去调用plugin自己的safe_run方法(因为在定义@runner的时候有传第二个参数true)

lib/ohai/system.rb
1
2
3
4
5
6
7
8
9
10
def safe_run
  begin
    self.run
  rescue Ohai::Exceptions::Error => e
    raise e
  rescue => e
    Ohai::Log.debug("Plugin #{self.name} threw #{e.inspect}")
    e.backtrace.each { |line| Ohai::Log.debug( line )}
  end
end

它又调用了run方法

lib/ohai/system.rb
1
2
3
4
def run
  @has_run = true
  run_plugin
end

它又调用了run_plugin方法,这个方法是在lib/ohai/dsl/plugin/versionvii.rb中定义的

lib/ohai/system.rb
1
2
3
4
5
6
7
8
9
10
11
12
def run_plugin
  collector = self.class.data_collector
  platform = collect_os

  if collector.has_key?(platform)
    self.instance_eval(&collector[platform])
  elsif collector.has_key?(:default)
    self.instance_eval(&collector[:default])
  else
    Ohai::Log.debug("No data to collect for plugin #{self.name}. Continuing...")
  end
end

它用instance_eval执行了前面动态构造Plugin类的时候保存下来的块(collect_data后面跟的块)

整个过程过了一遍,但有一点还没明白,最开始的地方我们发现显示出来的信息都收集在Ohai::System的实例@ohai的实例变量@data里,执行插件里的代码怎么会修改到它呢?

这是因为在实例化plugin的时候它把实例变量@data传了进去

lib/ohai/loader.rb
1
2
3
4
5
def load_v7_plugin(plugin_class)
  plugin = plugin_class.new(@controller.data)
  collect_provides(plugin)
  plugin
end

在看一下plugin的部分代码

lib/ohai/loader.rb
1
2
3
4
5
6
7
Ohai.plugin(:Command) do
  provides "command"

  collect_data do
    command Mash.new
  end
end

传给collect_data的块中的command是不是很奇怪,它是个什么东西?

答案的下面的代码中

lib/ohai/dsl/plugin.rb
1
2
3
4
5
6
7
8
9
def method_missing(name, *args)
  return get_attribute(name) if args.length == 0

  set_attribute(name, *args)
end

def get_attribute(name)
  @data[name]
end

到此所有迷雾都解开了, Yeah!!!

2014年规划

| Comments

2013年已经在昨天划上的句号,回想过去的这一年,真的可以说收获很多:

  • 毕业两年换了三个工作,终于在去年找到了自己满意的工作,同事个个都是高手,一周还能打两次蓝球
  • 终于用上了梦寐以求苹果电脑,虽然是公司配的不是自己买的,虽然Mac OSX系统没有想象中的好
  • 其实最重要的是去年这一年大部分时间是过的比较开心的,快乐一直是我追求的最终目标
  • 学会了发气咆音(vocal fry)和弹舌音
  • 没有生病
  • 学会了游泳

当然去年没有做好的事情也非常多,这里就不拿出来一一示众,没有做好的原因都是自己在各方面都不够成熟强大,接下来的这一年还需要花很多的时候去修行

今天是2014年的每一天,正好简单列一下对这一年自己的规划,一些希望能够完成的事情.为了提高可行性,每一项都分解成小步骤

感觉不用多久,我就会升职加薪,当上总经理,出任CEO,迎娶白富美,走上人生巅峰,想想还有点小激动呢

  • [x] 今天完成这个规划
  • [x] 学习吉他
    • [x] 首先需要买一把吉他
    • [ ] 学会<她来听我的演唱会>的弹唱
  • [ ] 读Rails源码,以问题的方式去读而不是从头一个文件一个文件的读,那样肯定完成不了
    • [ ] 列一些关于Rails如果实现的并且三兴趣的小问题,如:
      • [ ] Rails启动过程
      • [ ] 请求的处理过程
  • [ ] 可以轻松唱A2
    • [ ] 练习Brett Manning的Singing Success
    • [ ] 练习Brett Manning的Mastering Vibrato
    • [ ] 练习Brett Manning的Mastering Mix
    • [ ] 重复以上步骤走到达到目标
  • [ ] 见一次大海
    • [ ] 找到最这里最近的海在哪里
    • [ ] 查找路线
    • [ ] 找假期或请假去
  • [ ] 突破英语口语
    • [ ] 前半年继续用百词斩学单词,四级已经快学完了,之后背六级,再之后托福或雅思
    • [ ] 后半年可以考虑参加口语培训班
  • [x] 早起累计超过一百天
    • [x] 早起后在Twiter上记录方便以后统计.完成后就可以放肆的熬夜和赖床了(第一天就睡到11点,不是好的预兆呀)
  • [x] 学习佛教经典
    • [x] 先把已经买的相关的书看完:
      • [x] 读佛即是拜佛:六祖慧能传(已看完)
      • [x] 读佛即是拜佛:真实的唐僧
      • [x] 读佛即是拜佛:弥勒佛传
      • [x] 读佛即是拜佛:地藏菩萨传
  • [ ] 写5篇技术含量比较高的博客
    • [x] Chef Ohai源码学习
  • [ ] 进一步深入了解Linux
    • [ ] 现在对Linux的了解还是停在两年前的水平,现在还没想好可行的方案,有可能会是
      • [ ] 读源代码,可以从openSUSE中的已经用Ruby重写了的yast开始
      • [ ] 玩开源硬件(目前一点兴趣都没有)
  • [ ] 读很多年前就想读但由于各种原因一直没有开始的几本书(都比较厚,可能没太多的时间,能读一本算一本了)
    • [ ] 算法导论
    • [ ] 代码大全
    • [ ] 编译原理(龙书)
  • [ ] 进一步深入了解工作中遇到的或者自己感兴趣的技术或语言
    • [ ] Javascript
      • [ ] 把买的犀牛书看完,不然太浪费了
    • [ ] Erlang
      • [ ] 把豆瓣上买的Erlang程序设计看完,不然太浪费了(豆瓣阅读真TM的烂)
      • [ ] 把已经买的Erlang/OTP并发编程实战看完,不然太浪费了
  • [x] 加入骑行队伍
    • [x] 买一辆公路自行车