require 'osx/cocoa' module SAFoundation module OS def self.host_name `hostname`.chomp end # Returns the OS version. def self.os_version os_version_and_build.first end # Returns the OS build number. def self.os_build os_version_and_build.last end private def self.os_version_and_build @@__os_version ||= `/usr/bin/sw_vers`.scan(/ProductVersion:\t([\d\w\.]+)\nBuildVersion:\t([\d\w\.]+)/).first end end end class SACrashReporter < OSX::NSWindowController class Report attr_accessor :exception # Calls the specified method and returns an array. # The first element contains the key as it should show up in the report, # so for instance: 'Host Name:' # And the second element contains the actual result. # # If a method is called through +get+ and it returns anything other than an array # the key will be created from the method name. Eg: :host_name => 'Host Name:' # If you more control over the key, simply return an array with the correct key and result. # # Please note that most of the predefined methods expect to be called through this method. def get(name) result = self.send(name) if result.is_a? Array result else [name.to_s.split('_').map { |e| e.capitalize }.join(' ') << ':', result] end end # Returns the machine's hostname: ["Host Name:", "macbook.local"] def host_name `hostname`.chomp end # Returns the date/time: ["Date/Time:", "Fri Oct 12 15:16:28 +0200 2007"] def date_time ['Date/Time:', Time.now.to_s] end # Returns the os version: ["OS Version:", "10.4.10 (8R2232)"] def os_version ['OS Version:', "#{SAFoundation::OS.os_version} (#{SAFoundation::OS.os_build})"] end # Returns the SACrashReporter version: ["Report Version:", "SACrashReporter version 1"] def report_version "SACrashReporter version #{SACrashReporter::VERSION}" end # Returns the Ruby intepreter version def ruby_version RUBY_VERSION end # Returns the RubyCocoa version def rubycocoa_version ["RubyCocoa Version:", OSX::RUBYCOCOA_VERSION] end # Returns the application executable: ["Command:", "MyApp"] def command OSX::NSBundle.mainBundle.infoDictionary['CFBundleExecutable'].to_s end # Returns the full path to the executable: ["Path:", "/Applications/Foo.app/Contents/MacOS/Foo"] def path OSX::NSBundle.mainBundle.executablePath.fileSystemRepresentation.to_s end # Returns the application's short version and the version: ["Version:", "1.0 final (1.0)"] def version app_info_plist = OSX::NSBundle.mainBundle.infoDictionary "#{app_info_plist['CFBundleShortVersionString']} (#{app_info_plist['CFBundleVersion']})" end # Returns the process id (pid) of the application: ["PID:", "10999"] def pid ['PID:', OSX::NSProcessInfo.processInfo.processIdentifier.to_s] end # Sets the order that the message will be rendered in. # Use it to specify which logs will be used and group them together for nicer layouts. # # report.order = [[:host_name], [:os_version, :pid]] # report.message # # Results in: # # ********** # # Host Name: supermachine.local # # OS Version: 10.4.10 (8R2232) # PID: 10999 # # Exception: Some random error. # # BACKTRACE: # ... def order=(*order) @order = (order.length == 1 ? order.first : order) unless order.empty? end # Returns the current order. # # report.order #=> [[:host_name], [:os_version, :pid]] def order @order end # Returns a string which contains the rendered message. # Control the output by setting the +order+. def message logs = ordered_logs longest_key = logs.flatten.inject(0) { |count, key| count < key.to_s.length ? key.to_s.length : count }.next "\n\n**********\n\n" << logs.map { |keys| render_section(keys, longest_key) }.join << error_and_bt(longest_key) end private # The default layout of an Apple crash log DEFAULT_APPLE_STYLE_CRASH_LOG = [[:host_name, :date_time, :os_version, :ruby_version, :rubycocoa_version, :report_version], [:command, :path], [:version], [:pid]] def ordered_logs @order || DEFAULT_APPLE_STYLE_CRASH_LOG end WHITESPACE = ' ' #:nodoc: def whitespace(count) WHITESPACE[0...(count - 1)] end def error_and_bt(longest_key) whitespace(longest_key - 9) << "Exception: #{@exception}\n\nBACKTRACE:\n" << @exception.backtrace.join("\n") end def render_section(keys, longest_key) keys.map { |key| whitespace(longest_key - key.to_s.length) << self.get(key).join(' ') }.join("\n") << "\n\n" end end end