Class: Locd::Agent

Inherits:
Object
  • Object
show all
Includes:
Comparable, NRSER::Log::Mixin
Defined in:
lib/locd/agent.rb,
lib/locd/agent/proxy.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, Status

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



759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
# File 'lib/locd/agent.rb', line 759

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

Instance Attribute Details

#pathPathname (readonly)

Absolute path to the agent's .plist file.

Returns:

  • (Pathname)


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

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.



746
747
748
# File 'lib/locd/agent.rb', line 746

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:



667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'lib/locd/agent.rb', line 667

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 <<~END
        Agent <%= label %> already exists at:
        
            <%= plist_abs_path %>
        
      END
    end
  end

  plist_data = create_plist_data label: label, workdir: workdir, **kwds
  logger.debug "Property list created", data: plist_data
  
  plist_string = Plist::Emit.dump plist_data
  plist_abs_path.write plist_string
  logger.debug "Property list written", path: plist_abs_path
  
  from_path( plist_abs_path ).tap { |agent|
    agent.send :log_info, "added"
  }
end

.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:



726
727
728
729
730
731
732
# File 'lib/locd/agent.rb', line 726

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:



331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
# File 'lib/locd/agent.rb', line 331

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).



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/locd/agent.rb', line 236

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>)



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
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
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
# File 'lib/locd/agent.rb', line 547

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



156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/locd/agent.rb', line 156

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)


313
314
315
# File 'lib/locd/agent.rb', line 313

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:



447
448
449
450
# File 'lib/locd/agent.rb', line 447

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.



463
464
465
466
467
468
469
470
471
472
473
474
475
# File 'lib/locd/agent.rb', line 463

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 }",
        value: 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:



428
429
430
# File 'lib/locd/agent.rb', line 428

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:



270
271
272
273
274
# File 'lib/locd/agent.rb', line 270

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) ⇒ Agent?

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

Parameters:

  • label (String)

    The agent's label.

Returns:

  • (Agent)

    If a Loc'd agent with label is installed.

  • (nil)

    If no Loc'd agent with label is installed.



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

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

.get!(label) ⇒ Agent

Like get but raises if the agent is not found.

Parameters:

  • label (String)

    The agent's label.

Returns:

Raises:



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

def self.get! label
  get( label ).tap do |agent|
    if agent.nil?
      raise NotFoundError, [
          self, "with label", label,
          "not found. Expected the property list at", plist_abs_path( label )
        ].map( &:to_s ).join( ' ' )
    end
  end
end

.labelsHamster::Vector<String>

Labels for all installed Loc'd agents.

Returns:

  • (Hamster::Vector<String>)


369
370
371
# File 'lib/locd/agent.rb', line 369

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.



111
112
113
# File 'lib/locd/agent.rb', line 111

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)


137
138
139
# File 'lib/locd/agent.rb', line 137

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.



287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/locd/agent.rb', line 287

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.



511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
# File 'lib/locd/agent.rb', line 511

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.



204
205
206
207
208
209
210
# File 'lib/locd/agent.rb', line 204

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)


124
125
126
# File 'lib/locd/agent.rb', line 124

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)


1222
1223
1224
# File 'lib/locd/agent.rb', line 1222

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

#cmd_templateObject



860
861
862
# File 'lib/locd/agent.rb', line 860

def cmd_template
  config['cmd_template']
end

#configObject



855
856
857
# File 'lib/locd/agent.rb', line 855

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.



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

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

#ensure_running(**start_kwds) ⇒ Object



1040
1041
1042
# File 'lib/locd/agent.rb', line 1040

def ensure_running **start_kwds
  start( **start_kwds ) unless running?
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.



895
896
897
# File 'lib/locd/agent.rb', line 895

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

#labelString

Returns:

  • (String)


813
814
815
# File 'lib/locd/agent.rb', line 813

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.



850
851
852
# File 'lib/locd/agent.rb', line 850

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)


963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
# File 'lib/locd/agent.rb', line 963

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>)


904
905
906
907
908
909
# File 'lib/locd/agent.rb', line 904

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.



880
881
882
# File 'lib/locd/agent.rb', line 880

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.



828
829
830
# File 'lib/locd/agent.rb', line 828

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)


1005
1006
1007
1008
# File 'lib/locd/agent.rb', line 1005

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)


1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
# File 'lib/locd/agent.rb', line 1092

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)


1078
1079
1080
1081
# File 'lib/locd/agent.rb', line 1078

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)


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

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)


1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
# File 'lib/locd/agent.rb', line 1025

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) ⇒ Status

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:



927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
# File 'lib/locd/agent.rb', line 927

def status refresh: false
  if refresh || @status.nil?
    raw_status = Locd::Launchctl.status[label]
    
    # Happens when the agent is not loaded
    @status = if raw_status.nil?
      Status.new \
        loaded: false,
        running: false,
        pid: nil,
        last_exit_code: nil
    else
      Status.new \
        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)


1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
# File 'lib/locd/agent.rb', line 1054

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)


838
839
840
# File 'lib/locd/agent.rb', line 838

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

#to_hObject



1227
1228
1229
1230
1231
# File 'lib/locd/agent.rb', line 1227

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

#to_json(*args) ⇒ Object



1234
1235
1236
# File 'lib/locd/agent.rb', line 1234

def to_json *args
  to_h.to_json *args
end

#to_yaml(*args) ⇒ Object

TODO Doesn't work!



1241
1242
1243
# File 'lib/locd/agent.rb', line 1241

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)


989
990
991
992
993
994
995
996
997
# File 'lib/locd/agent.rb', line 989

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.



1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
# File 'lib/locd/agent.rb', line 1133

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 <<-END
          A different agent already exists at:
          
              <%= new_plist_abs_path %>
          
          Remove that agent first if you really want to replace it with an
          updated version of this one or provide `force: true`.
          
        END
      end
    end
    
    # Ok, we're in the clear (save for the obvious race condition, but,
    # hey, it's a development tool, so fuck it... it's not even clear it's
    # possible to do an atomic file add from Ruby)
    new_plist_abs_path.write Plist::Emit.dump( new_plist_data )
    
    # Remove this agent
    remove
    
    # And instantiate and load a new agent from the new path
    self.class.from_path( new_plist_abs_path ).load
    
  end
end

#workdirPathname

Returns The working directory of the agent.

Returns:

  • (Pathname)

    The working directory of the agent.



867
868
869
# File 'lib/locd/agent.rb', line 867

def workdir
  plist['WorkingDirectory'].to_pn
end