module Locd

Definitions

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Namespace

Manage Agent definitions attached to projects.

Namespace

Namespace

Namespace

Namespace

Definitions

Namespace

Definitions

Definitions

Constants

GEM_NAME

The gem name, read from the `//NAME` file, and used in the gemspec.

See {Locd::VERSION} for an explanation of why bare files in the package root are used.

@return [String]

HOST_RE

{Regexp} to match HTTP “Host” header line.

@return [Regexp]

ROOT

Absolute, expanded path to the gem's root directory.

@return [Pathname]

VERSION

String version read from `//VERSION` and used in the gemspec.

I use this approach in order to create a standard versioning system across any package, regardless or runtime, and so that tools can figure out what version a package is without even having it's runtime available.

This has turned out to be super helpful, and I'm surprised it isn't more common or standardized in some way… I think pretty much every package in very language has a root directory and is capable of reading a file to figure out it's state.

@return [String]

Attributes

path[R]

Absolute path to the agent's `.plist` file.

@return [Pathname]

plist[R]

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

@return [Hash<String, V>]

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

[plist]: http://www.rubydoc.info/gems/plist

Public Class Methods

add(label:, force: false, workdir: Pathname.getwd, **kwds) click to toggle source

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

Does not start the agent.

@param [String] label:

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

@param [Boolean] force:

Overwrite any existing agent with the same label.

@param [String | Pathname] workdir:

Working directory for the agent.

@param [Hash<Symbol, Object>] **kwds

Additional keyword arguments to pass to {.create_plist_data}.

@return [Locd::Agent]

The new agent.
# 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) click to toggle source

Pretty much what the name says.

@see .add @see update

@param [String] label

Agent's label (AKA name AKA domain).

@param **values

Agent properties.

@return [Array<((:add | :update), Locd::Agent)]

Whether the agent was added or updated, followed by the agent instance.
# 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
all() click to toggle source

All {Locd::Agent} that are instances of `self`.

