Class: By::Server

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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(socket_path: default_socket_path, argv: default_argv, debug: default_debug, daemonize: default_daemonize, daemon_args: default_daemon_args, worker_class: default_worker_class) ⇒ Server

Creates a new server. Arguments: socket_path: The path to the UNIX socket to create and listen on. argv: The arguments to the server, which are libraries to be required by default. debug: If set, operations on an existing server socket will be logged.

If the value is <tt>'log'</tt>, <tt>$LOADED_FEATURES</tt> will also be logged to the stdout
after libraries have been required.

daemonize: Whether to daemonize, true by default. daemon_args: Arguments to use when daemonizing, [false, false] by default. worker_class: The class to use for worker process handling, Worker by default.



30
31
32
33
34
35
36
37
38
39
40
# File 'lib/by/server.rb', line 30

def initialize(socket_path: default_socket_path, argv: default_argv, debug: default_debug,
               daemonize: default_daemonize, daemon_args: default_daemon_args,
               worker_class: default_worker_class)
  @socket_path = socket_path
  @argv = argv
  @debug = debug
  if @daemonize = !!daemonize
    @daemon_args = Array(daemon_args)
  end
  @worker_class = worker_class
end

Class Method Details

.with_argument_handler(&block) ⇒ Object

Return a subclass that will use a Worker subclass with a handle_args method defined by the given block. Allows for easily customizing/overriding the default argument handling.



12
13
14
15
16
17
18
19
# File 'lib/by/server.rb', line 12

def self.with_argument_handler(&block)
  worker_subclass = Class.new(new.default_worker_class) do
    define_method(:handle_args, &block)
  end
  Class.new(self) do
    define_method(:default_worker_class){worker_subclass}
  end
end

Instance Method Details

#accept_clientObject

Accept a new client connection, or return nil if stop_accepting_clients! has been called.



181
182
183
184
185
186
# File 'lib/by/server.rb', line 181

def accept_client
  @socket.accept
rescue IOError, Errno::EBADF
  # likely closed stream, return nil to exit accept_clients loop
  nil
end

#accept_clientsObject

Accept each client connection and fork a worker socket for it. Terminate loop when stop_accepting_clients! is called.



173
174
175
176
177
# File 'lib/by/server.rb', line 173

def accept_clients
  while socket = accept_client
    fork_worker(socket)
  end
end

#auto_require_filesObject

Files to automatically require, uses the BY_SERVER_AUTO_REQUIRE environment variable by default.



132
133
134
# File 'lib/by/server.rb', line 132

def auto_require_files
  (ENV['BY_SERVER_AUTO_REQUIRE'] || '').split
end

#daemonizeObject

Daemonize with configured daemon args using Process.daemon.



146
147
148
# File 'lib/by/server.rb', line 146

def daemonize
  Process.daemon(*@daemon_args)
end

#daemonize?Boolean

Whether to daemonize.

Returns:

  • (Boolean)


151
152
153
# File 'lib/by/server.rb', line 151

def daemonize?
  !!@daemonize
end

#default_argvObject

The default server arguments, uses ARGV by default.



49
50
51
# File 'lib/by/server.rb', line 49

def default_argv
  ARGV
end

#default_daemon_argsObject

The default arguments when daemonizing. By default, considers the BY_SERVER_DAEMON_NO_CHDIR and BY_SERVER_DAEMON_NO_REDIR_STDIO environment variables.



67
68
69
# File 'lib/by/server.rb', line 67

def default_daemon_args
  [!!ENV['BY_SERVER_DAEMON_NO_CHDIR'], !!ENV['BY_SERVER_DAEMON_NO_REDIR_STDIO']]
end

#default_daemonizeObject

The default for whether to daemonize. It is true if the BY_SERVER_NO_DAEMON environment variable is not set.



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

def default_daemonize
  !ENV['BY_SERVER_NO_DAEMON']
end

#default_debugObject

The default debug mode. This uses and removes the DEBUG environment variable.



54
55
56
# File 'lib/by/server.rb', line 54

def default_debug
  ENV.delete('DEBUG')
end

