require 'unix_socks'
require 'term/ansicolor'
module Utils
class ProcessJob
include Term::ANSIColor
# Initializes a new ProcessJob instance with the specified arguments and
# optional probe server.
#
# This method creates a process job object that can be enqueued for
# execution by a probe server. It assigns a unique job ID from the probe
# server if provided and stores the command arguments as an array.
#
# @param args [ Array ] the command arguments to be executed by the job
# @param probe_server [ Utils::ProbeServer, nil ] the probe server instance
# to use for generating job IDs
#
# @return [ Utils::ProcessJob ] a new ProcessJob instance configured with
# the provided arguments and server reference
def initialize(args:, probe_server: nil)
@id = probe_server&.next_job_id
@args = Array(args)
end
# Returns the unique identifier of the process job.
#
# @return [ Integer ] the job ID
attr_reader :id
# The args reader method provides access to the arguments stored in the
# instance.
#
# @return [ Array ] the array of arguments
attr_reader :args
# The ok method sets the success status of the process job.
#
# @param value [ TrueClass, FalseClass, nil ] the success status to set
attr_writer :ok
# Returns the type identifier for the process job.
#
# This method provides a constant string value that identifies the object
# as a process job within the probe server system, facilitating type-based
# dispatch and handling.
#
# @return [ String ] the string 'process_job' indicating the object's type
def type
'process_job'
end
# The ok method returns a character representation of the job's success
# status.
#
# This method provides a visual indicator of whether a process job has
# succeeded, failed, or is still in progress. It returns 'y' for successful
# jobs, 'n' for failed jobs, and '…' for jobs that are currently running or
# pending.
#
# @return [ String ] 'y' if the job succeeded, 'n' if it failed, or '…' if
# the status is unknown
def ok
case @ok
when false then 'n'
when true then 'y'
else '…'
end
end
# The ok_colorize method applies color formatting to a string based on the
# success status.
#
# This method returns the input string wrapped with color codes to indicate
# whether the associated process job succeeded, failed, or is in progress.
# Successful jobs are highlighted in green, failed jobs in red, and pending
# jobs are returned without any color formatting.
#
# @param string [ String ] the string to be colorized
#
# @return [ String ] the colorized string or the original string if status is unknown
def ok_colorize(string)
case @ok
when false then white { on_red { string } }
when true then black { on_green { string } }
else string
end
end
# The inspect method generates a colorized string representation of the
# process job.
#
# This method creates a formatted string that includes the job's unique
# identifier and its command arguments, with the status indicator
# color-coded based on whether the job succeeded, failed, or is pending.
#
# @return [ String ] a formatted string representation of the process job
# including its ID, arguments, and color-coded status indicator
def inspect
ok_colorize("#{id} #{args.map { |a| a.include?(' ') ? a.inspect : a } * ' '}")
end
alias to_s inspect
# The as_json method converts the process job object into a
# JSON-serializable hash.
#
# This method creates and returns a hash representation of the process job,
# containing its type, unique identifier, and command arguments.
#
# @return [ Hash ] a hash containing the type, id, and args of the process job
def as_json(*)
{ type:, id:, args:, }
end
# The to_json method converts the object to a JSON string representation.
#
# This method delegates to the as_json method to generate a hash representation
# of the object, then converts that hash to a JSON string using the
# standard JSON library's to_json method.
#
# @return [ String ] a JSON string representation of the object
def to_json(*)
as_json.to_json(*)
end
end
class ProbeClient
class EnvProxy
# The initialize method sets up a new instance with the provided server
# object.
#
# @param server [ UnixSocks::Server ] the server object to be assigned
# to the instance variable
def initialize(server)
@server = server
end
# The []= method sets an environment variable value through the probe server.
#
# This method transmits a request to the probe server to set the specified
# environment variable key to the given value, then returns the updated
# environment value from the server's response.
#
# @param key [ String ] the environment variable key to set
# @param value [ String ] the value to assign to the environment variable
#
# @return [ String ] the updated environment variable value returned by the server
def []=(key, value)
response = @server.transmit_with_response(type: 'set_env', key:, value:)
response.env
end
# The [] method retrieves the value of an environment variable from the probe server.
#
# This method sends a request to the probe server to fetch the current value of the specified
# environment variable key and returns the corresponding value.
#
# @param key [ String ] the environment variable key to retrieve
#
# @return [ String ] the value of the specified environment variable
def [](key)
response = @server.transmit_with_response(type: 'get_env', key:)
response.env
end
attr_reader :env
end
# The initialize method sets up a new probe server instance.
#
# This method creates and configures a Unix domain socket server for
# handling probe jobs and communication. It initializes the server with a
# specific socket name and runtime directory, preparing it to listen for
# incoming connections and process jobs.
#
# @return [ Utils::ProbeServer ] a new probe server instance configured with
# the specified socket name and runtime directory
def initialize
@server = UnixSocks::Server.new(socket_name: 'probe.sock', runtime_dir: Dir.pwd)
end
# The env method provides access to environment variable management through
# a proxy object.
#
# This method returns an EnvProxy instance that allows for setting and
# retrieving environment variables via the probe server communication
# channel.
#
# @return [ Utils::ProbeServer::EnvProxy ] a proxy object for environment
# variable operations
def env
EnvProxy.new(@server)
end
# The enqueue method submits a new process job to the probe server for
# execution.
#
# This method transmits a process job request to the underlying Unix domain
# socket server, which then adds the job to the processing queue. The job
# includes the specified command arguments that will be executed by the
# probe server.
#
# @param args [ Array ] the command arguments to be executed by the process
# job
def enqueue(args)
@server.transmit({ type: 'process_job', args: })
end
end
class ProbeServer
include Term::ANSIColor
# The initialize method sets up a new probe server instance.
#
# This method creates and configures the core components of the probe
# server, including initializing the Unix domain socket server for
# communication, setting up the job queue for processing tasks, and
# preparing the history tracking for completed jobs.
#
# @return [ Utils::ProbeServer ] a new probe server instance configured
# with the specified socket name and runtime directory
def initialize
@server = UnixSocks::Server.new(socket_name: 'probe.sock', runtime_dir: Dir.pwd)
@history = [].freeze
@jobs_queue = Queue.new
@current_job_id = 0
end
# The start method initializes and begins operation of the probe server.
#
# This method sets up the probe server by starting a thread to process jobs
# from the queue and entering a receive loop to handle incoming requests.
# It also manages interrupt signals to enter interactive mode when needed.
def start
output_message "Starting probe server listening to #{@server.server_socket_path}.", type: :info
Thread.new do
loop do
job = @jobs_queue.pop
run_job job
end
end
begin
receive_loop.join
rescue Interrupt
ARGV.clear << '-f'
output_message %{\nEntering interactive mode.}, type: :info
help
begin
old, $VERBOSE = $VERBOSE, nil
examine(self)
ensure
$VERBOSE = old
end
@server.remove_socket_path
output_message "Quitting interactive mode, but still listening to #{@server.server_socket_path}.", type: :info
retry
end
end
# The inspect method returns a string representation of the probe server
# instance.
#
# This method provides a concise overview of the probe server's state by
# displaying its type and the current size of the job queue, making it
# useful for debugging and monitoring purposes.
#
# @return [ String ] a formatted string containing the probe server identifier
# and the number of jobs currently in the queue
def inspect
"#<Probe #queue=#{@jobs_queue.size}>"
end
alias to_s inspect
annotate :doc
annotate :shortcut
# The help method displays a formatted list of available commands and their
# descriptions.
#
# This method organizes and presents the documented commands along with their
# shortcuts and descriptions in a formatted table layout for easy reference.
doc 'Display this help.'
shortcut :h
def help
docs = doc_annotations.sort_by(&:first)
docs_size = docs.map { |a| a.first.size }.max
format = "%-#{docs_size}s %-3s %s"
output_message [
on_color(20) { white { format % %w[ command sho description ] } }
] << docs.map { |cmd, doc|
shortcut = shortcut_of(cmd) and shortcut = "(#{shortcut})"
format % [ cmd, shortcut, doc ]
}
end
# The job_enqueue method adds a new process job to the execution queue.
#
# This method creates a process job instance with the provided arguments
# and enqueues it for execution by the probe server. It provides feedback
# about the enqueued job through output messaging.
#
# @param args [ Array ] the command arguments to be executed by the process job
doc 'Enqueue a new job with the argument array <args>.'
shortcut :e
def job_enqueue(args)
job = ProcessJob.new(args:, probe_server: self)
output_message " → #{job.inspect} enqueued.", type: :info
@jobs_queue.push job
end
alias enqueue job_enqueue
# The shutdown method terminates the probe server process immediately.
#
# This method outputs a warning message indicating that the server is being
# shut down forcefully and then exits the program with status code 23.
doc 'Quit the server.'
shortcut :q
def shutdown
output_message "Server was shutdown down manually!", type: :info
exit 23
end
# The job_repeat method re-executes a previously run job from the history.
#
# This method takes a job identifier and attempts to find the corresponding
# job in the server's execution history. If found, it enqueues a new
# instance of that job for execution with the same arguments as the
# original.
#
# @param job_id [ Integer, Utils::ProcessJob ] the identifier of the job to repeat
# or the job object itself
#
# @return [ TrueClass, FalseClass ] true if the job was found and re-enqueued,
# false otherwise
doc 'Repeat the job with <job_id> or the last, it will be assigned a new id, though.'
shortcut :r
def job_repeat(job_id = @history.last)
ProcessJob === job_id and job_id = job_id.id
if old_job = @history.find { |job| job.id == job_id }
job_enqueue old_job.args
true
else
false
end
end
# The history_list method displays the list of previously executed jobs
# from the server's history.
#
# This method outputs all completed jobs that have been processed by the probe server,
# showing their identifiers and command arguments for review.
doc 'List the history of run jobs.'
shortcut :l
def history_list
output_message @history
end
# The history_clear method clears all entries from the server's execution
# history.
#
# This method resets the internal history array to an empty state,
# effectively removing all records of previously executed jobs from the
# probe server.
#
# @return [ TrueClass ] always returns true after clearing the history
doc 'Clear the history of run jobs.'
def history_clear
@history = []
true
end
class LogWrapper < BasicObject
# The initialize method sets up a new instance with the provided server
# object and object.
#
# This method creates and configures a LogWrapper instance by storing
# references to the specified server and object parameters. It prepares
# the wrapper for use in logging environment variable operations while
# maintaining access to both the server for messaging and the underlying
# object for attribute access.
#
# @param server [ Utils::ProbeServer ] the probe server instance to be assigned
# @param object [ ENV ] the environment object to be wrapped
def initialize(server, object)
@server, @object = server, object
end
# The []= method sets an environment variable value through the probe
# server.
#
# This method transmits a request to the probe server to set the
# specified environment variable key to the given value, then returns the
# updated environment value from the server's response.
#
# @param name [ String ] the environment variable key to set
# @param value [ String ] the value to assign to the environment variable
#
# @return [ String ] the updated environment variable value returned by
# the server
def []=(name, value)
name, value = name.to_s, value.to_s
@server.output_message("Setting #{name}=#{value.inspect}.", type: :info)
@object[name] = value
end
# The method_missing method delegates calls to the wrapped object's
# methods.
#
# This method acts as a fallback handler that forwards undefined method
# calls to the internal object instance, enabling dynamic method dispatch
# while maintaining access to all available methods through the wrapper
# interface.
#
# @param a [ Array ] the arguments passed to the missing method
# @param b [ Proc ] the block passed to the missing method
#
# @return [ Object ] the result of the delegated method call on the
# wrapped object
def method_missing(*a, &b)
@object.__send__(*a, &b)
end
end
doc "The environment of the server process, use env['a'] = 'b' and env['a']."
# The env method provides access to the server's environment variables
# through a wrapped interface.
#
# This method returns a LogWrapper instance that allows for setting and
# retrieving environment variables while logging the operations. The
# wrapper maintains access to both the probe server for messaging and the
# underlying ENV object for attribute access.
#
# @return [ Utils::ProbeServer::LogWrapper ] a wrapped environment object
# for variable management
memoize method:
def env
LogWrapper.new(self, ENV)
end
doc "Clear the terminal screen"
shortcut :c
# The clear method clears the terminal screen by executing the clear
# command.
def clear
system "clear"
end
for (method_name, shortcut) in shortcut_annotations
alias_method shortcut, method_name
end
# The next_job_id method increments and returns the current job identifier.
#
# This method maintains a sequential counter for job identification within
# the probe server, providing unique IDs for newly enqueued process jobs.
#
# @return [ Integer ] the next available job identifier in the sequence
def next_job_id
@current_job_id += 1
end
# The output_message method displays a formatted message to standard output
# with optional styling based on the message type.
#
# This method takes a message and an optional type parameter to determine
# the formatting style for the output. It handles both string and array
# messages, converting arrays into multi-line strings. Different message
# types are styled using color codes and formatting attributes to provide
# visual distinction.
#
# @param msg [ String, Array ] the message to be displayed
# @param type [ Symbol ] the type of message for styling (success, info, warn, failure)
#
# @return [ Utils::ProbeServer ] returns self to allow for method chaining
def output_message(msg, type: nil)
msg.respond_to?(:to_a) and msg = msg.to_a * "\n"
msg =
case type
when :success
on_color(22) { white { msg } }
when :info
on_color(20) { white { msg } }
when :warn
on_color(94) { white { msg } }
when :failure
on_color(124) { blink { white { msg } } }
else
msg
end
STDOUT.puts msg
STDOUT.flush
self
end
private
# The run_job method executes a process job and updates the server's
# history with the result.
#
# This method takes a process job, outputs a message indicating it is
# running, executes the job using the system command, and then updates the
# job's success status. It also logs the outcome of the job execution and
# adds the completed job to the server's history for future reference.
#
# @param job [ Utils::ProcessJob ] the process job to be executed
def run_job(job)
output_message " → #{job.inspect} now running.", type: :info
system(*cmd(job.args))
message = " → #{job.inspect} was just run"
if $?.success?
job.ok = true
message << " successfully."
output_message message, type: :success
else
job.ok = false
message << " and failed with exit status #{$?.exitstatus}!"
output_message message, type: :failure
end
@history += [ job.freeze ]
@history.freeze
nil
end
# The receive_loop method sets up and starts processing incoming jobs from
# the server.
#
# This method configures a background receiver on the probe server to handle
# incoming job requests. It processes different job types by delegating to
# appropriate handler methods, including enqueuing process jobs and managing
# environment variable operations through response handling.
def receive_loop
@server.receive_in_background do |job|
case job.type
when 'process_job'
enqueue job.args
when 'set_env'
env[job.key] = job.value
job.respond(env: env[job.key])
when 'get_env'
job.respond(env: env[job.key])
end
end
end
# The cmd method constructs and returns a command array for execution.
#
# This method builds a command array by first checking for the presence of
# a BUNDLE_GEMFILE environment variable. If found, it appends the bundle
# exec command to the call array.
# It then adds the current script name ($0) followed by the provided job
# arguments to complete the command. The method also outputs an
# informational message about the command being executed.
#
# @param job [ Array ] the job arguments to be included in the command
#
# @return [ Array<String> ] the constructed command array ready for execution
def cmd(job)
call = []
if ENV.key?('BUNDLE_GEMFILE') and bundle = `which bundle`.full?(:chomp)
call << bundle << 'exec'
end
call.push($0, *job)
output_message "Executing #{call.inspect} now.", type: :info
call
end
end
end