Class: Locd::Agent

Inherits:
Object
  • Object
show all
Includes:
Comparable, NRSER::Log::Mixin
Defined in:
lib/locd/agent.rb

Overview

Represents a backend agent that the proxy can route to.

Agents are managed by launchd, macOS's system service manager, and created on-demand by generating "property list" files (.plist, XML format) and installing them in the user's ~/Library/LaunchAgents directory.

From there they can be managed directly with macOS's launchctl utility, with the lunchy gem, etc.

Direct Known Subclasses

Job, Proxy, Site

Defined Under Namespace

Modules: System Classes: Job, Proxy, RotateLogs, Site

Constant Summary collapse

TO_H_NAMES =

Attribute / method names that #to_h uses.

Returns:

  • (Hamster::SortedSet<Symbol>)
Hamster::SortedSet[:label, :path, :plist, :status]

Creating Agents collapse

Computing Paths collapse

Instantiating collapse

Querying collapse

Creating Agents collapse

Instance Methods: Attribute Readers collapse

Instance Methods: `launchctl` Interface collapse

Modifying Agents collapse

Instance Methods: Language Integration collapse

Class Method Summary collapse

Constructor Details

#initialize(plist:, path:) ⇒ Agent

Constructor



697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
# File 'lib/locd/agent.rb', line 697

def initialize plist:, path:
  @path = path.to_pn.expand_path
  @plist = plist
  @status = :UNKNOWN
  
  
  # Sanity check...
  
  unless plist.key? Locd.config[:agent, :config_key]
    raise ArgumentError.new binding.erb "      Not a Loc'd plist (no <%= Locd.config[:agent, :config_key] %> key)\n      \n      path: <%= path %>\n      plist:\n      \n          <%= plist.pretty_inspect %>\n      \n    END\n  end\n  \n  unless @path.basename( '.plist' ).to_s == label\n    raise ArgumentError.new binding.erb <<~END\n      Label and filename don't match.\n      \n      Filename should be `<label>.plist`, found\n      \n          label:    <%= label %>\n          filename: <%= @path.basename %>\n          path:     <%= path %>\n    \n    END\n  end\n  \n  init_ensure_out_dirs_exist\nend\n"

Instance Attribute Details

#pathPathname (readonly)

Absolute path to the agent's .plist file.

Returns:

  • (Pathname)


691
692
693
# File 'lib/locd/agent.rb', line 691

def path
  @path
end

#plistHash<String, V> (readonly)

Hash of the agent's .plist file (keys and values from the top-level <dict> element).

Returns:

  • (Hash<String, V>)

    Check out the plist gem for an idea of what types V may assume.



684
685
686
# File 'lib/locd/agent.rb', line 684

def plist
  @plist
end

Class Method Details

.add(label:, force: false, workdir: Pathname.getwd, **kwds) ⇒ Locd::Agent

Add an agent, writing a .plist to ~/Library/LaunchAgents.

Does not start the agent.

Parameters:

  • label: (String)

    The agent's label, which is its:

    1. Unique identifier in launchd
    2. Domain via the proxy.
    3. Property list filename (plus the .plist extension).
  • force: (Boolean) (defaults to: false)

    Overwrite any existing agent with the same label.

  • workdir: (String | Pathname) (defaults to: Pathname.getwd)

    Working directory for the agent.

  • **kwds (Hash<Symbol, Object>)

    Additional keyword arguments to pass to create_plist_data.

Returns:



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
# File 'lib/locd/agent.rb', line 605

