部署 gem mina 的学习

bajiudongfeng · 2017年04月12日 · 2421 次阅读

有些地方理解的还不是很到位,还请大家指点.

1. 概述

mina 是一个快速部署工具。原理:

  • 将部署过程需要用到的所有命令放置到数组之中
  • 转换成对应的字符串
  • 登录远程服务器,执行命令

2. 深入分析

  • mina 主体代码结构:

  • 各个文件作用简述:

Mina::Application 类 主要是初始化 mina,执行队列中的命令. 主要方法 run

Mina::Backend::LocalMina::Backend::Remote 类 一个是在当前机器执行命令,一个是在远程服务器执行命令。 关键方法:ssh

Mina::Helpers::Internal 模块 主要是对命令进行一些字符串方面的处理,记录部署花费时间等.<br> Mina::Helpers::Output 模块 主要是输出的操作,用不同的颜色表示不同信息的输出。

Mina::Runner::Exec Mina::Runner::Pretty Mina::Runner::Printer Mina::Runner::System 这几个类实现了 ruby 调用 shell 的几种形式。

Mina::Commands 类 定义了对命令的一些基本操作. 例如: 方法:command 将命令加入队列 方法:process 将命令的队列转换成字符串 方法:run 执行命令

Mina::Configuration 类 定义了 mina 中最基本的方法 例如: set fetch

Mina::DSL 模块 定义了一些关键的,会直接使用到的方法 例如: 方法:invoke 让任务可以被调用 方法:in_path 在某个目录下运行命令 方法:run 执行某个命令

Mina::Runner 类 主要是命令的最后执行.

3. 举例说明运行流程

  • 例子:

    mina run["ls -l"]

  • mina 任务源码(tasks/mina/default.rb) 接下来分析下 mina 运行这个命令的流程 mina 定义了运行命令的任务

desc 'Runs a command in the server.'
task :run, [:command] do |_, args|
  ensure!(:deploy_to)
  command = args[:command]

  unless command
    puts "You need to provide a command. Try: mina 'run[ls -la]'"
    exit 1
  end

  in_path fetch(:deploy_to) do
    command command
  end
end
  • 起点和终点

所有的 mina 任务的运行都要先执行文件 bin/mina

#!/usr/bin/env ruby require 'mina' Mina::Application.new.run

初始化 mina,同时开始执行命令

我们来看这个过程都发生了那些操作,关键点:

def run
  Rake.application = self
  super
end
  1. 将 Mina::Application 初始化对象复制给 Rake.application
  2. 调用父类 Rake::Application 的 run 方法,这个会导致对方法top_level_tasks调用。其中很关键的是: :run_commands.这个会导致对 run_commands 这个任务的执行(稍后会提到)

之后会回到任务mina run["ls -l"] 之中

ensure!(:deploy_to)
command = args[:command]

确认部署目录有值,获取参数

核心来了

in_path fetch(:deploy_to) do
    command command
 end

我们来看 in_path 方法:

def in_path(path, indent: nil)
  real_commands = commands
  @commands = Commands.new
  yield
  real_commands.command(commands.process(path), quiet: true, indent: indent)
  @commands = real_commands
end

yield中的操作也就是command command 这是将命令ls -l放入队列之中.

def initialize(stage = :default)
  @stage = stage
  @queue = Hash.new { |hash, key| hash[key] = [] }
end

def command(code, strip: true, quiet: false, indent: nil)
  code = unindent(code) if strip
  code = indent(indent, code) if indent
  queue[stage] << (quiet ? code : echo_cmd(code))
end

对命令进行一些简单的处理,然后加入队列queue[:default]中. 类似结果:

#<Mina::Commands:0x0000000291a5d0 @stage=:default, @queue={:default=>["ls -l"]}>

将命令进行一些处理再次放入队列之中

def process(path = nil)
  if path
    queue[stage].unshift(%{echo "$ cd #{path}"}) if fetch(:verbose)
    %{(cd #{path} && #{queue[stage].join(' && ')} && cd -)}
  else
    queue[stage].join("\n")
  end
end

类似结果: > #<Mina::Commands:0x0000000291a698 @stage=:default, @queue={:default=>["(cd /home/zhang/dongfeng && ls -l && cd -)"]}>

接下来会执行任务 run_commands,这个应该还是Mina::Applicationrun 方法中的super的因素。具体原因还不清楚.

task :environment do
end

task :run_commands do
  if commands.run_default?
    invoke :environment
    commands.run(:remote)
  end
end

当 stage == :default 且命令队列不为空的时候才执行. 会调用任务 mina environment,默认为空,但是项目中的配置文件一般会覆写这个任务用来加载ruby环境,例如rvm等.

快到最后时刻了

def report_time
       time_start = Time.now
       output = yield
       print_info "Elapsed time: %.2f seconds" % [Time.now - time_start]
       output
end

定义了计算时间的方法.


def run(backend)
      return if queue.empty?
      report_time do
        status = Mina::Runner.new(process, backend).run
        error! 'Run Error' unless status
      end
 end

命令的执行即将开始. 关键参数类似结果:

process : (cd /home/zhang/dongfeng && ls -l && cd -) backend: remote

Mina::Runner的关键方法:

 def run
      Mina::Runner.const_get(class_name_for(execution_mode)).new(script).run
 end
private

def script
    Mina::Backend.const_get(class_name_for(backend)).new(commands).prepare
end

Mina::Runner.const_get(class_name_for(execution_mode)) 主要就是选用那种执行方式:

Mina::Runner::Exec Mina::Runner::Pretty Mina::Runner::Printer Mina::Runner::System

而 script 则确定在远程还是在本地执行,此处是在远程执行.

def prepare
  if fetch(:simulate)
    [
      '#!/usr/bin/env bash', "# Executing the following via '#{ssh}':",
      '#', commands, ' '
    ].join("\n")
  else
    command = Shellwords.escape(commands)
    w = [ssh, '--', command].join(' ')
  end
end

def ssh
  ensure!(:domain)
  args = fetch(:domain)
  args = "#{fetch(:user)}@#{fetch(:domain)}" if set?(:user)
  args += " -i #{fetch(:identity_file)}" if set?(:identity_file)
  args += " -p #{fetch(:port)}" if set?(:port)
  args += ' -A' if set?(:forward_agent)
  args += " #{fetch(:ssh_options)}" if set?(:ssh_options)
  args += ' -tt'
  "ssh #{args}"
end

prepare 会对命令进行一些处理 类似结果:

(cd\ /home/zhang/dongfeng\ &&\ ls\ -l\ &&\ cd\ -)

然后把 ssh 远程登录的命令加入到所有命令的最开始处. 类似结果:

ssh user_name@***.xyz -p 22 -tt -- (cd\ /home/zhang/dongfeng\ &&\ ls\ -l\ &&\ cd\ -)

此时就开始登录远程服务器执行命令了,静待即可.

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请 注册新账号