| OLD | NEW |
| (Empty) | |
| 1 # Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 import copy |
| 6 import logging |
| 7 |
| 8 from google.appengine.ext import ndb |
| 9 |
| 10 from common import appengine_util |
| 11 from common import chromium_deps |
| 12 from common import constants |
| 13 from common import time_util |
| 14 from crash.crash_pipeline import CrashWrapperPipeline |
| 15 from crash.crash_report import CrashReport |
| 16 from crash.culprit import NullCulprit |
| 17 from model import analysis_status |
| 18 from model.crash.crash_config import CrashConfig |
| 19 from model.crash.cracas_crash_analysis import CracasCrashAnalysis |
| 20 from model.crash.fracas_crash_analysis import FracasCrashAnalysis |
| 21 |
| 22 |
| 23 # TODO: this class depends on ndb stuff, and should therefore move to |
| 24 # cr-culprit-finder/service/predator as part of the big reorganization. |
| 25 # Also, this class should be renamed to "PredatorApp" (alas, "Azalea" |
| 26 # was renamed to "Predator"). |
| 27 class Findit(object): |
| 28 def __init__(self): |
| 29 # TODO(wrengr): because self.client is volatile, we need some way |
| 30 # of updating the Azelea instance whenever the config changes. How to |
| 31 # do that cleanly? |
| 32 self.azalea = None |
| 33 self.stacktrace_parser = None |
| 34 |
| 35 # This is a method rather than an attribute to ensure it can't be |
| 36 # changed. And it's a classmethod rather than a property, since we can |
| 37 # get it directly from the class itself, without needing to allocate |
| 38 # an instance first. |
| 39 @classmethod |
| 40 def ClientID(cls): |
| 41 """Reify the name of this class as a CrashClient id, for serialization.""" |
| 42 raise NotImplementedError() |
| 43 |
| 44 @property |
| 45 def client_id(self): |
| 46 """Get the client id from the class of this object. |
| 47 |
| 48 N.B., this property is static and should not be overridden.""" |
| 49 return self.__class__.ClientID() |
| 50 |
| 51 # TODO(wrengr): can we remove the dependency on CrashConfig |
| 52 # entirely? It'd be better to receive method calls whenever things |
| 53 # change, so that we know the change happened (and what in particular |
| 54 # changed) so that we can update our internal state as appropriate. |
| 55 @property |
| 56 def config(self): |
| 57 """Get the current value of the client config. |
| 58 |
| 59 N.B., this property is volatile and may change asynchronously.""" |
| 60 return CrashConfig.Get().GetClientConfig(self.client_id) |
| 61 |
| 62 # TODO(wrengr): rename to CanonicalizePlatform or something like that. |
| 63 def RenamePlatform(self, platform): |
| 64 """Remap the platform to a different one, based on the config.""" |
| 65 # TODO(katesonia): Remove the default value after adding validity check to |
| 66 # config. |
| 67 return self.config.get('platform_rename', {}).get(platform, platform) |
| 68 |
| 69 def CheckPolicy(self, crash_data): |
| 70 """Check whether this client supports analyzing the given report. |
| 71 |
| 72 Some clients only support analysis for crashes on certain platforms |
| 73 or channels, etc. This method checks to see whether this client can |
| 74 analyze the given crash. |
| 75 |
| 76 Args: |
| 77 crash_data (dict from JSON): ?? |
| 78 |
| 79 Returns: |
| 80 If satisfied, we return the |crash_data| which may have had some |
| 81 fields modified. Otherwise returns None. |
| 82 """ |
| 83 return None |
| 84 |
| 85 # TODO(wrengr): rename this to something like _NewAnalysis, since |
| 86 # it only does the "allocation" and needs to/will be followed up with |
| 87 # _InitializeAnalysis anyways. |
| 88 def CreateAnalysis(self, crash_identifiers): |
| 89 return None |
| 90 |
| 91 def GetAnalysis(self, crash_identifiers): |
| 92 """Return the CrashAnalysis for the |crash_identifiers|, if one exists. |
| 93 |
| 94 Args: |
| 95 crash_identifiers (??): ?? |
| 96 |
| 97 Returns: |
| 98 If a CrashAnalysis ndb.Model already exists for the |
| 99 |crash_identifiers|, then we return it. Otherwise, returns None. |
| 100 """ |
| 101 return None |
| 102 |
| 103 # TODO(wrengr): this should be a method on CrashAnalysis, not on Findit. |
| 104 def _InitializeAnalysis(self, model, crash_data): |
| 105 """(Re)Initialize a CrashAnalysis ndb.Model, but do not |put()| it yet. |
| 106 |
| 107 This method is only ever called from _NeedsNewAnalysis which is only |
| 108 ever called from ScheduleNewAnalysis. It is used for filling in the |
| 109 fields of a CrashAnalysis ndb.Model for the first time (though it |
| 110 can also be used to re-initialize a given CrashAnalysis). Subclasses |
| 111 should extend (not override) this to (re)initialize any |
| 112 client-specific fields they may have.""" |
| 113 # Get rid of any previous values there may have been. |
| 114 model.Reset() |
| 115 |
| 116 # Set common properties. |
| 117 model.crashed_version = crash_data['crashed_version'] |
| 118 model.stack_trace = crash_data['stack_trace'] |
| 119 model.signature = crash_data['signature'] |
| 120 model.platform = crash_data['platform'] |
| 121 # TODO(wrengr): assert that crash_data['client_id'] == self.client_id |
| 122 model.client_id = self.client_id |
| 123 |
| 124 # Set progress properties. |
| 125 model.status = analysis_status.PENDING |
| 126 model.requested_time = time_util.GetUTCNow() |
| 127 |
| 128 @ndb.transactional |
| 129 def _NeedsNewAnalysis(self, crash_data): |
| 130 raise NotImplementedError() |
| 131 |
| 132 def ScheduleNewAnalysis(self, crash_data, queue_name=constants.DEFAULT_QUEUE): |
| 133 """Schedules an analysis.""" |
| 134 # Check policy and tune arguments if needed. |
| 135 crash_data = self.CheckPolicy(crash_data) |
| 136 if crash_data is None: |
| 137 return False |
| 138 |
| 139 # Detect the regression range, and decide if we actually need to |
| 140 # run a new anlaysis or not. |
| 141 if not self._NeedsNewAnalysis(crash_data): |
| 142 return False |
| 143 |
| 144 crash_identifiers = crash_data['crash_identifiers'] |
| 145 analysis_pipeline = CrashWrapperPipeline(self, crash_identifiers) |
| 146 # Attribute defined outside __init__ - pylint: disable=W0201 |
| 147 analysis_pipeline.target = appengine_util.GetTargetNameForModule( |
| 148 constants.CRASH_BACKEND[self.client_id]) |
| 149 analysis_pipeline.start(queue_name=queue_name) |
| 150 logging.info('New %s analysis is scheduled for %s', self.client_id, |
| 151 repr(crash_identifiers)) |
| 152 return True |
| 153 |
| 154 # TODO(wrengr): does the parser actually need the version, signature, |
| 155 # and platform? If not, then we should be able to just pass the string |
| 156 # to be parsed (which would make a lot more sense than passing the |
| 157 # whole model). |
| 158 def ParseStacktrace(self, model): |
| 159 """Parse a CrashAnalysis's |stack_trace| string into a Stacktrace object. |
| 160 |
| 161 Args: |
| 162 model (CrashAnalysis): The model containing the stack_trace string |
| 163 to be parsed. |
| 164 |
| 165 Returns: |
| 166 On success, returns a Stacktrace object; on failure, returns None. |
| 167 """ |
| 168 stacktrace = self.stacktrace_parser.Parse(model.stack_trace, |
| 169 chromium_deps.GetChromeDependency( |
| 170 model.crashed_version, model.platform), |
| 171 model.signature) |
| 172 if not stacktrace: |
| 173 logging.warning('Failed to parse the stacktrace %s', model.stack_trace) |
| 174 return None |
| 175 |
| 176 return stacktrace |
| 177 |
| 178 # TODO(wrengr): This is only called by |CrashAnalysisPipeline.run|; |
| 179 # we should be able to adjust things so that we only need to take in |
| 180 # |crash_identifiers|, or a CrashReport, rather than taking in the |
| 181 # whole model. |
| 182 # TODO(wrengr): as part of inverting crash_pipeline.py wrt |
| 183 # findit.py, this method should probably be the one to create the |
| 184 # |CrashWrapperPipeline| and |CrashAnalysisPipeline| therein, rather |
| 185 # than the other way around. |
| 186 def FindCulprit(self, model): |
| 187 """Given a CrashAnalysis ndb.Model, return a Culprit.""" |
| 188 stacktrace = self.ParseStacktrace(model) |
| 189 if stacktrace is None: |
| 190 # TODO(wrengr): refactor things so we don't need the NullCulprit class. |
| 191 return NullCulprit() |
| 192 |
| 193 return self.azalea.FindCulprit(CrashReport( |
| 194 crashed_version = model.crashed_version, |
| 195 signature = model.signature, |
| 196 platform = model.platform, |
| 197 stacktrace = stacktrace, |
| 198 regression_range = model.regression_range)) |
| OLD | NEW |