def self.add  label:,
              force: false,
              workdir: Pathname.getwd,
              **kwds
  logger.debug "Creating {Agent}...",
    label: label,
    force: force,
    workdir: workdir,
    **kwds
  
  plist_abs_path = self.plist_abs_path label
  
  # Handle file already existing
  if File.exists? plist_abs_path
    logger.debug "Agent already exists!",
      label: label,
      path: plist_abs_path
    
    if force
      logger.info "Forcing agent creation (overwrite)",
        label: label,
        path: plist_abs_path
    else
      raise binding.erb "        Agent <%= label %> already exists at:\n        \n            <%= plist_abs_path %>\n        \n      END\n    end\n  end\n\n  plist_data = create_plist_data label: label, workdir: workdir, **kwds\n  logger.debug \"Property list created\", data: plist_data\n  \n  plist_string = Plist::Emit.dump plist_data\n  plist_abs_path.write plist_string\n  logger.debug \"Property list written\", path: plist_abs_path\n  \n  from_path( plist_abs_path ).tap { |agent|\n    agent.send :log_info, \"added\"\n  }\nend\n"

.add_or_update(label:, **values) ⇒ Array<((:add | :update), Locd::Agent)] Whether the agent was added or updated, followed by the agent instance.

Pretty much what the name says.

Parameters:

  • label (String)

    Agent's label (AKA name AKA domain).

  • **values

    Agent properties.

Returns:

  • (Array<((:add | :update), Locd::Agent)] Whether the agent was added or updated, followed by the agent instance.)

    Array<((:add | :update), Locd::Agent)] Whether the agent was added or updated, followed by the agent instance.

See Also:



664
665
666
667
668
669
670
# File 'lib/locd/agent.rb', line 664

def self.add_or_update label:, **values
  if exists? label
    [:update, get( label ).update( **values )]
  else
    [:add, add( label: label, **values )]
  end
end

.allHamster::Hash<String, Locd::Agent>

All Locd::Agent that are instances of self.

So, when invoked through all returns all agents.

When invoked from a Locd::Agent subclass, returns all agents that are instances of that subclass - Locd::Agent::Site.all returns all Locd::Agent that are Site instances.

Returns:



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# File 'lib/locd/agent.rb', line 292

def self.all
  # If we're being invoked through {Locd::Agent} itself actually find and
  # load everyone
  if self == Locd::Agent
    plists.each_pair.map { |path, plist|
      begin
        agent_class = class_for plist
        
        if agent_class.nil?
          nil
        else
          agent = agent_class.new path: path, plist: plist
          [agent.label, agent]
        end
        
      rescue Exception => error
        logger.error "Failed to parse Loc'd Agent plist",
          path: path,
          error: error,
          backtrace: error.backtrace
        
        nil
      end
    }.
    compact.
    thru( &Hamster::Hash.method( :new ) )
  else
    # We're being invoked through a {Locd::Agent} subclass, so invoke
    # through {Locd::Agent} and filter the results to instance of `self`.
    Locd::Agent.all.select { |label, agent| agent.is_a? self }
  end
end

.class_for(plist) ⇒ Class<Locd::Agent>?

Get the agent class that a plist should be instantiated as.

Parameters:

  • plist (Hash<String, Object>)

Returns:

  • (Class<Locd::Agent>)

    If the plist is for an agent of the current Loc'd configuration, the appropriate class to instantiate it as.

    Plists for user sites and jobs will always return their subclass.

    Plists for system agents will only return their class when they are for the current configuration's label namespace.

  • (nil)

    If the plist is not for an agent of the current Loc'd configuration (and should be ignored by Loc'd).



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/locd/agent.rb', line 197

def self.class_for plist
  # 1.  See if it's a Loc'd system agent plist
  if system_class = Locd::Agent::System.class_for( plist )
    return system_class
  end
  
  # 2.  See if it's a Loc'd user agent plist
  if user_class = [
                    Locd::Agent::Site,
                    Locd::Agent::Job,
                  ].find_bounded( max: 1 ) { |cls| cls.plist? plist }.first
    return user_class
  end
  
  # 3.  Return a vanilla agent if it's a plist for one
  #     
  #     Really, not sure when this would happen...
  #     
  if Locd::Agent.plist?( plist )
    return Locd::Agent
  end
  
  # Nada
  nil
end

.create_plist_data(cmd_template:, label:, workdir:, log_path: nil, keep_alive: false, run_at_load: false, **extras) ⇒ Hash<String, Object>

Create the launchd property list data for a new Locd::Agent.

