Module: LaunchAgentManagementInstanceMethods

Includes:
LaunchAgentConstants, Which
Included in:
LaunchAgentManager
Defined in:
lib/kanseishitsu/launch_agent_manager.rb

Overview

Define module LaunchAgentManagementInstanceMethods

Constant Summary

Constants included from LaunchAgentConstants

LaunchAgentConstants::BOOTOUT_TEMPLATE, LaunchAgentConstants::CDATA_PATTERN, LaunchAgentConstants::DEFAULT_ENCODING, LaunchAgentConstants::DOCTYPES, LaunchAgentConstants::DOCTYPE_ELEMENT, LaunchAgentConstants::INTERVALS, LaunchAgentConstants::INTERVAL_XPATH_TEMPLATE, LaunchAgentConstants::KEYS_REQUIRING_CDATA, LaunchAgentConstants::LABEL_NAMESPACE, LaunchAgentConstants::LABEL_XPATH, LaunchAgentConstants::LAUNCHCTL_TEMPLATE, LaunchAgentConstants::LAUNCH_AGENTS_DIR_PATH, LaunchAgentConstants::LOCAL_STRING_XPATH, LaunchAgentConstants::ON_LOGIN, LaunchAgentConstants::PROGRAM_ARGUMENTS_XPATH, LaunchAgentConstants::REMOVE_TEMPLATE, LaunchAgentConstants::SCHEDULE_PARTS_COUNT, LaunchAgentConstants::SCHEDULE_XPATH_TEMPLATE, LaunchAgentConstants::START_CALENDAR_INTERVAL_XPATH, LaunchAgentConstants::SUCCESS_MESSAGE, LaunchAgentConstants::WATCH_PATHS_XPATH

Instance Method Summary collapse

Methods included from Which

#executable?, #explicit_which, #find_executable_in_path, #portable_which

Instance Method Details

#create_launch_agent(exe, exe_path, schedule) ⇒ Object

Create a file monitor launch agent



208
209
210
211
212
213
214
215
216
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 208

def create_launch_agent(exe, exe_path, schedule)
  args = extract_program_path(exe)
  label = derive_job_label_from_file_path(exe_path || args.first)
  definition = define_plist_contents(label, args, schedule)
  doc = generate_plist_xml(definition)
  plist_path = save_plist(label, doc)
  load_launchd_job(plist_path)
  puts format(SUCCESS_MESSAGE, label: label)
end

#define_calendar_interval(cron_schedule) ⇒ Object

Define cron settings for plist



51
52
53
54
55
56
57
58
59
60
61
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 51

def define_calendar_interval(cron_schedule)
  cron_fields = cron_schedule.split
  calendar_interval = {}
  INTERVALS.each do |key|
    value = cron_fields.shift
    raise "Invalid cron string: #{cron_schedule}" if value.nil? || value.empty?

    calendar_interval[key] = value
  end
  calendar_interval
end

#define_plist_contents(label, args, cron_schedule) ⇒ Object

Define the plist contents rubocop: disable Metrics/MethodLength



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 65

def define_plist_contents(label, args, cron_schedule)
  definition = {}
  definition['Label'] = label
  definition['StartCalendarInterval'] = define_calendar_interval(cron_schedule)
  unless (system_path = ENV.fetch('PATH', nil)).nil? || system_path.empty?
    definition['EnvironmentVariables'] = {}
    definition['EnvironmentVariables']['PATH'] = system_path
  end
  if args.length == 1
    definition['Program'] = args.first
  else
    definition['ProgramArguments'] = args
  end
  definition
end

#derive_job_label_from_file_path(file_path) ⇒ Object

Create the job name based on the given file path



44
45
46
47
48
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 44

def derive_job_label_from_file_path(file_path)
  label = LABEL_NAMESPACE.dup
  label << File.basename(file_path, '.*')
  label.join('.')
end

#doctypeObject



104
105
106
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 104

def doctype
  format(DOCTYPE_ELEMENT, doctypes: DOCTYPES.join(' '))
end

#execute(command) ⇒ Object



94
95
96
97
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 94

def execute(command)
  log.debug "Executing command: #{command}"
  system(command)
end

#extract_program_path(executable_with_args) ⇒ Object

Extract program path and arguments, verifying executable rubocop: disable Metrics/MethodLength



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 25

def extract_program_path(executable_with_args)
  *args = if executable_with_args.respond_to?(:split)
    executable_with_args.split
  else
    executable_with_args
  end
  exe_path = which(args.shift)
  if exe_path.nil? || !File.exist?(exe_path)
    abort "Cannot find executable in path: #{exe_path}"
  end
  if exe_path.nil? || !File.executable?(exe_path) || File.directory?(exe_path)
    abort "Given file is not executable: #{exe_path}"
  end
  args.unshift(exe_path)
  args
