From dc67d69bd2bd2f9653fb1fffec1812c8324bc828 Mon Sep 17 00:00:00 2001 From: Andrey Shabarshov Date: Sat, 8 Jul 2023 15:34:33 +0100 Subject: [PATCH] DEEP-26 Load Generator added. --- QRBee.Load.Generator/ClientPool.cs | 32 ++ QRBee.Load.Generator/ClientSettings.cs | 68 +++++ QRBee.Load.Generator/GeneratorSettings.cs | 21 ++ QRBee.Load.Generator/LoadGenerator.cs | 273 ++++++++++++++++++ .../PaymentRequestGenerator.cs | 111 +++++++ QRBee.Load.Generator/PrivateKeyHandler.cs | 17 ++ QRBee.Load.Generator/Program.cs | 42 +++ .../QRBee.Load.Generator.csproj | 61 ++++ QRBee.Load.Generator/ThreadSafeRandom.cs | 47 +++ .../appsettings.Development.json | 8 + QRBee.Load.Generator/appsettings.json | 38 +++ QRBee.Load.Generator/log4net.config | 55 ++++ QRBee.sln | 14 + 13 files changed, 787 insertions(+) create mode 100644 QRBee.Load.Generator/ClientPool.cs create mode 100644 QRBee.Load.Generator/ClientSettings.cs create mode 100644 QRBee.Load.Generator/GeneratorSettings.cs create mode 100644 QRBee.Load.Generator/LoadGenerator.cs create mode 100644 QRBee.Load.Generator/PaymentRequestGenerator.cs create mode 100644 QRBee.Load.Generator/PrivateKeyHandler.cs create mode 100644 QRBee.Load.Generator/Program.cs create mode 100644 QRBee.Load.Generator/QRBee.Load.Generator.csproj create mode 100644 QRBee.Load.Generator/ThreadSafeRandom.cs create mode 100644 QRBee.Load.Generator/appsettings.Development.json create mode 100644 QRBee.Load.Generator/appsettings.json create mode 100644 QRBee.Load.Generator/log4net.config diff --git a/QRBee.Load.Generator/ClientPool.cs b/QRBee.Load.Generator/ClientPool.cs new file mode 100644 index 0000000..b9e1673 --- /dev/null +++ b/QRBee.Load.Generator/ClientPool.cs @@ -0,0 +1,32 @@ +using QRBee.Core.Client; +using System.Collections.Concurrent; + +namespace QRBee.Load.Generator; + +public class ClientPool +{ + private readonly ConcurrentDictionary _clientPool = new(); + private readonly ConcurrentDictionary _merchantPool = new(); + private readonly SecurityServiceFactory _securityServiceFactory; + private readonly Client _client; + + public ClientPool(SecurityServiceFactory securityServiceFactory, QRBee.Core.Client.Client client) + { + _securityServiceFactory = securityServiceFactory; + this._client = client; + } + + public async Task GetMerchant(int no) + { + var merchant = _merchantPool.GetOrAdd(no+100_000_000, x => new ClientSettings(_securityServiceFactory, no, true)); + await merchant.InitialSetup(_client); + return merchant; + } + + public async Task GetClient(int no) + { + var customer = _clientPool.GetOrAdd(no, x => new ClientSettings(_securityServiceFactory, no, false)); + await customer.InitialSetup(_client); + return customer; + } +} diff --git a/QRBee.Load.Generator/ClientSettings.cs b/QRBee.Load.Generator/ClientSettings.cs new file mode 100644 index 0000000..ab99c1b --- /dev/null +++ b/QRBee.Load.Generator/ClientSettings.cs @@ -0,0 +1,68 @@ +using QRBee.Core.Data; +using QRBee.Core.Security; + +namespace QRBee.Load.Generator; + +public class ClientSettings +{ + private ISecurityService _securityService; + + public ClientSettings(SecurityServiceFactory securityServiceFactory, int no, bool isMerchant) + { + Id = no; + IsMerchant = isMerchant; + CardNumber = IsMerchant ? "": $"123400000000{no:0000}"; + CardHolderName = IsMerchant ? $"Merchant {no}" : $"Mr {no}"; + CVC = IsMerchant ? "" : $"{no:000}"; + ExpirationDate = IsMerchant ? "" : (DateTime.Now.Date + TimeSpan.FromDays(364)).ToString("yyyy-MM"); + ValidFrom = IsMerchant ? "" : (DateTime.Now.Date - TimeSpan.FromDays(7)).ToString("yyyy-MM"); + Email = IsMerchant ? $"{no}@merchant.org" : $"{no}@client.org"; + + _securityService = securityServiceFactory(no); + } + + public int Id { get; } + public string? ClientId { get; private set; } + public string CardNumber { get; } + public string CardHolderName { get; } + public string CVC { get; } + public int? IssueNo { get; } + public string? ExpirationDate { get; } + public string? ValidFrom { get; } + + public bool IsMerchant { get; } + public string Email { get; } + public ISecurityService SecurityService { get => _securityService; } + + public async Task InitialSetup(QRBee.Core.Client.Client client) + { + var idFileName = Environment.ExpandEnvironmentVariables($"%TEMP%/!QRBee/QRBee-{Id:8X}.txt"); + + if (File.Exists(idFileName)) + { + var l = File.ReadAllLines(idFileName); + if (l != null && l.Length > 0) + ClientId = l[0]; + } + + if (!string.IsNullOrWhiteSpace(ClientId) && SecurityService.PrivateKeyHandler.Exists()) + return; + + var request = new RegistrationRequest + { + Name = CardHolderName, + Email = Email, + DateOfBirth = "2000-01-01", + RegisterAsMerchant = IsMerchant, + CertificateRequest = SecurityService.PrivateKeyHandler.CreateCertificateRequest(Email) + }; + + var resp = await client.RegisterAsync(request); + _securityService.APIServerCertificate = _securityService.Deserialize(resp.APIServerCertificate); + + ClientId = resp.ClientId; + File.WriteAllText(idFileName, ClientId); + + } + +} diff --git a/QRBee.Load.Generator/GeneratorSettings.cs b/QRBee.Load.Generator/GeneratorSettings.cs new file mode 100644 index 0000000..f0c5359 --- /dev/null +++ b/QRBee.Load.Generator/GeneratorSettings.cs @@ -0,0 +1,21 @@ +namespace QRBee.Load.Generator; + +internal class Anomaly +{ + public double Probability { get; set; } + public Dictionary Parameters { get; set; } = new(); +} + +internal class GeneratorSettings +{ + public int NumberOfClients { get; set; } = 100; + public int NumberOfMerchants { get; set; } = 10; + public int NumberOfThreads { get; set; } = 20; + public int DelayBetweenMessagesMSec { get; set; } = 100; + public int DelayJitterMSec { get; set; } = 50; + public double MinAmount { get; set; } = 10; + public double MaxAmount { get; set; } = 100; + + public Anomaly LoadSpike { get; set; } = new(); + public Anomaly LargeAmount { get; set; } = new(); +} diff --git a/QRBee.Load.Generator/LoadGenerator.cs b/QRBee.Load.Generator/LoadGenerator.cs new file mode 100644 index 0000000..301b1cd --- /dev/null +++ b/QRBee.Load.Generator/LoadGenerator.cs @@ -0,0 +1,273 @@ +// See https://aka.ms/new-console-template for more information +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using QRBee.Core.Client; +using QRBee.Core.Data; +using QRBee.Load.Generator; + +internal class LoadGenerator : IHostedService +{ + private readonly Client _client; + private readonly ClientPool _clientPool; + private readonly PaymentRequestGenerator _paymentRequestGenerator; + private readonly ILogger _logger; + private readonly IOptions _settings; + + public LoadGenerator( + QRBee.Core.Client.Client client, + ClientPool clientPool, + PaymentRequestGenerator paymentRequestGenerator, + ILogger logger, + IOptions settings + ) + { + _client = client; + _clientPool = clientPool; + _paymentRequestGenerator = paymentRequestGenerator; + _logger = logger; + _settings = settings; + } + public async Task StartAsync(CancellationToken cancellationToken) + { + await InitClients(); + _ = Task.Run(ReportingThread); + _ = Task.Run(ConfirmationThread); + _ = Task.Run(ReceivingThread); + _ = Task.Run(GenerateLoad); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + private async Task InitClients() + { + _logger.LogInformation($"Initializing {_settings.Value.NumberOfClients} clients..."); + for ( var i = 1; i < _settings.Value.NumberOfClients+1; i++ ) + { + await _clientPool.GetClient(i); + } + _logger.LogInformation($"Initializing {_settings.Value.NumberOfMerchants} merchants..."); + for (var i = 1; i < _settings.Value.NumberOfMerchants + 1; i++) + { + await _clientPool.GetMerchant(i); + } + _logger.LogInformation($"=== Initialization complete ==="); + } + + private async Task ReportingThread() + { + DateTime nextReport = DateTime.MinValue; + + while (true) + { + if (DateTime.Now > nextReport) + { + _logger.LogInformation($"S: {_paymentsGenerated,10:N0} R: {_paymentsProcessed,10:N0} C: {_paymentsConfirmed,10:N0} F: {_paymentsFailed,10:N0}"); + nextReport = DateTime.Now + TimeSpan.FromSeconds(1); + } + await Task.Delay(1000); + } + } + + private async Task GenerateLoad() + { + _logger.LogInformation("Generator started"); + + var threadTasks = Enumerable.Range(0, _settings.Value.NumberOfThreads) + .Select(_ => GenerationThread()) + .ToArray(); + + await Task.WhenAll(threadTasks); + + _logger.LogInformation("Generator finished"); + } + + private List> _responseQueue = new(); + private List _confirmationQueue = new(); + private ThreadSafeRandom _rng = new(); + private object _lock = new(); + + private long _paymentsGenerated; + private long _paymentsProcessed; + private long _paymentsConfirmed; + private long _paymentsFailed; + + private async Task ConfirmationThread() + { + while (true) + { + try + { + var newQueue = new List(); + + lock (_lock) + newQueue = Interlocked.Exchange(ref _confirmationQueue, newQueue); + + if (newQueue.Count == 0) + { + await Task.Delay( _rng.NextInRange(300, 600)); + continue; + } + + var tasks = newQueue.ToList(); + + while (tasks.Any(x => !x.IsCompleted)) + { + try + { + var t = await Task.WhenAny(tasks); + tasks.Remove(t); + + Interlocked.Increment(ref _paymentsConfirmed); + } + catch (Exception ex) + { + Interlocked.Increment(ref _paymentsFailed); + _logger.LogError(ex, "Confirmation thread"); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Receining thread"); + } + } + } + + + private async Task ReceivingThread() + { + while (true) + { + try + { + var newQueue = new List>(); + + lock (_lock) + newQueue = Interlocked.Exchange(ref _responseQueue, newQueue); + + if (newQueue.Count == 0) + { + await Task.Delay(_rng.NextInRange(300, 600)); + continue; + } + + var tasks = newQueue.ToList(); + + while ( tasks.Any() ) + { + try + { + var t = await Task.WhenAny(tasks); + tasks.Remove(t); + + var res = await t; + + Interlocked.Increment(ref _paymentsProcessed); + + if (res?.Success ?? false) + { + var paymentConfirmation = new PaymentConfirmation + { + MerchantId = res.PaymentRequest.ClientResponse.MerchantRequest.MerchantId, + MerchantTransactionId = res.PaymentRequest.ClientResponse.MerchantRequest.MerchantTransactionId, + GatewayTransactionId = res.GatewayTransactionId + }; + + var confirmationTask = _client.ConfirmPayAsync(paymentConfirmation); + _confirmationQueue.Add(confirmationTask); + } + else + Interlocked.Increment(ref _paymentsFailed); + } + catch (Exception ex) + { + Interlocked.Increment(ref _paymentsFailed); + _logger.LogError(ex, "Receining thread (confirmation)"); + } + } + } + catch ( Exception ex ) + { + _logger.LogError(ex, "Receining thread"); + } + } + } + + + private async Task GenerationThread() + { + + // initial delay + await Task.Delay(500 + _rng.Next() % 124); + + var spikeEnd = DateTime.MinValue; + var loadSpike = _settings.Value.LoadSpike; + var spikeDuration = TimeSpan.Zero; + var spikeDelay = TimeSpan.Zero; + var spikeProbability = loadSpike?.Probability ?? 0.0; + + if ( loadSpike != null && loadSpike.Probability > 0.0 ) + { + if ( !loadSpike.Parameters.TryGetValue("Duration", out var duration) + || !TimeSpan.TryParse( duration, out spikeDuration ) ) + { + spikeProbability = 0.0; + } + else + { + if (!loadSpike.Parameters.TryGetValue("Delay", out duration) + || !TimeSpan.TryParse(duration, out spikeDelay)) + { + spikeDelay = TimeSpan.FromMilliseconds(10); + } + } + } + + while (true) + { + try + { + var req = await _paymentRequestGenerator.GeneratePaymentRequest( + _rng.NextInRange(1, _settings.Value.NumberOfClients + 1), + _rng.NextInRange(1, _settings.Value.NumberOfMerchants + 1) + ); + + var resp = _client.PayAsync(req); + + _responseQueue.Add(resp); + Interlocked.Increment(ref _paymentsGenerated); + } + catch (Exception ex) + { + Interlocked.Increment(ref _paymentsFailed); + _logger.LogError(ex, "Generation thread"); + } + + if (DateTime.Now > spikeEnd) + { + if (loadSpike != null && _rng.NextDouble() < spikeProbability) + { + // start load spike + spikeEnd = DateTime.Now + spikeDuration; + _logger.LogWarning($"Anomaly: Load spike until {spikeEnd}"); + await Task.Delay(spikeDuration); + } + else + { + await Task.Delay(_rng.NextInRange( + _settings.Value.DelayBetweenMessagesMSec, + _settings.Value.DelayBetweenMessagesMSec + _settings.Value.DelayJitterMSec + )); + } + } + else + { + await Task.Delay(spikeDuration); + } + } + } +} \ No newline at end of file diff --git a/QRBee.Load.Generator/PaymentRequestGenerator.cs b/QRBee.Load.Generator/PaymentRequestGenerator.cs new file mode 100644 index 0000000..a35db10 --- /dev/null +++ b/QRBee.Load.Generator/PaymentRequestGenerator.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using QRBee.Core.Data; +using System.Text; + +namespace QRBee.Load.Generator; + +internal class PaymentRequestGenerator +{ + private readonly ClientPool _clientPool; + private readonly ILogger _logger; + private ThreadSafeRandom _rng = new ThreadSafeRandom(); + private double _minAmount = 1; + private double _maxAmount = 100; + private double _largeAmountProbability; + private double _largeAmountValue; + + public PaymentRequestGenerator(ClientPool clientPool, IOptions settings, ILogger logger) + { + _clientPool = clientPool; + _logger = logger; + _minAmount = settings.Value.MinAmount; + _maxAmount = settings.Value.MaxAmount; + + var largeAmount = settings.Value.LargeAmount; + _largeAmountProbability = largeAmount.Probability; + if (_largeAmountProbability > 0) + { + if ( largeAmount.Parameters.TryGetValue("Value", out var s)) + _largeAmountValue = Double.Parse(s); + } + + if (_largeAmountValue <= 0.0) + _largeAmountProbability = 0.0; + } + + public async Task GeneratePaymentRequest(int clientId, int merchantId) + { + var merchant = await GetMerchant(merchantId); + var merchantReq = new MerchantToClientRequest() + { + MerchantId = merchant.ClientId, + MerchantTransactionId = Guid.NewGuid().ToString(), + Name = merchant.CardHolderName, + Amount = GetAmount(), + TimeStampUTC = DateTime.UtcNow + }; + + var merchantSignature = merchant.SecurityService.Sign(Encoding.UTF8.GetBytes(merchantReq.AsDataForSignature())); + merchantReq.MerchantSignature = Convert.ToBase64String(merchantSignature); + + var clientResp = await CreateClientResponse(merchantReq, clientId); + + var req = new PaymentRequest() + { + ClientResponse = clientResp + }; + + return req; + } + + private async Task CreateClientResponse(MerchantToClientRequest merchantReq, int clientId) + { + var client = await GetClient(clientId); + + var response = new ClientToMerchantResponse + { + ClientId = client.ClientId, + TimeStampUTC = DateTime.UtcNow, + MerchantRequest = merchantReq, + EncryptedClientCardData = EncryptCardData(client, merchantReq.MerchantTransactionId) + }; + + var clientSignature = client.SecurityService.Sign(Encoding.UTF8.GetBytes(response.AsDataForSignature())); + response.ClientSignature = Convert.ToBase64String(clientSignature); + + return response; + } + + private decimal GetAmount() + { + if (_rng.NextDouble() < _largeAmountProbability) + { + _logger.LogWarning($"Anomaly: Large amount"); + return Convert.ToDecimal(_rng.NextDoubleInRange(_largeAmountValue, _largeAmountValue* 1.10)); + } + return Convert.ToDecimal(_rng.NextDoubleInRange(_minAmount, _maxAmount)); + } + + private Task GetMerchant(int id) => _clientPool.GetMerchant(id); + + private Task GetClient(int id) => _clientPool.GetClient(id); + + private string EncryptCardData(ClientSettings settings, string transactionId) + { + var clientCardData = new ClientCardData + { + TransactionId = transactionId, + CardNumber = settings.CardNumber, + ExpirationDateYYYYMM = string.IsNullOrWhiteSpace(settings.ExpirationDate) ? null : DateTime.Parse(settings.ExpirationDate).ToString("yyyy-MM"), + ValidFromYYYYMM = string.IsNullOrWhiteSpace(settings.ValidFrom) ? null : DateTime.Parse(settings.ValidFrom).ToString("yyyy-MM"), + CardHolderName = settings.CardHolderName, + CVC = settings.CVC, + IssueNo = settings.IssueNo + }; + + var bytes = settings.SecurityService.Encrypt(Encoding.UTF8.GetBytes(clientCardData.AsString()), settings.SecurityService.APIServerCertificate); + return Convert.ToBase64String(bytes); + } + +} diff --git a/QRBee.Load.Generator/PrivateKeyHandler.cs b/QRBee.Load.Generator/PrivateKeyHandler.cs new file mode 100644 index 0000000..a13ca05 --- /dev/null +++ b/QRBee.Load.Generator/PrivateKeyHandler.cs @@ -0,0 +1,17 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using QRBee.Api.Services; +using QRBee.Core.Security; + +namespace QRBee.Load.Generator; + +public delegate IPrivateKeyHandler PrivateKeyHandlerFactory(int keyNo); +public delegate ISecurityService SecurityServiceFactory(int keyNo); + +internal class PrivateKeyHandler : ServerPrivateKeyHandler +{ + public PrivateKeyHandler(ILogger logger, IConfiguration config, int keyNo) : base(logger, config) + { + PrivateKeyFileName = Environment.ExpandEnvironmentVariables($"%TEMP%/!QRBee/QRBee-{keyNo:8X}.key"); + } +} diff --git a/QRBee.Load.Generator/Program.cs b/QRBee.Load.Generator/Program.cs new file mode 100644 index 0000000..6dd2df2 --- /dev/null +++ b/QRBee.Load.Generator/Program.cs @@ -0,0 +1,42 @@ +// See https://aka.ms/new-console-template for more information +using log4net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using QRBee.Api.Services; +using QRBee.Droid.Services; +using QRBee.Load.Generator; +using Microsoft.Extensions.Configuration; + +Console.WriteLine("=== QRBee artificaial load generator ==="); + +var builder = Host.CreateDefaultBuilder(); + +builder.ConfigureServices((context, services) => +{ + services.AddLogging(logging => + { + logging.ClearProviders(); + GlobalContext.Properties["LOGS_ROOT"] = Environment.GetEnvironmentVariable("LOGS_ROOT") ?? ""; + System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance); + logging.AddLog4Net("log4net.config"); + }); + + services + .AddHttpClient(httpClient => new QRBee.Core.Client.Client("https://localhost:7000/", httpClient)) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler() { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }); + ; + services + .Configure(context.Configuration.GetSection("GeneratorSettings")) + .AddSingleton() + .AddSingleton() + .AddSingleton(x => no => new PrivateKeyHandler(x.GetRequiredService>(), x.GetRequiredService(), no)) + .AddSingleton(x => no => new AndroidSecurityService(x.GetRequiredService()(no))) + .AddHostedService() + ; +}); + + + +var host = builder.Build(); +host.Run(); diff --git a/QRBee.Load.Generator/QRBee.Load.Generator.csproj b/QRBee.Load.Generator/QRBee.Load.Generator.csproj new file mode 100644 index 0000000..58d37aa --- /dev/null +++ b/QRBee.Load.Generator/QRBee.Load.Generator.csproj @@ -0,0 +1,61 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + + + + PreserveNewest + + + + diff --git a/QRBee.Load.Generator/ThreadSafeRandom.cs b/QRBee.Load.Generator/ThreadSafeRandom.cs new file mode 100644 index 0000000..dde962a --- /dev/null +++ b/QRBee.Load.Generator/ThreadSafeRandom.cs @@ -0,0 +1,47 @@ +namespace QRBee.Load.Generator; + +/// +/// https://stackoverflow.com/questions/3049467/is-c-sharp-random-number-generator-thread-safe +/// +public class ThreadSafeRandom +{ + private static readonly Random _global = new Random(); + [ThreadStatic] private static Random? _local; + + public int Next() + { + Init(); + return _local!.Next(); + } + + public double NextDouble() + { + Init(); + return _local!.NextDouble(); + } + + public int NextInRange(int start, int end) + { + var n = Next(); + return start + n % (end - start); + } + + public double NextDoubleInRange(double min, double max) + { + var n = NextDouble(); + return min + (max - min) * n; + } + + private static void Init() + { + if (_local == null) + { + int seed; + lock (_global) + { + seed = _global.Next(); + } + _local = new Random(seed); + } + } +} \ No newline at end of file diff --git a/QRBee.Load.Generator/appsettings.Development.json b/QRBee.Load.Generator/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/QRBee.Load.Generator/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/QRBee.Load.Generator/appsettings.json b/QRBee.Load.Generator/appsettings.json new file mode 100644 index 0000000..9b9a11d --- /dev/null +++ b/QRBee.Load.Generator/appsettings.json @@ -0,0 +1,38 @@ +{ + "PrivateKey" : { + "FileName": "private_key.p12", + "Password": "" + }, + + "Logging": { + "LogLevel": { + "Default": "Trace", + "Microsoft.AspNetCore": "Information" + } + }, + + "GeneratorSettings": { + "NumberOfClients": 100, + "NumberOfMerchants": 10, + "NumberOfThreads": 20, + "DelayBetweenMessagesMSec": 100, + "DelayJitterMSec": 50, + "MinAmount": 10, + "MaxAmount": 100, + + "LoadSpike": { + "Probability": 0.001, + "Parameters": { + "Duration": "00:00:05", + "Delay": "00:00:00.0100000" + } + }, + + "LargeAmount": { + "Probability": 0.03, + "Parameters": { + "Valie": "1_000" + } + } + } +} diff --git a/QRBee.Load.Generator/log4net.config b/QRBee.Load.Generator/log4net.config new file mode 100644 index 0000000..ac296f7 --- /dev/null +++ b/QRBee.Load.Generator/log4net.config @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/QRBee.sln b/QRBee.sln index b7506dd..a290818 100644 --- a/QRBee.sln +++ b/QRBee.sln @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRBee.Core", "QRBee.Core\QR EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QRBee.Tests", "QRBee.Tests\QRBee.Tests.csproj", "{C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QRBee.Load.Generator", "QRBee.Load.Generator\QRBee.Load.Generator.csproj", "{656BE747-ADE3-4E85-A70C-13167E716260}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -95,6 +97,18 @@ Global {C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhone.Build.0 = Release|Any CPU {C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {C48223E7-4AEF-4F6B-A8A0-DDE16B7111DA}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Debug|Any CPU.Build.0 = Debug|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhone.Build.0 = Debug|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Release|Any CPU.ActiveCfg = Release|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Release|Any CPU.Build.0 = Release|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhone.ActiveCfg = Release|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhone.Build.0 = Release|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {656BE747-ADE3-4E85-A70C-13167E716260}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE