# frozen_string_literal: true # Requirements # ======================================================================= # Stdlib # ----------------------------------------------------------------------- # Deps # ----------------------------------------------------------------------- # Project / Package # ----------------------------------------------------------------------- # Refinements # ======================================================================= using NRSER using NRSER::Types # Definitions # ======================================================================= # CLI interface using the `thor` gem. # # @see http://whatisthor.com/ # class Locd::CLI::Command::Agent < Locd::CLI::Command::Base # Helpers # ========================================================================== # def self.agent_class Locd::Agent end # .agent_class def self.agent_type agent_class.name.split( '::' ).last.downcase end protected # ========================================================================== def agent_class self.class.agent_class end def agent_type self.class.agent_type end def agent_table agents Locd::CLI::Table.build do |t| t.col "PID", &:pid t.col "LEC", desc: "Last Exit Code", &:last_exit_code t.col "Label", &:label t.col "File" do |agent| agent_file agent end t.rows agents end end # Find exactly one {Locd::Agent} for a `pattern`, using the any `:pattern` # shared options provided, and raising if there are no matches or more # than one. # # @param pattern (see Locd::Agent.find_only!) # # @return [Locd::Agent] # Matched agent. # # @raise If more or less than one agent is matched. # def find_only! pattern agent_class.find_only! pattern, **option_kwds( groups: :pattern ) end def find_multi! pattern # Behavior depend on the `:all` option... if options[:all] # `:all` is set, so we find all the agents for the pattern, raising # if we don't find any agent_class.find_all!( pattern, **option_kwds( groups: :pattern ) ).values else # `:all` is not set, so we need to find exactly one or error [find_only!( pattern )] end end # end protected public # Shared Options # ============================================================================ shared_option :long, groups: :respond_with_agents, desc: "Display agent details", aliases: '-l', type: :boolean # `:pattern` Group # ---------------------------------------------------------------------------- # # Options when provided a `PATTERN` argument used to find agents by label. # shared_option :full, groups: :pattern, desc: "Require label PATTERN to match entire string", aliases: '-u', type: :boolean shared_option :ignore_case, groups: :pattern, desc: "Make label PATTERN case-insensitive", aliases: '-i', type: :boolean shared_option :recursive, groups: :pattern, desc: "Make workdir PATTERN match all subdirs too", aliases: '-r', type: :boolean # NOTE We don't expose the workdir pattern's `:cwd` option... # If you want to match a directory from the CLI, just provide that # directory... no reason to specify the `:cwd` in an option then # provide a relative PATTERN # # `:multi` Group # ---------------------------------------------------------------------------- # # For commands that can match multiple agents. # shared_option :all, groups: :multi, desc: "Apply to ALL agents that PATTERN matches", aliases: '-a', type: :boolean # `:write` Group # ---------------------------------------------------------------------------- # # Options when writing an agent `.plist` (`create`, `update`). # shared_option :label, groups: :write, desc: "Agent label, which is also the domain the proxy will serve it at", aliases: ['--name', '-n'], type: :string #, # required: true shared_option :workdir, groups: :write, desc: "Working directory for the agent's command", aliases: ['--dir'], type: :string shared_option :log_path, groups: :write, desc: "Path to log agent's STDOUT and STDERR (combined)", aliases: ['--log'], type: :string shared_option :keep_alive, groups: :write, desc: "Try to keep the agent running", type: :boolean, default: false shared_option :run_at_load, groups: :write, desc: "Start the agent when loading it", type: :boolean, default: false # `:add` Group # ---------------------------------------------------------------------------- shared_option :force, groups: :add, desc: "Overwrite any existing agent", type: :boolean, default: false shared_option :load, groups: :add, desc: "Load the agent into `launchd`", type: :boolean, default: true # Commands # ============================================================================ # Querying # ---------------------------------------------------------------------------- desc "ls [PATTERN]", "List agents" map list: :ls include_options :long, groups: :pattern def ls pattern = nil results = if pattern.nil? agent_class.all else agent_class.list pattern, **option_kwds( groups: :pattern ) end respond results.values.sort end desc "plist PATTERN", "Print an agent's launchd property list" include_options groups: :pattern def plist pattern agent = find_only! pattern if options[:json] || options[:yaml] respond agent.plist else respond agent.path.read end end desc 'status PATTERN', "Print agent status" include_options groups: :pattern def status pattern agent = find_only! pattern respond \ label: agent.label, port: agent.port, status: agent.status end # Commands for Manipulating Agent Definitions # ---------------------------------------------------------------------------- desc "add CMD_TEMPLATE...", "Add an agent that runs a command in the current directory" include_options groups: [:write, :add, :respond_with_agents] def add *cmd_template, **kwds logger.trace __method__.to_s, cmd_template: cmd_template, kwds: kwds, options: options # Merge all the keywords together into the format needed to call # {Locd::Agent.add} kwds.merge! **option_kwds( :force, groups: [:write] ), cmd_template: cmd_template # Check args # `:cmd_template` can not be empty at this point if kwds[:cmd_template].empty? || kwds[:cmd_template].all?( &:empty? ) raise Thor::RequiredArgumentMissingError, "CMD_TEMPLATE argument is required to add an agent" end # Need a `:label` too unless t.non_empty_str === kwds[:label] raise Thor::RequiredArgumentMissingError, "--label=LABEL option is required to add an agent" end # Do the add agent = agent_class.add **kwds # Reload (unless we were told not to... usually you want to reload) agent.reload if options[:load] respond agent end desc "update PATTERN [OPTIONS] [-- CMD_TEMPLATE...]", "Update an existing agent" include_options groups: [:pattern, :write, :respond_with_agents] def update pattern, *cmd_template agent = find_only! pattern new_agent = agent.update \ cmd_template: cmd_template, **option_kwds( groups: :write ) logger.info "Agent `#{ agent.label }` updated" respond agent end desc "rm PATTERN", "Remove (uninstall, delete) a agent" map remove: :rm include_options groups: [:pattern, :multi] option :logs, desc: "Remove logs too", type: :boolean, default: false def rm pattern kwds = option_kwds :logs find_multi!( pattern ).each { |agent| agent.remove **kwds } end # Commands for Manipulating Agent State # -------------------------------------------------------------------------- desc "start PATTERN", "Start an agent" include_options groups: :pattern option :load, desc: "Load the agent before starting", type: :boolean, default: true option :force, desc: "Force loading of agent even if it's disabled", type: :boolean, default: false option :enable, desc: "Set `launchd` *Disabled* key to `false`", type: :boolean, default: false def start pattern find_only!( pattern ).start **option_kwds( :load, :force, :enable ) end desc "stop PATTERN", "Stop an agent" include_options groups: [:pattern, :stop] option :unload, desc: "Unload the agent from `launchd` after stopping", type: :boolean, default: true option :disable, desc: "Set `launchd` *Disabled* key to `true`", type: :boolean, default: false def stop pattern find_only!( pattern ).stop **option_kwds( :unload, :disable ) end desc "restart PATTERN", "Restart an agent" include_options groups: [:pattern, :stop] option :reload, desc: "Unload and reload the agent in `launchd`", type: :boolean, default: true option :force, desc: "Force loading of agent even if it's disabled", type: :boolean, default: false option :enable, desc: "Set `launchd` *Disabled* key to `false`", type: :boolean, default: false def restart pattern find_only!( pattern ).restart **option_kwds( :reload, :force, :enable ) end # Assorted Other Commands # ---------------------------------------------------------------------------- desc "open PATTERN", "Open an agent's URL in the browser" include_options groups: [:pattern, :multi] def open pattern find_multi!( pattern ).each do |agent| Cmds! "open %s", agent.url logger.info "Opened agent `#{ agent.label }` at #{ agent.url }" end end desc 'truncate_logs PATTERN', "Truncate agent log file(s)." include_options groups: [:pattern, :multi] option :restart, desc: "Restart the agent after truncation", type: :boolean, default: true def truncate_logs pattern find_multi!( pattern ).each do |agent| log_paths = [agent.out_path, agent.err_path].compact.uniq unless log_paths.empty? restart = options[:restart] && agent.running? agent.stop if restart log_paths.each do |log_path| begin log_path.open( 'w' ) { |f| f.truncate 0 } rescue Exception => error logger.error "Failed to truncate #{ log_path }", error else logger.info "Truncated", 'file' => log_path.to_s, 'agent.label' => agent.label end end # each log_path agent.start if restart end # unless log_paths.empty? end # each agent end # #truncate_logs desc 'tail [OPTIONS] PATTERN [-- TAIL_OPTIONS]', "Tail agent logs" include_options groups: :pattern option :stream, desc: "Stream to tail. May omit if uses single log file.", aliases: ['-s'], type: :string, enum: ['out', 'err'] def tail pattern, *tail_options agent = find_only! pattern path = case options[:stream] when nil paths = agent.log_paths unless paths.length == 1 raise Thor::RequiredArgumentMissingError.new binding.erb <<~END Agent `<%= agent.label %>` has multiple log files. out: <%= agent.out_path.to_s %> err: <%= agent.err_path.to_s %> Must specify one via the `--stream` option. END end paths[0] when 'out' agent.out_path when 'err' agent.err_path else raise "WTF" end exec "tail", *tail_options, path.to_s end # #tail end # class Locd::CLI::Command::Agent