Parameters:

  • log_path: (nil | String | Pathname) (defaults to: nil)

    Optional path to log agent standard outputs to (combined STDOUT and STDERR).

    See resolve_log_path for details on how the different types and values are treated.

Returns:

  • (Hash<String, Object>)



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
# File 'lib/locd/agent.rb', line 485

def self.create_plist_data  cmd_template:,
                            label:,
                            workdir:,
                            log_path: nil,
                            keep_alive: false,
                            run_at_load: false,
                            **extras
  # Configure daemon variables...
  
  # Normalize `workdir` to an expanded {Pathname}
  workdir = workdir.to_pn.expand_path
  
  # Resolve the log (`STDOUT` & `STDERR`) path
  log_path = resolve_log_path(
    log_path: log_path,
    workdir: workdir,
    label: label,
  ).to_s
  
  # Interpolate variables into command template
  cmd = render_cmd(
    cmd_template: cmd_template,
    label: label,
    workdir: workdir,
    **extras
  )
  
  # Form the property list hash
  {
    # Unique label, format: `locd.<owner>.<name>.<agent.path...>`
    'Label' => label,
    
    # What to run
    'ProgramArguments' => [
      # TODO  Maybe this should be configurable or smarter in some way?
      'bash',
      # *login* shell... need this to source the user profile and set up
      # all the ENV vars
      '-l',
      # Run the command in the login shell
      '-c', cmd,
    ],
    
    # Directory to run the command in
    'WorkingDirectory' => workdir.to_s,
    
    # Where to send STDOUT
    'StandardOutPath' => log_path,
    
    # Where to send STDERR (we send both to the same file)
    'StandardErrorPath' => log_path,
    
    # Bring the process back up if it goes down (has backoff and stuff
    # built-in)
    'KeepAlive' => keep_alive,
    
    # Start the process when the plist is loaded
    'RunAtLoad' => run_at_load,
    
    'ProcessType' => 'Interactive',
    
    # Extras we need... `launchd` complains in the system log about this
    # but it's the easiest way to handle it at the moment
    Locd.config[:agent, :config_key] => {
      # Save this too why the hell not, might help debuging at least
      cmd_template: cmd_template,
      
      # Add subclass-specific extras
      **extras,
    }.str_keys,
    
    # Stuff that *doesn't* work... so you don't try it again, because
    # Apple's online docs seems totally out of date.
    # 
    # Not allowed for user agents
    # 'UserName' => ENV['USER'],
    # 
    # "The Debug key is no longer respected. Please remove it."
    # 'Debug' => true,
    # 
    # Yeah, it would have been nice to just use the plist to store the port,
    # but this runs into all sorts of impenetrable security mess... gotta
    # put it somewhere else! Weirdly enough it just totally works outside
    # of here, so I'm not what the security is really stopping..?
    # 
    # 'Sockets' => {
    #   'Listeners' => {
    #     'SockNodeName' => BIND,
    #     'SockServiceName' => port,
    #     'SockType' => 'stream',
    #     'SockFamily' => 'IPv4',
    #   },
    # },
  }.reject { |key, value| value.nil? } # Drop any `nil` values
end

.default_log_path(workdir, label) ⇒ Pathname?

Default path for a Locd::Agent output file.

Parameters:

  • workdir (Pathname)

    The working directory the agent will run in.

  • label (String)

    The agent's label.

Returns:

  • (Pathname)

    If we could find a tmp directory walking up from workdir.

  • (nil)

    If we could not find a tmp directory walking



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/locd/agent.rb', line 117

def self.default_log_path workdir, label
  tmp_dir = workdir.find_up 'tmp', test: :directory?, result: :path
  
  if tmp_dir.nil?
    logger.warn "Unable to find `tmp` dir, output will not be redirected",
      workdir: workdir,
      label: label,
      stream: stream
    return nil
  end
  
  unless tmp_dir.writable?
    logger.warn \
      "Found `tmp` dir, but not writable. Output will not be redirected",
      workdir: workdir,
      label: label,
      stream: stream
    return nil
  end
  
  tmp_dir / 'locd' / "#{ label }.log"
