美文网首页
rails spring 的实现原理一 client部分

rails spring 的实现原理一 client部分

作者: will2yang | 来源:发表于2019-01-06 21:50 被阅读0次

我们从加载spring的入口开始看代码:

# bin/rails
begin
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
end
# bin/spring
unless defined?(Spring)
  require 'rubygems'
  require 'bundler'

  lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read)
  spring = lockfile.specs.detect { |spec| spec.name == "spring" }
  if spring
    Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path
    gem 'spring', spring.version
    require 'spring/binstub'
  end
end

binstub的代码主要是为了根据命令类型处理ARGV,然后加载bin/spring:

# binstub
load bin_path

sprin里主要是调用了Client.run

lib = File.expand_path("../../lib", __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) # enable local development
require 'spring/client'
Spring::Client.run(ARGV)

然后根据给予的ARGV运行相应的call,接下在我们主要使用console作为代码学习的路线.

# spring/client
require "spring/errors"
require "spring/json"

require "spring/client/command"
require "spring/client/run"
require "spring/client/help"
require "spring/client/binstub"
require "spring/client/stop"
require "spring/client/status"
require "spring/client/rails"
require "spring/client/version"
require "spring/client/server"

module Spring
  module Client
    COMMANDS = {
      "help"      => Client::Help,
      "-h"        => Client::Help,
      "--help"    => Client::Help,
      "binstub"   => Client::Binstub,
      "stop"      => Client::Stop,
      "status"    => Client::Status,
      "rails"     => Client::Rails,
      "-v"        => Client::Version,
      "--version" => Client::Version,
      "server"    => Client::Server,
    }

    def self.run(args)
      command_for(args.first).call(args)
    rescue CommandNotFound
      Client::Help.call(args)
    rescue ClientError => e
      $stderr.puts e.message
      exit 1
    end

    def self.command_for(name)
      COMMANDS[name] || Client::Run
    end
  end
end

# allow users to add hooks that do not run in the server
# or modify start/stop
if File.exist?("config/spring_client.rb")
  require "./config/spring_client.rb"
end

这里找到Client::Rails文件,然后调用他的call方法:

require "set"

module Spring
  module Client
    class Rails < Command
      COMMANDS = Set.new %w(console runner generate destroy test)

      ALIASES = {
        "c" => "console",
        "r" => "runner",
        "g" => "generate",
        "d" => "destroy",
        "t" => "test"
      }

      def self.description
        "Run a rails command. The following sub commands will use spring: #{COMMANDS.to_a.join ', '}."
      end

      def call
        command_name = ALIASES[args[1]] || args[1]

        if COMMANDS.include?(command_name)
          Run.call(["rails_#{command_name}", *args.drop(2)])
        else
          require "spring/configuration"
          ARGV.shift
          load Dir.glob(Spring.application_root_path.join("{bin,script}/rails")).first
          exit
        end
      end
    end
  end
end

注意,以上这个文件里的call实际上调用的是Comand的self.call方法, 而主要作用就是实例化Client::Rails然后调用他的call:

require "spring/env"

module Spring
  module Client
    class Command
      def self.call(args)
        new(args).call
      end

      attr_reader :args, :env

      def initialize(args)
        @args = args
        @env  = Env.new
      end
    end
  end
end

Client::Rails的call调用了Run的call方法:

def call
  begin
    connect
  rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
    cold_run
  else
    warm_run
  end
ensure
  server.close if server
end

这个方法存在两种情况当spring服务没有打开的时候会跑cold_run:

def cold_run
  boot_server
  connect
  run
end

开启spring的服务

def boot_server
  env.socket_path.unlink if env.socket_path.exist?

  pid     = Process.spawn(gem_env, env.server_command, out: File::NULL)
  timeout = Time.now + BOOT_TIMEOUT

  @server_booted = true

  until env.socket_path.exist?
    _, status = Process.waitpid2(pid, Process::WNOHANG)

    if status
      exit status.exitstatus
    elsif Time.now > timeout
      $stderr.puts "Starting Spring server with `#{env.server_command}` " \
                   "timed out after #{BOOT_TIMEOUT} seconds"
      exit 1
    end

    sleep 0.1
  end
end

连上spring服务的connect

def connect
  @server = UNIXSocket.open(env.socket_name)
end
def run
  verify_server_version

  application, client = UNIXSocket.pair

  queue_signals
  connect_to_application(client)
  run_command(client, application)
rescue Errno::ECONNRESET
  exit 1
end

def connect_to_application(client)
  server.send_io client
  send_json server, "args" => args, "default_rails_env" => default_rails_env

  if IO.select([server], [], [], CONNECT_TIMEOUT)
    server.gets or raise CommandNotFound
  else
    raise "Error connecting to Spring server"
  end
end

def run_command(client, application)
  log "sending command"

  application.send_io STDOUT
  application.send_io STDERR
  application.send_io STDIN

  send_json application, "args" => args, "env" => ENV.to_hash

  pid = server.gets
  pid = pid.chomp if pid

  # We must not close the client socket until we are sure that the application has
  # received the FD. Otherwise the FD can end up getting closed while it's in the server
  # socket buffer on OS X. This doesn't happen on Linux.
  client.close

  if pid && !pid.empty?
    log "got pid: #{pid}"

    forward_signals(application)
    status = application.read.to_i

    log "got exit status #{status}"

    exit status
  else
    log "got no pid"
    exit 1
  end
ensure
  application.close
end

UNIXSocket.pair主要是打开了一对相互连接的socket.然后在connect_to_application里我们看到了将我们的client对象发送给spring的服务,以及在run_command里的send_io将标准输出流、标准输入流和标准错流发送给client,这样我们的客户端的输出输入就完全被spring服务的预加载好的进程代理了。

在server打开的情况下就跑warn_run,主要就是spring服务开好了,之后的代码执行流程和cold_run的后半部分相同:

def warm_run
  run
rescue CommandNotFound
  require "spring/commands"

  if Spring.command?(args.first)
    # Command installed since spring started
    stop_server
    cold_run
  else
    raise
  end
end

相关文章

网友评论

      本文标题:rails spring 的实现原理一 client部分

      本文链接:https://www.haomeiwen.com/subject/purerqtx.html