2222import io .sentry .SentryLockReason ;
2323import io .sentry .SentryOptions ;
2424import io .sentry .SentryStackTraceFactory ;
25+ import io .sentry .protocol .DebugImage ;
2526import io .sentry .protocol .SentryStackFrame ;
2627import io .sentry .protocol .SentryStackTrace ;
2728import io .sentry .protocol .SentryThread ;
29+ import java .math .BigInteger ;
30+ import java .nio .BufferUnderflowException ;
31+ import java .nio .ByteBuffer ;
32+ import java .nio .ByteOrder ;
2833import java .util .ArrayList ;
2934import java .util .Collections ;
3035import java .util .HashMap ;
@@ -42,12 +47,40 @@ public class ThreadDumpParser {
4247 private static final Pattern BEGIN_UNMANAGED_NATIVE_THREAD_RE =
4348 Pattern .compile ("\" (.*)\" (.*) ?sysTid=(\\ d+)" );
4449
50+ // For reference, see native_stack_dump.cc and tombstone_proto_to_text.cpp in Android sources
51+ // Groups
52+ // 0:entire regex
53+ // 1:index
54+ // 2:pc
55+ // 3:mapinfo
56+ // 4:filename
57+ // 5:mapoffset
58+ // 6:function
59+ // 7:fnoffset
60+ // 8:buildid
4561 private static final Pattern NATIVE_RE =
4662 Pattern .compile (
47- " *(?:native: )?#\\ d+ \\ S+ [0-9a-fA-F]+\\ s+(.*?)\\ s+\\ ((.*)\\ +(\\ d+)\\ )(?: \\ (.*\\ ))?" );
48- private static final Pattern NATIVE_NO_LOC_RE =
49- Pattern .compile (
50- " *(?:native: )?#\\ d+ \\ S+ [0-9a-fA-F]+\\ s+(.*)\\ s*\\ (?(.*)\\ )?(?: \\ (.*\\ ))?" );
63+ // " native: #12 pc 0xabcd1234"
64+ " *(?:native: )?#(\\ d+) \\ S+ ([0-9a-fA-F]+)"
65+ // The map info includes a filename and an optional offset into the file
66+ + ("\\ s+("
67+ // "/path/to/file.ext",
68+ + "(.*?)"
69+ // optional " (deleted)" suffix (deleted files) needed here to bias regex
70+ // correctly
71+ + "(?:\\ s+\\ (deleted\\ ))?"
72+ // " (offset 0xabcd1234)", if the mapping is not into the beginning of the file
73+ + "(?:\\ s+\\ (offset (.*?)\\ ))?"
74+ + ")" )
75+ // Optional function
76+ + ("(?:\\ s+\\ ((?:"
77+ + "\\ ?\\ ?\\ ?" // " (???) marks a missing function, so don't capture it in a group
78+ + "|(.*?)(?:\\ +(\\ d+))?" // " (func+1234)", offset is
79+ // optional
80+ + ")\\ ))?" )
81+ // Optional " (BuildId: abcd1234abcd1234abcd1234abcd1234abcd1234)"
82+ + "(?:\\ s+\\ (BuildId: (.*?)\\ ))?" );
83+
5184 private static final Pattern JAVA_RE =
5285 Pattern .compile (" *at (?:(.+)\\ .)?([^.]+)\\ .([^.]+)\\ ((.*):([\\ d-]+)\\ )" );
5386 private static final Pattern JNI_RE =
@@ -75,15 +108,48 @@ public class ThreadDumpParser {
75108
76109 private final @ NotNull SentryStackTraceFactory stackTraceFactory ;
77110
111+ private final @ NotNull Map <String , DebugImage > debugImages ;
112+
113+ private final @ NotNull List <SentryThread > threads ;
114+
78115 public ThreadDumpParser (final @ NotNull SentryOptions options , final boolean isBackground ) {
79116 this .options = options ;
80117 this .isBackground = isBackground ;
81118 this .stackTraceFactory = new SentryStackTraceFactory (options );
119+ this .debugImages = new HashMap <>();
120+ this .threads = new ArrayList <>();
121+ }
122+
123+ @ NotNull
124+ public List <DebugImage > getDebugImages () {
125+ return new ArrayList <>(debugImages .values ());
82126 }
83127
84128 @ NotNull
85- public List <SentryThread > parse (final @ NotNull Lines lines ) {
86- final List <SentryThread > sentryThreads = new ArrayList <>();
129+ public List <SentryThread > getThreads () {
130+ return threads ;
131+ }
132+
133+ @ Nullable
134+ private static String buildIdToDebugId (final @ NotNull String buildId ) {
135+ try {
136+ // Abuse BigInteger as a hex string parser. Extra byte needed to handle leading zeros.
137+ final ByteBuffer buf = ByteBuffer .wrap (new BigInteger ("10" + buildId , 16 ).toByteArray ());
138+ buf .get ();
139+ return String .format (
140+ "%08x-%04x-%04x-%04x-%04x%08x" ,
141+ buf .order (ByteOrder .LITTLE_ENDIAN ).getInt (),
142+ buf .getShort (),
143+ buf .getShort (),
144+ buf .order (ByteOrder .BIG_ENDIAN ).getShort (),
145+ buf .getShort (),
146+ buf .getInt ());
147+ } catch (NumberFormatException | BufferUnderflowException e ) {
148+ return null ;
149+ }
150+ }
151+
152+ public void parse (final @ NotNull Lines lines ) {
87153
88154 final Matcher beginManagedThreadRe = BEGIN_MANAGED_THREAD_RE .matcher ("" );
89155 final Matcher beginUnmanagedNativeThreadRe = BEGIN_UNMANAGED_NATIVE_THREAD_RE .matcher ("" );
@@ -92,7 +158,7 @@ public List<SentryThread> parse(final @NotNull Lines lines) {
92158 final Line line = lines .next ();
93159 if (line == null ) {
94160 options .getLogger ().log (SentryLevel .WARNING , "Internal error while parsing thread dump." );
95- return sentryThreads ;
161+ return ;
96162 }
97163 final String text = line .text ;
98164 // we only handle managed threads, as unmanaged/not attached do not have the thread id and
@@ -102,11 +168,10 @@ public List<SentryThread> parse(final @NotNull Lines lines) {
102168
103169 final SentryThread thread = parseThread (lines );
104170 if (thread != null ) {
105- sentryThreads .add (thread );
171+ threads .add (thread );
106172 }
107173 }
108174 }
109- return sentryThreads ;
110175 }
111176
112177 private SentryThread parseThread (final @ NotNull Lines lines ) {
@@ -176,7 +241,6 @@ private SentryStackTrace parseStacktrace(
176241 SentryStackFrame lastJavaFrame = null ;
177242
178243 final Matcher nativeRe = NATIVE_RE .matcher ("" );
179- final Matcher nativeNoLocRe = NATIVE_NO_LOC_RE .matcher ("" );
180244 final Matcher javaRe = JAVA_RE .matcher ("" );
181245 final Matcher jniRe = JNI_RE .matcher ("" );
182246 final Matcher lockedRe = LOCKED_RE .matcher ("" );
@@ -194,20 +258,7 @@ private SentryStackTrace parseStacktrace(
194258 break ;
195259 }
196260 final String text = line .text ;
197- if (matches (nativeRe , text )) {
198- final SentryStackFrame frame = new SentryStackFrame ();
199- frame .setPackage (nativeRe .group (1 ));
200- frame .setFunction (nativeRe .group (2 ));
201- frame .setLineno (getInteger (nativeRe , 3 , null ));
202- frames .add (frame );
203- lastJavaFrame = null ;
204- } else if (matches (nativeNoLocRe , text )) {
205- final SentryStackFrame frame = new SentryStackFrame ();
206- frame .setPackage (nativeNoLocRe .group (1 ));
207- frame .setFunction (nativeNoLocRe .group (2 ));
208- frames .add (frame );
209- lastJavaFrame = null ;
210- } else if (matches (javaRe , text )) {
261+ if (matches (javaRe , text )) {
211262 final SentryStackFrame frame = new SentryStackFrame ();
212263 final String packageName = javaRe .group (1 );
213264 final String className = javaRe .group (2 );
@@ -219,6 +270,31 @@ private SentryStackTrace parseStacktrace(
219270 frame .setInApp (stackTraceFactory .isInApp (module ));
220271 frames .add (frame );
221272 lastJavaFrame = frame ;
273+ } else if (matches (nativeRe , text )) {
274+ final SentryStackFrame frame = new SentryStackFrame ();
275+ frame .setPackage (nativeRe .group (3 ));
276+ frame .setFunction (nativeRe .group (6 ));
277+ frame .setLineno (getInteger (nativeRe , 7 , null ));
278+ frame .setInstructionAddr ("0x" + nativeRe .group (2 ));
279+ frame .setPlatform ("native" );
280+
281+ final String buildId = nativeRe .group (8 );
282+ final String debugId = buildId == null ? null : buildIdToDebugId (buildId );
283+ if (debugId != null ) {
284+ if (!debugImages .containsKey (debugId )) {
285+ final DebugImage debugImage = new DebugImage ();
286+ debugImage .setDebugId (debugId );
287+ debugImage .setType ("elf" );
288+ debugImage .setCodeFile (nativeRe .group (4 ));
289+ debugImage .setCodeId (buildId );
290+ debugImages .put (debugId , debugImage );
291+ }
292+ // The addresses in the thread dump are relative to the image
293+ frame .setAddrMode ("rel:" + debugId );
294+ }
295+
296+ frames .add (frame );
297+ lastJavaFrame = null ;
222298 } else if (matches (jniRe , text )) {
223299 final SentryStackFrame frame = new SentryStackFrame ();
224300 final String packageName = jniRe .group (1 );
@@ -227,6 +303,7 @@ private SentryStackTrace parseStacktrace(
227303 frame .setModule (module );
228304 frame .setFunction (jniRe .group (3 ));
229305 frame .setInApp (stackTraceFactory .isInApp (module ));
306+ frame .setNative (true );
230307 frames .add (frame );
231308 lastJavaFrame = frame ;
232309 } else if (matches (lockedRe , text )) {
@@ -334,8 +411,8 @@ private Long getLong(
334411
335412 @ Nullable
336413 private Integer getInteger (
337- final @ NotNull Matcher matcher , final int group , final @ Nullable Integer defaultValue ) {
338- final String str = matcher .group (group );
414+ final @ NotNull Matcher matcher , final int groupIndex , final @ Nullable Integer defaultValue ) {
415+ final String str = matcher .group (groupIndex );
339416 if (str == null || str .length () == 0 ) {
340417 return defaultValue ;
341418 } else {
0 commit comments