end

.exists?(label) ⇒ Boolean

Does this agent exist?

Parameters:

  • label (String)

    Agent's label (AKA name AKA domain).

Returns:

  • (Boolean)


274
275
276
# File 'lib/locd/agent.rb', line 274

def self.exists? label
  File.exists? plist_abs_path( label )
end

.find_all(pattern, **options) ⇒ Hamster::Hash<String, Locd::Agent>

Find all the agents that match a pattern.

Parameters:

  • pattern (String | Pattern)

    Pattern to match against agent.

    When it's a String, passed to Pattern.from to get the pattern.

  • options (**<Symbol, V>)

    Passed to Pattern.from when pattern is a String.

Returns:

See Also:



385
386
387
388
# File 'lib/locd/agent.rb', line 385

def self.find_all pattern, **options
  pattern = Locd::Pattern.from pattern, **options
  all.select { |label, agent| pattern.match? agent }
end

.find_all!(pattern, **options) ⇒ Hamster::Hash<String, Locd::Agent>

Just like find_all but raises if result is empty.

Parameters:

  • pattern (String | Pattern)

    Pattern to match against agent.

    When it's a String, passed to Pattern.from to get the pattern.

  • options (**<Symbol, V>)

    Passed to Pattern.from when pattern is a String.

Returns:

Raises:

  • (NRSER::CountError)

    If no agents were found.



401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/locd/agent.rb', line 401

def self.find_all! pattern, **options
  pattern = Locd::Pattern.from pattern, **options
  
  find_all( pattern ).tap { |agents|
    if agents.empty?
      raise NRSER::CountError.new(
        "No agents found for pattern from #{ pattern.source.inspect }",
        subject: agents,
        expected: '#count > 0',
      )
    end
  }
end

.find_only!(*find_all_args) ⇒ Locd::Agent

Find a single Locd::Agent matching pattern or raise.

Parameters are passed to Label.regexp_for_glob and the resulting Regexp is matched against each agent's #label.

Parameters:

  • pattern (String | Pattern)

    Pattern to match against agent.

    When it's a String, passed to Pattern.from to get the pattern.

  • options (**<Symbol, V>)

    Passed to Pattern.from when pattern is a String.

Returns:

See Also:



366
367
368
# File 'lib/locd/agent.rb', line 366

def self.find_only! *find_all_args
  find_all( *find_all_args ).values.to_a.only!
end

.from_path(plist_path) ⇒ Locd::Agent

Instantiate a Locd::Agent from the path to it's .plist file.

Parameters:

  • plist_path (Pathname | String)

    Path to the .plist file.

Returns:



231
232
233
234
235
# File 'lib/locd/agent.rb', line 231

def self.from_path plist_path
  plist = Plist.parse_xml plist_path.to_s
  
  class_for( plist ).new plist: plist, path: plist_path
end

.get(label) ⇒ Locd::Agent?

Get a Loc'd agent by it's label.

Parameters:

  • label (String)

    The agent's label.

Returns:

  • (Locd::Agent)

    If a Loc'd agent with label is installed.

  • (nil)

    If no Loc'd agent with label is installed.



346
347
348
349
350
351
352
# File 'lib/locd/agent.rb', line 346

def self.get label
  path = plist_abs_path label
  
  if path.file?
    from_path path
  end
end

.labelsHamster::Vector<String>

Labels for all installed Loc'd agents.

Returns:

  • (Hamster::Vector<String>)


330
331
332
# File 'lib/locd/agent.rb', line 330

def self.labels
  all.keys.sort
end

.plist?(plist) ⇒ Boolean

Test if the parse of a property list is for a Loc'd agent by seeing if it has the config key (from Locd.config[:agent, :config_key]) as a key.

Parameters:

  • plist (Hash<String, Object>)

Returns:

  • (Boolean)

    true if the plist looks like it's from Loc'd.



72
73
74
# File 'lib/locd/agent.rb', line 72

def self.plist? plist
  plist.key? Locd.config[:agent, :config_key]
end

