├── .rspec
├── Rakefile
├── lib
├── launchd_tools
│ ├── version.rb
│ ├── environment_variables.rb
│ ├── path_content.rb
│ ├── program_args_parser.rb
│ ├── environment_parser.rb
│ ├── cmd2launchd_cli.rb
│ ├── path_parser.rb
│ ├── path.rb
│ ├── launchd_job.rb
│ ├── launchd_plist.rb
│ └── launchd2cmd_cli.rb
└── launchd_tools.rb
├── TODO
├── Gemfile
├── bin
├── cmd2launchd
└── launchd2cmd
├── spec
├── fixtures
│ ├── LaunchDaemons
│ │ ├── com.apple.opendirectoryd.plist
│ │ ├── org.apache.httpd.plist
│ │ └── inaccessible.plist
│ └── LaunchAgents
│ │ ├── com.apple.dt.CommandLineTools.installondemand.plist
│ │ └── com.apple.storeagent.plist
├── spec_helper.rb
└── integration
│ ├── cmd2launchd_spec.rb
│ └── launchd2cmd_spec.rb
├── .gitignore
├── launchd_tools.gemspec
├── LICENSE.txt
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format progress
3 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
--------------------------------------------------------------------------------
/lib/launchd_tools/version.rb:
--------------------------------------------------------------------------------
1 | module LaunchdTools
2 | VERSION = "0.5.0"
3 | end
4 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | Release package made with fpm
2 | Output errors to stderr
3 | Test for exit statuses
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in launchd_tools.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/lib/launchd_tools.rb:
--------------------------------------------------------------------------------
1 | require "launchd_tools/version"
2 |
3 | module LaunchdTools
4 | # Your code goes here...
5 | end
6 |
--------------------------------------------------------------------------------
/bin/cmd2launchd:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'launchd_tools/cmd2launchd_cli'
4 |
5 | LaunchdTools::Cmd2LaunchdCli.new(ARGV).run
6 |
--------------------------------------------------------------------------------
/bin/launchd2cmd:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require 'launchd_tools/launchd2cmd_cli'
4 |
5 | LaunchdTools::Launchd2CmdCli.new(ARGV).run
6 |
--------------------------------------------------------------------------------
/spec/fixtures/LaunchDaemons/com.apple.opendirectoryd.plist:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kcrawford/launchd_tools/HEAD/spec/fixtures/LaunchDaemons/com.apple.opendirectoryd.plist
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.rbc
3 | .bundle
4 | .config
5 | .yardoc
6 | Gemfile.lock
7 | InstalledFiles
8 | _yardoc
9 | coverage
10 | doc/
11 | lib/bundler/man
12 | pkg
13 | rdoc
14 | spec/reports
15 | test/tmp
16 | test/version_tmp
17 | tmp
18 |
--------------------------------------------------------------------------------
/lib/launchd_tools/environment_variables.rb:
--------------------------------------------------------------------------------
1 | module LaunchdTools
2 | class EnvironmentVariables
3 | attr_reader :variables
4 | def initialize(variables = {})
5 | @variables = variables
6 | end
7 |
8 | def to_a
9 | env_items = []
10 | variables.each do |key,value|
11 | env_items << "#{key}=#{value}"
12 | end
13 | env_items
14 | end
15 | end
16 | end
17 |
18 |
--------------------------------------------------------------------------------
/lib/launchd_tools/path_content.rb:
--------------------------------------------------------------------------------
1 | module LaunchdTools
2 | class PathContent
3 | attr_reader :path
4 | def initialize(path)
5 | @path = path
6 | end
7 |
8 | def to_s
9 | if binary?
10 | `plutil -convert xml1 -o /dev/stdout '#{path}'`
11 | else
12 | File.read(path)
13 | end
14 | end
15 |
16 | def binary?
17 | f = File.open(path, "r")
18 | f.read(6) == "bplist"
19 | ensure
20 | f.close if f
21 | end
22 | end
23 |
24 | end
25 |
--------------------------------------------------------------------------------
/lib/launchd_tools/program_args_parser.rb:
--------------------------------------------------------------------------------
1 | require 'rexml/document'
2 | include REXML
3 | module LaunchdTools
4 | class ProgramArgsParser
5 | attr_reader :xml_doc, :element
6 | def initialize(xml_doc)
7 | @xml_doc = xml_doc
8 | end
9 |
10 | def parse
11 | element = REXML::XPath.first(xml_doc, "plist/dict/key[text()='ProgramArguments']/following-sibling::array")
12 | if element
13 | args_strings = XPath.match(element, 'string')
14 | args_strings.map {|e| e.text }
15 | else
16 | program_string_element = REXML::XPath.first(xml_doc, "plist/dict/key[text()='Program']/following-sibling::string")
17 | [program_string_element.text]
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/spec/fixtures/LaunchDaemons/org.apache.httpd.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Disabled
6 |
7 | EnvironmentVariables
8 |
9 | XPC_SERVICES_UNAVAILABLE
10 | 1
11 |
12 | Label
13 | org.apache.httpd
14 | OnDemand
15 |
16 | ProgramArguments
17 |
18 | /usr/sbin/httpd
19 | -D
20 | FOREGROUND
21 |
22 | SHAuthorizationRight
23 | system.preferences
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/launchd_tools/environment_parser.rb:
--------------------------------------------------------------------------------
1 | require 'rexml/document'
2 | include REXML
3 | module LaunchdTools
4 | class EnvironmentParser
5 | attr_reader :xml_doc, :element
6 | def initialize(xml_doc)
7 | @xml_doc = xml_doc
8 | end
9 |
10 | def element
11 | @element ||= REXML::XPath.first(xml_doc, "plist/dict/key[text()='EnvironmentVariables']/following-sibling::dict")
12 | end
13 |
14 | def extract_env
15 | env = {}
16 | REXML::XPath.match(element, 'key').each do |environment_key|
17 | env[environment_key.text] = environment_key.next_sibling.next_sibling.text
18 | end
19 | env
20 | end
21 |
22 | def parse
23 | if element
24 | extract_env
25 | else
26 | {}
27 | end
28 | end
29 |
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # This file was generated by the `rspec --init` command. Conventionally, all
2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3 | # Require this file using `require "spec_helper"` to ensure that it is only
4 | # loaded once.
5 | #
6 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7 | RSpec.configure do |config|
8 | config.treat_symbols_as_metadata_keys_with_true_values = true
9 | config.run_all_when_everything_filtered = true
10 | config.filter_run :focus
11 |
12 | # Run specs in random order to surface order dependencies. If you find an
13 | # order dependency and want to debug it, you can fix the order by providing
14 | # the seed, which is printed after each run.
15 | # --seed 1234
16 | config.order = 'random'
17 | end
18 |
--------------------------------------------------------------------------------
/spec/fixtures/LaunchAgents/com.apple.dt.CommandLineTools.installondemand.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | com.apple.dt.CommandLineTools.installondemand
7 | MachServices
8 |
9 | com.apple.dt.CommandLineTools.installondemand
10 |
11 |
12 | OnDemand
13 |
14 | EnableTransactions
15 |
16 | Program
17 | /System/Library/CoreServices/Install Command Line Developer Tools.app/Contents/MacOS/Install Command Line Developer Tools
18 | LimitLoadToSessionType
19 | Aqua
20 |
21 |
22 |
--------------------------------------------------------------------------------
/lib/launchd_tools/cmd2launchd_cli.rb:
--------------------------------------------------------------------------------
1 | require 'launchd_tools/launchd_plist'
2 |
3 | module LaunchdTools
4 | class Cmd2LaunchdCli
5 |
6 | attr_reader :args
7 | def initialize(args)
8 | if args.empty?
9 | show_usage
10 | exit 0
11 | elsif args.length == 1
12 | case args.first
13 | when "--help"
14 | show_usage
15 | exit 0
16 | when "-h"
17 | show_usage
18 | exit 0
19 | when "--version"
20 | puts LaunchdTools::VERSION
21 | exit 0
22 | end
23 | end
24 | @args = args
25 | end
26 |
27 | def show_usage
28 | puts "Usage: #{$0} command [arg1] [arg2]"
29 | end
30 |
31 | def run
32 | plist = LaunchdPlist.new
33 | plist.add_program_args(args)
34 | puts plist.to_s
35 | end
36 | end
37 | end
38 |
39 |
--------------------------------------------------------------------------------
/spec/integration/cmd2launchd_spec.rb:
--------------------------------------------------------------------------------
1 | require 'tempfile'
2 | describe 'running cmd2launchd' do
3 | it "converts right back to original commmand using launchd2cmd" do
4 | t = Tempfile.new(File.basename(__FILE__))
5 | temp_file_path = t.path
6 | t.close
7 | output = `bundle exec bin/cmd2launchd ls -la /tmp > #{temp_file_path} && bundle exec bin/launchd2cmd #{temp_file_path}`
8 | expect(output.chomp).to include("ls -la /tmp")
9 | File.unlink(temp_file_path)
10 | end
11 |
12 | context "no arguments" do
13 | it "outputs usage info" do
14 | output = `bundle exec bin/cmd2launchd`
15 | expect(output.chomp).to include("Usage: ")
16 | end
17 | end
18 |
19 | context "help option" do
20 | it "outputs usage info" do
21 | output = `bundle exec bin/cmd2launchd -h`
22 | expect(output.chomp).to include("Usage: ")
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/launchd_tools/path_parser.rb:
--------------------------------------------------------------------------------
1 | require 'rexml/document'
2 | require 'launchd_tools/launchd_job'
3 | require 'launchd_tools/environment_parser'
4 | require 'launchd_tools/environment_variables'
5 | require 'launchd_tools/program_args_parser'
6 |
7 | include REXML
8 | module LaunchdTools
9 | class PathParser
10 | attr_reader :path, :xml_doc
11 | def initialize(path)
12 | @path = path
13 | end
14 |
15 | # returns a parsed launchd job
16 | def parse
17 | LaunchdJob.new({ 'EnvironmentVariables' => parse_env, 'ProgramArguments' => parse_program_args })
18 | end
19 |
20 | def parse_env
21 | variables = EnvironmentParser.new(xml_doc).parse
22 | EnvironmentVariables.new(variables).to_a
23 | end
24 |
25 | def parse_program_args
26 | ProgramArgsParser.new(xml_doc).parse
27 | end
28 |
29 | def xml_doc
30 | @xml_doc ||= Document.new(path.content)
31 | end
32 |
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/launchd_tools/path.rb:
--------------------------------------------------------------------------------
1 | require 'launchd_tools/path_parser'
2 | require 'launchd_tools/path_content'
3 |
4 | module LaunchdTools
5 | class Path
6 |
7 | class UnparsablePlist < Exception
8 | end
9 |
10 | class PermissionsError < Exception
11 | end
12 |
13 | attr_reader :path
14 |
15 | def initialize(path)
16 | @path = path
17 | end
18 |
19 | def validate
20 | raise PathMissingError unless File.exist?(path)
21 | self
22 | end
23 |
24 | def content
25 | PathContent.new(path).to_s
26 | end
27 |
28 | def parse
29 | begin
30 | path_parser.parse
31 | rescue Errno::EACCES
32 | raise PermissionsError.new
33 | rescue
34 | raise UnparsablePlist.new
35 | end
36 | end
37 |
38 | def path_parser
39 | PathParser.new(self)
40 | end
41 |
42 | def expanded
43 | File.expand_path(path)
44 | end
45 |
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/launchd_tools.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'launchd_tools/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "launchd_tools"
8 | spec.version = LaunchdTools::VERSION
9 | spec.authors = ["Kyle Crawford"]
10 | spec.email = ["kcrwfrd@gmail.com"]
11 | spec.description = %q{Provides tools for converting from command line arguments to a formatted launchd plist and vice versa}
12 | spec.summary = %q{launchd tools convert from command to launchd and launchd to command}
13 | spec.homepage = ""
14 | spec.license = "MIT"
15 |
16 | spec.files = `git ls-files`.split($/)
17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19 | spec.require_paths = ["lib"]
20 |
21 | spec.add_development_dependency "bundler", "~> 1.3"
22 | spec.add_development_dependency "rspec"
23 | spec.add_development_dependency "rake"
24 | end
25 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013 Kyle Crawford
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/spec/fixtures/LaunchDaemons/inaccessible.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Disabled
6 |
7 | Label
8 | com.openssh.sshd
9 | Program
10 | /usr/libexec/sshd-keygen-wrapper
11 | ProgramArguments
12 |
13 | /usr/sbin/sshd
14 | -i
15 |
16 | Sockets
17 |
18 | Listeners
19 |
20 | SockServiceName
21 | ssh
22 | Bonjour
23 |
24 | ssh
25 | sftp-ssh
26 |
27 |
28 |
29 | inetdCompatibility
30 |
31 | Wait
32 |
33 |
34 | StandardErrorPath
35 | /dev/null
36 | SHAuthorizationRight
37 | system.preferences
38 | POSIXSpawnType
39 | Interactive
40 |
41 |
42 |
--------------------------------------------------------------------------------
/lib/launchd_tools/launchd_job.rb:
--------------------------------------------------------------------------------
1 | module LaunchdTools
2 | class LaunchdJob
3 | attr_reader :attributes
4 | def initialize(attributes = {})
5 | @attributes = attributes
6 | end
7 |
8 | def environment_variables
9 | attributes.fetch('EnvironmentVariables', {})
10 | end
11 |
12 | def program_arguments
13 | attributes.fetch('ProgramArguments', [])
14 | end
15 |
16 | def to_s
17 | escaped_args = program_arguments.map {|arg| shellescape(arg) }
18 | puts (environment_variables + escaped_args).join(" ")
19 | end
20 |
21 | # from ruby's shellescape
22 | # included here for ruby 1.8 compatibility
23 | def shellescape(str)
24 | str = str.to_s
25 |
26 | # An empty argument will be skipped, so return empty quotes.
27 | return "''" if str.empty?
28 |
29 | str = str.dup
30 |
31 | # Treat multibyte characters as is. It is caller's responsibility
32 | # to encode the string in the right encoding for the shell
33 | # environment.
34 | str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/, "\\\\\\1")
35 |
36 | # A LF cannot be escaped with a backslash because a backslash + LF
37 | # combo is regarded as line continuation and simply ignored.
38 | str.gsub!(/\n/, "'\n'")
39 |
40 | return str
41 | end
42 |
43 | end
44 | end
45 |
46 |
--------------------------------------------------------------------------------
/lib/launchd_tools/launchd_plist.rb:
--------------------------------------------------------------------------------
1 | require 'rexml/document'
2 | include REXML
3 | module LaunchdTools
4 | class LaunchdPlist
5 | attr_reader :doc, :program_args_array_element
6 |
7 | def initialize
8 | base = %q[
9 | ]
10 | @doc = Document.new(base)
11 | plist_element = Element.new('plist')
12 | doc.add_element(plist_element, {"version" => "1.0"})
13 | base_dictionary = Element.new('dict')
14 | plist_element.add_element(base_dictionary)
15 | program_args_key_element = Element.new('key')
16 | program_args_key_element.text = 'ProgramArguments'
17 | base_dictionary.add_element(program_args_key_element)
18 | @program_args_array_element = Element.new "array"
19 | base_dictionary.add_element(program_args_array_element)
20 | end
21 |
22 |
23 | def add_program_arg(arg)
24 | string_element = Element.new "string"
25 | string_element.text = arg
26 | program_args_array_element << string_element
27 | end
28 |
29 | def add_program_args(args)
30 | args.each do |arg|
31 | add_program_arg(arg)
32 | end
33 | end
34 |
35 | def to_s
36 | formatter = REXML::Formatters::Pretty.new # (2)
37 | formatter.compact = true
38 | xml_string = String.new
39 | formatter.write(doc, xml_string)
40 | xml_string
41 | end
42 | end
43 | end
44 |
45 |
--------------------------------------------------------------------------------
/spec/fixtures/LaunchAgents/com.apple.storeagent.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Label
6 | com.apple.storeagent
7 | MachServices
8 |
9 | com.apple.storeagent
10 |
11 | com.apple.storeagent-xpc
12 |
13 | com.apple.storeagent.storekit
14 |
15 |
16 | OnDemand
17 |
18 | Program
19 | /System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeagent
20 | EnableTransactions
21 |
22 | LimitLoadToSessionType
23 |
24 | LoginWindow
25 | Aqua
26 |
27 | LaunchEvents
28 |
29 | com.apple.usernotificationcenter.matching
30 |
31 | store
32 |
33 | bundleid
34 | com.apple.appstore
35 | delay registration
36 |
37 | events
38 |
39 | didDismissAlert
40 | didActivateNotification
41 | didDeliverNotification
42 | didSnoozeAlert
43 | didRemoveDeliveredNotifications
44 | didExpireNotifications
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/lib/launchd_tools/launchd2cmd_cli.rb:
--------------------------------------------------------------------------------
1 | require 'launchd_tools/path'
2 | require 'optparse'
3 | module LaunchdTools
4 | class Launchd2CmdCli
5 | attr_reader :paths, :args
6 | def initialize(args)
7 | showing_help = false
8 | opt_parser = OptionParser.new do |opt|
9 | opt.banner = "Usage: #{$0} path/to/launchd.plist"
10 | opt.separator ""
11 | opt.separator "Options"
12 | opt.on("--help", "-h", "Displays this help message") do
13 | showing_help = true
14 | end
15 | opt.on("--version", "outputs version information for this tool") do
16 | puts LaunchdTools::VERSION
17 | end
18 | opt.separator ""
19 | end
20 |
21 | opt_parser.parse!(args)
22 |
23 | if args.empty? || showing_help
24 | puts opt_parser
25 | else
26 | @args = args
27 | @paths = extract_paths(args)
28 | end
29 | end
30 |
31 | def extract_paths(path_args)
32 | path_args = path_args.map do |path_arg|
33 | if File.directory?(path_arg)
34 | path_arg += "/" if path_arg[-1] != "/"
35 | path_arg += "*"
36 | end
37 | path_arg
38 | end
39 | Dir.glob(path_args)
40 | end
41 |
42 | def run
43 | errors = 0
44 | if paths
45 | if paths.length > 0
46 | errors = process_each_path
47 | else
48 | args.each {|arg| puts "No launchd job found at '#{arg}'" }
49 | exit 1
50 | end
51 | end
52 | exit 2 if errors > 0
53 | end
54 |
55 | def process_path(path_string)
56 | error_count = 0
57 | begin
58 | path = Path.new(path_string).validate
59 | puts "# #{path.expanded}"
60 | puts path.parse.to_s
61 | rescue LaunchdTools::Path::UnparsablePlist
62 | puts "Error: unable to parse launchd job\n"
63 | error_count = 1
64 | rescue LaunchdTools::Path::PermissionsError
65 | require 'etc'
66 | username = Etc.getpwuid(Process.euid).name
67 | puts "Error: user #{username} does not have access to read launchd job\n"
68 | error_count = 1
69 | end
70 | return error_count
71 | end
72 |
73 | def process_each_path
74 | error_count = 0
75 | puts
76 | paths.each do |path_string|
77 | error_count += process_path(path_string)
78 | end
79 | return error_count
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Launchd Tools
2 |
3 | Convert from command to launchd plist or from launchd plist to command
4 |
5 | ## Installation
6 |
7 | $ gem install launchd_tools
8 |
9 | ## Command Usage
10 |
11 | Output the command and arguments from a launchd plist:
12 |
13 | $ launchd2cmd /path/to/launchd.plist [another/launchd.plist]
14 |
15 | Create a launchd plist from a command and arguments:
16 |
17 | $ cmd2launchd /path/to/daemon [arg] [-option]
18 |
19 | ## Examples
20 |
21 | ####launchd2cmd with multiple directories:
22 |
23 | $ launchd2cmd /System/Library/LaunchAgents /System/Library/LaunchDaemons
24 | ...
25 | # /System/Library/LaunchDaemons/tftp.plist
26 | /usr/libexec/tftpd -i /private/tftpboot
27 |
28 | ####launchd2cmd wildcards/globbing:
29 |
30 | $ launchd2cmd /Library/Launch*/com.googlecode.munki.*
31 | ...
32 | # /Library/LaunchDaemons/com.googlecode.munki.managedsoftwareupdate-manualcheck.plist
33 | /usr/local/munki/supervisor --timeout 43200 -- /usr/local/munki/managedsoftwareupdate --manualcheck
34 |
35 | ####cmd2launchd creating a launchd plist skeleton:
36 |
37 | $ cmd2launchd /usr/local/bin/daemond -d --mode foreground
38 |
39 |
40 |
41 |
42 | ProgramArguments
43 |
44 | /usr/local/bin/daemond
45 | -d
46 | --mode
47 | foreground
48 |
49 |
50 |
51 |
52 | ## Packaging
53 |
54 | Use the excellent fpm https://github.com/jordansissel/fpm
55 |
56 | gem install fpm
57 | fpm -s gem -t osxpkg --osxpkg-identifier-prefix org.rubygems.kcrawford --no-gem-env-shebang launchd_tools
58 |
59 | The package will be output in the current directory as rubygem-launchd_tools-\.pkg
60 |
61 | ## Usage from Ruby
62 |
63 | ### Parsing a launchd plist
64 |
65 | ```ruby
66 | require 'launchd_tools/path'
67 | sshd_plist_path = LaunchdTools::Path.new("/System/Library/LaunchDaemons/ssh.plist")
68 | sshd_job = sshd_plist_path.parse
69 | sshd_job.program_arguments
70 | # => ["/usr/sbin/sshd", "-i"]
71 | ```
72 |
73 | ### Creating a launchd plist
74 |
75 | ```ruby
76 | require 'launchd_tools/launchd_plist'
77 | plist = LaunchdTools::LaunchdPlist.new
78 | plist.add_program_args(["/usr/local/bin/daemon", "-f", "-o",
79 | "/var/log/daemon.log"])
80 | plist.to_s
81 | # =>
82 | #
83 | #
85 | #
86 | #
87 | # ProgramArguments
88 | #
89 | # /usr/local/bin/daemon
90 | # -f
91 | # -o
92 | # /var/log/daemon.log
93 | #
94 | #
95 | #
96 | ```
97 |
98 |
99 | ## Contributing
100 |
101 | 1. Fork it
102 | 2. Create your feature branch (`git checkout -b my-new-feature`)
103 | 3. Commit your changes (`git commit -am 'Add some feature'`)
104 | 4. Push to the branch (`git push origin my-new-feature`)
105 | 5. Create new Pull Request
106 |
107 |
108 |
--------------------------------------------------------------------------------
/spec/integration/launchd2cmd_spec.rb:
--------------------------------------------------------------------------------
1 | describe 'running launchd2cmd' do
2 |
3 | context 'launch agent' do
4 | it 'outputs the command' do
5 | output = `bundle exec bin/launchd2cmd spec/fixtures/LaunchAgents/com.apple.storeagent.plist`
6 | expect(output.chomp).to include("/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeagent")
7 | end
8 | it "outputs a comment line with path to the plist" do
9 | output = `bundle exec bin/launchd2cmd spec/fixtures/LaunchAgents/com.apple.storeagent.plist`
10 | expect(output.chomp).to include("# #{File.expand_path("spec/fixtures/LaunchAgents/com.apple.storeagent.plist")}")
11 | end
12 | end
13 |
14 | context "no arguments" do
15 | it "outputs usage info" do
16 | output = `bundle exec bin/launchd2cmd`
17 | expect(output.chomp).to include("Usage: ")
18 | end
19 | end
20 |
21 | context "help option" do
22 | it "outputs usage info" do
23 | output = `bundle exec bin/launchd2cmd -h`
24 | expect(output.chomp).to include("Usage: ")
25 | end
26 | end
27 |
28 | context "inaccessible file" do
29 | it "outputs relevant error message" do
30 | inaccessible_path = 'spec/fixtures/LaunchDaemons/inaccessible.plist'
31 | File.chmod(0000, inaccessible_path)
32 | output = `bundle exec bin/launchd2cmd #{inaccessible_path}`
33 | File.chmod(0644, inaccessible_path)
34 | require 'etc'
35 | username = Etc.getpwuid(Process.euid).name
36 | expect(output).to include("Error: user #{username} does not have access to read launchd job")
37 | end
38 | end
39 |
40 | context "unparsable file" do
41 | it "outputs relevant error message" do
42 | output = `bundle exec bin/launchd2cmd #{__FILE__}`
43 | expect(output).to include("Error: unable to parse launchd job")
44 | end
45 | end
46 |
47 | context 'bad file paths' do
48 | it "outputs relevant error message" do
49 | output = `bundle exec bin/launchd2cmd not/a/real/path and/another/bad/path`
50 | expect(output).to include("No launchd job found at 'not/a/real/path'")
51 | expect(output).to include("No launchd job found at 'and/another/bad/path'")
52 | end
53 | end
54 | context 'launch daemon' do
55 | context 'with environment variables' do
56 | it 'includes argument and environment variables in output' do
57 | output = `bundle exec bin/launchd2cmd spec/fixtures/LaunchDaemons/org.apache.httpd.plist`
58 | expect(output.chomp).to include("XPC_SERVICES_UNAVAILABLE=1 /usr/sbin/httpd -D FOREGROUND")
59 | end
60 | end
61 | end
62 | context "binary plist" do
63 | it "handles it without error" do
64 | output = `bundle exec bin/launchd2cmd spec/fixtures/LaunchDaemons/com.apple.opendirectoryd.plist`
65 | expect(output.chomp).to include("__CFPREFERENCES_AVOID_DAEMON=1 __CF_USER_TEXT_ENCODING=0x0:0:0 /usr/libexec/opendirectoryd")
66 | end
67 | end
68 |
69 | context "args are directories" do
70 | it "lists contents of directory" do
71 | command = "bundle exec bin/launchd2cmd spec/fixtures/LaunchAgents > /dev/null"
72 | expect(system(command)).to be_true
73 | end
74 | end
75 |
76 | context "program arguments contain spaces" do
77 | it "escapes them for use in the shell" do
78 | output = `bundle exec bin/launchd2cmd spec/fixtures/LaunchAgents/com.apple.dt.CommandLineTools.installondemand.plist`
79 | expect(output.chomp).to include("/System/Library/CoreServices/Install\\ Command\\ Line\\ Developer\\ Tools.app/Contents/MacOS/Install\\ Command\\ Line\\ Developer\\ Tools")
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------