diff --git a/app/models/occurrence.rb b/app/models/occurrence.rb index c1f8053c..3d62d558 100644 --- a/app/models/occurrence.rb +++ b/app/models/occurrence.rb @@ -12,6 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +begin + if(SquashIosCrashLogSymbolication.osx?) + @@SquashIosCrashLogSymbolicationAvailable = true; + else + @@SquashIosCrashLogSymbolicationAvailable = false; + end +rescue LoadError + @@SquashIosCrashLogSymbolicationAvailable = false; +end + # An individual occurrence of a {Bug}, or put another way, a single instance of # an exception occurring and being recorded. Occurrences record all relevant # information about the exception itself and the state of the program when the @@ -383,6 +393,7 @@ class Occurrence < ActiveRecord::Base # Universal message: {presence: true, length: {maximum: 1000}}, backtraces: {type: Array, presence: true}, + crash_log: {type: String, allow_nil: true}, ivars: {type: Hash, allow_nil: true}, user_data: {type: Hash, allow_nil: true}, parent_exceptions: {type: Array, allow_nil: true}, @@ -566,6 +577,12 @@ def additional? user_data.present? || extra_data.present? || ivars.present? end + # @return [true, false] Whether or not this Occurrence has a crash log attached + + def crash_log? + crash_log.present? + end + # @return [true, false] Whether or not this exception occurred as part of an # XMLHttpRequest (Ajax) request. @@ -691,6 +708,29 @@ def symbolicate(symb=nil) end end self.backtraces = bt # refresh the actual JSON + + if (user_data.present? && @@SquashIosCrashLogSymbolicationAvailable ) + begin + Rails.logger.debug "-- SquashIosCrashLogSymbolication.symbolicate_crash called... --" + + self.crash_log = SquashIosCrashLogSymbolication.symbolicate_crash(user_data, SquashIosCrashLogSymbolication.env) + + # if we have a symbolicated crash_log, remove the encoded plcrashlog + if(self.crash_log && self.crash_log.present?) + self.user_data = nil + Rails.logger.debug "-- SquashIosCrashLogSymbolication.symbolicate_crash complete. crash_log contains crash_log, user_data set to nil --" + end + rescue Object => err + # don't get into an infinite loop of notifying Squash + Rails.logger.error "-- ERROR IN symbolicate #{err.object_id} --" + Rails.logger.error "SquashIosCrashLogSymbolication.symbolicate(user_data, #{SquashIosCrashLogSymbolication.env})\n" + Rails.logger.error err + Rails.logger.error err.backtrace.join("\n") + Rails.logger.error @attrs.inspect + Rails.logger.error "-- END ERROR #{err.object_id} --" + raise if Rails.env.test? + end + end end # Like {#symbolicate}, but saves the record. diff --git a/app/views/additions/crashlog_rendering.rb b/app/views/additions/crashlog_rendering.rb new file mode 100644 index 00000000..6bb1acb7 --- /dev/null +++ b/app/views/additions/crashlog_rendering.rb @@ -0,0 +1,38 @@ +# encoding: utf-8 + +# Copyright 2013 Cerner Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Adds methods to a view class that allow it to render Crash Logs in a standard +# style. + +module CrashlogRendering + protected + + # Renders a Crash Log. + # + # @param [String] crash_log The Crash Log to render, in the format + # used by {Occurrence}. + + def render_crash_log(crash_log) + + h4 "Crash Log" + div(id:'crash_log') do + text raw ("
#{crash_log}")
+ end
+ end
+
+
+end
+
diff --git a/app/views/occurrences/show.html.rb b/app/views/occurrences/show.html.rb
index 6f6b4f44..618fca80 100644
--- a/app/views/occurrences/show.html.rb
+++ b/app/views/occurrences/show.html.rb
@@ -23,6 +23,7 @@ module Occurrences
# @private
class Show < Views::Layouts::Application
include BacktraceRendering
+ include CrashlogRendering
needs :project, :environment, :bug, :occurrence
@@ -116,7 +117,13 @@ def tab_content
end
def backtrace_tab
- render_backtraces @occurrence.backtraces, 'root'
+ if @occurrence.crash_log?
+ # do not convert \n to using simple_format() + # instead use
tags in render_crash_log + render_crash_log @occurrence.crash_log + else + render_backtraces @occurrence.backtraces, 'root' + end end def parents_tab diff --git a/config/symbolication_paths.yml b/config/symbolication_paths.yml new file mode 100644 index 00000000..9c148e6b --- /dev/null +++ b/config/symbolication_paths.yml @@ -0,0 +1,35 @@ +--- +# for each environment, provide paths used by the symbolication process +development: + + # PATH to PLCrashReporter's plcrashutil + plcrashutil: /usr/local/bin/plcrashutil + + # PATH TO Xcode's symbolicatecrash script + symbolicationcrash: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash + + # PATH TO Application symbols - Apple's spotlight will try to locate the Application symbols but give it a hint + symbolpath: /Library/ + +test: + + # PATH to PLCrashReporter's plcrashutil + plcrashutil: /usr/local/bin/plcrashutil + + # PATH TO Xcode's symbolicatecrash script + symbolicationcrash: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash + + # PATH TO Application symbols - Apple's spotlight will try to locate the Application symbols but give it a hint + symbolpath: /Library/ + +production: + + # PATH to PLCrashReporter's plcrashutil + plcrashutil: /usr/local/bin/plcrashutil + + # PATH TO Xcode's symbolicatecrash script + symbolicationcrash: /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash + + # PATH TO Application symbols - Apple's spotlight will try to locate the Application symbols but give it a hint + symbolpath: /Library/ + diff --git a/doc/README_FOR_APP.md b/doc/README_FOR_APP.md index 76397f64..719ae453 100644 --- a/doc/README_FOR_APP.md +++ b/doc/README_FOR_APP.md @@ -43,6 +43,7 @@ Additional configuration options can be found in the following locations: * `config/application.rb` * `config/environments/*.rb` * `config/environments/*/*.yml` +* `config/symbolication_paths.yml` If you don't see what you're looking for in any of those files, you'll probably have to change the code to make it work. Don't be afraid -- the code is @@ -58,6 +59,19 @@ Squash requires the following: * The Bundler gem * Git 1.7 or newer +To realize full stack symbolication for iOS/OS X crashes, the following +are additional requirements (either on the computer running Squash or +on the computer where script/squash_symbolicate_ios_crash will be run): + +* OS X +* Xcode +* plcrashutil (part of PLCrashReporter) +* iOS/OS X application .dSYM files + +In the latter form, the script/squash_symbolicate_ios_crash, config/databas.yml, +config/symbolication_paths.yml, and the lib/squash_ios_crash_log_symbolication.rb +files and folder structure are required. + ### Notes on some of the gem and library choices **Why do you specifically require PostgreSQL?** Squash uses a lot of diff --git a/lib/squash_ios_crash_log_symbolication.rb b/lib/squash_ios_crash_log_symbolication.rb new file mode 100644 index 00000000..0661a8ab --- /dev/null +++ b/lib/squash_ios_crash_log_symbolication.rb @@ -0,0 +1,250 @@ +# +# Copyright 2013 Cerner Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'YAML' +require 'base64' +require 'tempfile' +require 'Logger' + +# +# The main driver for SquashIosCrashLogSymbolication. +# +# Used in conjunction with SquareSquash using the modified SquareSquash Cocoa library. +# The script leveraging this class will convert and symbolicate plcrashlog formatted +# user_info values from metadata column for iOS crash occurrences into crash_log value +# of metadata column in occurrence table within the SquareSquash postgres database. +# +# Note: this class requires configuration files: +# config/symbolication_paths.yml +# config/database.yml + +class SquashIosCrashLogSymbolication + + # Configuration filename + ConfigurationFileName = "symbolication_paths.yml" + + # Database configuration filename + DatabaseConnectionConfigFileName = "database.yml" + + # + # if we are in a Rails environment, use the Rails logger + # otherwise use STDOUT + # + def self.logger + (@logger ||= + begin + if(Rails) + Rails.logger + end + rescue Exception => ex + Logger.new(STDOUT) + end + ) + end + + # Config environment to use: + # 'development', 'test', or 'production' + def self.env + @env ||= ENV['env'] || 'development' + end + + # + # This method takes the unsymbolicated data, and the environment value + # The squash_symbolicate_ios_crash cronscript uses this method in the + # symbolication process. The SquareSquash web app can also use this method + # if SquareSquash web app is installed and configured on a Mac. + # + # + # Fully symbolicate your iOS crash logs stored in SquareSquash using Xcode and plcrashutil + # + # Arguments: + # user_data: a hashmap with a key containing 'PLCrashLog' which holds the iOS unsymbolicated, base64-encoded plcrashlog data + # environment: 'development' (default), 'test' or 'production' + # + # Return: + # crash_log: set to the contents of the symbolicated crash log + # + def self.symbolicate_crash(user_data, env = 'development') + @env = env + + if (user_data.nil?) + self.logger.warn 'symbolicate_crash: user_data is nil' + return nil + end + + # create temp filenames + now = Time.now + plcrash_filename = 'CrashLog.plcrash.' + now.to_i.to_s + crash_log_filename = 'CrashLog.crash.' + now.to_i.to_s + crash_log_done_filename = 'CrashLog.crash.done.' + now.to_i.to_s + # create temp files + pl_crash_log_file = Tempfile.new(plcrash_filename) + pl_crash_log_file.binmode # <-- binary mode + crash_log_file = Tempfile.new(crash_log_filename) + crash_log_done_file = Tempfile.new(crash_log_done_filename) + + # grab PLCrashLog out of 'user_data', decode, and write Tempfile CrashLog.plcrash.XXXXXXX + pl_crash_log = Base64.urlsafe_decode64(user_data["PLCrashLog"]) + + pl_crash_log_file.write(pl_crash_log) + pl_crash_log_file.close + self.logger.info "#{pl_crash_log_file.path} written" + + # exec plcrashutil to convert to Xcode crash log format + command = "#{SquashIosCrashLogSymbolication.plcrashutil_path} convert --format=ios #{pl_crash_log_file.path} > #{crash_log_file.path}" + self.logger.info "executing #{command}..." + + # $? # The exit status of the last process terminated. + output = `#{command}`; result=$? + if($?.exitstatus == 0) + self.logger.info "converted #{pl_crash_log_file.path} to #{crash_log_file.path}" + + # Use symbolicatecrash script from Xcode + command = "#{SquashIosCrashLogSymbolication.symbolicatecrash_path} -o #{crash_log_done_file.path} #{crash_log_file.path} #{SquashIosCrashLogSymbolication.symbol_path}" + self.logger.info "executing #{command}..." + # $? # The exit status of the last process terminated. + output = `#{command}`; result=$? + if($?.exitstatus == 0) + self.logger.info "symbolicated crash log to #{crash_log_done_file.path}" + else + self.logger.error "failed to symbolicate crash log to #{crash_log_done_file.path} - #{output}" + end + else + self.logger.error "failed to converted #{pl_crash_log_file.path} to #{crash_log_file.path} - #{output}" + end + + # now read the symbolicated crash log + if (File.exists?("#{crash_log_done_file.path}")) + symbolicated_crash_contents = File.read("#{crash_log_done_file.path}",File.size("#{crash_log_done_file.path}")) + + # set crash_log to our symbolicated crashlog contents + crash_log = symbolicated_crash_contents + + begin + # clean up + crash_log_file.close + crash_log_done_file.close + pl_crash_log_file.unlink + crash_log_file.unlink + crash_log_done_file.unlink + + rescue Exception => ex + self.logger.error "symbolicate_crash file clean up Exception:\n #{ex.to_s}" + self.logger.error ex.message + self.logger.error ex.backtrace.join("\n") + end + end + + crash_log # return nil or symbolicated crash log contents + end + + # + # This method is a wrapper to call bin/squash_symbolicate_ios_crash + # It connects to PostgreSQL, selects the unsymbolicated data from the + # occurrence table, symbolicates using commandline scripts, then makes + # another connection to the database and performs an update, removing + # the unsymbolicated data and inserting the symbolicated data. + # + # Example: + # => squash_symbolicate_ios_crash development + # + # Arguments: + # environment: 'development', 'test' or 'production' + # + def self.symbolicate(env = nil) + @env = env + # exec squash_symbolicate_ios_crash + script = File.join("#{ENV['PWD']}", 'bin', 'squash_symbolicate_ios_crash') + + command = "#{script} #{env} 2>&1" + puts "executing #{command}" + + # $? # The exit status of the last process terminated. + output = `#{command}`; result=$? + puts output + return result + end + + # If this is running on OS X, return true + def self.osx? + if RUBY_PLATFORM =~ /darwin/i + true + else + false + end + end + + # Path to Xcode's symbolicatecrash script + def self.symbolicatecrash_path + symbolicatecrash_path = symbolication_paths_config['symbolicationcrash'] + end + + # Path to Xcode + def self.xcode_path + if (self.symbolicatecrash_path =~ /Xcode\.app/) + $~.pre_match + else + nil + end + end + + # Path to PLCrashReporter's plcrashutil + def self.plcrashutil_path + symbolication_paths_config['plcrashutil'] + end + + # Path to Application symbols + def self.symbol_path + symbolication_paths_config['symbolpath'] + end + + # + # = Configuration + # + + # Search LOAD_PATH for config/filename + # return path to config + def self.config_path(filename) + path = $LOAD_PATH.find { |dir| + file = File.join(dir, '..', 'config', filename) + # puts file + File.exists?(file) + } + path ? File.join(path, '..', 'config') : 'config' + end + + # The path to the database configuration file + def self.symbolication_database_config_file + File.join(config_path(DatabaseConnectionConfigFileName), DatabaseConnectionConfigFileName) + end + + + # The database configuration + def self.symbolication_database_config + (@database ||= YAML::load(File.read(symbolication_database_config_file)))[env] + end + + # The path to the configuration file + def self.symbolication_paths_config_file + File.join(config_path(ConfigurationFileName), ConfigurationFileName) + end + + # The path to the configuration file + def self.symbolication_paths_config + (@configuration ||= YAML::load(File.read(symbolication_paths_config_file)))[env] + end +end + diff --git a/script/squash_symbolicate_ios_crash b/script/squash_symbolicate_ios_crash new file mode 100755 index 00000000..68ec55a1 --- /dev/null +++ b/script/squash_symbolicate_ios_crash @@ -0,0 +1,152 @@ +#!/usr/bin/env ruby +# +# +# Copyright 2013 Cerner Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# +# Used in conjunction with SquareSquash using the modified SquareSquash Cocoa library. +# This script can be used to convert and symbolicate user_info values from metadata +# column of occurrences table into crash_log metadata value of metadata column in a +# postgres database. +# +# Note: this script requires config/symbolication_paths.yml and config/database.yml +# + +require 'pg' +require 'json' +require 'Logger' +require_relative '../lib/squash_ios_crash_log_symbolication.rb' + +# load our symbolicationpaths and database config files +#SquashIosCrashLogSymbolication + +# variables + +# TODO: config option for logging to a specified file +@logger = Logger.new(STDOUT) +@id = nil +@metadata = nil + +# load database config +db_config = SquashIosCrashLogSymbolication.symbolication_database_config + +# database name, host, user, password +db_host = db_config['hostname'] + +if (db_host.nil?) + puts 'using db_host = localhost' + db_host = 'localhost' +end + +db_user = db_config['username'] +db_name = db_config['database'] +db_pw = db_config['password'] + +# connect to postgres +@select_conn = PG.connect( + dbname: db_name, + host: db_host, + user: db_user, + password: db_pw +) + +# separate update connection +@update_conn = PG.connect( + dbname: db_name, + host: db_host, + user: db_user, + password: db_pw +) + + +def symbolicate_crash_log(data) + user_data = data['user_data'] + crash_log = nil + + begin + crash_log = SquashIosCrashLogSymbolication.symbolicate_crash(user_data, nil) + rescue Exception => ex + @logger.info "symbolicate Exception:\n #{ex.to_s}" + @logger.error ex.message + @logger.error ex.backtrace.join("\n") + end + + if(crash_log) + + # add crash_log field to metadata + data['crash_log'] = crash_log + + # remove 'user_data' - no longer needed + data.delete('user_data') + + # update metadata column + @metadata = data + + begin + # update database + @logger.info "update occurrences set metadata = xxxx where id = #{@id}..." + @update_conn.exec("UPDATE OCCURRENCES SET METADATA = $1 where id = $2", [@metadata.to_json.to_s, @id]) + @logger.info "update occurrences set metadata = xxxx where id = #{@id} committed" + rescue Exception => ex + @logger.info "update occurrences set metadata caught Exception:\n #{ex.to_s}" + @logger.error ex.message + @logger.error ex.backtrace.join("\n") + end + else + @logger.error 'symbolicate_crash: crash_log == nil' + end +end + + +# for each cocoa crash occurrence, try to symbolicate it +# +# TODO: add a column with a flag to indicate symbolication is complete so QUERY will run fast +# +begin + result = @select_conn.exec( "SELECT id, metadata FROM occurrences where client = 'cocoa'" ) + + result.each do |row| + @id = row['id'] + @metadata = row['metadata'] + @logger.info "processing occurrence id=#{@id}" + + data = JSON.parse(@metadata) + user_data = data['user_data'] + crash_log = data['crash_log'] + # until a boolean column is added, test if crash_log is already present + begin + if (crash_log && !crash_log.empty?) + @logger.info "skipping symbolicated occurrence id=#{@id}" + else + if(user_data && !user_data.empty?) + symbolicate_crash_log(data) + else + @logger.info "skipping (user_data == nil || empty) occurrence id=#{@id}" + end + end + rescue Exception => ex + @logger.info "select loop Exception:\n #{ex.to_s}" + @logger.error ex.message + @logger.error ex.backtrace.join("\n") + end + end + +rescue Exception => ex + @logger.info "begin block Exception:\n #{ex.to_s}" + @logger.error ex.message + @logger.error ex.backtrace.join("\n") + raise ex # reraise +end +