.plist_abs_path(label) ⇒ Pathname

Absolute path for the plist file given it's label, equivalent to expanding ~/Library/LaunchAgents/<label>.plist.

Parameters:

  • label (String)

    The agent's label.

Returns:

  • (Pathname)


98
99
100
# File 'lib/locd/agent.rb', line 98

def self.plist_abs_path label
  user_plist_abs_dir / "#{ label }.plist"
end

.plistsHamster::Hash<String, Locd::Agent>

All installed Loc'd agents.

Returns:

  • (Hamster::Hash<String, Locd::Agent>)

    Map of all Loc'd agents by label.



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# File 'lib/locd/agent.rb', line 248

def self.plists
  Pathname.glob( user_plist_abs_dir / '*.plist' ).
    map { |path|
      begin
        [path, Plist.parse_xml( path.to_s )]
      rescue Exception => error
        logger.trace "{Plist.parse_xml} failed to parse plist",
          path: path.to_s,
          error: error,
          backtrace: error.backtrace
        nil
      end
    }.
    compact.
    select { |path, plist| plist? plist }.
    thru( &Hamster::Hash.method( :new ) )
end

.render_cmd(cmd_template:, label:, workdir:, **extras) ⇒ String

Render a command string by substituting any {name} parts for their values.

Examples:

render_cmd \
  cmd_template: "serve --name={label} --port={port}",
  label: 'server.test',
  workdir: Pathname.new( '~' ).expand_path,
  port: 1234
# => "server --name=server.test --port=1234"

