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



733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
# File 'lib/locd/agent.rb', line 733

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)


727
728
729
# File 'lib/locd/agent.rb', line 727

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.



720
721
722
# File 'lib/locd/agent.rb', line 720

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:



641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
# File 'lib/locd/agent.rb', line 641

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:



700
701
702
703
704
705
706
# File 'lib/locd/agent.rb', line 700

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:



328
329
330
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
# File 'lib/locd/agent.rb', line 328

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



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

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



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
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
# File 'lib/locd/agent.rb', line 521

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



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

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)


310
311
312
# File 'lib/locd/agent.rb', line 310

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:



421
422
423
424
# File 'lib/locd/agent.rb', line 421

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.



437
438
439
440
441
442
443
444
445
446
447
448
449
# File 'lib/locd/agent.rb', line 437

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:



402
403
404
# File 'lib/locd/agent.rb', line 402

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:



267
268
269
270
271
# File 'lib/locd/agent.rb', line 267

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.



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

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


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

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.



108
109
110
# File 'lib/locd/agent.rb', line 108

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)


134
135
136
# File 'lib/locd/agent.rb', line 134

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.



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

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.



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/locd/agent.rb', line 485

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.



201
202
203
204
205
206
207
# File 'lib/locd/agent.rb', line 201

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)


121
122
123
# File 'lib/locd/agent.rb', line 121

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)


1191
1192
1193
# File 'lib/locd/agent.rb', line 1191

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

#cmd_templateObject



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

def cmd_template
  config['cmd_template']
end

#configObject



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

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.



774
775
776
# File 'lib/locd/agent.rb', line 774

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.



869
870
871
# File 'lib/locd/agent.rb', line 869

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

#labelString

Returns:

  • (String)


787
788
789
# File 'lib/locd/agent.rb', line 787

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.



824
825
826
# File 'lib/locd/agent.rb', line 824

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)


937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
# File 'lib/locd/agent.rb', line 937

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


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

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.



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

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.



802
803
804
# File 'lib/locd/agent.rb', line 802

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)


979
980
981
982
# File 'lib/locd/agent.rb', line 979

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)


1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
# File 'lib/locd/agent.rb', line 1061

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)


1047
1048
1049
1050
# File 'lib/locd/agent.rb', line 1047

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)


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

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)


999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
# File 'lib/locd/agent.rb', line 999

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:



901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
# File 'lib/locd/agent.rb', line 901

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)


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

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)


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

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

#to_hObject



1196
1197
1198
1199
1200
# File 'lib/locd/agent.rb', line 1196

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

#to_json(*args) ⇒ Object



1203
1204
1205
# File 'lib/locd/agent.rb', line 1203

def to_json *args
  to_h.to_json *args
end

#to_yaml(*args) ⇒ Object

TODO Doesn't work!



1210
1211
1212
# File 'lib/locd/agent.rb', line 1210

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)


963
964
965
966
967
968
969
970
971
# File 'lib/locd/agent.rb', line 963

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.



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
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
# File 'lib/locd/agent.rb', line 1102

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.



841
842
843
# File 'lib/locd/agent.rb', line 841

def workdir
  plist['WorkingDirectory'].to_pn
end