#default_socket_pathObject

The default socket path to use. Use the BY_SOCKET environment variable if set, or ~/.by_socket if not set.



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

def default_socket_path
  ENV['BY_SOCKET'] || File.join(ENV["HOME"], '.by_socket')
end

#default_worker_classObject

The default worker class to use for worker processes, Worker by default.



72
73
74
# File 'lib/by/server.rb', line 72

def default_worker_class
  Worker
end

#fork_worker(socket) ⇒ Object

Fork a worker process to handle the client connection. Close the given socket after the fork, so the socket will open be open in the worker process.



197
198
199
200
201
202
203
204
# File 'lib/by/server.rb', line 197

def fork_worker(socket)
  Process.detach(Process.fork do
    Signal.trap(:QUIT, @sigquit_default)
    Signal.trap(:TERM, @sigterm_default)
    @worker_class.new(socket).run
  end)
  socket.close
end

#handle_argvObject

Handle arguments provided to the server. Requires each argument by default.



125
126
127
128
# File 'lib/by/server.rb', line 125

def handle_argv
  (auto_require_files + @argv).each{|f| require f}
  print_loaded_features if @debug == 'log'
end

#handle_existing_serverObject

Handle an existing server socket. This attempts to connect to the socket and then shutdown the server. If successful, it removes the socket. If unsuccessful, it will print an error.



94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/by/server.rb', line 94

def handle_existing_server
  if File.socket?(@socket_path)
    begin
      @socket = UNIXSocket.new(@socket_path)
      print "Shutting down existing by_server at #{@socket_path}..." if @debug
      raise "Invalid by_server worker pid" unless @socket.readline("\0", chomp: true).to_i > 1
      @socket.send_io($stdin)
      @socket.send_io($stdout)
      @socket.send_io($stderr)
      @socket.write("stop")
      @socket.shutdown(Socket::SHUT_WR)
      @socket.read
      @socket.close
    rescue => e
      puts "FAILED!!!" if @debug
      $stderr.puts "Error shutting down server on existing socket: #{e.class}: #{e.message}"
      exit(1)
    else
      puts "Success!" if @debug
    end
    @socket = nil
    File.delete(@socket_path)
  end
end

Print $LOADED_FEATURES to stdout.



207
208
209
# File 'lib/by/server.rb', line 207

def print_loaded_features
  puts $LOADED_FEATURES
end

#runObject

Runs the server. This will not terminate until the server receives SIGTERM or stop_accepting_clients! is called manually.

If stop? is true, does not run a server, just handles an existing server.



80
81
82
83
84
85
86
87
88
89
# File 'lib/by/server.rb', line 80

def run
  handle_existing_server
  return if stop?

  handle_argv
  setup_server
  daemonize if daemonize?
  setup_signals
  accept_clients
end

#setup_serverObject

Creates and listens on the server socket.



137
138
139
140
141
142
143
# File 'lib/by/server.rb', line 137

def setup_server
  # Prevent TOCTOU on server socket creation
  umask = File.umask(077)
  @socket = UNIXServer.new(@socket_path)
  File.umask(umask)
  system('chmod', '600', @socket_path)
end

#setup_signalsObject

Trap SIGTERM and have it stop accepting clients. Trap SIGTERM and have it remove the socket and stop accepting clients.



157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/by/server.rb', line 157

def setup_signals
  @sigquit_default = Signal.trap(:QUIT) do
    stop_accepting_clients!
  end
  @sigterm_default = Signal.trap(:TERM) do
    begin
      File.delete(@socket_path)
    rescue Errno::ENOENT
      # server socket already deleted, ignore
    end
    stop_accepting_clients!
  end
end

#stop?Boolean

Whether to only stop an existing server and not start a new server.

Returns:

  • (Boolean)


120
121
122
# File 'lib/by/server.rb', line 120

def stop?
  @argv == ['stop']
end

#stop_accepting_clients!Object

Close the server socket. This will trigger the accept_clients loop to terminate.



190
191
192
# File 'lib/by/server.rb', line 190

def stop_accepting_clients!
  @socket.close
end