Class: By::Worker

Inherits:
Object
  • Object
show all
Defined in:
lib/by/worker.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(socket) ⇒ Worker

Create the worker. Arguments

socket

The Unix socket to use to communicate with the client.

sigterm_handler



14
15
16
17
# File 'lib/by/worker.rb', line 14

def initialize(socket)
  @socket = socket
  @normal_exit = nil
end

Instance Attribute Details

#normal_exit=(value) ⇒ Object (writeonly)

Whether the worker process should signal a normal exit to the client. The default is nil, which signals a normal exit when the worker process exits normally. This can be set to false to signal an abnormal exit, such as to indicate test failures.



9
10
11
# File 'lib/by/worker.rb', line 9

def normal_exit=(value)
  @normal_exit = value
end

Instance Method Details

#chdir(dir) ⇒ Object

Change to the given directory.



78
79
80
# File 'lib/by/worker.rb', line 78

def chdir(dir)
  Dir.chdir(dir)
end

#chdir_or_stopObject

Change to the given directory, unless the client is telling the worker to stop the server.



44
45
46
47
# File 'lib/by/worker.rb', line 44

def chdir_or_stop
  arg = @socket.readline("\0", chomp: true)
  arg == 'stop' ? stop_server : chdir(arg)
end

#cleanup_procObject

A proc for communicating exit status to the client, and printing the loaded features if configured. Used during process shutdown.



90
91
92
93
94
95
96
97
98
99
100
# File 'lib/by/worker.rb', line 90

def cleanup_proc
  proc do
    if @normal_exit.nil?
      @normal_exit = $!.nil? || ($!.is_a?(SystemExit) && $!.success?)
    end
    @socket.write(@normal_exit ? '0' : '1')
    @socket.shutdown(Socket::SHUT_WR)
    @socket.close
    print_loaded_features if ENV['DEBUG'] == 'log'
  end
end

#get_argsObject

An array of arguments provided by the client.



114
115
116
117
118
119
120
# File 'lib/by/worker.rb', line 114

def get_args
  args = []
  while !@socket.eof?
    args << @socket.readline("\0", chomp: true)
  end
  args
end

#handle_args(args) ⇒ Object

Handle arguments provided by the client. Ensure correct client after handling the arguments.



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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
# File 'lib/by/worker.rb', line 124

def handle_args(args)
  worker = self
  cleanup = cleanup_proc

  case arg = args.first
  when 'm', /\.rb:\d+\z/
    args.shift if arg == 'm'
    ARGV.replace(args)
    require 'm'
    M.define_singleton_method(:exit!) do |exit_code|
      worker.normal_exit = exit_code == true
      cleanup.call
      super(exit_code)
    end
    M.run(args)
  when 'rspec'
    args.shift
    ARGV.replace(args)
    require 'rspec/core'
    RSpec.configure do |c|
      # Fix start_time to be accurate if rspec was required by by-server
      c.instance_variable_set(:@start_time, Time.now)
    end
    # invoke exits non-zero if there are failures
    RSpec::Core::Runner.define_singleton_method(:exit) do |exit_code|
      worker.normal_exit = exit_code == 0
      cleanup.call
      super(exit_code)
    end
    RSpec::Core::Runner.invoke
    at_exit(&cleanup)
  when 'irb'
    at_exit(&cleanup)
    args.shift
    ARGV.replace(args)
    require 'irb'
    IRB.start(__FILE__)
  when '-e'
    at_exit(&cleanup)
    unless args.length >= 2
      $stderr.puts 'no code specified for -e (RuntimeError)'
      exit(1)
    end
    args.shift
    code = args.shift
    ARGV.replace(args)
    ::TOPLEVEL_BINDING.eval(code)
  when String
    args.shift
    ARGV.replace(args)

    begin
      require File.expand_path(arg)
    rescue
      @normal_exit = false
      at_exit(&cleanup)
      raise
    end

    if defined?(Minitest) && Minitest.class_variable_get(:@@installed_at_exit)
      Minitest.singleton_class.prepend(Module.new do
        define_method(:run) do |argv|
          super(argv).tap{|exit_code| worker.normal_exit = exit_code == true}
        end
      end)
      Minitest.after_run(&cleanup)
    else
      at_exit(&cleanup)
    end
  else
    # no arguments
    at_exit(&cleanup)
    ::TOPLEVEL_BINDING.eval($stdin.read)
  end
end

Print $LOADED_FEATURES to stdout.



83
84
85
# File 'lib/by/worker.rb', line 83

def print_loaded_features
  puts $LOADED_FEATURES
end

#reopen_stdioObject

Replace stdin, stdout, stderr with the IO values provided by the client.



36
37
38
39
40
# File 'lib/by/worker.rb', line 36

def reopen_stdio
  $stdin.reopen(@socket.recv_io(IO))
  $stdout.reopen(@socket.recv_io(IO))
  $stderr.reopen(@socket.recv_io(IO))
end

#replace_envObject

Replace ENV with the environment provided by the client.



103
104
105
106
107
108
109
110
111
# File 'lib/by/worker.rb', line 103

def replace_env
  env = {}
  while line = @socket.readline("\0", chomp: true)
    break if line.empty?
    k, v = line.split("=")
    env[k] = v
  end
  ENV.replace(env)
end

#runObject

Run the worker process.



20
21
22
23
24
25
26
# File 'lib/by/worker.rb', line 20

def run
  write_pid
  reopen_stdio
  chdir_or_stop
  replace_env
  handle_args(get_args)
end

#stop_serverObject

Stop the server process by sending it the SIGQUIT signal, then exit.



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/by/worker.rb', line 50

def stop_server
  i = 0
  while Process.ppid != 1 && Process.kill(0, Process.ppid)
    if i < 4
      Process.kill(:QUIT, Process.ppid)
    end
    sleep 0.1

    if i == 5
      # This is only reached if the QUIT signal does not
      # cause the process to exit.
      Process.kill(:KILL, Process.ppid)
    end
    if i > 8
      # This is only reached if the process has still not
      # stopped even after the KILL signal was sent.
      $stderr.puts "ERROR: cannot stop by-server"
      @normal_exit = false
      break
    end

    i += 1
  end
  cleanup_proc.call
  exit
end

#write_pidObject

Write the current process pid to the client, to signal that the worker process is ready.



30
31
32
33
# File 'lib/by/worker.rb', line 30

def write_pid
  @socket.write($$.to_s)
  @socket.write("\0")
end