diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs index 56fbf8d44d..68b0fa8283 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Providers/TelemetrySolutionInfoEnricher.cs @@ -78,8 +78,6 @@ public class TelemetrySolutionInfoEnricher : ITelemetryActivityDataEnricher, ISi AddSolutionInformation(activityData, root.GetProperty("creatingStudioConfiguration")); - activityData[ActivityPropertyNames.LicenseType] = await GetLicenseTypeAsync(); - if (root.TryGetProperty("modules", out var modulesElement) && modulesElement.ValueKind == JsonValueKind.Object) { @@ -248,32 +246,4 @@ public class TelemetrySolutionInfoEnricher : ITelemetryActivityDataEnricher, ISi }; } - private async Task GetLicenseTypeAsync() - { - if (!File.Exists(TelemetryPaths.AccessToken)) - { - return 0; - } - - try - { - using var httpClient = new HttpClient(); - var accessToken = File.ReadAllText(TelemetryPaths.AccessToken); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - - var response = await httpClient.GetAsync($"{AbpPlatformUrls.AbpIoUrl}api/license/api-key"); - if (!response.IsSuccessStatusCode) - { - return 0; - } - - var content = await response.Content.ReadAsStringAsync(); - using var doc = JsonDocument.Parse(content); - return doc.RootElement.GetProperty("licenseType").GetInt32(); - } - catch - { - return 0; - } - } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs index e2931d730e..1914a56870 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Activity/Storage/TelemetryActivityStorage.cs @@ -1,48 +1,44 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Security.Cryptography; using System.Text; using System.Text.Encodings.Web; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using Volo.Abp.DependencyInjection; using Volo.Abp.Telemetry.Activity.Contracts; using Volo.Abp.Telemetry.Constants; +using Volo.Abp.Telemetry.Helpers; namespace Volo.Abp.Telemetry.Activity.Storage; public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDependency { - private const int MaxFileRetries = 5; - private const int RetryDelayMs = 100; - private const int MutexTimeoutMs = 5000; private const string MutexName = "Global\\TelemetryActivityStorage"; - private const string EncryptionKey = "AbpTelemetryStorageKey"; + private const string TestModeEnvironmentVariable = "ABP_TELEMETRY_TEST_MODE"; - private static TimeSpan _infoExpirationPeriod = TimeSpan.FromDays(7); - private static TimeSpan _activitySendPeriod = TimeSpan.FromDays(1); - + private readonly static TimeSpan DefaultInfoExpirationPeriod = TimeSpan.FromDays(7); + private readonly static TimeSpan DefaultActivitySendPeriod = TimeSpan.FromDays(1); + private readonly static TimeSpan TestModeInfoExpirationPeriod = TimeSpan.FromMinutes(1); + private readonly static TimeSpan TestModeActivitySendPeriod = TimeSpan.FromSeconds(5); + + private readonly TimeSpan _infoExpirationPeriod; + private readonly TimeSpan _activitySendPeriod; private TelemetryActivityStorageState? _cachedState; - public TelemetryActivityStorage() { - var enableTelemetryTestModeVariable = Environment.GetEnvironmentVariable("ABP_TELEMETRY_TEST_MODE" , EnvironmentVariableTarget.User); - if (bool.TryParse(enableTelemetryTestModeVariable, out var enableTelemetryTestMode) && enableTelemetryTestMode) - { - _infoExpirationPeriod = TimeSpan.FromMinutes(1); - _activitySendPeriod = TimeSpan.FromSeconds(5); - } + var isTestMode = IsTestModeEnabled(); + _infoExpirationPeriod = isTestMode ? TestModeInfoExpirationPeriod : DefaultInfoExpirationPeriod; + _activitySendPeriod = isTestMode ? TestModeActivitySendPeriod : DefaultActivitySendPeriod; } public async Task BufferActivityAsync(ActivityData activityData) { - var state = await GetStateAsync(); - state.Activities.Insert(0, activityData); - await SaveAsync(); + await ModifyStateAsync(state => + { + state.Activities.Insert(0, activityData); + }); } public async Task> GetBufferedActivitiesAsync() @@ -53,57 +49,59 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep public async Task EndSessionAsync() { - var state = await GetStateAsync(); - state.SessionId = null; - await SaveAsync(); - } - - private async Task GetLastActivitySendTimeAsync() - { - var state = await GetStateAsync(); - return state.ActivitySendTime; + await ModifyStateAsync(state => + { + state.SessionId = null; + }); } public async Task GetOrCreateSessionAsync() { var state = await GetStateAsync(); - if (state.SessionId is null) + if (state.SessionId.HasValue) { - state.SessionId = Guid.NewGuid(); - await SaveAsync(); + return state.SessionId.Value; } - return state.SessionId.Value; + await ModifyStateAsync(s => + { + s.SessionId = Guid.NewGuid(); + }); + return state.SessionId!.Value; } public async Task MarkActivitiesAsSentAsync() { - var state = await GetStateAsync(); - state.ActivitySendTime = DateTimeOffset.UtcNow; - state.Activities.Clear(); - await SaveAsync(); + await ModifyStateAsync(state => + { + state.ActivitySendTime = DateTimeOffset.UtcNow; + state.Activities.Clear(); + }); } public async Task MarkSolutionInfoAsAddedAsync(Guid solutionId) { - var state = await GetStateAsync(); - state.Solutions[solutionId] = DateTimeOffset.UtcNow; - await SaveAsync(); + await ModifyStateAsync(state => + { + state.Solutions[solutionId] = DateTimeOffset.UtcNow; + }); } public async Task MarkApplicationInfoAsAddedAsync(Guid applicationId) { - var state = await GetStateAsync(); - state.Applications[applicationId] = DateTimeOffset.UtcNow; - await SaveAsync(); + await ModifyStateAsync(state => + { + state.Applications[applicationId] = DateTimeOffset.UtcNow; + }); } public async Task MarkDeviceInfoAsAddedAsync() { - var state = await GetStateAsync(); - state.LastDeviceInfoAddTime = DateTimeOffset.UtcNow; - await SaveAsync(); + await ModifyStateAsync(state => + { + state.LastDeviceInfoAddTime = DateTimeOffset.UtcNow; + }); } public virtual async Task ShouldAddDeviceInfoAsync() @@ -130,6 +128,26 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep return lastActivitySendTime is null || DateTimeOffset.UtcNow - lastActivitySendTime > _activitySendPeriod; } + private static bool IsTestModeEnabled() + { + var testModeVariable = + Environment.GetEnvironmentVariable(TestModeEnvironmentVariable, EnvironmentVariableTarget.User); + return bool.TryParse(testModeVariable, out var isTestMode) && isTestMode; + } + + private async Task ModifyStateAsync(Action modifyAction) + { + var state = await GetStateAsync(); + modifyAction(state); + await SaveAsync(); + } + + private async Task GetLastActivitySendTimeAsync() + { + var state = await GetStateAsync(); + return state.ActivitySendTime; + } + private async Task GetLastSolutionInfoSendTimeAsync(Guid solutionId) { var state = await GetStateAsync(); @@ -148,6 +166,11 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep return state.LastDeviceInfoAddTime; } + private bool ShouldAddInfo(DateTimeOffset? lastSend) + { + return lastSend is null || DateTimeOffset.UtcNow - lastSend > _infoExpirationPeriod; + } + private async Task GetStateAsync() { if (_cachedState != null) @@ -155,195 +178,95 @@ public class TelemetryActivityStorage : ITelemetryActivityStorage, ISingletonDep return _cachedState; } - await EnsureFileExistsAsync(); - _cachedState = await WithExclusiveFileLockAsync(async stream => + EnsureFileExists(); + _cachedState = await LoadStateFromFileAsync() ?? new TelemetryActivityStorageState(); + return _cachedState; + } + + private async Task LoadStateFromFileAsync() + { + return await WithExclusiveFileLockAsync(async stream => { using var reader = new StreamReader(stream, Encoding.UTF8); var encryptedJson = await reader.ReadToEndAsync(); - + if (string.IsNullOrEmpty(encryptedJson)) { return new TelemetryActivityStorageState(); } - - var json = Decrypt(encryptedJson); + + var json = EncryptionHelper.Decrypt(encryptedJson); return JsonSerializer.Deserialize(json, GetJsonSerializerOptions()) ?? new TelemetryActivityStorageState(); - }) ?? new TelemetryActivityStorageState(); - return _cachedState; - } - - private async Task WithExclusiveFileLockAsync(Func> action) - { - return await RetryWithMutexAsync(async () => - { - using var stream = new FileStream( - TelemetryPaths.ActivityStorage, - FileMode.OpenOrCreate, - FileAccess.ReadWrite, - FileShare.Read - ); - return await action(stream); }); } - private async Task RetryWithMutexAsync(Func> operation) - { - using var mutex = new Mutex(false, MutexName); - - for (int attempt = 1; attempt <= MaxFileRetries; attempt++) - { - try - { - if (!await WaitForMutexAsync(mutex)) - { - if (attempt == MaxFileRetries) - { - return default; - } - - await Task.Delay(RetryDelayMs); - continue; - } - - try - { - return await operation(); - } - finally - { - ReleaseMutexSafely(mutex); - } - } - catch (AbandonedMutexException) - { - try - { - return await operation(); - } - catch - { - if (attempt == MaxFileRetries) - { - return default; - } - - await Task.Delay(RetryDelayMs); - } - finally - { - ReleaseMutexSafely(mutex); - } - } - catch (IOException) - { - if (attempt == MaxFileRetries) - { - return default; - } - - await Task.Delay(RetryDelayMs); - } - catch - { - return default; - } - } - - return default; - } - - private async Task WaitForMutexAsync(Mutex mutex) - { - return await Task.Run(() => mutex.WaitOne(MutexTimeoutMs)); - } - - private static void ReleaseMutexSafely(Mutex mutex) - { - try - { - mutex.ReleaseMutex(); - } - catch - { - // Ignore release errors - } - } - private Task SaveAsync() { - var json = JsonSerializer.Serialize(_cachedState ?? new TelemetryActivityStorageState(), GetJsonSerializerOptions()); - var encryptedJson = Encrypt(json); + var state = _cachedState ?? new TelemetryActivityStorageState(); + var json = JsonSerializer.Serialize(state, GetJsonSerializerOptions()); + var encryptedJson = EncryptionHelper.Encrypt(json); + File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8); return Task.CompletedTask; } - private Task EnsureFileExistsAsync() + private void EnsureFileExists() { try { - var directory = Path.GetDirectoryName(TelemetryPaths.ActivityStorage); - - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory!); - } + EnsureDirectoryExists(); if (!File.Exists(TelemetryPaths.ActivityStorage)) { - var json = JsonSerializer.Serialize(_cachedState ?? new TelemetryActivityStorageState(), GetJsonSerializerOptions()); - var encryptedJson = Encrypt(json); - File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8); + CreateInitialFile(); } } catch { // Ignored intentionally } - - return Task.CompletedTask; } - private static JsonSerializerOptions GetJsonSerializerOptions() + private void EnsureDirectoryExists() { - return new JsonSerializerOptions + var directory = Path.GetDirectoryName(TelemetryPaths.ActivityStorage); + if (!Directory.Exists(directory)) { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true, - Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping - }; + Directory.CreateDirectory(directory!); + } } - private bool ShouldAddInfo(DateTimeOffset? lastSend) + private void CreateInitialFile() { - return lastSend is null || DateTimeOffset.UtcNow - lastSend > _infoExpirationPeriod; + var initialState = _cachedState ?? new TelemetryActivityStorageState(); + var json = JsonSerializer.Serialize(initialState, GetJsonSerializerOptions()); + var encryptedJson = EncryptionHelper.Encrypt(json); + File.WriteAllText(TelemetryPaths.ActivityStorage, encryptedJson, Encoding.UTF8); } - private static string Encrypt(string plainText) + private async Task WithExclusiveFileLockAsync(Func> action) { - using var aes = Aes.Create(); - using var sha256 = SHA256.Create(); - aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.PKCS7; - - var encryptor = aes.CreateEncryptor(); - var inputBytes = Encoding.UTF8.GetBytes(plainText); - var encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); - return Convert.ToBase64String(encryptedBytes); + return await MutexExecutor.ExecuteAsync(MutexName, async () => + { + using var stream = new FileStream( + TelemetryPaths.ActivityStorage, + FileMode.OpenOrCreate, + FileAccess.ReadWrite, + FileShare.Read + ); + return await action(stream); + }); } - private static string Decrypt(string cipherText) + + private static JsonSerializerOptions GetJsonSerializerOptions() { - using var aes = Aes.Create(); - using var sha256 = SHA256.Create(); - aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.PKCS7; - - var decryptor = aes.CreateDecryptor(); - var inputBytes = Convert.FromBase64String(cipherText); - var decryptedBytes = decryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); - return Encoding.UTF8.GetString(decryptedBytes); + return new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }; } } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityNameConsts.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityNameConsts.cs index fbc1913234..338cb426ba 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityNameConsts.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityNameConsts.cs @@ -38,6 +38,7 @@ public static class ActivityNameConsts public const string AbpStudioSolutionRunnerAddProfile = "AbpStudio.SolutionRunner.Add.Profile"; public const string AbpStudioSolutionRunnerAppManageMetadata = "AbpStudio.SolutionRunner.App.Manage.Metadata"; public const string AbpStudioSolutionRunnerAppManageSecrets = "AbpStudio.SolutionRunner.App.Manage.Secrets"; + public const string AbpStudioSolutionOpen = "AbpStudio.Solution.Open"; public const string AbpStudioMonitoringBrowse = "AbpStudio.Monitoring.Browse"; public const string AbpStudioMonitoringHttpRequests = "AbpStudio.Monitoring.HttpRequests"; public const string AbpStudioMonitoringHttpRequestsDetail = "AbpStudio.Monitoring.HttpRequests.Detail"; diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityPropertyNames.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityPropertyNames.cs index 7c156c634b..0e225c457e 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityPropertyNames.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Constants/ActivityPropertyNames.cs @@ -53,9 +53,9 @@ public static class ActivityPropertyNames public const string ActivityDetails = nameof(ActivityDetails); public const string ApplicationId = nameof(ApplicationId); public const string Time = nameof(Time); - public const string SoftwareName = nameof(SoftwareName); - public const string SoftwareVersion = nameof(SoftwareVersion); - public const string SoftwareUiTheme = nameof(SoftwareUiTheme); + public const string SoftwareName = "Name"; + public const string SoftwareVersion = "Version"; + public const string SoftwareUiTheme = "UiTheme"; public const string SoftwareType = nameof(SoftwareType); public const string UserDeviceId = nameof(UserDeviceId); public const string SolutionTemplate = nameof(SolutionTemplate); @@ -76,5 +76,4 @@ public static class ActivityPropertyNames public const string ApplicationType = nameof(ApplicationType); public const string AbpModuleCount = nameof(AbpModuleCount); public const string ExtraProperties = nameof(ExtraProperties); - } \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Helpers/EncryptionHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Helpers/EncryptionHelper.cs new file mode 100644 index 0000000000..f9c49e4361 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Helpers/EncryptionHelper.cs @@ -0,0 +1,40 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Volo.Abp.Telemetry.Helpers; + +static internal class EncryptionHelper +{ + private const string EncryptionKey = "AbpTelemetryStorageKey"; + + public static string Encrypt(string plainText) + { + using var aes = Aes.Create(); + using var sha256 = SHA256.Create(); + + aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.PKCS7; + + var encryptor = aes.CreateEncryptor(); + var inputBytes = Encoding.UTF8.GetBytes(plainText); + var encryptedBytes = encryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); + return Convert.ToBase64String(encryptedBytes); + } + + public static string Decrypt(string cipherText) + { + using var aes = Aes.Create(); + using var sha256 = SHA256.Create(); + + aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(EncryptionKey)); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.PKCS7; + + var decryptor = aes.CreateDecryptor(); + var inputBytes = Convert.FromBase64String(cipherText); + var decryptedBytes = decryptor.TransformFinalBlock(inputBytes, 0, inputBytes.Length); + return Encoding.UTF8.GetString(decryptedBytes); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Helpers/MutexExecutor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Helpers/MutexExecutor.cs new file mode 100644 index 0000000000..97176af56e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Telemetry/Helpers/MutexExecutor.cs @@ -0,0 +1,75 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Volo.Abp.Telemetry.Helpers; + +static internal class MutexExecutor +{ + private const int MaxRetries = 5; + private const int RetryDelay = 100; + private const int Timeout = 5000; + + public async static Task ExecuteAsync(string mutexName, Func> action) + { + using var mutex = new Mutex(false, mutexName); + + for (int attempt = 1; attempt <= MaxRetries; attempt++) + { + if (!await WaitAsync(mutex)) + { + if (attempt == MaxRetries) + { + return default; + } + + await Task.Delay(RetryDelay); + continue; + } + + try + { + return await action(); + } + catch (AbandonedMutexException) + { + if (attempt == MaxRetries) + { + return default; + } + + await Task.Delay(RetryDelay); + } + catch (IOException) + { + if (attempt == MaxRetries) + { + return default; + } + + await Task.Delay(RetryDelay); + } + finally + { + ReleaseSafely(mutex); + } + } + + return default; + } + + private async static Task WaitAsync(Mutex mutex) + { + return await Task.Run(() => mutex.WaitOne(Timeout)); + } + + private static void ReleaseSafely(Mutex mutex) + { + try { mutex.ReleaseMutex(); } + catch + { + //ignored + } + } +} \ No newline at end of file