end

#generate_plist_xml(definition_hash) ⇒ Object

Use Nokogiri to create a launchd plist XML document



109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 109

def generate_plist_xml(definition_hash)
  doc = Nokogiri::XML(doctype)
  doc.encoding = DEFAULT_ENCODING
  plist = doc.create_element('plist', version: '1.0')
  root = doc.create_element('dict')

  definition_hash.each do |key, value|
    create_xml_tag(doc, root, key, value)
  end

  plist << root
  doc << plist
  doc
end

#launch_agent_labelsObject

Return a list of all user launch agent labels



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 125

def launch_agent_labels
  labels = []

  Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist')).each do |file_path|
    doc = File.open(file_path) { |file| Nokogiri::XML(file) }
    label_node = doc.xpath(LABEL_XPATH)
    labels << label_node.text unless label_node.empty?
  end

  labels
end

#list_launch_agent_labelsObject

List labels of launch agents



184
185
186
187
188
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 184

def list_launch_agent_labels
  launch_agent_labels.each do |label|
    puts label
  end
end

#load_launchd_job(plist_path) ⇒ Object

Load the launchd job



100
101
102
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 100

def load_launchd_job(plist_path)
  execute(format(LAUNCHCTL_TEMPLATE, uid: Process.uid, plist: plist_path))
end

#parse_plist(plist_path) ⇒ Object

Function to parse plist and extract crontab-like schedule rubocop: disable Metrics/AbcSize rubocop: disable Metrics/MethodLength



155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 155

def parse_plist(plist_path)
  plist = {}
  doc = Nokogiri::XML(File.read(plist_path))
  label = doc.xpath(LABEL_XPATH).text
  watch_paths = doc.xpath(WATCH_PATHS_XPATH).xpath(LOCAL_STRING_XPATH).map(&:text)
  program_args = doc.xpath(PROGRAM_ARGUMENTS_XPATH).xpath(LOCAL_STRING_XPATH).map(&:text)
  schedule = parse_schedule(doc)
  plist[:label] = label unless label.nil?
  plist[:command] = program_args.empty? ? label : program_args.join(' ')
  plist[:watch_paths] = watch_paths unless watch_paths.nil?
  plist[:schedule] = schedule unless schedule.nil?
  plist
end

#parse_schedule(doc) ⇒ Object

Parse the calendar interval



138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 138

def parse_schedule(doc)
  watch_paths_node = doc.xpath(WATCH_PATHS_XPATH)
  watch_paths = watch_paths_node.xpath(LOCAL_STRING_XPATH).map(&:text)
  return watch_paths.first unless watch_paths.empty?

  intervals = doc.xpath(START_CALENDAR_INTERVAL_XPATH).first
  return ON_LOGIN unless intervals

  INTERVALS.map do |interval|
    xpath = format(INTERVAL_XPATH_TEMPLATE, key: interval)
    intervals.at_xpath(xpath).text rescue '*'
  end.join(' ')
end

#remove_launch_agent(label) ⇒ Object

Remove plist Launch Agents by label rubocop: disable Metrics/MethodLength



192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 192

def remove_launch_agent(label)
  log.debug "Removing launch agent: #{label}"
  plist_path = File.expand_path(File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist"))
  log.debug "Removing launch agent plist definition file: #{plist_path}"
  if File.exist?(plist_path)
    execute(format(BOOTOUT_TEMPLATE, uid: Process.uid, label: label))
    execute(format(REMOVE_TEMPLATE, uid: Process.uid, label: label))
    FileUtils.rm(plist_path)
    puts "Removed launch agent: #{label}"
  else
    warn "Not found; crontab or launch agent: #{label}"
  end
end

#save_plist(label, doc) ⇒ Object

Save plist file to LaunchAgents directory; do not overwrite an already existing file with the same name at the generated path



84
85
86
87
88
89
90
91
92
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 84

def save_plist(label, doc)
  FileUtils.mkdir_p(LAUNCH_AGENTS_DIR_PATH)
  plist_path = File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist")
  return plist_path if File.exist?(plist_path)

  log.debug "Contents of plist xml document:\n#{doc.to_xml}"
  File.write(plist_path, doc.to_xml)
  plist_path
end

#show_all_launch_agentsObject

Show the relevant launch agents



172
173
174
175
176
177
178
179
180
181
# File 'lib/kanseishitsu/launch_agent_manager.rb', line 172

def show_all_launch_agents
  Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist')).map do |plist_path|
    job = parse_plist(plist_path)
    if job[:command].empty?
      puts "@disabled #{plist_path}"
    else
      puts "#{job[:schedule]} #{job[:command]}"
    end
  end
end