/** * \file UploadToSentry.cs * Support for reading verbose unmanaged crash dumps * * Author: * Alexander Kyte (alkyte@microsoft.com) * * (C) 2018 Microsoft, Inc. * */ using System; using System.IO; using System.Linq; using System.Collections.Generic; using Mono.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using Newtonsoft.Json.Linq; using System.Reflection; using Mono.Cecil; using Mono.Cecil.Cil; using System.Net; [assembly: CLSCompliant (false)] namespace UploadToSentry { // Modeled after https://github.com/getsentry/raven-csharp/blob/develop/src/app/SharpRaven/Dsn.cs class Dsn { public readonly string PrivateKey; public readonly string PublicKey; public readonly string ProjectID; private readonly string path; private readonly int port; private readonly string sentryUriString; private readonly Uri uri; public Dsn (string dsn) { this.uri = new Uri(dsn); this.PrivateKey = GetPrivateKey(this.uri); this.PublicKey = GetPublicKey(this.uri) ?? throw new ArgumentException("A publicKey is required.", nameof(dsn)); this.port = this.uri.Port; this.ProjectID = GetProjectID(this.uri); this.path = GetPath(this.uri); this.sentryUriString = String.Format("{0}://{1}:{2}{3}/api/{4}/store/", this.uri.Scheme, this.uri.DnsSafeHost, this.port, this.path, this.ProjectID); } private static string GetPath(Uri uri) { int lastSlash = uri.AbsolutePath.LastIndexOf("/", StringComparison.Ordinal); return uri.AbsolutePath.Substring(0, lastSlash); } private static string GetProjectID(Uri uri) { int lastSlash = uri.AbsoluteUri.LastIndexOf("/", StringComparison.Ordinal); return uri.AbsoluteUri.Substring(lastSlash + 1); } public Uri SentryUri { get { return new Uri (sentryUriString); } } private static string GetPrivateKey(Uri uri) { var parts = uri.UserInfo.Split(':'); return parts.Length == 2 ? parts[1] : null; } private static string GetPublicKey(Uri uri) { var publicKey = uri.UserInfo.Split(':')[0]; return publicKey != string.Empty ? publicKey : null; } } class CodeCollection { Dictionary, Collection> Lookup; Dictionary, Tuple> Types; public void Add (string assembly, string klass, string function, string mvid, uint token, Collection seqs) { var key = new Tuple(mvid, token); Lookup[key] = seqs; Types[key] = new Tuple(assembly, klass, function); } public CodeCollection(string [] assemblies) { Lookup = new Dictionary, Collection>(); Types = new Dictionary, Tuple>(); foreach (string assembly in assemblies) { if (assembly.EndsWith(".dll") || assembly.EndsWith(".exe")) { // Console.WriteLine("Reading {0}", assembly); var readerParameters = new ReaderParameters { ReadSymbols = true, InMemory = true }; AssemblyDefinition myLibrary = null; try { myLibrary = AssemblyDefinition.ReadAssembly(assembly, readerParameters); string mvid = myLibrary.MainModule.Mvid.ToString().ToUpper(); Console.WriteLine("\t-- Success Parsing {0}: {1}", assembly, mvid); foreach (var ty in myLibrary.MainModule.Types) { for (int i = 0; i < ty.Methods.Count; i++) { string klass = ty.FullName; string function = ty.Methods[i].FullName; uint token = Convert.ToUInt32(ty.Methods[i].MetadataToken.ToInt32()); this.Add(assembly, klass, function, mvid, token, ty.Methods[i].DebugInformation.SequencePoints); } } } catch (SymbolsNotFoundException) { // ignore assemblies without debug symbols continue; } catch (Exception e) { Console.WriteLine("\t-- Error Parsing {0}: {1}", assembly, e.Message); } } } } public JObject Find (string mvid, uint token, uint goal) { var method_idx = new Tuple(mvid, token); if (!Lookup.ContainsKey(method_idx)) return null; var seqs = Lookup[method_idx]; var accum = new JObject(); foreach (var seq in seqs) { if (goal != seq.Offset) continue; accum.Add (new JProperty("lineno", seq.StartLine)); accum.Add (new JProperty("filename", seq.Document.Url)); break; } var typ = Types[method_idx]; var assembly = typ.Item1; var klass = typ.Item2; accum.Add (new JProperty("module", String.Format("{0} {1}", assembly, klass))); accum.Add (new JProperty("function", typ.Item3)); return accum; } } class Uploader { CodeCollection codebase; public JObject Format_0_0_3 (string fileName, JObject payload, string hash) { var event_id = new JProperty("event_id", Guid.NewGuid().ToString("n")); var timestamp = new JProperty("timestamp", DateTime.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture)); var exc_objs = new List (); var thread_objs = new List (); var stackTraces = payload["threads"] as JArray; var path = Path.GetDirectoryName (fileName); // Best differentiator in-tree for files is where they are run from foreach (var st in stackTraces) { var thread_id = st["native_thread_id"]?.ToString(); var unmanaged_frames = new List(); var managed_frames = new List(); var exceptions = new List(); var thread_name = st["thread_name"]?.ToString (); if (thread_name == null || thread_name?.Length == 0) thread_name = "Unnamed thread"; var payload_unmanaged_frames = st["unmanaged_frames"] as JArray; for (int fr=0; payload_unmanaged_frames != null && fr < payload_unmanaged_frames.Count; fr++) { var frame = payload_unmanaged_frames [fr] as JObject; var native_address = frame["native_address"]; var unmanaged_name = frame["unmanaged_name"] != null ? frame["unmanaged_name"].ToString() : ""; var fn_filename = new JProperty("filename", ""); var function = new JProperty("function", unmanaged_name); var module = new JProperty("module", "mono-sgen"); var vars = new JProperty("vars", new JObject(new JProperty ("native_address", native_address))); var blob = new JObject(fn_filename, function, module, vars); unmanaged_frames.Add (blob); } var payload_managed_frames = st["managed_frames"] as JArray; for (int fr = 0; payload_managed_frames != null && fr < payload_managed_frames.Count; fr++) { var frame = payload_managed_frames [fr] as JObject; if (frame["is_managed"] != null && frame["is_managed"].ToString ().ToUpper () == "TRUE") { var guid_val = frame["guid"].ToString (); var token_val = Convert.ToUInt32(frame["token"].ToString (), 16); var offset_val = Convert.ToUInt32(frame["il_offset"].ToString (), 16); var output_frame = codebase.Find (guid_val, token_val, offset_val); if (output_frame == null) continue; var guid = new JProperty("guid", guid_val); var token = new JProperty("token", token_val); var il_offset = new JProperty("il_offset", offset_val); output_frame.Add (new JProperty("vars", new JObject(guid, token, il_offset))); managed_frames.Add(output_frame); } else { var native_address = frame["native_address"]; var unmanaged_name = frame["unmanaged_name"] != null ? frame["unmanaged_name"].ToString() : ""; var fn_filename = new JProperty("filename", "mono-sgen"); var function = new JProperty("function", unmanaged_name); var module = new JProperty("module", ""); var vars = new JProperty("vars", frame); var blob = new JObject(fn_filename, function, module, vars); managed_frames.Add (blob); } } var payload_exceptions = st["exceptions"] as JArray; for (int exc=0; payload_exceptions != null && exc < payload_exceptions.Count; exc++) { var exception_obj = payload_exceptions [exc] as JObject; var exc_managed_frames = new List(); var payload_exception_frames = exception_obj ["managed_frames"] as JArray; for (int fr = 0; payload_exception_frames != null && fr < payload_exception_frames.Count; fr++) { var frame = payload_exception_frames [fr] as JObject; if (frame["is_managed"] != null && frame["is_managed"].ToString ().ToUpper () == "TRUE") { var guid_val = frame["guid"].ToString (); var token_val = Convert.ToUInt32(frame["token"].ToString (), 16); var offset_val = Convert.ToUInt32(frame["il_offset"].ToString (), 16); var output_frame = codebase.Find (guid_val, token_val, offset_val); if (output_frame == null) continue; var guid = new JProperty("guid", guid_val); var token = new JProperty("token", token_val); var il_offset = new JProperty("il_offset", offset_val); output_frame.Add (new JProperty("vars", new JObject(guid, token, il_offset))); exc_managed_frames.Add(output_frame); } else { var native_address = frame["native_address"]; var unmanaged_name = frame["unmanaged_name"] != null ? frame["unmanaged_name"].ToString() : ""; var fn_filename = new JProperty("filename", "mono-sgen"); var function = new JProperty("function", unmanaged_name); var module = new JProperty("module", ""); var vars = new JProperty("vars", frame); var blob = new JObject(fn_filename, function, module, vars); exc_managed_frames.Add (blob); } } var type = new JProperty ("type", exception_obj ["type"].ToString ()); var managed_st = new JProperty("frames", new JArray(exc_managed_frames.ToArray())); var exc_blob = new JObject(type, managed_st); exceptions.Add (exc_blob); } if (unmanaged_frames.Count > 0) { var unmanaged_st = new JObject(new JProperty("frames", new JArray(unmanaged_frames.ToArray ()))); var id = String.Format ("{0}_unmanaged", st ["native_thread_id"]); var active = new JProperty ("active", "true"); if (st["crashed"]?.ToString ().ToUpper () == "TRUE") { var unmanaged_thread = new JObject (active, new JProperty ("crashed", "true"), new JProperty ("name", String.Format ("{0} unmanaged", thread_name)), new JProperty ("id", id)); var unmanaged_exc = new JObject(new JProperty("module", String.Format("{0}_managed_frames", thread_id)), new JProperty("type", path), new JProperty("value", ""), new JProperty("stacktrace", unmanaged_st), new JProperty("thread_id", id)); thread_objs.Add(unmanaged_thread); exc_objs.Add (unmanaged_exc); } else { var unmanaged_thread = new JObject (active, new JProperty ("name", String.Format ("{0} Unmanaged", thread_name)), new JProperty ("id", id), new JProperty ("stacktrace", unmanaged_st)); thread_objs.Add(unmanaged_thread); } } if (managed_frames.Count > 0) { var managed_st = new JObject(new JProperty("frames", new JArray(managed_frames.ToArray()))); // If we are the crashing thread, set the exception object to the // managed stacktrace and the thread object to the managed thread // // If we aren't, add the thread + st to var id = String.Format ("{0}_managed", st["native_thread_id"]); var active = new JProperty ("active", "true"); if (unmanaged_frames.Count == 0 && st["crashed"]?.ToString ().ToUpper () == "TRUE") { var managed_thread = new JObject (active, new JProperty ("crashed", "true"), new JProperty ("name", String.Format ("{0} managed", thread_name)), new JProperty ("id", id)); var managed_exc = new JObject(new JProperty("module", String.Format("{0}_managed_frames", thread_id)), new JProperty("type", path), new JProperty("value", ""), new JProperty("stacktrace", managed_st), new JProperty("thread_id", id)); thread_objs.Add(managed_thread); exc_objs.Add (managed_exc); } else { var managed_thread = new JObject (active, new JProperty ("name", String.Format ("{0} managed", thread_name)), new JProperty ("id", id), new JProperty ("stacktrace", managed_st)); thread_objs.Add(managed_thread); } } bool first_exception = true; foreach (var exc_input in exceptions) { // The 0.0.3 managed exception only has one thread, so we want it to be active and set crashed to true. var active = new JProperty ("active", first_exception ? "true" : "false"); first_exception = false; var name = "Caught Managed Exception"; var exception_thread = new JObject (active, new JProperty ("crashed", "true"), new JProperty ("name", name), new JProperty ("id", name), new JProperty ("exception", exc_input)); var exception_exc = new JObject(new JProperty("module", name), new JProperty("type", path), new JProperty("value", ""), new JProperty("stacktrace", exc_input ["frames"])); thread_objs.Add (exception_thread); exc_objs.Add (exception_exc); } } var exception = new JProperty("exception", new JObject (new JProperty ("values", new JArray(exc_objs.ToArray ())))); var threads = new JProperty("threads", new JObject (new JProperty ("values", new JArray(thread_objs.ToArray ())))); // Bake in the whole blob var embedded = new JProperty("extra", payload); var fingerprint = new JProperty ("fingerprint", new JArray (new JValue (hash))); var sentry_message = new JObject (timestamp, event_id, exception, embedded, threads, fingerprint); return sentry_message; } public JObject Format_0_0_2 (string fileName, JObject payload, string hash) { var event_id = new JProperty("event_id", Guid.NewGuid().ToString("n")); var timestamp = new JProperty("timestamp", DateTime.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture)); var exc_objs = new List (); var thread_objs = new List (); var stackTraces = payload["threads"] as JArray; var path = Path.GetDirectoryName (fileName); // Best differentiator in-tree for files is where they are run from for (int i=0; i < stackTraces.Count; i++){ var thread_id = stackTraces[i]["native_thread_id"]?.ToString(); var unmanaged_frames = new List(); var managed_frames = new List(); var thread_name = stackTraces [i]["thread_name"].ToString (); if (thread_name == null || thread_name.Length == 0) thread_name = "Unnamed thread"; var payload_unmanaged_frames = stackTraces[i]["unmanaged_frames"] as JArray; for (int fr=0; payload_unmanaged_frames != null && fr < payload_unmanaged_frames.Count; fr++) { var frame = payload_unmanaged_frames [fr] as JObject; var native_address = frame["native_address"]; var unmanaged_name = frame["unmanaged_name"] != null ? frame["unmanaged_name"].ToString() : ""; var fn_filename = new JProperty("filename", ""); var function = new JProperty("function", unmanaged_name); var module = new JProperty("module", "mono-sgen"); var vars = new JProperty("vars", new JObject(new JProperty ("native_address", native_address))); var blob = new JObject(fn_filename, function, module, vars); unmanaged_frames.Add (blob); } var payload_managed_frames = stackTraces[i]["managed_frames"] as JArray; for (int fr = 0; payload_managed_frames != null && fr < payload_managed_frames.Count; fr++) { var frame = payload_managed_frames [fr] as JObject; if (frame["is_managed"] != null && frame["is_managed"].ToString ().ToUpper () == "TRUE") { var guid_val = frame["guid"].ToString (); var token_val = Convert.ToUInt32(frame["token"].ToString (), 16); var offset_val = Convert.ToUInt32(frame["il_offset"].ToString (), 16); var output_frame = codebase.Find (guid_val, token_val, offset_val); if (output_frame == null) continue; var guid = new JProperty("guid", guid_val); var token = new JProperty("token", token_val); var il_offset = new JProperty("il_offset", offset_val); output_frame.Add (new JProperty("vars", new JObject(guid, token, il_offset))); managed_frames.Add(output_frame); } else { var native_address = frame["native_address"]; var unmanaged_name = frame["unmanaged_name"] != null ? frame["unmanaged_name"].ToString() : ""; var fn_filename = new JProperty("filename", "mono-sgen"); var function = new JProperty("function", unmanaged_name); var module = new JProperty("module", ""); var vars = new JProperty("vars", frame); var blob = new JObject(fn_filename, function, module, vars); managed_frames.Add (blob); } } if (unmanaged_frames.Count > 0) { var unmanaged_st = new JObject(new JProperty("frames", new JArray(unmanaged_frames.ToArray ()))); var id = String.Format ("{0}_unmanaged", stackTraces[i]["native_thread_id"]); var active = new JProperty ("active", "true"); if (stackTraces[i]["crashed"].ToString ().ToUpper () == "TRUE") { var unmanaged_thread = new JObject (active, new JProperty ("crashed", "true"), new JProperty ("name", String.Format ("{0} unmanaged", thread_name)), new JProperty ("id", id)); var unmanaged_exc = new JObject(new JProperty("module", String.Format("{0}_managed_frames", thread_id)), new JProperty("type", path), new JProperty("value", ""), new JProperty("stacktrace", unmanaged_st), new JProperty("thread_id", id)); thread_objs.Add(unmanaged_thread); exc_objs.Add (unmanaged_exc); } else { var unmanaged_thread = new JObject (active, new JProperty ("name", String.Format ("{0} Unmanaged", thread_name)), new JProperty ("id", id), new JProperty ("stacktrace", unmanaged_st)); thread_objs.Add(unmanaged_thread); } } if (managed_frames.Count > 0) { var managed_st = new JObject(new JProperty("frames", new JArray(managed_frames.ToArray()))); // If we are the crashing thread, set the exception object to the // managed stacktrace and the thread object to the managed thread // // If we aren't, add the thread + st to var id = String.Format ("{0}_managed", stackTraces[i]["native_thread_id"]); var active = new JProperty ("active", "true"); if (unmanaged_frames.Count == 0 && stackTraces[i]["crashed"].ToString ().ToUpper () == "TRUE") { var managed_thread = new JObject (active, new JProperty ("crashed", "true"), new JProperty ("name", String.Format ("{0} managed", thread_name)), new JProperty ("id", id)); var managed_exc = new JObject(new JProperty("module", String.Format("{0}_managed_frames", thread_id)), new JProperty("type", path), new JProperty("value", ""), new JProperty("stacktrace", managed_st), new JProperty("thread_id", id)); thread_objs.Add(managed_thread); exc_objs.Add (managed_exc); } else { var managed_thread = new JObject (active, new JProperty ("name", String.Format ("{0} managed", thread_name)), new JProperty ("id", id), new JProperty ("stacktrace", managed_st)); thread_objs.Add(managed_thread); } } } var exception = new JProperty("exception", new JObject (new JProperty ("values", new JArray(exc_objs.ToArray ())))); var threads = new JProperty("threads", new JObject (new JProperty ("values", new JArray(thread_objs.ToArray ())))); // Bake in the whole blob var embedded = new JProperty("extra", payload); var fingerprint = new JProperty ("fingerprint", new JArray (new JValue (hash))); var sentry_message = new JObject (timestamp, event_id, exception, embedded, threads, fingerprint); return sentry_message; } public void SendMessage (JObject sentry_message, Dsn url) { // Console.WriteLine ("Sending {0}", sentry_message.ToString ()); var request = (HttpWebRequest) WebRequest.Create (url.SentryUri); request.Method = "POST"; request.ContentType = "application/json"; request.UserAgent = "MonoSentryUploader/1.0.0.0"; var sentryVersion = 7; var time = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds; var key = url.PrivateKey != null ? ", sentry_secret=" + url.PrivateKey : null; var header = String.Format("Sentry sentry_version={0}" + ", sentry_client={1}, sentry_timestamp={2}, sentry_key={3}{4}", sentryVersion, request.UserAgent, time, url.PublicKey, key); request.Headers ["X-Sentry-Auth"] = header; byte[] byteArray = Encoding.UTF8.GetBytes(sentry_message.ToString ()); request.ContentLength = byteArray.Length; Stream dataStream = request.GetRequestStream(); dataStream.Write(byteArray, 0, byteArray.Length); dataStream.Close(); try { WebResponse response = request.GetResponse (); // Display the status. // Console.WriteLine(((HttpWebResponse)response).StatusDescription); StreamReader reader = new StreamReader(response.GetResponseStream()); // Read the content. string responseFromServer = reader.ReadToEnd(); // Display the content. Console.WriteLine("\t-- HTTP POST Success {0}", responseFromServer); // Clean up the streams. } catch (WebException ex) { Console.WriteLine("\t-- HTTP POST Error:", ex.Response.Headers [""]); for (int i=0; i < ex.Response.Headers.Count; i++) Console.WriteLine("\t\t {0} : {1}", ex.Response.Headers.Keys [i], ex.Response.Headers [i]); } } public void Upload (string filePath, string os_tag, Dsn url) { if (!File.Exists(filePath)) throw new Exception(String.Format("Json file not found {0}", filePath)); var dump = File.ReadAllText(filePath); //var message = new SentryMessage(dump); // var blob = new SentryEvent(message); var payload = JObject.Parse(dump); // Try to extract a test name var fileName = Path.GetFileName (filePath); var extract = Regex.Match(fileName, @"mono_crash\.([A-Za-z0-9]+)\.(\d)\.json"); if (!extract.Success) throw new Exception ("File name does not match correct format"); var groups = extract.Groups; var hash = groups[1].Value; // var increment = groups[2].Value; var version_string = payload["protocol_version"].ToString(); JObject sentry_message = null; if (version_string == "0.0.4") { // Same for now sentry_message = Format_0_0_3 (filePath, payload, hash); } else if (version_string == "0.0.3") { sentry_message = Format_0_0_3 (filePath, payload, hash); } else if (version_string == "0.0.2") { sentry_message = Format_0_0_2 (filePath, payload, hash); } else { Console.WriteLine ("ERROR: Crash reporting version mismatch"); return; } // sent to url via post? // Console.WriteLine (sentry_message); SendMessage (sentry_message, url); } public Uploader (CodeCollection assemblies) { this.codebase = assemblies; } static string[] GetAssemblies (string fileRoot) { var dlls = Directory.GetFiles (fileRoot, "*.dll", SearchOption.AllDirectories); var exes = Directory.GetFiles (fileRoot, "*.exe", SearchOption.AllDirectories); return dlls.Concat (exes).ToArray (); } static string[] GetFiles (string fileRoot) { return Directory.GetFiles (fileRoot, "mono_crash.*.json", SearchOption.AllDirectories); } public static void Main (string[] args) { var url = System.Environment.GetEnvironmentVariable ("MONO_SENTRY_URL"); if (url == null) { Console.WriteLine ("MONO_SENTRY_URL missing"); return; } var fileRoot = System.Environment.GetEnvironmentVariable ("MONO_SENTRY_ROOT"); if (fileRoot == null) { Console.WriteLine ("MONO_SENTRY_ROOT missing"); return; } var os_tag = System.Environment.GetEnvironmentVariable ("MONO_SENTRY_OS"); if (os_tag == null) { Console.WriteLine ("MONO_SENTRY_OS missing"); return; } var dsn = new Dsn(url); var files = GetFiles (fileRoot); if (files.Length == 0) return; // Find all of the assemblies in tree that could have made the crash dump var assemblies = GetAssemblies (fileRoot); var codebase = new CodeCollection (assemblies); foreach (var file in files) { Console.WriteLine ($"Processing {file} ..."); var state = new Uploader (codebase); state.Upload (file, os_tag, dsn); } } } }