Module: Locd::Launchctl

Includes:
NRSER::Log::Mixin
Defined in:
lib/locd/launchctl.rb

Overview

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.

Defined Under Namespace

Classes: Error, ParseError

Constant Summary collapse

OPTS_MAP =

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

Returns:

  • (Hash<Symbol, Symbol>)
{
  force: :F,
  write: :w,
}
STATUS_RE =

Regexp to parse launchd list output for #status method.

Returns:

  • (Regexp)
/^(?<pid>\S+)\s+(?<status>\S+)\s+(?<label>\S+)/.freeze
STATUS_CACHE_TIMEOUT_SEC =

Max number of seconds to

5
@@bin =

launchctl executable to use. You shouldn't need to touch this, but might be useful for testing or if you have path weirdness.

Returns:

  • (String)
'launchctl'

Class Method Summary collapse

Class Method Details

.disabledObject



160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# 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) ⇒ Cmds::Result

Run a launchctl subcommand with positional args and options.

Parameters:

  • subcmd (Symbol | String)

    Subcommand to run, like :load.

  • *args (Array<String>)

    Positional args, will be added on end of command.

  • **opts (Hash<Symbol, V>)

    Options, will be added between subcommand and positional args.

    Keys are mapped via map_opts.

Returns:

  • (Cmds::Result)


119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# 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) ⇒ Cmds::Result

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

Parameters:

  • subcmd (Symbol | String)

    Subcommand to run, like :load.

  • *args (Array<String>)

    Positional args, will be added on end of command.

  • **opts (Hash<Symbol, V>)

    Options, will be added between subcommand and positional args.

    Keys are mapped via map_opts.

Returns:

  • (Cmds::Result)


144
145
146
# File 'lib/locd/launchctl.rb', line 144

def self.execute! *args
  execute( *args ).assert
end

.map_opts(**opts) ⇒ Hash<Symbol, V>

Swap any keys in OPTS_MAP.

Parameters:

  • **opts (Hash<Symbol, V>)

    Options as passed to execute, etc.

Returns:

  • (Hash<Symbol, V>)

    Options ready for launchctl.



93
94
95
96
97
98
99
100
101
# 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?Boolean

Returns:

  • (Boolean)


185
186
187
188
# 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? ) ⇒ Hash<String, {pid: Fixnum?, status: Fixnum?}>

Returns Map of agent labels to hash with process ID (if any) and last exit code (if any).

Parameters:

  • refresh: (Boolean) (defaults to: refresh_status? )

Returns:

  • (Hash<String, {pid: Fixnum?, status: Fixnum?}>)

    Map of agent labels to hash with process ID (if any) and last exit code (if any).



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# 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