feat: extend cli with write_pidfile and remove_pidfile

This commit is contained in:
dualfroz committed 2026-09-06 18:49:24 +02:00
1 parent c3ff02a741
commit 7478aeadf2
1 file changed
+192
+192
View File
@@ -206,6 +206,59 @@ module Prism
worker = Jobs::Worker.new( worker = Jobs::Worker.new(
queue, queue,
queue_name: options[:queue_name], queue_name: options[:queue_name],
error_handler: lambda do |error, job|
warn "[prism jobs] job #{job[:id]} (#{job[:job_class]}) failed: #{error.message}"
end
)
running = true
trap('INT') { running = false }
trap('TERM') { running = false }
cycles = 0
loop do
processed = worker.work_off
out.puts "[prism jobs] processed #{processed} job(s)" if options[:verbose] && processed.positive?
cycles += 1
break if options[:once]
break if options[:max_cycles] && cycles >= options[:max_cycles]
break unless running
sleep(options[:interval])
break unless running
end
cycles
ensure
remove_pidfile(pidfile) if pidfile
end
# Refuses to start a second daemon against the same pidfile while a
# process recorded in it is still alive (mirrors the classic
# pidfile-lock pattern used by unicorn/sidekiq); a pidfile left behind
# by a process that crashed without cleaning up (stale pid, nothing
# answers Process.kill(0, ...)) is silently reclaimed instead of
# blocking every future start forever.
def write_pidfile(pidfile)
if File.exist?(pidfile)
existing_pid = File.read(pidfile).strip.to_i
if existing_pid.positive? && process_alive?(existing_pid)
raise UsageError, "jobs worker already running (pid #{existing_pid}, pidfile #{pidfile})"
end
end
FileUtils.mkdir_p(File.dirname(pidfile))
File.write(pidfile, Process.pid.to_s)
end
def remove_pidfile(pidfile)
File.delete(pidfile) if File.exist?(pidfile) && File.read(pidfile).strip.to_i == Process.pid
end
def process_alive?(pid)
Process.kill(0, pid)
true
rescue Errno::ESRCH
false
rescue Errno::EPERM
true
end end
def new_app(name) def new_app(name)
@@ -253,6 +306,145 @@ module Prism
end end
end end
def jobs(rest)
subcommand, *args = rest
case subcommand
when 'work' then jobs_work(args)
else raise UsageError, "unknown jobs subcommand #{subcommand.inspect}"
end
end
def parse_jobs_work_options(args)
options = { file: 'db/jobs.pstore', interval: 1.0, queue_name: nil, once: false,
max_cycles: nil, verbose: false, require: [], pidfile: nil }
args.each do |arg|
case arg
when '--once' then options[:once] = true
when '--verbose' then options[:verbose] = true
when /\A--file=(.+)\z/ then options[:file] = Regexp.last_match(1)
when /\A--interval=(.+)\z/ then options[:interval] = Regexp.last_match(1).to_f
when /\A--queue=(.+)\z/ then options[:queue_name] = Regexp.last_match(1)
when /\A--max-cycles=(\d+)\z/ then options[:max_cycles] = Regexp.last_match(1).to_i
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
when /\A--pidfile=(.+)\z/ then options[:pidfile] = Regexp.last_match(1)
else raise UsageError, "unknown option #{arg.inspect}"
end
end
options
end
def parse_routes_options(args)
options = { require: [], app: nil }
args.each do |arg|
case arg
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
when /\A--app=(.+)\z/ then options[:app] = Regexp.last_match(1)
else raise UsageError, "unknown option #{arg.inspect}"
end
end
options
end
# `--app` picks by name when a project loads more than one
# Prism::Application subclass (e.g. a host app plus a mounted engine).
# Auto-detection deliberately looks only at subclasses newly defined by
# THIS call's --require, not at Application.subclasses as a whole: a
# Ruby class is never garbage-collected just because its constant gets
# undefined, so in a long-lived process (the test suite, an irb/console
# session reloading code) every Application subclass ever defined stays
# in that list forever - scanning it whole would make auto-detection
# increasingly wrong the longer the process lives, not just after a
# fresh `ruby bin/prism routes --require=app.rb` (where it happens to
# equal "the one class app.rb just defined" anyway).
def resolve_app_class(explicit_name, newly_loaded)
return Object.const_get(explicit_name) if explicit_name
raise UsageError, 'no Prism::Application subclass loaded (pass --require=FILE or --app=ClassName)' if newly_loaded.empty?
if newly_loaded.size > 1
names = newly_loaded.map(&:name).join(', ')
raise UsageError, "multiple Prism::Application subclasses loaded (#{names}) - pick one with --app=ClassName"
end
newly_loaded.first
end
def print_routes(router, out)
rows = router.routes.map do |route|
[route.method, route.pattern, route_target(route), route.name ? "#{route.name}_path" : '']
end
header = %w[VERB PATH TO NAME]
widths = header.each_index.map { |i| ([header[i].length] + rows.map { |row| row[i].length }).max }
out.puts format_row(header, widths)
rows.each { |row| out.puts format_row(row, widths) }
router.mounts.each do |prefix, mounted_app|
out.puts "MOUNT #{prefix} -> #{mounted_app.class}"
end
nil
end
def route_target(route)
route.controller_route? ? "#{route.controller_name}##{route.action_name}" : '(block)'
end
def format_row(row, widths)
row.each_index.map { |i| row[i].ljust(widths[i]) }.join(' ').rstrip
end
def parse_console_options(args)
options = { require: [], prompt: 'prism> ' }
args.each do |arg|
case arg
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
when /\A--prompt=(.+)\z/ then options[:prompt] = Regexp.last_match(1)
else raise UsageError, "unknown option #{arg.inspect}"
end
end
options
end
def parse_seed_options(args)
options = { require: [], file: 'db/seeds.rb' }
args.each do |arg|
case arg
when /\A--require=(.+)\z/ then options[:require] << Regexp.last_match(1)
when /\A--file=(.+)\z/ then options[:file] = Regexp.last_match(1)
else raise UsageError, "unknown option #{arg.inspect}"
end
end
options
end
def parse_db_options(args)
options = { database: 'db/development.pstore', migrations: 'db/migrate', step: 1 }
args.each do |arg|
case arg
when /\A--database=(.+)\z/ then options[:database] = Regexp.last_match(1)
when /\A--migrations=(.+)\z/ then options[:migrations] = Regexp.last_match(1)
when /\A--step=(\d+)\z/ then options[:step] = Regexp.last_match(1).to_i
else raise UsageError, "unknown option #{arg.inspect}"
end
end
options
end
def build_migrator(options)
store = Orm::Store.new(File.expand_path(options[:database], @root))
Orm::Migrator.new(store, File.expand_path(options[:migrations], @root))
end
def parse_assets_precompile_options(args)
options = {}
args.each do |arg|
case arg
when /\A--source=(.+)\z/ then options[:source] = Regexp.last_match(1)
when /\A--output=(.+)\z/ then options[:output] = Regexp.last_match(1)
else raise UsageError, "unknown option #{arg.inspect}"
end
end
raise UsageError, 'usage: prism assets:precompile --source=DIR --output=DIR' unless options[:source] && options[:output]
options
end
def next_migration_version def next_migration_version
version = Time.now.utc.strftime('%Y%m%d%H%M%S') version = Time.now.utc.strftime('%Y%m%d%H%M%S')
version = (version.to_i + 1).to_s while File.exist?(File.join(@root, 'db', 'migrate')) && version = (version.to_i + 1).to_s while File.exist?(File.join(@root, 'db', 'migrate')) &&