So, when invoked through {Locd::Agent.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 {Locd::Agent::Site} instances.

@return [Hamster::Hash<String, Locd::Agent>]

Map of agent {#label} to instance.
# 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) click to toggle source

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

@param [Hash<String, Object>] plist

{include:file:doc/include/plist.md}

@return [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.

@return [nil]

If the plist is not for an agent of the current Loc'd configuration
(and should be ignored by Loc'd).
# 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
config() click to toggle source

@return [Locd::Config]

The configuration.
# File lib/locd.rb, line 60
def self.config
  @config ||= Locd::Config.new
end
create_plist_data(cmd_template:, label:, workdir:, log_path: nil, keep_alive: false, run_at_load: false, **extras) click to toggle source

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

@param [nil | String | Pathname] log_path:

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.

@return [Hash<String, Object>]

{include:file:doc/include/plist.md}
# 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
exists?(label) click to toggle source

Does this agent exist?

@param [String] label

Agent's label (AKA name AKA domain).

@return [Boolean]

# File lib/locd/agent.rb, line 313
def self.exists? label
  File.exists? plist_abs_path( label )
end
find_all(pattern, **options) click to toggle source

Find all the agents that match a pattern.

@see Locd::Pattern

@param [String | Pattern] pattern

Pattern to match against agent.

When it's a {String}, passed to {Locd::Pattern.from} to get the pattern.

@param [**<Symbol, V>] options

Passed to {Locd::Pattern.from} when `pattern` is a {String}.

@return (see .all)

# 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) click to toggle source

Just like {.find_all} but raises if result is empty.

@param (see .find_all) @return (see .find_all)

@raise [NRSER::CountError]

If no agents were found.
# 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) click to toggle source

Find a single {Locd::Agent} matching `pattern` or raise.

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

@see Locd::Pattern

@param (see .find_all)

@return [Locd::Agent]

# 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) click to toggle source

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

@param [Pathname | String] plist_path

Path to the `.plist` file.

@return [Locd::Agent]

# 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) click to toggle source

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

@param [String] label

The agent's label.

@return [Agent]

If a Loc'd agent with `label` is installed.

@return [nil]

If no Loc'd agent with `label` is installed.
# 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) click to toggle source

Like {.get} but raises if the agent is not found.

@param [String] label

The agent's label.

@return [Agent]

The agent.

@raise [NotFoundError]

If the agent isn't there.
# 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
labels() click to toggle source

Labels for all installed Loc'd agents.

@return [Hamster::Vector<String>]

# File lib/locd/agent.rb, line 369
def self.labels
  all.keys.sort
end
new(plist:, path: @path = path.to_pn.expand_path) click to toggle source

Constructor

# 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
plists() click to toggle source

All installed Loc'd agents.

@return [Hamster::Hash<String, Locd::Agent>]

Map of all Loc'd agents by label.
# 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) click to toggle source

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

@example

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"

@param [String | Array<String>] cmd_template:

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

@param [String] label:

The agent's {#label}.

@param [Pathname] workdir:

The agent's {#workdir}.

@param [Hash<Symbol, to_s>] **extras

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

@return [String]

Command string with substitutions made.
# 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(project_root, path = nil) click to toggle source
# File lib/locd/util.rb, line 9
def self.resolve project_root, path = nil
  return project_root if path.nil?
  
  if path.start_with? '//'
    project_root / path[2..-1]
  elsif path.start_with? '/'
    path.to_pn
  else
    project_root / path
  end
end
resolve_to_s(*args) click to toggle source

Just like {.resolve} but returns a {String} (instead of a {Pathname}).

@param (see .resolve) @return [String]

# File lib/locd/util.rb, line 27
def self.resolve_to_s *args
  resolve( *args ).to_s
end

Public Instance Methods

<=>(other) click to toggle source

Compare to another agent by their labels.

@param [Locd::Agent] other

The other agent.

@return [Fixnum]

# File lib/locd/agent.rb, line 1222
def <=> other
  Locd::Label.compare label, other.label
end
cmd_template() click to toggle source
# File lib/locd/agent.rb, line 860
def cmd_template
  config['cmd_template']
end
config() click to toggle source
# File lib/locd/agent.rb, line 855
def config
  plist[Locd.config[:agent, :config_key]]
end
default_log_path?() click to toggle source

@return [Boolean]

`true` if the {#log_path} is the default one we generate.
# 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) click to toggle source
# File lib/locd/agent.rb, line 1040
def ensure_running **start_kwds
  start( **start_kwds ) unless running?
end
err_path() click to toggle source

Path the agent is logging `STDERR` to.

@return [Pathname]

If the agent is logging `STDERR` to a file path.

@return [nil]

If the agent is not logging `STDERR`.
# File lib/locd/agent.rb, line 895
def err_path
  plist['StandardErrorPath'].to_pn if plist['StandardErrorPath']
end
label() click to toggle source

@return [String]

# File lib/locd/agent.rb, line 813
def label
  plist['Label'].freeze
end
last_exit_code(refresh: false) click to toggle source

@return [nil]

No last exit code information.

@return [Fixnum]

Last exit code of process.
# 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) click to toggle source

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

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

@param [Boolean] force:

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

@param [Boolean] enable:

Overrides the `launchd` *Disabled* key and sets it to `false`.

@return [self]

# 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_path()
Alias for: out_path
log_paths() click to toggle source

Get a list of all unique log paths.

@return [Array<Pathname>]

# File lib/locd/agent.rb, line 904
def log_paths
  [
    out_path,
    err_path,
  ].compact.uniq
end
out_path() click to toggle source

Path the agent is logging `STDOUT` to.

@return [Pathname]

If the agent is logging `STDOUT` to a file path.

@return [nil]

If the agent is not logging `STDOUT`.
# File lib/locd/agent.rb, line 880
def out_path
  plist['StandardOutPath'].to_pn if plist['StandardOutPath']
end
Also aliased as: log_path
pid(refresh: false) click to toggle source

Current process ID of the agent (if running).

@param (see status)

@return [nil]

No process ID (not running).

@return [Fixnum]

Process ID.
# File lib/locd/agent.rb, line 828
def pid refresh: false
  status( refresh: refresh ) && status[:pid]
end
reload(force: false, enable: false) click to toggle source

{#unload} then {#load} the agent.

@param (see load) @return (see load)

# File lib/locd/agent.rb, line 1005
def reload force: false, enable: false
  unload
  load force: force, enable: enable
end
remove(logs: false) click to toggle source

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

@param [Boolean] logs:

When `true` remove all logs as well.

@return [self]

# 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) click to toggle source

Restart the agent ({#stop} then {#start}).

@param [Boolean] reload:

Unload then load the agent in `launchd`.

@param force (see start) @param enable (see start)

@return [self]

# 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) click to toggle source
# File lib/locd/agent.rb, line 833
def running? refresh: false
  status( refresh: refresh )[:running]
end
start(load: true, force: false, enable: false) click to toggle source

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

@param [Boolean] load:

Call {#load} first, passing it `enable` and `force`.

@param force (see load) @param enable (see load)

@return [self]

# 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) click to toggle source

The agent's status from parsing `launchctl list`.

Status is read on demand and cached on the instance.

@param [Boolean] refresh:

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

@return [Status]

# 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) click to toggle source

Stop the agent.

@param [Boolean] unload:

Call {#unload} first, passing it `write`.

@param disable (see unload)

@return [self]

# 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) click to toggle source
# File lib/locd/agent.rb, line 838
def stopped? refresh: false
  !running?( refresh: refresh )
end
to_h() click to toggle source
# 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) click to toggle source
# File lib/locd/agent.rb, line 1234
def to_json *args
  to_h.to_json *args
end
to_yaml(*args) click to toggle source

TODO Doesn't work!

# File lib/locd/agent.rb, line 1241
def to_yaml *args
  to_h.to_yaml *args
end
unload(disable: false) click to toggle source

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

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

@param [Boolean] disable:

Overrides the `launchd` *Disabled* key and sets it to `true`.

@return [self]

# 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) click to toggle source

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._**

@param [Boolean] force:

Overwrite any existing agent with the same label.

@param [Hash<Symbol, Object>] **values

See {.create_plist_data}.

@return [Locd::Agent]

A new instance with the updated values.
# 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
workdir() click to toggle source

@return [Pathname]

The working directory of the agent.
# File lib/locd/agent.rb, line 867
def workdir
  plist['WorkingDirectory'].to_pn
end

Protected Instance Methods

init_ensure_out_dirs_exist() click to toggle source

Create directories for any output file if they don't already exist.

We need to create the directories for the output files before we ever try to start the agent because otherwise it seems that `launchd` *will create them*, but it will *create them as `root`*, then try to write to them *as the user*, fail, and crash the process with a `78` exit code.

How or why that makes sense to `launchd`, who knows.

But, since it's nice to be able to start the agents through `launchctl` or `lunchy`, and having a caveat that *they need to be started through Loc'd the first time* sucks, during initialization seems like a reasonable time, at least for now (I don't like doing stuff like this curing construction, but whatever for the moment, let's get it working).

@return [nil]

# File lib/locd/agent.rb, line 1269
def init_ensure_out_dirs_exist
  [out_path, err_path].compact.each do |path|
    dir = path.dirname
    unless dir.exist?
      logger.debug "Creating directory for output",
        label: label,
        dir: dir,
        path: path
      FileUtils.mkdir_p dir
    end
  end
  
  nil
end
log_info(message, **payload) click to toggle source

Log a message with a details payload, prefixing the agent's label.

Used to relay info to the user in the CLI.

@param [#to_s] message

Message to log.

@param [Hash<Symbol, Object>] **payload

Optional values to dump.

@return [void]

# File lib/locd/agent.rb, line 1297
def log_info message, **payload
  logger.info "Agent `#{ label }` #{ message }", **payload
end