Parameters:

  • cmd_template: (String | Array<String>)

    Template for the string. Arrays are just rendered per-entry then joined.

  • label: (String)

    The agent's #label.

  • workdir: (Pathname)

    The agent's #workdir.

  • **extras (Hash<Symbol, #to_s>)

    Subclass-specific extra keys and values to make available. Values will be converted to strings via #to_s before substitution.

Returns:

  • (String)

    Command string with substitutions made.



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/locd/agent.rb', line 449

def self.render_cmd cmd_template:, label:, workdir:, **extras
  t.match cmd_template,
    Array, ->( array ) {
      array.map {|arg|
        render_cmd \
          cmd_template: arg,
          label: label,
          workdir: workdir,
          **extras
      }.shelljoin
    },
    
    String, ->( string ) {
      {
        label: label,
        workdir: workdir,
        **extras,
      }.reduce( string ) do |cmd, (key, value)|
        cmd.gsub  "{#{ key }}", value.to_s.shellescape
      end
    }
end

.resolve_log_path(log_path:, workdir:, label:) ⇒ Pathname?

Get the path to log STDOUT and STDERR to given the option value and other options we need to figure it out if that doesn't suffice.

Note that we might not figure it out at all (returning nil), and that needs to be ok too (though it's not expected to happen all too often).

Parameters:

  • log_path: (nil | String | Pathname)

    The value the user provided (if any).

    1. nil - default_log_path is called and it's result returned.

    2. String | Pathname - Value is expanded against workdir and the resulting absolute path is returned (which of course may not exist).

      This means that absolute, home-relative (~/ paths) and workdir-relative paths should all produce the expected results.

Returns:

  • (Pathname)

    Absolute path where the agent will try to log output.

  • (nil)

    If the user doesn't supply a value and default_log_path returns nil. The agent will not log output in this case.



165
166
167
168
169
170
171
# File 'lib/locd/agent.rb', line 165

def self.resolve_log_path log_path:, workdir:, label:
  if log_path.nil?
    default_log_path workdir, label
  else
    log_path.to_pn.expand_path workdir
  end
end

.user_plist_abs_dirPathname

Absolute path to launchd plist directory for the current user, which is an expansion of ~/Library/LaunchAgents.

Returns:

  • (Pathname)


85
86
87
# File 'lib/locd/agent.rb', line 85

def self.user_plist_abs_dir
  Pathname.new( '~/Library/LaunchAgents' ).expand_path
end

Instance Method Details

#<=>(other) ⇒ Fixnum

Compare to another agent by their labels.

Parameters:

Returns:

  • (Fixnum)


1162
1163
1164
# File 'lib/locd/agent.rb', line 1162

def <=> other
  Locd::Label.compare label, other.label
end

#cmd_templateObject



799
800
801
# File 'lib/locd/agent.rb', line 799

def cmd_template
  config['cmd_template']
end

#configObject



794
795
796
# File 'lib/locd/agent.rb', line 794

def config
  plist[Locd.config[:agent, :config_key]]
end

#default_log_path?Boolean

Returns true if the #log_path is the default one we generate.

Returns:

  • (Boolean)

    true if the #log_path is the default one we generate.



739
740
741
# File 'lib/locd/agent.rb', line 739

def default_log_path?
  log_path == self.class.default_log_path( workdir, label )
end

#err_pathPathname?

Path the agent is logging STDERR to.

Returns:

  • (Pathname)

    If the agent is logging STDERR to a file path.

  • (nil)

    If the agent is not logging STDERR.



834
835
836
# File 'lib/locd/agent.rb', line 834

def err_path
  plist['StandardErrorPath'].to_pn if plist['StandardErrorPath']
end

#labelString

Returns:

  • (String)


752
753
754
# File 'lib/locd/agent.rb', line 752

def label
  plist['Label'].freeze
end

#last_exit_code(refresh: false) ⇒ nil, Fixnum

Returns:

  • (nil)

    No last exit code information.

  • (Fixnum)

    Last exit code of process.



789
790
791
# File 'lib/locd/agent.rb', line 789

def last_exit_code refresh: false
  status( refresh: refresh ) && status[:last_exit_code]
end

#load(force: false, enable: false) ⇒ self

Load the agent by executing launchctl load [OPTIONS] LABEL.

This is a bit low-level; you probably want to use #start.

Parameters:

  • force: (Boolean) (defaults to: false)

    Force the loading of the plist. Ignore the launchd Disabled key.

  • enable: (Boolean) (defaults to: false)

    Overrides the launchd Disabled key and sets it to false.

Returns:

  • (self)


908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
# File 'lib/locd/agent.rb', line 908

def load force: false, enable: false
  logger.debug "Loading #{ label } agent...", force: force, enable: enable
  
  result = Locd::Launchctl.load! path, force: force, write: enable
  
  message = if result.err =~ /service\ already\ loaded/
    "already loaded"
  else
    "LOADED"
  end
  
  log_info message, status: status
  
  self
end

#log_pathsArray<Pathname>

Get a list of all unique log paths.

Returns:

  • (Array<Pathname>)


843
844
845
846
847
848
# File 'lib/locd/agent.rb', line 843

def log_paths
  [
    out_path,
    err_path,
  ].compact.uniq
end

#out_pathPathname? Also known as: log_path

Path the agent is logging STDOUT to.

Returns:

  • (Pathname)

    If the agent is logging STDOUT to a file path.

  • (nil)

    If the agent is not logging STDOUT.



819
820
821
# File 'lib/locd/agent.rb', line 819

def out_path
  plist['StandardOutPath'].to_pn if plist['StandardOutPath']
end

#pid(refresh: false) ⇒ nil, Fixnum

Current process ID of the agent (if running).

Parameters:

  • refresh: (Boolean) (defaults to: false)

    When true, will re-read from launchd (and cache results) before returning.

Returns:

  • (nil)

    No process ID (not running).

  • (Fixnum)

    Process ID.



767
768
769
# File 'lib/locd/agent.rb', line 767

def pid refresh: false
  status( refresh: refresh ) && status[:pid]
end

#reload(force: false, enable: false) ⇒ self

#unload then #load the agent.

Parameters:

  • force: (Boolean) (defaults to: false)

    Force the loading of the plist. Ignore the launchd Disabled key.

  • enable: (Boolean) (defaults to: false)

    Overrides the launchd Disabled key and sets it to false.

Returns:

  • (self)


950
951
952
953
# File 'lib/locd/agent.rb', line 950

def reload force: false, enable: false
  unload
  load force: force, enable: enable
end

#remove(logs: false) ⇒ self

Remove the agent by removing it's #path file. Will #stop and #unloads the agent first.

Parameters:

  • logs: (Boolean) (defaults to: false)

    When true remove all logs as well.

Returns:

  • (self)


1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
# File 'lib/locd/agent.rb', line 1032

def remove logs: false
  stop unload: true
  
  if logs
    log_paths.each { |log_path|
      if log_path.exists?
        FileUtils.rm log_path
        log_info "Removed log", path: log_path.to_s
      else
        log_info "Log path does not exist", path: log_path.to_s
      end
    }
  end
  
  FileUtils.rm path
  log_info "REMOVED"
  
  self
end

#restart(reload: true, force: true, enable: false) ⇒ self

Restart the agent (#stop then #start).

Parameters:

  • reload: (Boolean) (defaults to: true)

    Unload then load the agent in launchd.

Returns:

  • (self)


1018
1019
1020
1021
# File 'lib/locd/agent.rb', line 1018

def restart reload: true, force: true, enable: false
  stop unload: reload
  start load: reload, force: force, enable: enable
end

#running?(refresh: false) ⇒ Boolean

Returns:

  • (Boolean)


772
773
774
# File 'lib/locd/agent.rb', line 772

def running? refresh: false
  status( refresh: refresh )[:running]
end

#start(load: true, force: false, enable: false) ⇒ self

Start the agent.

If load is true, calls #load first, and defaults it's force keyword argument to true (the idea being that you actually want the agent to start, even if it's #disabled?).

Parameters:

  • load: (Boolean) (defaults to: true)

    Call #load first, passing it enable and force.

Returns:

  • (self)


970
971
972
973
974
975
976
977
978
979
980
981
982
# File 'lib/locd/agent.rb', line 970

def start load: true, force: false, enable: false
  logger.trace "Starting `#{ label }` agent...",
    load: load,
    force: force,
    enable: enable
  
  self.load( force: force, enable: enable ) if load
  
  Locd::Launchctl.start! label
  log_info "STARTED"
  
  self
end

#status(refresh: false) ⇒ Hash{pid: (Fixnum | nil), status: (Fixnum | nil)}?

The agent's status from parsing launchctl list.

Status is read on demand and cached on the instance.

Parameters:

  • refresh: (Boolean) (defaults to: false)

    When true, will re-read from launchd (and cache results) before returning.

Returns:

  • (Hash{pid: (Fixnum | nil), status: (Fixnum | nil)})

    When launchd has a status entry for the agent.

  • (nil)

    When launchd doesn't have a status entry for the agent.



870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
# File 'lib/locd/agent.rb', line 870

def status refresh: false
  if refresh || @status == :UNKNOWN
    raw_status = Locd::Launchctl.status[label]
    
    # Happens when the agent is not loaded
    @status = if raw_status.nil?
      {
        loaded: false,
        running: false,
        pid: nil,
        last_exit_code: nil,
      }
    else
      {
        loaded: true,
        running: !raw_status[:pid].nil?,
        pid: raw_status[:pid],
        last_exit_code: raw_status[:status],
      }
    end
  end
  
  @status
end

#stop(unload: true, disable: false) ⇒ self

Stop the agent.

Parameters:

  • unload: (Boolean) (defaults to: true)

    Call #unload first, passing it write.

Returns:

  • (self)


994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
# File 'lib/locd/agent.rb', line 994

def stop unload: true, disable: false
  logger.debug "Stopping `#{ label } agent...`",
    unload: unload,
    disable: disable
  
  Locd::Launchctl.stop label
  log_info "STOPPED"
  
  self.unload( disable: disable ) if unload
  
  self
end

#stopped?(refresh: false) ⇒ Boolean

Returns:

  • (Boolean)


777
778
779
# File 'lib/locd/agent.rb', line 777

def stopped? refresh: false
  !running?( refresh: refresh )
end

#to_hObject



1167
1168
1169
1170
1171
# File 'lib/locd/agent.rb', line 1167

def to_h
  self.class::TO_H_NAMES.map { |name|
    [name, send( name )]
  }.to_h
end

#to_json(*args) ⇒ Object



1174
1175
1176
# File 'lib/locd/agent.rb', line 1174

def to_json *args
  to_h.to_json *args
end

#to_yaml(*args) ⇒ Object

TODO Doesn't work!



1181
1182
1183
# File 'lib/locd/agent.rb', line 1181

def to_yaml *args
  to_h.to_yaml *args
end

#unload(disable: false) ⇒ self

Unload the agent by executing launchctl unload [OPTIONS] LABEL.

This is a bit low-level; you probably want to use #stop.

Parameters:

  • disable: (Boolean) (defaults to: false)

    Overrides the launchd Disabled key and sets it to true.

Returns:

  • (self)


934
935
936
937
938
939
940
941
942
# File 'lib/locd/agent.rb', line 934

def unload disable: false
  logger.debug "Unloading #{ label } agent...", disable: disable
  
  result = Locd::Launchctl.unload! path, write: disable
  
  log_info "UNLOADED"
  
  self
end

#update(force: false, **values) ⇒ Locd::Agent

Update specific values on the agent, which may change it's file path if a different label is provided.

Does not mutate this instance! Returns a new Locd::Agent with the updated values.

Parameters:

  • force: (Boolean) (defaults to: false)

    Overwrite any existing agent with the same label.

  • **values (Hash<Symbol, Object>)

Returns:

  • (Locd::Agent)

    A new instance with the updated values.



1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
# File 'lib/locd/agent.rb', line 1073

def update force: false, **values
  logger.trace "Updating `#{ label }` agent", **values
  
  # Remove the `cmd_template` if it's nil of an empty array so that
  # we use the current one
  if  values[:cmd_template].nil? ||
      (values[:cmd_template].is_a?( Array ) && values[:cmd_template].empty?)
    values.delete  :cmd_template
  end
  
  # Make a new plist
  new_plist_data = self.class.create_plist_data(
    label: label,
    workdir: workdir,
    
    log_path: (
      values.key?( :log_path ) ? values.key?( :log_path ) : (
        default_log_path? ? nil : log_path
      )
    ),
    
    # Include the config values, which have the `cmd_template` as well as
    # any extras. Need to symbolize the keys to make the kwds call work
    **config.sym_keys,
    
    # Now merge over with the values we received
    **values.except( :log_path )
  )
  
  new_label = new_plist_data['Label']
  new_plist_abs_path = self.class.plist_abs_path new_label
  
  if new_label == label
    # We maintained the same label, overwrite the file
    path.write Plist::Emit.dump( new_plist_data )
    
    # Load up the new agent from the same path, reload and return it
    self.class.from_path( path ).reload
    
  else
    # The label has changed, so we want to make sure we're not overwriting
    # another agent
    
    if File.exists? new_plist_abs_path
      # There's someone already there!
      # Bail out unless we are forcing the operation
      if force
        logger.info "Overwriting agent #{ new_label } with update to " \
          "agent #{ label } (force: `true`)"
      else
        raise binding.erb "          A different agent already exists at:\n          \n              <%= new_plist_abs_path %>\n          \n          Remove that agent first if you really want to replace it with an\n          updated version of this one or provide `force: true`.\n          \n        END\n      end\n    end\n    \n    # Ok, we're in the clear (save for the obvious race condition, but,\n    # hey, it's a development tool, so fuck it... it's not even clear it's\n    # possible to do an atomic file add from Ruby)\n    new_plist_abs_path.write Plist::Emit.dump( new_plist_data )\n    \n    # Remove this agent\n    remove\n    \n    # And instantiate and load a new agent from the new path\n    self.class.from_path( new_plist_abs_path ).load\n    \n  end\nend\n"

#workdirPathname

Returns The working directory of the agent.

Returns:

  • (Pathname)

    The working directory of the agent.



806
807
808
# File 'lib/locd/agent.rb', line 806

def workdir
  plist['WorkingDirectory'].to_pn
end