diff --git a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.AddIn/AnalyticsMonitor.cs b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.AddIn/AnalyticsMonitor.cs index 5156e53920..629fcac2a3 100644 --- a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.AddIn/AnalyticsMonitor.cs +++ b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector.AddIn/AnalyticsMonitor.cs @@ -6,14 +6,16 @@ // using System; -using System.Linq; +using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using ICSharpCode.Core; using ICSharpCode.Core.Services; using ICSharpCode.SharpDevelop; +using ICSharpCode.UsageDataCollector.Contracts; namespace ICSharpCode.UsageDataCollector { @@ -78,29 +80,12 @@ namespace ICSharpCode.UsageDataCollector /// public void OpenSession() { + IEnumerable appEnvironmentProperties = GetAppProperties(); bool sessionOpened = false; lock (lockObj) { if (session == null) { try { session = new UsageDataSessionWriter(dbFileName); - session.OnException = MessageService.ShowException; - session.AddEnvironmentData("appVersion", RevisionClass.FullVersion); - session.AddEnvironmentData("language", ResourceService.Language); - session.AddEnvironmentData("culture", CultureInfo.CurrentCulture.Name); - string PROCESSOR_ARCHITECTURE = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432"); - if (string.IsNullOrEmpty(PROCESSOR_ARCHITECTURE)) { - PROCESSOR_ARCHITECTURE = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE"); - } - if (!string.IsNullOrEmpty(PROCESSOR_ARCHITECTURE)) { - session.AddEnvironmentData("architecture", PROCESSOR_ARCHITECTURE); - } - session.AddEnvironmentData("userAddInCount", AddInTree.AddIns.Where(a => !a.IsPreinstalled).Count().ToString()); - - #if DEBUG - session.AddEnvironmentData("debug", "true"); - #endif - - sessionOpened = true; } catch (IncompatibleDatabaseException ex) { if (ex.ActualVersion < ex.ExpectedVersion) { LoggingService.Info("AnalyticsMonitor: " + ex.Message + ", removing old database"); @@ -115,14 +100,43 @@ namespace ICSharpCode.UsageDataCollector LoggingService.Warn("AnalyticsMonitor: " + ex.Message); } } + + if (session != null) { + session.OnException = MessageService.ShowException; + session.AddEnvironmentData(appEnvironmentProperties); + + sessionOpened = true; + } } } if (sessionOpened) { UsageDataUploader uploader = new UsageDataUploader(dbFileName); + uploader.EnvironmentDataForDummySession = appEnvironmentProperties; ThreadPool.QueueUserWorkItem(delegate { uploader.StartUpload(UploadUrl); }); } } + static IEnumerable GetAppProperties() + { + List properties = new List { + new UsageDataEnvironmentProperty { Name = "appVersion", Value = RevisionClass.FullVersion }, + new UsageDataEnvironmentProperty { Name = "language", Value = ResourceService.Language }, + new UsageDataEnvironmentProperty { Name = "culture", Value = CultureInfo.CurrentCulture.Name }, + new UsageDataEnvironmentProperty { Name = "userAddInCount", Value = AddInTree.AddIns.Where(a => !a.IsPreinstalled).Count().ToString() } + }; + string PROCESSOR_ARCHITECTURE = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITEW6432"); + if (string.IsNullOrEmpty(PROCESSOR_ARCHITECTURE)) { + PROCESSOR_ARCHITECTURE = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE"); + } + if (!string.IsNullOrEmpty(PROCESSOR_ARCHITECTURE)) { + properties.Add(new UsageDataEnvironmentProperty { Name = "architecture", Value = PROCESSOR_ARCHITECTURE }); + } + #if DEBUG + properties.Add(new UsageDataEnvironmentProperty { Name = "debug", Value = "true" }); + #endif + return properties; + } + /// /// Retrieves all stored data as XML text. /// diff --git a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataSessionWriter.cs b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataSessionWriter.cs index 452e9c5bb3..222c839965 100644 --- a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataSessionWriter.cs +++ b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataSessionWriter.cs @@ -11,13 +11,17 @@ using System.Data.SQLite; using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Security; +using System.Security.Permissions; using System.Text; using System.Threading; +using ICSharpCode.UsageDataCollector.Contracts; + namespace ICSharpCode.UsageDataCollector { /// @@ -25,7 +29,7 @@ namespace ICSharpCode.UsageDataCollector /// /// This class is thread-safe. /// - public class UsageDataSessionWriter : IDisposable + public sealed class UsageDataSessionWriter : IDisposable { readonly object lockObj = new object(); SQLiteConnection connection; @@ -153,6 +157,19 @@ namespace ICSharpCode.UsageDataCollector } } + /// + /// Gets the default environment data. + /// + public static UsageDataEnvironmentProperty[] GetDefaultEnvironmentData() + { + return new [] { + new UsageDataEnvironmentProperty { Name = "platform", Value = Environment.OSVersion.Platform.ToString() }, + new UsageDataEnvironmentProperty { Name = "osVersion", Value = Environment.OSVersion.Version.ToString() }, + new UsageDataEnvironmentProperty { Name = "processorCount", Value = Environment.ProcessorCount.ToString() }, + new UsageDataEnvironmentProperty { Name = "dotnetRuntime", Value = Environment.Version.ToString() } + }; + } + void StartSession() { using (SQLiteTransaction transaction = this.connection.BeginTransaction()) { @@ -161,10 +178,7 @@ namespace ICSharpCode.UsageDataCollector "SELECT last_insert_rowid();"; sessionID = (long)cmd.ExecuteScalar(); } - AddEnvironmentData("platform", Environment.OSVersion.Platform.ToString()); - AddEnvironmentData("osVersion", Environment.OSVersion.Version.ToString()); - AddEnvironmentData("processorCount", Environment.ProcessorCount.ToString()); - AddEnvironmentData("dotnetRuntime", Environment.Version.ToString()); + AddEnvironmentData(GetDefaultEnvironmentData()); transaction.Commit(); } } @@ -185,22 +199,26 @@ namespace ICSharpCode.UsageDataCollector /// /// Adds environment data to the current session. /// - /// Name of the data entry. - /// Value of the data entry. - public void AddEnvironmentData(string name, string value) + public void AddEnvironmentData(IEnumerable properties) { - if (name == null) - throw new ArgumentNullException("name"); + if (properties == null) + throw new ArgumentNullException("properties"); + UsageDataEnvironmentProperty[] pArray = properties.ToArray(); lock (lockObj) { if (isDisposed) throw new ObjectDisposedException(GetType().Name); - using (SQLiteCommand cmd = this.connection.CreateCommand()) { - cmd.CommandText = "INSERT INTO Environment (session, name, value)" + - " VALUES (?, ?, ?);"; - cmd.Parameters.Add(new SQLiteParameter { Value = sessionID }); - cmd.Parameters.Add(new SQLiteParameter { Value = name }); - cmd.Parameters.Add(new SQLiteParameter { Value = value }); - cmd.ExecuteNonQuery(); + using (SQLiteTransaction transaction = this.connection.BeginTransaction()) { + foreach (UsageDataEnvironmentProperty p in pArray) { + using (SQLiteCommand cmd = this.connection.CreateCommand()) { + cmd.CommandText = "INSERT INTO Environment (session, name, value)" + + " VALUES (?, ?, ?);"; + cmd.Parameters.Add(new SQLiteParameter { Value = sessionID }); + cmd.Parameters.Add(new SQLiteParameter { Value = p.Name }); + cmd.Parameters.Add(new SQLiteParameter { Value = p.Value }); + cmd.ExecuteNonQuery(); + } + } + transaction.Commit(); } } } @@ -482,6 +500,16 @@ namespace ICSharpCode.UsageDataCollector /// public IncompatibleDatabaseException() {} + /// + /// Creates a new IncompatibleDatabaseException instance. + /// + public IncompatibleDatabaseException(string message) : base(message) {} + + /// + /// Creates a new IncompatibleDatabaseException instance. + /// + public IncompatibleDatabaseException(string message, Exception innerException) : base(message, innerException) {} + /// /// Creates a new IncompatibleDatabaseException instance. /// @@ -504,6 +532,7 @@ namespace ICSharpCode.UsageDataCollector } /// + [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)] public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); diff --git a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataUploader.cs b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataUploader.cs index c8b87c9142..3e5b712fc4 100644 --- a/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataUploader.cs +++ b/src/AddIns/Misc/UsageDataCollector/UsageDataCollector/UsageDataUploader.cs @@ -37,6 +37,11 @@ namespace ICSharpCode.UsageDataCollector this.databaseFileName = databaseFileName; } + /// + /// Gets/Sets environment data that is sent with the dummy session on the first upload. + /// + public IEnumerable EnvironmentDataForDummySession { get; set; } + SQLiteConnection OpenConnection() { SQLiteConnectionStringBuilder conn = new SQLiteConnectionStringBuilder(); @@ -60,7 +65,7 @@ namespace ICSharpCode.UsageDataCollector message = FetchDataForUpload(connection, true); } } - using (StringWriter w = new StringWriter()) { + using (StringWriter w = new StringWriter(CultureInfo.InvariantCulture)) { using (XmlTextWriter xmlWriter = new XmlTextWriter(w)) { xmlWriter.Formatting = Formatting.Indented; DataContractSerializer serializer = new DataContractSerializer(typeof(UsageDataMessage)); @@ -93,13 +98,17 @@ namespace ICSharpCode.UsageDataCollector public void StartUpload(Binding binding, EndpointAddress endpoint) { UsageDataMessage message; + bool addDummySession = false; using (SQLiteConnection connection = OpenConnection()) { using (SQLiteTransaction transaction = connection.BeginTransaction()) { CheckDatabaseVersion(connection); - if (HasAlreadyUploadedToday(connection)) { + HasUploadedTodayStatus status = HasAlreadyUploadedToday(connection); + if (status == HasUploadedTodayStatus.Yes) { message = null; } else { message = FetchDataForUpload(connection, false); + if (status == HasUploadedTodayStatus.NeverUploaded) + addDummySession = true; } transaction.Commit(); } @@ -107,6 +116,19 @@ namespace ICSharpCode.UsageDataCollector if (message != null) { string commaSeparatedSessionIDList = GetCommaSeparatedIDList(message.Sessions); + if (addDummySession) { + // A dummy session is used for the first upload to notify the server of the user's environment. + // Without this, we couldn't tell the version of a user who tries SharpDevelop once but doesn't use it long + // enough for an actual session to be uploaded. + UsageDataSession dummySession = new UsageDataSession(); + dummySession.SessionID = 0; + dummySession.StartTime = DateTime.UtcNow; + dummySession.EnvironmentProperties.AddRange(UsageDataSessionWriter.GetDefaultEnvironmentData()); + if (this.EnvironmentDataForDummySession != null) + dummySession.EnvironmentProperties.AddRange(this.EnvironmentDataForDummySession); + message.Sessions.Add(dummySession); + } + DataContractSerializer serializer = new DataContractSerializer(typeof(UsageDataMessage)); byte[] data; using (MemoryStream ms = new MemoryStream()) { @@ -158,19 +180,26 @@ namespace ICSharpCode.UsageDataCollector } } - bool HasAlreadyUploadedToday(SQLiteConnection connection) + enum HasUploadedTodayStatus + { + No, + Yes, + NeverUploaded + } + + static HasUploadedTodayStatus HasAlreadyUploadedToday(SQLiteConnection connection) { using (SQLiteCommand cmd = connection.CreateCommand()) { cmd.CommandText = "SELECT value > datetime('now','-1 day') FROM Properties WHERE name='lastUpload';"; object result = cmd.ExecuteScalar(); if (result == null) - return false; // no lastUpload entry -> DB was never uploaded - return (long)result > 0; + return HasUploadedTodayStatus.NeverUploaded; // no lastUpload entry -> DB was never uploaded + return (long)result > 0 ? HasUploadedTodayStatus.Yes : HasUploadedTodayStatus.No; } } #region FetchDataForUpload - UsageDataMessage FetchDataForUpload(SQLiteConnection connection, bool fetchIncompleteSessions) + static UsageDataMessage FetchDataForUpload(SQLiteConnection connection, bool fetchIncompleteSessions) { UsageDataMessage message = new UsageDataMessage(); // Retrieve the User ID @@ -261,7 +290,7 @@ namespace ICSharpCode.UsageDataCollector } #endregion - string GetCommaSeparatedIDList(IEnumerable sessions) + static string GetCommaSeparatedIDList(IEnumerable sessions) { return string.Join( ",",