美文网首页
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