lib/spider-gazelle/options.rb
# frozen_string_literal: true
require 'set'
require 'optparse'
require 'spider-gazelle/logger'
module SpiderGazelle
module Options
DEFAULTS = {
host: "0.0.0.0",
port: 3000,
verbose: false,
tls: false,
backlog: 4096,
rackup: "#{Dir.pwd}/config.ru",
mode: :thread,
isolate: true
}.freeze
# Options that can't be used when more than one set of options is being applied
APP_OPTIONS = [:port, :host, :verbose, :debug, :environment, :rackup, :mode, :backlog, :count, :name, :loglevel].freeze
MUTUALLY_EXCLUSIVE = {
# Only :password is valid when this option is present
update: APP_OPTIONS
}.freeze
def self.parse(args)
options = {}
parser = OptionParser.new do |opts|
# ================
# STANDARD OPTIONS
# ================
opts.on "-p", "--port PORT", Integer, "Define what port TCP port to bind to (default: 3000)" do |arg|
options[:port] = arg
end
opts.on "-h", "--host ADDRESS", "bind to address (default: 0.0.0.0)" do |arg|
options[:host] = arg
end
opts.on "-v", "--verbose", "loud output" do
options[:verbose] = true
end
opts.on "-d", "--debug", "debugging mode with lowered security and manual processes" do
options[:debug] = true
end
opts.on "-e", "--environment ENVIRONMENT", "The environment to run the Rack app on (default: development)" do |arg|
options[:environment] = arg
end
opts.on "-r", "--rackup FILE", "Load Rack config from this file (default: config.ru)" do |arg|
options[:rackup] = arg
end
opts.on "-rl", "--rack-lint", "enable rack lint on all requests" do
options[:lint] = true
end
opts.on "-m", "--mode MODE", MODES, "Either reactor, thread or inline (default: reactor)" do |arg|
options[:mode] = arg
end
opts.on "-b", "--backlog BACKLOG", Integer, "Number of pending connections allowed (default: 5000)" do |arg|
options[:backlog] = arg
end
# =================
# TLS Configuration
# =================
opts.on "-t", "--use-tls PRIVATE_KEY_FILE", "Enables TLS on the port specified using the provided private key in PEM format" do |arg|
options[:tls] = true
options[:private_key] = arg
end
opts.on "-tc", "--tls-chain-file CERT_CHAIN_FILE", "The certificate chain to provide clients" do |arg|
options[:cert_chain] = arg
end
opts.on "-ts", "--tls-ciphers CIPHER_LIST", "A list of Ciphers that the server will accept" do |arg|
options[:ciphers] = arg
end
opts.on "-tv", "--tls-verify-peer", "Do we want to verify the client connections? (default: false)" do
options[:verify_peer] = true
end
# ========================
# CHILD PROCESS INDICATORS
# ========================
opts.on "-g", "--gazelle PASSWORD", 'For internal use only' do |arg|
options[:gazelle] = arg
end
opts.on "-f", "--file IPC", 'For internal use only' do |arg|
options[:gazelle_ipc] = arg
end
opts.on "-s", "--spider PASSWORD", 'For internal use only' do |arg|
options[:spider] = arg
end
opts.on "-i", "--interactive-mode", 'Loads a multi-process version of spider-gazelle that can live update your app' do
options[:isolate] = false
end
opts.on "-c", "--count NUMBER", Integer, "Number of gazelle processes to launch (default: number of CPU cores)" do |arg|
options[:count] = arg
end
# ==================
# SIGNALLING OPTIONS
# ==================
opts.on "-u", "--update", "Live migrates to a new process without dropping existing connections" do |arg|
options[:update] = true
end
opts.on "-up", "--update-password PASSWORD", "Sets a password for performing updates" do |arg|
options[:password] = arg
end
opts.on "-l", "--loglevel LEVEL", Logger::LEVELS, "Sets the log level" do |arg|
options[:loglevel] = arg
end
end
parser.banner = "sg <options> <rackup file>"
parser.on_tail "-h", "--help", "Show help" do
puts parser
exit 1
end
parser.parse!(args)
# Check for rackup file
if args.last =~ /\.ru$/
options[:rackup] = args.last
end
# Unless this is a signal then we want to include the default options
unless options[:update]
options = DEFAULTS.merge(options)
unless File.exist? options[:rackup]
abort "No rackup found at #{options[:rackup]}"
end
options[:environment] ||= ENV['RACK_ENV'] || 'development'
ENV['RACK_ENV'] = options[:environment]
# isolation and process mode don't mix
options[:isolate] = false if options[:mode] == :process
# Force no_ipc mode on Windows (sockets over pipes are not working in threaded mode)
options[:mode] = :no_ipc if ::FFI::Platform.windows? && options[:mode] == :thread
end
options
end
def self.sanitize(args)
# Use "\0" as this character won't be used in the command
cmdline = args.join("\0")
components = cmdline.split("\0--", -1)
# Ensure there is at least one component
# (This will occur when no options are provided)
components << String.new if components.empty?
# Parse the commandline options
options = []
components.each do |app_opts|
options << parse(app_opts.split(/\0+/))
end
# Check for any invalid requests
exclusive = Set.new(MUTUALLY_EXCLUSIVE.keys)
if options.length > 1
# Some options can only be used by themselves
options.each do |opt|
keys = Set.new(opt.keys)
if exclusive.intersect? keys
invalid = exclusive & keys
abort "The requested actions can only be used in isolation: #{invalid.to_a}"
end
end
# Ensure there are no conflicting ports
ports = [options[0][:port]]
options[1..-1].each do |opt|
# If there is a clash we'll increment the port by 1
while ports.include? opt[:port]
opt[:port] += 1
end
ports << opt[:port]
end
end
options
end
end
end