kanseishitsu 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 25050a62712f4707b8f1b388647b618d7d722fc15f9a7713a6a2b06d504236d6
4
+ data.tar.gz: adf5c5eba9308caeb08cec65eefa966caf9f9ab6784eeef243d50030be5a0f79
5
+ SHA512:
6
+ metadata.gz: 1ec5b00e46746dbf649876030ca6b7825471c2e4d22ea1f8ba5af2ca7adbdc03328ae61be34b74f81537c99aa4cc75be9dcd60425f44a60584ec433121c6f8a2
7
+ data.tar.gz: 6f89365ab7c0d2cc4d724ec71fae360cc66d70bb94492b209b66dba2cb23f470d22fcb634b7eb1776190774c8aa36ea0131304c9060c35a7e569a69a54f80eda
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nels Nelson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # kanseishitsu
2
+
3
+ A user who wishes to define their own cron jobs and file monitors in macOS faces some obstacles.
4
+
5
+ As of 2024, the current state of the art relies on XML plist files to define jobs in the `Library/LaunchAgents` directory in a user's home directory.
6
+
7
+ XML is not a very simple format for a human being to read or write.
8
+
9
+ If you already have the skills to read and write macOS LaunchAgent XML documents with ease, then the authors commend you for your determination and studiousness.
10
+
11
+ Otherwise, for all of you lazy and ignorant people like the authors, this gem is for you!
12
+
13
+ The `kanseishitsu` gem provides some little ruby scripts which will support simpler management of such launch agents.
14
+
15
+
16
+ ## Usage
17
+
18
+ Here is some usage and examples for each script.
19
+
20
+
21
+ ### cron.rb
22
+
23
+ The usage for `cron.rb`.
24
+
25
+ The `cron.rb` executable uses something similar to a `crontab` entry in GNU/Linux.
26
+
27
+ ```sh
28
+ $ cron.rb --help
29
+ Usage: cron.rb <crontab>|<options>
30
+
31
+ Options:
32
+ --show-all Show all cron jobs
33
+ -l, --list List launch agent labels
34
+ -r, --remove=<label> Remove a cron job by label
35
+ ```
36
+
37
+
38
+ #### Example
39
+
40
+ For example:
41
+
42
+ ```sh
43
+ cron.rb "0 1 * * * archive.rb ~/Downloads/archive"
44
+ ```
45
+
46
+
47
+ ### watch.rb
48
+
49
+ The usage for `watch.rb`.
50
+
51
+ ```sh
52
+ $ watch.rb --help
53
+ Usage: watch.rb <watch_path> <handler_program_path>
54
+
55
+ Options:
56
+ --show-all Show watchers
57
+ -l, --list List launch agent labels
58
+ -r, --remove=<label> Remove a watcher by label
59
+ ```
60
+
61
+
62
+ #### Example
63
+
64
+ For example:
65
+
66
+ ```sh
67
+ watch.rb ~/Desktop ~/.local/usr/bin/desktop.rb
68
+ ```
69
+
70
+
71
+ ## Docker
72
+
73
+ Build the docker image to support CI/CD containers.
74
+
75
+ ```sh
76
+ docker buildx build --tag="$(basename $(pwd))" .
77
+ docker run --interactive --tty --rm --name "$(basename $(pwd))" "$(basename $(pwd))"
78
+ ```
79
+
80
+
81
+ ## Name meaning that nobody should really care about
82
+
83
+ The Japanese word "管制室" (Kanseishitsu) translates to "control room" in English. It refers to a room from which operations are directed or monitored, typically involving equipment and personnel to oversee and manage various functions such as in factories, airports, or television studios.
84
+
85
+ Pronounced: Kahn-say-she-tsu
86
+
87
+ Here's how to say each part:
88
+
89
+ - Kahn: like the beginning of "con" in "convenient," but with a more open 'a' sound.
90
+ - Say: just like the English word "say."
91
+ - She: as in "she."
92
+ - Tsu: this part can be a bit tricky because it's not a sound typically found in English. It's similar to saying "sue," but start it with a soft 't' sound placed right before the 's'.
93
+
94
+
95
+ ## Project file tree
96
+
97
+ Here is a bird's-eye view of the project layout.
98
+
99
+ ```sh
100
+ date && tree -A -I "Gemfile.lock|*.gem|tmp|vendor"
101
+ Wed May 15 23:07:43 CDT 2024
102
+ .
103
+ ├── Dockerfile
104
+ ├── Gemfile
105
+ ├── LICENSE.md
106
+ ├── README.md
107
+ ├── Rakefile
108
+ ├── bin
109
+ │ ├── cron.rb
110
+ │ └── watch.rb
111
+ ├── kanseishitsu.gemspec
112
+ ├── kanseishitsu.png
113
+ ├── lib
114
+ │ ├── kanseishitsu
115
+ │ │ ├── cron
116
+ │ │ │ ├── argument_parser.rb
117
+ │ │ │ └── launch_agent_manager_rexml.rb
118
+ │ │ ├── cron.rb
119
+ │ │ ├── launch_agent_constants.rb
120
+ │ │ ├── launch_agent_manager.rb
121
+ │ │ ├── logging.rb
122
+ │ │ ├── plist_parser.rb
123
+ │ │ ├── version.rb
124
+ │ │ ├── watch
125
+ │ │ │ └── argument_parser.rb
126
+ │ │ ├── watch.rb
127
+ │ │ ├── which.rb
128
+ │ │ ├── xml_helper.rb
129
+ │ │ └── xml_helper_rexml.rb
130
+ │ └── kanseishitsu.rb
131
+ ├── scripts
132
+ │ └── version_bump.sh
133
+ └── spec
134
+ ├── spec_helper.rb
135
+ ├── test_spec.rb
136
+ └── verify
137
+ ├── launchctl.rb
138
+ └── verify_spec.rb
139
+
140
+ 8 directories, 28 files
141
+ ```
data/bin/cron.rb ADDED
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # Copyright Nels Nelson 2024 but freely usable (see license)
10
+
11
+ # Usage: cron.rb <crontab>|<options>
12
+ #
13
+ # Options:
14
+ # --show-all Show cron jobs
15
+ # -l, --list List cron jobs labels
16
+ # -r, --remove=<label> Remove a cron job by label
17
+ #
18
+ # Examples:
19
+ #
20
+ # cron.rb "0 1 * * * archive.rb ${HOME}/Downloads/archive"
21
+ # cron.rb --show-all
22
+ # cron.rb --list
23
+ # cron.rb --remove=com.local.archive
24
+
25
+ require_relative '../lib/kanseishitsu'
26
+
27
+ Object.new.extend(Cron).main
data/bin/watch.rb ADDED
@@ -0,0 +1,27 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # encoding: utf-8
4
+ # frozen_string_literal: false
5
+
6
+ # -*- mode: ruby -*-
7
+ # vi: set ft=ruby :
8
+
9
+ # Copyright Nels Nelson 2024 but freely usable (see license)
10
+
11
+ # Usage: watch.rb <path> [handler_program_path]
12
+ #
13
+ # Options:
14
+ # --show-all Show watchers
15
+ # -l, --list List launch agent labels
16
+ # -r, --remove=<label> Remove a watcher by label
17
+ #
18
+ # Examples:
19
+ #
20
+ # watch.rb ~/Desktop ~/.local/usr/bin/desktop.rb
21
+ # watch.rb --show-all
22
+ # watch.rb --list
23
+ # watch.rb --remove=com.local.desktop
24
+
25
+ require_relative '../lib/kanseishitsu'
26
+
27
+ Object.new.extend(Watcher).main
@@ -0,0 +1,120 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ require 'logger'
10
+ require 'optparse'
11
+
12
+ require_relative '../version'
13
+
14
+ # Define module Cron
15
+ module Cron
16
+ # Define the ArgumentsParser class
17
+ class ArgumentsParser
18
+ FLAGS = %i[banner show_all list remove verbose version].freeze
19
+ POSITIONAL = %i[crontab].freeze
20
+ attr_reader :parser, :options
21
+
22
+ def initialize(option_parser = OptionParser.new)
23
+ @parser = option_parser
24
+ @options = {}
25
+ FLAGS.each { |method_name| self.method(method_name).call }
26
+ end
27
+
28
+ def banner
29
+ @parser.banner = "Usage: #{File.basename($PROGRAM_NAME)} <crontab>|<options>"
30
+ @parser.separator ''
31
+ @parser.separator 'Options:'
32
+ end
33
+
34
+ def show_all
35
+ @parser.on('--show-all', 'Show cron jobs') do
36
+ @options[:show_all] = true
37
+ end
38
+ end
39
+
40
+ def list
41
+ @parser.on('-l', '--list', 'List cron jobs labels') do
42
+ @options[:list] = true
43
+ end
44
+ end
45
+
46
+ def remove
47
+ @parser.on('-r', '--remove=<label>', 'Remove a cron job by label') do |label|
48
+ @options[:remove] = label
49
+ end
50
+ end
51
+
52
+ def verbose
53
+ @options[:log_level] ||= Logger::INFO
54
+ @parser.on_tail('-v', '--verbose', 'Increase verbosity') do
55
+ @options[:log_level] -= 1
56
+ end
57
+ end
58
+
59
+ def version
60
+ @parser.on_tail('--version', 'Show version') do
61
+ puts "#{File.basename($PROGRAM_NAME)} version #{Kanseishitsu::VERSION}"
62
+ exit
63
+ end
64
+ end
65
+
66
+ def crontab(args)
67
+ crontab = args.shift.gsub(/\A['"]|['"]\z/, '').split
68
+ @options[:crontab] = crontab
69
+ @options[:cron_schedule] =
70
+ crontab.take(LaunchAgentManager::SCHEDULE_PARTS_COUNT).join(' ')
71
+ @options[:executable_path_with_args] =
72
+ crontab.drop(LaunchAgentManager::SCHEDULE_PARTS_COUNT).join(' ')
73
+ end
74
+
75
+ def demand(arg, positional: false)
76
+ return @options[arg] unless @options[arg].nil?
77
+
78
+ required_arg = if positional then "<#{arg}>"
79
+ else "--#{arg.to_s.gsub(UNDERSCORE_PATTERN, HYPHEN_STRING)}"
80
+ end
81
+ raise OptionParser::MissingArgument, "Required argument: #{required_arg}"
82
+ end
83
+
84
+ def positional!(args)
85
+ POSITIONAL.each do |opt|
86
+ self.method(opt).call(args)
87
+ self.demand(opt, positional: true)
88
+ end
89
+ end
90
+
91
+ def options?
92
+ ArgumentsParser::FLAGS.any? { |flag| @options.include?(flag) }
93
+ end
94
+
95
+ def usage!
96
+ puts @parser
97
+ exit
98
+ end
99
+
100
+ # rubocop: disable Metrics/MethodLength
101
+ def self.parse(args = ARGV, _file_path = ARGF, arguments_parser = ArgumentsParser.new)
102
+ arguments_parser.parser.parse!(args)
103
+ if !arguments_parser.options? && ARGV.length != 1
104
+ message = 'A crontab definition is required'
105
+ raise OptionParser::MissingArgument, message
106
+ elsif !args.empty?
107
+ arguments_parser.positional!(args)
108
+ end
109
+ arguments_parser.options
110
+ rescue OptionParser::AmbiguousOption => e
111
+ abort e.message
112
+ rescue OptionParser::ParseError => e
113
+ puts e.message
114
+ arguments_parser.usage!
115
+ end
116
+ # rubocop: enable Metrics/MethodLength
117
+ end
118
+ # class ArgumentsParser
119
+ end
120
+ # module Cron
@@ -0,0 +1,183 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # vi: set ft=ruby :
5
+ # -*- mode: ruby -*-
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ require 'rexml/formatters/pretty'
10
+
11
+ require 'fileutils'
12
+
13
+ require_relative '../launch_agent_plist'
14
+
15
+ # Define module LaunchAgentManagerConstants
16
+ module LaunchAgentManagerConstants
17
+ VERSION = '0.1.0'.freeze unless defined?(VERSION)
18
+ LABEL_NAMESPACE = %w[com local].freeze
19
+ INTERVALS = %w[Minute Hour Day Month Weekday].freeze
20
+ SCHEDULE_PARTS_COUNT = INTERVALS.length
21
+ LAUNCHCTL_TEMPLATE = 'launchctl bootstrap gui/%<uid>s %<plist>s'.freeze
22
+ SUCCESS_MESSAGE = 'Created and enabled launchd job: %<label>s'.freeze
23
+ LAUNCH_AGENTS_DIR_PATH = File.expand_path(File.join(Dir.home, 'Library', 'LaunchAgents'))
24
+ BOOTOUT_TEMPLATE = 'launchctl bootout gui/%<uid>s/%<label>s'.freeze
25
+ REMOVE_TEMPLATE = 'launchctl remove gui/%<uid>s/%<label>s'.freeze
26
+ end
27
+
28
+ # Define module LaunchAgentManagerInstanceMethods
29
+ module LaunchAgentManagerInstanceMethods
30
+ include LaunchAgentManagerConstants
31
+
32
+ def find_executable_in_path(cmd)
33
+ directory = ENV['PATH'].split(File::PATH_SEPARATOR).find do |dir|
34
+ File.executable?(File.join(dir, cmd))
35
+ end
36
+ directory.nil? ? nil : File.join(directory, cmd)
37
+ end
38
+
39
+ def executable?(exe)
40
+ !exe.empty? && File.executable?(exe) && !File.directory?(exe)
41
+ end
42
+
43
+ def explicit_which(cmd)
44
+ exe = `which #{cmd}`.chomp
45
+ executable?(exe) ? exe : nil
46
+ rescue Errno::ENOENT => _e
47
+ nil
48
+ end
49
+
50
+ def portable_which(cmd)
51
+ explicit_which(cmd) || find_executable_in_path(cmd)
52
+ end
53
+
54
+ # Extract program path and arguments, verifying executable
55
+ # rubocop: disable Metrics/MethodLength
56
+ def extract_program_path(executable_with_args)
57
+ *args = executable_with_args.split
58
+ exe = args.shift
59
+ exe_path = File.exist?(exe) ? exe : portable_which(exe)
60
+ if exe_path.nil? || !File.exist?(exe_path)
61
+ abort "Cannot find executable in path: #{exe}"
62
+ end
63
+ if exe_path.nil? || !File.executable?(exe_path) || File.directory?(exe_path)
64
+ abort "Given file is not executable: #{exe}"
65
+ end
66
+ args.unshift(exe_path)
67
+ args
68
+ end
69
+ # rubocop: enable Metrics/MethodLength
70
+
71
+ # Create the job name based on the given executable file path
72
+ def derive_job_label_from_executable(exe_path)
73
+ label = LABEL_NAMESPACE.dup
74
+ label << File.basename(exe_path, '.*')
75
+ label.join('.')
76
+ end
77
+
78
+ # Define cron settings for plist
79
+ def derive_calendar_interval(cron_schedule)
80
+ cron_fields = cron_schedule.split
81
+ calendar_interval = {}
82
+ INTERVALS.each do |key|
83
+ value = cron_fields.shift
84
+ raise "Invalid cron string: #{cron_schedule}" if value.nil? || value.empty?
85
+
86
+ calendar_interval[key] = value
87
+ end
88
+ calendar_interval
89
+ end
90
+
91
+ # Save plist file to LaunchAgents directory; do not overwrite an
92
+ # already existing file with the same name at the generated path
93
+ # rubocop: disable Metrics/MethodLength
94
+ def save_plist(label, doc)
95
+ FileUtils.mkdir_p(LAUNCH_AGENTS_DIR_PATH)
96
+ plist_path = File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist")
97
+ puts "plist_path: #{plist_path}"
98
+ return plist_path if File.exist?(plist_path)
99
+
100
+ document = ''
101
+ formatter = REXML::Formatters::Pretty.new(4)
102
+ formatter.write(doc, document)
103
+ puts '[DEBUG] Contents of plist xml document:'
104
+ puts document
105
+ File.write(plist_path, document)
106
+ plist_path
107
+ end
108
+ # rubocop: enable Metrics/MethodLength
109
+
110
+ def execute(command)
111
+ puts "[DEBUG] Executing command: #{command}"
112
+ system(command)
113
+ end
114
+
115
+ # Load the launchd job
116
+ def load_launchd_job(plist_path)
117
+ execute(format(LAUNCHCTL_TEMPLATE, uid: Process.uid, plist: plist_path))
118
+ end
119
+
120
+ # Show the launchd agents as crontabs
121
+ def show_all_cron_jobs
122
+ Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist')).map do |plist_path|
123
+ job = parse_plist(plist_path)
124
+ puts "#{job[:schedule]} #{job[:command]}"
125
+ end
126
+ end
127
+
128
+ # Return a list of all user launchd agent labels
129
+ def launch_agent_labels
130
+ plist_file_paths = Dir.glob(File.join(LAUNCH_AGENTS_DIR_PATH, '*.plist'))
131
+ plist_file_paths.each_with_object([]) do |file_path, labels|
132
+ label = LaunchAgentPlist.parse(file_path)[:label]
133
+ labels << label unless label.nil?
134
+ end
135
+ end
136
+
137
+ # List labels of launchd agents
138
+ def list_launch_agent_labels
139
+ launch_agent_labels.each do |label|
140
+ puts label
141
+ end
142
+ end
143
+
144
+ # Remove plist Launch Agents by label
145
+ def remove_cron_job(label)
146
+ # warn "[DEBUG] Removing launch agent: #{label}"
147
+ plist_path = File.expand_path(File.join(LAUNCH_AGENTS_DIR_PATH, "#{label}.plist"))
148
+ # warn "[DEBUG] Removing launch agent plist definition file: #{plist_path}"
149
+ if File.exist?(plist_path)
150
+ execute(format(BOOTOUT_TEMPLATE, uid: Process.uid, label: label))
151
+ execute(format(REMOVE_TEMPLATE, uid: Process.uid, label: label))
152
+ FileUtils.rm(plist_path)
153
+ puts "Removed launch agent: #{label}"
154
+ else
155
+ warn "Not found; crontab or launch agent: #{label}"
156
+ end
157
+ end
158
+
159
+ # Create the launchd job
160
+ def create_launchd_job(options)
161
+ args = extract_program_path(options[:executable_path_with_args])
162
+ label = derive_job_label_from_executable(args.first)
163
+ calendar_interval = derive_calendar_interval(options[:cron_schedule])
164
+ plist_doc = LaunchAgentPlist.new(label, args, calendar_interval).to_doc
165
+ plist_path = save_plist(label, plist_doc)
166
+ puts "Executing: cat #{plist_path}"
167
+ puts `cat #{plist_path}`.strip
168
+ load_launchd_job(plist_path)
169
+ puts format(SUCCESS_MESSAGE, label: label)
170
+ end
171
+ end
172
+ # module LaunchAgentManagerInstanceMethods
173
+
174
+ # Define the LaunchAgentManager class
175
+ class LaunchAgentManager
176
+ include LaunchAgentManagerInstanceMethods
177
+
178
+ attr_reader :options
179
+
180
+ def initialize(options)
181
+ @options = options
182
+ end
183
+ end
@@ -0,0 +1,53 @@
1
+ #! /usr/bin/env ruby
2
+ # encoding: utf-8
3
+ # frozen_string_literal: false
4
+
5
+ # vi: set ft=ruby :
6
+ # -*- mode: ruby -*-
7
+
8
+ # Copyright Nels Nelson 2024 but freely usable (see license)
9
+
10
+ # Usage: cron.rb <crontab>|<options>
11
+ #
12
+ # Options:
13
+ # --show-all Show cron jobs
14
+ # -l, --list List cron jobs labels
15
+ # -r, --remove=<label> Remove a cron job by label
16
+ #
17
+ # Examples:
18
+ #
19
+ # cron.rb "0 1 * * * archive.rb ${HOME}/Downloads/archive"
20
+ # cron.rb --show-all
21
+ # cron.rb --list
22
+ # cron.rb --remove=com.local.archive
23
+
24
+ require_relative 'launch_agent_manager'
25
+ require_relative 'cron/argument_parser'
26
+
27
+ # Define module Cron
28
+ module Cron
29
+ LaunchAgentManager = ::LaunchAgentManager
30
+
31
+ # rubocop: disable Metrics/MethodLength
32
+ def main(args = Cron::ArgumentsParser.parse)
33
+ log.log_level = args[:log_level]
34
+ manager = Cron::LaunchAgentManager.new(args)
35
+ if args[:show_all]
36
+ manager.show_all_launch_agents
37
+ elsif args[:list]
38
+ manager.list_launch_agent_labels
39
+ elsif args[:remove]
40
+ manager.remove_launch_agent(args[:remove])
41
+ else
42
+ manager.create_launch_agent(
43
+ args[:executable_path_with_args],
44
+ nil,
45
+ args[:cron_schedule]
46
+ )
47
+ end
48
+ end
49
+ # rubocop: enable Metrics/MethodLength
50
+ end
51
+ # module Cron
52
+
53
+ Object.new.extend(Cron).main if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ # frozen_string_literal: false
3
+
4
+ # -*- mode: ruby -*-
5
+ # vi: set ft=ruby :
6
+
7
+ # Copyright Nels Nelson 2024 but freely usable (see license)
8
+
9
+ # Define the LaunchAgentConstants module
10
+ module LaunchAgentConstants
11
+ DEFAULT_ENCODING = 'UTF-8'.freeze
12
+ DOCTYPE_ELEMENT = '<!%<doctypes>s>'.freeze
13
+ DOCTYPES = [
14
+ 'DOCTYPE',
15
+ 'plist',
16
+ 'PUBLIC',
17
+ '"-//Apple//DTD PLIST 1.0//EN"',
18
+ '"http://www.apple.com/DTDs/PropertyList-1.0.dtd"'
19
+ ].freeze
20
+ LABEL_NAMESPACE = %w[com local].freeze
21
+ INTERVALS = %w[Minute Hour Day Month Weekday].freeze
22
+ SCHEDULE_PARTS_COUNT = INTERVALS.length
23
+ CDATA_PATTERN = %r{[<>&'"\n]+}
24
+ SCHEDULE_XPATH_TEMPLATE = './/key[text()="%<key>s"]/following-sibling::integer'.freeze
25
+ KEYS_REQUIRING_CDATA = ['PATH'].freeze
26
+ LAUNCHCTL_TEMPLATE = 'launchctl bootstrap gui/%<uid>s %<plist>s'.freeze
27
+ SUCCESS_MESSAGE = 'Created and enabled launchd job: %<label>s'.freeze
28
+ LAUNCH_AGENTS_DIR_PATH = File.expand_path(File.join(Dir.home, 'Library', 'LaunchAgents'))
29
+ LABEL_XPATH = '//key[text()="Label"]/following-sibling::*[1]'.freeze
30
+ START_CALENDAR_INTERVAL_XPATH =
31
+ '//key[text()="StartCalendarInterval"]/following-sibling::*[1]'.freeze
32
+ WATCH_PATHS_XPATH = '//key[text()="WatchPaths"]/following-sibling::*[1]'.freeze
33
+ LOCAL_STRING_XPATH = './string'.freeze
34
+ PROGRAM_ARGUMENTS_XPATH =
35
+ '//key[text()="ProgramArguments"]/following-sibling::array[1]'.freeze
36
+ INTERVAL_XPATH_TEMPLATE = './key[text()="%<key>s"]/following-sibling::*[1]'.freeze
37
+ BOOTOUT_TEMPLATE = 'launchctl bootout gui/%<uid>s/%<label>s'.freeze
38
+ REMOVE_TEMPLATE = 'launchctl remove gui/%<uid>s/%<label>s'.freeze
39
+ ON_LOGIN = '@login'.freeze
40
+ end
41
+ # module LaunchAgentConstants