Class: RemoteRails::Server

Inherits:
Object
  • Object
show all
Defined in:
lib/rrails/server.rb

Overview

server to run rails/rake command.

@example
  server = RemoteRails::Server.new(:rails_env => "development")
  server.start

Constant Summary collapse

PAGE_SIZE =
4096

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ Server

Returns a new instance of Server.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/rrails/server.rb', line 24

def initialize(options={})
  @rails_env  = options[:rails_env] || ENV['RAILS_ENV'] || "development"
  @pidfile    = "#{options[:pidfile] || './tmp/pids/rrails-'}#{@rails_env}.pid"
  @background = options[:background] || false
  if (options[:host] || options[:port]) && !options[:socket]
    @socket   = nil
    @host     = options[:host] || 'localhost'
    @port     = options[:port] || DEFAULT_PORT[@rails_env]
  else
    @socket   = "#{options[:socket] || './tmp/sockets/rrails-'}#{@rails_env}.socket"
  end
  @app_path   = File.expand_path('./config/application')
  @logger     = Logger.new(options[:logfile] ? options[:logfile] : (@background ? nil : STDERR))
  @logger.level = options[:loglevel] || 0
end

Instance Method Details

#alive?Boolean

Returns:

  • (Boolean)


60
61
62
# File 'lib/rrails/server.rb', line 60

def alive?
  previous_instance ? true : false
end

#boot_railsObject



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/rrails/server.rb', line 149

def boot_rails
  @logger.info("prepare rails environment (#{@rails_env})")
  ENV["RAILS_ENV"] = @rails_env

  # make IRB = Pry hacks (https://gist.github.com/941174) work:
  # pre-require all irb compoments needed in rails/commands
  # otherwise 'module IRB' will cause 'IRB is not a module' error.
  require 'irb'
  require 'irb/completion'

  require File.expand_path('./config/environment')

  unless Rails.application.config.cache_classes
    ActionDispatch::Reloader.cleanup!
    ActionDispatch::Reloader.prepare!
  end
  @logger.info("finished preparing rails environment")
end

#dispatch(sock, line, pty = false) ⇒ Object



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/rrails/server.rb', line 168

def dispatch(sock, line, pty=false)
  if pty
    m_out, c_out = PTY.open
    c_in = c_err = c_out
    m_fds = [m_out, c_out]
    c_fds = [c_out]
    clisocks = {in: m_out, out: m_out}
  else
    c_in, m_in = IO.pipe
    m_out, c_out = IO.pipe
    m_err, c_err = IO.pipe
    m_fds = [m_in, m_out, m_err]
    c_fds = [c_in, c_out, c_err]
    clisocks = {in: m_in, out: m_out, err: m_err}
  end

  running = true
  heartbeat = 0

  pid = fork do
    m_fds.map(&:close) if not pty
    STDIN.reopen(c_in)
    STDOUT.reopen(c_out)
    STDERR.reopen(c_err)
    ActiveRecord::Base.establish_connection if defined?(ActiveRecord::Base)
    execute *Shellwords.shellsplit(line)
  end

  c_fds.map(&:close) if not pty

  # pump input. since it will block, make it in another thread
  thread = Thread.start do
    while running do
      begin
        input = sock.__send__(pty ? :getc : :gets)
      rescue => ex
        @logger.debug "input thread got #{ex}"
        running = false
      end
      clisocks[:in].write(input) rescue nil
    end
  end

  loop do
    [:out, :err].each do |channel|
      next if not clisocks[channel]
      begin
        loop do
          response = clisocks[channel].read_nonblock(PAGE_SIZE)
          sock.puts("#{channel.upcase}\t#{response.bytes.to_a.join(',')}")
          sock.flush
        end
      rescue Errno::EAGAIN, EOFError => ex
        next
      end
    end

    if running
      _, stat = Process.waitpid2(pid, Process::WNOHANG)
      if stat
        @logger.debug "child exits. #{stat}"
        return stat
      end
    end

    # send heartbeat so that we got EPIPE immediately when client dies
    heartbeat += 1
    if heartbeat > 20
      sock.puts("PING")
      sock.flush
      heartbeat = 0
    end

    # do not make CPU hot
    sleep 0.025
  end
ensure
  running = false
  [*c_fds, *m_fds].each {|io| io.close unless io.closed?}
  if pid
    begin
      Process.kill 0, pid
      @logger.debug "killing pid #{pid}"
      Process.kill 'TERM', pid rescue nil
    rescue Errno::ESRCH
    end
  end
  thread.kill if thread
end

#reloadObject



55
56
57
58
# File 'lib/rrails/server.rb', line 55

def reload
  pid = previous_instance
  Process.kill :HUP, pid
end

#restartObject



50
51
52
53
# File 'lib/rrails/server.rb', line 50

def restart
  stop && sleep(1)
  start
end

#startObject

Raises:

  • (RuntimeError)


73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/rrails/server.rb', line 73

def start
  # check previous process
  raise RuntimeError.new('rrails is already running') if alive?

  if @background
    pid = Process.fork do
      @background = false
      start
    end
    Process.detach(pid)
    return
  end

  # make 'bundle exec' not necessary for most time.
  require 'bundler/setup'

  begin
    [@pidfile, @socket].compact.each do |path|
      FileUtils.rm_f path
      FileUtils.mkdir_p File.dirname(path)
    end

    File.write(@pidfile, $$)
    server = if @socket
               UNIXServer.open(@socket)
             else
               TCPServer.open(@host, @port)
             end
    server.close_on_exec = true

    @logger.info("starting rrails server: #{@socket || "#{@host}:#{@port}"}")

    [:INT, :TERM].each do |sig|
      trap(sig) do
        @logger.info("SIG#{sig} recieved. shutdown...")
        exit
      end
    end

    trap(:HUP) do
      @logger.info("SIGHUP recieved. reload...")
      ActionDispatch::Callbacks.new(Proc.new {}).call({})
      self.boot_rails
    end

    self.boot_rails

    Thread.abort_on_exception = true

    loop do
      Thread.start(server.accept) do |s|
        @logger.debug("accepted")
        begin
          line = s.gets.chomp
          pty, line = (line[0] == 'P'), line[1..-1]
          @logger.info("invoke: #{line} (pty=#{pty})")
          status = nil
          time = Benchmark.realtime do
            status = dispatch(s, line, pty)
          end
          exitcode = status ? status.exitstatus || (status.termsig + 128) : 0
          s.puts("EXIT\t#{exitcode}")
          s.flush
          @logger.info("finished: #{line} (#{time} seconds)")
        rescue Errno::EPIPE
          @logger.info("disconnected: #{line}")
        end
      end
    end
  ensure
    server.close unless server.closed?
    @logger.info("cleaning pid and socket files...")
    FileUtils.rm_f [@socket, @pidfile].compact
  end
end

#statusObject



64
65
66
67
68
69
70
71
# File 'lib/rrails/server.rb', line 64

def status
  pid = previous_instance
  if pid
    puts "running \tpid = #{pid}"
  else
    puts 'stopped'
  end
end

#stopObject



40
41
42
43
44
45
46
47
48
# File 'lib/rrails/server.rb', line 40

def stop
  pid = previous_instance
  if pid
    @logger.info "stopping previous instance #{pid}"
    Process.kill :TERM, pid
    FileUtils.rm_f [@socket, @pidfile]
    return true
  end
end