module Locd::Launchctl

Thin wrapper to run `launchctl` commands and capture outputs via {Cmds}.

Basically just centralizes this stuff all in one place and presents a friendly API, does some common debug logging, etc.

Constants

OPTS_MAP

Mappings between any option keyword names we use to the ones `launchctl` expects.

@return [Hash<Symbol, Symbol>]

STATUS_CACHE_TIMEOUT_SEC

Max number of seconds to

STATUS_RE

Regexp to parse `launchd list` output for {#status} method.

@return [Regexp]

Public Class Methods

disabled() click to toggle source
# File lib/locd/launchctl.rb, line 160
  def self.disabled
    uid = Cmds.chomp! 'id -u'
    result = execute :'print-disabled', "user/#{ uid }"
    
    lines = if match = /disabled\ services\ =\ \{([^\}]+)\}/.match( result.out )
      match[1].lines
    else
      raise ParseError.new binding.erb <<~END
        Unable to find 'disabled services' section output
        
        Command:
        
            <%= result.cmd %>
        
        Output:
        
            <%= result.out %>
        
      END
    end
    
    lines
  end
execute(subcmd, *args, **opts) click to toggle source

Run a `launchctl` subcommand with positional args and options.

@param [Symbol | String] subcmd

Subcommand to run, like `:load`.

@param [Array<String>] *args

Positional args, will be added on end of command.

@param [Hash<Symbol, V>] **opts

Options, will be added between subcommand and positional args.

Keys are mapped via {.map_opts}.

@return [Cmds::Result]

# File lib/locd/launchctl.rb, line 119
def self.execute subcmd, *args, **opts
  logger.debug "Executing `#{ @@bin } #{ subcmd }` command",
    args: args,
    opts: opts
  
  result = Cmds "#{ @@bin } #{ subcmd } <%= opts %> <%= *args %>",
                *args,
                opts: map_opts( **opts )
  
  logger.debug "`#{ @@bin }` command result",
    cmd: result.cmd,
    status: result.status,
    out: NRSER.ellipsis( result.out.lines, 20 ),
    err: NRSER.ellipsis( result.err.lines, 20 )
  
  result
end
execute!(*args) click to toggle source

Just like {#execute} but will raise if the exit status is not `0`.

@param (see .execute) @return (see .execute) @raise (see Cmds::Result#assert)

# File lib/locd/launchctl.rb, line 144
def self.execute! *args
  execute( *args ).assert
end
map_opts(**opts) click to toggle source

Swap any keys in {OPTS_MAP}.

@param [Hash<Symbol, V>] **opts

Options as passed to {.execute}, etc.

@return [Hash<Symbol, V>]

Options ready for `launchctl`.
# File lib/locd/launchctl.rb, line 93
def self.map_opts **opts
  opts.map { |key, value|
    if OPTS_MAP[key]
      [OPTS_MAP[key], value]
    else
      [key, value]
    end
  }.to_h
end
refresh_status?() click to toggle source
# File lib/locd/launchctl.rb, line 185
def self.refresh_status?
  @last_status_time.nil? ||
    (Time.now - @last_status_time) > STATUS_CACHE_TIMEOUT_SEC
end
status(refresh: refresh_status?) click to toggle source

@param [Boolean] refresh:

@return [Hash<String, {pid: Fixnum?, status: Fixnum?}>]

Map of agent labels to hash with process ID (if any) and last exit
code (if any).
# File lib/locd/launchctl.rb, line 199
def self.status refresh: refresh_status?
  if refresh || @status.nil?
    @status = execute!( :list ).out.lines.rest.
      map { |line|
        if match = STATUS_RE.match( line )
          pid = case match[:pid]
          when '-'
            nil
          when /^\d+$/
            match[:pid].to_i
          else
            logger.error "Unexpected `pid` parse in {#status}",
              captures: match.captures,
              line: line
            nil
          end
          
          status = case match[:status]
          when /^\-?\d+$/
            match[:status].to_i
          else
            logger.error "Unexpected `status` parse in {#status}",
              captures: match.captures,
              line: line
            nil
          end
            
          label = match[:label].freeze
          
          [label, {pid: pid, status: status}]
          
        else
          logger.error "FAILED to parse status line", line: line
          nil
          
        end
      }.
      reject( &:nil? ).
      to_h
    
    # Set the time so we can compare next call
    @last_status_time = Time.now
  end
  
  @status
end