├── .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 | --------------------------------------------------------------------------------