From 6263791dffacacc0ad267379dba6c2ebe029c407 Mon Sep 17 00:00:00 2001 From: BlubbFish Date: Wed, 4 Dec 2019 18:57:18 +0100 Subject: [PATCH] Init... --- .gitignore | 11 + LICENSE | 31 + README.md | 7 +- .../Collections/CollectionCacheRepository.cs | 52 + .../Collections/ComponentCollection`1.cs | 76 ++ .../Collections/ConcurrentDataDictionary`2.cs | 304 ++++++ Swan.Lite/Collections/DataDictionary`2.cs | 341 +++++++ .../DisposableComponentCollection`1.cs | 49 + .../Collections/IComponentCollection`1.cs | 54 ++ Swan.Lite/Collections/IDataDictionary`2.cs | 34 + Swan.Lite/Configuration/ConfiguredObject.cs | 77 ++ .../Configuration/PropertyDisplayAttribute.cs | 54 ++ Swan.Lite/Configuration/SettingsProvider.cs | 184 ++++ Swan.Lite/Cryptography/Hasher.cs | 124 +++ Swan.Lite/DateTimeSpan.cs | 174 ++++ Swan.Lite/Definitions.Types.cs | 138 +++ Swan.Lite/Definitions.cs | 39 + Swan.Lite/Diagnostics/Benchmark.cs | 121 +++ Swan.Lite/Diagnostics/BenchmarkUnit.cs | 50 + Swan.Lite/Diagnostics/HighResolutionTimer.cs | 32 + Swan.Lite/EnumHelper.cs | 171 ++++ Swan.Lite/Enums.cs | 65 ++ Swan.Lite/Extensions.ByteArrays.cs | 504 ++++++++++ Swan.Lite/Extensions.ComponentCollections.cs | 21 + Swan.Lite/Extensions.Dates.cs | 234 +++++ Swan.Lite/Extensions.Dictionaries.cs | 86 ++ Swan.Lite/Extensions.Exceptions.cs | 50 + Swan.Lite/Extensions.Functional.cs | 179 ++++ Swan.Lite/Extensions.Reflection.cs | 455 +++++++++ Swan.Lite/Extensions.Strings.cs | 405 ++++++++ Swan.Lite/Extensions.Tasks.cs | 63 ++ Swan.Lite/Extensions.ValueTypes.cs | 165 ++++ Swan.Lite/Extensions.cs | 276 ++++++ Swan.Lite/Formatters/CsvReader.cs | 648 +++++++++++++ Swan.Lite/Formatters/CsvWriter.cs | 459 +++++++++ Swan.Lite/Formatters/HumanizeJson.cs | 150 +++ Swan.Lite/Formatters/Json.Converter.cs | 338 +++++++ Swan.Lite/Formatters/Json.Deserializer.cs | 348 +++++++ Swan.Lite/Formatters/Json.Serializer.cs | 348 +++++++ .../Formatters/Json.SerializerOptions.cs | 144 +++ Swan.Lite/Formatters/Json.cs | 379 ++++++++ Swan.Lite/Formatters/JsonPropertyAttribute.cs | 39 + Swan.Lite/FromString.cs | 344 +++++++ Swan.Lite/Logging/ConsoleLogger.cs | 146 +++ Swan.Lite/Logging/DebugLogger.cs | 53 ++ Swan.Lite/Logging/FileLogger.cs | 134 +++ Swan.Lite/Logging/ILogger.cs | 24 + Swan.Lite/Logging/LogLevel.cs | 43 + .../Logging/LogMessageReceivedEventArgs.cs | 131 +++ Swan.Lite/Logging/Logger.cs | 654 +++++++++++++ Swan.Lite/Logging/TextLogger.cs | 89 ++ Swan.Lite/Mappers/CopyableAttribute.cs | 13 + Swan.Lite/Mappers/IObjectMap.cs | 27 + Swan.Lite/Mappers/ObjectMap.cs | 115 +++ .../ObjectMapper.PropertyInfoComparer.cs | 24 + Swan.Lite/Mappers/ObjectMapper.cs | 372 ++++++++ Swan.Lite/ObjectComparer.cs | 193 ++++ Swan.Lite/Paginator.cs | 99 ++ Swan.Lite/Parsers/ArgumentOptionAttribute.cs | 102 ++ Swan.Lite/Parsers/ArgumentParse.Validator.cs | 160 ++++ .../Parsers/ArgumentParser.TypeResolver.cs | 57 ++ Swan.Lite/Parsers/ArgumentParser.cs | 253 +++++ Swan.Lite/Parsers/ArgumentParserSettings.cs | 53 ++ Swan.Lite/Parsers/ExpressionParser.cs | 117 +++ Swan.Lite/Parsers/Operator.cs | 32 + Swan.Lite/Parsers/Token.cs | 35 + Swan.Lite/Parsers/TokenType.cs | 48 + Swan.Lite/Parsers/Tokenizer.cs | 361 +++++++ Swan.Lite/Parsers/VerbOptionAttribute.cs | 40 + Swan.Lite/Reflection/AttributeCache.cs | 188 ++++ Swan.Lite/Reflection/ConstructorTypeCache.cs | 51 + Swan.Lite/Reflection/ExtendedPropertyInfo.cs | 107 +++ Swan.Lite/Reflection/ExtendedTypeInfo.cs | 267 ++++++ Swan.Lite/Reflection/IPropertyProxy.cs | 22 + Swan.Lite/Reflection/MethodInfoCache.cs | 117 +++ Swan.Lite/Reflection/PropertyProxy.cs | 47 + Swan.Lite/Reflection/PropertyTypeCache.cs | 74 ++ Swan.Lite/Reflection/TypeCache.cs | 78 ++ Swan.Lite/SingletonBase.cs | 59 ++ Swan.Lite/StringConversionException.cs | 76 ++ Swan.Lite/StructEndiannessAttribute.cs | 30 + Swan.Lite/Swan.Lite.csproj | 18 + Swan.Lite/SwanRuntime.cs | 233 +++++ Swan.Lite/Terminal.Graphics.cs | 37 + Swan.Lite/Terminal.Interaction.cs | 261 ++++++ Swan.Lite/Terminal.Output.cs | 97 ++ Swan.Lite/Terminal.Settings.cs | 49 + Swan.Lite/Terminal.cs | 339 +++++++ Swan.Lite/TerminalWriters.Enums.cs | 31 + Swan.Lite/Threading/AtomicBoolean.cs | 24 + Swan.Lite/Threading/AtomicDateTime.cs | 26 + Swan.Lite/Threading/AtomicDouble.cs | 28 + Swan.Lite/Threading/AtomicEnum.cs | 43 + Swan.Lite/Threading/AtomicInteger.cs | 28 + Swan.Lite/Threading/AtomicLong.cs | 24 + Swan.Lite/Threading/AtomicTimeSpan.cs | 26 + Swan.Lite/Threading/AtomicTypeBase.cs | 243 +++++ Swan.Lite/Threading/CancellationTokenOwner.cs | 68 ++ Swan.Lite/Threading/ExclusiveTimer.cs | 236 +++++ Swan.Lite/Threading/ISyncLocker.cs | 24 + Swan.Lite/Threading/IWaitEvent.cs | 57 ++ Swan.Lite/Threading/IWorker.cs | 69 ++ Swan.Lite/Threading/IWorkerDelayProvider.cs | 21 + Swan.Lite/Threading/PeriodicTask.cs | 101 ++ Swan.Lite/Threading/RunnerBase.cs | 178 ++++ Swan.Lite/Threading/SyncLockerFactory.cs | 185 ++++ Swan.Lite/Threading/WaitEventFactory.cs | 219 +++++ Swan.Lite/Threading/WorkerState.cs | 33 + Swan.Lite/Validators/IValidator.cs | 21 + .../Validators/ObjectValidationResult.cs | 47 + Swan.Lite/Validators/ObjectValidator.cs | 173 ++++ Swan.Lite/Validators/Validators.cs | 132 +++ .../DependencyContainer.cs | 743 +++++++++++++++ ...ependencyContainerRegistrationException.cs | 46 + .../DependencyContainerResolutionException.cs | 31 + .../DependencyContainerResolveOptions.cs | 114 +++ ...pendencyContainerWeakReferenceException.cs | 22 + Swan/DependencyInjection/ObjectFactoryBase.cs | 423 +++++++++ Swan/DependencyInjection/RegisterOptions.cs | 131 +++ Swan/DependencyInjection/TypeRegistration.cs | 67 ++ .../TypesConcurrentDictionary.cs | 351 +++++++ Swan/Diagnostics/RealtimeClock.cs | 143 +++ Swan/Extensions.MimeMessage.cs | 56 ++ Swan/Extensions.Network.cs | 58 ++ Swan/Extensions.WindowsServices.cs | 89 ++ Swan/Messaging/IMessageHubMessage.cs | 13 + Swan/Messaging/IMessageHubSubscription.cs | 26 + Swan/Messaging/MessageHub.cs | 442 +++++++++ Swan/Messaging/MessageHubMessageBase.cs | 57 ++ Swan/Messaging/MessageHubSubscriptionToken.cs | 51 + Swan/Net/Connection.cs | 886 ++++++++++++++++++ Swan/Net/ConnectionDataReceivedTrigger.cs | 28 + Swan/Net/ConnectionListener.cs | 253 +++++ Swan/Net/Dns/DnsClient.Interfaces.cs | 62 ++ Swan/Net/Dns/DnsClient.Request.cs | 681 ++++++++++++++ Swan/Net/Dns/DnsClient.ResourceRecords.cs | 419 +++++++++ Swan/Net/Dns/DnsClient.Response.cs | 215 +++++ Swan/Net/Dns/DnsClient.cs | 79 ++ Swan/Net/Dns/DnsQueryException.cs | 37 + Swan/Net/Dns/DnsQueryResult.cs | 123 +++ Swan/Net/Dns/DnsRecord.cs | 208 ++++ Swan/Net/Dns/Enums.Dns.cs | 172 ++++ Swan/Net/Eventing.ConnectionListener.cs | 158 ++++ Swan/Net/Eventing.cs | 84 ++ Swan/Net/JsonClient.cs | 418 +++++++++ Swan/Net/JsonRequestException.cs | 47 + Swan/Net/Network.cs | 328 +++++++ Swan/Net/Smtp/Enums.Smtp.cs | 166 ++++ Swan/Net/Smtp/SmtpClient.cs | 388 ++++++++ Swan/Net/Smtp/SmtpDefinitions.cs | 29 + Swan/Net/Smtp/SmtpSender.cs | 60 ++ Swan/Net/Smtp/SmtpServerReply.cs | 243 +++++ Swan/Net/Smtp/SmtpSessionState.cs | 158 ++++ Swan/ProcessResult.cs | 46 + Swan/ProcessRunner.cs | 443 +++++++++ Swan/Services/ServiceBase.cs | 92 ++ Swan/Swan.csproj | 22 + Swan/Threading/DelayProvider.cs | 141 +++ Swan/Threading/ThreadWorkerBase.cs | 292 ++++++ Swan/Threading/TimerWorkerBase.cs | 328 +++++++ Swan/Threading/WorkerBase.cs | 240 +++++ Swan/Threading/WorkerDelayProvider.cs | 151 +++ Swan/ViewModelBase.cs | 124 +++ .../Definitions.cs | 28 + Unosquare.RaspberryIO.Abstractions/Enums.cs | 527 +++++++++++ .../IBootstrap.cs | 13 + .../IGpioController.cs | 51 + .../IGpioPin.cs | 105 +++ Unosquare.RaspberryIO.Abstractions/II2CBus.cs | 39 + .../II2CDevice.cs | 77 ++ Unosquare.RaspberryIO.Abstractions/ISpiBus.cs | 48 + .../ISpiChannel.cs | 43 + .../ISystemInfo.cs | 26 + .../IThreading.cs | 30 + Unosquare.RaspberryIO.Abstractions/ITiming.cs | 40 + .../Native/HardwareException.cs | 71 ++ .../Native/Standard.cs | 45 + .../Unosquare.RaspberryIO.Abstractions.csproj | 21 + Unosquare.RaspberryIO.sln | 49 + .../BluetoothErrorException.cs | 20 + Unosquare.RaspberryIO/Camera/CameraColor.cs | 134 +++ .../Camera/CameraController.cs | 209 +++++ Unosquare.RaspberryIO/Camera/CameraRect.cs | 82 ++ .../Camera/CameraSettingsBase.cs | 340 +++++++ .../Camera/CameraStillSettings.cs | 120 +++ .../Camera/CameraVideoSettings.cs | 104 ++ Unosquare.RaspberryIO/Camera/Enums.cs | 423 +++++++++ .../Computer/AudioSettings.cs | 113 +++ Unosquare.RaspberryIO/Computer/AudioState.cs | 64 ++ Unosquare.RaspberryIO/Computer/Bluetooth.cs | 271 ++++++ Unosquare.RaspberryIO/Computer/DsiDisplay.cs | 71 ++ .../Computer/NetworkAdapterInfo.cs | 40 + .../Computer/NetworkSettings.cs | 281 ++++++ Unosquare.RaspberryIO/Computer/OsInfo.cs | 46 + Unosquare.RaspberryIO/Computer/PiVersion.cs | 394 ++++++++ Unosquare.RaspberryIO/Computer/SystemInfo.cs | 390 ++++++++ .../Computer/WirelessNetworkInfo.cs | 23 + Unosquare.RaspberryIO/Native/Standard.cs | 17 + Unosquare.RaspberryIO/Native/SystemName.cs | 47 + Unosquare.RaspberryIO/Pi.cs | 141 +++ .../Unosquare.RaspberryIO.csproj | 26 + Unosquare.WiringPi/BootstrapWiringPi.cs | 30 + Unosquare.WiringPi/Enums.cs | 289 ++++++ Unosquare.WiringPi/GpioController.cs | 582 ++++++++++++ Unosquare.WiringPi/GpioPin.Factory.cs | 200 ++++ Unosquare.WiringPi/GpioPin.cs | 654 +++++++++++++ Unosquare.WiringPi/I2CBus.cs | 72 ++ Unosquare.WiringPi/I2CDevice.cs | 181 ++++ Unosquare.WiringPi/Native/Delegates.cs | 12 + Unosquare.WiringPi/Native/SysCall.cs | 19 + Unosquare.WiringPi/Native/WiringPi.I2C.cs | 74 ++ .../Native/WiringPi.SerialPort.cs | 69 ++ Unosquare.WiringPi/Native/WiringPi.Shift.cs | 36 + Unosquare.WiringPi/Native/WiringPi.SoftPwm.cs | 64 ++ Unosquare.WiringPi/Native/WiringPi.Spi.cs | 53 ++ Unosquare.WiringPi/Native/WiringPi.cs | 376 ++++++++ .../Resources/EmbeddedResources.cs | 65 ++ Unosquare.WiringPi/Resources/gpio | Bin 0 -> 36860 bytes .../Resources/libwiringPi.so.2.50 | Bin 0 -> 69808 bytes Unosquare.WiringPi/SpiBus.cs | 43 + Unosquare.WiringPi/SpiChannel.cs | 130 +++ Unosquare.WiringPi/SystemInfo.cs | 45 + Unosquare.WiringPi/Threading.cs | 72 ++ Unosquare.WiringPi/Timing.cs | 36 + Unosquare.WiringPi/Unosquare.WiringPi.csproj | 27 + 225 files changed, 33065 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Swan.Lite/Collections/CollectionCacheRepository.cs create mode 100644 Swan.Lite/Collections/ComponentCollection`1.cs create mode 100644 Swan.Lite/Collections/ConcurrentDataDictionary`2.cs create mode 100644 Swan.Lite/Collections/DataDictionary`2.cs create mode 100644 Swan.Lite/Collections/DisposableComponentCollection`1.cs create mode 100644 Swan.Lite/Collections/IComponentCollection`1.cs create mode 100644 Swan.Lite/Collections/IDataDictionary`2.cs create mode 100644 Swan.Lite/Configuration/ConfiguredObject.cs create mode 100644 Swan.Lite/Configuration/PropertyDisplayAttribute.cs create mode 100644 Swan.Lite/Configuration/SettingsProvider.cs create mode 100644 Swan.Lite/Cryptography/Hasher.cs create mode 100644 Swan.Lite/DateTimeSpan.cs create mode 100644 Swan.Lite/Definitions.Types.cs create mode 100644 Swan.Lite/Definitions.cs create mode 100644 Swan.Lite/Diagnostics/Benchmark.cs create mode 100644 Swan.Lite/Diagnostics/BenchmarkUnit.cs create mode 100644 Swan.Lite/Diagnostics/HighResolutionTimer.cs create mode 100644 Swan.Lite/EnumHelper.cs create mode 100644 Swan.Lite/Enums.cs create mode 100644 Swan.Lite/Extensions.ByteArrays.cs create mode 100644 Swan.Lite/Extensions.ComponentCollections.cs create mode 100644 Swan.Lite/Extensions.Dates.cs create mode 100644 Swan.Lite/Extensions.Dictionaries.cs create mode 100644 Swan.Lite/Extensions.Exceptions.cs create mode 100644 Swan.Lite/Extensions.Functional.cs create mode 100644 Swan.Lite/Extensions.Reflection.cs create mode 100644 Swan.Lite/Extensions.Strings.cs create mode 100644 Swan.Lite/Extensions.Tasks.cs create mode 100644 Swan.Lite/Extensions.ValueTypes.cs create mode 100644 Swan.Lite/Extensions.cs create mode 100644 Swan.Lite/Formatters/CsvReader.cs create mode 100644 Swan.Lite/Formatters/CsvWriter.cs create mode 100644 Swan.Lite/Formatters/HumanizeJson.cs create mode 100644 Swan.Lite/Formatters/Json.Converter.cs create mode 100644 Swan.Lite/Formatters/Json.Deserializer.cs create mode 100644 Swan.Lite/Formatters/Json.Serializer.cs create mode 100644 Swan.Lite/Formatters/Json.SerializerOptions.cs create mode 100644 Swan.Lite/Formatters/Json.cs create mode 100644 Swan.Lite/Formatters/JsonPropertyAttribute.cs create mode 100644 Swan.Lite/FromString.cs create mode 100644 Swan.Lite/Logging/ConsoleLogger.cs create mode 100644 Swan.Lite/Logging/DebugLogger.cs create mode 100644 Swan.Lite/Logging/FileLogger.cs create mode 100644 Swan.Lite/Logging/ILogger.cs create mode 100644 Swan.Lite/Logging/LogLevel.cs create mode 100644 Swan.Lite/Logging/LogMessageReceivedEventArgs.cs create mode 100644 Swan.Lite/Logging/Logger.cs create mode 100644 Swan.Lite/Logging/TextLogger.cs create mode 100644 Swan.Lite/Mappers/CopyableAttribute.cs create mode 100644 Swan.Lite/Mappers/IObjectMap.cs create mode 100644 Swan.Lite/Mappers/ObjectMap.cs create mode 100644 Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs create mode 100644 Swan.Lite/Mappers/ObjectMapper.cs create mode 100644 Swan.Lite/ObjectComparer.cs create mode 100644 Swan.Lite/Paginator.cs create mode 100644 Swan.Lite/Parsers/ArgumentOptionAttribute.cs create mode 100644 Swan.Lite/Parsers/ArgumentParse.Validator.cs create mode 100644 Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs create mode 100644 Swan.Lite/Parsers/ArgumentParser.cs create mode 100644 Swan.Lite/Parsers/ArgumentParserSettings.cs create mode 100644 Swan.Lite/Parsers/ExpressionParser.cs create mode 100644 Swan.Lite/Parsers/Operator.cs create mode 100644 Swan.Lite/Parsers/Token.cs create mode 100644 Swan.Lite/Parsers/TokenType.cs create mode 100644 Swan.Lite/Parsers/Tokenizer.cs create mode 100644 Swan.Lite/Parsers/VerbOptionAttribute.cs create mode 100644 Swan.Lite/Reflection/AttributeCache.cs create mode 100644 Swan.Lite/Reflection/ConstructorTypeCache.cs create mode 100644 Swan.Lite/Reflection/ExtendedPropertyInfo.cs create mode 100644 Swan.Lite/Reflection/ExtendedTypeInfo.cs create mode 100644 Swan.Lite/Reflection/IPropertyProxy.cs create mode 100644 Swan.Lite/Reflection/MethodInfoCache.cs create mode 100644 Swan.Lite/Reflection/PropertyProxy.cs create mode 100644 Swan.Lite/Reflection/PropertyTypeCache.cs create mode 100644 Swan.Lite/Reflection/TypeCache.cs create mode 100644 Swan.Lite/SingletonBase.cs create mode 100644 Swan.Lite/StringConversionException.cs create mode 100644 Swan.Lite/StructEndiannessAttribute.cs create mode 100644 Swan.Lite/Swan.Lite.csproj create mode 100644 Swan.Lite/SwanRuntime.cs create mode 100644 Swan.Lite/Terminal.Graphics.cs create mode 100644 Swan.Lite/Terminal.Interaction.cs create mode 100644 Swan.Lite/Terminal.Output.cs create mode 100644 Swan.Lite/Terminal.Settings.cs create mode 100644 Swan.Lite/Terminal.cs create mode 100644 Swan.Lite/TerminalWriters.Enums.cs create mode 100644 Swan.Lite/Threading/AtomicBoolean.cs create mode 100644 Swan.Lite/Threading/AtomicDateTime.cs create mode 100644 Swan.Lite/Threading/AtomicDouble.cs create mode 100644 Swan.Lite/Threading/AtomicEnum.cs create mode 100644 Swan.Lite/Threading/AtomicInteger.cs create mode 100644 Swan.Lite/Threading/AtomicLong.cs create mode 100644 Swan.Lite/Threading/AtomicTimeSpan.cs create mode 100644 Swan.Lite/Threading/AtomicTypeBase.cs create mode 100644 Swan.Lite/Threading/CancellationTokenOwner.cs create mode 100644 Swan.Lite/Threading/ExclusiveTimer.cs create mode 100644 Swan.Lite/Threading/ISyncLocker.cs create mode 100644 Swan.Lite/Threading/IWaitEvent.cs create mode 100644 Swan.Lite/Threading/IWorker.cs create mode 100644 Swan.Lite/Threading/IWorkerDelayProvider.cs create mode 100644 Swan.Lite/Threading/PeriodicTask.cs create mode 100644 Swan.Lite/Threading/RunnerBase.cs create mode 100644 Swan.Lite/Threading/SyncLockerFactory.cs create mode 100644 Swan.Lite/Threading/WaitEventFactory.cs create mode 100644 Swan.Lite/Threading/WorkerState.cs create mode 100644 Swan.Lite/Validators/IValidator.cs create mode 100644 Swan.Lite/Validators/ObjectValidationResult.cs create mode 100644 Swan.Lite/Validators/ObjectValidator.cs create mode 100644 Swan.Lite/Validators/Validators.cs create mode 100644 Swan/DependencyInjection/DependencyContainer.cs create mode 100644 Swan/DependencyInjection/DependencyContainerRegistrationException.cs create mode 100644 Swan/DependencyInjection/DependencyContainerResolutionException.cs create mode 100644 Swan/DependencyInjection/DependencyContainerResolveOptions.cs create mode 100644 Swan/DependencyInjection/DependencyContainerWeakReferenceException.cs create mode 100644 Swan/DependencyInjection/ObjectFactoryBase.cs create mode 100644 Swan/DependencyInjection/RegisterOptions.cs create mode 100644 Swan/DependencyInjection/TypeRegistration.cs create mode 100644 Swan/DependencyInjection/TypesConcurrentDictionary.cs create mode 100644 Swan/Diagnostics/RealtimeClock.cs create mode 100644 Swan/Extensions.MimeMessage.cs create mode 100644 Swan/Extensions.Network.cs create mode 100644 Swan/Extensions.WindowsServices.cs create mode 100644 Swan/Messaging/IMessageHubMessage.cs create mode 100644 Swan/Messaging/IMessageHubSubscription.cs create mode 100644 Swan/Messaging/MessageHub.cs create mode 100644 Swan/Messaging/MessageHubMessageBase.cs create mode 100644 Swan/Messaging/MessageHubSubscriptionToken.cs create mode 100644 Swan/Net/Connection.cs create mode 100644 Swan/Net/ConnectionDataReceivedTrigger.cs create mode 100644 Swan/Net/ConnectionListener.cs create mode 100644 Swan/Net/Dns/DnsClient.Interfaces.cs create mode 100644 Swan/Net/Dns/DnsClient.Request.cs create mode 100644 Swan/Net/Dns/DnsClient.ResourceRecords.cs create mode 100644 Swan/Net/Dns/DnsClient.Response.cs create mode 100644 Swan/Net/Dns/DnsClient.cs create mode 100644 Swan/Net/Dns/DnsQueryException.cs create mode 100644 Swan/Net/Dns/DnsQueryResult.cs create mode 100644 Swan/Net/Dns/DnsRecord.cs create mode 100644 Swan/Net/Dns/Enums.Dns.cs create mode 100644 Swan/Net/Eventing.ConnectionListener.cs create mode 100644 Swan/Net/Eventing.cs create mode 100644 Swan/Net/JsonClient.cs create mode 100644 Swan/Net/JsonRequestException.cs create mode 100644 Swan/Net/Network.cs create mode 100644 Swan/Net/Smtp/Enums.Smtp.cs create mode 100644 Swan/Net/Smtp/SmtpClient.cs create mode 100644 Swan/Net/Smtp/SmtpDefinitions.cs create mode 100644 Swan/Net/Smtp/SmtpSender.cs create mode 100644 Swan/Net/Smtp/SmtpServerReply.cs create mode 100644 Swan/Net/Smtp/SmtpSessionState.cs create mode 100644 Swan/ProcessResult.cs create mode 100644 Swan/ProcessRunner.cs create mode 100644 Swan/Services/ServiceBase.cs create mode 100644 Swan/Swan.csproj create mode 100644 Swan/Threading/DelayProvider.cs create mode 100644 Swan/Threading/ThreadWorkerBase.cs create mode 100644 Swan/Threading/TimerWorkerBase.cs create mode 100644 Swan/Threading/WorkerBase.cs create mode 100644 Swan/Threading/WorkerDelayProvider.cs create mode 100644 Swan/ViewModelBase.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/Definitions.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/Enums.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/IBootstrap.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/IGpioController.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/IGpioPin.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/II2CBus.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/II2CDevice.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/ISpiBus.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/ISpiChannel.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/ISystemInfo.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/IThreading.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/ITiming.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/Native/HardwareException.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/Native/Standard.cs create mode 100644 Unosquare.RaspberryIO.Abstractions/Unosquare.RaspberryIO.Abstractions.csproj create mode 100644 Unosquare.RaspberryIO.sln create mode 100644 Unosquare.RaspberryIO/BluetoothErrorException.cs create mode 100644 Unosquare.RaspberryIO/Camera/CameraColor.cs create mode 100644 Unosquare.RaspberryIO/Camera/CameraController.cs create mode 100644 Unosquare.RaspberryIO/Camera/CameraRect.cs create mode 100644 Unosquare.RaspberryIO/Camera/CameraSettingsBase.cs create mode 100644 Unosquare.RaspberryIO/Camera/CameraStillSettings.cs create mode 100644 Unosquare.RaspberryIO/Camera/CameraVideoSettings.cs create mode 100644 Unosquare.RaspberryIO/Camera/Enums.cs create mode 100644 Unosquare.RaspberryIO/Computer/AudioSettings.cs create mode 100644 Unosquare.RaspberryIO/Computer/AudioState.cs create mode 100644 Unosquare.RaspberryIO/Computer/Bluetooth.cs create mode 100644 Unosquare.RaspberryIO/Computer/DsiDisplay.cs create mode 100644 Unosquare.RaspberryIO/Computer/NetworkAdapterInfo.cs create mode 100644 Unosquare.RaspberryIO/Computer/NetworkSettings.cs create mode 100644 Unosquare.RaspberryIO/Computer/OsInfo.cs create mode 100644 Unosquare.RaspberryIO/Computer/PiVersion.cs create mode 100644 Unosquare.RaspberryIO/Computer/SystemInfo.cs create mode 100644 Unosquare.RaspberryIO/Computer/WirelessNetworkInfo.cs create mode 100644 Unosquare.RaspberryIO/Native/Standard.cs create mode 100644 Unosquare.RaspberryIO/Native/SystemName.cs create mode 100644 Unosquare.RaspberryIO/Pi.cs create mode 100644 Unosquare.RaspberryIO/Unosquare.RaspberryIO.csproj create mode 100644 Unosquare.WiringPi/BootstrapWiringPi.cs create mode 100644 Unosquare.WiringPi/Enums.cs create mode 100644 Unosquare.WiringPi/GpioController.cs create mode 100644 Unosquare.WiringPi/GpioPin.Factory.cs create mode 100644 Unosquare.WiringPi/GpioPin.cs create mode 100644 Unosquare.WiringPi/I2CBus.cs create mode 100644 Unosquare.WiringPi/I2CDevice.cs create mode 100644 Unosquare.WiringPi/Native/Delegates.cs create mode 100644 Unosquare.WiringPi/Native/SysCall.cs create mode 100644 Unosquare.WiringPi/Native/WiringPi.I2C.cs create mode 100644 Unosquare.WiringPi/Native/WiringPi.SerialPort.cs create mode 100644 Unosquare.WiringPi/Native/WiringPi.Shift.cs create mode 100644 Unosquare.WiringPi/Native/WiringPi.SoftPwm.cs create mode 100644 Unosquare.WiringPi/Native/WiringPi.Spi.cs create mode 100644 Unosquare.WiringPi/Native/WiringPi.cs create mode 100644 Unosquare.WiringPi/Resources/EmbeddedResources.cs create mode 100644 Unosquare.WiringPi/Resources/gpio create mode 100644 Unosquare.WiringPi/Resources/libwiringPi.so.2.50 create mode 100644 Unosquare.WiringPi/SpiBus.cs create mode 100644 Unosquare.WiringPi/SpiChannel.cs create mode 100644 Unosquare.WiringPi/SystemInfo.cs create mode 100644 Unosquare.WiringPi/Threading.cs create mode 100644 Unosquare.WiringPi/Timing.cs create mode 100644 Unosquare.WiringPi/Unosquare.WiringPi.csproj diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d67f583 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +/.vs +/Swan/obj +/Swan/bin +/Swan.Lite/bin +/Swan.Lite/obj +/Unosquare.RaspberryIO/bin +/Unosquare.RaspberryIO/obj +/Unosquare.RaspberryIO.Abstractions/bin +/Unosquare.RaspberryIO.Abstractions/obj +/Unosquare.WiringPi/bin +/Unosquare.WiringPi/obj diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8875e49 --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +MIT License + +Copyright (c) 2016 Unosquare + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This software contains a compiled, unmodified version of the WiringPi library +WiringPi is a GPIO access library written in C for the BCM2835 used in the +Raspberry Pi. It’s released under the GNU LGPLv3 license and is usable from C +and C++ and many other languages with suitable wrappers. A program that contains +no derivative of any portion of the Library, but is designed to work with +the Library by being compiled or linked with it, is called a "work that uses +the Library". Such a work, in isolation, is not a derivative work of the Library, +and therefore falls outside the scope of this License. Raspberry IO is then, +by definition, "work that uses the Library" diff --git a/README.md b/README.md index cd3c86f..67d2b88 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ -# RaspberryIO_26 - +# UnoSquare +## RaspberryIO +Based on [Unosquare.RaspberryIO v0.26.0](https://github.com/unosquare/raspberryio) +## SWAN +Based on https://github.com/unosquare/swan \ No newline at end of file diff --git a/Swan.Lite/Collections/CollectionCacheRepository.cs b/Swan.Lite/Collections/CollectionCacheRepository.cs new file mode 100644 index 0000000..496f410 --- /dev/null +++ b/Swan.Lite/Collections/CollectionCacheRepository.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Collections +{ + /// + /// A thread-safe collection cache repository for types. + /// + /// The type of member to cache. + public class CollectionCacheRepository + { + private readonly Lazy>> _data = + new Lazy>>(() => + new ConcurrentDictionary>(), true); + + /// + /// Determines whether the cache contains the specified key. + /// + /// The key. + /// true if the cache contains the key, otherwise false. + public bool ContainsKey(Type key) => _data.Value.ContainsKey(key); + + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The key. + /// The factory. + /// + /// An array of the properties stored for the specified type. + /// + /// + /// key + /// or + /// factory. + /// + /// type. + public IEnumerable Retrieve(Type key, Func> factory) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + return _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); + } + } +} diff --git a/Swan.Lite/Collections/ComponentCollection`1.cs b/Swan.Lite/Collections/ComponentCollection`1.cs new file mode 100644 index 0000000..7421c09 --- /dev/null +++ b/Swan.Lite/Collections/ComponentCollection`1.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Swan.Configuration; + +namespace Swan.Collections +{ + /// + /// Implements a collection of components. + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + /// + public class ComponentCollection : ConfiguredObject, IComponentCollection + { + private readonly List _components = new List(); + + private readonly List<(string, T)> _componentsWithSafeNames = new List<(string, T)>(); + + private readonly Dictionary _namedComponents = new Dictionary(); + + /// + public int Count => _components.Count; + + /// + public IReadOnlyDictionary Named => _namedComponents; + + /// + public IReadOnlyList<(string SafeName, T Component)> WithSafeNames => _componentsWithSafeNames; + + /// + public T this[int index] => _components[index]; + + /// + public T this[string key] => _namedComponents[key]; + + /// + public IEnumerator GetEnumerator() => _components.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_components).GetEnumerator(); + + /// + /// The collection is locked. + public void Add(string name, T component) + { + EnsureConfigurationNotLocked(); + + if (name != null) + { + if (name.Length == 0) + throw new ArgumentException("Component name is empty.", nameof(name)); + + if (_namedComponents.ContainsKey(name)) + throw new ArgumentException("Duplicate component name.", nameof(name)); + } + + if (component == null) + throw new ArgumentNullException(nameof(component)); + + if (_components.Contains(component)) + throw new ArgumentException("Component has already been added.", nameof(component)); + + _components.Add(component); + _componentsWithSafeNames.Add((name ?? $"<{component.GetType().Name}>", component)); + + if (name != null) + _namedComponents.Add(name, component); + } + + /// + /// Locks the collection, preventing further additions. + /// + public void Lock() => LockConfiguration(); + } +} diff --git a/Swan.Lite/Collections/ConcurrentDataDictionary`2.cs b/Swan.Lite/Collections/ConcurrentDataDictionary`2.cs new file mode 100644 index 0000000..58fa75a --- /dev/null +++ b/Swan.Lite/Collections/ConcurrentDataDictionary`2.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Collections +{ + /// + /// Represents a thread-safe collection of key/value pairs that does not store null values + /// and can be accessed by multiple threads concurrently. + /// + /// The type of keys in the dictionary. This must be a reference type. + /// The type of values in the dictionary. This must be a reference type. + /// + public sealed class ConcurrentDataDictionary : IDataDictionary + where TKey : class + where TValue : class + { + #region Private data + + private readonly ConcurrentDictionary _dictionary; + + #endregion + + #region Instance management + + /// + /// Initializes a new instance of the class + /// that is empty, has the default concurrency level, has the default initial capacity, + /// and uses the default comparer for . + /// + /// + public ConcurrentDataDictionary() + { + _dictionary = new ConcurrentDictionary(); + } + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , has the default concurrency level, + /// has the default initial capacity, and uses the default comparer for . + /// + /// The whose elements are copied + /// to the new . + /// is . + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + public ConcurrentDataDictionary(IEnumerable> collection) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + + _dictionary = new ConcurrentDictionary(collection.Where(pair => pair.Value != null)); + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the default concurrency level and capacity, and uses the specified . + /// + /// The equality comparison implementation to use when comparing keys. + /// is . + /// + public ConcurrentDataDictionary(IEqualityComparer comparer) + { + _dictionary = new ConcurrentDictionary(comparer); + } + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , has the default concurrency level, + /// has the default initial capacity, and uses the specified . + /// + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// + public ConcurrentDataDictionary(IEnumerable> collection, IEqualityComparer comparer) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + + _dictionary = new ConcurrentDictionary(collection.Where(pair => pair.Value != null), comparer); + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the specified concurrency level and capacity, and uses the default comparer for the key type. + /// + /// The estimated number of threads that will update + /// the concurrently. + /// The initial number of elements that the can contain. + /// + /// is less than 1. + /// - or -. + /// is less than 0. + /// + /// + public ConcurrentDataDictionary(int concurrencyLevel, int capacity) + { + _dictionary = new ConcurrentDictionary(concurrencyLevel, capacity); + } + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , has the specified concurrency level, + /// has the default initial capacity, and uses the specified . + /// + /// The estimated number of threads that will update + /// the concurrently. + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// is less than 1. + /// + public ConcurrentDataDictionary(int concurrencyLevel, IEnumerable> collection, IEqualityComparer comparer) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + + _dictionary = new ConcurrentDictionary( + concurrencyLevel, + collection.Where(pair => pair.Value != null), + comparer); + } + + #endregion + + #region Public APIs + + /// + public int Count => _dictionary.Count; + + /// + public bool IsEmpty => _dictionary.IsEmpty; + + /// + public ICollection Keys => _dictionary.Keys; + + /// + public ICollection Values => _dictionary.Values; + + /// + public TValue? this[TKey key] + { + get => _dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out var value) ? value : null; + set + { + if (value != null) + { + _dictionary[key] = value; + } + else + { + _dictionary.TryRemove(key, out _); + } + } + } + + /// + public void Clear() => _dictionary.Clear(); + + /// + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + + /// + public TValue? GetOrAdd(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (value != null) + return _dictionary.GetOrAdd(key, value); + + return _dictionary.TryGetValue(key, out var retrievedValue) ? retrievedValue : null; + } + + /// + public bool Remove(TKey key) => _dictionary.TryRemove(key, out _); + + /// + public bool TryAdd(TKey key, TValue value) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + return value == null || _dictionary.TryAdd(key, value); + } + + /// + public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value); + + /// + public bool TryRemove(TKey key, out TValue value) => _dictionary.TryRemove(key, out value); + + /// + public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue) + { + if (key == null) + throw new ArgumentNullException(nameof(key)); + + return newValue != null && comparisonValue != null && _dictionary.TryUpdate(key, newValue, comparisonValue); + } + + #endregion + + #region Implementation of IDictionary + + /// + void IDictionary.Add(TKey key, TValue value) + { + if (value != null) + { + ((IDictionary)_dictionary).Add(key, value); + } + else + { + _dictionary.TryRemove(key, out _); + } + } + + #endregion + + #region Implementation of IReadOnlyDictionary + + /// + IEnumerable IReadOnlyDictionary.Keys => _dictionary.Keys; + + /// + IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; + + #endregion + + #region Implementation of ICollection> + + /// + /// + /// This property is always for a . + /// + bool ICollection>.IsReadOnly => false; + + /// + void ICollection>.Add(KeyValuePair item) + { + if (item.Value != null) + { + ((ICollection>)_dictionary).Add(item); + } + else + { + _dictionary.TryRemove(item.Key, out _); + } + } + + /// + bool ICollection>.Contains(KeyValuePair item) + => ((ICollection>)_dictionary).Contains(item); + + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + => ((ICollection>)_dictionary).CopyTo(array, arrayIndex); + + /// + bool ICollection>.Remove(KeyValuePair item) + => ((ICollection>)_dictionary).Remove(item); + + #endregion + + #region Implementation of IEnumerable> + + /// + IEnumerator> IEnumerable>.GetEnumerator() => _dictionary.GetEnumerator(); + + #endregion + + #region Implementation of IEnumerable + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); + + #endregion + } +} diff --git a/Swan.Lite/Collections/DataDictionary`2.cs b/Swan.Lite/Collections/DataDictionary`2.cs new file mode 100644 index 0000000..24f5143 --- /dev/null +++ b/Swan.Lite/Collections/DataDictionary`2.cs @@ -0,0 +1,341 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Collections +{ + /// + /// Represents a non-thread-safe collection of key/value pairs that does not store null values. + /// + /// The type of keys in the dictionary. This must be a reference type. + /// The type of values in the dictionary. This must be a reference type. + /// + public sealed class DataDictionary : IDataDictionary + where TKey : class + where TValue : class + { + #region Private data + + private readonly Dictionary _dictionary; + + #endregion + + #region Instance management + + /// + /// Initializes a new instance of the class + /// that is empty, has the default initial capacity, + /// and uses the default comparer for . + /// + /// + public DataDictionary() + { + _dictionary = new Dictionary(); + } + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , + /// has the default initial capacity, and uses the default comparer for . + /// + /// The whose elements are copied + /// to the new . + /// is . + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + public DataDictionary(IEnumerable> collection) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + + _dictionary = new Dictionary(); + foreach (var pair in collection.Where(pair => pair.Value != null)) + { + _dictionary.Add(pair.Key, pair.Value); + } + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the default capacity, and uses the specified . + /// + /// The equality comparison implementation to use when comparing keys. + /// is . + /// + public DataDictionary(IEqualityComparer comparer) + { + _dictionary = new Dictionary(comparer); + } + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , + /// has the default initial capacity, and uses the specified . + /// + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// + public DataDictionary(IEnumerable> collection, IEqualityComparer comparer) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + + _dictionary = new Dictionary(comparer); + foreach (var pair in collection.Where(pair => pair.Value != null)) + { + _dictionary.Add(pair.Key, pair.Value); + } + } + + /// + /// Initializes a new instance of the class + /// that is empty, has the specified capacity, and uses the default comparer for the key type. + /// + /// The initial number of elements that the can contain. + /// is less than 0. + /// + public DataDictionary(int capacity) + { + _dictionary = new Dictionary(capacity); + } + + /// + /// Initializes a new instance of the class + /// that contains elements copied from the specified , + /// has the specified capacity, and uses the specified . + /// + /// The initial number of elements that the can contain. + /// The whose elements are copied + /// to the new . + /// The equality comparison implementation to use when comparing keys. + /// + /// Since does not store null values, + /// key/value pairs whose value is will not be copied from . + /// + /// + /// is . + /// - or -. + /// is . + /// + /// is less than 0. + /// + public DataDictionary(int capacity, IEnumerable> collection, IEqualityComparer comparer) + { + if (collection == null) + throw new ArgumentNullException(nameof(collection)); + + _dictionary = new Dictionary(capacity, comparer); + foreach (var pair in collection.Where(pair => pair.Value != null)) + { + _dictionary.Add(pair.Key, pair.Value); + } + } + + #endregion + + #region Public APIs + + /// + public int Count => _dictionary.Count; + + /// + public bool IsEmpty => _dictionary.Count == 0; + + /// + public ICollection Keys => _dictionary.Keys; + + /// + public ICollection Values => _dictionary.Values; + + /// + public TValue? this[TKey key] + { + get => _dictionary.TryGetValue(key ?? throw new ArgumentNullException(nameof(key)), out var value) ? value : null; + set + { + if (value != null) + { + _dictionary[key] = value; + } + else + { + _dictionary.Remove(key); + } + } + } + + /// + public void Clear() => _dictionary.Clear(); + + /// + public bool ContainsKey(TKey key) + { + // _dictionary.ContainsKey will take care of throwing on a null key. + return _dictionary.ContainsKey(key); + } + + /// + public TValue? GetOrAdd(TKey key, TValue value) + { + // _dictionary.TryGetValue will take care of throwing on a null key. + if (_dictionary.TryGetValue(key, out var result)) + return result; + + if (value == null) + return null; + + _dictionary.Add(key, value); + return value; + } + + /// + public bool Remove(TKey key) + { + // _dictionary.Remove will take care of throwing on a null key. + return _dictionary.Remove(key); + } + + /// + public bool TryAdd(TKey key, TValue value) + { + // _dictionary.ContainsKey will take care of throwing on a null key. + if (_dictionary.ContainsKey(key)) + return false; + + if (value != null) + _dictionary.Add(key, value); + + return true; + } + + /// + public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value); + + /// + public bool TryRemove(TKey key, out TValue value) + { + // TryGetValue will take care of throwing on a null key. + if (!_dictionary.TryGetValue(key, out value)) + return false; + + _dictionary.Remove(key); + return true; + } + + /// + public bool TryUpdate(TKey key, TValue newValue, TValue comparisonValue) + { + // TryGetValue will take care of throwing on a null key. + if (!_dictionary.TryGetValue(key, out var value)) + return false; + + if (value != comparisonValue) + return false; + + _dictionary[key] = newValue; + return true; + } + + #endregion + + #region Implementation of IDictionary + + /// + void IDictionary.Add(TKey key, TValue value) + { + // Validating the key seems redundant, because both Add and Remove + // will throw on a null key. + // This way, though, the code path on null key does not depend on value. + // Without this validation, there should be two unit tests for null key, + // one with a null value and one with a non-null value, + // which makes no sense. + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (value != null) + { + _dictionary.Add(key, value); + } + else + { + _dictionary.Remove(key); + } + } + + #endregion + + #region Implementation of IReadOnlyDictionary + + /// + IEnumerable IReadOnlyDictionary.Keys => _dictionary.Keys; + + /// + IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; + + #endregion + + #region Implementation of ICollection> + + /// + /// + /// This property is always for a . + /// + bool ICollection>.IsReadOnly => false; + + /// + void ICollection>.Add(KeyValuePair item) + { + if (item.Value != null) + { + ((ICollection>)_dictionary).Add(item); + } + else + { + _dictionary.Remove(item.Key); + } + } + + /// + bool ICollection>.Contains(KeyValuePair item) + => ((ICollection>)_dictionary).Contains(item); + + /// + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + => ((ICollection>)_dictionary).CopyTo(array, arrayIndex); + + /// + bool ICollection>.Remove(KeyValuePair item) + => ((ICollection>) _dictionary).Remove(item); + + #endregion + + #region Implementation of IEnumerable> + + /// + IEnumerator> IEnumerable>.GetEnumerator() => _dictionary.GetEnumerator(); + + #endregion + + #region Implementation of IEnumerable + + /// + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); + + #endregion + } +} diff --git a/Swan.Lite/Collections/DisposableComponentCollection`1.cs b/Swan.Lite/Collections/DisposableComponentCollection`1.cs new file mode 100644 index 0000000..4f42fa6 --- /dev/null +++ b/Swan.Lite/Collections/DisposableComponentCollection`1.cs @@ -0,0 +1,49 @@ +using System; + +namespace Swan.Collections +{ + /// + /// Implements a collection of components that automatically disposes each component + /// implementing . + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + /// + /// + public class DisposableComponentCollection : ComponentCollection, IDisposable + { + /// + /// Finalizes an instance of the class. + /// + ~DisposableComponentCollection() + { + Dispose(false); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposing) return; + + foreach (var component in this) + { + if (component is IDisposable disposable) + disposable.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Collections/IComponentCollection`1.cs b/Swan.Lite/Collections/IComponentCollection`1.cs new file mode 100644 index 0000000..cf16760 --- /dev/null +++ b/Swan.Lite/Collections/IComponentCollection`1.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace Swan.Collections +{ + /// + /// Represents a collection of components. + /// Each component in the collection may be given a unique name for later retrieval. + /// + /// The type of components in the collection. + public interface IComponentCollection : IReadOnlyList + { + /// + /// Gets an interface representing the named components. + /// + /// + /// The named components. + /// + IReadOnlyDictionary Named { get; } + + /// + /// Gets an interface representing all components + /// associated with safe names. + /// The safe name of a component is never . + /// If a component's unique name if , its safe name + /// will be some non- string somehow identifying it. + /// Note that safe names are not necessarily unique. + /// + /// + /// A list of s, each containing a safe name and a component. + /// + IReadOnlyList<(string SafeName, T Component)> WithSafeNames { get; } + + /// + /// Gets the component with the specified name. + /// + /// + /// The component. + /// + /// The name. + /// The component with the specified . + /// is null. + /// The property is retrieved and is not found. + T this[string name] { get; } + + /// + /// Adds a component to the collection, + /// giving it the specified if it is not . + /// + /// The name given to the module, or . + /// The component. + void Add(string name, T component); + } +} \ No newline at end of file diff --git a/Swan.Lite/Collections/IDataDictionary`2.cs b/Swan.Lite/Collections/IDataDictionary`2.cs new file mode 100644 index 0000000..800a7b0 --- /dev/null +++ b/Swan.Lite/Collections/IDataDictionary`2.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Swan.Collections +{ + /// + /// Represents a generic collection of key/value pairs that does not store + /// null values. + /// + /// The type of keys in the dictionary. This must be a reference type. + /// The type of values in the dictionary. This must be a reference type. + public interface IDataDictionary : IDictionary, IReadOnlyDictionary + where TKey : class + where TValue : class + { + /// + /// Gets a value that indicates whether the is empty. + /// + /// + /// if the is empty; otherwise, . + /// + bool IsEmpty { get; } + + /// + /// Attempts to remove and return the value that has the specified key from the . + /// + /// The key of the element to remove and return. + /// When this method returns, the value removed from the , + /// if the key is found; otherwise, . This parameter is passed uninitialized. + /// if the value was removed successfully; otherwise, . + /// is . + bool TryRemove(TKey key, out TValue value); + } +} diff --git a/Swan.Lite/Configuration/ConfiguredObject.cs b/Swan.Lite/Configuration/ConfiguredObject.cs new file mode 100644 index 0000000..1855c81 --- /dev/null +++ b/Swan.Lite/Configuration/ConfiguredObject.cs @@ -0,0 +1,77 @@ +using System; + +namespace Swan.Configuration +{ + /// + /// Base class for objects whose configuration may be locked, + /// thus becoming read-only, at a certain moment in their lifetime. + /// + public abstract class ConfiguredObject + { + private readonly object _syncRoot = new object(); + private bool _configurationLocked; + + /// + /// Gets a value indicating whether s configuration has already been locked + /// and has therefore become read-only. + /// + /// + /// if the configuration is locked; otherwise, . + /// + /// + protected bool ConfigurationLocked + { + get + { + lock (_syncRoot) + { + return _configurationLocked; + } + } + } + + /// + /// Locks this instance's configuration, preventing further modifications. + /// + /// + /// Configuration locking must be enforced by derived classes + /// by calling at the start + /// of methods and property setters that could change the object's + /// configuration. + /// Immediately before locking the configuration, this method calls + /// as a last chance to validate configuration data, and to lock the configuration of contained objects. + /// + /// + protected void LockConfiguration() + { + lock (_syncRoot) + { + if (_configurationLocked) + return; + + OnBeforeLockConfiguration(); + _configurationLocked = true; + } + } + + /// + /// Called immediately before locking the configuration. + /// + /// + protected virtual void OnBeforeLockConfiguration() + { + } + + /// + /// Checks whether a module's configuration has become read-only + /// and, if so, throws an . + /// + /// The configuration is locked. + /// + protected void EnsureConfigurationNotLocked() + { + if (ConfigurationLocked) + throw new InvalidOperationException($"Configuration of this {GetType().Name} instance is locked."); + } + } +} diff --git a/Swan.Lite/Configuration/PropertyDisplayAttribute.cs b/Swan.Lite/Configuration/PropertyDisplayAttribute.cs new file mode 100644 index 0000000..2f45af5 --- /dev/null +++ b/Swan.Lite/Configuration/PropertyDisplayAttribute.cs @@ -0,0 +1,54 @@ +using System; + +namespace Swan.Configuration +{ + /// + /// An attribute used to include additional information to a Property for serialization. + /// + /// Previously we used DisplayAttribute from DataAnnotation. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class PropertyDisplayAttribute : Attribute + { + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the description. + /// + /// + /// The description. + /// + public string Description { get; set; } + + /// + /// Gets or sets the name of the group. + /// + /// + /// The name of the group. + /// + public string GroupName { get; set; } + + /// + /// Gets or sets the default value. + /// + /// + /// The default value. + /// + public object DefaultValue { get; set; } + + /// + /// Gets or sets the format string to call with method ToString. + /// + /// + /// The format. + /// + public string Format { get; set; } + } +} diff --git a/Swan.Lite/Configuration/SettingsProvider.cs b/Swan.Lite/Configuration/SettingsProvider.cs new file mode 100644 index 0000000..6e1020b --- /dev/null +++ b/Swan.Lite/Configuration/SettingsProvider.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Swan.Formatters; +using Swan.Reflection; + +namespace Swan.Configuration +{ + /// + /// Represents a provider to save and load settings using a plain JSON file. + /// + /// + /// The following example shows how to save and load settings. + /// + /// using Swan.Configuration; + /// + /// public class Example + /// { + /// public static void Main() + /// { + /// // get user from settings + /// var user = SettingsProvider<Settings>.Instance.Global.User; + /// + /// // modify the port + /// SettingsProvider<Settings>.Instance.Global.Port = 20; + /// + /// // if we want these settings to persist + /// SettingsProvider<Settings>.Instance.PersistGlobalSettings(); + /// } + /// + /// public class Settings + /// { + /// public int Port { get; set; } = 9696; + /// + /// public string User { get; set; } = "User"; + /// } + /// } + /// + /// + /// The type of settings model. + public sealed class SettingsProvider + : SingletonBase> + { + private readonly object _syncRoot = new object(); + + private T _global; + + /// + /// Gets or sets the configuration file path. By default the entry assembly directory is used + /// and the filename is 'appsettings.json'. + /// + /// + /// The configuration file path. + /// + public string ConfigurationFilePath { get; set; } = + Path.Combine(SwanRuntime.EntryAssemblyDirectory, "appsettings.json"); + + /// + /// Gets the global settings object. + /// + /// + /// The global settings object. + /// + public T Global + { + get + { + lock (_syncRoot) + { + if (Equals(_global, default(T))) + ReloadGlobalSettings(); + + return _global; + } + } + } + + /// + /// Reloads the global settings. + /// + public void ReloadGlobalSettings() + { + if (File.Exists(ConfigurationFilePath) == false || File.ReadAllText(ConfigurationFilePath).Length == 0) + { + ResetGlobalSettings(); + return; + } + + lock (_syncRoot) + _global = Json.Deserialize(File.ReadAllText(ConfigurationFilePath)); + } + + /// + /// Persists the global settings. + /// + public void PersistGlobalSettings() => File.WriteAllText(ConfigurationFilePath, Json.Serialize(Global, true)); + + /// + /// Updates settings from list. + /// + /// The list. + /// + /// A list of settings of type ref="ExtendedPropertyInfo". + /// + /// propertyList. + public List RefreshFromList(List> propertyList) + { + if (propertyList == null) + throw new ArgumentNullException(nameof(propertyList)); + + var changedSettings = new List(); + var globalProps = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(); + + foreach (var property in propertyList) + { + var propertyInfo = globalProps.FirstOrDefault(x => x.Name == property.Property); + + if (propertyInfo == null) continue; + + var originalValue = propertyInfo.GetValue(Global); + var isChanged = propertyInfo.PropertyType.IsArray + ? property.Value is IEnumerable enumerable && propertyInfo.TrySetArray(enumerable.Cast(), Global) + : SetValue(property.Value, originalValue, propertyInfo); + + if (!isChanged) continue; + + changedSettings.Add(property.Property); + PersistGlobalSettings(); + } + + return changedSettings; + } + + /// + /// Gets the list. + /// + /// A List of ExtendedPropertyInfo of the type T. + public List>? GetList() + { + var jsonData = Json.Deserialize(Json.Serialize(Global)) as Dictionary; + + return jsonData?.Keys + .Select(p => new ExtendedPropertyInfo(p) { Value = jsonData[p] }) + .ToList(); + } + + /// + /// Resets the global settings. + /// + public void ResetGlobalSettings() + { + lock (_syncRoot) + _global = Activator.CreateInstance(); + + PersistGlobalSettings(); + } + + private bool SetValue(object property, object originalValue, PropertyInfo propertyInfo) + { + switch (property) + { + case null when originalValue == null: + break; + case null: + propertyInfo.SetValue(Global, null); + return true; + default: + if (propertyInfo.PropertyType.TryParseBasicType(property, out var propertyValue) && + !propertyValue.Equals(originalValue)) + { + propertyInfo.SetValue(Global, propertyValue); + return true; + } + + break; + } + + return false; + } + } +} diff --git a/Swan.Lite/Cryptography/Hasher.cs b/Swan.Lite/Cryptography/Hasher.cs new file mode 100644 index 0000000..a78adac --- /dev/null +++ b/Swan.Lite/Cryptography/Hasher.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace Swan.Cryptography +{ + /// + /// Use this class to compute a hash in MD4, SHA1, SHA256 or SHA512. + /// + public static class Hasher + { + private static readonly Lazy Md5Hasher = new Lazy(MD5.Create, true); + private static readonly Lazy SHA1Hasher = new Lazy(SHA1.Create, true); + private static readonly Lazy SHA256Hasher = new Lazy(SHA256.Create, true); + private static readonly Lazy SHA512Hasher = new Lazy(SHA512.Create, true); + + /// + /// Computes the MD5 hash of the given stream. + /// Do not use for large streams as this reads ALL bytes at once. + /// + /// The stream. + /// if set to true [create hasher]. + /// + /// The computed hash code. + /// + /// stream. + [Obsolete("Use a better hasher.")] + public static byte[] ComputeMD5(Stream @this, bool createHasher = false) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var md5 = MD5.Create(); + const int bufferSize = 4096; + + var readAheadBuffer = new byte[bufferSize]; + var readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); + + do + { + var bytesRead = readAheadBytesRead; + var buffer = readAheadBuffer; + + readAheadBuffer = new byte[bufferSize]; + readAheadBytesRead = @this.Read(readAheadBuffer, 0, readAheadBuffer.Length); + + if (readAheadBytesRead == 0) + md5.TransformFinalBlock(buffer, 0, bytesRead); + else + md5.TransformBlock(buffer, 0, bytesRead, buffer, 0); + } + while (readAheadBytesRead != 0); + + return md5.Hash; + } + + /// + /// Computes the MD5 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// The computed hash code. + [Obsolete("Use a better hasher.")] + public static byte[] ComputeMD5(string value, bool createHasher = false) => + ComputeMD5(Encoding.UTF8.GetBytes(value), createHasher); + + /// + /// Computes the MD5 hash of the given byte array. + /// + /// The data. + /// if set to true [create hasher]. + /// The computed hash code. + [Obsolete("Use a better hasher.")] + public static byte[] ComputeMD5(byte[] data, bool createHasher = false) => + (createHasher ? MD5.Create() : Md5Hasher.Value).ComputeHash(data); + + /// + /// Computes the SHA-1 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// using the SHA1 hash function. + /// + [Obsolete("Use a better hasher.")] + public static byte[] ComputeSha1(string @this, bool createHasher = false) + { + var inputBytes = Encoding.UTF8.GetBytes(@this); + return (createHasher ? SHA1.Create() : SHA1Hasher.Value).ComputeHash(inputBytes); + } + + /// + /// Computes the SHA-256 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// by using the SHA256 hash function. + /// + public static byte[] ComputeSha256(string value, bool createHasher = false) + { + var inputBytes = Encoding.UTF8.GetBytes(value); + return (createHasher ? SHA256.Create() : SHA256Hasher.Value).ComputeHash(inputBytes); + } + + /// + /// Computes the SHA-512 hash of the given string using UTF8 byte encoding. + /// + /// The input string. + /// if set to true [create hasher]. + /// + /// The computes a Hash-based Message Authentication Code (HMAC) + /// using the SHA512 hash function. + /// + public static byte[] ComputeSha512(string value, bool createHasher = false) + { + var inputBytes = Encoding.UTF8.GetBytes(value); + return (createHasher ? SHA512.Create() : SHA512Hasher.Value).ComputeHash(inputBytes); + } + } +} diff --git a/Swan.Lite/DateTimeSpan.cs b/Swan.Lite/DateTimeSpan.cs new file mode 100644 index 0000000..f512d9c --- /dev/null +++ b/Swan.Lite/DateTimeSpan.cs @@ -0,0 +1,174 @@ +using System; + +namespace Swan +{ + /// + /// Represents a struct of DateTimeSpan to compare dates and get in + /// separate fields the amount of time between those dates. + /// + /// Based on https://stackoverflow.com/a/9216404/1096693. + /// + public struct DateTimeSpan + { + /// + /// Initializes a new instance of the struct. + /// + /// The years. + /// The months. + /// The days. + /// The hours. + /// The minutes. + /// The seconds. + /// The milliseconds. + public DateTimeSpan(int years, int months, int days, int hours, int minutes, int seconds, int milliseconds) + { + Years = years; + Months = months; + Days = days; + Hours = hours; + Minutes = minutes; + Seconds = seconds; + Milliseconds = milliseconds; + } + + /// + /// Gets the years. + /// + /// + /// The years. + /// + public int Years { get; } + + /// + /// Gets the months. + /// + /// + /// The months. + /// + public int Months { get; } + + /// + /// Gets the days. + /// + /// + /// The days. + /// + public int Days { get; } + + /// + /// Gets the hours. + /// + /// + /// The hours. + /// + public int Hours { get; } + + /// + /// Gets the minutes. + /// + /// + /// The minutes. + /// + public int Minutes { get; } + + /// + /// Gets the seconds. + /// + /// + /// The seconds. + /// + public int Seconds { get; } + + /// + /// Gets the milliseconds. + /// + /// + /// The milliseconds. + /// + public int Milliseconds { get; } + + internal static DateTimeSpan CompareDates(DateTime date1, DateTime date2) + { + if (date2 < date1) + { + var sub = date1; + date1 = date2; + date2 = sub; + } + + var current = date1; + var years = 0; + var months = 0; + var days = 0; + + var phase = Phase.Years; + var span = new DateTimeSpan(); + var officialDay = current.Day; + + while (phase != Phase.Done) + { + switch (phase) + { + case Phase.Years: + if (current.AddYears(years + 1) > date2) + { + phase = Phase.Months; + current = current.AddYears(years); + } + else + { + years++; + } + + break; + case Phase.Months: + if (current.AddMonths(months + 1) > date2) + { + phase = Phase.Days; + current = current.AddMonths(months); + if (current.Day < officialDay && + officialDay <= DateTime.DaysInMonth(current.Year, current.Month)) + current = current.AddDays(officialDay - current.Day); + } + else + { + months++; + } + + break; + case Phase.Days: + if (current.AddDays(days + 1) > date2) + { + current = current.AddDays(days); + var timespan = date2 - current; + span = new DateTimeSpan( + years, + months, + days, + timespan.Hours, + timespan.Minutes, + timespan.Seconds, + timespan.Milliseconds); + phase = Phase.Done; + } + else + { + days++; + } + + break; + } + } + + return span; + } + + private enum Phase + { + Years, + Months, + Days, + Done, + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Definitions.Types.cs b/Swan.Lite/Definitions.Types.cs new file mode 100644 index 0000000..68895e9 --- /dev/null +++ b/Swan.Lite/Definitions.Types.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions + { + #region Main Dictionary Definition + + /// + /// The basic types information. + /// + public static readonly Lazy> BasicTypesInfo = new Lazy>(() => + new Dictionary + { + // Non-Nullables + {typeof(DateTime), new ExtendedTypeInfo()}, + {typeof(byte), new ExtendedTypeInfo()}, + {typeof(sbyte), new ExtendedTypeInfo()}, + {typeof(int), new ExtendedTypeInfo()}, + {typeof(uint), new ExtendedTypeInfo()}, + {typeof(short), new ExtendedTypeInfo()}, + {typeof(ushort), new ExtendedTypeInfo()}, + {typeof(long), new ExtendedTypeInfo()}, + {typeof(ulong), new ExtendedTypeInfo()}, + {typeof(float), new ExtendedTypeInfo()}, + {typeof(double), new ExtendedTypeInfo()}, + {typeof(char), new ExtendedTypeInfo()}, + {typeof(bool), new ExtendedTypeInfo()}, + {typeof(decimal), new ExtendedTypeInfo()}, + {typeof(Guid), new ExtendedTypeInfo()}, + + // Strings is also considered a basic type (it's the only basic reference type) + {typeof(string), new ExtendedTypeInfo()}, + + // Nullables + {typeof(DateTime?), new ExtendedTypeInfo()}, + {typeof(byte?), new ExtendedTypeInfo()}, + {typeof(sbyte?), new ExtendedTypeInfo()}, + {typeof(int?), new ExtendedTypeInfo()}, + {typeof(uint?), new ExtendedTypeInfo()}, + {typeof(short?), new ExtendedTypeInfo()}, + {typeof(ushort?), new ExtendedTypeInfo()}, + {typeof(long?), new ExtendedTypeInfo()}, + {typeof(ulong?), new ExtendedTypeInfo()}, + {typeof(float?), new ExtendedTypeInfo()}, + {typeof(double?), new ExtendedTypeInfo()}, + {typeof(char?), new ExtendedTypeInfo()}, + {typeof(bool?), new ExtendedTypeInfo()}, + {typeof(decimal?), new ExtendedTypeInfo()}, + {typeof(Guid?), new ExtendedTypeInfo()}, + + // Additional Types + {typeof(TimeSpan), new ExtendedTypeInfo()}, + {typeof(TimeSpan?), new ExtendedTypeInfo()}, + {typeof(IPAddress), new ExtendedTypeInfo()}, + }); + + #endregion + + /// + /// Contains all basic types, including string, date time, and all of their nullable counterparts. + /// + /// + /// All basic types. + /// + public static IReadOnlyCollection AllBasicTypes { get; } = new ReadOnlyCollection(BasicTypesInfo.Value.Keys.ToArray()); + + /// + /// Gets all numeric types including their nullable counterparts. + /// Note that Booleans and Guids are not considered numeric types. + /// + /// + /// All numeric types. + /// + public static IReadOnlyCollection AllNumericTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsNumeric) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Gets all numeric types without their nullable counterparts. + /// Note that Booleans and Guids are not considered numeric types. + /// + /// + /// All numeric value types. + /// + public static IReadOnlyCollection AllNumericValueTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsNumeric && !kvp.Value.IsNullableValueType) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Contains all basic value types. i.e. excludes string and nullables. + /// + /// + /// All basic value types. + /// + public static IReadOnlyCollection AllBasicValueTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsValueType) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Contains all basic value types including the string type. i.e. excludes nullables. + /// + /// + /// All basic value and string types. + /// + public static IReadOnlyCollection AllBasicValueAndStringTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsValueType || kvp.Key == typeof(string)) + .Select(kvp => kvp.Key).ToArray()); + + /// + /// Gets all nullable value types. i.e. excludes string and all basic value types. + /// + /// + /// All basic nullable value types. + /// + public static IReadOnlyCollection AllBasicNullableValueTypes { get; } = new ReadOnlyCollection( + BasicTypesInfo + .Value + .Where(kvp => kvp.Value.IsNullableValueType) + .Select(kvp => kvp.Key).ToArray()); + } +} diff --git a/Swan.Lite/Definitions.cs b/Swan.Lite/Definitions.cs new file mode 100644 index 0000000..3c4e97a --- /dev/null +++ b/Swan.Lite/Definitions.cs @@ -0,0 +1,39 @@ +using System.Text; + +namespace Swan +{ + /// + /// Contains useful constants and definitions. + /// + public static partial class Definitions + { + /// + /// The MS Windows codepage 1252 encoding used in some legacy scenarios + /// such as default CSV text encoding from Excel. + /// + public static readonly Encoding Windows1252Encoding; + + /// + /// The encoding associated with the default ANSI code page in the operating + /// system's regional and language settings. + /// + public static readonly Encoding CurrentAnsiEncoding; + + /// + /// Initializes the class. + /// + static Definitions() + { + CurrentAnsiEncoding = Encoding.GetEncoding(default(int)); + try + { + Windows1252Encoding = Encoding.GetEncoding(1252); + } + catch + { + // ignore, the codepage is not available use default + Windows1252Encoding = CurrentAnsiEncoding; + } + } + } +} diff --git a/Swan.Lite/Diagnostics/Benchmark.cs b/Swan.Lite/Diagnostics/Benchmark.cs new file mode 100644 index 0000000..d79f98b --- /dev/null +++ b/Swan.Lite/Diagnostics/Benchmark.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; + +namespace Swan.Diagnostics +{ + /// + /// A simple benchmarking class. + /// + /// + /// The following code demonstrates how to create a simple benchmark. + /// + /// namespace Examples.Benchmark.Simple + /// { + /// using Swan.Diagnostics; + /// + /// public class SimpleBenchmark + /// { + /// public static void Main() + /// { + /// using (Benchmark.Start("Test")) + /// { + /// // do some logic in here + /// } + /// + /// // dump results into a string + /// var results = Benchmark.Dump(); + /// } + /// } + /// + /// } + /// + /// + public static partial class Benchmark + { + private static readonly object SyncLock = new object(); + private static readonly Dictionary> Measures = new Dictionary>(); + + /// + /// Starts measuring with the given identifier. + /// + /// The identifier. + /// A disposable object that when disposed, adds a benchmark result. + public static IDisposable Start(string identifier) => new BenchmarkUnit(identifier); + + /// + /// Outputs the benchmark statistics. + /// + /// A string containing human-readable statistics. + public static string Dump() + { + var builder = new StringBuilder(); + + lock (SyncLock) + { + foreach (var kvp in Measures) + { + builder.Append($"BID: {kvp.Key,-30} | ") + .Append($"CNT: {kvp.Value.Count,6} | ") + .Append($"AVG: {kvp.Value.Average(t => t.TotalMilliseconds),8:0.000} ms. | ") + .Append($"MAX: {kvp.Value.Max(t => t.TotalMilliseconds),8:0.000} ms. | ") + .Append($"MIN: {kvp.Value.Min(t => t.TotalMilliseconds),8:0.000} ms. | ") + .Append(Environment.NewLine); + } + } + + return builder.ToString().TrimEnd(); + } + + /// + /// Measures the elapsed time of the given action as a TimeSpan + /// This method uses a high precision Stopwatch if it is available. + /// + /// The target. + /// + /// A time interval that represents a specified time, where the specification is in units of ticks. + /// + /// target. + public static TimeSpan BenchmarkAction(Action target) + { + if (target == null) + throw new ArgumentNullException(nameof(target)); + + var sw = Stopwatch.IsHighResolution ? new HighResolutionTimer() : new Stopwatch(); + + try + { + sw.Start(); + target.Invoke(); + } + catch + { + // swallow + } + finally + { + sw.Stop(); + } + + return TimeSpan.FromTicks(sw.ElapsedTicks); + } + + /// + /// Adds the specified result to the given identifier. + /// + /// The identifier. + /// The elapsed. + private static void Add(string identifier, TimeSpan elapsed) + { + lock (SyncLock) + { + if (Measures.ContainsKey(identifier) == false) + Measures[identifier] = new List(1024 * 1024); + + Measures[identifier].Add(elapsed); + } + } + } +} diff --git a/Swan.Lite/Diagnostics/BenchmarkUnit.cs b/Swan.Lite/Diagnostics/BenchmarkUnit.cs new file mode 100644 index 0000000..e896995 --- /dev/null +++ b/Swan.Lite/Diagnostics/BenchmarkUnit.cs @@ -0,0 +1,50 @@ +using System; +using System.Diagnostics; + +namespace Swan.Diagnostics +{ + public static partial class Benchmark + { + /// + /// Represents a disposable benchmark unit. + /// + /// + private sealed class BenchmarkUnit : IDisposable + { + private readonly string _identifier; + private bool _isDisposed; // To detect redundant calls + private Stopwatch? _stopwatch = new Stopwatch(); + + /// + /// Initializes a new instance of the class. + /// + /// The identifier. + public BenchmarkUnit(string identifier) + { + _identifier = identifier; + _stopwatch?.Start(); + } + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(bool alsoManaged) + { + if (_isDisposed) return; + + if (alsoManaged) + { + Add(_identifier, _stopwatch?.Elapsed ?? default); + _stopwatch?.Stop(); + } + + _stopwatch = null; + _isDisposed = true; + } + } + } +} diff --git a/Swan.Lite/Diagnostics/HighResolutionTimer.cs b/Swan.Lite/Diagnostics/HighResolutionTimer.cs new file mode 100644 index 0000000..b419d09 --- /dev/null +++ b/Swan.Lite/Diagnostics/HighResolutionTimer.cs @@ -0,0 +1,32 @@ +namespace Swan.Diagnostics +{ + using System; + using System.Diagnostics; + + /// + /// Provides access to a high-resolution, time measuring device. + /// + /// + public class HighResolutionTimer : Stopwatch + { + /// + /// Initializes a new instance of the class. + /// + /// High-resolution timer not available. + public HighResolutionTimer() + { + if (!IsHighResolution) + throw new NotSupportedException("High-resolution timer not available"); + } + + /// + /// Gets the number of microseconds per timer tick. + /// + public static double MicrosecondsPerTick { get; } = 1000000d / Frequency; + + /// + /// Gets the elapsed microseconds. + /// + public long ElapsedMicroseconds => (long)(ElapsedTicks * MicrosecondsPerTick); + } +} diff --git a/Swan.Lite/EnumHelper.cs b/Swan.Lite/EnumHelper.cs new file mode 100644 index 0000000..cf49ec0 --- /dev/null +++ b/Swan.Lite/EnumHelper.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Swan.Collections; + +namespace Swan +{ + /// + /// Provide Enumerations helpers with internal cache. + /// + public class EnumHelper + : SingletonBase>> + { + /// + /// Gets all the names and enumerators from a specific Enum type. + /// + /// The type of the attribute to be retrieved. + /// A tuple of enumerator names and their value stored for the specified type. + public static IEnumerable> Retrieve() + where T : struct, IConvertible + { + return Instance.Retrieve(typeof(T), t => Enum.GetValues(t) + .Cast() + .Select(item => Tuple.Create(Enum.GetName(t, item), item))); + } + + /// + /// Gets the cached items with the enum item value. + /// + /// The type of enumeration. + /// if set to true [humanize]. + /// + /// A collection of Type/Tuple pairs + /// that represents items with the enum item value. + /// + public static IEnumerable> GetItemsWithValue(bool humanize = true) + where T : struct, IConvertible + { + return Retrieve() + .Select(x => Tuple.Create((int) x.Item2, humanize ? x.Item1.Humanize() : x.Item1)); + } + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(int value, bool ignoreZero = false) + where TEnum : struct, IConvertible + { + return Retrieve() + .Select(x => (int) x.Item2) + .When(() => ignoreZero, q => q.Where(f => f != 0)) + .Where(x => (x & value) == x); + } + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(long value, bool ignoreZero = false) + where TEnum : struct, IConvertible + { + return Retrieve() + .Select(x => (long) x.Item2) + .When(() => ignoreZero, q => q.Where(f => f != 0)) + .Where(x => (x & value) == x); + } + + /// + /// Gets the flag values. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// + /// A list of values in the flag. + /// + public static IEnumerable GetFlagValues(byte value, bool ignoreZero = false) + where TEnum : struct, IConvertible + { + return Retrieve() + .Select(x => (byte) x.Item2) + .When(() => ignoreZero, q => q.Where(f => f != 0)) + .Where(x => (x & value) == x); + } + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// the value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(int value, bool ignoreZero = false, bool humanize = true) + where TEnum : struct, IConvertible + { + return Retrieve() + .When(() => ignoreZero, q => q.Where(f => (int) f.Item2 != 0)) + .Where(x => ((int) x.Item2 & value) == (int) x.Item2) + .Select(x => humanize ? x.Item1.Humanize() : x.Item1); + } + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(long value, bool ignoreZero = false, bool humanize = true) + where TEnum : struct, IConvertible + { + return Retrieve() + .When(() => ignoreZero, q => q.Where(f => (long) f.Item2 != 0)) + .Where(x => ((long) x.Item2 & value) == (long) x.Item2) + .Select(x => humanize ? x.Item1.Humanize() : x.Item1); + } + + /// + /// Gets the flag names. + /// + /// The type of the enum. + /// The value. + /// if set to true [ignore zero]. + /// if set to true [humanize]. + /// + /// A list of flag names. + /// + public static IEnumerable GetFlagNames(byte value, bool ignoreZero = false, bool humanize = true) + where TEnum : struct, IConvertible + { + return Retrieve() + .When(() => ignoreZero, q => q.Where(f => (byte) f.Item2 != 0)) + .Where(x => ((byte) x.Item2 & value) == (byte) x.Item2) + .Select(x => humanize ? x.Item1.Humanize() : x.Item1); + } + + /// + /// Gets the cached items with the enum item index. + /// + /// The type of enumeration. + /// if set to true [humanize]. + /// + /// A collection of Type/Tuple pairs that represents items with the enum item value. + /// + public static IEnumerable> GetItemsWithIndex(bool humanize = true) + where T : struct, IConvertible + { + var i = 0; + + return Retrieve() + .Select(x => Tuple.Create(i++, humanize ? x.Item1.Humanize() : x.Item1)); + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Enums.cs b/Swan.Lite/Enums.cs new file mode 100644 index 0000000..38d2b42 --- /dev/null +++ b/Swan.Lite/Enums.cs @@ -0,0 +1,65 @@ +namespace Swan +{ + /// + /// Enumeration of Operating Systems. + /// + public enum OperatingSystem + { + /// + /// Unknown OS + /// + Unknown, + + /// + /// Windows + /// + Windows, + + /// + /// UNIX/Linux + /// + Unix, + + /// + /// macOS (OSX) + /// + Osx, + } + + /// + /// Defines Endianness, big or little. + /// + public enum Endianness + { + /// + /// In big endian, you store the most significant byte in the smallest address. + /// + Big, + + /// + /// In little endian, you store the least significant byte in the smallest address. + /// + Little, + } + + /// + /// Enumerates the JSON serializer cases to use: None (keeps the same case), PascalCase, or camelCase. + /// + public enum JsonSerializerCase + { + /// + /// The none + /// + None, + + /// + /// The pascal case (eg. PascalCase) + /// + PascalCase, + + /// + /// The camel case (eg. camelCase) + /// + CamelCase, + } +} diff --git a/Swan.Lite/Extensions.ByteArrays.cs b/Swan.Lite/Extensions.ByteArrays.cs new file mode 100644 index 0000000..3888912 --- /dev/null +++ b/Swan.Lite/Extensions.ByteArrays.cs @@ -0,0 +1,504 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Swan +{ + /// + /// Provides various extension methods for byte arrays and streams. + /// + public static class ByteArrayExtensions + { + /// + /// Converts an array of bytes to its lower-case, hexadecimal representation. + /// + /// The bytes. + /// if set to true add the 0x prefix tot he output. + /// + /// The specified string instance; no actual conversion is performed. + /// + /// bytes. + public static string ToLowerHex(this byte[] bytes, bool addPrefix = false) + => ToHex(bytes, addPrefix, "x2"); + + /// + /// Converts an array of bytes to its upper-case, hexadecimal representation. + /// + /// The bytes. + /// if set to true [add prefix]. + /// + /// The specified string instance; no actual conversion is performed. + /// + /// bytes. + public static string ToUpperHex(this byte[] bytes, bool addPrefix = false) + => ToHex(bytes, addPrefix, "X2"); + + /// + /// Converts an array of bytes to a sequence of dash-separated, hexadecimal, + /// uppercase characters. + /// + /// The bytes. + /// + /// A string of hexadecimal pairs separated by hyphens, where each pair represents + /// the corresponding element in value; for example, "7F-2C-4A-00". + /// + public static string ToDashedHex(this byte[] bytes) => BitConverter.ToString(bytes); + + /// + /// Converts an array of bytes to a base-64 encoded string. + /// + /// The bytes. + /// A converted from an array of bytes. + public static string ToBase64(this byte[] bytes) => Convert.ToBase64String(bytes); + + /// + /// Converts a set of hexadecimal characters (uppercase or lowercase) + /// to a byte array. String length must be a multiple of 2 and + /// any prefix (such as 0x) has to be avoided for this to work properly. + /// + /// The hexadecimal. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// hex. + public static byte[] ConvertHexadecimalToBytes(this string @this) + { + if (string.IsNullOrWhiteSpace(@this)) + throw new ArgumentNullException(nameof(@this)); + + return Enumerable + .Range(0, @this.Length / 2) + .Select(x => Convert.ToByte(@this.Substring(x * 2, 2), 16)) + .ToArray(); + } + + /// + /// Gets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The length. + /// + /// Bit value at the given offset. + /// + public static byte GetBitValueAt(this byte @this, byte offset, byte length = 1) => (byte)((@this >> offset) & ~(0xff << length)); + + /// + /// Sets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The length. + /// The value. + /// Bit value at the given offset. + public static byte SetBitValueAt(this byte @this, byte offset, byte length, byte value) + { + var mask = ~(0xff << length); + var valueAt = (byte)(value & mask); + + return (byte)((valueAt << offset) | (@this & ~(mask << offset))); + } + + /// + /// Sets the bit value at the given offset. + /// + /// The b. + /// The offset. + /// The value. + /// Bit value at the given offset. + public static byte SetBitValueAt(this byte @this, byte offset, byte value) => @this.SetBitValueAt(offset, 1, value); + + /// + /// Splits a byte array delimited by the specified sequence of bytes. + /// Each individual element in the result will contain the split sequence terminator if it is found to be delimited by it. + /// For example if you split [1,2,3,4] by a sequence of [2,3] this method will return a list with 2 byte arrays, one containing [1,2,3] and the + /// second one containing 4. Use the Trim extension methods to remove terminator sequences. + /// + /// The buffer. + /// The offset at which to start splitting bytes. Any bytes before this will be discarded. + /// The sequence. + /// + /// A byte array containing the results the specified sequence of bytes. + /// + /// + /// buffer + /// or + /// sequence. + /// + public static List Split(this byte[] @this, int offset, params byte[] sequence) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + if (sequence == null) + throw new ArgumentNullException(nameof(sequence)); + + var seqOffset = offset.Clamp(0, @this.Length - 1); + + var result = new List(); + + while (seqOffset < @this.Length) + { + var separatorStartIndex = @this.GetIndexOf(sequence, seqOffset); + + if (separatorStartIndex >= 0) + { + var item = new byte[separatorStartIndex - seqOffset + sequence.Length]; + Array.Copy(@this, seqOffset, item, 0, item.Length); + result.Add(item); + seqOffset += item.Length; + } + else + { + var item = new byte[@this.Length - seqOffset]; + Array.Copy(@this, seqOffset, item, 0, item.Length); + result.Add(item); + break; + } + } + + return result; + } + + /// + /// Clones the specified buffer, byte by byte. + /// + /// The buffer. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// this + public static byte[] DeepClone(this byte[] @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var result = new byte[@this.Length]; + Array.Copy(@this, result, @this.Length); + return result; + } + + /// + /// Removes the specified sequence from the start of the buffer if the buffer begins with such sequence. + /// + /// The buffer. + /// The sequence. + /// + /// A new trimmed byte array. + /// + /// buffer. + public static byte[] TrimStart(this byte[] buffer, params byte[] sequence) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + if (buffer.StartsWith(sequence) == false) + return buffer.DeepClone(); + + var result = new byte[buffer.Length - sequence.Length]; + Array.Copy(buffer, sequence.Length, result, 0, result.Length); + return result; + } + + /// + /// Removes the specified sequence from the end of the buffer if the buffer ends with such sequence. + /// + /// The buffer. + /// The sequence. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// buffer. + public static byte[] TrimEnd(this byte[] buffer, params byte[] sequence) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + if (buffer.EndsWith(sequence) == false) + return buffer.DeepClone(); + + var result = new byte[buffer.Length - sequence.Length]; + Array.Copy(buffer, 0, result, 0, result.Length); + return result; + } + + /// + /// Removes the specified sequence from the end and the start of the buffer + /// if the buffer ends and/or starts with such sequence. + /// + /// The buffer. + /// The sequence. + /// A byte array containing the results of encoding the specified set of characters. + public static byte[] Trim(this byte[] buffer, params byte[] sequence) + { + var trimStart = buffer.TrimStart(sequence); + return trimStart.TrimEnd(sequence); + } + + /// + /// Determines if the specified buffer ends with the given sequence of bytes. + /// + /// The buffer. + /// The sequence. + /// + /// True if the specified buffer is ends; otherwise, false. + /// + /// buffer. + public static bool EndsWith(this byte[] buffer, params byte[] sequence) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + var startIndex = buffer.Length - sequence.Length; + return buffer.GetIndexOf(sequence, startIndex) == startIndex; + } + + /// + /// Determines if the specified buffer starts with the given sequence of bytes. + /// + /// The buffer. + /// The sequence. + /// true if the specified buffer starts; otherwise, false. + public static bool StartsWith(this byte[] buffer, params byte[] sequence) => buffer.GetIndexOf(sequence) == 0; + + /// + /// Determines whether the buffer contains the specified sequence. + /// + /// The buffer. + /// The sequence. + /// + /// true if [contains] [the specified sequence]; otherwise, false. + /// + public static bool Contains(this byte[] buffer, params byte[] sequence) => buffer.GetIndexOf(sequence) >= 0; + + /// + /// Determines whether the buffer exactly matches, byte by byte the specified sequence. + /// + /// The buffer. + /// The sequence. + /// + /// true if [is equal to] [the specified sequence]; otherwise, false. + /// + /// buffer. + public static bool IsEqualTo(this byte[] buffer, params byte[] sequence) + { + if (ReferenceEquals(buffer, sequence)) + return true; + + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + return buffer.Length == sequence.Length && buffer.GetIndexOf(sequence) == 0; + } + + /// + /// Returns the first instance of the matched sequence based on the given offset. + /// If no matches are found then this method returns -1. + /// + /// The buffer. + /// The sequence. + /// The offset. + /// The index of the sequence. + /// + /// buffer + /// or + /// sequence. + /// + public static int GetIndexOf(this byte[] buffer, byte[] sequence, int offset = 0) + { + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + if (sequence == null) + throw new ArgumentNullException(nameof(sequence)); + if (sequence.Length == 0) + return -1; + if (sequence.Length > buffer.Length) + return -1; + + var seqOffset = offset < 0 ? 0 : offset; + + var matchedCount = 0; + for (var i = seqOffset; i < buffer.Length; i++) + { + if (buffer[i] == sequence[matchedCount]) + matchedCount++; + else + matchedCount = 0; + + if (matchedCount == sequence.Length) + return i - (matchedCount - 1); + } + + return -1; + } + + /// + /// Appends the Memory Stream with the specified buffer. + /// + /// The stream. + /// The buffer. + /// + /// The same MemoryStream instance. + /// + /// + /// stream + /// or + /// buffer. + /// + public static MemoryStream Append(this MemoryStream stream, byte[] buffer) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + if (buffer == null) + throw new ArgumentNullException(nameof(buffer)); + + stream.Write(buffer, 0, buffer.Length); + return stream; + } + + /// + /// Appends the Memory Stream with the specified buffer. + /// + /// The stream. + /// The buffer. + /// + /// Block of bytes to the current stream using data read from a buffer. + /// + /// buffer. + public static MemoryStream Append(this MemoryStream stream, IEnumerable buffer) => Append(stream, buffer?.ToArray()); + + /// + /// Appends the Memory Stream with the specified set of buffers. + /// + /// The stream. + /// The buffers. + /// + /// Block of bytes to the current stream using data read from a buffer. + /// + /// buffers. + public static MemoryStream Append(this MemoryStream stream, IEnumerable buffers) + { + if (buffers == null) + throw new ArgumentNullException(nameof(buffers)); + + foreach (var buffer in buffers) + Append(stream, buffer); + + return stream; + } + + /// + /// Converts an array of bytes into text with the specified encoding. + /// + /// The buffer. + /// The encoding. + /// A that contains the results of decoding the specified sequence of bytes. + public static string ToText(this IEnumerable buffer, Encoding encoding) => encoding.GetString(buffer.ToArray()); + + /// + /// Converts an array of bytes into text with UTF8 encoding. + /// + /// The buffer. + /// A that contains the results of decoding the specified sequence of bytes. + public static string ToText(this IEnumerable buffer) => buffer.ToText(Encoding.UTF8); + + /// + /// Reads the bytes asynchronous. + /// + /// The stream. + /// The length. + /// Length of the buffer. + /// The cancellation token. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// stream. + public static async Task ReadBytesAsync(this Stream stream, long length, int bufferLength, CancellationToken cancellationToken = default) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + using (var dest = new MemoryStream()) + { + try + { + var buff = new byte[bufferLength]; + while (length > 0) + { + if (length < bufferLength) + bufferLength = (int)length; + + var nread = await stream.ReadAsync(buff, 0, bufferLength, cancellationToken).ConfigureAwait(false); + if (nread == 0) + break; + + dest.Write(buff, 0, nread); + length -= nread; + } + } + catch + { + // ignored + } + + return dest.ToArray(); + } + } + + /// + /// Reads the bytes asynchronous. + /// + /// The stream. + /// The length. + /// The cancellation token. + /// + /// A byte array containing the results of encoding the specified set of characters. + /// + /// stream. + public static async Task ReadBytesAsync(this Stream stream, int length, CancellationToken cancellationToken = default) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + var buff = new byte[length]; + var offset = 0; + try + { + while (length > 0) + { + var nread = await stream.ReadAsync(buff, offset, length, cancellationToken).ConfigureAwait(false); + if (nread == 0) + break; + + offset += nread; + length -= nread; + } + } + catch + { + // ignored + } + + return new ArraySegment(buff, 0, offset).ToArray(); + } + + private static string ToHex(byte[] bytes, bool addPrefix, string format) + { + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + + var sb = new StringBuilder(bytes.Length * 2); + + foreach (var item in bytes) + sb.Append(item.ToString(format, CultureInfo.InvariantCulture)); + + return $"{(addPrefix ? "0x" : string.Empty)}{sb}"; + } + } +} diff --git a/Swan.Lite/Extensions.ComponentCollections.cs b/Swan.Lite/Extensions.ComponentCollections.cs new file mode 100644 index 0000000..f14b782 --- /dev/null +++ b/Swan.Lite/Extensions.ComponentCollections.cs @@ -0,0 +1,21 @@ +using System; +using Swan.Collections; + +namespace Swan +{ + /// + /// Provides extension methods for types implementing . + /// + public static class ComponentCollectionExtensions + { + /// + /// Adds the specified component to a collection, without giving it a name. + /// + /// The type of components in the collection. + /// The on which this method is called. + /// The component to add. + /// is . + /// + public static void Add(this IComponentCollection @this, T component) => @this.Add(null, component); + } +} \ No newline at end of file diff --git a/Swan.Lite/Extensions.Dates.cs b/Swan.Lite/Extensions.Dates.cs new file mode 100644 index 0000000..811df20 --- /dev/null +++ b/Swan.Lite/Extensions.Dates.cs @@ -0,0 +1,234 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Swan +{ + /// + /// Provides extension methods for . + /// + public static class DateExtensions + { + private static readonly Dictionary DateRanges = new Dictionary() + { + { "minute", 59}, + { "hour", 23}, + { "dayOfMonth", 31}, + { "month", 12}, + { "dayOfWeek", 6}, + }; + + /// + /// Converts the date to a YYYY-MM-DD string. + /// + /// The on which this method is called. + /// The concatenation of date.Year, date.Month and date.Day. + public static string ToSortableDate(this DateTime @this) + => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00}"; + + /// + /// Converts the date to a YYYY-MM-DD HH:II:SS string. + /// + /// The on which this method is called. + /// The concatenation of date.Year, date.Month, date.Day, date.Hour, date.Minute and date.Second. + public static string ToSortableDateTime(this DateTime @this) + => $"{@this.Year:0000}-{@this.Month:00}-{@this.Day:00} {@this.Hour:00}:{@this.Minute:00}:{@this.Second:00}"; + + /// + /// Parses a YYYY-MM-DD and optionally it time part, HH:II:SS into a DateTime. + /// + /// The sortable date. + /// + /// A new instance of the DateTime structure to + /// the specified year, month, day, hour, minute and second. + /// + /// sortableDate. + /// + /// Represents errors that occur during application execution. + /// + /// + /// Unable to parse sortable date and time. - sortableDate. + /// + public static DateTime ToDateTime(this string @this) + { + if (string.IsNullOrWhiteSpace(@this)) + throw new ArgumentNullException(nameof(@this)); + + var hour = 0; + var minute = 0; + var second = 0; + + var dateTimeParts = @this.Split(' '); + + try + { + if (dateTimeParts.Length != 1 && dateTimeParts.Length != 2) + throw new Exception(); + + var dateParts = dateTimeParts[0].Split('-'); + if (dateParts.Length != 3) throw new Exception(); + + var year = int.Parse(dateParts[0]); + var month = int.Parse(dateParts[1]); + var day = int.Parse(dateParts[2]); + + if (dateTimeParts.Length > 1) + { + var timeParts = dateTimeParts[1].Split(':'); + if (timeParts.Length != 3) throw new Exception(); + + hour = int.Parse(timeParts[0]); + minute = int.Parse(timeParts[1]); + second = int.Parse(timeParts[2]); + } + + return new DateTime(year, month, day, hour, minute, second); + } + catch (Exception) + { + throw new ArgumentException("Unable to parse sortable date and time.", nameof(@this)); + } + } + + /// + /// Creates a date range. + /// + /// The start date. + /// The end date. + /// + /// A sequence of integral numbers within a specified date's range. + /// + public static IEnumerable DateRange(this DateTime startDate, DateTime endDate) + => Enumerable.Range(0, (endDate - startDate).Days + 1).Select(d => startDate.AddDays(d)); + + /// + /// Rounds up a date to match a timespan. + /// + /// The datetime. + /// The timespan to match. + /// + /// A new instance of the DateTime structure to the specified datetime and timespan ticks. + /// + public static DateTime RoundUp(this DateTime date, TimeSpan timeSpan) + => new DateTime(((date.Ticks + timeSpan.Ticks - 1) / timeSpan.Ticks) * timeSpan.Ticks); + + /// + /// Get this datetime as a Unix epoch timestamp (seconds since Jan 1, 1970, midnight UTC). + /// + /// The on which this method is called. + /// Seconds since Unix epoch. + public static long ToUnixEpochDate(this DateTime @this) => new DateTimeOffset(@this).ToUniversalTime().ToUnixTimeSeconds(); + + /// + /// Compares a Date to another and returns a DateTimeSpan. + /// + /// The date start. + /// The date end. + /// A DateTimeSpan with the Years, Months, Days, Hours, Minutes, Seconds and Milliseconds between the dates. + public static DateTimeSpan GetDateTimeSpan(this DateTime dateStart, DateTime dateEnd) + => DateTimeSpan.CompareDates(dateStart, dateEnd); + + /// + /// Compare the Date elements(Months, Days, Hours, Minutes). + /// + /// The on which this method is called. + /// The minute (0-59). + /// The hour. (0-23). + /// The day of month. (1-31). + /// The month. (1-12). + /// The day of week. (0-6)(Sunday = 0). + /// Returns true if Months, Days, Hours and Minutes match, otherwise false. + public static bool AsCronCanRun(this DateTime @this, int? minute = null, int? hour = null, int? dayOfMonth = null, int? month = null, int? dayOfWeek = null) + { + var results = new List + { + GetElementParts(minute, @this.Minute), + GetElementParts(hour, @this.Hour), + GetElementParts(dayOfMonth, @this.Day), + GetElementParts(month, @this.Month), + GetElementParts(dayOfWeek, (int) @this.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + /// + /// Compare the Date elements(Months, Days, Hours, Minutes). + /// + /// The on which this method is called. + /// The minute (0-59). + /// The hour. (0-23). + /// The day of month. (1-31). + /// The month. (1-12). + /// The day of week. (0-6)(Sunday = 0). + /// Returns true if Months, Days, Hours and Minutes match, otherwise false. + public static bool AsCronCanRun(this DateTime @this, string minute = "*", string hour = "*", string dayOfMonth = "*", string month = "*", string dayOfWeek = "*") + { + var results = new List + { + GetElementParts(minute, nameof(minute), @this.Minute), + GetElementParts(hour, nameof(hour), @this.Hour), + GetElementParts(dayOfMonth, nameof(dayOfMonth), @this.Day), + GetElementParts(month, nameof(month), @this.Month), + GetElementParts(dayOfWeek, nameof(dayOfWeek), (int) @this.DayOfWeek), + }; + + return results.Any(x => x != false); + } + + /// + /// Converts a to the RFC1123 format. + /// + /// The on which this method is called. + /// The string representation of according to RFC1123. + /// + /// If is not a UTC date / time, its UTC equivalent is converted, leaving unchanged. + /// + public static string ToRfc1123String(this DateTime @this) + => @this.ToUniversalTime().ToString("R", CultureInfo.InvariantCulture); + + private static bool? GetElementParts(int? status, int value) => status.HasValue ? status.Value == value : (bool?) null; + + private static bool? GetElementParts(string parts, string type, int value) + { + if (string.IsNullOrWhiteSpace(parts) || parts == "*") + return null; + + if (parts.Contains(",")) + { + return parts.Split(',').Select(int.Parse).Contains(value); + } + + var stop = DateRanges[type]; + + if (parts.Contains("/")) + { + var multiple = int.Parse(parts.Split('/').Last()); + var start = type == "dayOfMonth" || type == "month" ? 1 : 0; + + for (var i = start; i <= stop; i += multiple) + if (i == value) return true; + + return false; + } + + if (parts.Contains("-")) + { + var range = parts.Split('-'); + var start = int.Parse(range.First()); + stop = Math.Max(stop, int.Parse(range.Last())); + + if ((type == "dayOfMonth" || type == "month") && start == 0) + start = 1; + + for (var i = start; i <= stop; i++) + if (i == value) return true; + + return false; + } + + return int.Parse(parts) == value; + } + } +} diff --git a/Swan.Lite/Extensions.Dictionaries.cs b/Swan.Lite/Extensions.Dictionaries.cs new file mode 100644 index 0000000..bb830cf --- /dev/null +++ b/Swan.Lite/Extensions.Dictionaries.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; + +namespace Swan +{ + /// + /// Extension methods. + /// + public static partial class Extensions + { + /// + /// Gets the value if exists or default. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The key. + /// The default value. + /// + /// The value of the provided key or default. + /// + /// dict. + public static TValue GetValueOrDefault(this IDictionary dict, TKey key, TValue defaultValue = default) + { + if (dict == null) + throw new ArgumentNullException(nameof(dict)); + + return dict.ContainsKey(key) ? dict[key] : defaultValue; + } + + /// + /// Adds a key/value pair to the Dictionary if the key does not already exist. + /// If the value is null, the key will not be updated. + /// Based on ConcurrentDictionary.GetOrAdd method. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The key. + /// The value factory. + /// + /// The value for the key. + /// + /// + /// dict + /// or + /// valueFactory. + /// + public static TValue GetOrAdd(this IDictionary dict, TKey key, Func valueFactory) + { + if (dict == null) + throw new ArgumentNullException(nameof(dict)); + + if (valueFactory == null) + throw new ArgumentNullException(nameof(valueFactory)); + + if (!dict.ContainsKey(key)) + { + var value = valueFactory(key); + if (Equals(value, default)) return default; + dict[key] = value; + } + + return dict[key]; + } + + /// + /// Executes the item action for each element in the Dictionary. + /// + /// The type of the key. + /// The type of the value. + /// The dictionary. + /// The item action. + /// dict. + public static void ForEach(this IDictionary dict, Action itemAction) + { + if (dict == null) + throw new ArgumentNullException(nameof(dict)); + + foreach (var kvp in dict) + { + itemAction(kvp.Key, kvp.Value); + } + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Extensions.Exceptions.cs b/Swan.Lite/Extensions.Exceptions.cs new file mode 100644 index 0000000..dc75679 --- /dev/null +++ b/Swan.Lite/Extensions.Exceptions.cs @@ -0,0 +1,50 @@ +using System; +using System.Linq; +using System.Threading; + +namespace Swan +{ + /// + /// Provides extension methods for . + /// + public static class ExceptionExtensions + { + /// + /// Returns a value that tells whether an is of a type that + /// we better not catch and ignore. + /// + /// The exception being thrown. + /// if is a critical exception; + /// otherwise, . + public static bool IsCriticalException(this Exception @this) + => @this.IsCriticalExceptionCore() + || (@this.InnerException?.IsCriticalException() ?? false) + || (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsCriticalException())); + + /// + /// Returns a value that tells whether an is of a type that + /// will likely cause application failure. + /// + /// The exception being thrown. + /// if is a fatal exception; + /// otherwise, . + public static bool IsFatalException(this Exception @this) + => @this.IsFatalExceptionCore() + || (@this.InnerException?.IsFatalException() ?? false) + || (@this is AggregateException aggregateException && aggregateException.InnerExceptions.Any(e => e.IsFatalException())); + + private static bool IsCriticalExceptionCore(this Exception @this) + => IsFatalExceptionCore(@this) + || @this is AppDomainUnloadedException + || @this is BadImageFormatException + || @this is CannotUnloadAppDomainException + || @this is InvalidProgramException + || @this is NullReferenceException; + + private static bool IsFatalExceptionCore(this Exception @this) + => @this is StackOverflowException + || @this is OutOfMemoryException + || @this is ThreadAbortException + || @this is AccessViolationException; + } +} \ No newline at end of file diff --git a/Swan.Lite/Extensions.Functional.cs b/Swan.Lite/Extensions.Functional.cs new file mode 100644 index 0000000..dcbcbf9 --- /dev/null +++ b/Swan.Lite/Extensions.Functional.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan +{ + /// + /// Functional programming extension methods. + /// + public static class FunctionalExtensions + { + /// + /// Whens the specified condition. + /// + /// The type of IQueryable. + /// The list. + /// The condition. + /// The function. + /// + /// The IQueryable. + /// + /// + /// this + /// or + /// condition + /// or + /// fn. + /// + public static IQueryable When( + this IQueryable list, + Func condition, + Func, IQueryable> fn) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (fn == null) + throw new ArgumentNullException(nameof(fn)); + + return condition() ? fn(list) : list; + } + + /// + /// Whens the specified condition. + /// + /// The type of IEnumerable. + /// The list. + /// The condition. + /// The function. + /// + /// The IEnumerable. + /// + /// + /// this + /// or + /// condition + /// or + /// fn. + /// + public static IEnumerable When( + this IEnumerable list, + Func condition, + Func, IEnumerable> fn) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (fn == null) + throw new ArgumentNullException(nameof(fn)); + + return condition() ? fn(list) : list; + } + + /// + /// Adds the value when the condition is true. + /// + /// The type of IList element. + /// The list. + /// The condition. + /// The value. + /// + /// The IList. + /// + /// + /// this + /// or + /// condition + /// or + /// value. + /// + public static IList AddWhen( + this IList list, + Func condition, + Func value) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (condition()) + list.Add(value()); + + return list; + } + + /// + /// Adds the value when the condition is true. + /// + /// The type of IList element. + /// The list. + /// if set to true [condition]. + /// The value. + /// + /// The IList. + /// + /// list. + public static IList AddWhen( + this IList list, + bool condition, + T value) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition) + list.Add(value); + + return list; + } + + /// + /// Adds the range when the condition is true. + /// + /// The type of List element. + /// The list. + /// The condition. + /// The value. + /// + /// The List. + /// + /// + /// this + /// or + /// condition + /// or + /// value. + /// + public static List AddRangeWhen( + this List list, + Func condition, + Func> value) + { + if (list == null) + throw new ArgumentNullException(nameof(list)); + + if (condition == null) + throw new ArgumentNullException(nameof(condition)); + + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (condition()) + list.AddRange(value()); + + return list; + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Extensions.Reflection.cs b/Swan.Lite/Extensions.Reflection.cs new file mode 100644 index 0000000..93ea372 --- /dev/null +++ b/Swan.Lite/Extensions.Reflection.cs @@ -0,0 +1,455 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Swan.Configuration; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Provides various extension methods for Reflection and Types. + /// + public static class ReflectionExtensions + { + private static readonly Lazy, Func>> CacheGetMethods = + new Lazy, Func>>(() => new ConcurrentDictionary, Func>(), true); + + private static readonly Lazy, Action>> CacheSetMethods = + new Lazy, Action>>(() => new ConcurrentDictionary, Action>(), true); + + #region Assembly Extensions + + /// + /// Gets all types within an assembly in a safe manner. + /// + /// The assembly. + /// + /// Array of Type objects representing the types specified by an assembly. + /// + /// assembly. + public static IEnumerable GetAllTypes(this Assembly assembly) + { + if (assembly == null) + throw new ArgumentNullException(nameof(assembly)); + + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException e) + { + return e.Types.Where(t => t != null); + } + } + + #endregion + + #region Type Extensions + + /// + /// The closest programmatic equivalent of default(T). + /// + /// The type. + /// + /// Default value of this type. + /// + /// type. + public static object? GetDefault(this Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + return type.IsValueType ? Activator.CreateInstance(type) : default; + } + + /// + /// Determines whether this type is compatible with ICollection. + /// + /// The type. + /// + /// true if the specified source type is collection; otherwise, false. + /// + /// sourceType. + public static bool IsCollection(this Type sourceType) + { + if (sourceType == null) + throw new ArgumentNullException(nameof(sourceType)); + + return sourceType != typeof(string) && + typeof(IEnumerable).IsAssignableFrom(sourceType); + } + + /// + /// Gets a method from a type given the method name, binding flags, generic types and parameter types. + /// + /// Type of the source. + /// The binding flags. + /// Name of the method. + /// The generic types. + /// The parameter types. + /// + /// An object that represents the method with the specified name. + /// + /// + /// The exception that is thrown when binding to a member results in more than one member matching the + /// binding criteria. This class cannot be inherited. + /// + public static MethodInfo GetMethod( + this Type type, + BindingFlags bindingFlags, + string methodName, + Type[] genericTypes, + Type[] parameterTypes) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (methodName == null) + throw new ArgumentNullException(nameof(methodName)); + + if (genericTypes == null) + throw new ArgumentNullException(nameof(genericTypes)); + + if (parameterTypes == null) + throw new ArgumentNullException(nameof(parameterTypes)); + + var methods = type + .GetMethods(bindingFlags) + .Where(mi => string.Equals(methodName, mi.Name, StringComparison.Ordinal)) + .Where(mi => mi.ContainsGenericParameters) + .Where(mi => mi.GetGenericArguments().Length == genericTypes.Length) + .Where(mi => mi.GetParameters().Length == parameterTypes.Length) + .Select(mi => mi.MakeGenericMethod(genericTypes)) + .Where(mi => mi.GetParameters().Select(pi => pi.ParameterType).SequenceEqual(parameterTypes)) + .ToList(); + + return methods.Count > 1 ? throw new AmbiguousMatchException() : methods.FirstOrDefault(); + } + + /// + /// Determines whether [is i enumerable request]. + /// + /// The type. + /// + /// true if [is i enumerable request] [the specified type]; otherwise, false. + /// + /// type. + public static bool IsIEnumerable(this Type type) + => type == null + ? throw new ArgumentNullException(nameof(type)) + : type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>); + + #endregion + + /// + /// Tries to parse using the basic types. + /// + /// The type. + /// The value. + /// The result. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static bool TryParseBasicType(this Type type, object value, out object? result) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (type != typeof(bool)) + return TryParseBasicType(type, value.ToStringInvariant(), out result); + + result = value.ToBoolean(); + return true; + } + + /// + /// Tries to parse using the basic types. + /// + /// The type. + /// The value. + /// The result. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static bool TryParseBasicType(this Type type, string value, out object? result) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + result = null; + + return Definitions.BasicTypesInfo.Value.ContainsKey(type) && Definitions.BasicTypesInfo.Value[type].TryParse(value, out result); + } + + /// + /// Tries the type of the set basic value to a property. + /// + /// The property information. + /// The value. + /// The object. + /// + /// true if parsing was successful; otherwise, false. + /// + /// propertyInfo. + public static bool TrySetBasicType(this PropertyInfo propertyInfo, object value, object target) + { + if (propertyInfo == null) + throw new ArgumentNullException(nameof(propertyInfo)); + + try + { + if (propertyInfo.PropertyType.TryParseBasicType(value, out var propertyValue)) + { + propertyInfo.SetValue(target, propertyValue); + return true; + } + } + catch + { + // swallow + } + + return false; + } + + /// + /// Tries the type of the set to an array a basic type. + /// + /// The type. + /// The value. + /// The array. + /// The index. + /// + /// true if parsing was successful; otherwise, false. + /// + /// type + public static bool TrySetArrayBasicType(this Type type, object value, Array target, int index) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (target == null) + return false; + + try + { + if (value == null) + { + target.SetValue(null, index); + return true; + } + + if (type.TryParseBasicType(value, out var propertyValue)) + { + target.SetValue(propertyValue, index); + return true; + } + + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + target.SetValue(null, index); + return true; + } + } + catch + { + // swallow + } + + return false; + } + + /// + /// Tries to set a property array with another array. + /// + /// The property. + /// The value. + /// The object. + /// + /// true if parsing was successful; otherwise, false. + /// + /// propertyInfo. + public static bool TrySetArray(this PropertyInfo propertyInfo, IEnumerable? value, object obj) + { + if (propertyInfo == null) + throw new ArgumentNullException(nameof(propertyInfo)); + + var elementType = propertyInfo.PropertyType.GetElementType(); + + if (elementType == null || value == null) + return false; + + var targetArray = Array.CreateInstance(elementType, value.Count()); + + var i = 0; + + foreach (var sourceElement in value) + { + var result = elementType.TrySetArrayBasicType(sourceElement, targetArray, i++); + + if (!result) return false; + } + + propertyInfo.SetValue(obj, targetArray); + + return true; + } + + /// + /// Gets property actual value or PropertyDisplayAttribute.DefaultValue if presented. + /// + /// If the PropertyDisplayAttribute.Format value is presented, the property value + /// will be formatted accordingly. + /// + /// If the object contains a null value, a empty string will be returned. + /// + /// The property information. + /// The object. + /// The property value or null. + /// propertyInfo. + public static string? ToFormattedString(this PropertyInfo propertyInfo, object target) + { + if (propertyInfo == null) + throw new ArgumentNullException(nameof(propertyInfo)); + + try + { + var value = propertyInfo.GetValue(target); + var attr = AttributeCache.DefaultCache.Value.RetrieveOne(propertyInfo); + + if (attr == null) return value?.ToString() ?? string.Empty; + + var valueToFormat = value ?? attr.DefaultValue; + + return string.IsNullOrEmpty(attr.Format) + ? (valueToFormat?.ToString() ?? string.Empty) + : ConvertObjectAndFormat(propertyInfo.PropertyType, valueToFormat, attr.Format); + } + catch + { + return null; + } + } + + /// + /// Gets a MethodInfo from a Property Get method. + /// + /// The property information. + /// if set to true [non public]. + /// + /// The cached MethodInfo. + /// + public static Func? GetCacheGetMethod(this PropertyInfo propertyInfo, bool nonPublic = false) + { + var key = Tuple.Create(!nonPublic, propertyInfo); + + // TODO: Fix public logic + return !nonPublic && !CacheGetMethods.Value.ContainsKey(key) && !propertyInfo.GetGetMethod(true).IsPublic + ? null + : CacheGetMethods.Value + .GetOrAdd(key, + x => y => x.Item2.GetGetMethod(nonPublic).Invoke(y, null)); + //y => x => y.Item2.CreatePropertyProxy().GetValue(x)); + } + + /// + /// Gets a MethodInfo from a Property Set method. + /// + /// The property information. + /// if set to true [non public]. + /// + /// The cached MethodInfo. + /// + public static Action? GetCacheSetMethod(this PropertyInfo propertyInfo, bool nonPublic = false) + { + var key = Tuple.Create(!nonPublic, propertyInfo); + + return !nonPublic && !CacheSetMethods.Value.ContainsKey(key) && !propertyInfo.GetSetMethod(true).IsPublic + ? null + : CacheSetMethods.Value + .GetOrAdd(key, + x => (obj, args) => x.Item2.GetSetMethod(nonPublic).Invoke(obj, args)); + //y => (obj, args) => y.Item2.CreatePropertyProxy().SetValue(obj, args)); + } + + /// + /// Convert a string to a boolean. + /// + /// The string. + /// + /// true if the string represents a valid truly value, otherwise false. + /// + public static bool ToBoolean(this string str) + { + try + { + return Convert.ToBoolean(str); + } + catch (FormatException) + { + // ignored + } + + try + { + return Convert.ToBoolean(Convert.ToInt32(str)); + } + catch + { + // ignored + } + + return false; + } + + /// + /// Creates a property proxy that stores getter and setter delegates. + /// + /// The property information. + /// + /// The property proxy. + /// + /// this. + public static IPropertyProxy? CreatePropertyProxy(this PropertyInfo @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var genericType = typeof(PropertyProxy<,>) + .MakeGenericType(@this.DeclaringType, @this.PropertyType); + + return Activator.CreateInstance(genericType, @this) as IPropertyProxy; + } + + /// + /// Convert a object to a boolean. + /// + /// The value. + /// + /// true if the string represents a valid truly value, otherwise false. + /// + public static bool ToBoolean(this object value) => value.ToStringInvariant().ToBoolean(); + + private static string ConvertObjectAndFormat(Type propertyType, object value, string format) + { + if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) + return Convert.ToDateTime(value, CultureInfo.InvariantCulture).ToString(format); + if (propertyType == typeof(int) || propertyType == typeof(int?)) + return Convert.ToInt32(value, CultureInfo.InvariantCulture).ToString(format); + if (propertyType == typeof(decimal) || propertyType == typeof(decimal?)) + return Convert.ToDecimal(value, CultureInfo.InvariantCulture).ToString(format); + if (propertyType == typeof(double) || propertyType == typeof(double?)) + return Convert.ToDouble(value, CultureInfo.InvariantCulture).ToString(format); + if (propertyType == typeof(byte) || propertyType == typeof(byte?)) + return Convert.ToByte(value, CultureInfo.InvariantCulture).ToString(format); + + return value?.ToString() ?? string.Empty; + } + } +} diff --git a/Swan.Lite/Extensions.Strings.cs b/Swan.Lite/Extensions.Strings.cs new file mode 100644 index 0000000..a927034 --- /dev/null +++ b/Swan.Lite/Extensions.Strings.cs @@ -0,0 +1,405 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Swan.Formatters; + +namespace Swan +{ + /// + /// String related extension methods. + /// + public static class StringExtensions + { + #region Private Declarations + + private const RegexOptions StandardRegexOptions = + RegexOptions.Multiline | RegexOptions.Compiled | RegexOptions.CultureInvariant; + + private static readonly string[] ByteSuffixes = { "B", "KB", "MB", "GB", "TB" }; + + private static readonly Lazy SplitLinesRegex = + new Lazy(() => new Regex("\r\n|\r|\n", StandardRegexOptions)); + + private static readonly Lazy UnderscoreRegex = + new Lazy(() => new Regex(@"_", StandardRegexOptions)); + + private static readonly Lazy CamelCaseRegEx = + new Lazy(() => new Regex(@"[a-z][A-Z]", StandardRegexOptions)); + + private static readonly Lazy SplitCamelCaseString = new Lazy(() => m => + { + var x = m.ToString(); + return x[0] + " " + x.Substring(1, x.Length - 1); + }); + + private static readonly Lazy InvalidFilenameChars = + new Lazy(() => Path.GetInvalidFileNameChars().Select(c => c.ToString()).ToArray()); + + #endregion + + /// + /// Returns a string that represents the given item + /// It tries to use InvariantCulture if the ToString(IFormatProvider) + /// overload exists. + /// + /// The item. + /// A that represents the current object. + public static string ToStringInvariant(this object? @this) + { + if (@this == null) + return string.Empty; + + var itemType = @this.GetType(); + + if (itemType == typeof(string)) + return @this as string ?? string.Empty; + + return Definitions.BasicTypesInfo.Value.ContainsKey(itemType) + ? Definitions.BasicTypesInfo.Value[itemType].ToStringInvariant(@this) + : @this.ToString(); + } + + /// + /// Returns a string that represents the given item + /// It tries to use InvariantCulture if the ToString(IFormatProvider) + /// overload exists. + /// + /// The type to get the string. + /// The item. + /// A that represents the current object. + public static string ToStringInvariant(this T item) + => typeof(string) == typeof(T) ? item as string ?? string.Empty : ToStringInvariant(item as object); + + /// + /// Removes the control characters from a string except for those specified. + /// + /// The input. + /// When specified, these characters will not be removed. + /// + /// A string that represents the current object. + /// + /// input. + public static string RemoveControlCharsExcept(this string value, params char[] excludeChars) + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (excludeChars == null) + excludeChars = Array.Empty(); + + return new string(value + .Where(c => char.IsControl(c) == false || excludeChars.Contains(c)) + .ToArray()); + } + + /// + /// Removes all control characters from a string, including new line sequences. + /// + /// The input. + /// A that represents the current object. + /// input. + public static string RemoveControlChars(this string value) => value.RemoveControlCharsExcept(null); + + /// + /// Outputs JSON string representing this object. + /// + /// The object. + /// if set to true format the output. + /// A that represents the current object. + public static string ToJson(this object @this, bool format = true) => + @this == null ? string.Empty : Json.Serialize(@this, format); + + /// + /// Returns text representing the properties of the specified object in a human-readable format. + /// While this method is fairly expensive computationally speaking, it provides an easy way to + /// examine objects. + /// + /// The object. + /// A that represents the current object. + public static string Stringify(this object @this) + { + if (@this == null) + return "(null)"; + + try + { + var jsonText = Json.Serialize(@this, false, "$type"); + var jsonData = Json.Deserialize(jsonText); + + return new HumanizeJson(jsonData, 0).GetResult(); + } + catch + { + return @this.ToStringInvariant(); + } + } + + /// + /// Retrieves a section of the string, inclusive of both, the start and end indexes. + /// This behavior is unlike JavaScript's Slice behavior where the end index is non-inclusive + /// If the string is null it returns an empty string. + /// + /// The string. + /// The start index. + /// The end index. + /// Retrieves a substring from this instance. + public static string Slice(this string @this, int startIndex, int endIndex) + { + if (@this == null) + return string.Empty; + + var end = endIndex.Clamp(startIndex, @this.Length - 1); + + return startIndex >= end ? string.Empty : @this.Substring(startIndex, (end - startIndex) + 1); + } + + /// + /// Gets a part of the string clamping the length and startIndex parameters to safe values. + /// If the string is null it returns an empty string. This is basically just a safe version + /// of string.Substring. + /// + /// The string. + /// The start index. + /// The length. + /// Retrieves a substring from this instance. + public static string SliceLength(this string @this, int startIndex, int length) + { + if (@this == null) + return string.Empty; + + var start = startIndex.Clamp(0, @this.Length - 1); + var len = length.Clamp(0, @this.Length - start); + + return len == 0 ? string.Empty : @this.Substring(start, len); + } + + /// + /// Splits the specified text into r, n or rn separated lines. + /// + /// The text. + /// + /// An array whose elements contain the substrings from this instance + /// that are delimited by one or more characters in separator. + /// + public static string[] ToLines(this string @this) => + @this == null ? Array.Empty() : SplitLinesRegex.Value.Split(@this); + + /// + /// Humanizes (make more human-readable) an identifier-style string + /// in either camel case or snake case. For example, CamelCase will be converted to + /// Camel Case and Snake_Case will be converted to Snake Case. + /// + /// The identifier-style string. + /// A humanized. + public static string Humanize(this string value) + { + if (value == null) + return string.Empty; + + var returnValue = UnderscoreRegex.Value.Replace(value, " "); + returnValue = CamelCaseRegEx.Value.Replace(returnValue, SplitCamelCaseString.Value); + return returnValue; + } + + /// + /// Humanizes (make more human-readable) an boolean. + /// + /// if set to true [value]. + /// A that represents the current boolean. + public static string Humanize(this bool value) => value ? "Yes" : "No"; + + /// + /// Humanizes (make more human-readable) the specified value. + /// + /// The value. + /// A that represents the current object. + public static string Humanize(this object value) => + value switch + { + string stringValue => stringValue.Humanize(), + bool boolValue => boolValue.Humanize(), + _ => value.Stringify() + }; + + /// + /// Indents the specified multi-line text with the given amount of leading spaces + /// per line. + /// + /// The text. + /// The spaces. + /// A that represents the current object. + public static string Indent(this string value, int spaces = 4) + { + if (value == null) value = string.Empty; + if (spaces <= 0) return value; + + var lines = value.ToLines(); + var builder = new StringBuilder(); + var indentStr = new string(' ', spaces); + + foreach (var line in lines) + { + builder.AppendLine($"{indentStr}{line}"); + } + + return builder.ToString().TrimEnd(); + } + + /// + /// Gets the line and column number (i.e. not index) of the + /// specified character index. Useful to locate text in a multi-line + /// string the same way a text editor does. + /// Please not that the tuple contains first the line number and then the + /// column number. + /// + /// The string. + /// Index of the character. + /// A 2-tuple whose value is (item1, item2). + public static Tuple TextPositionAt(this string value, int charIndex) + { + if (value == null) + return Tuple.Create(0, 0); + + var index = charIndex.Clamp(0, value.Length - 1); + + var lineIndex = 0; + var colNumber = 0; + + for (var i = 0; i <= index; i++) + { + if (value[i] == '\n') + { + lineIndex++; + colNumber = 0; + continue; + } + + if (value[i] != '\r') + colNumber++; + } + + return Tuple.Create(lineIndex + 1, colNumber); + } + + /// + /// Makes the file name system safe. + /// + /// The s. + /// + /// A string with a safe file name. + /// + /// s. + public static string ToSafeFilename(this string value) => + value == null + ? throw new ArgumentNullException(nameof(value)) + : InvalidFilenameChars.Value + .Aggregate(value, (current, c) => current.Replace(c, string.Empty)) + .Slice(0, 220); + + /// + /// Formats a long into the closest bytes string. + /// + /// The bytes length. + /// + /// The string representation of the current Byte object, formatted as specified by the format parameter. + /// + public static string FormatBytes(this long bytes) => ((ulong)bytes).FormatBytes(); + + /// + /// Formats a long into the closest bytes string. + /// + /// The bytes length. + /// + /// A copy of format in which the format items have been replaced by the string + /// representations of the corresponding arguments. + /// + public static string FormatBytes(this ulong bytes) + { + int i; + double dblSByte = bytes; + + for (i = 0; i < ByteSuffixes.Length && bytes >= 1024; i++, bytes /= 1024) + { + dblSByte = bytes / 1024.0; + } + + return $"{dblSByte:0.##} {ByteSuffixes[i]}"; + } + + /// + /// Truncates the specified value. + /// + /// The value. + /// The maximum length. + /// + /// Retrieves a substring from this instance. + /// The substring starts at a specified character position and has a specified length. + /// + public static string? Truncate(this string value, int maximumLength) => + Truncate(value, maximumLength, string.Empty); + + /// + /// Truncates the specified value and append the omission last. + /// + /// The value. + /// The maximum length. + /// The omission. + /// + /// Retrieves a substring from this instance. + /// The substring starts at a specified character position and has a specified length. + /// + public static string? Truncate(this string value, int maximumLength, string omission) + { + if (value == null) + return null; + + return value.Length > maximumLength + ? value.Substring(0, maximumLength) + (omission ?? string.Empty) + : value; + } + + /// + /// Determines whether the specified contains any of characters in + /// the specified array of . + /// + /// + /// true if contains any of ; + /// otherwise, false. + /// + /// + /// A to test. + /// + /// + /// An array of that contains characters to find. + /// + public static bool Contains(this string value, params char[] chars) => + chars?.Length == 0 || (!string.IsNullOrEmpty(value) && value.IndexOfAny(chars) > -1); + + /// + /// Replaces all chars in a string. + /// + /// The value. + /// The replace value. + /// The chars. + /// The string with the characters replaced. + public static string ReplaceAll(this string value, string replaceValue, params char[] chars) => + chars.Aggregate(value, (current, c) => current.Replace(new string(new[] { c }), replaceValue)); + + /// + /// Convert hex character to an integer. Return -1 if char is something + /// other than a hex char. + /// + /// The c. + /// Converted integer. + public static int Hex2Int(this char value) => + value >= '0' && value <= '9' + ? value - '0' + : value >= 'A' && value <= 'F' + ? value - 'A' + 10 + : value >= 'a' && value <= 'f' + ? value - 'a' + 10 + : -1; + } +} diff --git a/Swan.Lite/Extensions.Tasks.cs b/Swan.Lite/Extensions.Tasks.cs new file mode 100644 index 0000000..f43d34b --- /dev/null +++ b/Swan.Lite/Extensions.Tasks.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading.Tasks; + +namespace Swan +{ + /// + /// Provides extension methods for and . + /// + public static class TaskExtensions + { + /// + /// Suspends execution until the specified is completed. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The on which this method is called. + /// is . + public static void Await(this Task @this) => @this.GetAwaiter().GetResult(); + + /// + /// Suspends execution until the specified is completed + /// and returns its result. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The type of the task's result. + /// The on which this method is called. + /// The result of . + /// is . + public static TResult Await(this Task @this) => @this.GetAwaiter().GetResult(); + + /// + /// Suspends execution until the specified is completed. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The on which this method is called. + /// If set to , + /// attempts to marshal the continuation back to the original context captured. + /// This parameter has the same effect as calling the + /// method. + /// is . + public static void Await(this Task @this, bool continueOnCapturedContext) + => @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); + + /// + /// Suspends execution until the specified is completed + /// and returns its result. + /// This method operates similarly to the C# operator, + /// but is meant to be called from a non- method. + /// + /// The type of the task's result. + /// The on which this method is called. + /// If set to , + /// attempts to marshal the continuation back to the original context captured. + /// This parameter has the same effect as calling the + /// method. + /// The result of . + /// is . + public static TResult Await(this Task @this, bool continueOnCapturedContext) + => @this.ConfigureAwait(continueOnCapturedContext).GetAwaiter().GetResult(); + } +} diff --git a/Swan.Lite/Extensions.ValueTypes.cs b/Swan.Lite/Extensions.ValueTypes.cs new file mode 100644 index 0000000..6bd4066 --- /dev/null +++ b/Swan.Lite/Extensions.ValueTypes.cs @@ -0,0 +1,165 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Provides various extension methods for value types and structs. + /// + public static class ValueTypeExtensions + { + /// + /// Clamps the specified value between the minimum and the maximum. + /// + /// The type of value to clamp. + /// The value. + /// The minimum. + /// The maximum. + /// A value that indicates the relative order of the objects being compared. + public static T Clamp(this T @this, T min, T max) + where T : struct, IComparable + { + if (@this.CompareTo(min) < 0) return min; + + return @this.CompareTo(max) > 0 ? max : @this; + } + + /// + /// Clamps the specified value between the minimum and the maximum. + /// + /// The value. + /// The minimum. + /// The maximum. + /// A value that indicates the relative order of the objects being compared. + public static int Clamp(this int @this, int min, int max) + => @this < min ? min : (@this > max ? max : @this); + + /// + /// Determines whether the specified value is between a minimum and a maximum value. + /// + /// The type of value to check. + /// The value. + /// The minimum. + /// The maximum. + /// + /// true if the specified minimum is between; otherwise, false. + /// + public static bool IsBetween(this T @this, T min, T max) + where T : struct, IComparable + { + return @this.CompareTo(min) >= 0 && @this.CompareTo(max) <= 0; + } + + /// + /// Converts an array of bytes into the given struct type. + /// + /// The type of structure to convert. + /// The data. + /// a struct type derived from convert an array of bytes ref=ToStruct". + public static T ToStruct(this byte[] @this) + where T : struct + { + return @this == null ? throw new ArgumentNullException(nameof(@this)) : ToStruct(@this, 0, @this.Length); + } + + /// + /// Converts an array of bytes into the given struct type. + /// + /// The type of structure to convert. + /// The data. + /// The offset. + /// The length. + /// + /// A managed object containing the data pointed to by the ptr parameter. + /// + /// data. + public static T ToStruct(this byte[] @this, int offset, int length) + where T : struct + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var buffer = new byte[length]; + Array.Copy(@this, offset, buffer, 0, buffer.Length); + var handle = GCHandle.Alloc(GetStructBytes(buffer), GCHandleType.Pinned); + + try + { + return Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + } + finally + { + handle.Free(); + } + } + + /// + /// Converts a struct to an array of bytes. + /// + /// The type of structure to convert. + /// The object. + /// A byte array containing the results of encoding the specified set of characters. + public static byte[] ToBytes(this T @this) + where T : struct + { + var data = new byte[Marshal.SizeOf(@this)]; + var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + + try + { + Marshal.StructureToPtr(@this, handle.AddrOfPinnedObject(), false); + return GetStructBytes(data); + } + finally + { + handle.Free(); + } + } + + /// + /// Swaps the endianness of an unsigned long to an unsigned integer. + /// + /// The bytes contained in a long. + /// + /// A 32-bit unsigned integer equivalent to the ulong + /// contained in longBytes. + /// + public static uint SwapEndianness(this ulong @this) + => (uint)(((@this & 0x000000ff) << 24) + + ((@this & 0x0000ff00) << 8) + + ((@this & 0x00ff0000) >> 8) + + ((@this & 0xff000000) >> 24)); + + private static byte[] GetStructBytes(byte[] data) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var fields = typeof(T).GetTypeInfo() + .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + var endian = AttributeCache.DefaultCache.Value.RetrieveOne(); + + foreach (var field in fields) + { + if (endian == null && !field.IsDefined(typeof(StructEndiannessAttribute), false)) + continue; + + var offset = Marshal.OffsetOf(field.Name).ToInt32(); + var length = Marshal.SizeOf(field.FieldType); + + endian = endian ?? AttributeCache.DefaultCache.Value.RetrieveOne(field); + + if (endian != null && (endian.Endianness == Endianness.Big && BitConverter.IsLittleEndian || + endian.Endianness == Endianness.Little && !BitConverter.IsLittleEndian)) + { + Array.Reverse(data, offset, length); + } + } + + return data; + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Extensions.cs b/Swan.Lite/Extensions.cs new file mode 100644 index 0000000..9d6837f --- /dev/null +++ b/Swan.Lite/Extensions.cs @@ -0,0 +1,276 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Swan.Lite.Reflection; +using Swan.Mappers; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Extension methods. + /// + public static partial class Extensions + { + /// + /// Iterates over the public, instance, readable properties of the source and + /// tries to write a compatible value to a public, instance, writable property in the destination. + /// + /// The type of the source. + /// The source. + /// The target. + /// The ignore properties. + /// + /// Number of properties that was copied successful. + /// + public static int CopyPropertiesTo(this T source, object target, params string[]? ignoreProperties) + where T : class => + ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties); + + /// + /// Iterates over the public, instance, readable properties of the source and + /// tries to write a compatible value to a public, instance, writable property in the destination. + /// + /// The source. + /// The destination. + /// Properties to copy. + /// + /// Number of properties that were successfully copied. + /// + public static int CopyOnlyPropertiesTo(this object source, object target, params string[]? propertiesToCopy) + => ObjectMapper.Copy(source, target, propertiesToCopy); + + /// + /// Copies the properties to new instance of T. + /// + /// The new object type. + /// The source. + /// The ignore properties. + /// + /// The specified type with properties copied. + /// + /// source. + public static T CopyPropertiesToNew(this object source, string[]? ignoreProperties = null) + where T : class + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var target = Activator.CreateInstance(); + ObjectMapper.Copy(source, target, GetCopyableProperties(target), ignoreProperties); + + return target; + } + + /// + /// Copies the only properties to new instance of T. + /// + /// Object Type. + /// The source. + /// The properties to copy. + /// + /// The specified type with properties copied. + /// + /// source. + public static T CopyOnlyPropertiesToNew(this object source, params string[] propertiesToCopy) + where T : class + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var target = Activator.CreateInstance(); + ObjectMapper.Copy(source, target, propertiesToCopy); + + return target; + } + + /// + /// Iterates over the keys of the source and tries to write a compatible value to a public, + /// instance, writable property in the destination. + /// + /// The source. + /// The target. + /// The ignore keys. + /// Number of properties that was copied successful. + public static int CopyKeyValuePairTo( + this IDictionary source, + object target, + params string[] ignoreKeys) => + source == null + ? throw new ArgumentNullException(nameof(source)) + : ObjectMapper.Copy(source, target, null, ignoreKeys); + + /// + /// Iterates over the keys of the source and tries to write a compatible value to a public, + /// instance, writable property in the destination. + /// + /// Object Type. + /// The source. + /// The ignore keys. + /// + /// The specified type with properties copied. + /// + public static T CopyKeyValuePairToNew( + this IDictionary source, + params string[] ignoreKeys) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + var target = Activator.CreateInstance(); + source.CopyKeyValuePairTo(target, ignoreKeys); + return target; + } + + /// + /// Does the specified action. + /// + /// The action. + /// The retry interval. + /// The retry count. + public static void Retry( + this Action action, + TimeSpan retryInterval = default, + int retryCount = 3) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + Retry(() => + { + action(); + return null; + }, + retryInterval, + retryCount); + } + + /// + /// Does the specified action. + /// + /// The type of the source. + /// The action. + /// The retry interval. + /// The retry count. + /// + /// The return value of the method that this delegate encapsulates. + /// + /// action. + /// Represents one or many errors that occur during application execution. + public static T Retry( + this Func action, + TimeSpan retryInterval = default, + int retryCount = 3) + { + if (action == null) + throw new ArgumentNullException(nameof(action)); + + if (retryInterval == default) + retryInterval = TimeSpan.FromSeconds(1); + + var exceptions = new List(); + + for (var retry = 0; retry < retryCount; retry++) + { + try + { + if (retry > 0) + Task.Delay(retryInterval).Wait(); + + return action(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + + throw new AggregateException(exceptions); + } + + /// + /// Gets the copyable properties. + /// + /// If there is no properties with the attribute AttributeCache returns all the properties. + /// + /// The object. + /// + /// Array of properties. + /// + /// model. + /// + public static IEnumerable GetCopyableProperties(this object @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var collection = PropertyTypeCache.DefaultCache.Value + .RetrieveAllProperties(@this.GetType(), true); + + var properties = collection + .Select(x => new + { + x.Name, + HasAttribute = AttributeCache.DefaultCache.Value.RetrieveOne(x) != null, + }) + .Where(x => x.HasAttribute) + .Select(x => x.Name); + + return properties.Any() + ? properties + : collection.Select(x => x.Name); + } + + internal static void CreateTarget( + this object source, + Type targetType, + bool includeNonPublic, + ref object? target) + { + switch (source) + { + // do nothing. Simply skip creation + case string _: + break; + // When using arrays, there is no default constructor, attempt to build a compatible array + case IList sourceObjectList when targetType.IsArray: + var elementType = targetType.GetElementType(); + + if (elementType != null) + target = Array.CreateInstance(elementType, sourceObjectList.Count); + break; + default: + var constructors = ConstructorTypeCache.DefaultCache.Value + .RetrieveAllConstructors(targetType, includeNonPublic); + + // Try to check if empty constructor is available + if (constructors.Any(x => x.Item2.Length == 0)) + { + target = Activator.CreateInstance(targetType, includeNonPublic); + } + else + { + var firstCtor = constructors + .OrderBy(x => x.Item2.Length) + .FirstOrDefault(); + + target = Activator.CreateInstance(targetType, + firstCtor?.Item2.Select(arg => arg.GetType().GetDefault()).ToArray()); + } + + break; + } + } + + internal static string GetNameWithCase(this string name, JsonSerializerCase jsonSerializerCase) => + jsonSerializerCase switch + { + JsonSerializerCase.PascalCase => char.ToUpperInvariant(name[0]) + name.Substring(1), + JsonSerializerCase.CamelCase => char.ToLowerInvariant(name[0]) + name.Substring(1), + JsonSerializerCase.None => name, + _ => throw new ArgumentOutOfRangeException(nameof(jsonSerializerCase), jsonSerializerCase, null) + }; + } +} diff --git a/Swan.Lite/Formatters/CsvReader.cs b/Swan.Lite/Formatters/CsvReader.cs new file mode 100644 index 0000000..fa8fa39 --- /dev/null +++ b/Swan.Lite/Formatters/CsvReader.cs @@ -0,0 +1,648 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// Represents a reader designed for CSV text. + /// It is capable of deserializing objects from individual lines of CSV text, + /// transforming CSV lines of text into objects, + /// or simply reading the lines of CSV as an array of strings. + /// + /// + /// + /// The following example describes how to load a list of objects from a CSV file. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// // load records from a CSV file + /// var loadedRecords = + /// CsvReader.LoadRecords<Person>("C:\\Users\\user\\Documents\\file.csv"); + /// + /// // loadedRecords = + /// // [ + /// // { Age = 20, Name = "George" } + /// // { Age = 18, Name = "Juan" } + /// // ] + /// } + /// } + /// + /// The following code explains how to read a CSV formatted string. + /// + /// using Swan.Formatters; + /// using System.Text; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // data to be read + /// var data = @"Company,OpenPositions,MainTechnology,Revenue + /// Co,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",500 + /// Ca,2,""C#, MySQL, JavaScript, HTML5 and CSS3"",600"; + /// + /// using(var stream = new MemoryStream(Encoding.UTF8.GetBytes(data))) + /// { + /// // create a CSV reader + /// var reader = new CsvReader(stream, false, Encoding.UTF8); + /// } + /// } + /// } + /// + /// + public class CsvReader : IDisposable + { + private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); + + private readonly object _syncLock = new object(); + + private ulong _count; + private char _escapeCharacter = '"'; + private char _separatorCharacter = ','; + + private bool _hasDisposed; // To detect redundant calls + private string[] _headings; + private Dictionary _defaultMap; + private StreamReader _reader; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The stream. + /// if set to true leaves the input stream open. + /// The text encoding. + public CsvReader(Stream inputStream, bool leaveOpen, Encoding textEncoding) + { + if (inputStream == null) + throw new ArgumentNullException(nameof(inputStream)); + + if (textEncoding == null) + throw new ArgumentNullException(nameof(textEncoding)); + + _reader = new StreamReader(inputStream, textEncoding, true, 2048, leaveOpen); + } + + /// + /// Initializes a new instance of the class. + /// It will automatically close the stream upon disposing. + /// + /// The stream. + /// The text encoding. + public CsvReader(Stream stream, Encoding textEncoding) + : this(stream, false, textEncoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the stream when disposing this reader + /// and uses the Windows 1253 encoding. + /// + /// The stream. + public CsvReader(Stream stream) + : this(stream, false, Definitions.Windows1252Encoding) + { + } + + /// + /// Initializes a new instance of the class. + /// It uses the Windows 1252 Encoding by default and it automatically closes the file + /// when this reader is disposed of. + /// + /// The filename. + public CsvReader(string filename) + : this(File.OpenRead(filename), false, Definitions.Windows1252Encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the file when disposing this reader. + /// + /// The filename. + /// The encoding. + public CsvReader(string filename, Encoding encoding) + : this(File.OpenRead(filename), false, encoding) + { + // placeholder + } + + #endregion + + #region Properties + + /// + /// Gets number of lines that have been read, including the headings. + /// + /// + /// The count. + /// + public ulong Count + { + get + { + lock (_syncLock) + { + return _count; + } + } + } + + /// + /// Gets or sets the escape character. + /// By default it is the double quote '"'. + /// + /// + /// The escape character. + /// + public char EscapeCharacter + { + get => _escapeCharacter; + set + { + lock (_syncLock) + { + _escapeCharacter = value; + } + } + } + + /// + /// Gets or sets the separator character. + /// By default it is the comma character ','. + /// + /// + /// The separator character. + /// + public char SeparatorCharacter + { + get => _separatorCharacter; + set + { + lock (_syncLock) + { + _separatorCharacter = value; + } + } + } + + /// + /// Gets a value indicating whether the stream reader is at the end of the stream + /// In other words, if no more data can be read, this will be set to true. + /// + /// + /// true if [end of stream]; otherwise, false. + /// + public bool EndOfStream + { + get + { + lock (_syncLock) + { + return _reader.EndOfStream; + } + } + } + + #endregion + + #region Generic, Main ReadLine method + + /// + /// Reads a line of CSV text into an array of strings. + /// + /// An array of the specified element type containing copies of the elements of the ArrayList. + /// Cannot read past the end of the stream. + public string[] ReadLine() + { + lock (_syncLock) + { + if (_reader.EndOfStream) + throw new EndOfStreamException("Cannot read past the end of the stream"); + + var values = ParseRecord(_reader, _escapeCharacter, _separatorCharacter); + _count++; + return values; + } + } + + #endregion + + #region Read Methods + + /// + /// Skips a line of CSV text. + /// This operation does not increment the Count property and it is useful when you need to read the headings + /// skipping over a few lines as Reading headings is only supported + /// as the first read operation (i.e. while count is still 0). + /// + /// Cannot read past the end of the stream. + public void SkipRecord() + { + lock (_syncLock) + { + if (_reader.EndOfStream) + throw new EndOfStreamException("Cannot read past the end of the stream"); + + ParseRecord(_reader, _escapeCharacter, _separatorCharacter); + } + } + + /// + /// Reads a line of CSV text and stores the values read as a representation of the column names + /// to be used for parsing objects. You have to call this method before calling ReadObject methods. + /// + /// An array of the specified element type containing copies of the elements of the ArrayList. + /// + /// Reading headings is only supported as the first read operation. + /// or + /// ReadHeadings. + /// + /// Cannot read past the end of the stream. + public string[] ReadHeadings() + { + lock (_syncLock) + { + if (_headings != null) + throw new InvalidOperationException($"The {nameof(ReadHeadings)} method had already been called."); + + if (_count != 0) + throw new InvalidOperationException("Reading headings is only supported as the first read operation."); + + _headings = ReadLine(); + _defaultMap = _headings.ToDictionary(x => x, x => x); + + return _headings.ToArray(); + } + } + + /// + /// Reads a line of CSV text, converting it into a dynamic object in which properties correspond to the names of the headings. + /// + /// The mappings between CSV headings (keys) and object properties (values). + /// Object of the type of the elements in the collection of key/value pairs. + /// ReadHeadings. + /// Cannot read past the end of the stream. + /// map. + public IDictionary ReadObject(IDictionary map) + { + lock (_syncLock) + { + if (_headings == null) + throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); + + if (map == null) + throw new ArgumentNullException(nameof(map)); + + var result = new Dictionary(); + var values = ReadLine(); + + for (var i = 0; i < _headings.Length; i++) + { + if (i > values.Length - 1) + break; + + result[_headings[i]] = values[i]; + } + + return result; + } + } + + /// + /// Reads a line of CSV text, converting it into a dynamic object + /// The property names correspond to the names of the CSV headings. + /// + /// Object of the type of the elements in the collection of key/value pairs. + public IDictionary ReadObject() => ReadObject(_defaultMap); + + /// + /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) + /// where the keys are the names of the headings and the values are the names of the instance properties + /// in the given Type. The result object must be already instantiated. + /// + /// The type of object to map. + /// The map. + /// The result. + /// map + /// or + /// result. + /// ReadHeadings. + /// Cannot read past the end of the stream. + public void ReadObject(IDictionary map, ref T result) + { + lock (_syncLock) + { + // Check arguments + { + if (map == null) + throw new ArgumentNullException(nameof(map)); + + if (_reader.EndOfStream) + throw new EndOfStreamException("Cannot read past the end of the stream"); + + if (_headings == null) + throw new InvalidOperationException($"Call the {nameof(ReadHeadings)} method before reading as an object."); + + if (Equals(result, default(T))) + throw new ArgumentNullException(nameof(result)); + } + + // Read line and extract values + var values = ReadLine(); + + // Extract properties from cache + var properties = TypeCache + .RetrieveFilteredProperties(typeof(T), true, x => x.CanWrite && Definitions.BasicTypesInfo.Value.ContainsKey(x.PropertyType)); + + // Assign property values for each heading + for (var i = 0; i < _headings.Length; i++) + { + // break if no more headings are matched + if (i > values.Length - 1) + break; + + // skip if no heading is available or the heading is empty + if (map.ContainsKey(_headings[i]) == false && + string.IsNullOrWhiteSpace(map[_headings[i]]) == false) + continue; + + // Prepare the target property + var propertyName = map[_headings[i]]; + + // Parse and assign the basic type value to the property if exists + properties + .FirstOrDefault(p => p.Name == propertyName)? + .TrySetBasicType(values[i], result); + } + } + } + + /// + /// Reads a line of CSV text converting it into an object of the given type, using a map (or Dictionary) + /// where the keys are the names of the headings and the values are the names of the instance properties + /// in the given Type. + /// + /// The type of object to map. + /// The map of CSV headings (keys) and Type property names (values). + /// The conversion of specific type of object. + /// map. + /// ReadHeadings. + /// Cannot read past the end of the stream. + public T ReadObject(IDictionary map) + where T : new() + { + var result = Activator.CreateInstance(); + ReadObject(map, ref result); + return result; + } + + /// + /// Reads a line of CSV text converting it into an object of the given type, and assuming + /// the property names of the target type match the heading names of the file. + /// + /// The type of object. + /// The conversion of specific type of object. + public T ReadObject() + where T : new() + { + return ReadObject(_defaultMap); + } + + #endregion + + #region Support Methods + + /// + /// Parses a line of standard CSV text into an array of strings. + /// Note that quoted values might have new line sequences in them. Field values will contain such sequences. + /// + /// The reader. + /// The escape character. + /// The separator character. + /// An array of the specified element type containing copies of the elements of the ArrayList. + private static string[] ParseRecord(StreamReader reader, char escapeCharacter = '"', char separatorCharacter = ',') + { + var values = new List(); + var currentValue = new StringBuilder(1024); + var currentState = ReadState.WaitingForNewField; + string line; + + while ((line = reader.ReadLine()) != null) + { + for (var charIndex = 0; charIndex < line.Length; charIndex++) + { + // Get the current and next character + var currentChar = line[charIndex]; + var nextChar = charIndex < line.Length - 1 ? line[charIndex + 1] : new char?(); + + // Perform logic based on state and decide on next state + switch (currentState) + { + case ReadState.WaitingForNewField: + { + currentValue.Clear(); + + if (currentChar == escapeCharacter) + { + currentState = ReadState.PushingQuoted; + continue; + } + + if (currentChar == separatorCharacter) + { + values.Add(currentValue.ToString()); + currentState = ReadState.WaitingForNewField; + continue; + } + + currentValue.Append(currentChar); + currentState = ReadState.PushingNormal; + continue; + } + + case ReadState.PushingNormal: + { + // Handle field content delimiter by comma + if (currentChar == separatorCharacter) + { + currentState = ReadState.WaitingForNewField; + values.Add(currentValue.ToString()); + currentValue.Clear(); + continue; + } + + // Handle double quote escaping + if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) + { + // advance 1 character now. The loop will advance one more. + currentValue.Append(currentChar); + charIndex++; + continue; + } + + currentValue.Append(currentChar); + break; + } + + case ReadState.PushingQuoted: + { + // Handle field content delimiter by ending double quotes + if (currentChar == escapeCharacter && (nextChar.HasValue == false || nextChar != escapeCharacter)) + { + currentState = ReadState.PushingNormal; + continue; + } + + // Handle double quote escaping + if (currentChar == escapeCharacter && nextChar.HasValue && nextChar == escapeCharacter) + { + // advance 1 character now. The loop will advance one more. + currentValue.Append(currentChar); + charIndex++; + continue; + } + + currentValue.Append(currentChar); + break; + } + } + } + + // determine if we need to continue reading a new line if it is part of the quoted + // field value + if (currentState == ReadState.PushingQuoted) + { + // we need to add the new line sequence to the output of the field + // because we were pushing a quoted value + currentValue.Append(Environment.NewLine); + } + else + { + // push anything that has not been pushed (flush) into a last value + values.Add(currentValue.ToString()); + currentValue.Clear(); + + // stop reading more lines we have reached the end of the CSV record + break; + } + } + + // If we ended up pushing quoted and no closing quotes we might + // have additional text in yt + if (currentValue.Length > 0) + { + values.Add(currentValue.ToString()); + } + + return values.ToArray(); + } + + #endregion + + #region Helpers + + /// + /// Loads the records from the stream + /// This method uses Windows 1252 encoding. + /// + /// The type of IList items to load. + /// The stream. + /// A generic collection of objects that can be individually accessed by index. + public static IList LoadRecords(Stream stream) + where T : new() + { + var result = new List(); + + using (var reader = new CsvReader(stream)) + { + reader.ReadHeadings(); + while (!reader.EndOfStream) + { + result.Add(reader.ReadObject()); + } + } + + return result; + } + + /// + /// Loads the records from the give file path. + /// This method uses Windows 1252 encoding. + /// + /// The type of IList items to load. + /// The file path. + /// A generic collection of objects that can be individually accessed by index. + public static IList LoadRecords(string filePath) + where T : new() + { + return LoadRecords(File.OpenRead(filePath)); + } + + #endregion + + #region IDisposable Support + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_hasDisposed) return; + + if (disposing) + { + try + { + _reader.Dispose(); + } + finally + { + _reader = null; + } + } + + _hasDisposed = true; + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + #endregion + + /// + /// Defines the 3 different read states + /// for the parsing state machine. + /// + private enum ReadState + { + WaitingForNewField, + PushingNormal, + PushingQuoted, + } + } +} diff --git a/Swan.Lite/Formatters/CsvWriter.cs b/Swan.Lite/Formatters/CsvWriter.cs new file mode 100644 index 0000000..8cfa6c5 --- /dev/null +++ b/Swan.Lite/Formatters/CsvWriter.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A CSV writer useful for exporting a set of objects. + /// + /// + /// The following code describes how to save a list of objects into a CSV file. + /// + /// using System.Collections.Generic; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// // create a list of people + /// var people = new List<Person> + /// { + /// new Person { Name = "Artyom", Age = 20 }, + /// new Person { Name = "Aloy", Age = 18 } + /// } + /// + /// // write items inside file.csv + /// CsvWriter.SaveRecords(people, "C:\\Users\\user\\Documents\\file.csv"); + /// + /// // output + /// // | Name | Age | + /// // | Artyom | 20 | + /// // | Aloy | 18 | + /// } + /// } + /// + /// + public class CsvWriter : IDisposable + { + private static readonly PropertyTypeCache TypeCache = new PropertyTypeCache(); + + private readonly object _syncLock = new object(); + private readonly Stream _outputStream; + private readonly Encoding _encoding; + private readonly bool _leaveStreamOpen; + private bool _isDisposing; + private ulong _mCount; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The output stream. + /// if set to true [leave open]. + /// The encoding. + public CsvWriter(Stream outputStream, bool leaveOpen, Encoding encoding) + { + _outputStream = outputStream; + _encoding = encoding; + _leaveStreamOpen = leaveOpen; + } + + /// + /// Initializes a new instance of the class. + /// It automatically closes the stream when disposing this writer. + /// + /// The output stream. + /// The encoding. + public CsvWriter(Stream outputStream, Encoding encoding) + : this(outputStream, false, encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It uses the Windows 1252 encoding and automatically closes + /// the stream upon disposing this writer. + /// + /// The output stream. + public CsvWriter(Stream outputStream) + : this(outputStream, false, Definitions.Windows1252Encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It opens the file given file, automatically closes the stream upon + /// disposing of this writer, and uses the Windows 1252 encoding. + /// + /// The filename. + public CsvWriter(string filename) + : this(File.OpenWrite(filename), false, Definitions.Windows1252Encoding) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// It opens the file given file, automatically closes the stream upon + /// disposing of this writer, and uses the given text encoding for output. + /// + /// The filename. + /// The encoding. + public CsvWriter(string filename, Encoding encoding) + : this(File.OpenWrite(filename), false, encoding) + { + // placeholder + } + + #endregion + + #region Properties + + /// + /// Gets or sets the field separator character. + /// + /// + /// The separator character. + /// + public char SeparatorCharacter { get; set; } = ','; + + /// + /// Gets or sets the escape character to use to escape field values. + /// + /// + /// The escape character. + /// + public char EscapeCharacter { get; set; } = '"'; + + /// + /// Gets or sets the new line character sequence to use when writing a line. + /// + /// + /// The new line sequence. + /// + public string NewLineSequence { get; set; } = Environment.NewLine; + + /// + /// Defines a list of properties to ignore when outputting CSV lines. + /// + /// + /// The ignore property names. + /// + public List IgnorePropertyNames { get; } = new List(); + + /// + /// Gets number of lines that have been written, including the headings line. + /// + /// + /// The count. + /// + public ulong Count + { + get + { + lock (_syncLock) + { + return _mCount; + } + } + } + + #endregion + + #region Helpers + + /// + /// Saves the items to a stream. + /// It uses the Windows 1252 text encoding for output. + /// + /// The type of enumeration. + /// The items. + /// The stream. + /// true if stream is truncated, default false. + /// Number of item saved. + public static int SaveRecords(IEnumerable items, Stream stream, bool truncateData = false) + { + // truncate the file if it had data + if (truncateData && stream.Length > 0) + stream.SetLength(0); + + using (var writer = new CsvWriter(stream)) + { + writer.WriteHeadings(); + writer.WriteObjects(items); + return (int)writer.Count; + } + } + + /// + /// Saves the items to a CSV file. + /// If the file exits, it overwrites it. If it does not, it creates it. + /// It uses the Windows 1252 text encoding for output. + /// + /// The type of enumeration. + /// The items. + /// The file path. + /// Number of item saved. + public static int SaveRecords(IEnumerable items, string filePath) => SaveRecords(items, File.OpenWrite(filePath), true); + + #endregion + + #region Generic, main Write Line Method + + /// + /// Writes a line of CSV text. Items are converted to strings. + /// If items are found to be null, empty strings are written out. + /// If items are not string, the ToStringInvariant() method is called on them. + /// + /// The items. + public void WriteLine(params object[] items) + => WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant())); + + /// + /// Writes a line of CSV text. Items are converted to strings. + /// If items are found to be null, empty strings are written out. + /// If items are not string, the ToStringInvariant() method is called on them. + /// + /// The items. + public void WriteLine(IEnumerable items) + => WriteLine(items.Select(x => x == null ? string.Empty : x.ToStringInvariant())); + + /// + /// Writes a line of CSV text. + /// If items are found to be null, empty strings are written out. + /// + /// The items. + public void WriteLine(params string[] items) => WriteLine((IEnumerable) items); + + /// + /// Writes a line of CSV text. + /// If items are found to be null, empty strings are written out. + /// + /// The items. + public void WriteLine(IEnumerable items) + { + lock (_syncLock) + { + var length = items.Count(); + var separatorBytes = _encoding.GetBytes(new[] { SeparatorCharacter }); + var endOfLineBytes = _encoding.GetBytes(NewLineSequence); + + // Declare state variables here to avoid recreation, allocation and + // reassignment in every loop + bool needsEnclosing; + string textValue; + byte[] output; + + for (var i = 0; i < length; i++) + { + textValue = items.ElementAt(i); + + // Determine if we need the string to be enclosed + // (it either contains an escape, new line, or separator char) + needsEnclosing = textValue.IndexOf(SeparatorCharacter) >= 0 + || textValue.IndexOf(EscapeCharacter) >= 0 + || textValue.IndexOf('\r') >= 0 + || textValue.IndexOf('\n') >= 0; + + // Escape the escape characters by repeating them twice for every instance + textValue = textValue.Replace($"{EscapeCharacter}", + $"{EscapeCharacter}{EscapeCharacter}"); + + // Enclose the text value if we need to + if (needsEnclosing) + textValue = string.Format($"{EscapeCharacter}{textValue}{EscapeCharacter}", textValue); + + // Get the bytes to write to the stream and write them + output = _encoding.GetBytes(textValue); + _outputStream.Write(output, 0, output.Length); + + // only write a separator if we are moving in between values. + // the last value should not be written. + if (i < length - 1) + _outputStream.Write(separatorBytes, 0, separatorBytes.Length); + } + + // output the newline sequence + _outputStream.Write(endOfLineBytes, 0, endOfLineBytes.Length); + _mCount += 1; + } + } + + #endregion + + #region Write Object Method + + /// + /// Writes a row of CSV text. It handles the special cases where the object is + /// a dynamic object or and array. It also handles non-collection objects fine. + /// If you do not like the way the output is handled, you can simply write an extension + /// method of this class and use the WriteLine method instead. + /// + /// The item. + /// item. + public void WriteObject(object item) + { + if (item == null) + throw new ArgumentNullException(nameof(item)); + + lock (_syncLock) + { + switch (item) + { + case IDictionary typedItem: + WriteLine(GetFilteredDictionary(typedItem)); + return; + case ICollection typedItem: + WriteLine(typedItem.Cast()); + return; + default: + WriteLine(GetFilteredTypeProperties(item.GetType()) + .Select(x => x.ToFormattedString(item))); + break; + } + } + } + + /// + /// Writes a row of CSV text. It handles the special cases where the object is + /// a dynamic object or and array. It also handles non-collection objects fine. + /// If you do not like the way the output is handled, you can simply write an extension + /// method of this class and use the WriteLine method instead. + /// + /// The type of object to write. + /// The item. + public void WriteObject(T item) => WriteObject(item as object); + + /// + /// Writes a set of items, one per line and atomically by repeatedly calling the + /// WriteObject method. For more info check out the description of the WriteObject + /// method. + /// + /// The type of object to write. + /// The items. + public void WriteObjects(IEnumerable items) + { + lock (_syncLock) + { + foreach (var item in items) + WriteObject(item); + } + } + + #endregion + + #region Write Headings Methods + + /// + /// Writes the headings. + /// + /// The type of object to extract headings. + /// type. + public void WriteHeadings(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + var properties = GetFilteredTypeProperties(type).Select(p => p.Name).Cast(); + WriteLine(properties); + } + + /// + /// Writes the headings. + /// + /// The type of object to extract headings. + public void WriteHeadings() => WriteHeadings(typeof(T)); + + /// + /// Writes the headings. + /// + /// The dictionary to extract headings. + /// dictionary. + public void WriteHeadings(IDictionary dictionary) + { + if (dictionary == null) + throw new ArgumentNullException(nameof(dictionary)); + + WriteLine(GetFilteredDictionary(dictionary, true)); + } + + /// + /// Writes the headings. + /// + /// The object to extract headings. + /// obj. + public void WriteHeadings(object obj) + { + if (obj == null) + throw new ArgumentNullException(nameof(obj)); + + WriteHeadings(obj.GetType()); + } + + #endregion + + #region IDisposable Support + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposeAlsoManaged) + { + if (_isDisposing) return; + + if (disposeAlsoManaged) + { + if (_leaveStreamOpen == false) + { + _outputStream.Dispose(); + } + } + + _isDisposing = true; + } + + #endregion + + #region Support Methods + + private IEnumerable GetFilteredDictionary(IDictionary dictionary, bool filterKeys = false) + => dictionary + .Keys + .Cast() + .Select(key => key == null ? string.Empty : key.ToStringInvariant()) + .Where(stringKey => !IgnorePropertyNames.Contains(stringKey)) + .Select(stringKey => + filterKeys + ? stringKey + : dictionary[stringKey] == null ? string.Empty : dictionary[stringKey].ToStringInvariant()); + + private IEnumerable GetFilteredTypeProperties(Type type) + => TypeCache.Retrieve(type, t => + t.GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(p => p.CanRead)) + .Where(p => !IgnorePropertyNames.Contains(p.Name)); + + #endregion + + } +} \ No newline at end of file diff --git a/Swan.Lite/Formatters/HumanizeJson.cs b/Swan.Lite/Formatters/HumanizeJson.cs new file mode 100644 index 0000000..102aebd --- /dev/null +++ b/Swan.Lite/Formatters/HumanizeJson.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Swan.Formatters +{ + internal class HumanizeJson + { + private readonly StringBuilder _builder = new StringBuilder(); + private readonly int _indent; + private readonly string _indentStr; + private readonly object _obj; + + public HumanizeJson(object obj, int indent) + { + if (obj == null) + { + return; + } + + _indent = indent; + _indentStr = new string(' ', indent * 4); + _obj = obj; + + ParseObject(); + } + + public string GetResult() => _builder == null ? string.Empty : _builder.ToString().TrimEnd(); + + private void ParseObject() + { + switch (_obj) + { + case Dictionary dictionary: + AppendDictionary(dictionary); + break; + case List list: + AppendList(list); + break; + default: + AppendString(); + break; + } + } + + private void AppendDictionary(Dictionary objects) + { + foreach (var kvp in objects) + { + if (kvp.Value == null) continue; + + var writeOutput = false; + + switch (kvp.Value) + { + case Dictionary valueDictionary: + if (valueDictionary.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}{kvp.Key,-16}: object") + .AppendLine(); + } + + break; + case List valueList: + if (valueList.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}{kvp.Key,-16}: array[{valueList.Count}]") + .AppendLine(); + } + + break; + default: + writeOutput = true; + _builder.Append($"{_indentStr}{kvp.Key,-16}: "); + break; + } + + if (writeOutput) + _builder.AppendLine(new HumanizeJson(kvp.Value, _indent + 1).GetResult()); + } + } + + private void AppendList(List objects) + { + var index = 0; + foreach (var value in objects) + { + var writeOutput = false; + + switch (value) + { + case Dictionary valueDictionary: + if (valueDictionary.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}[{index}]: object") + .AppendLine(); + } + + break; + case List valueList: + if (valueList.Count > 0) + { + writeOutput = true; + _builder + .Append($"{_indentStr}[{index}]: array[{valueList.Count}]") + .AppendLine(); + } + + break; + default: + writeOutput = true; + _builder.Append($"{_indentStr}[{index}]: "); + break; + } + + index++; + + if (writeOutput) + _builder.AppendLine(new HumanizeJson(value, _indent + 1).GetResult()); + } + } + + private void AppendString() + { + var stringValue = _obj.ToString(); + + if (stringValue.Length + _indentStr.Length > 96 || stringValue.IndexOf('\r') >= 0 || + stringValue.IndexOf('\n') >= 0) + { + _builder.AppendLine(); + var stringLines = stringValue.ToLines().Select(l => l.Trim()); + + foreach (var line in stringLines) + { + _builder.AppendLine($"{_indentStr}{line}"); + } + } + else + { + _builder.Append($"{stringValue}"); + } + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Formatters/Json.Converter.cs b/Swan.Lite/Formatters/Json.Converter.cs new file mode 100644 index 0000000..38b5401 --- /dev/null +++ b/Swan.Lite/Formatters/Json.Converter.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public static partial class Json + { + private class Converter + { + private static readonly ConcurrentDictionary MemberInfoNameCache = + new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary ListAddMethodCache = new ConcurrentDictionary(); + + private readonly object? _target; + private readonly Type _targetType; + private readonly bool _includeNonPublic; + private readonly JsonSerializerCase _jsonSerializerCase; + + private Converter( + object? source, + Type targetType, + ref object? targetInstance, + bool includeNonPublic, + JsonSerializerCase jsonSerializerCase) + { + _targetType = targetInstance != null ? targetInstance.GetType() : targetType; + _includeNonPublic = includeNonPublic; + _jsonSerializerCase = jsonSerializerCase; + + if (source == null) + { + return; + } + + var sourceType = source.GetType(); + + if (_targetType == null || _targetType == typeof(object)) _targetType = sourceType; + if (sourceType == _targetType) + { + _target = source; + return; + } + + if (!TrySetInstance(targetInstance, source, ref _target)) + return; + + ResolveObject(source, ref _target); + } + + internal static object? FromJsonResult( + object? source, + JsonSerializerCase jsonSerializerCase, + Type? targetType = null, + bool includeNonPublic = false) + { + object? nullRef = null; + return new Converter(source, targetType ?? typeof(object), ref nullRef, includeNonPublic, jsonSerializerCase).GetResult(); + } + + private static object? FromJsonResult(object source, + Type targetType, + ref object? targetInstance, + bool includeNonPublic) + { + return new Converter(source, targetType, ref targetInstance, includeNonPublic, JsonSerializerCase.None).GetResult(); + } + + private static Type? GetAddMethodParameterType(Type targetType) + => ListAddMethodCache.GetOrAdd(targetType, + x => x.GetMethods() + .FirstOrDefault( + m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 1)? + .GetParameters()[0] + .ParameterType); + + private static void GetByteArray(string sourceString, ref object? target) + { + try + { + target = Convert.FromBase64String(sourceString); + } // Try conversion from Base 64 + catch (FormatException) + { + target = Encoding.UTF8.GetBytes(sourceString); + } // Get the string bytes in UTF8 + } + + private object GetSourcePropertyValue( + IDictionary sourceProperties, + MemberInfo targetProperty) + { + var targetPropertyName = MemberInfoNameCache.GetOrAdd( + targetProperty, + x => AttributeCache.DefaultCache.Value.RetrieveOne(x)?.PropertyName ?? x.Name.GetNameWithCase(_jsonSerializerCase)); + + return sourceProperties.GetValueOrDefault(targetPropertyName); + } + + private bool TrySetInstance(object? targetInstance, object source, ref object? target) + { + if (targetInstance == null) + { + // Try to create a default instance + try + { + source.CreateTarget(_targetType, _includeNonPublic, ref target); + } + catch + { + return false; + } + } + else + { + target = targetInstance; + } + + return true; + } + + private object? GetResult() => _target ?? _targetType.GetDefault(); + + private void ResolveObject(object source, ref object? target) + { + switch (source) + { + // Case 0: Special Cases Handling (Source and Target are of specific convertible types) + // Case 0.1: Source is string, Target is byte[] + case string sourceString when _targetType == typeof(byte[]): + GetByteArray(sourceString, ref target); + break; + + // Case 1.1: Source is Dictionary, Target is IDictionary + case Dictionary sourceProperties when target is IDictionary targetDictionary: + PopulateDictionary(sourceProperties, targetDictionary); + break; + + // Case 1.2: Source is Dictionary, Target is not IDictionary (i.e. it is a complex type) + case Dictionary sourceProperties: + PopulateObject(sourceProperties); + break; + + // Case 2.1: Source is List, Target is Array + case List sourceList when target is Array targetArray: + PopulateArray(sourceList, targetArray); + break; + + // Case 2.2: Source is List, Target is IList + case List sourceList when target is IList targetList: + PopulateIList(sourceList, targetList); + break; + + // Case 3: Source is a simple type; Attempt conversion + default: + var sourceStringValue = source.ToStringInvariant(); + + // Handle basic types or enumerations if not + if (!_targetType.TryParseBasicType(sourceStringValue, out target)) + GetEnumValue(sourceStringValue, ref target); + + break; + } + } + + private void PopulateIList(IEnumerable objects, IList list) + { + var parameterType = GetAddMethodParameterType(_targetType); + if (parameterType == null) return; + + foreach (var item in objects) + { + try + { + list.Add(FromJsonResult( + item, + _jsonSerializerCase, + parameterType, + _includeNonPublic)); + } + catch + { + // ignored + } + } + } + + private void PopulateArray(IList objects, Array array) + { + var elementType = _targetType.GetElementType(); + + for (var i = 0; i < objects.Count; i++) + { + try + { + var targetItem = FromJsonResult( + objects[i], + _jsonSerializerCase, + elementType, + _includeNonPublic); + array.SetValue(targetItem, i); + } + catch + { + // ignored + } + } + } + + private void GetEnumValue(string sourceStringValue, ref object? target) + { + var enumType = Nullable.GetUnderlyingType(_targetType); + if (enumType == null && _targetType.IsEnum) enumType = _targetType; + if (enumType == null) return; + + try + { + target = Enum.Parse(enumType, sourceStringValue); + } + catch + { + // ignored + } + } + + private void PopulateDictionary(IDictionary sourceProperties, IDictionary targetDictionary) + { + // find the add method of the target dictionary + var addMethod = _targetType.GetMethods() + .FirstOrDefault( + m => m.Name == AddMethodName && m.IsPublic && m.GetParameters().Length == 2); + + // skip if we don't have a compatible add method + if (addMethod == null) return; + var addMethodParameters = addMethod.GetParameters(); + if (addMethodParameters[0].ParameterType != typeof(string)) return; + + // Retrieve the target entry type + var targetEntryType = addMethodParameters[1].ParameterType; + + // Add the items to the target dictionary + foreach (var sourceProperty in sourceProperties) + { + try + { + var targetEntryValue = FromJsonResult( + sourceProperty.Value, + _jsonSerializerCase, + targetEntryType, + _includeNonPublic); + targetDictionary.Add(sourceProperty.Key, targetEntryValue); + } + catch + { + // ignored + } + } + } + + private void PopulateObject(IDictionary sourceProperties) + { + if (_targetType.IsValueType) + { + PopulateFields(sourceProperties); + } + + PopulateProperties(sourceProperties); + } + + private void PopulateProperties(IDictionary sourceProperties) + { + var properties = PropertyTypeCache.DefaultCache.Value.RetrieveFilteredProperties(_targetType, false, p => p.CanWrite); + + foreach (var property in properties) + { + var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, property); + if (sourcePropertyValue == null) continue; + + try + { + var currentPropertyValue = !property.PropertyType.IsArray + ? property.GetCacheGetMethod(_includeNonPublic)(_target) + : null; + + var targetPropertyValue = FromJsonResult( + sourcePropertyValue, + property.PropertyType, + ref currentPropertyValue, + _includeNonPublic); + + property.GetCacheSetMethod(_includeNonPublic)(_target, new[] { targetPropertyValue }); + } + catch + { + // ignored + } + } + } + + private void PopulateFields(IDictionary sourceProperties) + { + foreach (var field in FieldTypeCache.DefaultCache.Value.RetrieveAllFields(_targetType)) + { + var sourcePropertyValue = GetSourcePropertyValue(sourceProperties, field); + if (sourcePropertyValue == null) continue; + + var targetPropertyValue = FromJsonResult( + sourcePropertyValue, + _jsonSerializerCase, + field.FieldType, + _includeNonPublic); + + try + { + field.SetValue(_target, targetPropertyValue); + } + catch + { + // ignored + } + } + } + } + } +} diff --git a/Swan.Lite/Formatters/Json.Deserializer.cs b/Swan.Lite/Formatters/Json.Deserializer.cs new file mode 100644 index 0000000..4caa463 --- /dev/null +++ b/Swan.Lite/Formatters/Json.Deserializer.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public partial class Json + { + /// + /// A simple JSON Deserializer. + /// + private class Deserializer + { + #region State Variables + + private readonly object? _result; + private readonly string _json; + + private Dictionary _resultObject; + private List _resultArray; + private ReadState _state = ReadState.WaitingForRootOpen; + private string? _currentFieldName; + + private int _index; + + #endregion + + private Deserializer(string json, int startIndex) + { + _json = json; + + for (_index = startIndex; _index < _json.Length; _index++) + { + switch (_state) + { + case ReadState.WaitingForRootOpen: + WaitForRootOpen(); + continue; + case ReadState.WaitingForField when char.IsWhiteSpace(_json, _index): + continue; + case ReadState.WaitingForField when (_resultObject != null && _json[_index] == CloseObjectChar) + || (_resultArray != null && _json[_index] == CloseArrayChar): + // Handle empty arrays and empty objects + _result = _resultObject ?? _resultArray as object; + return; + case ReadState.WaitingForField when _json[_index] != StringQuotedChar: + throw CreateParserException($"'{StringQuotedChar}'"); + case ReadState.WaitingForField: + { + var charCount = GetFieldNameCount(); + + _currentFieldName = Unescape(_json.SliceLength(_index + 1, charCount)); + _index += charCount + 1; + _state = ReadState.WaitingForColon; + continue; + } + + case ReadState.WaitingForColon when char.IsWhiteSpace(_json, _index): + continue; + case ReadState.WaitingForColon when _json[_index] != ValueSeparatorChar: + throw CreateParserException($"'{ValueSeparatorChar}'"); + case ReadState.WaitingForColon: + _state = ReadState.WaitingForValue; + continue; + case ReadState.WaitingForValue when char.IsWhiteSpace(_json, _index): + continue; + case ReadState.WaitingForValue when (_resultObject != null && _json[_index] == CloseObjectChar) + || (_resultArray != null && _json[_index] == CloseArrayChar): + // Handle empty arrays and empty objects + _result = _resultObject ?? _resultArray as object; + return; + case ReadState.WaitingForValue: + ExtractValue(); + continue; + } + + if (_state != ReadState.WaitingForNextOrRootClose || char.IsWhiteSpace(_json, _index)) continue; + + if (_json[_index] == FieldSeparatorChar) + { + if (_resultObject != null) + { + _state = ReadState.WaitingForField; + _currentFieldName = null; + continue; + } + + _state = ReadState.WaitingForValue; + continue; + } + + if ((_resultObject == null || _json[_index] != CloseObjectChar) && + (_resultArray == null || _json[_index] != CloseArrayChar)) + { + throw CreateParserException($"'{FieldSeparatorChar}' '{CloseObjectChar}' or '{CloseArrayChar}'"); + } + + _result = _resultObject ?? _resultArray as object; + return; + } + } + + internal static object? DeserializeInternal(string json) => new Deserializer(json, 0)._result; + + private void WaitForRootOpen() + { + if (char.IsWhiteSpace(_json, _index)) return; + + switch (_json[_index]) + { + case OpenObjectChar: + _resultObject = new Dictionary(); + _state = ReadState.WaitingForField; + return; + case OpenArrayChar: + _resultArray = new List(); + _state = ReadState.WaitingForValue; + return; + default: + throw CreateParserException($"'{OpenObjectChar}' or '{OpenArrayChar}'"); + } + } + + private void ExtractValue() + { + // determine the value based on what it starts with + switch (_json[_index]) + { + case StringQuotedChar: // expect a string + ExtractStringQuoted(); + break; + + case OpenObjectChar: // expect object + case OpenArrayChar: // expect array + ExtractObject(); + break; + + case 't': // expect true + ExtractConstant(TrueLiteral, true); + break; + + case 'f': // expect false + ExtractConstant(FalseLiteral, false); + break; + + case 'n': // expect null + ExtractConstant(NullLiteral, null); + break; + + default: // expect number + ExtractNumber(); + break; + } + + _currentFieldName = null; + _state = ReadState.WaitingForNextOrRootClose; + } + + private static string Unescape(string str) + { + // check if we need to unescape at all + if (str.IndexOf(StringEscapeChar) < 0) + return str; + + var builder = new StringBuilder(str.Length); + for (var i = 0; i < str.Length; i++) + { + if (str[i] != StringEscapeChar) + { + builder.Append(str[i]); + continue; + } + + if (i + 1 > str.Length - 1) + break; + + // escape sequence begins here + switch (str[i + 1]) + { + case 'u': + i = ExtractEscapeSequence(str, i, builder); + break; + case 'b': + builder.Append('\b'); + i += 1; + break; + case 't': + builder.Append('\t'); + i += 1; + break; + case 'n': + builder.Append('\n'); + i += 1; + break; + case 'f': + builder.Append('\f'); + i += 1; + break; + case 'r': + builder.Append('\r'); + i += 1; + break; + default: + builder.Append(str[i + 1]); + i += 1; + break; + } + } + + return builder.ToString(); + } + + private static int ExtractEscapeSequence(string str, int i, StringBuilder builder) + { + var startIndex = i + 2; + var endIndex = i + 5; + if (endIndex > str.Length - 1) + { + builder.Append(str[i + 1]); + i += 1; + return i; + } + + var hexCode = str.Slice(startIndex, endIndex).ConvertHexadecimalToBytes(); + builder.Append(Encoding.BigEndianUnicode.GetChars(hexCode)); + i += 5; + return i; + } + + private int GetFieldNameCount() + { + var charCount = 0; + for (var j = _index + 1; j < _json.Length; j++) + { + if (_json[j] == StringQuotedChar && _json[j - 1] != StringEscapeChar) + break; + + charCount++; + } + + return charCount; + } + + private void ExtractObject() + { + // Extract and set the value + var deserializer = new Deserializer(_json, _index); + + if (_currentFieldName != null) + _resultObject[_currentFieldName] = deserializer._result; + else + _resultArray.Add(deserializer._result); + + _index = deserializer._index; + } + + private void ExtractNumber() + { + var charCount = 0; + for (var j = _index; j < _json.Length; j++) + { + if (char.IsWhiteSpace(_json[j]) || _json[j] == FieldSeparatorChar + || (_resultObject != null && _json[j] == CloseObjectChar) + || (_resultArray != null && _json[j] == CloseArrayChar)) + break; + + charCount++; + } + + // Extract and set the value + var stringValue = _json.SliceLength(_index, charCount); + + if (decimal.TryParse(stringValue, System.Globalization.NumberStyles.Number, System.Globalization.CultureInfo.InvariantCulture, out var value) == false) + throw CreateParserException("[number]"); + + if (_currentFieldName != null) + _resultObject[_currentFieldName] = value; + else + _resultArray.Add(value); + + _index += charCount - 1; + } + + private void ExtractConstant(string boolValue, bool? value) + { + if (_json.SliceLength(_index, boolValue.Length) != boolValue) + throw CreateParserException($"'{ValueSeparatorChar}'"); + + // Extract and set the value + if (_currentFieldName != null) + _resultObject[_currentFieldName] = value; + else + _resultArray.Add(value); + + _index += boolValue.Length - 1; + } + + private void ExtractStringQuoted() + { + var charCount = 0; + var escapeCharFound = false; + for (var j = _index + 1; j < _json.Length; j++) + { + if (_json[j] == StringQuotedChar && !escapeCharFound) + break; + + escapeCharFound = _json[j] == StringEscapeChar && !escapeCharFound; + charCount++; + } + + // Extract and set the value + var value = Unescape(_json.SliceLength(_index + 1, charCount)); + if (_currentFieldName != null) + _resultObject[_currentFieldName] = value; + else + _resultArray.Add(value); + + _index += charCount + 1; + } + + private FormatException CreateParserException(string expected) + { + var textPosition = _json.TextPositionAt(_index); + return new FormatException( + $"Parser error (Line {textPosition.Item1}, Col {textPosition.Item2}, State {_state}): Expected {expected} but got '{_json[_index]}'."); + } + + /// + /// Defines the different JSON read states. + /// + private enum ReadState + { + WaitingForRootOpen, + WaitingForField, + WaitingForColon, + WaitingForValue, + WaitingForNextOrRootClose, + } + } + } +} diff --git a/Swan.Lite/Formatters/Json.Serializer.cs b/Swan.Lite/Formatters/Json.Serializer.cs new file mode 100644 index 0000000..95a3181 --- /dev/null +++ b/Swan.Lite/Formatters/Json.Serializer.cs @@ -0,0 +1,348 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public partial class Json + { + /// + /// A simple JSON serializer. + /// + private class Serializer + { + #region Private Declarations + + private static readonly Dictionary IndentStrings = new Dictionary(); + + private readonly SerializerOptions _options; + private readonly string _result; + private readonly StringBuilder _builder; + private readonly string _lastCommaSearch; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The object. + /// The depth. + /// The options. + private Serializer(object? obj, int depth, SerializerOptions options) + { + if (depth > 20) + { + throw new InvalidOperationException( + "The max depth (20) has been reached. Serializer can not continue."); + } + + // Basic Type Handling (nulls, strings, number, date and bool) + _result = ResolveBasicType(obj); + + if (!string.IsNullOrWhiteSpace(_result)) + return; + + _options = options; + + // Handle circular references correctly and avoid them + if (options.IsObjectPresent(obj!)) + { + _result = $"{{ \"$circref\": \"{Escape(obj!.GetHashCode().ToStringInvariant(), false)}\" }}"; + return; + } + + // At this point, we will need to construct the object with a StringBuilder. + _lastCommaSearch = FieldSeparatorChar + (_options.Format ? Environment.NewLine : string.Empty); + _builder = new StringBuilder(); + + _result = obj switch + { + IDictionary itemsZero when itemsZero.Count == 0 => EmptyObjectLiteral, + IDictionary items => ResolveDictionary(items, depth), + IEnumerable enumerableZero when !enumerableZero.Cast().Any() => EmptyArrayLiteral, + IEnumerable enumerableBytes when enumerableBytes is byte[] bytes => Serialize(bytes.ToBase64(), depth, _options), + IEnumerable enumerable => ResolveEnumerable(enumerable, depth), + _ => ResolveObject(obj!, depth) + }; + } + + internal static string Serialize(object? obj, int depth, SerializerOptions options) => new Serializer(obj, depth, options)._result; + + #endregion + + #region Helper Methods + + private static string ResolveBasicType(object? obj) + { + switch (obj) + { + case null: + return NullLiteral; + case string s: + return Escape(s, true); + case bool b: + return b ? TrueLiteral : FalseLiteral; + case Type _: + case Assembly _: + case MethodInfo _: + case PropertyInfo _: + case EventInfo _: + return Escape(obj.ToString(), true); + case DateTime d: + return $"{StringQuotedChar}{d:s}{StringQuotedChar}"; + default: + var targetType = obj.GetType(); + + if (!Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) + return string.Empty; + + var escapedValue = Escape(Definitions.BasicTypesInfo.Value[targetType].ToStringInvariant(obj), false); + + return decimal.TryParse(escapedValue, out _) + ? $"{escapedValue}" + : $"{StringQuotedChar}{escapedValue}{StringQuotedChar}"; + } + } + + private static bool IsNonEmptyJsonArrayOrObject(string serialized) + { + if (serialized == EmptyObjectLiteral || serialized == EmptyArrayLiteral) return false; + + // find the first position the character is not a space + return serialized.Where(c => c != ' ').Select(c => c == OpenObjectChar || c == OpenArrayChar).FirstOrDefault(); + } + + private static string Escape(string str, bool quoted) + { + if (str == null) + return string.Empty; + + var builder = new StringBuilder(str.Length * 2); + if (quoted) builder.Append(StringQuotedChar); + Escape(str, builder); + if (quoted) builder.Append(StringQuotedChar); + return builder.ToString(); + } + + private static void Escape(string str, StringBuilder builder) + { + foreach (var currentChar in str) + { + switch (currentChar) + { + case '\\': + case '"': + case '/': + builder + .Append('\\') + .Append(currentChar); + break; + case '\b': + builder.Append("\\b"); + break; + case '\t': + builder.Append("\\t"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\r': + builder.Append("\\r"); + break; + default: + if (currentChar < ' ') + { + var escapeBytes = BitConverter.GetBytes((ushort)currentChar); + if (BitConverter.IsLittleEndian == false) + Array.Reverse(escapeBytes); + + builder.Append("\\u") + .Append(escapeBytes[1].ToString("X").PadLeft(2, '0')) + .Append(escapeBytes[0].ToString("X").PadLeft(2, '0')); + } + else + { + builder.Append(currentChar); + } + + break; + } + } + } + + private Dictionary CreateDictionary( + Dictionary fields, + string targetType, + object target) + { + // Create the dictionary and extract the properties + var objectDictionary = new Dictionary(); + + if (string.IsNullOrWhiteSpace(_options.TypeSpecifier) == false) + objectDictionary[_options.TypeSpecifier] = targetType; + + foreach (var field in fields) + { + // Build the dictionary using property names and values + // Note: used to be: property.GetValue(target); but we would be reading private properties + try + { + objectDictionary[field.Key] = field.Value is PropertyInfo property + ? property.GetCacheGetMethod(_options.IncludeNonPublic)?.Invoke(target) + : (field.Value as FieldInfo)?.GetValue(target); + } + catch + { + /* ignored */ + } + } + + return objectDictionary; + } + + private string ResolveDictionary(IDictionary items, int depth) + { + Append(OpenObjectChar, depth); + AppendLine(); + + // Iterate through the elements and output recursively + var writeCount = 0; + foreach (var key in items.Keys) + { + // Serialize and append the key (first char indented) + Append(StringQuotedChar, depth + 1); + Escape(key.ToString(), _builder); + _builder + .Append(StringQuotedChar) + .Append(ValueSeparatorChar) + .Append(" "); + + // Serialize and append the value + var serializedValue = Serialize(items[key], depth + 1, _options); + + if (IsNonEmptyJsonArrayOrObject(serializedValue)) AppendLine(); + Append(serializedValue, 0); + + // Add a comma and start a new line -- We will remove the last one when we are done writing the elements + Append(FieldSeparatorChar, 0); + AppendLine(); + writeCount++; + } + + // Output the end of the object and set the result + RemoveLastComma(); + Append(CloseObjectChar, writeCount > 0 ? depth : 0); + return _builder.ToString(); + } + + private string ResolveObject(object target, int depth) + { + var targetType = target.GetType(); + + if (targetType.IsEnum) + return Convert.ToInt64(target, System.Globalization.CultureInfo.InvariantCulture).ToString(); + + var fields = _options.GetProperties(targetType); + + if (fields.Count == 0 && string.IsNullOrWhiteSpace(_options.TypeSpecifier)) + return EmptyObjectLiteral; + + // If we arrive here, then we convert the object into a + // dictionary of property names and values and call the serialization + // function again + var objectDictionary = CreateDictionary(fields, targetType.ToString(), target); + + return Serialize(objectDictionary, depth, _options); + } + + private string ResolveEnumerable(IEnumerable target, int depth) + { + // Cast the items as a generic object array + var items = target.Cast(); + + Append(OpenArrayChar, depth); + AppendLine(); + + // Iterate through the elements and output recursively + var writeCount = 0; + foreach (var entry in items) + { + var serializedValue = Serialize(entry, depth + 1, _options); + + if (IsNonEmptyJsonArrayOrObject(serializedValue)) + Append(serializedValue, 0); + else + Append(serializedValue, depth + 1); + + Append(FieldSeparatorChar, 0); + AppendLine(); + writeCount++; + } + + // Output the end of the array and set the result + RemoveLastComma(); + Append(CloseArrayChar, writeCount > 0 ? depth : 0); + return _builder.ToString(); + } + + private void SetIndent(int depth) + { + if (_options.Format == false || depth <= 0) return; + + _builder.Append(IndentStrings.GetOrAdd(depth, x => new string(' ', x * 4))); + } + + /// + /// Removes the last comma in the current string builder. + /// + private void RemoveLastComma() + { + if (_builder.Length < _lastCommaSearch.Length) + return; + + if (_lastCommaSearch.Where((t, i) => _builder[_builder.Length - _lastCommaSearch.Length + i] != t).Any()) + { + return; + } + + // If we got this far, we simply remove the comma character + _builder.Remove(_builder.Length - _lastCommaSearch.Length, 1); + } + + private void Append(string text, int depth) + { + SetIndent(depth); + _builder.Append(text); + } + + private void Append(char text, int depth) + { + SetIndent(depth); + _builder.Append(text); + } + + private void AppendLine() + { + if (_options.Format == false) return; + _builder.Append(Environment.NewLine); + } + + #endregion + } + } +} diff --git a/Swan.Lite/Formatters/Json.SerializerOptions.cs b/Swan.Lite/Formatters/Json.SerializerOptions.cs new file mode 100644 index 0000000..ee80f12 --- /dev/null +++ b/Swan.Lite/Formatters/Json.SerializerOptions.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public class SerializerOptions + { + private static readonly ConcurrentDictionary, MemberInfo>> + TypeCache = new ConcurrentDictionary, MemberInfo>>(); + + private readonly string[]? _includeProperties; + private readonly string[]? _excludeProperties; + private readonly Dictionary> _parentReferences = new Dictionary>(); + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [format]. + /// The type specifier. + /// The include properties. + /// The exclude properties. + /// if set to true [include non public]. + /// The parent references. + /// The json serializer case. + public SerializerOptions( + bool format, + string? typeSpecifier, + string[]? includeProperties, + string[]? excludeProperties = null, + bool includeNonPublic = true, + IReadOnlyCollection? parentReferences = null, + JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) + { + _includeProperties = includeProperties; + _excludeProperties = excludeProperties; + + IncludeNonPublic = includeNonPublic; + Format = format; + TypeSpecifier = typeSpecifier; + JsonSerializerCase = jsonSerializerCase; + + if (parentReferences == null) + return; + + foreach (var parentReference in parentReferences.Where(x => x.IsAlive)) + { + IsObjectPresent(parentReference.Target); + } + } + + /// + /// Gets a value indicating whether this is format. + /// + /// + /// true if format; otherwise, false. + /// + public bool Format { get; } + + /// + /// Gets the type specifier. + /// + /// + /// The type specifier. + /// + public string? TypeSpecifier { get; } + + /// + /// Gets a value indicating whether [include non public]. + /// + /// + /// true if [include non public]; otherwise, false. + /// + public bool IncludeNonPublic { get; } + + /// + /// Gets the json serializer case. + /// + /// + /// The json serializer case. + /// + public JsonSerializerCase JsonSerializerCase { get; } + + internal bool IsObjectPresent(object target) + { + var hashCode = target.GetHashCode(); + + if (_parentReferences.ContainsKey(hashCode)) + { + if (_parentReferences[hashCode].Any(p => ReferenceEquals(p.Target, target))) + return true; + + _parentReferences[hashCode].Add(new WeakReference(target)); + return false; + } + + _parentReferences.Add(hashCode, new List { new WeakReference(target) }); + return false; + } + + internal Dictionary GetProperties(Type targetType) + => GetPropertiesCache(targetType) + .When(() => _includeProperties?.Length > 0, + query => query.Where(p => _includeProperties.Contains(p.Key.Item1))) + .When(() => _excludeProperties?.Length > 0, + query => query.Where(p => !_excludeProperties.Contains(p.Key.Item1))) + .ToDictionary(x => x.Key.Item2, x => x.Value); + + private Dictionary, MemberInfo> GetPropertiesCache(Type targetType) + { + if (TypeCache.TryGetValue(targetType, out var current)) + return current; + + var fields = + new List(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).Where(p => p.CanRead)); + + // If the target is a struct (value type) navigate the fields. + if (targetType.IsValueType) + { + fields.AddRange(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType)); + } + + var value = fields + .ToDictionary( + x => Tuple.Create(x.Name, + x.GetCustomAttribute()?.PropertyName ?? x.Name.GetNameWithCase(JsonSerializerCase)), + x => x); + + TypeCache.TryAdd(targetType, value); + + return value; + } + } +} diff --git a/Swan.Lite/Formatters/Json.cs b/Swan.Lite/Formatters/Json.cs new file mode 100644 index 0000000..d214965 --- /dev/null +++ b/Swan.Lite/Formatters/Json.cs @@ -0,0 +1,379 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Swan.Collections; +using Swan.Reflection; + +namespace Swan.Formatters +{ + /// + /// A very simple, light-weight JSON library written by Mario + /// to teach Geo how things are done + /// + /// This is an useful helper for small tasks but it doesn't represent a full-featured + /// serializer such as the beloved Json.NET. + /// + public static partial class Json + { + #region Constants + + internal const string AddMethodName = "Add"; + + private const char OpenObjectChar = '{'; + private const char CloseObjectChar = '}'; + + private const char OpenArrayChar = '['; + private const char CloseArrayChar = ']'; + + private const char FieldSeparatorChar = ','; + private const char ValueSeparatorChar = ':'; + + private const char StringEscapeChar = '\\'; + private const char StringQuotedChar = '"'; + + private const string EmptyObjectLiteral = "{ }"; + private const string EmptyArrayLiteral = "[ ]"; + private const string TrueLiteral = "true"; + private const string FalseLiteral = "false"; + private const string NullLiteral = "null"; + + #endregion + + private static readonly CollectionCacheRepository IgnoredPropertiesCache = new CollectionCacheRepository(); + + #region Public API + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The type specifier. Leave null or empty to avoid setting. + /// if set to true non-public getters will be also read. + /// The included property names. + /// The excluded property names. + /// + /// A that represents the current object. + /// + /// + /// The following example describes how to serialize a simple object. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// var obj = new { One = "One", Two = "Two" }; + /// + /// var serial = Json.Serialize(obj); // {"One": "One","Two": "Two"} + /// } + /// } + /// + /// + /// The following example details how to serialize an object using the . + /// + /// + /// using Swan.Attributes; + /// using Swan.Formatters; + /// + /// class Example + /// { + /// class JsonPropertyExample + /// { + /// [JsonProperty("data")] + /// public string Data { get; set; } + /// + /// [JsonProperty("ignoredData", true)] + /// public string IgnoredData { get; set; } + /// } + /// + /// static void Main() + /// { + /// var obj = new JsonPropertyExample() { Data = "OK", IgnoredData = "OK" }; + /// + /// // {"data": "OK"} + /// var serializedObj = Json.Serialize(obj); + /// } + /// } + /// + /// + public static string Serialize( + object? obj, + bool format = false, + string? typeSpecifier = null, + bool includeNonPublic = false, + string[]? includedNames = null, + params string[] excludedNames) + { + return Serialize(obj, format, typeSpecifier, includeNonPublic, includedNames, excludedNames, null, JsonSerializerCase.None); + } + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// The json serializer case. + /// if set to true [format]. + /// The type specifier. + /// + /// A that represents the current object. + /// + public static string Serialize( + object? obj, + JsonSerializerCase jsonSerializerCase, + bool format = false, + string? typeSpecifier = null) => Serialize(obj, format, typeSpecifier, false, null, null, null, jsonSerializerCase); + + /// + /// Serializes the specified object into a JSON string. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The type specifier. Leave null or empty to avoid setting. + /// if set to true non-public getters will be also read. + /// The included property names. + /// The excluded property names. + /// The parent references. + /// The json serializer case. + /// + /// A that represents the current object. + /// + public static string Serialize( + object? obj, + bool format, + string? typeSpecifier, + bool includeNonPublic, + string[]? includedNames, + string[]? excludedNames, + List? parentReferences, + JsonSerializerCase jsonSerializerCase) + { + if (obj != null && (obj is string || Definitions.AllBasicValueTypes.Contains(obj.GetType()))) + { + return SerializePrimitiveValue(obj); + } + + var options = new SerializerOptions( + format, + typeSpecifier, + includedNames, + GetExcludedNames(obj?.GetType(), excludedNames), + includeNonPublic, + parentReferences, + jsonSerializerCase); + + return Serialize(obj, options); + } + + /// + /// Serializes the specified object using the SerializerOptions provided. + /// + /// The object. + /// The options. + /// + /// A that represents the current object. + /// + public static string Serialize(object? obj, SerializerOptions options) => Serializer.Serialize(obj, 0, options); + + /// + /// Serializes the specified object only including the specified property names. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The include names. + /// A that represents the current object. + /// + /// The following example shows how to serialize a simple object including the specified properties. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // object to serialize + /// var obj = new { One = "One", Two = "Two", Three = "Three" }; + /// + /// // the included names + /// var includedNames = new[] { "Two", "Three" }; + /// + /// // serialize only the included names + /// var data = Json.SerializeOnly(basicObject, true, includedNames); + /// // {"Two": "Two","Three": "Three" } + /// } + /// } + /// + /// + public static string SerializeOnly(object? obj, bool format, params string[] includeNames) + => Serialize(obj, new SerializerOptions(format, null, includeNames)); + + /// + /// Serializes the specified object excluding the specified property names. + /// + /// The object. + /// if set to true it formats and indents the output. + /// The exclude names. + /// A that represents the current object. + /// + /// The following code shows how to serialize a simple object excluding the specified properties. + /// + /// using Swan.Formatters; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // object to serialize + /// var obj = new { One = "One", Two = "Two", Three = "Three" }; + /// + /// // the excluded names + /// var excludeNames = new[] { "Two", "Three" }; + /// + /// // serialize excluding + /// var data = Json.SerializeExcluding(basicObject, false, includedNames); + /// // {"One": "One"} + /// } + /// } + /// + /// + public static string SerializeExcluding(object? obj, bool format, params string[] excludeNames) + => Serialize(obj, new SerializerOptions(format, null, null, excludeNames)); + + /// + /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object] + /// depending on the syntax of the JSON string. + /// + /// The JSON string. + /// The json serializer case. + /// + /// Type of the current deserializes. + /// + /// + /// The following code shows how to deserialize a JSON string into a Dictionary. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json to deserialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified json into a Dictionary<string, object>. + /// var data = Json.Deserialize(basicJson, JsonSerializerCase.None); + /// } + /// } + /// + public static object? Deserialize( + string json, + JsonSerializerCase jsonSerializerCase) + => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase); + + /// + /// Deserializes the specified json string as either a Dictionary[string, object] or as a List[object] + /// depending on the syntax of the JSON string. + /// + /// The JSON string. + /// + /// Type of the current deserializes. + /// + /// + /// The following code shows how to deserialize a JSON string into a Dictionary. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json to deserialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified json into a Dictionary<string, object>. + /// var data = Json.Deserialize(basicJson); + /// } + /// } + /// + public static object? Deserialize(string json) + => Deserialize(json, JsonSerializerCase.None); + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// Non-public constructors and property setters are ignored. + /// + /// The type of object to deserialize. + /// The JSON string. + /// The JSON serializer case. + /// + /// The deserialized specified type object. + /// + /// + /// The following code describes how to deserialize a JSON string into an object of type T. + /// + /// using Swan.Formatters; + /// class Example + /// { + /// static void Main() + /// { + /// // json type BasicJson to serialize + /// var basicJson = "{\"One\":\"One\",\"Two\":\"Two\",\"Three\":\"Three\"}"; + /// // deserializes the specified string in a new instance of the type BasicJson. + /// var data = Json.Deserialize<BasicJson>(basicJson); + /// } + /// } + /// + public static T Deserialize(string json, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) + => (T)Deserialize(json, typeof(T), jsonSerializerCase: jsonSerializerCase); + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// + /// The type of object to deserialize. + /// The JSON string. + /// if set to true, it also uses the non-public constructors and property setters. + /// The deserialized specified type object. + public static T Deserialize(string json, bool includeNonPublic) => (T)Deserialize(json, typeof(T), includeNonPublic); + + /// + /// Deserializes the specified JSON string and converts it to the specified object type. + /// + /// The JSON string. + /// Type of the result. + /// if set to true, it also uses the non-public constructors and property setters. + /// The json serializer case. + /// + /// Type of the current conversion from json result. + /// + public static object? Deserialize(string json, Type resultType, bool includeNonPublic = false, JsonSerializerCase jsonSerializerCase = JsonSerializerCase.None) + => Converter.FromJsonResult(Deserializer.DeserializeInternal(json), jsonSerializerCase, resultType, includeNonPublic); + + #endregion + + #region Private API + + private static string[]? GetExcludedNames(Type? type, string[]? excludedNames) + { + if (type == null) + return excludedNames; + + var excludedByAttr = IgnoredPropertiesCache.Retrieve(type, t => t.GetProperties() + .Where(x => AttributeCache.DefaultCache.Value.RetrieveOne(x)?.Ignored == true) + .Select(x => x.Name)); + + if (excludedByAttr?.Any() != true) + return excludedNames; + + return excludedNames?.Any(string.IsNullOrWhiteSpace) == true + ? excludedByAttr.Intersect(excludedNames.Where(y => !string.IsNullOrWhiteSpace(y))).ToArray() + : excludedByAttr.ToArray(); + } + + private static string SerializePrimitiveValue(object obj) => + obj switch + { + string stringValue => stringValue, + bool boolValue => boolValue ? TrueLiteral : FalseLiteral, + _ => obj.ToString() + }; + + #endregion + } +} diff --git a/Swan.Lite/Formatters/JsonPropertyAttribute.cs b/Swan.Lite/Formatters/JsonPropertyAttribute.cs new file mode 100644 index 0000000..d05ac4d --- /dev/null +++ b/Swan.Lite/Formatters/JsonPropertyAttribute.cs @@ -0,0 +1,39 @@ +using System; + +namespace Swan.Formatters +{ + /// + /// An attribute used to help setup a property behavior when serialize/deserialize JSON. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class JsonPropertyAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// Name of the property. + /// if set to true [ignored]. + public JsonPropertyAttribute(string propertyName, bool ignored = false) + { + PropertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); + Ignored = ignored; + } + + /// + /// Gets or sets the name of the property. + /// + /// + /// The name of the property. + /// + public string PropertyName { get; } + + /// + /// Gets or sets a value indicating whether this is ignored. + /// + /// + /// true if ignored; otherwise, false. + /// + public bool Ignored { get; } + } +} \ No newline at end of file diff --git a/Swan.Lite/FromString.cs b/Swan.Lite/FromString.cs new file mode 100644 index 0000000..6d920dc --- /dev/null +++ b/Swan.Lite/FromString.cs @@ -0,0 +1,344 @@ +using System; +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Linq.Expressions; +using System.Reflection; + +namespace Swan +{ + /// + /// Provides a standard way to convert strings to different types. + /// + public static class FromString + { + // It doesn't matter which converter we get here: ConvertFromInvariantString is not virtual. + private static readonly MethodInfo ConvertFromInvariantStringMethod + = new Func(TypeDescriptor.GetConverter(typeof(int)).ConvertFromInvariantString).Method; + + private static readonly MethodInfo TryConvertToInternalMethod + = typeof(FromString).GetMethod(nameof(TryConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly MethodInfo ConvertToInternalMethod + = typeof(FromString).GetMethod(nameof(ConvertToInternal), BindingFlags.Static | BindingFlags.NonPublic); + + private static readonly ConcurrentDictionary> GenericTryConvertToMethods + = new ConcurrentDictionary>(); + + private static readonly ConcurrentDictionary> GenericConvertToMethods + = new ConcurrentDictionary>(); + + /// + /// Determines whether a string can be converted to the specified type. + /// + /// The type resulting from the conversion. + /// if the conversion is possible; + /// otherwise, . + /// is . + public static bool CanConvertTo(Type type) + => TypeDescriptor.GetConverter(type).CanConvertFrom(typeof(string)); + + /// + /// Determines whether a string can be converted to the specified type. + /// + /// The type resulting from the conversion. + /// if the conversion is possible; + /// otherwise, . + public static bool CanConvertTo() + => TypeDescriptor.GetConverter(typeof(TResult)).CanConvertFrom(typeof(string)); + + /// + /// Attempts to convert a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + /// is . + public static bool TryConvertTo(Type type, string str, out object? result) + { + var converter = TypeDescriptor.GetConverter(type); + if (!converter.CanConvertFrom(typeof(string))) + { + result = null; + return false; + } + + try + { + result = converter.ConvertFromInvariantString(str); + return true; + } + catch (Exception e) when (!e.IsCriticalException()) + { + result = null; + return false; + } + } + + /// + /// Attempts to convert a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + public static bool TryConvertTo(string str, out TResult result) + { + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + if (!converter.CanConvertFrom(typeof(string))) + { + result = default; + return false; + } + + try + { + result = (TResult)converter.ConvertFromInvariantString(str); + return true; + } + catch (Exception e) when (!e.IsCriticalException()) + { + result = default; + return false; + } + } + + /// + /// Converts a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// An instance of . + /// is . + /// The conversion was not successful. + public static object ConvertTo(Type type, string str) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + try + { + return TypeDescriptor.GetConverter(type).ConvertFromInvariantString(str); + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(type, e); + } + } + + /// + /// Converts a string to the specified type. + /// + /// The type resulting from the conversion. + /// The string to convert. + /// An instance of . + /// + /// The conversion was not successful. + /// + public static TResult ConvertTo(string str) + { + try + { + return (TResult)TypeDescriptor.GetConverter(typeof(TResult)).ConvertFromInvariantString(str); + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(typeof(TResult), e); + } + } + + /// + /// Attempts to convert an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + /// is . + public static bool TryConvertTo(Type type, string[] strings, out object? result) + { + if (strings == null) + { + result = null; + return false; + } + + var method = GenericTryConvertToMethods.GetOrAdd(type, BuildNonGenericTryConvertLambda); + var (success, methodResult) = method(strings); + result = methodResult; + return success; + } + + /// + /// Attempts to convert an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// When this method returns , + /// the result of the conversion. This parameter is passed uninitialized. + /// if the conversion is successful; + /// otherwise, . + public static bool TryConvertTo(string[] strings, out TResult[]? result) + { + if (strings == null) + { + result = null; + return false; + } + + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + if (!converter.CanConvertFrom(typeof(string))) + { + result = null; + return false; + } + + try + { + result = new TResult[strings.Length]; + var i = 0; + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + + return true; + } + catch (Exception e) when (!e.IsCriticalException()) + { + result = null; + return false; + } + } + + /// + /// Converts an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// An array of . + /// is . + /// The conversion of at least one + /// of the elements of was not successful. + public static object? ConvertTo(Type type, string[] strings) + { + if (strings == null) + return null; + + var method = GenericConvertToMethods.GetOrAdd(type, BuildNonGenericConvertLambda); + return method(strings); + } + + /// + /// Converts an array of strings to an array of the specified type. + /// + /// The type resulting from the conversion of each + /// element of . + /// The array to convert. + /// An array of . + /// The conversion of at least one + /// of the elements of was not successful. + public static TResult[]? ConvertTo(string[] strings) + { + if (strings == null) + return null; + + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + var result = new TResult[strings.Length]; + var i = 0; + try + { + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(typeof(TResult), e); + } + + return result; + } + + /// + /// Converts a expression, if the type can be converted to string, to a new expression including + /// the conversion to string. + /// + /// The type. + /// The string. + /// A new expression where the previous expression is converted to string. + public static Expression? ConvertExpressionTo(Type type, Expression str) + { + var converter = TypeDescriptor.GetConverter(type); + + return converter.CanConvertFrom(typeof(string)) + ? Expression.Convert( + Expression.Call(Expression.Constant(converter), ConvertFromInvariantStringMethod, str), + type) + : null; + } + + private static Func BuildNonGenericTryConvertLambda(Type type) + { + var methodInfo = TryConvertToInternalMethod.MakeGenericMethod(type); + var parameter = Expression.Parameter(typeof(string[])); + var body = Expression.Call(methodInfo, parameter); + var lambda = Expression.Lambda>(body, parameter); + return lambda.Compile(); + } + + private static (bool Success, object? Result) TryConvertToInternal(string[] strings) + { + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + if (!converter.CanConvertFrom(typeof(string))) + return (false, null); + + var result = new TResult[strings.Length]; + var i = 0; + try + { + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + + return (true, result); + } + catch (Exception e) when (!e.IsCriticalException()) + { + return (false, null); + } + } + + private static Func BuildNonGenericConvertLambda(Type type) + { + var methodInfo = ConvertToInternalMethod.MakeGenericMethod(type); + var parameter = Expression.Parameter(typeof(string[])); + var body = Expression.Call(methodInfo, parameter); + var lambda = Expression.Lambda>(body, parameter); + return lambda.Compile(); + } + + private static object ConvertToInternal(string[] strings) + { + var converter = TypeDescriptor.GetConverter(typeof(TResult)); + var result = new TResult[strings.Length]; + var i = 0; + try + { + foreach (var str in strings) + result[i++] = (TResult)converter.ConvertFromInvariantString(str); + + return result; + } + catch (Exception e) when (!e.IsCriticalException()) + { + throw new StringConversionException(typeof(TResult), e); + } + } + } +} diff --git a/Swan.Lite/Logging/ConsoleLogger.cs b/Swan.Lite/Logging/ConsoleLogger.cs new file mode 100644 index 0000000..38df7c5 --- /dev/null +++ b/Swan.Lite/Logging/ConsoleLogger.cs @@ -0,0 +1,146 @@ +using System; +using Swan.Lite.Logging; + +namespace Swan.Logging +{ + /// + /// Represents a Console implementation of ILogger. + /// + /// + public class ConsoleLogger : TextLogger, ILogger + { + /// + /// Initializes a new instance of the class. + /// + protected ConsoleLogger() + { + // Empty + } + + /// + /// Gets the current instance of ConsoleLogger. + /// + /// + /// The instance. + /// + public static ConsoleLogger Instance { get; } = new ConsoleLogger(); + + /// + /// Gets or sets the debug logging prefix. + /// + /// + /// The debug prefix. + /// + public static string DebugPrefix { get; set; } = "DBG"; + + /// + /// Gets or sets the trace logging prefix. + /// + /// + /// The trace prefix. + /// + public static string TracePrefix { get; set; } = "TRC"; + + /// + /// Gets or sets the warning logging prefix. + /// + /// + /// The warn prefix. + /// + public static string WarnPrefix { get; set; } = "WRN"; + + /// + /// Gets or sets the fatal logging prefix. + /// + /// + /// The fatal prefix. + /// + public static string FatalPrefix { get; set; } = "FAT"; + + /// + /// Gets or sets the error logging prefix. + /// + /// + /// The error prefix. + /// + public static string ErrorPrefix { get; set; } = "ERR"; + + /// + /// Gets or sets the information logging prefix. + /// + /// + /// The information prefix. + /// + public static string InfoPrefix { get; set; } = "INF"; + + /// + /// Gets or sets the color of the information output logging. + /// + /// + /// The color of the information. + /// + public static ConsoleColor InfoColor { get; set; } = ConsoleColor.Cyan; + + /// + /// Gets or sets the color of the debug output logging. + /// + /// + /// The color of the debug. + /// + public static ConsoleColor DebugColor { get; set; } = ConsoleColor.Gray; + + /// + /// Gets or sets the color of the trace output logging. + /// + /// + /// The color of the trace. + /// + public static ConsoleColor TraceColor { get; set; } = ConsoleColor.DarkGray; + + /// + /// Gets or sets the color of the warning logging. + /// + /// + /// The color of the warn. + /// + public static ConsoleColor WarnColor { get; set; } = ConsoleColor.Yellow; + + /// + /// Gets or sets the color of the error logging. + /// + /// + /// The color of the error. + /// + public static ConsoleColor ErrorColor { get; set; } = ConsoleColor.DarkRed; + + /// + /// Gets or sets the color of the error logging. + /// + /// + /// The color of the error. + /// + public static ConsoleColor FatalColor { get; set; } = ConsoleColor.Red; + + /// + public LogLevel LogLevel { get; set; } = DebugLogger.IsDebuggerAttached ? LogLevel.Trace : LogLevel.Info; + + /// + public void Log(LogMessageReceivedEventArgs logEvent) + { + // Select the writer based on the message type + var writer = logEvent.MessageType == LogLevel.Error + ? TerminalWriters.StandardError + : TerminalWriters.StandardOutput; + + var (outputMessage, color) = GetOutputAndColor(logEvent); + + Terminal.Write(outputMessage, color, writer); + } + + /// + public void Dispose() + { + // Do nothing + } + } +} diff --git a/Swan.Lite/Logging/DebugLogger.cs b/Swan.Lite/Logging/DebugLogger.cs new file mode 100644 index 0000000..4ff26a5 --- /dev/null +++ b/Swan.Lite/Logging/DebugLogger.cs @@ -0,0 +1,53 @@ +using Swan.Lite.Logging; + +namespace Swan.Logging +{ + /// + /// Represents a logger target. This target will write to the + /// Debug console using System.Diagnostics.Debug. + /// + /// + public class DebugLogger : TextLogger, ILogger + { + /// + /// Initializes a new instance of the class. + /// + protected DebugLogger() + { + // Empty + } + + /// + /// Gets the current instance of DebugLogger. + /// + /// + /// The instance. + /// + public static DebugLogger Instance { get; } = new DebugLogger(); + + /// + /// Gets a value indicating whether a debugger is attached. + /// + /// + /// true if this instance is debugger attached; otherwise, false. + /// + public static bool IsDebuggerAttached => System.Diagnostics.Debugger.IsAttached; + + /// + public LogLevel LogLevel { get; set; } = IsDebuggerAttached ? LogLevel.Trace : LogLevel.None; + + /// + public void Log(LogMessageReceivedEventArgs logEvent) + { + var (outputMessage, _) = GetOutputAndColor(logEvent); + + System.Diagnostics.Debug.Write(outputMessage); + } + + /// + public void Dispose() + { + // do nothing + } + } +} diff --git a/Swan.Lite/Logging/FileLogger.cs b/Swan.Lite/Logging/FileLogger.cs new file mode 100644 index 0000000..b131360 --- /dev/null +++ b/Swan.Lite/Logging/FileLogger.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Swan.Lite.Logging; +using Swan.Threading; + +namespace Swan.Logging +{ + /// + /// A helper class to write into files the messages sent by the . + /// + /// + public class FileLogger : TextLogger, ILogger + { + private readonly ManualResetEventSlim _doneEvent = new ManualResetEventSlim(true); + private readonly ConcurrentQueue _logQueue = new ConcurrentQueue(); + private readonly ExclusiveTimer _timer; + private readonly string _filePath; + + private bool _disposedValue; // To detect redundant calls + + /// + /// Initializes a new instance of the class. + /// + public FileLogger() + : this(SwanRuntime.EntryAssemblyDirectory, true) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The filePath. + /// if set to true [daily file]. + public FileLogger(string filePath, bool dailyFile) + { + _filePath = filePath; + DailyFile = dailyFile; + + _timer = new ExclusiveTimer( + async () => await WriteLogEntries().ConfigureAwait(false), + TimeSpan.Zero, + TimeSpan.FromSeconds(5)); + } + + /// + public LogLevel LogLevel { get; set; } + + /// + /// Gets the file path. + /// + /// + /// The file path. + /// + public string FilePath => DailyFile + ? Path.Combine(Path.GetDirectoryName(_filePath), Path.GetFileNameWithoutExtension(_filePath) + $"_{DateTime.UtcNow:yyyyMMdd}" + Path.GetExtension(_filePath)) + : _filePath; + + /// + /// Gets a value indicating whether [daily file]. + /// + /// + /// true if [daily file]; otherwise, false. + /// + public bool DailyFile { get; } + + /// + public void Log(LogMessageReceivedEventArgs logEvent) + { + var (outputMessage, _) = GetOutputAndColor(logEvent); + + _logQueue.Enqueue(outputMessage); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (_disposedValue) return; + + if (disposing) + { + _timer.Pause(); + _timer.Dispose(); + + _doneEvent.Wait(); + _doneEvent.Reset(); + WriteLogEntries(true).Await(); + _doneEvent.Dispose(); + } + + _disposedValue = true; + } + + private async Task WriteLogEntries(bool finalCall = false) + { + if (_logQueue.IsEmpty) + return; + + if (!finalCall && !_doneEvent.IsSet) + return; + + _doneEvent.Reset(); + + try + { + using (var file = File.AppendText(FilePath)) + { + while (!_logQueue.IsEmpty) + { + if (_logQueue.TryDequeue(out var entry)) + await file.WriteAsync(entry).ConfigureAwait(false); + } + } + } + finally + { + if (!finalCall) + _doneEvent.Set(); + } + } + } +} diff --git a/Swan.Lite/Logging/ILogger.cs b/Swan.Lite/Logging/ILogger.cs new file mode 100644 index 0000000..4dbf6ca --- /dev/null +++ b/Swan.Lite/Logging/ILogger.cs @@ -0,0 +1,24 @@ +namespace Swan.Logging +{ + using System; + + /// + /// Interface for a logger implementation. + /// + public interface ILogger : IDisposable + { + /// + /// Gets the log level. + /// + /// + /// The log level. + /// + LogLevel LogLevel { get; } + + /// + /// Logs the specified log event. + /// + /// The instance containing the event data. + void Log(LogMessageReceivedEventArgs logEvent); + } +} diff --git a/Swan.Lite/Logging/LogLevel.cs b/Swan.Lite/Logging/LogLevel.cs new file mode 100644 index 0000000..24da6f5 --- /dev/null +++ b/Swan.Lite/Logging/LogLevel.cs @@ -0,0 +1,43 @@ +namespace Swan +{ + /// + /// Defines the log levels. + /// + public enum LogLevel + { + /// + /// The none message type + /// + None, + + /// + /// The trace message type + /// + Trace, + + /// + /// The debug message type + /// + Debug, + + /// + /// The information message type + /// + Info, + + /// + /// The warning message type + /// + Warning, + + /// + /// The error message type + /// + Error, + + /// + /// The fatal message type + /// + Fatal, + } +} \ No newline at end of file diff --git a/Swan.Lite/Logging/LogMessageReceivedEventArgs.cs b/Swan.Lite/Logging/LogMessageReceivedEventArgs.cs new file mode 100644 index 0000000..6614ae4 --- /dev/null +++ b/Swan.Lite/Logging/LogMessageReceivedEventArgs.cs @@ -0,0 +1,131 @@ +using System; + +namespace Swan +{ + /// + /// Event arguments representing the message that is logged + /// on to the terminal. Use the properties to forward the data to + /// your logger of choice. + /// + /// + public class LogMessageReceivedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The sequence. + /// Type of the message. + /// The UTC date. + /// The source. + /// The message. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public LogMessageReceivedEventArgs( + ulong sequence, + LogLevel messageType, + DateTime utcDate, + string source, + string message, + object? extendedData, + string callerMemberName, + string callerFilePath, + int callerLineNumber) + { + Sequence = sequence; + MessageType = messageType; + UtcDate = utcDate; + Source = source; + Message = message; + CallerMemberName = callerMemberName; + CallerFilePath = callerFilePath; + CallerLineNumber = callerLineNumber; + ExtendedData = extendedData; + } + + /// + /// Gets logging message sequence. + /// + /// + /// The sequence. + /// + public ulong Sequence { get; } + + /// + /// Gets the type of the message. + /// It can be a combination as the enumeration is a set of bitwise flags. + /// + /// + /// The type of the message. + /// + public LogLevel MessageType { get; } + + /// + /// Gets the UTC date at which the event at which the message was logged. + /// + /// + /// The UTC date. + /// + public DateTime UtcDate { get; } + + /// + /// Gets the name of the source where the logging message + /// came from. This can come empty if the logger did not set it. + /// + /// + /// The source. + /// + public string Source { get; } + + /// + /// Gets the body of the message. + /// + /// + /// The message. + /// + public string Message { get; } + + /// + /// Gets the name of the caller member. + /// + /// + /// The name of the caller member. + /// + public string CallerMemberName { get; } + + /// + /// Gets the caller file path. + /// + /// + /// The caller file path. + /// + public string CallerFilePath { get; } + + /// + /// Gets the caller line number. + /// + /// + /// The caller line number. + /// + public int CallerLineNumber { get; } + + /// + /// Gets an object representing extended data. + /// It could be an exception or anything else. + /// + /// + /// The extended data. + /// + public object? ExtendedData { get; } + + /// + /// Gets the Extended Data properties cast as an Exception (if possible) + /// Otherwise, it return null. + /// + /// + /// The exception. + /// + public Exception? Exception => ExtendedData as Exception; + } +} diff --git a/Swan.Lite/Logging/Logger.cs b/Swan.Lite/Logging/Logger.cs new file mode 100644 index 0000000..9a06269 --- /dev/null +++ b/Swan.Lite/Logging/Logger.cs @@ -0,0 +1,654 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Swan.Logging +{ + /// + /// Entry-point for logging. Use this static class to register/unregister + /// loggers instances. By default, the ConsoleLogger is registered. + /// + public static class Logger + { + private static readonly object SyncLock = new object(); + private static readonly List Loggers = new List(); + + private static ulong _loggingSequence; + + static Logger() + { + if (Terminal.IsConsolePresent) + Loggers.Add(ConsoleLogger.Instance); + + if (DebugLogger.IsDebuggerAttached) + Loggers.Add(DebugLogger.Instance); + } + + #region Standard Public API + + /// + /// Registers the logger. + /// + /// The type of logger to register. + /// There is already a logger with that class registered. + public static void RegisterLogger() + where T : ILogger + { + lock (SyncLock) + { + var loggerInstance = Loggers.FirstOrDefault(x => x.GetType() == typeof(T)); + + if (loggerInstance != null) + throw new InvalidOperationException("There is already a logger with that class registered."); + + Loggers.Add(Activator.CreateInstance()); + } + } + + /// + /// Registers the logger. + /// + /// The logger. + public static void RegisterLogger(ILogger logger) + { + lock (SyncLock) + Loggers.Add(logger); + } + + /// + /// Unregisters the logger. + /// + /// The logger. + /// logger. + public static void UnregisterLogger(ILogger logger) => RemoveLogger(x => x == logger); + + /// + /// Unregisters the logger. + /// + /// The type of logger to unregister. + public static void UnregisterLogger() => RemoveLogger(x => x.GetType() == typeof(T)); + + /// + /// Remove all the loggers. + /// + public static void NoLogging() + { + lock (SyncLock) + Loggers.Clear(); + } + + #region Debug + + /// + /// Logs a debug message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Debug( + this string message, + string source = null, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a debug message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Debug( + this string message, + Type source, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Debug, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a debug message to the console. + /// + /// The exception. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Debug( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Debug, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Trace + + /// + /// Logs a trace message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Trace( + this string message, + string source = null, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Trace( + this string message, + Type source, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Trace, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Trace( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Trace, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Warn + + /// + /// Logs a warning message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Warn( + this string message, + string source = null, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Warn( + this string message, + Type source, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Warning, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Warn( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Warning, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Fatal + + /// + /// Logs a warning message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Fatal( + this string message, + string source = null, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Fatal( + this string message, + Type source, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Fatal, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a warning message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Fatal( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Fatal, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Info + + /// + /// Logs an info message to the console. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Info( + this string message, + string source = null, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an info message to the console. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Info( + this string message, + Type source, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Info, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an info message to the console. + /// + /// The extended data. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Info( + this Exception extendedData, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Info, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #region Error + + /// + /// Logs an error message to the console's standard error. + /// + /// The text. + /// The source. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Error( + this string message, + string source = null, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The message. + /// The source. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Error( + this string message, + Type source, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The exception. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Error( + this Exception ex, + string source, + string message, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message, source, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + #endregion + + #region Extended Public API + + /// + /// Logs the specified message. + /// + /// The message. + /// The source. + /// Type of the message. + /// The extended data. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log( + this string message, + string source, + LogLevel messageType, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(messageType, message, source, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs the specified message. + /// + /// The message. + /// The source. + /// Type of the message. + /// The extended data. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Log( + this string message, + Type source, + LogLevel messageType, + object extendedData = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(messageType, message, source?.FullName, extendedData, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The ex. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log( + this Exception ex, + string source = null, + string message = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message ?? ex.Message, source ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs an error message to the console's standard error. + /// + /// The ex. + /// The source. + /// The message. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Log( + this Exception ex, + Type source = null, + string message = null, + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + LogMessage(LogLevel.Error, message ?? ex.Message, source?.FullName ?? ex.Source, ex, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message showing all possible non-null properties of the given object + /// This method is expensive as it uses Stringify internally. + /// + /// The object. + /// The source. + /// The title. + /// Name of the caller member. This is automatically populated. + /// The caller file path. This is automatically populated. + /// The caller line number. This is automatically populated. + public static void Dump( + this object obj, + string source, + string text = "Object Dump", + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + if (obj == null) return; + var message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; + LogMessage(LogLevel.Trace, message, source, obj, callerMemberName, callerFilePath, callerLineNumber); + } + + /// + /// Logs a trace message showing all possible non-null properties of the given object + /// This method is expensive as it uses Stringify internally. + /// + /// The object. + /// The source. + /// The text. + /// Name of the caller member. + /// The caller file path. + /// The caller line number. + public static void Dump( + this object obj, + Type source, + string text = "Object Dump", + [CallerMemberName] string callerMemberName = "", + [CallerFilePath] string callerFilePath = "", + [CallerLineNumber] int callerLineNumber = 0) + { + if (obj == null) return; + var message = $"{text} ({obj.GetType()}): {Environment.NewLine}{obj.Stringify().Indent(5)}"; + LogMessage(LogLevel.Trace, message, source?.FullName, obj, callerMemberName, callerFilePath, callerLineNumber); + } + + #endregion + + private static void RemoveLogger(Func criteria) + { + lock (SyncLock) + { + var loggerInstance = Loggers.FirstOrDefault(criteria); + + if (loggerInstance == null) + throw new InvalidOperationException("The logger is not registered."); + + loggerInstance.Dispose(); + + Loggers.Remove(loggerInstance); + } + } + + private static void LogMessage( + LogLevel logLevel, + string message, + string sourceName, + object extendedData, + string callerMemberName, + string callerFilePath, + int callerLineNumber) + { + var sequence = _loggingSequence; + var date = DateTime.UtcNow; + _loggingSequence++; + + var loggerMessage = string.IsNullOrWhiteSpace(message) ? + string.Empty : message.RemoveControlCharsExcept('\n'); + + var eventArgs = new LogMessageReceivedEventArgs( + sequence, + logLevel, + date, + sourceName, + loggerMessage, + extendedData, + callerMemberName, + callerFilePath, + callerLineNumber); + + foreach (var logger in Loggers) + { + Task.Run(() => + { + if (logger.LogLevel <= logLevel) + logger.Log(eventArgs); + }); + } + } + } +} diff --git a/Swan.Lite/Logging/TextLogger.cs b/Swan.Lite/Logging/TextLogger.cs new file mode 100644 index 0000000..b087592 --- /dev/null +++ b/Swan.Lite/Logging/TextLogger.cs @@ -0,0 +1,89 @@ +using Swan.Logging; +using System; + +namespace Swan.Lite.Logging +{ + /// + /// Use this class for text-based logger. + /// + public abstract class TextLogger + { + /// + /// Gets or sets the logging time format. + /// set to null or empty to prevent output. + /// + /// + /// The logging time format. + /// + public static string LoggingTimeFormat { get; set; } = "HH:mm:ss.fff"; + + /// + /// Gets the color of the output of the message (the output message has a new line char in the end). + /// + /// The instance containing the event data. + /// + /// The output message formatted and the color of the console to be used. + /// + protected (string outputMessage, ConsoleColor color) GetOutputAndColor(LogMessageReceivedEventArgs logEvent) + { + var (prefix , color) = GetConsoleColorAndPrefix(logEvent.MessageType); + + var loggerMessage = string.IsNullOrWhiteSpace(logEvent.Message) + ? string.Empty + : logEvent.Message.RemoveControlCharsExcept('\n'); + + var outputMessage = CreateOutputMessage(logEvent.Source, loggerMessage, prefix, logEvent.UtcDate); + + // Further format the output in the case there is an exception being logged + if (logEvent.MessageType == LogLevel.Error && logEvent.Exception != null) + { + try + { + outputMessage += $"{logEvent.Exception.Stringify().Indent()}{Environment.NewLine}"; + } + catch + { + // Ignore + } + } + + return (outputMessage, color); + } + + private static (string Prefix, ConsoleColor color) GetConsoleColorAndPrefix(LogLevel messageType) + { + switch (messageType) + { + case LogLevel.Debug: + return (ConsoleLogger.DebugPrefix, ConsoleLogger.DebugColor); + case LogLevel.Error: + return (ConsoleLogger.ErrorPrefix, ConsoleLogger.ErrorColor); + case LogLevel.Info: + return (ConsoleLogger.InfoPrefix, ConsoleLogger.InfoColor); + case LogLevel.Trace: + return (ConsoleLogger.TracePrefix, ConsoleLogger.TraceColor); + case LogLevel.Warning: + return (ConsoleLogger.WarnPrefix, ConsoleLogger.WarnColor); + case LogLevel.Fatal: + return (ConsoleLogger.FatalPrefix, ConsoleLogger.FatalColor); + default: + return (new string(' ', ConsoleLogger.InfoPrefix.Length), Terminal.Settings.DefaultColor); + } + } + + private static string CreateOutputMessage(string sourceName, string loggerMessage, string prefix, DateTime date) + { + var friendlySourceName = string.IsNullOrWhiteSpace(sourceName) + ? string.Empty + : sourceName.SliceLength(sourceName.LastIndexOf('.') + 1, sourceName.Length); + + var outputMessage = string.IsNullOrWhiteSpace(sourceName) + ? loggerMessage + : $"[{friendlySourceName}] {loggerMessage}"; + + return string.IsNullOrWhiteSpace(LoggingTimeFormat) + ? $" {prefix} >> {outputMessage}{Environment.NewLine}" + : $" {date.ToLocalTime().ToString(LoggingTimeFormat)} {prefix} >> {outputMessage}{Environment.NewLine}"; + } + } +} diff --git a/Swan.Lite/Mappers/CopyableAttribute.cs b/Swan.Lite/Mappers/CopyableAttribute.cs new file mode 100644 index 0000000..4f04954 --- /dev/null +++ b/Swan.Lite/Mappers/CopyableAttribute.cs @@ -0,0 +1,13 @@ +using System; + +namespace Swan.Mappers +{ + /// + /// Represents an attribute to select which properties are copyable between objects. + /// + /// + [AttributeUsage(AttributeTargets.Property)] + public class CopyableAttribute : Attribute + { + } +} diff --git a/Swan.Lite/Mappers/IObjectMap.cs b/Swan.Lite/Mappers/IObjectMap.cs new file mode 100644 index 0000000..77a3c1b --- /dev/null +++ b/Swan.Lite/Mappers/IObjectMap.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Swan.Mappers +{ + /// + /// Interface object map. + /// + public interface IObjectMap + { + /// + /// Gets or sets the map. + /// + Dictionary> Map { get; } + + /// + /// Gets or sets the type of the source. + /// + Type SourceType { get; } + + /// + /// Gets or sets the type of the destination. + /// + Type DestinationType { get; } + } +} diff --git a/Swan.Lite/Mappers/ObjectMap.cs b/Swan.Lite/Mappers/ObjectMap.cs new file mode 100644 index 0000000..edc59b0 --- /dev/null +++ b/Swan.Lite/Mappers/ObjectMap.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; + +namespace Swan.Mappers +{ + /// + /// Represents an object map. + /// + /// The type of the source. + /// The type of the destination. + /// + public class ObjectMap : IObjectMap + { + internal ObjectMap(IEnumerable intersect) + { + SourceType = typeof(TSource); + DestinationType = typeof(TDestination); + Map = intersect.ToDictionary( + property => DestinationType.GetProperty(property.Name), + property => new List {SourceType.GetProperty(property.Name)}); + } + + /// + public Dictionary> Map { get; } + + /// + public Type SourceType { get; } + + /// + public Type DestinationType { get; } + + /// + /// Maps the property. + /// + /// The type of the destination property. + /// The type of the source property. + /// The destination property. + /// The source property. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + public ObjectMap MapProperty + ( + Expression> destinationProperty, + Expression> sourceProperty) + { + var propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; + + if (propertyDestinationInfo == null) + { + throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); + } + + var sourceMembers = GetSourceMembers(sourceProperty); + + if (sourceMembers.Any() == false) + { + throw new ArgumentException("Invalid source expression", nameof(sourceProperty)); + } + + // reverse order + sourceMembers.Reverse(); + Map[propertyDestinationInfo] = sourceMembers; + + return this; + } + + /// + /// Removes the map property. + /// + /// The type of the destination property. + /// The destination property. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + /// Invalid destination expression. + public ObjectMap RemoveMapProperty( + Expression> destinationProperty) + { + var propertyDestinationInfo = (destinationProperty.Body as MemberExpression)?.Member as PropertyInfo; + + if (propertyDestinationInfo == null) + throw new ArgumentException("Invalid destination expression", nameof(destinationProperty)); + + if (Map.ContainsKey(propertyDestinationInfo)) + { + Map.Remove(propertyDestinationInfo); + } + + return this; + } + + private static List GetSourceMembers(Expression> sourceProperty) + { + var sourceMembers = new List(); + var initialExpression = sourceProperty.Body as MemberExpression; + + while (true) + { + var propertySourceInfo = initialExpression?.Member as PropertyInfo; + + if (propertySourceInfo == null) break; + sourceMembers.Add(propertySourceInfo); + initialExpression = initialExpression.Expression as MemberExpression; + } + + return sourceMembers; + } + } +} diff --git a/Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs b/Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs new file mode 100644 index 0000000..bd5f1a7 --- /dev/null +++ b/Swan.Lite/Mappers/ObjectMapper.PropertyInfoComparer.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Reflection; + +namespace Swan.Mappers +{ + /// + /// Represents an AutoMapper-like object to map from one object type + /// to another using defined properties map or using the default behaviour + /// to copy same named properties from one object to another. + /// + /// The extension methods like CopyPropertiesTo use the default behaviour. + /// + public partial class ObjectMapper + { + internal class PropertyInfoComparer : IEqualityComparer + { + public bool Equals(PropertyInfo x, PropertyInfo y) + => x != null && y != null && x.Name == y.Name && x.PropertyType == y.PropertyType; + + public int GetHashCode(PropertyInfo obj) + => obj.Name.GetHashCode() + obj.PropertyType.Name.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Mappers/ObjectMapper.cs b/Swan.Lite/Mappers/ObjectMapper.cs new file mode 100644 index 0000000..a2c3ce7 --- /dev/null +++ b/Swan.Lite/Mappers/ObjectMapper.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Mappers +{ + /// + /// Represents an AutoMapper-like object to map from one object type + /// to another using defined properties map or using the default behaviour + /// to copy same named properties from one object to another. + /// + /// The extension methods like CopyPropertiesTo use the default behaviour. + /// + /// + /// The following code explains how to map an object's properties into an instance of type T. + /// + /// using Swan.Mappers; + /// + /// class Example + /// { + /// class Person + /// { + /// public string Name { get; set; } + /// public int Age { get; set; } + /// } + /// + /// static void Main() + /// { + /// var obj = new { Name = "John", Age = 42 }; + /// + /// var person = Runtime.ObjectMapper.Map<Person>(obj); + /// } + /// } + /// + /// + /// The following code explains how to explicitly map certain properties. + /// + /// using Swan.Mappers; + /// + /// class Example + /// { + /// class User + /// { + /// public string Name { get; set; } + /// public Role Role { get; set; } + /// } + /// + /// public class Role + /// { + /// public string Name { get; set; } + /// } + /// + /// class UserDto + /// { + /// public string Name { get; set; } + /// public string Role { get; set; } + /// } + /// + /// static void Main() + /// { + /// // create a User object + /// var person = + /// new User { Name = "Phillip", Role = new Role { Name = "Admin" } }; + /// + /// // create an Object Mapper + /// var mapper = new ObjectMapper(); + /// + /// // map the User's Role.Name to UserDto's Role + /// mapper.CreateMap<User, UserDto>() + /// .MapProperty(d => d.Role, x => x.Role.Name); + /// + /// // apply the previous map and retrieve a UserDto object + /// var destination = mapper.Map<UserDto>(person); + /// } + /// } + /// + /// + public partial class ObjectMapper + { + private static readonly Lazy LazyInstance = new Lazy(() => new ObjectMapper()); + + private readonly List _maps = new List(); + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ObjectMapper Current => LazyInstance.Value; + + /// + /// Copies the specified source. + /// + /// The source. + /// The target. + /// The properties to copy. + /// The ignore properties. + /// + /// Copied properties count. + /// + /// + /// source + /// or + /// target. + /// + public static int Copy( + object source, + object target, + IEnumerable? propertiesToCopy = null, + params string[]? ignoreProperties) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (target == null) + throw new ArgumentNullException(nameof(target)); + + return CopyInternal( + target, + GetSourceMap(source), + propertiesToCopy, + ignoreProperties); + } + + /// + /// Copies the specified source. + /// + /// The source. + /// The target. + /// The properties to copy. + /// The ignore properties. + /// + /// Copied properties count. + /// + /// + /// source + /// or + /// target. + /// + public static int Copy( + IDictionary source, + object target, + IEnumerable? propertiesToCopy = null, + params string[] ignoreProperties) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (target == null) + throw new ArgumentNullException(nameof(target)); + + return CopyInternal( + target, + source.ToDictionary( + x => x.Key.ToLowerInvariant(), + x => Tuple.Create(typeof(object), x.Value)), + propertiesToCopy, + ignoreProperties); + } + + /// + /// Creates the map. + /// + /// The type of the source. + /// The type of the destination. + /// + /// An object map representation of type of the destination property + /// and type of the source property. + /// + /// + /// You can't create an existing map + /// or + /// Types doesn't match. + /// + public ObjectMap CreateMap() + { + if (_maps.Any(x => x.SourceType == typeof(TSource) && x.DestinationType == typeof(TDestination))) + throw new InvalidOperationException("You can't create an existing map"); + + var sourceType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); + var destinationType = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true); + + var intersect = sourceType.Intersect(destinationType, new PropertyInfoComparer()).ToArray(); + + if (!intersect.Any()) + throw new InvalidOperationException("Types doesn't match"); + + var map = new ObjectMap(intersect); + + _maps.Add(map); + + return map; + } + + /// + /// Maps the specified source. + /// + /// The type of the destination. + /// The source. + /// if set to true [automatic resolve]. + /// + /// A new instance of the map. + /// + /// source. + /// You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}. + public TDestination Map(object source, bool autoResolve = true) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + var destination = Activator.CreateInstance(); + var map = _maps + .FirstOrDefault(x => x.SourceType == source.GetType() && x.DestinationType == typeof(TDestination)); + + if (map != null) + { + foreach (var property in map.Map) + { + var finalSource = property.Value.Aggregate(source, + (current, sourceProperty) => sourceProperty.GetValue(current)); + + property.Key.SetValue(destination, finalSource); + } + } + else + { + if (!autoResolve) + { + throw new InvalidOperationException( + $"You can't map from type {source.GetType().Name} to {typeof(TDestination).Name}"); + } + + // Missing mapping, try to use default behavior + Copy(source, destination); + } + + return destination; + } + + private static int CopyInternal( + object target, + Dictionary> sourceProperties, + IEnumerable? propertiesToCopy, + IEnumerable? ignoreProperties) + { + // Filter properties + var requiredProperties = propertiesToCopy? + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.ToLowerInvariant()); + + var ignoredProperties = ignoreProperties? + .Where(p => !string.IsNullOrWhiteSpace(p)) + .Select(p => p.ToLowerInvariant()); + + var properties = PropertyTypeCache.DefaultCache.Value + .RetrieveFilteredProperties(target.GetType(), true, x => x.CanWrite); + + return properties + .Select(x => x.Name) + .Distinct() + .ToDictionary(x => x.ToLowerInvariant(), x => properties.First(y => y.Name == x)) + .Where(x => sourceProperties.Keys.Contains(x.Key)) + .When(() => requiredProperties != null, q => q.Where(y => requiredProperties.Contains(y.Key))) + .When(() => ignoredProperties != null, q => q.Where(y => !ignoredProperties.Contains(y.Key))) + .ToDictionary(x => x.Value, x => sourceProperties[x.Key]) + .Sum(x => TrySetValue(x.Key, x.Value, target) ? 1 : 0); + } + + private static bool TrySetValue(PropertyInfo propertyInfo, Tuple property, object target) + { + try + { + var (type, value) = property; + + if (type.IsEnum) + { + propertyInfo.SetValue(target, + Enum.ToObject(propertyInfo.PropertyType, value)); + + return true; + } + + if (type.IsValueType || propertyInfo.PropertyType != type) + return propertyInfo.TrySetBasicType(value, target); + + if (propertyInfo.PropertyType.IsArray) + { + propertyInfo.TrySetArray(value as IEnumerable, target); + return true; + } + + propertyInfo.SetValue(target, GetValue(value, propertyInfo.PropertyType)); + + return true; + } + catch + { + // swallow + } + + return false; + } + + private static object? GetValue(object source, Type targetType) + { + if (source == null) + return null; + + object? target = null; + + source.CreateTarget(targetType, false, ref target); + + switch (source) + { + case string _: + target = source; + break; + case IList sourceList when target is IList targetList: + var addMethod = targetType.GetMethods() + .FirstOrDefault( + m => m.Name == Formatters.Json.AddMethodName && m.IsPublic && m.GetParameters().Length == 1); + + if (addMethod == null) return target; + + var isItemValueType = targetList.GetType().GetElementType().IsValueType; + + foreach (var item in sourceList) + { + try + { + targetList.Add(isItemValueType + ? item + : item.CopyPropertiesToNew()); + } + catch + { + // ignored + } + } + + break; + default: + source.CopyPropertiesTo(target); + break; + } + + return target; + } + + private static Dictionary> GetSourceMap(object source) + { + // select distinct properties because they can be duplicated by inheritance + var sourceProperties = PropertyTypeCache.DefaultCache.Value + .RetrieveFilteredProperties(source.GetType(), true, x => x.CanRead) + .ToArray(); + + return sourceProperties + .Select(x => x.Name) + .Distinct() + .ToDictionary( + x => x.ToLowerInvariant(), + x => Tuple.Create(sourceProperties.First(y => y.Name == x).PropertyType, + sourceProperties.First(y => y.Name == x).GetValue(source))); + } + } +} diff --git a/Swan.Lite/ObjectComparer.cs b/Swan.Lite/ObjectComparer.cs new file mode 100644 index 0000000..a00a3f3 --- /dev/null +++ b/Swan.Lite/ObjectComparer.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan +{ + /// + /// Represents a quick object comparer using the public properties of an object + /// or the public members in a structure. + /// + public static class ObjectComparer + { + /// + /// Compare if two variables of the same type are equal. + /// + /// The type of objects to compare. + /// The left. + /// The right. + /// true if the variables are equal; otherwise, false. + public static bool AreEqual(T left, T right) => AreEqual(left, right, typeof(T)); + + /// + /// Compare if two variables of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// + /// true if the variables are equal; otherwise, false. + /// + /// targetType. + public static bool AreEqual(object left, object right, Type targetType) + { + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + if (Definitions.BasicTypesInfo.Value.ContainsKey(targetType)) + return Equals(left, right); + + return targetType.IsValueType || targetType.IsArray + ? AreStructsEqual(left, right, targetType) + : AreObjectsEqual(left, right, targetType); + } + + /// + /// Compare if two objects of the same type are equal. + /// + /// The type of objects to compare. + /// The left. + /// The right. + /// true if the objects are equal; otherwise, false. + public static bool AreObjectsEqual(T left, T right) + where T : class + { + return AreObjectsEqual(left, right, typeof(T)); + } + + /// + /// Compare if two objects of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// true if the objects are equal; otherwise, false. + /// targetType. + public static bool AreObjectsEqual(object left, object right, Type targetType) + { + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + var properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType).ToArray(); + + foreach (var propertyTarget in properties) + { + var targetPropertyGetMethod = propertyTarget.GetCacheGetMethod(); + + if (propertyTarget.PropertyType.IsArray) + { + var leftObj = targetPropertyGetMethod(left) as IEnumerable; + var rightObj = targetPropertyGetMethod(right) as IEnumerable; + + if (!AreEnumerationsEquals(leftObj, rightObj)) + return false; + } + else + { + if (!Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right))) + return false; + } + } + + return true; + } + + /// + /// Compare if two structures of the same type are equal. + /// + /// The type of structs to compare. + /// The left. + /// The right. + /// true if the structs are equal; otherwise, false. + public static bool AreStructsEqual(T left, T right) + where T : struct + { + return AreStructsEqual(left, right, typeof(T)); + } + + /// + /// Compare if two structures of the same type are equal. + /// + /// The left. + /// The right. + /// Type of the target. + /// + /// true if the structs are equal; otherwise, false. + /// + /// targetType. + public static bool AreStructsEqual(object left, object right, Type targetType) + { + if (targetType == null) + throw new ArgumentNullException(nameof(targetType)); + + var fields = new List(FieldTypeCache.DefaultCache.Value.RetrieveAllFields(targetType)) + .Union(PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(targetType)); + + foreach (var targetMember in fields) + { + switch (targetMember) + { + case FieldInfo field: + if (Equals(field.GetValue(left), field.GetValue(right)) == false) + return false; + break; + case PropertyInfo property: + var targetPropertyGetMethod = property.GetCacheGetMethod(); + + if (targetPropertyGetMethod != null && + !Equals(targetPropertyGetMethod(left), targetPropertyGetMethod(right))) + return false; + break; + } + } + + return true; + } + + /// + /// Compare if two enumerables are equal. + /// + /// The type of enums to compare. + /// The left. + /// The right. + /// + /// true if two specified types are equal; otherwise, false. + /// + /// + /// left + /// or + /// right. + /// + public static bool AreEnumerationsEquals(T left, T right) + where T : IEnumerable + { + if (Equals(left, default(T))) + throw new ArgumentNullException(nameof(left)); + + if (Equals(right, default(T))) + throw new ArgumentNullException(nameof(right)); + + var leftEnumerable = left.Cast().ToArray(); + var rightEnumerable = right.Cast().ToArray(); + + if (leftEnumerable.Length != rightEnumerable.Length) + return false; + + for (var i = 0; i < leftEnumerable.Length; i++) + { + var leftEl = leftEnumerable[i]; + var rightEl = rightEnumerable[i]; + + if (!AreEqual(leftEl, rightEl, leftEl.GetType())) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Paginator.cs b/Swan.Lite/Paginator.cs new file mode 100644 index 0000000..7be3e43 --- /dev/null +++ b/Swan.Lite/Paginator.cs @@ -0,0 +1,99 @@ +using System; + +namespace Swan +{ + /// + /// A utility class to compute paging or batching offsets. + /// + public class Paginator + { + /// + /// Initializes a new instance of the class. + /// + /// The total count of items to page over. + /// The desired size of individual pages. + public Paginator(int totalCount, int pageSize) + { + TotalCount = totalCount; + PageSize = pageSize; + PageCount = ComputePageCount(); + } + + /// + /// Gets the desired number of items per page. + /// + public int PageSize { get; } + + /// + /// Gets the total number of items to page over. + /// + public int TotalCount { get; } + + /// + /// Gets the computed number of pages. + /// + public int PageCount { get; } + + /// + /// Gets the start item index of the given page. + /// + /// Zero-based index of the page. + /// The start item index. + public int GetFirstItemIndex(int pageIndex) + { + pageIndex = FixPageIndex(pageIndex); + return pageIndex * PageSize; + } + + /// + /// Gets the end item index of the given page. + /// + /// Zero-based index of the page. + /// The end item index. + public int GetLastItemIndex(int pageIndex) + { + var startIndex = GetFirstItemIndex(pageIndex); + return Math.Min(startIndex + PageSize - 1, TotalCount - 1); + } + + /// + /// Gets the item count of the given page index. + /// + /// Zero-based index of the page. + /// The number of items that the page contains. + public int GetItemCount(int pageIndex) + { + pageIndex = FixPageIndex(pageIndex); + return (pageIndex >= PageCount - 1) + ? GetLastItemIndex(pageIndex) - GetFirstItemIndex(pageIndex) + 1 + : PageSize; + } + + /// + /// Fixes the index of the page by applying bound logic. + /// + /// Index of the page. + /// A limit-bound index. + private int FixPageIndex(int pageIndex) + { + if (pageIndex < 0) return 0; + + return pageIndex >= PageCount ? PageCount - 1 : pageIndex; + } + + /// + /// Computes the number of pages for the paginator. + /// + /// The page count. + private int ComputePageCount() + { + // include this if when you always want at least 1 page + if (TotalCount == 0) + return 0; + + return TotalCount % PageSize != 0 + ? (TotalCount / PageSize) + 1 + : TotalCount / PageSize; + } + } +} diff --git a/Swan.Lite/Parsers/ArgumentOptionAttribute.cs b/Swan.Lite/Parsers/ArgumentOptionAttribute.cs new file mode 100644 index 0000000..1353243 --- /dev/null +++ b/Swan.Lite/Parsers/ArgumentOptionAttribute.cs @@ -0,0 +1,102 @@ +using System; + +namespace Swan.Parsers +{ + /// + /// Models an option specification. + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class ArgumentOptionAttribute + : Attribute + { + /// + /// Initializes a new instance of the class. + /// The default long name will be inferred from target property. + /// + public ArgumentOptionAttribute() + : this(string.Empty, string.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The long name of the option. + public ArgumentOptionAttribute(string longName) + : this(string.Empty, longName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The short name of the option. + /// The long name of the option or null if not used. + public ArgumentOptionAttribute(char shortName, string longName) + : this(new string(shortName, 1), longName) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The short name of the option.. + public ArgumentOptionAttribute(char shortName) + : this(new string(shortName, 1), string.Empty) + { + } + + private ArgumentOptionAttribute(string shortName, string longName) + { + ShortName = shortName ?? throw new ArgumentNullException(nameof(shortName)); + LongName = longName ?? throw new ArgumentNullException(nameof(longName)); + } + + /// + /// Gets long name of this command line option. This name is usually a single English word. + /// + /// + /// The long name. + /// + public string LongName { get; } + + /// + /// Gets a short name of this command line option, made of one character. + /// + /// + /// The short name. + /// + public string ShortName { get; } + + /// + /// When applying attribute to target properties, + /// it allows you to split an argument and consume its content as a sequence. + /// + public char Separator { get; set; } = '\0'; + + /// + /// Gets or sets mapped property default value. + /// + /// + /// The default value. + /// + public object DefaultValue { get; set; } + + /// + /// Gets or sets a value indicating whether a command line option is required. + /// + /// + /// true if required; otherwise, false. + /// + public bool Required { get; set; } + + /// + /// Gets or sets a short description of this command line option. Usually a sentence summary. + /// + /// + /// The help text. + /// + public string HelpText { get; set; } + } +} \ No newline at end of file diff --git a/Swan.Lite/Parsers/ArgumentParse.Validator.cs b/Swan.Lite/Parsers/ArgumentParse.Validator.cs new file mode 100644 index 0000000..655ddba --- /dev/null +++ b/Swan.Lite/Parsers/ArgumentParse.Validator.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Parsers +{ + /// + /// Provides methods to parse command line arguments. + /// + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors). + /// + public partial class ArgumentParser + { + private sealed class Validator + { + private readonly object _instance; + private readonly IEnumerable _args; + private readonly List _updatedList = new List(); + private readonly ArgumentParserSettings _settings; + + private readonly PropertyInfo[] _properties; + + public Validator( + PropertyInfo[] properties, + IEnumerable args, + object instance, + ArgumentParserSettings settings) + { + _args = args; + _instance = instance; + _settings = settings; + _properties = properties; + + PopulateInstance(); + SetDefaultValues(); + GetRequiredList(); + } + + public List UnknownList { get; } = new List(); + public List RequiredList { get; } = new List(); + + public bool IsValid() => (_settings.IgnoreUnknownArguments || !UnknownList.Any()) && !RequiredList.Any(); + + public IEnumerable GetPropertiesOptions() + => _properties.Select(p => AttributeCache.DefaultCache.Value.RetrieveOne(p)) + .Where(x => x != null); + + private void GetRequiredList() + { + foreach (var targetProperty in _properties) + { + var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); + + if (optionAttr == null || optionAttr.Required == false) + continue; + + if (targetProperty.GetValue(_instance) == null) + { + RequiredList.Add(optionAttr.LongName ?? optionAttr.ShortName); + } + } + } + + private void SetDefaultValues() + { + foreach (var targetProperty in _properties.Except(_updatedList)) + { + var optionAttr = AttributeCache.DefaultCache.Value.RetrieveOne(targetProperty); + + var defaultValue = optionAttr?.DefaultValue; + + if (defaultValue == null) + continue; + + if (SetPropertyValue(targetProperty, defaultValue.ToString(), _instance, optionAttr)) + _updatedList.Add(targetProperty); + } + } + + private void PopulateInstance() + { + const char dash = '-'; + var propertyName = string.Empty; + + foreach (var arg in _args) + { + var ignoreSetValue = string.IsNullOrWhiteSpace(propertyName); + + if (ignoreSetValue) + { + if (string.IsNullOrWhiteSpace(arg) || arg[0] != dash) continue; + + propertyName = arg.Substring(1); + + if (!string.IsNullOrWhiteSpace(propertyName) && propertyName[0] == dash) + propertyName = propertyName.Substring(1); + } + + var targetProperty = TryGetProperty(propertyName); + + if (targetProperty == null) + { + // Skip if the property is not found + UnknownList.Add(propertyName); + continue; + } + + if (!ignoreSetValue && SetPropertyValue(targetProperty, arg, _instance)) + { + _updatedList.Add(targetProperty); + propertyName = string.Empty; + } + else if (targetProperty.PropertyType == typeof(bool)) + { + // If the arg is a boolean property set it to true. + targetProperty.SetValue(_instance, true); + + _updatedList.Add(targetProperty); + propertyName = string.Empty; + } + } + + if (!string.IsNullOrEmpty(propertyName)) + { + UnknownList.Add(propertyName); + } + } + + private bool SetPropertyValue( + PropertyInfo targetProperty, + string propertyValueString, + object result, + ArgumentOptionAttribute optionAttr = null) + { + if (!targetProperty.PropertyType.IsEnum) + { + return targetProperty.PropertyType.IsArray + ? targetProperty.TrySetArray(propertyValueString.Split(optionAttr?.Separator ?? ','), result) + : targetProperty.TrySetBasicType(propertyValueString, result); + } + + var parsedValue = Enum.Parse( + targetProperty.PropertyType, + propertyValueString, + _settings.CaseInsensitiveEnumValues); + + targetProperty.SetValue(result, Enum.ToObject(targetProperty.PropertyType, parsedValue)); + + return true; + } + + private PropertyInfo TryGetProperty(string propertyName) + => _properties.FirstOrDefault(p => + string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.LongName, propertyName, _settings.NameComparer) || + string.Equals(AttributeCache.DefaultCache.Value.RetrieveOne(p)?.ShortName, propertyName, _settings.NameComparer)); + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs b/Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs new file mode 100644 index 0000000..3241669 --- /dev/null +++ b/Swan.Lite/Parsers/ArgumentParser.TypeResolver.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Reflection; +using Swan.Reflection; + +namespace Swan.Parsers +{ + /// + /// Provides methods to parse command line arguments. + /// + public partial class ArgumentParser + { + private sealed class TypeResolver + { + private readonly string _selectedVerb; + + private PropertyInfo[]? _properties; + + public TypeResolver(string selectedVerb) + { + _selectedVerb = selectedVerb; + } + + public PropertyInfo[]? Properties => _properties?.Any() == true ? _properties : null; + + public object? GetOptionsObject(T instance) + { + _properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(true).ToArray(); + + if (!_properties.Any(x => x.GetCustomAttributes(typeof(VerbOptionAttribute), false).Any())) + return instance; + + var selectedVerb = string.IsNullOrWhiteSpace(_selectedVerb) + ? null + : _properties.FirstOrDefault(x => + AttributeCache.DefaultCache.Value.RetrieveOne(x).Name == _selectedVerb); + + if (selectedVerb == null) return null; + + var type = instance.GetType(); + + var verbProperty = type.GetProperty(selectedVerb.Name); + + if (verbProperty?.GetValue(instance) == null) + { + var propertyInstance = Activator.CreateInstance(selectedVerb.PropertyType); + verbProperty?.SetValue(instance, propertyInstance); + } + + _properties = PropertyTypeCache.DefaultCache.Value.RetrieveAllProperties(selectedVerb.PropertyType, true) + .ToArray(); + + return verbProperty?.GetValue(instance); + } + } + } +} diff --git a/Swan.Lite/Parsers/ArgumentParser.cs b/Swan.Lite/Parsers/ArgumentParser.cs new file mode 100644 index 0000000..9ee6c86 --- /dev/null +++ b/Swan.Lite/Parsers/ArgumentParser.cs @@ -0,0 +1,253 @@ +using Swan.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Parsers +{ + /// + /// Provides methods to parse command line arguments. + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + /// + /// The following example shows how to parse CLI arguments into objects. + /// + /// class Example + /// { + /// using System; + /// using Swan.Parsers; + /// + /// static void Main(string[] args) + /// { + /// // parse the supplied command-line arguments into the options object + /// var res = Runtime.ArgumentParser.ParseArguments(args, out var options); + /// } + /// + /// class Options + /// { + /// [ArgumentOption('v', "verbose", HelpText = "Set verbose mode.")] + /// public bool Verbose { get; set; } + /// + /// [ArgumentOption('u', Required = true, HelpText = "Set user name.")] + /// public string Username { get; set; } + /// + /// [ArgumentOption('n', "names", Separator = ',', + /// Required = true, HelpText = "A list of files separated by a comma")] + /// public string[] Files { get; set; } + /// + /// [ArgumentOption('p', "port", DefaultValue = 22, HelpText = "Set port.")] + /// public int Port { get; set; } + /// + /// [ArgumentOption("color", DefaultValue = ConsoleColor.Red, + /// HelpText = "Set a color.")] + /// public ConsoleColor Color { get; set; } + /// } + /// } + /// + /// The following code describes how to parse CLI verbs. + /// + /// class Example2 + /// { + /// using Swan; + /// using Swan.Parsers; + /// + /// static void Main(string[] args) + /// { + /// // create an instance of the VerbOptions class + /// var options = new VerbOptions(); + /// + /// // parse the supplied command-line arguments into the options object + /// var res = Runtime.ArgumentParser.ParseArguments(args, options); + /// + /// // if there were no errors parsing + /// if (res) + /// { + /// if(options.Run != null) + /// { + /// // run verb was selected + /// } + /// + /// if(options.Print != null) + /// { + /// // print verb was selected + /// } + /// } + /// + /// // flush all error messages + /// Terminal.Flush(); + /// } + /// + /// class VerbOptions + /// { + /// [VerbOption("run", HelpText = "Run verb.")] + /// public RunVerbOption Run { get; set; } + /// + /// [VerbOption("print", HelpText = "Print verb.")] + /// public PrintVerbOption Print { get; set; } + /// } + /// + /// class RunVerbOption + /// { + /// [ArgumentOption('o', "outdir", HelpText = "Output directory", + /// DefaultValue = "", Required = false)] + /// public string OutDir { get; set; } + /// } + /// + /// class PrintVerbOption + /// { + /// [ArgumentOption('t', "text", HelpText = "Text to print", + /// DefaultValue = "", Required = false)] + /// public string Text { get; set; } + /// } + /// } + /// + /// + public partial class ArgumentParser + { + /// + /// Initializes a new instance of the class. + /// + public ArgumentParser() + : this(new ArgumentParserSettings()) + { + } + + /// + /// Initializes a new instance of the class, + /// configurable with using a delegate. + /// + /// The parse settings. + public ArgumentParser(ArgumentParserSettings parseSettings) + { + Settings = parseSettings ?? throw new ArgumentNullException(nameof(parseSettings)); + } + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ArgumentParser Current { get; } = new ArgumentParser(); + + /// + /// Gets the instance that implements in use. + /// + /// + /// The settings. + /// + public ArgumentParserSettings Settings { get; } + + /// + /// Parses a string array of command line arguments constructing values in an instance of type . + /// + /// The type of the options. + /// The arguments. + /// The instance. + /// + /// true if was converted successfully; otherwise, false. + /// + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) + /// is passed to a method that does not accept it as a valid argument. + /// + /// + /// The exception that is thrown when a method call is invalid for the object's current state. + /// + public bool ParseArguments(IEnumerable args, out T instance) + { + instance = Activator.CreateInstance(); + return ParseArguments(args, instance); + } + + /// + /// Parses a string array of command line arguments constructing values in an instance of type . + /// + /// The type of the options. + /// The arguments. + /// The instance. + /// + /// true if was converted successfully; otherwise, false. + /// + /// + /// The exception that is thrown when a null reference (Nothing in Visual Basic) + /// is passed to a method that does not accept it as a valid argument. + /// + /// + /// The exception that is thrown when a method call is invalid for the object's current state. + /// + public bool ParseArguments(IEnumerable args, T instance) + { + if (args == null) + throw new ArgumentNullException(nameof(args)); + + if (Equals(instance, default(T))) + throw new ArgumentNullException(nameof(instance)); + + var typeResolver = new TypeResolver(args.FirstOrDefault()); + var options = typeResolver.GetOptionsObject(instance); + + if (options == null) + { + ReportUnknownVerb(); + return false; + } + + if (typeResolver.Properties == null) + throw new InvalidOperationException($"Type {typeof(T).Name} is not valid"); + + var validator = new Validator(typeResolver.Properties, args, options, Settings); + + if (validator.IsValid()) + return true; + + ReportIssues(validator); + return false; + } + + private static void ReportUnknownVerb() + { + Terminal.WriteLine("No verb was specified", ConsoleColor.Red); + Terminal.WriteLine("Valid verbs:", ConsoleColor.Cyan); + + PropertyTypeCache.DefaultCache.Value + .RetrieveAllProperties(true) + .Select(x => AttributeCache.DefaultCache.Value.RetrieveOne(x)) + .Where(x => x != null) + .ToList() + .ForEach(x => Terminal.WriteLine(x.ToString(), ConsoleColor.Cyan)); + } + + private void ReportIssues(Validator validator) + { + if (Settings.WriteBanner) + Terminal.WriteWelcomeBanner(); + + var options = validator.GetPropertiesOptions(); + + foreach (var option in options) + { + Terminal.WriteLine(string.Empty); + + // TODO: If Enum list values + var shortName = string.IsNullOrWhiteSpace(option.ShortName) ? string.Empty : $"-{option.ShortName}"; + var longName = string.IsNullOrWhiteSpace(option.LongName) ? string.Empty : $"--{option.LongName}"; + var comma = string.IsNullOrWhiteSpace(shortName) || string.IsNullOrWhiteSpace(longName) + ? string.Empty + : ", "; + var defaultValue = option.DefaultValue == null ? string.Empty : $"(Default: {option.DefaultValue}) "; + + Terminal.WriteLine($" {shortName}{comma}{longName}\t\t{defaultValue}{option.HelpText}", ConsoleColor.Cyan); + } + + Terminal.WriteLine(string.Empty); + Terminal.WriteLine(" --help\t\tDisplay this help screen.", ConsoleColor.Cyan); + + if (validator.UnknownList.Any()) + Terminal.WriteLine($"Unknown arguments: {string.Join(", ", validator.UnknownList)}", ConsoleColor.Red); + + if (validator.RequiredList.Any()) + Terminal.WriteLine($"Required arguments: {string.Join(", ", validator.RequiredList)}", ConsoleColor.Red); + } + } +} diff --git a/Swan.Lite/Parsers/ArgumentParserSettings.cs b/Swan.Lite/Parsers/ArgumentParserSettings.cs new file mode 100644 index 0000000..dbe2f5b --- /dev/null +++ b/Swan.Lite/Parsers/ArgumentParserSettings.cs @@ -0,0 +1,53 @@ +using System; + +namespace Swan.Parsers +{ + /// + /// Provides settings for . + /// Based on CommandLine (Copyright 2005-2015 Giacomo Stelluti Scala and Contributors.). + /// + public class ArgumentParserSettings + { + /// + /// Gets or sets a value indicating whether [write banner]. + /// + /// + /// true if [write banner]; otherwise, false. + /// + public bool WriteBanner { get; set; } = true; + + /// + /// Gets or sets a value indicating whether perform case sensitive comparisons. + /// Note that case insensitivity only applies to parameters, not the values + /// assigned to them (for example, enum parsing). + /// + /// + /// true if [case sensitive]; otherwise, false. + /// + public bool CaseSensitive { get; set; } = false; + + /// + /// Gets or sets a value indicating whether perform case sensitive comparisons of values. + /// Note that case insensitivity only applies to values, not the parameters. + /// + /// + /// true if [case insensitive enum values]; otherwise, false. + /// + public bool CaseInsensitiveEnumValues { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the parser shall move on to the next argument and ignore the given argument if it + /// encounter an unknown arguments. + /// + /// + /// true to allow parsing the arguments with different class options that do not have all the arguments. + /// + /// + /// This allows fragmented version class parsing, useful for project with add-on where add-ons also requires command line arguments but + /// when these are unknown by the main program at build time. + /// + public bool IgnoreUnknownArguments { get; set; } = true; + + internal StringComparison NameComparer => CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + } +} \ No newline at end of file diff --git a/Swan.Lite/Parsers/ExpressionParser.cs b/Swan.Lite/Parsers/ExpressionParser.cs new file mode 100644 index 0000000..c189e70 --- /dev/null +++ b/Swan.Lite/Parsers/ExpressionParser.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; + +namespace Swan.Parsers +{ + /// + /// Represents a generic expression parser. + /// + public abstract class ExpressionParser + { + /// + /// Resolves the expression. + /// + /// The type of expression result. + /// The tokens. + /// The representation of the expression parsed. + public virtual T ResolveExpression(IEnumerable tokens) => + ResolveExpression(tokens, System.Globalization.CultureInfo.InvariantCulture); + + /// + /// Resolves the expression. + /// + /// The type of expression result. + /// The tokens. + /// The format provider. + /// The representation of the expression parsed. + public virtual T ResolveExpression(IEnumerable tokens, IFormatProvider formatProvider) + { + var conversion = Expression.Convert(Parse(tokens,formatProvider), typeof(T)); + return Expression.Lambda>(conversion).Compile()(); + } + + /// + /// Parses the specified tokens. + /// + /// The tokens. + /// + /// The final expression. + /// + public virtual Expression Parse(IEnumerable tokens) => + Parse(tokens, System.Globalization.CultureInfo.InvariantCulture); + + /// + /// Parses the specified tokens. + /// + /// The tokens. + /// The format provider. + /// + /// The final expression. + /// + public virtual Expression Parse(IEnumerable tokens, IFormatProvider formatProvider) + { + var expressionStack = new List>(); + + foreach (var token in tokens) + { + if (expressionStack.Any() == false) + expressionStack.Add(new Stack()); + + switch (token.Type) + { + case TokenType.Wall: + expressionStack.Add(new Stack()); + break; + case TokenType.Number: + expressionStack.Last().Push(Expression.Constant(Convert.ToDecimal(token.Value, formatProvider))); + break; + case TokenType.Variable: + ResolveVariable(token.Value, expressionStack.Last()); + break; + case TokenType.String: + expressionStack.Last().Push(Expression.Constant(token.Value)); + break; + case TokenType.Operator: + ResolveOperator(token.Value, expressionStack.Last()); + break; + case TokenType.Function: + ResolveFunction(token.Value, expressionStack.Last()); + + if (expressionStack.Count > 1 && expressionStack.Last().Count == 1) + { + var lastValue = expressionStack.Last().Pop(); + expressionStack.Remove(expressionStack.Last()); + expressionStack.Last().Push(lastValue); + } + + break; + } + } + + return expressionStack.Last().Pop(); + } + + /// + /// Resolves the variable. + /// + /// The value. + /// The expression stack. + public abstract void ResolveVariable(string value, Stack expressionStack); + + /// + /// Resolves the operator. + /// + /// The value. + /// The expression stack. + public abstract void ResolveOperator(string value, Stack expressionStack); + + /// + /// Resolves the function. + /// + /// The value. + /// The expression stack. + public abstract void ResolveFunction(string value, Stack expressionStack); + } +} diff --git a/Swan.Lite/Parsers/Operator.cs b/Swan.Lite/Parsers/Operator.cs new file mode 100644 index 0000000..598645a --- /dev/null +++ b/Swan.Lite/Parsers/Operator.cs @@ -0,0 +1,32 @@ +namespace Swan.Parsers +{ + /// + /// Represents an operator with precedence. + /// + public class Operator + { + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public string Name { get; set; } + + /// + /// Gets or sets the precedence. + /// + /// + /// The precedence. + /// + public int Precedence { get; set; } + + /// + /// Gets or sets a value indicating whether [right associative]. + /// + /// + /// true if [right associative]; otherwise, false. + /// + public bool RightAssociative { get; set; } + } +} diff --git a/Swan.Lite/Parsers/Token.cs b/Swan.Lite/Parsers/Token.cs new file mode 100644 index 0000000..20d30b8 --- /dev/null +++ b/Swan.Lite/Parsers/Token.cs @@ -0,0 +1,35 @@ +namespace Swan.Parsers +{ + /// + /// Represents a Token structure. + /// + public struct Token + { + /// + /// Initializes a new instance of the struct. + /// + /// The type. + /// The value. + public Token(TokenType type, string value) + { + Type = type; + Value = type == TokenType.Function || type == TokenType.Operator ? value.ToLowerInvariant() : value; + } + + /// + /// Gets or sets the type. + /// + /// + /// The type. + /// + public TokenType Type { get; set; } + + /// + /// Gets the value. + /// + /// + /// The value. + /// + public string Value { get; } + } +} diff --git a/Swan.Lite/Parsers/TokenType.cs b/Swan.Lite/Parsers/TokenType.cs new file mode 100644 index 0000000..5777f0d --- /dev/null +++ b/Swan.Lite/Parsers/TokenType.cs @@ -0,0 +1,48 @@ +namespace Swan.Parsers +{ + /// + /// Enums the token types. + /// + public enum TokenType + { + /// + /// The number + /// + Number, + + /// + /// The string + /// + String, + + /// + /// The variable + /// + Variable, + + /// + /// The function + /// + Function, + + /// + /// The parenthesis + /// + Parenthesis, + + /// + /// The operator + /// + Operator, + + /// + /// The comma + /// + Comma, + + /// + /// The wall, used to specified the end of argument list of the following function + /// + Wall, + } +} diff --git a/Swan.Lite/Parsers/Tokenizer.cs b/Swan.Lite/Parsers/Tokenizer.cs new file mode 100644 index 0000000..4d56714 --- /dev/null +++ b/Swan.Lite/Parsers/Tokenizer.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Parsers +{ + /// + /// Represents a generic tokenizer. + /// + public abstract class Tokenizer + { + private const char PeriodChar = '.'; + private const char CommaChar = ','; + private const char StringQuotedChar = '"'; + private const char OpenFuncChar = '('; + private const char CloseFuncChar = ')'; + private const char NegativeChar = '-'; + + private const string OpenFuncStr = "("; + + private readonly List _operators = new List(); + + /// + /// Initializes a new instance of the class. + /// This constructor will use the following default operators: + /// + /// + /// + /// Operator + /// Precedence + /// + /// + /// = + /// 1 + /// + /// + /// != + /// 1 + /// + /// + /// > + /// 2 + /// + /// + /// < + /// 2 + /// + /// + /// >= + /// 2 + /// + /// + /// <= + /// 2 + /// + /// + /// + + /// 3 + /// + /// + /// & + /// 3 + /// + /// + /// - + /// 3 + /// + /// + /// * + /// 4 + /// + /// + /// (backslash) + /// 4 + /// + /// + /// / + /// 4 + /// + /// + /// ^ + /// 4 + /// + /// + /// + /// The input. + protected Tokenizer(string input) + { + _operators.AddRange(GetDefaultOperators()); + Tokenize(input); + } + + /// + /// Initializes a new instance of the class. + /// + /// The input. + /// The operators to use. + protected Tokenizer(string input, IEnumerable operators) + { + _operators.AddRange(operators); + Tokenize(input); + } + + /// + /// Gets the tokens. + /// + /// + /// The tokens. + /// + public List Tokens { get; } = new List(); + + /// + /// Validates the input and return the start index for tokenizer. + /// + /// The input. + /// The start index. + /// true if the input is valid, otherwise false. + public abstract bool ValidateInput(string input, out int startIndex); + + /// + /// Resolves the type of the function or member. + /// + /// The input. + /// The token type. + public abstract TokenType ResolveFunctionOrMemberType(string input); + + /// + /// Evaluates the function or member. + /// + /// The input. + /// The position. + /// true if the input is a valid function or variable, otherwise false. + public virtual bool EvaluateFunctionOrMember(string input, int position) => false; + + /// + /// Gets the default operators. + /// + /// An array with the operators to use for the tokenizer. + public virtual Operator[] GetDefaultOperators() => new[] + { + new Operator {Name = "=", Precedence = 1}, + new Operator {Name = "!=", Precedence = 1}, + new Operator {Name = ">", Precedence = 2}, + new Operator {Name = "<", Precedence = 2}, + new Operator {Name = ">=", Precedence = 2}, + new Operator {Name = "<=", Precedence = 2}, + new Operator {Name = "+", Precedence = 3}, + new Operator {Name = "&", Precedence = 3}, + new Operator {Name = "-", Precedence = 3}, + new Operator {Name = "*", Precedence = 4}, + new Operator {Name = "/", Precedence = 4}, + new Operator {Name = "\\", Precedence = 4}, + new Operator {Name = "^", Precedence = 4}, + }; + + /// + /// Shunting the yard. + /// + /// if set to true [include function stopper] (Token type Wall). + /// + /// Enumerable of the token in in. + /// + /// + /// Wrong token + /// or + /// Mismatched parenthesis. + /// + public virtual IEnumerable ShuntingYard(bool includeFunctionStopper = true) + { + var stack = new Stack(); + + foreach (var tok in Tokens) + { + switch (tok.Type) + { + case TokenType.Number: + case TokenType.Variable: + case TokenType.String: + yield return tok; + break; + case TokenType.Function: + stack.Push(tok); + break; + case TokenType.Operator: + while (stack.Any() && stack.Peek().Type == TokenType.Operator && + CompareOperators(tok.Value, stack.Peek().Value)) + yield return stack.Pop(); + + stack.Push(tok); + break; + case TokenType.Comma: + while (stack.Any() && (stack.Peek().Type != TokenType.Comma && + stack.Peek().Type != TokenType.Parenthesis)) + yield return stack.Pop(); + + break; + case TokenType.Parenthesis: + if (tok.Value == OpenFuncStr) + { + if (stack.Any() && stack.Peek().Type == TokenType.Function) + { + if (includeFunctionStopper) + yield return new Token(TokenType.Wall, tok.Value); + } + + stack.Push(tok); + } + else + { + while (stack.Peek().Value != OpenFuncStr) + yield return stack.Pop(); + + stack.Pop(); + + if (stack.Any() && stack.Peek().Type == TokenType.Function) + { + yield return stack.Pop(); + } + } + + break; + default: + throw new InvalidOperationException("Wrong token"); + } + } + + while (stack.Any()) + { + var tok = stack.Pop(); + if (tok.Type == TokenType.Parenthesis) + throw new InvalidOperationException("Mismatched parenthesis"); + + yield return tok; + } + } + + private static bool CompareOperators(Operator op1, Operator op2) => op1.RightAssociative + ? op1.Precedence < op2.Precedence + : op1.Precedence <= op2.Precedence; + + private void Tokenize(string input) + { + if (!ValidateInput(input, out var startIndex)) + { + return; + } + + for (var i = startIndex; i < input.Length; i++) + { + if (char.IsWhiteSpace(input, i)) continue; + + if (input[i] == CommaChar) + { + Tokens.Add(new Token(TokenType.Comma, new string(new[] { input[i] }))); + continue; + } + + if (input[i] == StringQuotedChar) + { + i = ExtractString(input, i); + continue; + } + + if (char.IsLetter(input, i) || EvaluateFunctionOrMember(input, i)) + { + i = ExtractFunctionOrMember(input, i); + + continue; + } + + if (char.IsNumber(input, i) || ( + input[i] == NegativeChar && + ((Tokens.Any() && Tokens.Last().Type != TokenType.Number) || !Tokens.Any()))) + { + i = ExtractNumber(input, i); + continue; + } + + if (input[i] == OpenFuncChar || + input[i] == CloseFuncChar) + { + Tokens.Add(new Token(TokenType.Parenthesis, new string(new[] { input[i] }))); + continue; + } + + i = ExtractOperator(input, i); + } + } + + private int ExtractData( + string input, + int i, + Func tokenTypeEvaluation, + Func evaluation, + int right = 0, + int left = -1) + { + var charCount = 0; + for (var j = i + right; j < input.Length; j++) + { + if (evaluation(input[j])) + break; + + charCount++; + } + + // Extract and set the value + var value = input.SliceLength(i + right, charCount); + Tokens.Add(new Token(tokenTypeEvaluation(value), value)); + + i += charCount + left; + return i; + } + + private int ExtractOperator(string input, int i) => + ExtractData( + input, + i, + x => TokenType.Operator, + x => x == OpenFuncChar || + x == CommaChar || + x == PeriodChar || + x == StringQuotedChar || + char.IsWhiteSpace(x) || + char.IsNumber(x)); + + private int ExtractFunctionOrMember(string input, int i) => + ExtractData( + input, + i, + ResolveFunctionOrMemberType, + x => x == OpenFuncChar || + x == CloseFuncChar || + x == CommaChar || + char.IsWhiteSpace(x)); + + private int ExtractNumber(string input, int i) => + ExtractData( + input, + i, + x => TokenType.Number, + x => !char.IsNumber(x) && x != PeriodChar && x != NegativeChar); + + private int ExtractString(string input, int i) + { + var length = ExtractData(input, i, x => TokenType.String, x => x == StringQuotedChar, 1, 1); + + // open string, report issue + if (length == input.Length && input[length - 1] != StringQuotedChar) + throw new FormatException($"Parser error (Position {i}): Expected '\"' but got '{input[length - 1]}'."); + + return length; + } + + private bool CompareOperators(string op1, string op2) + => CompareOperators(GetOperatorOrDefault(op1), GetOperatorOrDefault(op2)); + + private Operator GetOperatorOrDefault(string op) + => _operators.FirstOrDefault(x => x.Name == op) ?? new Operator { Name = op, Precedence = 0 }; + } +} diff --git a/Swan.Lite/Parsers/VerbOptionAttribute.cs b/Swan.Lite/Parsers/VerbOptionAttribute.cs new file mode 100644 index 0000000..79496b2 --- /dev/null +++ b/Swan.Lite/Parsers/VerbOptionAttribute.cs @@ -0,0 +1,40 @@ +using System; + +namespace Swan.Parsers +{ + /// + /// Models a verb option. + /// + [AttributeUsage(AttributeTargets.Property)] + public sealed class VerbOptionAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// name. + public VerbOptionAttribute(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Gets the name of the verb option. + /// + /// + /// Name. + /// + public string Name { get; } + + /// + /// Gets or sets a short description of this command line verb. Usually a sentence summary. + /// + /// + /// The help text. + /// + public string HelpText { get; set; } + + /// + public override string ToString() => $" {Name}\t\t{HelpText}"; + } +} \ No newline at end of file diff --git a/Swan.Lite/Reflection/AttributeCache.cs b/Swan.Lite/Reflection/AttributeCache.cs new file mode 100644 index 0000000..c4440af --- /dev/null +++ b/Swan.Lite/Reflection/AttributeCache.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// A thread-safe cache of attributes belonging to a given key (MemberInfo or Type). + /// + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + public class AttributeCache + { + private readonly Lazy, IEnumerable>> _data = + new Lazy, IEnumerable>>(() => + new ConcurrentDictionary, IEnumerable>(), true); + + /// + /// Initializes a new instance of the class. + /// + /// The property cache object. + public AttributeCache(PropertyTypeCache? propertyCache = null) + { + PropertyTypeCache = propertyCache ?? PropertyTypeCache.DefaultCache.Value; + } + + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new AttributeCache()); + + /// + /// A PropertyTypeCache object for caching properties and their attributes. + /// + public PropertyTypeCache PropertyTypeCache { get; } + + /// + /// Determines whether [contains] [the specified member]. + /// + /// The type of the attribute to be retrieved. + /// The member. + /// + /// true if [contains] [the specified member]; otherwise, false. + /// + public bool Contains(MemberInfo member) => _data.Value.ContainsKey(new Tuple(member, typeof(T))); + + /// + /// Gets specific attributes from a member constrained to an attribute. + /// + /// The type of the attribute to be retrieved. + /// The member. + /// true to inspect the ancestors of element; otherwise, false. + /// An array of the attributes stored for the specified type. + public IEnumerable Retrieve(MemberInfo member, bool inherit = false) + where T : Attribute + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + + return Retrieve(new Tuple(member, typeof(T)), t => member.GetCustomAttributes(inherit)); + } + + /// + /// Gets all attributes of a specific type from a member. + /// + /// The member. + /// The attribute type. + /// true to inspect the ancestors of element; otherwise, false. + /// An array of the attributes stored for the specified type. + public IEnumerable Retrieve(MemberInfo member, Type type, bool inherit = false) + { + if (member == null) + throw new ArgumentNullException(nameof(member)); + + if (type == null) + throw new ArgumentNullException(nameof(type)); + + return Retrieve( + new Tuple(member, type), + t => member.GetCustomAttributes(type, inherit)); + } + + /// + /// Gets one attribute of a specific type from a member. + /// + /// The attribute type. + /// The member. + /// true to inspect the ancestors of element; otherwise, false. + /// An attribute stored for the specified type. + public T RetrieveOne(MemberInfo member, bool inherit = false) + where T : Attribute + { + if (member == null) + return default; + + var attr = Retrieve( + new Tuple(member, typeof(T)), + t => member.GetCustomAttributes(typeof(T), inherit)); + + return ConvertToAttribute(attr); + } + + /// + /// Gets one attribute of a specific type from a generic type. + /// + /// The type of the attribute. + /// The type to retrieve the attribute. + /// if set to true [inherit]. + /// An attribute stored for the specified type. + public TAttribute RetrieveOne(bool inherit = false) + where TAttribute : Attribute + { + var attr = Retrieve( + new Tuple(typeof(T), typeof(TAttribute)), + t => typeof(T).GetCustomAttributes(typeof(TAttribute), inherit)); + + return ConvertToAttribute(attr); + } + + /// + /// Gets all properties an their attributes of a given type constrained to only attributes. + /// + /// The type of the attribute to retrieve. + /// The type of the object. + /// true to inspect the ancestors of element; otherwise, false. + /// A dictionary of the properties and their attributes stored for the specified type. + public Dictionary> Retrieve(Type type, bool inherit = false) + where T : Attribute => + PropertyTypeCache.RetrieveAllProperties(type, true) + .ToDictionary(x => x, x => Retrieve(x, inherit)); + + /// + /// Gets all properties and their attributes of a given type. + /// + /// The object type used to extract the properties from. + /// The type of the attribute. + /// true to inspect the ancestors of element; otherwise, false. + /// + /// A dictionary of the properties and their attributes stored for the specified type. + /// + public Dictionary> RetrieveFromType(bool inherit = false) + => RetrieveFromType(typeof(TAttribute), inherit); + + /// + /// Gets all properties and their attributes of a given type. + /// + /// The object type used to extract the properties from. + /// Type of the attribute. + /// true to inspect the ancestors of element; otherwise, false. + /// + /// A dictionary of the properties and their attributes stored for the specified type. + /// + public Dictionary> RetrieveFromType(Type attributeType, bool inherit = false) + { + if (attributeType == null) + throw new ArgumentNullException(nameof(attributeType)); + + return PropertyTypeCache.RetrieveAllProperties(true) + .ToDictionary(x => x, x => Retrieve(x, attributeType, inherit)); + } + + private static T ConvertToAttribute(IEnumerable attr) + where T : Attribute + { + if (attr?.Any() != true) + return default; + + return attr.Count() == 1 + ? (T) Convert.ChangeType(attr.First(), typeof(T)) + : throw new AmbiguousMatchException("Multiple custom attributes of the same type found."); + } + + private IEnumerable Retrieve(Tuple key, Func, IEnumerable> factory) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + return _data.Value.GetOrAdd(key, k => factory.Invoke(k).Where(item => item != null)); + } + } +} diff --git a/Swan.Lite/Reflection/ConstructorTypeCache.cs b/Swan.Lite/Reflection/ConstructorTypeCache.cs new file mode 100644 index 0000000..5498213 --- /dev/null +++ b/Swan.Lite/Reflection/ConstructorTypeCache.cs @@ -0,0 +1,51 @@ +using Swan.Reflection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Lite.Reflection +{ + /// + /// A thread-safe cache of constructors belonging to a given type. + /// + public class ConstructorTypeCache : TypeCache> + { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = + new Lazy(() => new ConstructorTypeCache()); + + /// + /// Retrieves all constructors order by the number of parameters ascending. + /// + /// The type to inspect. + /// if set to true [include non public]. + /// + /// A collection with all the constructors in the given type. + /// + public IEnumerable> RetrieveAllConstructors(bool includeNonPublic = false) + => Retrieve(GetConstructors(includeNonPublic)); + + /// + /// Retrieves all constructors order by the number of parameters ascending. + /// + /// The type. + /// if set to true [include non public]. + /// + /// A collection with all the constructors in the given type. + /// + public IEnumerable> RetrieveAllConstructors(Type type, bool includeNonPublic = false) + => Retrieve(type, GetConstructors(includeNonPublic)); + + private static Func>> GetConstructors(bool includeNonPublic) + => t => t.GetConstructors(includeNonPublic ? BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance : BindingFlags.Public | BindingFlags.Instance) + .Select(x => Tuple.Create(x, x.GetParameters())) + .OrderBy(x => x.Item2.Length) + .ToList(); + } +} \ No newline at end of file diff --git a/Swan.Lite/Reflection/ExtendedPropertyInfo.cs b/Swan.Lite/Reflection/ExtendedPropertyInfo.cs new file mode 100644 index 0000000..aa869f1 --- /dev/null +++ b/Swan.Lite/Reflection/ExtendedPropertyInfo.cs @@ -0,0 +1,107 @@ +using System; +using System.Reflection; +using Swan.Configuration; + +namespace Swan.Reflection +{ + /// + /// Represents a Property object from a Object Reflection Property with extended values. + /// + public class ExtendedPropertyInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The property information. + public ExtendedPropertyInfo(PropertyInfo propertyInfo) + { + if (propertyInfo == null) + { + throw new ArgumentNullException(nameof(propertyInfo)); + } + + Property = propertyInfo.Name; + DataType = propertyInfo.PropertyType.Name; + + foreach (PropertyDisplayAttribute display in AttributeCache.DefaultCache.Value.Retrieve(propertyInfo, true)) + { + Name = display.Name; + Description = display.Description; + GroupName = display.GroupName; + DefaultValue = display.DefaultValue; + } + } + + /// + /// Gets or sets the property. + /// + /// + /// The property. + /// + public string Property { get; } + + /// + /// Gets or sets the type of the data. + /// + /// + /// The type of the data. + /// + public string DataType { get; } + + /// + /// Gets or sets the value. + /// + /// + /// The value. + /// + public object Value { get; set; } + + /// + /// Gets or sets the default value. + /// + /// + /// The default value. + /// + public object DefaultValue { get; } + + /// + /// Gets or sets the name. + /// + /// + /// The name. + /// + public string Name { get; } + + /// + /// Gets or sets the description. + /// + /// + /// The description. + /// + public string Description { get; } + + /// + /// Gets or sets the name of the group. + /// + /// + /// The name of the group. + /// + public string GroupName { get; } + } + + /// + /// Represents a Property object from a Object Reflection Property with extended values. + /// + /// The type of the object. + public class ExtendedPropertyInfo : ExtendedPropertyInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The property. + public ExtendedPropertyInfo(string property) + : base(typeof(T).GetProperty(property)) + { + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Reflection/ExtendedTypeInfo.cs b/Swan.Lite/Reflection/ExtendedTypeInfo.cs new file mode 100644 index 0000000..fbaed95 --- /dev/null +++ b/Swan.Lite/Reflection/ExtendedTypeInfo.cs @@ -0,0 +1,267 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// Provides extended information about a type. + /// + /// This class is mainly used to define sets of types within the Definition class + /// and it is not meant for other than querying the BasicTypesInfo dictionary. + /// + public class ExtendedTypeInfo + { + private const string TryParseMethodName = nameof(byte.TryParse); + private const string ToStringMethodName = nameof(ToString); + + private static readonly Type[] NumericTypes = + { + typeof(byte), + typeof(sbyte), + typeof(decimal), + typeof(double), + typeof(float), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(short), + typeof(ushort), + }; + + private readonly ParameterInfo[]? _tryParseParameters; + private readonly int _toStringArgumentLength; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The t. + public ExtendedTypeInfo(Type t) + { + Type = t ?? throw new ArgumentNullException(nameof(t)); + IsNullableValueType = Type.IsGenericType + && Type.GetGenericTypeDefinition() == typeof(Nullable<>); + + IsValueType = t.IsValueType; + + UnderlyingType = IsNullableValueType ? + new NullableConverter(Type).UnderlyingType : + Type; + + IsNumeric = NumericTypes.Contains(UnderlyingType); + + // Extract the TryParse method info + try + { + TryParseMethodInfo = UnderlyingType.GetMethod(TryParseMethodName, + new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider), UnderlyingType.MakeByRefType() }) ?? + UnderlyingType.GetMethod(TryParseMethodName, + new[] { typeof(string), UnderlyingType.MakeByRefType() }); + + _tryParseParameters = TryParseMethodInfo?.GetParameters(); + } + catch + { + // ignored + } + + // Extract the ToString method Info + try + { + ToStringMethodInfo = UnderlyingType.GetMethod(ToStringMethodName, + new[] { typeof(IFormatProvider) }) ?? + UnderlyingType.GetMethod(ToStringMethodName, + Array.Empty()); + + _toStringArgumentLength = ToStringMethodInfo?.GetParameters().Length ?? 0; + } + catch + { + // ignored + } + } + + #endregion + + #region Properties + + /// + /// Gets the type this extended info class provides for. + /// + /// + /// The type. + /// + public Type Type { get; } + + /// + /// Gets a value indicating whether the type is a nullable value type. + /// + /// + /// true if this instance is nullable value type; otherwise, false. + /// + public bool IsNullableValueType { get; } + + /// + /// Gets a value indicating whether the type or underlying type is numeric. + /// + /// + /// true if this instance is numeric; otherwise, false. + /// + public bool IsNumeric { get; } + + /// + /// Gets a value indicating whether the type is value type. + /// Nullable value types have this property set to False. + /// + public bool IsValueType { get; } + + /// + /// When dealing with nullable value types, this property will + /// return the underlying value type of the nullable, + /// Otherwise it will return the same type as the Type property. + /// + /// + /// The type of the underlying. + /// + public Type UnderlyingType { get; } + + /// + /// Gets the try parse method information. If the type does not contain + /// a suitable TryParse static method, it will return null. + /// + /// + /// The try parse method information. + /// + public MethodInfo TryParseMethodInfo { get; } + + /// + /// Gets the ToString method info + /// It will prefer the overload containing the IFormatProvider argument. + /// + /// + /// To string method information. + /// + public MethodInfo ToStringMethodInfo { get; } + + /// + /// Gets a value indicating whether the type contains a suitable TryParse method. + /// + /// + /// true if this instance can parse natively; otherwise, false. + /// + public bool CanParseNatively => TryParseMethodInfo != null; + + #endregion + + #region Methods + + /// + /// Tries to parse the string into an object of the type this instance represents. + /// Returns false when no suitable TryParse methods exists for the type or when parsing fails + /// for any reason. When possible, this method uses CultureInfo.InvariantCulture and NumberStyles.Any. + /// + /// The s. + /// The result. + /// true if parse was converted successfully; otherwise, false. + public bool TryParse(string s, out object? result) + { + result = Type.GetDefault(); + + try + { + if (Type == typeof(string)) + { + result = Convert.ChangeType(s, Type, CultureInfo.InvariantCulture); + return true; + } + + if ((IsNullableValueType && string.IsNullOrEmpty(s)) || !CanParseNatively) + { + return true; + } + + // Build the arguments of the TryParse method + var dynamicArguments = new List { s }; + + for (var pi = 1; pi < _tryParseParameters.Length - 1; pi++) + { + var argInfo = _tryParseParameters[pi]; + if (argInfo.ParameterType == typeof(IFormatProvider)) + dynamicArguments.Add(CultureInfo.InvariantCulture); + else if (argInfo.ParameterType == typeof(NumberStyles)) + dynamicArguments.Add(NumberStyles.Any); + else + dynamicArguments.Add(null); + } + + dynamicArguments.Add(null); + var parseArguments = dynamicArguments.ToArray(); + + if ((bool) TryParseMethodInfo.Invoke(null, parseArguments)) + { + result = parseArguments[parseArguments.Length - 1]; + return true; + } + } + catch + { + // Ignore + } + + return false; + } + + /// + /// Converts this instance to its string representation, + /// trying to use the CultureInfo.InvariantCulture + /// IFormat provider if the overload is available. + /// + /// The instance. + /// A that represents the current object. + public string ToStringInvariant(object instance) + { + if (instance == null) + return string.Empty; + + return _toStringArgumentLength != 1 + ? instance.ToString() + : ToStringMethodInfo.Invoke(instance, new object[] {CultureInfo.InvariantCulture}) as string ?? string.Empty; + } + + #endregion + } + + /// + /// Provides extended information about a type. + /// + /// This class is mainly used to define sets of types within the Constants class + /// and it is not meant for other than querying the BasicTypesInfo dictionary. + /// + /// The type of extended type information. + public class ExtendedTypeInfo : ExtendedTypeInfo + { + /// + /// Initializes a new instance of the class. + /// + public ExtendedTypeInfo() + : base(typeof(T)) + { + // placeholder + } + + /// + /// Converts this instance to its string representation, + /// trying to use the CultureInfo.InvariantCulture + /// IFormat provider if the overload is available. + /// + /// The instance. + /// A that represents the current object. + public string ToStringInvariant(T instance) => base.ToStringInvariant(instance); + } +} diff --git a/Swan.Lite/Reflection/IPropertyProxy.cs b/Swan.Lite/Reflection/IPropertyProxy.cs new file mode 100644 index 0000000..8b88856 --- /dev/null +++ b/Swan.Lite/Reflection/IPropertyProxy.cs @@ -0,0 +1,22 @@ +namespace Swan.Reflection +{ + /// + /// Represents a generic interface to store getters and setters. + /// + public interface IPropertyProxy + { + /// + /// Gets the property value via a stored delegate. + /// + /// The instance. + /// The property value. + object GetValue(object instance); + + /// + /// Sets the property value via a stored delegate. + /// + /// The instance. + /// The value. + void SetValue(object instance, object value); + } +} \ No newline at end of file diff --git a/Swan.Lite/Reflection/MethodInfoCache.cs b/Swan.Lite/Reflection/MethodInfoCache.cs new file mode 100644 index 0000000..ca363d0 --- /dev/null +++ b/Swan.Lite/Reflection/MethodInfoCache.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Concurrent; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// Represents a Method Info Cache. + /// + public class MethodInfoCache : ConcurrentDictionary + { + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The type of type. + /// The name. + /// The alias. + /// The types. + /// + /// The cached MethodInfo. + /// + /// name + /// or + /// factory. + public MethodInfo Retrieve(string name, string alias, params Type[] types) + => Retrieve(typeof(T), name, alias, types); + + /// + /// Retrieves the specified name. + /// + /// The type of type. + /// The name. + /// The types. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(string name, params Type[] types) + => Retrieve(typeof(T), name, name, types); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// The types. + /// + /// An array of the properties stored for the specified type. + /// + public MethodInfo Retrieve(Type type, string name, params Type[] types) + => Retrieve(type, name, name, types); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// The alias. + /// The types. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(Type type, string name, string alias, params Type[] types) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (alias == null) + throw new ArgumentNullException(nameof(alias)); + + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return GetOrAdd( + alias, + x => type.GetMethod(name, types ?? Array.Empty())); + } + + /// + /// Retrieves the specified name. + /// + /// The type of type. + /// The name. + /// + /// The cached MethodInfo. + /// + public MethodInfo Retrieve(string name) + => Retrieve(typeof(T), name); + + /// + /// Retrieves the specified type. + /// + /// The type. + /// The name. + /// + /// The cached MethodInfo. + /// + /// + /// type + /// or + /// name. + /// + public MethodInfo Retrieve(Type type, string name) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (name == null) + throw new ArgumentNullException(nameof(name)); + + return GetOrAdd( + name, + type.GetMethod); + } + } +} diff --git a/Swan.Lite/Reflection/PropertyProxy.cs b/Swan.Lite/Reflection/PropertyProxy.cs new file mode 100644 index 0000000..0373d72 --- /dev/null +++ b/Swan.Lite/Reflection/PropertyProxy.cs @@ -0,0 +1,47 @@ +using System; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Swan.Reflection +{ + /// + /// Represents a generic class to store getters and setters. + /// + /// The type of the class. + /// The type of the property. + /// + public sealed class PropertyProxy : IPropertyProxy + where TClass : class + { + private readonly Func _getter; + private readonly Action _setter; + + /// + /// Initializes a new instance of the class. + /// + /// The property. + public PropertyProxy(PropertyInfo property) + { + if (property == null) + throw new ArgumentNullException(nameof(property)); + + var getterInfo = property.GetGetMethod(false); + if (getterInfo != null) + _getter = (Func)Delegate.CreateDelegate(typeof(Func), getterInfo); + + var setterInfo = property.GetSetMethod(false); + if (setterInfo != null) + _setter = (Action)Delegate.CreateDelegate(typeof(Action), setterInfo); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + object IPropertyProxy.GetValue(object instance) => + _getter(instance as TClass); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + void IPropertyProxy.SetValue(object instance, object value) => + _setter(instance as TClass, (TProperty)value); + } +} \ No newline at end of file diff --git a/Swan.Lite/Reflection/PropertyTypeCache.cs b/Swan.Lite/Reflection/PropertyTypeCache.cs new file mode 100644 index 0000000..444e8fa --- /dev/null +++ b/Swan.Lite/Reflection/PropertyTypeCache.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Swan.Reflection +{ + /// + /// A thread-safe cache of properties belonging to a given type. + /// + public class PropertyTypeCache : TypeCache + { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new PropertyTypeCache()); + + /// + /// Retrieves all properties. + /// + /// The type to inspect. + /// if set to true [only public]. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveAllProperties(bool onlyPublic = false) + => Retrieve(onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc()); + + /// + /// Retrieves all properties. + /// + /// The type. + /// if set to true [only public]. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveAllProperties(Type type, bool onlyPublic = false) + => Retrieve(type, onlyPublic ? GetAllPublicPropertiesFunc() : GetAllPropertiesFunc()); + + /// + /// Retrieves the filtered properties. + /// + /// The type. + /// if set to true [only public]. + /// The filter. + /// + /// A collection with all the properties in the given type. + /// + public IEnumerable RetrieveFilteredProperties( + Type type, + bool onlyPublic, + Func filter) + => Retrieve(type, + onlyPublic ? GetAllPublicPropertiesFunc(filter) : GetAllPropertiesFunc(filter)); + + private static Func> GetAllPropertiesFunc( + Func filter = null) + => GetPropertiesFunc( + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + filter); + + private static Func> GetAllPublicPropertiesFunc( + Func filter = null) + => GetPropertiesFunc(BindingFlags.Public | BindingFlags.Instance, filter); + + private static Func> GetPropertiesFunc(BindingFlags flags, + Func filter = null) + => t => t.GetProperties(flags) + .Where(filter ?? (p => p.CanRead || p.CanWrite)); + } +} \ No newline at end of file diff --git a/Swan.Lite/Reflection/TypeCache.cs b/Swan.Lite/Reflection/TypeCache.cs new file mode 100644 index 0000000..48ea8cd --- /dev/null +++ b/Swan.Lite/Reflection/TypeCache.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Swan.Collections; + +namespace Swan.Reflection +{ + /// + /// A thread-safe cache of members belonging to a given type. + /// + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + /// The type of Member to be cached. + public abstract class TypeCache : CollectionCacheRepository + { + /// + /// Determines whether the cache contains the specified type. + /// + /// The type of the out. + /// + /// true if [contains]; otherwise, false. + /// + public bool Contains() => ContainsKey(typeof(TOut)); + + /// + /// Retrieves the properties stored for the specified type. + /// If the properties are not available, it calls the factory method to retrieve them + /// and returns them as an array of PropertyInfo. + /// + /// The type of the out. + /// The factory. + /// An array of the properties stored for the specified type. + public IEnumerable Retrieve(Func> factory) + => Retrieve(typeof(TOut), factory); + } + + /// + /// A thread-safe cache of fields belonging to a given type + /// The Retrieve method is the most useful one in this class as it + /// calls the retrieval process if the type is not contained + /// in the cache. + /// + public class FieldTypeCache : TypeCache + { + /// + /// Gets the default cache. + /// + /// + /// The default cache. + /// + public static Lazy DefaultCache { get; } = new Lazy(() => new FieldTypeCache()); + + /// + /// Retrieves all fields. + /// + /// The type to inspect. + /// + /// A collection with all the fields in the given type. + /// + public IEnumerable RetrieveAllFields() + => Retrieve(GetAllFieldsFunc()); + + /// + /// Retrieves all fields. + /// + /// The type. + /// + /// A collection with all the fields in the given type. + /// + public IEnumerable RetrieveAllFields(Type type) + => Retrieve(type, GetAllFieldsFunc()); + + private static Func> GetAllFieldsFunc() + => t => t.GetFields(BindingFlags.Public | BindingFlags.Instance); + } +} \ No newline at end of file diff --git a/Swan.Lite/SingletonBase.cs b/Swan.Lite/SingletonBase.cs new file mode 100644 index 0000000..c932abb --- /dev/null +++ b/Swan.Lite/SingletonBase.cs @@ -0,0 +1,59 @@ +using System; + +namespace Swan +{ + /// + /// Represents a singleton pattern abstract class. + /// + /// The type of class. + public abstract class SingletonBase : IDisposable + where T : class + { + /// + /// The static, singleton instance reference. + /// + protected static readonly Lazy LazyInstance = new Lazy( + valueFactory: () => Activator.CreateInstance(typeof(T), true) as T, + isThreadSafe: true); + + private bool _isDisposing; // To detect redundant calls + + /// + /// Gets the instance that this singleton represents. + /// If the instance is null, it is constructed and assigned when this member is accessed. + /// + /// + /// The instance. + /// + public static T Instance => LazyInstance.Value; + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// Call the GC.SuppressFinalize if you override this method and use + /// a non-default class finalizer (destructor). + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposeManaged) + { + if (_isDisposing) return; + + _isDisposing = true; + + // free managed resources + if (LazyInstance == null) return; + + try + { + var disposableInstance = LazyInstance.Value as IDisposable; + disposableInstance?.Dispose(); + } + catch + { + // swallow + } + } + } +} diff --git a/Swan.Lite/StringConversionException.cs b/Swan.Lite/StringConversionException.cs new file mode 100644 index 0000000..30f7517 --- /dev/null +++ b/Swan.Lite/StringConversionException.cs @@ -0,0 +1,76 @@ +using System; +using System.Runtime.Serialization; + +namespace Swan +{ + /// + /// The exception that is thrown when a conversion from a string to a + /// specified type fails. + /// + /// + [Serializable] + public class StringConversionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + public StringConversionException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + public StringConversionException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, + /// or if no inner exception is specified. + public StringConversionException(string message, Exception innerException) + : base(message, innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The desired resulting type of the attempted conversion. + public StringConversionException(Type type) + : base(BuildStandardMessageForType(type)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The desired resulting type of the attempted conversion. + /// The exception that is the cause of the current exception, + /// or if no inner exception is specified. + public StringConversionException(Type type, Exception innerException) + : base(BuildStandardMessageForType(type), innerException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The that holds the serialized object data + /// about the exception being thrown. + /// The that contains contextual information + /// about the source or destination. + protected StringConversionException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + private static string BuildStandardMessageForType(Type type) + => $"Cannot convert a string to an instance of {type.FullName}"; + } +} diff --git a/Swan.Lite/StructEndiannessAttribute.cs b/Swan.Lite/StructEndiannessAttribute.cs new file mode 100644 index 0000000..16b7932 --- /dev/null +++ b/Swan.Lite/StructEndiannessAttribute.cs @@ -0,0 +1,30 @@ +using System; + +namespace Swan +{ + /// + /// An attribute used to help conversion structs back and forth into arrays of bytes via + /// extension methods included in this library ToStruct and ToBytes. + /// + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Struct)] + public class StructEndiannessAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The endianness. + public StructEndiannessAttribute(Endianness endianness) + { + Endianness = endianness; + } + + /// + /// Gets the endianness. + /// + /// + /// The endianness. + /// + public Endianness Endianness { get; } + } +} \ No newline at end of file diff --git a/Swan.Lite/Swan.Lite.csproj b/Swan.Lite/Swan.Lite.csproj new file mode 100644 index 0000000..8d1f58e --- /dev/null +++ b/Swan.Lite/Swan.Lite.csproj @@ -0,0 +1,18 @@ + + + + Repeating code and reinventing the wheel is generally considered bad practice. At Unosquare we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we and other good developers have developed and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started it. We decide to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should have no external dependencies, it should be cross-platform, and it should be useful. + Copyright (c) 2016-2019 - Unosquare + Unosquare SWAN + netcoreapp3.0 + Swan.Lite + 2.4.2 + Unosquare + https://github.com/unosquare/swan/raw/master/swan-logo-32.png + https://github.com/unosquare/swan + https://raw.githubusercontent.com/unosquare/swan/master/LICENSE + best-practices netcore network objectmapper json-serialization + 8.0 + enable + + diff --git a/Swan.Lite/SwanRuntime.cs b/Swan.Lite/SwanRuntime.cs new file mode 100644 index 0000000..de24abf --- /dev/null +++ b/Swan.Lite/SwanRuntime.cs @@ -0,0 +1,233 @@ +using Swan.Logging; +using System; +using System.IO; +using System.Reflection; +using System.Threading; + +namespace Swan +{ + /// + /// Provides utility methods to retrieve information about the current application. + /// + public static class SwanRuntime + { + private static readonly Lazy EntryAssemblyLazy = new Lazy(Assembly.GetEntryAssembly); + + private static readonly Lazy CompanyNameLazy = new Lazy(() => + { + var attribute = + EntryAssembly.GetCustomAttribute(typeof(AssemblyCompanyAttribute)) as AssemblyCompanyAttribute; + return attribute?.Company ?? string.Empty; + }); + + private static readonly Lazy ProductNameLazy = new Lazy(() => + { + var attribute = + EntryAssembly.GetCustomAttribute(typeof(AssemblyProductAttribute)) as AssemblyProductAttribute; + return attribute?.Product ?? string.Empty; + }); + + private static readonly Lazy ProductTrademarkLazy = new Lazy(() => + { + var attribute = + EntryAssembly.GetCustomAttribute(typeof(AssemblyTrademarkAttribute)) as AssemblyTrademarkAttribute; + return attribute?.Trademark ?? string.Empty; + }); + + private static readonly string ApplicationMutexName = "Global\\{{" + EntryAssembly.FullName + "}}"; + + private static readonly object SyncLock = new object(); + + private static OperatingSystem? _oS; + + #region Properties + + /// + /// Gets the current Operating System. + /// + /// + /// The os. + /// + public static OperatingSystem OS + { + get + { + if (_oS.HasValue == false) + { + var windowsDirectory = Environment.GetEnvironmentVariable("windir"); + if (string.IsNullOrEmpty(windowsDirectory) == false + && windowsDirectory.Contains(@"\") + && Directory.Exists(windowsDirectory)) + { + _oS = OperatingSystem.Windows; + } + else + { + _oS = File.Exists(@"/proc/sys/kernel/ostype") ? OperatingSystem.Unix : OperatingSystem.Osx; + } + } + + return _oS ?? OperatingSystem.Unknown; + } + } + + /// + /// Checks if this application (including version number) is the only instance currently running. + /// + /// + /// true if this instance is the only instance; otherwise, false. + /// + public static bool IsTheOnlyInstance + { + get + { + lock (SyncLock) + { + try + { + // Try to open existing mutex. + Mutex.OpenExisting(ApplicationMutexName); + } + catch + { + try + { + // If exception occurred, there is no such mutex. + var appMutex = new Mutex(true, ApplicationMutexName); + $"Application Mutex created {appMutex} named '{ApplicationMutexName}'".Debug( + typeof(SwanRuntime)); + + // Only one instance. + return true; + } + catch + { + // Sometimes the user can't create the Global Mutex + } + } + + // More than one instance. + return false; + } + } + } + + /// + /// Gets a value indicating whether this application instance is using the MONO runtime. + /// + /// + /// true if this instance is using MONO runtime; otherwise, false. + /// + public static bool IsUsingMonoRuntime => Type.GetType("Mono.Runtime") != null; + + /// + /// Gets the assembly that started the application. + /// + /// + /// The entry assembly. + /// + public static Assembly EntryAssembly => EntryAssemblyLazy.Value; + + /// + /// Gets the name of the entry assembly. + /// + /// + /// The name of the entry assembly. + /// + public static AssemblyName EntryAssemblyName => EntryAssemblyLazy.Value.GetName(); + + /// + /// Gets the entry assembly version. + /// + public static Version EntryAssemblyVersion => EntryAssemblyName.Version; + + /// + /// Gets the full path to the folder containing the assembly that started the application. + /// + /// + /// The entry assembly directory. + /// + public static string EntryAssemblyDirectory + { + get + { + var uri = new UriBuilder(EntryAssembly.CodeBase); + var path = Uri.UnescapeDataString(uri.Path); + return Path.GetDirectoryName(path); + } + } + + /// + /// Gets the name of the company. + /// + /// + /// The name of the company. + /// + public static string CompanyName => CompanyNameLazy.Value; + + /// + /// Gets the name of the product. + /// + /// + /// The name of the product. + /// + public static string ProductName => ProductNameLazy.Value; + + /// + /// Gets the trademark. + /// + /// + /// The product trademark. + /// + public static string ProductTrademark => ProductTrademarkLazy.Value; + + /// + /// Gets a local storage path with a version. + /// + /// + /// The local storage path. + /// + public static string LocalStoragePath + { + get + { + var localAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + EntryAssemblyName.Name); + + var returnPath = Path.Combine(localAppDataPath, EntryAssemblyVersion.ToString()); + + if (!Directory.Exists(returnPath)) + { + Directory.CreateDirectory(returnPath); + } + + return returnPath; + } + } + + #endregion + + #region Methods + + /// + /// Build a full path pointing to the current user's desktop with the given filename. + /// + /// The filename. + /// + /// The fully qualified location of path, such as "C:\MyFile.txt". + /// + /// filename. + public static string GetDesktopFilePath(string filename) + { + if (string.IsNullOrWhiteSpace(filename)) + throw new ArgumentNullException(nameof(filename)); + + var pathWithFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), + filename); + + return Path.GetFullPath(pathWithFilename); + } + + #endregion + } +} diff --git a/Swan.Lite/Terminal.Graphics.cs b/Swan.Lite/Terminal.Graphics.cs new file mode 100644 index 0000000..1a1b5be --- /dev/null +++ b/Swan.Lite/Terminal.Graphics.cs @@ -0,0 +1,37 @@ +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + /// + /// Represents a Table to print in console. + /// + private static class Table + { + public static void Vertical() => Write('\u2502', Settings.BorderColor); + + public static void RightTee() => Write('\u2524', Settings.BorderColor); + + public static void TopRight() => Write('\u2510', Settings.BorderColor); + + public static void BottomLeft() => Write('\u2514', Settings.BorderColor); + + public static void BottomTee() => Write('\u2534', Settings.BorderColor); + + public static void TopTee() => Write('\u252c', Settings.BorderColor); + + public static void LeftTee() => Write('\u251c', Settings.BorderColor); + + public static void Horizontal(int length) => Write(new string('\u2500', length), Settings.BorderColor); + + public static void Tee() => Write('\u253c', Settings.BorderColor); + + public static void BottomRight() => Write('\u2518', Settings.BorderColor); + + public static void TopLeft() => Write('\u250C', Settings.BorderColor); + } + } +} diff --git a/Swan.Lite/Terminal.Interaction.cs b/Swan.Lite/Terminal.Interaction.cs new file mode 100644 index 0000000..20d7e23 --- /dev/null +++ b/Swan.Lite/Terminal.Interaction.cs @@ -0,0 +1,261 @@ +using System; +using System.Collections.Generic; +using Swan.Lite.Logging; +using System.Globalization; +using Swan.Logging; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + #region ReadKey + + /// + /// Reads a key from the Terminal. This is the closest equivalent to Console.ReadKey. + /// + /// if set to true the pressed key will not be rendered to the output. + /// if set to true the output will continue to be shown. + /// This is useful for services and daemons that are running as console applications and wait for a key to exit the program. + /// The console key information. + public static ConsoleKeyInfo ReadKey(bool intercept, bool disableLocking = false) + { + if (!IsConsolePresent) return default; + if (disableLocking) return Console.ReadKey(intercept); + + lock (SyncLock) + { + Flush(); + InputDone.Reset(); + + try + { + Console.CursorVisible = true; + return Console.ReadKey(intercept); + } + finally + { + Console.CursorVisible = false; + InputDone.Set(); + } + } + } + + /// + /// Reads a key from the Terminal. + /// + /// The prompt. + /// if set to true [prevent echo]. + /// The console key information. + public static ConsoleKeyInfo ReadKey(string prompt, bool preventEcho = true) + { + if (!IsConsolePresent) return default; + + lock (SyncLock) + { + if (prompt != null) + { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} ", ConsoleColor.White); + } + + var input = ReadKey(true); + var echo = preventEcho ? string.Empty : input.Key.ToString(); + WriteLine(echo); + return input; + } + } + + #endregion + + #region Other Terminal Read Methods + + /// + /// Clears the screen. + /// + public static void Clear() + { + Flush(); + Console.Clear(); + } + + /// + /// Reads a line of text from the console. + /// + /// The read line. + public static string? ReadLine() + { + if (IsConsolePresent == false) return default; + + lock (SyncLock) + { + Flush(); + InputDone.Reset(); + + try + { + Console.CursorVisible = true; + return Console.ReadLine(); + } + finally + { + Console.CursorVisible = false; + InputDone.Set(); + } + } + } + + /// + /// Reads a line from the input. + /// + /// The prompt. + /// The read line. + public static string? ReadLine(string prompt) + { + if (!IsConsolePresent) return null; + + lock (SyncLock) + { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt}: ", ConsoleColor.White); + + return ReadLine(); + } + } + + /// + /// Reads a number from the input. If unable to parse, it returns the default number. + /// + /// The prompt. + /// The default number. + /// + /// Conversions of string representation of a number to its 32-bit signed integer equivalent. + /// + public static int ReadNumber(string prompt, int defaultNumber) + { + if (!IsConsolePresent) return defaultNumber; + + lock (SyncLock) + { + Write($"{GetNowFormatted()}{Settings.UserInputPrefix} << {prompt} (default is {defaultNumber}): ", + ConsoleColor.White); + + var input = ReadLine(); + return int.TryParse(input, out var parsedInt) ? parsedInt : defaultNumber; + } + } + + /// + /// Creates a table prompt where the user can enter an option based on the options dictionary provided. + /// + /// The title. + /// The options. + /// Any key option. + /// + /// A value that identifies the console key that was pressed. + /// + /// options. + public static ConsoleKeyInfo ReadPrompt( + string title, + IDictionary options, + string anyKeyOption) + { + if (!IsConsolePresent) return default; + + if (options == null) + throw new ArgumentNullException(nameof(options)); + + const ConsoleColor textColor = ConsoleColor.White; + var lineLength = Console.WindowWidth; + var lineAlign = -(lineLength - 2); + var textFormat = "{0," + lineAlign + "}"; + + // lock the output as an atomic operation + lock (SyncLock) + { + { + // Top border + Table.TopLeft(); + Table.Horizontal(-lineAlign); + Table.TopRight(); + } + + { + // Title + Table.Vertical(); + var titleText = string.Format(CultureInfo.CurrentCulture, + textFormat, + string.IsNullOrWhiteSpace(title) ? " Select an option from the list below." : $" {title}"); + Write(titleText, textColor); + Table.Vertical(); + } + + { + // Title Bottom + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + } + + // Options + foreach (var kvp in options) + { + Table.Vertical(); + Write(string.Format( + CultureInfo.CurrentCulture, + textFormat, + $" {"[ " + kvp.Key + " ]",-10} {kvp.Value}"), + textColor); + Table.Vertical(); + } + + // Any Key Options + if (string.IsNullOrWhiteSpace(anyKeyOption) == false) + { + Table.Vertical(); + Write(string.Format(CultureInfo.CurrentCulture, textFormat, " "), ConsoleColor.Gray); + Table.Vertical(); + + Table.Vertical(); + Write(string.Format( + CultureInfo.CurrentCulture, + textFormat, + $" {" ",-10} {anyKeyOption}"), + ConsoleColor.Gray); + Table.Vertical(); + } + + { + // Input section + Table.LeftTee(); + Table.Horizontal(lineLength - 2); + Table.RightTee(); + + Table.Vertical(); + Write(string.Format(CultureInfo.CurrentCulture, textFormat, Settings.UserOptionText), + ConsoleColor.Green); + Table.Vertical(); + + Table.BottomLeft(); + Table.Horizontal(lineLength - 2); + Table.BottomRight(); + } + } + + var inputLeft = Settings.UserOptionText.Length + 3; + + SetCursorPosition(inputLeft, CursorTop - 1); + var userInput = ReadKey(true); + Write(userInput.Key.ToString(), ConsoleColor.Gray); + + SetCursorPosition(0, CursorTop + 2); + return userInput; + } + + #endregion + + private static string GetNowFormatted() => + $" {(string.IsNullOrWhiteSpace(TextLogger.LoggingTimeFormat) ? string.Empty : DateTime.Now.ToString(TextLogger.LoggingTimeFormat) + " ")}"; + } +} diff --git a/Swan.Lite/Terminal.Output.cs b/Swan.Lite/Terminal.Output.cs new file mode 100644 index 0000000..d9b8f28 --- /dev/null +++ b/Swan.Lite/Terminal.Output.cs @@ -0,0 +1,97 @@ +using System; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + /// + /// Writes a character a number of times, optionally adding a new line at the end. + /// + /// The character code. + /// The color. + /// The count. + /// if set to true [new line]. + /// The writer flags. + public static void Write(char charCode, ConsoleColor? color = null, int count = 1, bool newLine = false, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + { + lock (SyncLock) + { + var text = new string(charCode, count); + + if (newLine) + { + text += Environment.NewLine; + } + + var buffer = OutputEncoding.GetBytes(text); + var context = new OutputContext + { + OutputColor = color ?? Settings.DefaultColor, + OutputText = OutputEncoding.GetChars(buffer), + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes the specified text in the given color. + /// + /// The text. + /// The color. + /// The writer flags. + public static void Write(string? text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + { + if (text == null) return; + + lock (SyncLock) + { + var buffer = OutputEncoding.GetBytes(text); + var context = new OutputContext + { + OutputColor = color ?? Settings.DefaultColor, + OutputText = OutputEncoding.GetChars(buffer), + OutputWriters = writerFlags, + }; + + EnqueueOutput(context); + } + } + + /// + /// Writes a New Line Sequence to the standard output. + /// + /// The writer flags. + public static void WriteLine(TerminalWriters writerFlags = TerminalWriters.StandardOutput) + => Write(Environment.NewLine, Settings.DefaultColor, writerFlags); + + /// + /// Writes a line of text in the current console foreground color + /// to the standard output. + /// + /// The text. + /// The color. + /// The writer flags. + public static void WriteLine(string text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + => Write($"{text ?? string.Empty}{Environment.NewLine}", color, writerFlags); + + /// + /// As opposed to WriteLine methods, it prepends a Carriage Return character to the text + /// so that the console moves the cursor one position up after the text has been written out. + /// + /// The text. + /// The color. + /// The writer flags. + public static void OverwriteLine(string text, ConsoleColor? color = null, TerminalWriters writerFlags = TerminalWriters.StandardOutput) + { + Write($"\r{text ?? string.Empty}", color, writerFlags); + Flush(); + CursorLeft = 0; + } + } +} diff --git a/Swan.Lite/Terminal.Settings.cs b/Swan.Lite/Terminal.Settings.cs new file mode 100644 index 0000000..20d8118 --- /dev/null +++ b/Swan.Lite/Terminal.Settings.cs @@ -0,0 +1,49 @@ +using System; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user + /// This class is thread-safe :). + /// + public static partial class Terminal + { + /// + /// Terminal global settings. + /// + public static class Settings + { + /// + /// Gets or sets the default output color. + /// + /// + /// The default color. + /// + public static ConsoleColor DefaultColor { get; set; } = Console.ForegroundColor; + + /// + /// Gets the color of the border. + /// + /// + /// The color of the border. + /// + public static ConsoleColor BorderColor { get; } = ConsoleColor.DarkGreen; + + /// + /// Gets or sets the user input prefix. + /// + /// + /// The user input prefix. + /// + public static string UserInputPrefix { get; set; } = "USR"; + + /// + /// Gets or sets the user option text. + /// + /// + /// The user option text. + /// + public static string UserOptionText { get; set; } = " Option: "; + } + } +} diff --git a/Swan.Lite/Terminal.cs b/Swan.Lite/Terminal.cs new file mode 100644 index 0000000..c407e4c --- /dev/null +++ b/Swan.Lite/Terminal.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Concurrent; +using System.Text; +using System.Threading; +using Swan.Threading; + +namespace Swan +{ + /// + /// A console terminal helper to create nicer output and receive input from the user. + /// This class is thread-safe :). + /// + public static partial class Terminal + { + #region Private Declarations + + private const int OutputFlushInterval = 15; + private static readonly ExclusiveTimer DequeueOutputTimer; + private static readonly object SyncLock = new object(); + private static readonly ConcurrentQueue OutputQueue = new ConcurrentQueue(); + + private static readonly ManualResetEventSlim OutputDone = new ManualResetEventSlim(false); + private static readonly ManualResetEventSlim InputDone = new ManualResetEventSlim(true); + + private static bool? _isConsolePresent; + + #endregion + + #region Constructors + + /// + /// Initializes static members of the class. + /// + static Terminal() + { + lock (SyncLock) + { + if (DequeueOutputTimer != null) return; + + if (IsConsolePresent) + { + Console.CursorVisible = false; + } + + // Here we start the output task, fire-and-forget + DequeueOutputTimer = new ExclusiveTimer(DequeueOutputCycle); + DequeueOutputTimer.Resume(OutputFlushInterval); + } + } + + #endregion + + #region Synchronized Cursor Movement + + /// + /// Gets or sets the cursor left position. + /// + /// + /// The cursor left. + /// + public static int CursorLeft + { + get + { + if (IsConsolePresent == false) return -1; + lock (SyncLock) + { + Flush(); + return Console.CursorLeft; + } + } + set + { + if (IsConsolePresent == false) return; + lock (SyncLock) + { + Flush(); + Console.CursorLeft = value; + } + } + } + + /// + /// Gets or sets the cursor top position. + /// + /// + /// The cursor top. + /// + public static int CursorTop + { + get + { + if (IsConsolePresent == false) return -1; + + lock (SyncLock) + { + Flush(); + return Console.CursorTop; + } + } + set + { + if (IsConsolePresent == false) return; + + lock (SyncLock) + { + Flush(); + Console.CursorTop = value; + } + } + } + + #endregion + + #region Properties + + /// + /// Gets a value indicating whether the Console is present. + /// + /// + /// true if this instance is console present; otherwise, false. + /// + public static bool IsConsolePresent + { + get + { + if (_isConsolePresent == null) + { + _isConsolePresent = true; + try + { + var windowHeight = Console.WindowHeight; + _isConsolePresent = windowHeight >= 0; + } + catch + { + _isConsolePresent = false; + } + } + + return _isConsolePresent.Value; + } + } + + /// + /// Gets the available output writers in a bitwise mask. + /// + /// + /// The available writers. + /// + public static TerminalWriters AvailableWriters => + IsConsolePresent + ? TerminalWriters.StandardError | TerminalWriters.StandardOutput + : TerminalWriters.None; + + /// + /// Gets or sets the output encoding for the current console. + /// + /// + /// The output encoding. + /// + public static Encoding OutputEncoding + { + get => Console.OutputEncoding; + set => Console.OutputEncoding = value; + } + + #endregion + + #region Methods + + /// + /// Waits for all of the queued output messages to be written out to the console. + /// Call this method if it is important to display console text before + /// quitting the application such as showing usage or help. + /// Set the timeout to null or TimeSpan.Zero to wait indefinitely. + /// + /// The timeout. Set the amount of time to black before this method exits. + public static void Flush(TimeSpan? timeout = null) + { + if (timeout == null) timeout = TimeSpan.Zero; + var startTime = DateTime.UtcNow; + + while (OutputQueue.Count > 0) + { + // Manually trigger a timer cycle to run immediately + DequeueOutputTimer.Change(0, OutputFlushInterval); + + // Wait for the output to finish + if (OutputDone.Wait(OutputFlushInterval)) + break; + + // infinite timeout + if (timeout.Value == TimeSpan.Zero) + continue; + + // break if we have reached a timeout condition + if (DateTime.UtcNow.Subtract(startTime) >= timeout.Value) + break; + } + } + + /// + /// Sets the cursor position. + /// + /// The left. + /// The top. + public static void SetCursorPosition(int left, int top) + { + if (!IsConsolePresent) return; + + lock (SyncLock) + { + Flush(); + Console.SetCursorPosition(left.Clamp(0, left), top.Clamp(0, top)); + } + } + + /// + /// Moves the output cursor one line up starting at left position 0 + /// Please note that backlining the cursor does not clear the contents of the + /// previous line so you might need to clear it by writing an empty string the + /// length of the console width. + /// + public static void BacklineCursor() => SetCursorPosition(0, CursorTop - 1); + + /// + /// Writes a standard banner to the standard output + /// containing the company name, product name, assembly version and trademark. + /// + /// The color. + public static void WriteWelcomeBanner(ConsoleColor color = ConsoleColor.Gray) + { + WriteLine($"{SwanRuntime.CompanyName} {SwanRuntime.ProductName} [Version {SwanRuntime.EntryAssemblyVersion}]", color); + WriteLine($"{SwanRuntime.ProductTrademark}", color); + } + + /// + /// Enqueues the output to be written to the console + /// This is the only method that should enqueue to the output + /// Please note that if AvailableWriters is None, then no output will be enqueued. + /// + /// The context. + private static void EnqueueOutput(OutputContext context) + { + lock (SyncLock) + { + var availableWriters = AvailableWriters; + + if (availableWriters == TerminalWriters.None || context.OutputWriters == TerminalWriters.None) + { + OutputDone.Set(); + return; + } + + if ((context.OutputWriters & availableWriters) == TerminalWriters.None) + return; + + OutputDone.Reset(); + OutputQueue.Enqueue(context); + } + } + + /// + /// Runs a Terminal I/O cycle in the thread. + /// + private static void DequeueOutputCycle() + { + if (AvailableWriters == TerminalWriters.None) + { + OutputDone.Set(); + return; + } + + InputDone.Wait(); + + if (OutputQueue.Count <= 0) + { + OutputDone.Set(); + return; + } + + OutputDone.Reset(); + + while (OutputQueue.Count > 0) + { + if (!OutputQueue.TryDequeue(out var context)) continue; + + // Process Console output and Skip over stuff we can't display so we don't stress the output too much. + if (!IsConsolePresent) continue; + + Console.ForegroundColor = context.OutputColor; + + // Output to the standard output + if (context.OutputWriters.HasFlag(TerminalWriters.StandardOutput)) + { + Console.Out.Write(context.OutputText); + } + + // output to the standard error + if (context.OutputWriters.HasFlag(TerminalWriters.StandardError)) + { + Console.Error.Write(context.OutputText); + } + + Console.ResetColor(); + Console.ForegroundColor = context.OriginalColor; + } + } + + #endregion + + #region Output Context + + /// + /// Represents an asynchronous output context. + /// + private sealed class OutputContext + { + /// + /// Initializes a new instance of the class. + /// + public OutputContext() + { + OriginalColor = Settings.DefaultColor; + OutputWriters = IsConsolePresent + ? TerminalWriters.StandardOutput + : TerminalWriters.None; + } + + public ConsoleColor OriginalColor { get; } + public ConsoleColor OutputColor { get; set; } + public char[] OutputText { get; set; } + public TerminalWriters OutputWriters { get; set; } + } + + #endregion + } +} diff --git a/Swan.Lite/TerminalWriters.Enums.cs b/Swan.Lite/TerminalWriters.Enums.cs new file mode 100644 index 0000000..b1d4b9d --- /dev/null +++ b/Swan.Lite/TerminalWriters.Enums.cs @@ -0,0 +1,31 @@ +using System; + +namespace Swan +{ + /// + /// Defines a set of bitwise standard terminal writers. + /// + [Flags] + public enum TerminalWriters + { + /// + /// Prevents output + /// + None = 0, + + /// + /// Writes to the Console.Out + /// + StandardOutput = 1, + + /// + /// Writes to the Console.Error + /// + StandardError = 2, + + /// + /// Writes to all possible terminal writers + /// + All = StandardOutput | StandardError, + } +} diff --git a/Swan.Lite/Threading/AtomicBoolean.cs b/Swan.Lite/Threading/AtomicBoolean.cs new file mode 100644 index 0000000..7db2f17 --- /dev/null +++ b/Swan.Lite/Threading/AtomicBoolean.cs @@ -0,0 +1,24 @@ +namespace Swan.Threading +{ + /// + /// Fast, atomic boolean combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicBoolean : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicBoolean(bool initialValue = default) + : base(initialValue ? 1 : 0) + { + // placeholder + } + + /// + protected override bool FromLong(long backingValue) => backingValue != 0; + + /// + protected override long ToLong(bool value) => value ? 1 : 0; + } +} \ No newline at end of file diff --git a/Swan.Lite/Threading/AtomicDateTime.cs b/Swan.Lite/Threading/AtomicDateTime.cs new file mode 100644 index 0000000..8bec976 --- /dev/null +++ b/Swan.Lite/Threading/AtomicDateTime.cs @@ -0,0 +1,26 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Defines an atomic DateTime. + /// + public sealed class AtomicDateTime : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + public AtomicDateTime(DateTime initialValue) + : base(initialValue.Ticks) + { + // placeholder + } + + /// + protected override DateTime FromLong(long backingValue) => new DateTime(backingValue); + + /// + protected override long ToLong(DateTime value) => value.Ticks; + } +} diff --git a/Swan.Lite/Threading/AtomicDouble.cs b/Swan.Lite/Threading/AtomicDouble.cs new file mode 100644 index 0000000..b611a08 --- /dev/null +++ b/Swan.Lite/Threading/AtomicDouble.cs @@ -0,0 +1,28 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Fast, atomic double combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicDouble : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicDouble(double initialValue = default) + : base(BitConverter.DoubleToInt64Bits(initialValue)) + { + // placeholder + } + + /// + protected override double FromLong(long backingValue) => + BitConverter.Int64BitsToDouble(backingValue); + + /// + protected override long ToLong(double value) => + BitConverter.DoubleToInt64Bits(value); + } +} \ No newline at end of file diff --git a/Swan.Lite/Threading/AtomicEnum.cs b/Swan.Lite/Threading/AtomicEnum.cs new file mode 100644 index 0000000..b06af12 --- /dev/null +++ b/Swan.Lite/Threading/AtomicEnum.cs @@ -0,0 +1,43 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Defines an atomic generic Enum. + /// + /// The type of enum. + public sealed class AtomicEnum + where T : struct, IConvertible + { + private long _backingValue; + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + /// T must be an enumerated type. + public AtomicEnum(T initialValue) + { + if (!Enum.IsDefined(typeof(T), initialValue)) + throw new ArgumentException("T must be an enumerated type"); + + Value = initialValue; + } + + /// + /// Gets or sets the value. + /// + public T Value + { + get => (T)Enum.ToObject(typeof(T), BackingValue); + set => BackingValue = Convert.ToInt64(value); + } + + private long BackingValue + { + get => Interlocked.Read(ref _backingValue); + set => Interlocked.Exchange(ref _backingValue, value); + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Threading/AtomicInteger.cs b/Swan.Lite/Threading/AtomicInteger.cs new file mode 100644 index 0000000..5cad25d --- /dev/null +++ b/Swan.Lite/Threading/AtomicInteger.cs @@ -0,0 +1,28 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Represents an atomically readable or writable integer. + /// + public class AtomicInteger : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicInteger(int initialValue = default) + : base(Convert.ToInt64(initialValue)) + { + // placeholder + } + + /// + protected override int FromLong(long backingValue) => + Convert.ToInt32(backingValue); + + /// + protected override long ToLong(int value) => + Convert.ToInt64(value); + } +} \ No newline at end of file diff --git a/Swan.Lite/Threading/AtomicLong.cs b/Swan.Lite/Threading/AtomicLong.cs new file mode 100644 index 0000000..b675187 --- /dev/null +++ b/Swan.Lite/Threading/AtomicLong.cs @@ -0,0 +1,24 @@ +namespace Swan.Threading +{ + /// + /// Fast, atomic long combining interlocked to write value and volatile to read values. + /// + public sealed class AtomicLong : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// if set to true [initial value]. + public AtomicLong(long initialValue = default) + : base(initialValue) + { + // placeholder + } + + /// + protected override long FromLong(long backingValue) => backingValue; + + /// + protected override long ToLong(long value) => value; + } +} diff --git a/Swan.Lite/Threading/AtomicTimeSpan.cs b/Swan.Lite/Threading/AtomicTimeSpan.cs new file mode 100644 index 0000000..ec27877 --- /dev/null +++ b/Swan.Lite/Threading/AtomicTimeSpan.cs @@ -0,0 +1,26 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Represents an atomic TimeSpan type. + /// + public sealed class AtomicTimeSpan : AtomicTypeBase + { + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + public AtomicTimeSpan(TimeSpan initialValue) + : base(initialValue.Ticks) + { + // placeholder + } + + /// + protected override TimeSpan FromLong(long backingValue) => TimeSpan.FromTicks(backingValue); + + /// + protected override long ToLong(TimeSpan value) => value.Ticks < 0 ? 0 : value.Ticks; + } +} diff --git a/Swan.Lite/Threading/AtomicTypeBase.cs b/Swan.Lite/Threading/AtomicTypeBase.cs new file mode 100644 index 0000000..fae6cd3 --- /dev/null +++ b/Swan.Lite/Threading/AtomicTypeBase.cs @@ -0,0 +1,243 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Provides a generic implementation of an Atomic (interlocked) type + /// + /// Idea taken from Memory model and .NET operations in article: + /// http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/. + /// + /// The structure type backed by a 64-bit value. + public abstract class AtomicTypeBase : IComparable, IComparable, IComparable>, IEquatable, IEquatable> + where T : struct, IComparable, IComparable, IEquatable + { + private long _backingValue; + + /// + /// Initializes a new instance of the class. + /// + /// The initial value. + protected AtomicTypeBase(long initialValue) + { + BackingValue = initialValue; + } + + /// + /// Gets or sets the value. + /// + public T Value + { + get => FromLong(BackingValue); + set => BackingValue = ToLong(value); + } + + /// + /// Gets or sets the backing value. + /// + protected long BackingValue + { + get => Interlocked.Read(ref _backingValue); + set => Interlocked.Exchange(ref _backingValue, value); + } + + /// + /// Implements the operator ==. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator ==(AtomicTypeBase a, T b) => a?.Equals(b) == true; + + /// + /// Implements the operator !=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator !=(AtomicTypeBase a, T b) => a?.Equals(b) == false; + + /// + /// Implements the operator >. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator >(AtomicTypeBase a, T b) => a.CompareTo(b) > 0; + + /// + /// Implements the operator <. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator <(AtomicTypeBase a, T b) => a.CompareTo(b) < 0; + + /// + /// Implements the operator >=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator >=(AtomicTypeBase a, T b) => a.CompareTo(b) >= 0; + + /// + /// Implements the operator <=. + /// + /// a. + /// The b. + /// + /// The result of the operator. + /// + public static bool operator <=(AtomicTypeBase a, T b) => a.CompareTo(b) <= 0; + + /// + /// Implements the operator ++. + /// + /// The instance. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator ++(AtomicTypeBase instance) + { + Interlocked.Increment(ref instance._backingValue); + return instance; + } + + /// + /// Implements the operator --. + /// + /// The instance. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator --(AtomicTypeBase instance) + { + Interlocked.Decrement(ref instance._backingValue); + return instance; + } + + /// + /// Implements the operator -<. + /// + /// The instance. + /// The operand. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator +(AtomicTypeBase instance, long operand) + { + instance.BackingValue = instance.BackingValue + operand; + return instance; + } + + /// + /// Implements the operator -. + /// + /// The instance. + /// The operand. + /// + /// The result of the operator. + /// + public static AtomicTypeBase operator -(AtomicTypeBase instance, long operand) + { + instance.BackingValue = instance.BackingValue - operand; + return instance; + } + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + /// When types are incompatible. + public int CompareTo(object other) + { + switch (other) + { + case null: + return 1; + case AtomicTypeBase atomic: + return BackingValue.CompareTo(atomic.BackingValue); + case T variable: + return Value.CompareTo(variable); + } + + throw new ArgumentException("Incompatible comparison types"); + } + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + public int CompareTo(T other) => Value.CompareTo(other); + + /// + /// Compares the value to the other instance. + /// + /// The other instance. + /// 0 if equal, 1 if this instance is greater, -1 if this instance is less than. + public int CompareTo(AtomicTypeBase other) => BackingValue.CompareTo(other?.BackingValue ?? default); + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object other) + { + switch (other) + { + case AtomicTypeBase atomic: + return Equals(atomic); + case T variable: + return Equals(variable); + } + + return false; + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() => BackingValue.GetHashCode(); + + /// + public bool Equals(AtomicTypeBase other) => + BackingValue == (other?.BackingValue ?? default); + + /// + public bool Equals(T other) => Equals(Value, other); + + /// + /// Converts from a long value to the target type. + /// + /// The backing value. + /// The value converted form a long value. + protected abstract T FromLong(long backingValue); + + /// + /// Converts from the target type to a long value. + /// + /// The value. + /// The value converted to a long value. + protected abstract long ToLong(T value); + } +} diff --git a/Swan.Lite/Threading/CancellationTokenOwner.cs b/Swan.Lite/Threading/CancellationTokenOwner.cs new file mode 100644 index 0000000..d09ec0c --- /dev/null +++ b/Swan.Lite/Threading/CancellationTokenOwner.cs @@ -0,0 +1,68 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Acts as a but with reusable tokens. + /// + public sealed class CancellationTokenOwner : IDisposable + { + private readonly object _syncLock = new object(); + private bool _isDisposed; + private CancellationTokenSource _tokenSource = new CancellationTokenSource(); + + /// + /// Gets the token of the current. + /// + public CancellationToken Token + { + get + { + lock (_syncLock) + { + return _isDisposed + ? CancellationToken.None + : _tokenSource.Token; + } + } + } + + /// + /// Cancels the last referenced token and creates a new token source. + /// + public void Cancel() + { + lock (_syncLock) + { + if (_isDisposed) return; + _tokenSource.Cancel(); + _tokenSource.Dispose(); + _tokenSource = new CancellationTokenSource(); + } + } + + /// + public void Dispose() => Dispose(true); + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(bool disposing) + { + lock (_syncLock) + { + if (_isDisposed) return; + + if (disposing) + { + _tokenSource.Cancel(); + _tokenSource.Dispose(); + } + + _isDisposed = true; + } + } + } +} diff --git a/Swan.Lite/Threading/ExclusiveTimer.cs b/Swan.Lite/Threading/ExclusiveTimer.cs new file mode 100644 index 0000000..5a214ff --- /dev/null +++ b/Swan.Lite/Threading/ExclusiveTimer.cs @@ -0,0 +1,236 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// A threading implementation that executes at most one cycle at a time + /// in a thread. Callback execution is NOT guaranteed to be carried out + /// on the same thread every time the timer fires. + /// + public sealed class ExclusiveTimer : IDisposable + { + private readonly object _syncLock = new object(); + private readonly ManualResetEventSlim _cycleDoneEvent = new ManualResetEventSlim(true); + private readonly Timer _backingTimer; + private readonly TimerCallback _userCallback; + private readonly AtomicBoolean _isDisposing = new AtomicBoolean(); + private readonly AtomicBoolean _isDisposed = new AtomicBoolean(); + private int _period; + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The state. + /// The due time. + /// The period. + public ExclusiveTimer(TimerCallback timerCallback, object state, int dueTime, int period) + { + _period = period; + _userCallback = timerCallback; + _backingTimer = new Timer(InternalCallback, state ?? this, dueTime, Timeout.Infinite); + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The state. + /// The due time. + /// The period. + public ExclusiveTimer(TimerCallback timerCallback, object state, TimeSpan dueTime, TimeSpan period) + : this(timerCallback, state, Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds)) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + public ExclusiveTimer(TimerCallback timerCallback) + : this(timerCallback, null, Timeout.Infinite, Timeout.Infinite) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The due time. + /// The period. + public ExclusiveTimer(Action timerCallback, int dueTime, int period) + : this(s => { timerCallback?.Invoke(); }, null, dueTime, period) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + /// The due time. + /// The period. + public ExclusiveTimer(Action timerCallback, TimeSpan dueTime, TimeSpan period) + : this(s => { timerCallback?.Invoke(); }, null, dueTime, period) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The timer callback. + public ExclusiveTimer(Action timerCallback) + : this(timerCallback, Timeout.Infinite, Timeout.Infinite) + { + // placeholder + } + + /// + /// Gets a value indicating whether this instance is disposing. + /// + /// + /// true if this instance is disposing; otherwise, false. + /// + public bool IsDisposing => _isDisposing.Value; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + public bool IsDisposed => _isDisposed.Value; + + /// + /// Waits until the time is elapsed. + /// + /// The until date. + /// The cancellation token. + public static void WaitUntil(DateTime untilDate, CancellationToken cancellationToken = default) + { + void Callback(IWaitEvent waitEvent) + { + try + { + waitEvent.Complete(); + waitEvent.Begin(); + } + catch + { + // ignore + } + } + + using (var delayLock = WaitEventFactory.Create(true)) + { + using (var _ = new ExclusiveTimer(() => Callback(delayLock), 0, 15)) + { + while (!cancellationToken.IsCancellationRequested && DateTime.UtcNow < untilDate) + delayLock.Wait(); + } + } + } + + /// + /// Waits the specified wait time. + /// + /// The wait time. + /// The cancellation token. + public static void Wait(TimeSpan waitTime, CancellationToken cancellationToken = default) => + WaitUntil(DateTime.UtcNow.Add(waitTime), cancellationToken); + + /// + /// Changes the start time and the interval between method invocations for the internal timer. + /// + /// The due time. + /// The period. + public void Change(int dueTime, int period) + { + _period = period; + + _backingTimer.Change(dueTime, Timeout.Infinite); + } + + /// + /// Changes the start time and the interval between method invocations for the internal timer. + /// + /// The due time. + /// The period. + public void Change(TimeSpan dueTime, TimeSpan period) + => Change(Convert.ToInt32(dueTime.TotalMilliseconds), Convert.ToInt32(period.TotalMilliseconds)); + + /// + /// Changes the interval between method invocations for the internal timer. + /// + /// The period. + public void Resume(int period) => Change(0, period); + + /// + /// Changes the interval between method invocations for the internal timer. + /// + /// The period. + public void Resume(TimeSpan period) => Change(TimeSpan.Zero, period); + + /// + /// Pauses this instance. + /// + public void Pause() => Change(Timeout.Infinite, Timeout.Infinite); + + /// + public void Dispose() + { + lock (_syncLock) + { + if (_isDisposed == true || _isDisposing == true) + return; + + _isDisposing.Value = true; + } + + try + { + _cycleDoneEvent.Wait(); + _cycleDoneEvent.Dispose(); + Pause(); + _backingTimer.Dispose(); + } + finally + { + _isDisposed.Value = true; + _isDisposing.Value = false; + } + } + + /// + /// Logic that runs every time the timer hits the due time. + /// + /// The state. + private void InternalCallback(object state) + { + lock (_syncLock) + { + if (IsDisposed || IsDisposing) + return; + } + + if (_cycleDoneEvent.IsSet == false) + return; + + _cycleDoneEvent.Reset(); + + try + { + _userCallback(state); + } + finally + { + _cycleDoneEvent?.Set(); + _backingTimer?.Change(_period, Timeout.Infinite); + } + } + } +} diff --git a/Swan.Lite/Threading/ISyncLocker.cs b/Swan.Lite/Threading/ISyncLocker.cs new file mode 100644 index 0000000..be75c8d --- /dev/null +++ b/Swan.Lite/Threading/ISyncLocker.cs @@ -0,0 +1,24 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Defines a generic interface for synchronized locking mechanisms. + /// + public interface ISyncLocker : IDisposable + { + /// + /// Acquires a writer lock. + /// The lock is released when the returned locking object is disposed. + /// + /// A disposable locking object. + IDisposable AcquireWriterLock(); + + /// + /// Acquires a reader lock. + /// The lock is released when the returned locking object is disposed. + /// + /// A disposable locking object. + IDisposable AcquireReaderLock(); + } +} diff --git a/Swan.Lite/Threading/IWaitEvent.cs b/Swan.Lite/Threading/IWaitEvent.cs new file mode 100644 index 0000000..6b949e8 --- /dev/null +++ b/Swan.Lite/Threading/IWaitEvent.cs @@ -0,0 +1,57 @@ +using System; + +namespace Swan.Threading +{ + /// + /// Provides a generalized API for ManualResetEvent and ManualResetEventSlim. + /// + /// + public interface IWaitEvent : IDisposable + { + /// + /// Gets a value indicating whether the event is in the completed state. + /// + bool IsCompleted { get; } + + /// + /// Gets a value indicating whether the Begin method has been called. + /// It returns false after the Complete method is called. + /// + bool IsInProgress { get; } + + /// + /// Returns true if the underlying handle is not closed and it is still valid. + /// + bool IsValid { get; } + + /// + /// Gets a value indicating whether this instance is disposed. + /// + bool IsDisposed { get; } + + /// + /// Enters the state in which waiters need to wait. + /// All future waiters will block when they call the Wait method. + /// + void Begin(); + + /// + /// Leaves the state in which waiters need to wait. + /// All current waiters will continue. + /// + void Complete(); + + /// + /// Waits for the event to be completed. + /// + void Wait(); + + /// + /// Waits for the event to be completed. + /// Returns true when there was no timeout. False if the timeout was reached. + /// + /// The maximum amount of time to wait for. + /// true when there was no timeout. false if the timeout was reached. + bool Wait(TimeSpan timeout); + } +} diff --git a/Swan.Lite/Threading/IWorker.cs b/Swan.Lite/Threading/IWorker.cs new file mode 100644 index 0000000..f70c60c --- /dev/null +++ b/Swan.Lite/Threading/IWorker.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; + +namespace Swan.Threading +{ + /// + /// Defines a standard API to control background application workers. + /// + public interface IWorker + { + /// + /// Gets the current state of the worker. + /// + WorkerState WorkerState { get; } + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// + /// true if this instance is disposed; otherwise, false. + /// + bool IsDisposed { get; } + + /// + /// Gets a value indicating whether this instance is currently being disposed. + /// + /// + /// true if this instance is disposing; otherwise, false. + /// + bool IsDisposing { get; } + + /// + /// Gets or sets the time interval used to execute cycles. + /// + TimeSpan Period { get; set; } + + /// + /// Gets the name identifier of this worker. + /// + string Name { get; } + + /// + /// Starts execution of worker cycles. + /// + /// The awaitable task. + Task StartAsync(); + + /// + /// Pauses execution of worker cycles. + /// + /// The awaitable task. + Task PauseAsync(); + + /// + /// Resumes execution of worker cycles. + /// + /// The awaitable task. + Task ResumeAsync(); + + /// + /// Permanently stops execution of worker cycles. + /// An interrupt is always sent to the worker. If you wish to stop + /// the worker without interrupting then call the + /// method, await it, and finally call the method. + /// + /// The awaitable task. + Task StopAsync(); + } +} diff --git a/Swan.Lite/Threading/IWorkerDelayProvider.cs b/Swan.Lite/Threading/IWorkerDelayProvider.cs new file mode 100644 index 0000000..8868c14 --- /dev/null +++ b/Swan.Lite/Threading/IWorkerDelayProvider.cs @@ -0,0 +1,21 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace Swan.Threading +{ + /// + /// An interface for a worker cycle delay provider. + /// + public interface IWorkerDelayProvider + { + /// + /// Suspends execution queues a new cycle for execution. The delay is given in + /// milliseconds. When overridden in a derived class the wait handle will be set + /// whenever an interrupt is received. + /// + /// The remaining delay to wait for in the cycle. + /// Contains a reference to a task with the scheduled period delay. + /// The cancellation token to cancel waiting. + void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token); + } +} diff --git a/Swan.Lite/Threading/PeriodicTask.cs b/Swan.Lite/Threading/PeriodicTask.cs new file mode 100644 index 0000000..11aaf37 --- /dev/null +++ b/Swan.Lite/Threading/PeriodicTask.cs @@ -0,0 +1,101 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Swan.Logging; + +namespace Swan.Threading +{ + /// + /// Schedule an action to be periodically executed on the thread pool. + /// + public sealed class PeriodicTask : IDisposable + { + /// + /// The minimum interval between action invocations. + /// The value of this field is equal to 100 milliseconds. + /// + public static readonly TimeSpan MinInterval = TimeSpan.FromMilliseconds(100); + + private readonly Func _action; + private readonly CancellationTokenSource _cancellationTokenSource; + + private TimeSpan _interval; + + /// + /// Initializes a new instance of the class. + /// + /// The interval between invocations of . + /// The callback to invoke periodically. + /// A that can be used to cancel operations. + public PeriodicTask(TimeSpan interval, Func action, CancellationToken cancellationToken = default) + { + _action = action ?? throw new ArgumentNullException(nameof(action)); + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _interval = ValidateInterval(interval); + + Task.Run(ActionLoop); + } + + /// + /// Finalizes an instance of the class. + /// + ~PeriodicTask() + { + Dispose(false); + } + + /// + /// Gets or sets the interval between periodic action invocations. + /// Changes to this property take effect after next action invocation. + /// + /// + public TimeSpan Interval + { + get => _interval; + set => _interval = ValidateInterval(value); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + } + + private static TimeSpan ValidateInterval(TimeSpan value) + => value < MinInterval ? MinInterval : value; + + private async Task ActionLoop() + { + for (; ; ) + { + try + { + await Task.Delay(Interval, _cancellationTokenSource.Token).ConfigureAwait(false); + await _action(_cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) when (_cancellationTokenSource.IsCancellationRequested) + { + break; + } + catch (TaskCanceledException) when (_cancellationTokenSource.IsCancellationRequested) + { + break; + } + catch (Exception ex) + { + ex.Log(nameof(PeriodicTask)); + } + } + } + } +} diff --git a/Swan.Lite/Threading/RunnerBase.cs b/Swan.Lite/Threading/RunnerBase.cs new file mode 100644 index 0000000..a3aec78 --- /dev/null +++ b/Swan.Lite/Threading/RunnerBase.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using Swan.Configuration; +using Swan.Logging; + +namespace Swan.Threading +{ + /// + /// Represents an background worker abstraction with a life cycle and running at a independent thread. + /// + public abstract class RunnerBase : ConfiguredObject + { + private Thread _worker; + private CancellationTokenSource _cancelTokenSource; + private ManualResetEvent _workFinished; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is enabled]. + protected RunnerBase(bool isEnabled) + { + Name = GetType().Name; + IsEnabled = isEnabled; + } + + /// + /// Gets the error messages. + /// + /// + /// The error messages. + /// + public List ErrorMessages { get; } = new List(); + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public string Name { get; } + + /// + /// Gets a value indicating whether this instance is running. + /// + /// + /// true if this instance is running; otherwise, false. + /// + public bool IsRunning { get; private set; } + + /// + /// Gets a value indicating whether this instance is enabled. + /// + /// + /// true if this instance is enabled; otherwise, false. + /// + public bool IsEnabled { get; } + + /// + /// Starts this instance. + /// + public virtual void Start() + { + if (IsEnabled == false) + return; + + "Start Requested".Debug(Name); + _cancelTokenSource = new CancellationTokenSource(); + _workFinished = new ManualResetEvent(false); + + _worker = new Thread(() => + { + _workFinished.Reset(); + IsRunning = true; + try + { + Setup(); + DoBackgroundWork(_cancelTokenSource.Token); + } + catch (ThreadAbortException) + { + $"{nameof(ThreadAbortException)} caught.".Warn(Name); + } + catch (Exception ex) + { + $"{ex.GetType()}: {ex.Message}\r\n{ex.StackTrace}".Error(Name); + } + finally + { + Cleanup(); + _workFinished?.Set(); + IsRunning = false; + "Stopped Completely".Debug(Name); + } + }) + { + IsBackground = true, + Name = $"{Name}Thread", + }; + + _worker.Start(); + } + + /// + /// Stops this instance. + /// + public virtual void Stop() + { + if (IsEnabled == false || IsRunning == false) + return; + + "Stop Requested".Debug(Name); + _cancelTokenSource.Cancel(); + var waitRetries = 5; + while (waitRetries >= 1) + { + if (_workFinished.WaitOne(250)) + { + waitRetries = -1; + break; + } + + waitRetries--; + } + + if (waitRetries < 0) + { + "Workbench stopped gracefully".Debug(Name); + } + else + { + "Did not respond to stop request. Aborting thread and waiting . . .".Warn(Name); + _worker.Abort(); + + if (_workFinished.WaitOne(5000) == false) + "Waited and no response. Worker might have been left in an inconsistent state.".Error(Name); + else + "Waited for worker and it finally responded (OK).".Debug(Name); + } + + _workFinished.Dispose(); + _workFinished = null; + } + + /// + /// Setups this instance. + /// + protected void Setup() + { + EnsureConfigurationNotLocked(); + OnSetup(); + LockConfiguration(); + } + + /// + /// Cleanups this instance. + /// + protected virtual void Cleanup() + { + // empty + } + + /// + /// Called when [setup]. + /// + protected virtual void OnSetup() + { + // empty + } + + /// + /// Does the background work. + /// + /// The ct. + protected abstract void DoBackgroundWork(CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/Swan.Lite/Threading/SyncLockerFactory.cs b/Swan.Lite/Threading/SyncLockerFactory.cs new file mode 100644 index 0000000..1d3ac94 --- /dev/null +++ b/Swan.Lite/Threading/SyncLockerFactory.cs @@ -0,0 +1,185 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Provides factory methods to create synchronized reader-writer locks + /// that support a generalized locking and releasing api and syntax. + /// + public static class SyncLockerFactory + { + #region Enums and Interfaces + + /// + /// Enumerates the locking operations. + /// + private enum LockHolderType + { + Read, + Write, + } + + /// + /// Defines methods for releasing locks. + /// + private interface ISyncReleasable + { + /// + /// Releases the writer lock. + /// + void ReleaseWriterLock(); + + /// + /// Releases the reader lock. + /// + void ReleaseReaderLock(); + } + + #endregion + + #region Factory Methods + + /// + /// Creates a reader-writer lock backed by a standard ReaderWriterLock. + /// + /// The synchronized locker. + public static ISyncLocker Create() => new SyncLocker(); + + /// + /// Creates a reader-writer lock backed by a ReaderWriterLockSlim. + /// + /// The synchronized locker. + public static ISyncLocker CreateSlim() => new SyncLockerSlim(); + + /// + /// Creates a reader-writer lock. + /// + /// if set to true it uses the Slim version of a reader-writer lock. + /// The Sync Locker. + public static ISyncLocker Create(bool useSlim) => useSlim ? CreateSlim() : Create(); + + #endregion + + #region Private Classes + + /// + /// The lock releaser. Calling the dispose method releases the lock entered by the parent SyncLocker. + /// + /// + private sealed class SyncLockReleaser : IDisposable + { + private readonly ISyncReleasable _parent; + private readonly LockHolderType _operation; + + private bool _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The parent. + /// The operation. + public SyncLockReleaser(ISyncReleasable parent, LockHolderType operation) + { + _parent = parent; + _operation = operation; + } + + /// + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + + if (_operation == LockHolderType.Read) + _parent.ReleaseReaderLock(); + else + _parent.ReleaseWriterLock(); + } + } + + /// + /// The Sync Locker backed by a ReaderWriterLock. + /// + /// + /// + private sealed class SyncLocker : ISyncLocker, ISyncReleasable + { + private bool _isDisposed; + private ReaderWriterLock _locker = new ReaderWriterLock(); + + /// + public IDisposable AcquireReaderLock() + { + _locker?.AcquireReaderLock(Timeout.Infinite); + return new SyncLockReleaser(this, LockHolderType.Read); + } + + /// + public IDisposable AcquireWriterLock() + { + _locker?.AcquireWriterLock(Timeout.Infinite); + return new SyncLockReleaser(this, LockHolderType.Write); + } + + /// + public void ReleaseWriterLock() => _locker?.ReleaseWriterLock(); + + /// + public void ReleaseReaderLock() => _locker?.ReleaseReaderLock(); + + /// + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _locker?.ReleaseLock(); + _locker = null; + } + } + + /// + /// The Sync Locker backed by ReaderWriterLockSlim. + /// + /// + /// + private sealed class SyncLockerSlim : ISyncLocker, ISyncReleasable + { + private bool _isDisposed; + + private ReaderWriterLockSlim _locker + = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); + + /// + public IDisposable AcquireReaderLock() + { + _locker?.EnterReadLock(); + return new SyncLockReleaser(this, LockHolderType.Read); + } + + /// + public IDisposable AcquireWriterLock() + { + _locker?.EnterWriteLock(); + return new SyncLockReleaser(this, LockHolderType.Write); + } + + /// + public void ReleaseWriterLock() => _locker?.ExitWriteLock(); + + /// + public void ReleaseReaderLock() => _locker?.ExitReadLock(); + + /// + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _locker?.Dispose(); + _locker = null; + } + } + + #endregion + } +} \ No newline at end of file diff --git a/Swan.Lite/Threading/WaitEventFactory.cs b/Swan.Lite/Threading/WaitEventFactory.cs new file mode 100644 index 0000000..1c098c0 --- /dev/null +++ b/Swan.Lite/Threading/WaitEventFactory.cs @@ -0,0 +1,219 @@ +using System; +using System.Threading; + +namespace Swan.Threading +{ + /// + /// Provides a Manual Reset Event factory with a unified API. + /// + /// + /// The following example shows how to use the WaitEventFactory class. + /// + /// using Swan.Threading; + /// + /// public class Example + /// { + /// // create a WaitEvent using the slim version + /// private static readonly IWaitEvent waitEvent = WaitEventFactory.CreateSlim(false); + /// + /// public static void Main() + /// { + /// Task.Factory.StartNew(() => + /// { + /// DoWork(1); + /// }); + /// + /// Task.Factory.StartNew(() => + /// { + /// DoWork(2); + /// }); + /// + /// // send first signal + /// waitEvent.Complete(); + /// waitEvent.Begin(); + /// + /// Thread.Sleep(TimeSpan.FromSeconds(2)); + /// + /// // send second signal + /// waitEvent.Complete(); + /// + /// Terminal.Readline(); + /// } + /// + /// public static void DoWork(int taskNumber) + /// { + /// $"Data retrieved:{taskNumber}".WriteLine(); + /// waitEvent.Wait(); + /// + /// Thread.Sleep(TimeSpan.FromSeconds(2)); + /// $"All finished up {taskNumber}".WriteLine(); + /// } + /// } + /// + /// + public static class WaitEventFactory + { + #region Factory Methods + + /// + /// Creates a Wait Event backed by a standard ManualResetEvent. + /// + /// if initially set to completed. Generally true. + /// The Wait Event. + public static IWaitEvent Create(bool isCompleted) => new WaitEvent(isCompleted); + + /// + /// Creates a Wait Event backed by a ManualResetEventSlim. + /// + /// if initially set to completed. Generally true. + /// The Wait Event. + public static IWaitEvent CreateSlim(bool isCompleted) => new WaitEventSlim(isCompleted); + + /// + /// Creates a Wait Event backed by a ManualResetEventSlim. + /// + /// if initially set to completed. Generally true. + /// if set to true creates a slim version of the wait event. + /// The Wait Event. + public static IWaitEvent Create(bool isCompleted, bool useSlim) => useSlim ? CreateSlim(isCompleted) : Create(isCompleted); + + #endregion + + #region Backing Classes + + /// + /// Defines a WaitEvent backed by a ManualResetEvent. + /// + private class WaitEvent : IWaitEvent + { + private ManualResetEvent _event; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is completed]. + public WaitEvent(bool isCompleted) + { + _event = new ManualResetEvent(isCompleted); + } + + /// + public bool IsDisposed { get; private set; } + + /// + public bool IsValid + { + get + { + if (IsDisposed || _event == null) + return false; + + if (_event?.SafeWaitHandle?.IsClosed ?? true) + return false; + + return !(_event?.SafeWaitHandle?.IsInvalid ?? true); + } + } + + /// + public bool IsCompleted + { + get + { + if (IsValid == false) + return true; + + return _event?.WaitOne(0) ?? true; + } + } + + /// + public bool IsInProgress => !IsCompleted; + + /// + public void Begin() => _event?.Reset(); + + /// + public void Complete() => _event?.Set(); + + /// + void IDisposable.Dispose() + { + if (IsDisposed) return; + IsDisposed = true; + + _event?.Set(); + _event?.Dispose(); + _event = null; + } + + /// + public void Wait() => _event?.WaitOne(); + + /// + public bool Wait(TimeSpan timeout) => _event?.WaitOne(timeout) ?? true; + } + + /// + /// Defines a WaitEvent backed by a ManualResetEventSlim. + /// + private class WaitEventSlim : IWaitEvent + { + private ManualResetEventSlim _event; + + /// + /// Initializes a new instance of the class. + /// + /// if set to true [is completed]. + public WaitEventSlim(bool isCompleted) + { + _event = new ManualResetEventSlim(isCompleted); + } + + /// + public bool IsDisposed { get; private set; } + + /// + public bool IsValid + { + get + { + if (IsDisposed || _event?.WaitHandle?.SafeWaitHandle == null) return false; + + return !_event.WaitHandle.SafeWaitHandle.IsClosed && !_event.WaitHandle.SafeWaitHandle.IsInvalid; + } + } + + /// + public bool IsCompleted => IsValid == false || _event.IsSet; + + /// + public bool IsInProgress => !IsCompleted; + + /// + public void Begin() => _event?.Reset(); + + /// + public void Complete() => _event?.Set(); + + /// + void IDisposable.Dispose() + { + if (IsDisposed) return; + IsDisposed = true; + + _event?.Set(); + _event?.Dispose(); + _event = null; + } + + /// + public void Wait() => _event?.Wait(); + + /// + public bool Wait(TimeSpan timeout) => _event?.Wait(timeout) ?? true; + } + + #endregion + } +} \ No newline at end of file diff --git a/Swan.Lite/Threading/WorkerState.cs b/Swan.Lite/Threading/WorkerState.cs new file mode 100644 index 0000000..6e5feee --- /dev/null +++ b/Swan.Lite/Threading/WorkerState.cs @@ -0,0 +1,33 @@ +namespace Swan.Threading +{ + /// + /// Enumerates the different states in which a worker can be. + /// + public enum WorkerState + { + /// + /// The worker has been created and it is ready to start. + /// + Created, + + /// + /// The worker is running it cycle logic. + /// + Running, + + /// + /// The worker is running its delay logic. + /// + Waiting, + + /// + /// The worker is in the paused or suspended state. + /// + Paused, + + /// + /// The worker is stopped and ready for disposal. + /// + Stopped, + } +} diff --git a/Swan.Lite/Validators/IValidator.cs b/Swan.Lite/Validators/IValidator.cs new file mode 100644 index 0000000..eb30992 --- /dev/null +++ b/Swan.Lite/Validators/IValidator.cs @@ -0,0 +1,21 @@ +namespace Swan.Validators +{ + /// + /// A simple Validator interface. + /// + public interface IValidator + { + /// + /// The error message. + /// + string ErrorMessage { get; } + + /// + /// Checks if a value is valid. + /// + /// The type. + /// The value. + /// True if it is valid.False if it is not. + bool IsValid(T value); + } +} \ No newline at end of file diff --git a/Swan.Lite/Validators/ObjectValidationResult.cs b/Swan.Lite/Validators/ObjectValidationResult.cs new file mode 100644 index 0000000..69519ed --- /dev/null +++ b/Swan.Lite/Validators/ObjectValidationResult.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Swan.Validators +{ + /// + /// Defines a validation result containing all validation errors and their properties. + /// + public class ObjectValidationResult + { + private readonly List _errors = new List(); + + /// + /// A list of errors. + /// + public IReadOnlyList Errors => _errors; + + /// + /// true if there are no errors; otherwise, false. + /// + public bool IsValid => !Errors.Any(); + + /// + /// Adds an error with a specified property name. + /// + /// The property name. + /// The error message. + public void Add(string propertyName, string errorMessage) => + _errors.Add(new ValidationError { ErrorMessage = errorMessage, PropertyName = propertyName }); + + /// + /// Defines a validation error. + /// + public class ValidationError + { + /// + /// The property name. + /// + public string PropertyName { get; internal set; } + + /// + /// The message error. + /// + public string ErrorMessage { get; internal set; } + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Validators/ObjectValidator.cs b/Swan.Lite/Validators/ObjectValidator.cs new file mode 100644 index 0000000..933d97b --- /dev/null +++ b/Swan.Lite/Validators/ObjectValidator.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using Swan.Reflection; + +namespace Swan.Validators +{ + /// + /// Represents an object validator. + /// + /// + /// The following code describes how to perform a simple object validation. + /// + /// using Swan.Validators; + /// + /// class Example + /// { + /// public static void Main() + /// { + /// // create an instance of ObjectValidator + /// var obj = new ObjectValidator(); + /// + /// // Add a validation to the 'Simple' class with a custom error message + /// obj.AddValidator<Simple>(x => + /// !string.IsNullOrEmpty(x.Name), "Name must not be empty"); + /// + /// // check if object is valid + /// var res = obj.IsValid(new Simple { Name = "Name" }); + /// } + /// + /// class Simple + /// { + /// public string Name { get; set; } + /// } + /// } + /// + /// + /// The following code shows of to validate an object with a custom validator and some attributes using the Runtime ObjectValidator singleton. + /// + /// using Swan.Validators; + /// + /// class Example + /// { + /// public static void Main() + /// { + /// // create an instance of ObjectValidator + /// Runtime.ObjectValidator + /// .AddValidator<Simple>(x => + /// !x.Name.Equals("Name"), "Name must not be 'Name'"); + /// + /// // validate object + /// var res = Runtime.ObjectValidator + /// .Validate(new Simple{ Name = "name", Number = 5, Email ="email@mail.com"}) + /// } + /// + /// class Simple + /// { + /// [NotNull] + /// public string Name { get; set; } + /// + /// [Range(1, 10)] + /// public int Number { get; set; } + /// + /// [Email] + /// public string Email { get; set; } + /// } + /// } + /// + /// + public class ObjectValidator + { + private static readonly Lazy LazyInstance = new Lazy(() => new ObjectValidator()); + + private readonly ConcurrentDictionary>> _predicates = + new ConcurrentDictionary>>(); + + /// + /// Gets the current. + /// + /// + /// The current. + /// + public static ObjectValidator Current => LazyInstance.Value; + + /// + /// Validates an object given the specified validators and attributes. + /// + /// The type of the object. + /// The object. + /// A validation result. + public ObjectValidationResult Validate(T target) + { + var errorList = new ObjectValidationResult(); + ValidateObject(target, false, errorList.Add); + + return errorList; + } + + /// + /// Validates an object given the specified validators and attributes. + /// + /// The type. + /// The object. + /// + /// true if the specified object is valid; otherwise, false. + /// + /// obj. + public bool IsValid(T target) => ValidateObject(target); + + /// + /// Adds a validator to a specific class. + /// + /// The type of the object. + /// The predicate that will be evaluated. + /// The message. + /// + /// predicate + /// or + /// message. + /// + public void AddValidator(Predicate predicate, string message) + where T : class + { + if (predicate == null) + throw new ArgumentNullException(nameof(predicate)); + + if (string.IsNullOrEmpty(message)) + throw new ArgumentNullException(message); + + if (!_predicates.TryGetValue(typeof(T), out var existing)) + { + existing = new List>(); + _predicates[typeof(T)] = existing; + } + + existing.Add(Tuple.Create((Delegate)predicate, message)); + } + + private bool ValidateObject(T obj, bool returnOnError = true, Action action = null) + { + if (Equals(obj, null)) + throw new ArgumentNullException(nameof(obj)); + + if (_predicates.ContainsKey(typeof(T))) + { + foreach (var (@delegate, value) in _predicates[typeof(T)]) + { + if ((bool)@delegate.DynamicInvoke(obj)) continue; + + action?.Invoke(string.Empty, value); + if (returnOnError) return false; + } + } + + var properties = AttributeCache.DefaultCache.Value.RetrieveFromType(); + + foreach (var prop in properties) + { + foreach (var attribute in prop.Value) + { + var val = (IValidator)attribute; + + if (val.IsValid(prop.Key.GetValue(obj, null))) continue; + + action?.Invoke(prop.Key.Name, val.ErrorMessage); + if (returnOnError) return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/Swan.Lite/Validators/Validators.cs b/Swan.Lite/Validators/Validators.cs new file mode 100644 index 0000000..5cf0692 --- /dev/null +++ b/Swan.Lite/Validators/Validators.cs @@ -0,0 +1,132 @@ +using System; +using System.Text.RegularExpressions; + +namespace Swan.Validators +{ + /// + /// Regex validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class MatchAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// + /// A regex string. + /// The error message. + /// Expression. + public MatchAttribute(string regex, string errorMessage = null) + { + Expression = regex ?? throw new ArgumentNullException(nameof(regex)); + ErrorMessage = errorMessage ?? "String does not match the specified regular expression"; + } + + /// + /// The string regex used to find a match. + /// + public string Expression { get; } + + /// + public string ErrorMessage { get; internal set; } + + /// + public bool IsValid(T value) + { + if (Equals(value, default(T))) + return false; + + return !(value is string) + ? throw new ArgumentException("Property is not a string") + : Regex.IsMatch(value.ToString(), Expression); + } + } + + /// + /// Email validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class EmailAttribute : MatchAttribute + { + private const string EmailRegExp = + @"^(?("")("".+?(? + /// Initializes a new instance of the class. + /// + /// The error message. + public EmailAttribute(string errorMessage = null) + : base(EmailRegExp, errorMessage ?? "String is not an email") + { + } + } + + /// + /// A not null validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class NotNullAttribute : Attribute, IValidator + { + /// + public string ErrorMessage => "Value is null"; + + /// + public bool IsValid(T value) => !Equals(default(T), value); + } + + /// + /// A range constraint validator. + /// + [AttributeUsage(AttributeTargets.Property)] + public class RangeAttribute : Attribute, IValidator + { + /// + /// Initializes a new instance of the class. + /// Constructor that takes integer minimum and maximum values. + /// + /// The minimum value. + /// The maximum value. + public RangeAttribute(int min, int max) + { + if (min >= max) + throw new InvalidOperationException("Maximum value must be greater than minimum"); + + Maximum = max; + Minimum = min; + } + + /// + /// Initializes a new instance of the class. + /// Constructor that takes double minimum and maximum values. + /// + /// The minimum value. + /// The maximum value. + public RangeAttribute(double min, double max) + { + if (min >= max) + throw new InvalidOperationException("Maximum value must be greater than minimum"); + + Maximum = max; + Minimum = min; + } + + /// + public string ErrorMessage => "Value is not within the specified range"; + + /// + /// Maximum value for the range. + /// + public IComparable Maximum { get; } + + /// + /// Minimum value for the range. + /// + public IComparable Minimum { get; } + + /// + public bool IsValid(T value) + => value is IComparable comparable + ? comparable.CompareTo(Minimum) >= 0 && comparable.CompareTo(Maximum) <= 0 + : throw new ArgumentException(nameof(value)); + } +} \ No newline at end of file diff --git a/Swan/DependencyInjection/DependencyContainer.cs b/Swan/DependencyInjection/DependencyContainer.cs new file mode 100644 index 0000000..48d6461 --- /dev/null +++ b/Swan/DependencyInjection/DependencyContainer.cs @@ -0,0 +1,743 @@ +namespace Swan.DependencyInjection +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + /// + /// The concrete implementation of a simple IoC container + /// based largely on TinyIoC (https://github.com/grumpydev/TinyIoC). + /// + /// + public partial class DependencyContainer : IDisposable + { + private readonly object _autoRegisterLock = new object(); + + private bool _disposed; + + static DependencyContainer() + { + } + + /// + /// Initializes a new instance of the class. + /// + public DependencyContainer() + { + RegisteredTypes = new TypesConcurrentDictionary(this); + Register(this); + } + + private DependencyContainer(DependencyContainer parent) + : this() + { + Parent = parent; + } + + /// + /// Lazy created Singleton instance of the container for simple scenarios. + /// + public static DependencyContainer Current { get; } = new DependencyContainer(); + + internal DependencyContainer Parent { get; } + + internal TypesConcurrentDictionary RegisteredTypes { get; } + + /// + public void Dispose() + { + if (_disposed) return; + + _disposed = true; + + foreach (var disposable in RegisteredTypes.Values.Select(item => item as IDisposable)) + { + disposable?.Dispose(); + } + + GC.SuppressFinalize(this); + } + + /// + /// Gets the child container. + /// + /// A new instance of the class. + public DependencyContainer GetChildContainer() => new DependencyContainer(this); + + #region Registration + + /// + /// Attempt to automatically register all non-generic classes and interfaces in the current app domain. + /// Types will only be registered if they pass the supplied registration predicate. + /// + /// What action to take when encountering duplicate implementations of an interface/base class. + /// Predicate to determine if a particular type should be registered. + public void AutoRegister( + DependencyContainerDuplicateImplementationAction duplicateAction = + DependencyContainerDuplicateImplementationAction.RegisterSingle, + Func registrationPredicate = null) + { + AutoRegister( + AppDomain.CurrentDomain.GetAssemblies().Where(a => !IsIgnoredAssembly(a)), + duplicateAction, + registrationPredicate); + } + + /// + /// Attempt to automatically register all non-generic classes and interfaces in the specified assemblies + /// Types will only be registered if they pass the supplied registration predicate. + /// + /// Assemblies to process. + /// What action to take when encountering duplicate implementations of an interface/base class. + /// Predicate to determine if a particular type should be registered. + public void AutoRegister( + IEnumerable assemblies, + DependencyContainerDuplicateImplementationAction duplicateAction = + DependencyContainerDuplicateImplementationAction.RegisterSingle, + Func registrationPredicate = null) + { + lock (_autoRegisterLock) + { + var types = assemblies + .SelectMany(a => a.GetAllTypes()) + .Where(t => !IsIgnoredType(t, registrationPredicate)) + .ToList(); + + var concreteTypes = types + .Where(type => + type.IsClass && !type.IsAbstract && + (type != GetType() && (type.DeclaringType != GetType()) && !type.IsGenericTypeDefinition)) + .ToList(); + + foreach (var type in concreteTypes) + { + try + { + RegisteredTypes.Register(type, string.Empty, GetDefaultObjectFactory(type, type)); + } + catch (MethodAccessException) + { + // Ignore methods we can't access - added for Silverlight + } + } + + var abstractInterfaceTypes = types.Where( + type => + ((type.IsInterface || type.IsAbstract) && (type.DeclaringType != GetType()) && + (!type.IsGenericTypeDefinition))); + + foreach (var type in abstractInterfaceTypes) + { + var localType = type; + var implementations = concreteTypes + .Where(implementationType => localType.IsAssignableFrom(implementationType)).ToList(); + + if (implementations.Skip(1).Any()) + { + if (duplicateAction == DependencyContainerDuplicateImplementationAction.Fail) + throw new DependencyContainerRegistrationException(type, implementations); + + if (duplicateAction == DependencyContainerDuplicateImplementationAction.RegisterMultiple) + { + RegisterMultiple(type, implementations); + } + } + + var firstImplementation = implementations.FirstOrDefault(); + + if (firstImplementation == null) continue; + + try + { + RegisteredTypes.Register(type, string.Empty, GetDefaultObjectFactory(type, firstImplementation)); + } + catch (MethodAccessException) + { + // Ignore methods we can't access - added for Silverlight + } + } + } + } + + /// + /// Creates/replaces a named container class registration with default options. + /// + /// Type to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, string name = "") + => RegisteredTypes.Register( + registerType, + name, + GetDefaultObjectFactory(registerType, registerType)); + + /// + /// Creates/replaces a named container class registration with a given implementation and default options. + /// + /// Type to register. + /// Type to instantiate that implements RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, Type registerImplementation, string name = "") => + RegisteredTypes.Register(registerType, name, GetDefaultObjectFactory(registerType, registerImplementation)); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Instance of RegisterType to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(Type registerType, object instance, string name = "") => + RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerType, instance)); + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Type of instance to register that implements RegisterType. + /// Instance of RegisterImplementation to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register( + Type registerType, + Type registerImplementation, + object instance, + string name = "") + => RegisteredTypes.Register(registerType, name, new InstanceFactory(registerType, registerImplementation, instance)); + + /// + /// Creates/replaces a container class registration with a user specified factory. + /// + /// Type to register. + /// Factory/lambda that returns an instance of RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register( + Type registerType, + Func, object> factory, + string name = "") + => RegisteredTypes.Register(registerType, name, new DelegateFactory(registerType, factory)); + + /// + /// Creates/replaces a named container class registration with default options. + /// + /// Type to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(string name = "") + where TRegister : class + { + return Register(typeof(TRegister), name); + } + + /// + /// Creates/replaces a named container class registration with a given implementation and default options. + /// + /// Type to register. + /// Type to instantiate that implements RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(string name = "") + where TRegister : class + where TRegisterImplementation : class, TRegister + { + return Register(typeof(TRegister), typeof(TRegisterImplementation), name); + } + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Instance of RegisterType to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(TRegister instance, string name = "") + where TRegister : class + { + return Register(typeof(TRegister), instance, name); + } + + /// + /// Creates/replaces a named container class registration with a specific, strong referenced, instance. + /// + /// Type to register. + /// Type of instance to register that implements RegisterType. + /// Instance of RegisterImplementation to register. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register(TRegisterImplementation instance, + string name = "") + where TRegister : class + where TRegisterImplementation : class, TRegister + { + return Register(typeof(TRegister), typeof(TRegisterImplementation), instance, name); + } + + /// + /// Creates/replaces a named container class registration with a user specified factory. + /// + /// Type to register. + /// Factory/lambda that returns an instance of RegisterType. + /// Name of registration. + /// RegisterOptions for fluent API. + public RegisterOptions Register( + Func, TRegister> factory, string name = "") + where TRegister : class + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + return Register(typeof(TRegister), factory, name); + } + + /// + /// Register multiple implementations of a type. + /// + /// Internally this registers each implementation using the full name of the class as its registration name. + /// + /// Type that each implementation implements. + /// Types that implement RegisterType. + /// MultiRegisterOptions for the fluent API. + public MultiRegisterOptions RegisterMultiple(IEnumerable implementationTypes) => + RegisterMultiple(typeof(TRegister), implementationTypes); + + /// + /// Register multiple implementations of a type. + /// + /// Internally this registers each implementation using the full name of the class as its registration name. + /// + /// Type that each implementation implements. + /// Types that implement RegisterType. + /// MultiRegisterOptions for the fluent API. + public MultiRegisterOptions RegisterMultiple(Type registrationType, IEnumerable implementationTypes) + { + if (implementationTypes == null) + throw new ArgumentNullException(nameof(implementationTypes), "types is null."); + + foreach (var type in implementationTypes.Where(type => !registrationType.IsAssignableFrom(type))) + { + throw new ArgumentException( + $"types: The type {registrationType.FullName} is not assignable from {type.FullName}"); + } + + if (implementationTypes.Count() != implementationTypes.Distinct().Count()) + { + var queryForDuplicatedTypes = implementationTypes + .GroupBy(i => i) + .Where(j => j.Count() > 1) + .Select(j => j.Key.FullName); + + var fullNamesOfDuplicatedTypes = string.Join(",\n", queryForDuplicatedTypes.ToArray()); + + throw new ArgumentException( + $"types: The same implementation type cannot be specified multiple times for {registrationType.FullName}\n\n{fullNamesOfDuplicatedTypes}"); + } + + var registerOptions = implementationTypes + .Select(type => Register(registrationType, type, type.FullName)) + .ToList(); + + return new MultiRegisterOptions(registerOptions); + } + + #endregion + + #region Unregistration + + /// + /// Remove a named container class registration. + /// + /// Type to unregister. + /// Name of registration. + /// true if the registration is successfully found and removed; otherwise, false. + public bool Unregister(string name = "") => Unregister(typeof(TRegister), name); + + /// + /// Remove a named container class registration. + /// + /// Type to unregister. + /// Name of registration. + /// true if the registration is successfully found and removed; otherwise, false. + public bool Unregister(Type registerType, string name = "") => + RegisteredTypes.RemoveRegistration(new TypeRegistration(registerType, name)); + + #endregion + + #region Resolution + + /// + /// Attempts to resolve a named type using specified options and the supplied constructor parameters. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Instance of type. + /// Unable to resolve the type. + public object Resolve( + Type resolveType, + string name = null, + DependencyContainerResolveOptions options = null) + => RegisteredTypes.ResolveInternal(new TypeRegistration(resolveType, name), options ?? DependencyContainerResolveOptions.Default); + + /// + /// Attempts to resolve a named type using specified options and the supplied constructor parameters. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Instance of type. + /// Unable to resolve the type. + public TResolveType Resolve( + string name = null, + DependencyContainerResolveOptions options = null) + where TResolveType : class + { + return (TResolveType)Resolve(typeof(TResolveType), name, options); + } + + /// + /// Attempts to predict whether a given type can be resolved with the supplied constructor parameters options. + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. + /// + /// Type to resolve. + /// The name. + /// Resolution options. + /// + /// Bool indicating whether the type can be resolved. + /// + public bool CanResolve( + Type resolveType, + string name = null, + DependencyContainerResolveOptions options = null) => + RegisteredTypes.CanResolve(new TypeRegistration(resolveType, name), options); + + /// + /// Attempts to predict whether a given named type can be resolved with the supplied constructor parameters options. + /// + /// Parameters are used in conjunction with normal container resolution to find the most suitable constructor (if one exists). + /// All user supplied parameters must exist in at least one resolvable constructor of RegisterType or resolution will fail. + /// + /// Note: Resolution may still fail if user defined factory registrations fail to construct objects when called. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Bool indicating whether the type can be resolved. + public bool CanResolve( + string name = null, + DependencyContainerResolveOptions options = null) + where TResolveType : class + { + return CanResolve(typeof(TResolveType), name, options); + } + + /// + /// Attempts to resolve a type using the default options. + /// + /// Type to resolve. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve(Type resolveType, out object resolvedType) + { + try + { + resolvedType = Resolve(resolveType); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options. + /// + /// Type to resolve. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve(Type resolveType, DependencyContainerResolveOptions options, out object resolvedType) + { + try + { + resolvedType = Resolve(resolveType, options: options); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options and given name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve(Type resolveType, string name, out object resolvedType) + { + try + { + resolvedType = Resolve(resolveType, name); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options and name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve( + Type resolveType, + string name, + DependencyContainerResolveOptions options, + out object resolvedType) + { + try + { + resolvedType = Resolve(resolveType, name, options); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = null; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options. + /// + /// Type to resolve. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve(out TResolveType resolvedType) + where TResolveType : class + { + try + { + resolvedType = Resolve(); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options. + /// + /// Type to resolve. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve(DependencyContainerResolveOptions options, out TResolveType resolvedType) + where TResolveType : class + { + try + { + resolvedType = Resolve(options: options); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the default options and given name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve(string name, out TResolveType resolvedType) + where TResolveType : class + { + try + { + resolvedType = Resolve(name); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = default; + return false; + } + } + + /// + /// Attempts to resolve a type using the given options and name. + /// + /// Type to resolve. + /// Name of registration. + /// Resolution options. + /// Resolved type or default if resolve fails. + /// true if resolved successfully, false otherwise. + public bool TryResolve( + string name, + DependencyContainerResolveOptions options, + out TResolveType resolvedType) + where TResolveType : class + { + try + { + resolvedType = Resolve(name, options); + return true; + } + catch (DependencyContainerResolutionException) + { + resolvedType = default; + return false; + } + } + + /// + /// Returns all registrations of a type. + /// + /// Type to resolveAll. + /// Whether to include un-named (default) registrations. + /// IEnumerable. + public IEnumerable ResolveAll(Type resolveType, bool includeUnnamed = false) + => RegisteredTypes.Resolve(resolveType, includeUnnamed); + + /// + /// Returns all registrations of a type. + /// + /// Type to resolveAll. + /// Whether to include un-named (default) registrations. + /// IEnumerable. + public IEnumerable ResolveAll(bool includeUnnamed = true) + where TResolveType : class + { + return ResolveAll(typeof(TResolveType), includeUnnamed).Cast(); + } + + /// + /// Attempts to resolve all public property dependencies on the given object using the given resolve options. + /// + /// Object to "build up". + /// Resolve options to use. + public void BuildUp(object input, DependencyContainerResolveOptions resolveOptions = null) + { + if (resolveOptions == null) + resolveOptions = DependencyContainerResolveOptions.Default; + + var properties = input.GetType() + .GetProperties() + .Where(property => property.GetCacheGetMethod() != null && property.GetCacheSetMethod() != null && + !property.PropertyType.IsValueType); + + foreach (var property in properties.Where(property => property.GetValue(input, null) == null)) + { + try + { + property.SetValue( + input, + RegisteredTypes.ResolveInternal(new TypeRegistration(property.PropertyType), resolveOptions), + null); + } + catch (DependencyContainerResolutionException) + { + // Catch any resolution errors and ignore them + } + } + } + + #endregion + + #region Internal Methods + + internal static bool IsValidAssignment(Type registerType, Type registerImplementation) + { + if (!registerType.IsGenericTypeDefinition) + { + if (!registerType.IsAssignableFrom(registerImplementation)) + return false; + } + else + { + if (registerType.IsInterface && registerImplementation.GetInterfaces().All(t => t.Name != registerType.Name)) + return false; + + if (registerType.IsAbstract && registerImplementation.BaseType != registerType) + return false; + } + + return true; + } + + private static bool IsIgnoredAssembly(Assembly assembly) + { + // TODO - find a better way to remove "system" assemblies from the auto registration + var ignoreChecks = new List> + { + asm => asm.FullName.StartsWith("Microsoft.", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("System.", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("System,", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("CR_ExtUnitTest", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("mscorlib,", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("CR_VSTest", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("DevExpress.CodeRush", StringComparison.Ordinal), + asm => asm.FullName.StartsWith("xunit.", StringComparison.Ordinal), + }; + + return ignoreChecks.Any(check => check(assembly)); + } + + private static bool IsIgnoredType(Type type, Func registrationPredicate) + { + // TODO - find a better way to remove "system" types from the auto registration + var ignoreChecks = new List>() + { + t => t.FullName?.StartsWith("System.", StringComparison.Ordinal) ?? false, + t => t.FullName?.StartsWith("Microsoft.", StringComparison.Ordinal) ?? false, + t => t.IsPrimitive, + t => t.IsGenericTypeDefinition, + t => (t.GetConstructors(BindingFlags.Instance | BindingFlags.Public).Length == 0) && + !(t.IsInterface || t.IsAbstract), + }; + + if (registrationPredicate != null) + { + ignoreChecks.Add(t => !registrationPredicate(t)); + } + + return ignoreChecks.Any(check => check(type)); + } + + private static ObjectFactoryBase GetDefaultObjectFactory(Type registerType, Type registerImplementation) => registerType.IsInterface || registerType.IsAbstract + ? (ObjectFactoryBase)new SingletonFactory(registerType, registerImplementation) + : new MultiInstanceFactory(registerType, registerImplementation); + + #endregion + } +} diff --git a/Swan/DependencyInjection/DependencyContainerRegistrationException.cs b/Swan/DependencyInjection/DependencyContainerRegistrationException.cs new file mode 100644 index 0000000..3890744 --- /dev/null +++ b/Swan/DependencyInjection/DependencyContainerRegistrationException.cs @@ -0,0 +1,46 @@ +namespace Swan.DependencyInjection +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Generic Constraint Registration Exception. + /// + /// + public class DependencyContainerRegistrationException : Exception + { + private const string ConvertErrorText = "Cannot convert current registration of {0} to {1}"; + private const string RegisterErrorText = + "Cannot register type {0} - abstract classes or interfaces are not valid implementation types for {1}."; + private const string ErrorText = "Duplicate implementation of type {0} found ({1})."; + + /// + /// Initializes a new instance of the class. + /// + /// Type of the register. + /// The types. + public DependencyContainerRegistrationException(Type registerType, IEnumerable types) + : base(string.Format(ErrorText, registerType, GetTypesString(types))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The method. + /// if set to true [is type factory]. + public DependencyContainerRegistrationException(Type type, string method, bool isTypeFactory = false) + : base(isTypeFactory + ? string.Format(RegisterErrorText, type.FullName, method) + : string.Format(ConvertErrorText, type.FullName, method)) + { + } + + private static string GetTypesString(IEnumerable types) + { + return string.Join(",", types.Select(type => type.FullName)); + } + } +} \ No newline at end of file diff --git a/Swan/DependencyInjection/DependencyContainerResolutionException.cs b/Swan/DependencyInjection/DependencyContainerResolutionException.cs new file mode 100644 index 0000000..da98665 --- /dev/null +++ b/Swan/DependencyInjection/DependencyContainerResolutionException.cs @@ -0,0 +1,31 @@ +namespace Swan.DependencyInjection +{ + using System; + + /// + /// An exception for dependency resolutions. + /// + /// + [Serializable] + public class DependencyContainerResolutionException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The type. + public DependencyContainerResolutionException(Type type) + : base($"Unable to resolve type: {type.FullName}") + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The inner exception. + public DependencyContainerResolutionException(Type type, Exception innerException) + : base($"Unable to resolve type: {type.FullName}", innerException) + { + } + } +} diff --git a/Swan/DependencyInjection/DependencyContainerResolveOptions.cs b/Swan/DependencyInjection/DependencyContainerResolveOptions.cs new file mode 100644 index 0000000..28d5cb0 --- /dev/null +++ b/Swan/DependencyInjection/DependencyContainerResolveOptions.cs @@ -0,0 +1,114 @@ +namespace Swan.DependencyInjection +{ + using System.Collections.Generic; + + /// + /// Resolution settings. + /// + public class DependencyContainerResolveOptions + { + /// + /// Gets the default options (attempt resolution of unregistered types, fail on named resolution if name not found). + /// + public static DependencyContainerResolveOptions Default { get; } = new DependencyContainerResolveOptions(); + + /// + /// Gets or sets the unregistered resolution action. + /// + /// + /// The unregistered resolution action. + /// + public DependencyContainerUnregisteredResolutionAction UnregisteredResolutionAction { get; set; } = + DependencyContainerUnregisteredResolutionAction.AttemptResolve; + + /// + /// Gets or sets the named resolution failure action. + /// + /// + /// The named resolution failure action. + /// + public DependencyContainerNamedResolutionFailureAction NamedResolutionFailureAction { get; set; } = + DependencyContainerNamedResolutionFailureAction.Fail; + + /// + /// Gets the constructor parameters. + /// + /// + /// The constructor parameters. + /// + public Dictionary ConstructorParameters { get; } = new Dictionary(); + + /// + /// Clones this instance. + /// + /// + public DependencyContainerResolveOptions Clone() => new DependencyContainerResolveOptions + { + NamedResolutionFailureAction = NamedResolutionFailureAction, + UnregisteredResolutionAction = UnregisteredResolutionAction, + }; + } + + /// + /// Defines Resolution actions. + /// + public enum DependencyContainerUnregisteredResolutionAction + { + /// + /// Attempt to resolve type, even if the type isn't registered. + /// + /// Registered types/options will always take precedence. + /// + AttemptResolve, + + /// + /// Fail resolution if type not explicitly registered + /// + Fail, + + /// + /// Attempt to resolve unregistered type if requested type is generic + /// and no registration exists for the specific generic parameters used. + /// + /// Registered types/options will always take precedence. + /// + GenericsOnly, + } + + /// + /// Enumerates failure actions. + /// + public enum DependencyContainerNamedResolutionFailureAction + { + /// + /// The attempt unnamed resolution + /// + AttemptUnnamedResolution, + + /// + /// The fail + /// + Fail, + } + + /// + /// Enumerates duplicate definition actions. + /// + public enum DependencyContainerDuplicateImplementationAction + { + /// + /// The register single + /// + RegisterSingle, + + /// + /// The register multiple + /// + RegisterMultiple, + + /// + /// The fail + /// + Fail, + } +} \ No newline at end of file diff --git a/Swan/DependencyInjection/DependencyContainerWeakReferenceException.cs b/Swan/DependencyInjection/DependencyContainerWeakReferenceException.cs new file mode 100644 index 0000000..eb1e085 --- /dev/null +++ b/Swan/DependencyInjection/DependencyContainerWeakReferenceException.cs @@ -0,0 +1,22 @@ +namespace Swan.DependencyInjection +{ + using System; + + /// + /// Weak Reference Exception. + /// + /// + public class DependencyContainerWeakReferenceException : Exception + { + private const string ErrorText = "Unable to instantiate {0} - referenced object has been reclaimed"; + + /// + /// Initializes a new instance of the class. + /// + /// The type. + public DependencyContainerWeakReferenceException(Type type) + : base(string.Format(ErrorText, type.FullName)) + { + } + } +} diff --git a/Swan/DependencyInjection/ObjectFactoryBase.cs b/Swan/DependencyInjection/ObjectFactoryBase.cs new file mode 100644 index 0000000..51ff96d --- /dev/null +++ b/Swan/DependencyInjection/ObjectFactoryBase.cs @@ -0,0 +1,423 @@ +namespace Swan.DependencyInjection +{ + using System; + using System.Collections.Generic; + using System.Reflection; + + /// + /// Represents an abstract class for Object Factory. + /// + public abstract class ObjectFactoryBase + { + /// + /// Whether to assume this factory successfully constructs its objects + /// + /// Generally set to true for delegate style factories as CanResolve cannot delve + /// into the delegates they contain. + /// + public virtual bool AssumeConstruction => false; + + /// + /// The type the factory instantiates. + /// + public abstract Type CreatesType { get; } + + /// + /// Constructor to use, if specified. + /// + public ConstructorInfo Constructor { get; private set; } + + /// + /// Gets the singleton variant. + /// + /// + /// The singleton variant. + /// + /// singleton. + public virtual ObjectFactoryBase SingletonVariant => + throw new DependencyContainerRegistrationException(GetType(), "singleton"); + + /// + /// Gets the multi instance variant. + /// + /// + /// The multi instance variant. + /// + /// multi-instance. + public virtual ObjectFactoryBase MultiInstanceVariant => + throw new DependencyContainerRegistrationException(GetType(), "multi-instance"); + + /// + /// Gets the strong reference variant. + /// + /// + /// The strong reference variant. + /// + /// strong reference. + public virtual ObjectFactoryBase StrongReferenceVariant => + throw new DependencyContainerRegistrationException(GetType(), "strong reference"); + + /// + /// Gets the weak reference variant. + /// + /// + /// The weak reference variant. + /// + /// weak reference. + public virtual ObjectFactoryBase WeakReferenceVariant => + throw new DependencyContainerRegistrationException(GetType(), "weak reference"); + + /// + /// Create the type. + /// + /// Type user requested to be resolved. + /// Container that requested the creation. + /// The options. + /// Instance of type. + public abstract object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options); + + /// + /// Gets the factory for child container. + /// + /// The type. + /// The parent. + /// The child. + /// + public virtual ObjectFactoryBase GetFactoryForChildContainer( + Type type, + DependencyContainer parent, + DependencyContainer child) + { + return this; + } + } + + /// + /// + /// IObjectFactory that creates new instances of types for each resolution. + /// + internal class MultiInstanceFactory : ObjectFactoryBase + { + private readonly Type _registerType; + private readonly Type _registerImplementation; + + public MultiInstanceFactory(Type registerType, Type registerImplementation) + { + if (registerImplementation.IsAbstract || registerImplementation.IsInterface) + { + throw new DependencyContainerRegistrationException(registerImplementation, + "MultiInstanceFactory", + true); + } + + if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) + { + throw new DependencyContainerRegistrationException(registerImplementation, + "MultiInstanceFactory", + true); + } + + _registerType = registerType; + _registerImplementation = registerImplementation; + } + + public override Type CreatesType => _registerImplementation; + + public override ObjectFactoryBase SingletonVariant => + new SingletonFactory(_registerType, _registerImplementation); + + public override ObjectFactoryBase MultiInstanceVariant => this; + + public override object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) + { + try + { + return container.RegisteredTypes.ConstructType(_registerImplementation, Constructor, options); + } + catch (DependencyContainerResolutionException ex) + { + throw new DependencyContainerResolutionException(_registerType, ex); + } + } + } + + /// + /// + /// IObjectFactory that invokes a specified delegate to construct the object. + /// + internal class DelegateFactory : ObjectFactoryBase + { + private readonly Type _registerType; + + private readonly Func, object> _factory; + + public DelegateFactory( + Type registerType, + Func, object> factory) + { + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + + _registerType = registerType; + } + + public override bool AssumeConstruction => true; + + public override Type CreatesType => _registerType; + + public override ObjectFactoryBase WeakReferenceVariant => new WeakDelegateFactory(_registerType, _factory); + + public override ObjectFactoryBase StrongReferenceVariant => this; + + public override object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) + { + try + { + return _factory.Invoke(container, options.ConstructorParameters); + } + catch (Exception ex) + { + throw new DependencyContainerResolutionException(_registerType, ex); + } + } + } + + /// + /// + /// IObjectFactory that invokes a specified delegate to construct the object + /// Holds the delegate using a weak reference. + /// + internal class WeakDelegateFactory : ObjectFactoryBase + { + private readonly Type _registerType; + + private readonly WeakReference _factory; + + public WeakDelegateFactory( + Type registerType, + Func, object> factory) + { + if (factory == null) + throw new ArgumentNullException(nameof(factory)); + + _factory = new WeakReference(factory); + + _registerType = registerType; + } + + public override bool AssumeConstruction => true; + + public override Type CreatesType => _registerType; + + public override ObjectFactoryBase StrongReferenceVariant + { + get + { + if (!(_factory.Target is Func, object> factory)) + throw new DependencyContainerWeakReferenceException(_registerType); + + return new DelegateFactory(_registerType, factory); + } + } + + public override ObjectFactoryBase WeakReferenceVariant => this; + + public override object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) + { + if (!(_factory.Target is Func, object> factory)) + throw new DependencyContainerWeakReferenceException(_registerType); + + try + { + return factory.Invoke(container, options.ConstructorParameters); + } + catch (Exception ex) + { + throw new DependencyContainerResolutionException(_registerType, ex); + } + } + } + + /// + /// Stores an particular instance to return for a type. + /// + internal class InstanceFactory : ObjectFactoryBase, IDisposable + { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly object _instance; + + public InstanceFactory(Type registerType, Type registerImplementation, object instance) + { + if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) + throw new DependencyContainerRegistrationException(registerImplementation, "InstanceFactory", true); + + _registerType = registerType; + _registerImplementation = registerImplementation; + _instance = instance; + } + + public override bool AssumeConstruction => true; + + public override Type CreatesType => _registerImplementation; + + public override ObjectFactoryBase MultiInstanceVariant => + new MultiInstanceFactory(_registerType, _registerImplementation); + + public override ObjectFactoryBase WeakReferenceVariant => + new WeakInstanceFactory(_registerType, _registerImplementation, _instance); + + public override ObjectFactoryBase StrongReferenceVariant => this; + + public override object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) + { + return _instance; + } + + public void Dispose() + { + var disposable = _instance as IDisposable; + + disposable?.Dispose(); + } + } + + /// + /// Stores the instance with a weak reference. + /// + internal class WeakInstanceFactory : ObjectFactoryBase, IDisposable + { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly WeakReference _instance; + + public WeakInstanceFactory(Type registerType, Type registerImplementation, object instance) + { + if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) + { + throw new DependencyContainerRegistrationException( + registerImplementation, + "WeakInstanceFactory", + true); + } + + _registerType = registerType; + _registerImplementation = registerImplementation; + _instance = new WeakReference(instance); + } + + public override Type CreatesType => _registerImplementation; + + public override ObjectFactoryBase MultiInstanceVariant => + new MultiInstanceFactory(_registerType, _registerImplementation); + + public override ObjectFactoryBase WeakReferenceVariant => this; + + public override ObjectFactoryBase StrongReferenceVariant + { + get + { + var instance = _instance.Target; + + if (instance == null) + throw new DependencyContainerWeakReferenceException(_registerType); + + return new InstanceFactory(_registerType, _registerImplementation, instance); + } + } + + public override object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) + { + var instance = _instance.Target; + + if (instance == null) + throw new DependencyContainerWeakReferenceException(_registerType); + + return instance; + } + + public void Dispose() => (_instance.Target as IDisposable)?.Dispose(); + } + + /// + /// A factory that lazy instantiates a type and always returns the same instance. + /// + internal class SingletonFactory : ObjectFactoryBase, IDisposable + { + private readonly Type _registerType; + private readonly Type _registerImplementation; + private readonly object _singletonLock = new object(); + private object _current; + + public SingletonFactory(Type registerType, Type registerImplementation) + { + if (registerImplementation.IsAbstract || registerImplementation.IsInterface) + { + throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); + } + + if (!DependencyContainer.IsValidAssignment(registerType, registerImplementation)) + { + throw new DependencyContainerRegistrationException(registerImplementation, nameof(SingletonFactory), true); + } + + _registerType = registerType; + _registerImplementation = registerImplementation; + } + + public override Type CreatesType => _registerImplementation; + + public override ObjectFactoryBase SingletonVariant => this; + + public override ObjectFactoryBase MultiInstanceVariant => + new MultiInstanceFactory(_registerType, _registerImplementation); + + public override object GetObject( + Type requestedType, + DependencyContainer container, + DependencyContainerResolveOptions options) + { + if (options.ConstructorParameters.Count != 0) + throw new ArgumentException("Cannot specify parameters for singleton types"); + + lock (_singletonLock) + { + if (_current == null) + _current = container.RegisteredTypes.ConstructType(_registerImplementation, Constructor, options); + } + + return _current; + } + + public override ObjectFactoryBase GetFactoryForChildContainer( + Type type, + DependencyContainer parent, + DependencyContainer child) + { + // We make sure that the singleton is constructed before the child container takes the factory. + // Otherwise the results would vary depending on whether or not the parent container had resolved + // the type before the child container does. + GetObject(type, parent, DependencyContainerResolveOptions.Default); + return this; + } + + public void Dispose() => (_current as IDisposable)?.Dispose(); + } +} diff --git a/Swan/DependencyInjection/RegisterOptions.cs b/Swan/DependencyInjection/RegisterOptions.cs new file mode 100644 index 0000000..f8ebfe8 --- /dev/null +++ b/Swan/DependencyInjection/RegisterOptions.cs @@ -0,0 +1,131 @@ +namespace Swan.DependencyInjection +{ + using System; + using System.Collections.Generic; + using System.Linq; + + /// + /// Registration options for "fluent" API. + /// + public sealed class RegisterOptions + { + private readonly TypesConcurrentDictionary _registeredTypes; + private readonly DependencyContainer.TypeRegistration _registration; + + /// + /// Initializes a new instance of the class. + /// + /// The registered types. + /// The registration. + public RegisterOptions(TypesConcurrentDictionary registeredTypes, DependencyContainer.TypeRegistration registration) + { + _registeredTypes = registeredTypes; + _registration = registration; + } + + /// + /// Make registration a singleton (single instance) if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions AsSingleton() + { + var currentFactory = _registeredTypes.GetCurrentFactory(_registration); + + if (currentFactory == null) + throw new DependencyContainerRegistrationException(_registration.Type, "singleton"); + + return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.SingletonVariant); + } + + /// + /// Make registration multi-instance if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions AsMultiInstance() + { + var currentFactory = _registeredTypes.GetCurrentFactory(_registration); + + if (currentFactory == null) + throw new DependencyContainerRegistrationException(_registration.Type, "multi-instance"); + + return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.MultiInstanceVariant); + } + + /// + /// Make registration hold a weak reference if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions WithWeakReference() + { + var currentFactory = _registeredTypes.GetCurrentFactory(_registration); + + if (currentFactory == null) + throw new DependencyContainerRegistrationException(_registration.Type, "weak reference"); + + return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.WeakReferenceVariant); + } + + /// + /// Make registration hold a strong reference if possible. + /// + /// A registration options for fluent API. + /// Generic constraint registration exception. + public RegisterOptions WithStrongReference() + { + var currentFactory = _registeredTypes.GetCurrentFactory(_registration); + + if (currentFactory == null) + throw new DependencyContainerRegistrationException(_registration.Type, "strong reference"); + + return _registeredTypes.AddUpdateRegistration(_registration, currentFactory.StrongReferenceVariant); + } + } + + /// + /// Registration options for "fluent" API when registering multiple implementations. + /// + public sealed class MultiRegisterOptions + { + private IEnumerable _registerOptions; + + /// + /// Initializes a new instance of the class. + /// + /// The register options. + public MultiRegisterOptions(IEnumerable registerOptions) + { + _registerOptions = registerOptions; + } + + /// + /// Make registration a singleton (single instance) if possible. + /// + /// A registration multi-instance for fluent API. + /// Generic Constraint Registration Exception. + public MultiRegisterOptions AsSingleton() + { + _registerOptions = ExecuteOnAllRegisterOptions(ro => ro.AsSingleton()); + return this; + } + + /// + /// Make registration multi-instance if possible. + /// + /// A registration multi-instance for fluent API. + /// Generic Constraint Registration Exception. + public MultiRegisterOptions AsMultiInstance() + { + _registerOptions = ExecuteOnAllRegisterOptions(ro => ro.AsMultiInstance()); + return this; + } + + private IEnumerable ExecuteOnAllRegisterOptions( + Func action) + { + return _registerOptions.Select(action).ToList(); + } + } +} \ No newline at end of file diff --git a/Swan/DependencyInjection/TypeRegistration.cs b/Swan/DependencyInjection/TypeRegistration.cs new file mode 100644 index 0000000..0b196f8 --- /dev/null +++ b/Swan/DependencyInjection/TypeRegistration.cs @@ -0,0 +1,67 @@ +namespace Swan.DependencyInjection +{ + using System; + + public partial class DependencyContainer + { + /// + /// Represents a Type Registration within the IoC Container. + /// + public sealed class TypeRegistration + { + private readonly int _hashCode; + + /// + /// Initializes a new instance of the class. + /// + /// The type. + /// The name. + public TypeRegistration(Type type, string name = null) + { + Type = type; + Name = name ?? string.Empty; + + _hashCode = string.Concat(Type.FullName, "|", Name).GetHashCode(); + } + + /// + /// Gets the type. + /// + /// + /// The type. + /// + public Type Type { get; } + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public string Name { get; } + + /// + /// Determines whether the specified , is equal to this instance. + /// + /// The to compare with this instance. + /// + /// true if the specified is equal to this instance; otherwise, false. + /// + public override bool Equals(object obj) + { + if (!(obj is TypeRegistration typeRegistration) || typeRegistration.Type != Type) + return false; + + return string.Compare(Name, typeRegistration.Name, StringComparison.Ordinal) == 0; + } + + /// + /// Returns a hash code for this instance. + /// + /// + /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. + /// + public override int GetHashCode() => _hashCode; + } + } +} \ No newline at end of file diff --git a/Swan/DependencyInjection/TypesConcurrentDictionary.cs b/Swan/DependencyInjection/TypesConcurrentDictionary.cs new file mode 100644 index 0000000..dd9a590 --- /dev/null +++ b/Swan/DependencyInjection/TypesConcurrentDictionary.cs @@ -0,0 +1,351 @@ +namespace Swan.DependencyInjection +{ + using System; + using System.Linq.Expressions; + using System.Reflection; + using System.Collections.Generic; + using System.Linq; + using System.Collections.Concurrent; + + /// + /// Represents a Concurrent Dictionary for TypeRegistration. + /// + public class TypesConcurrentDictionary : ConcurrentDictionary + { + private static readonly ConcurrentDictionary ObjectConstructorCache = + new ConcurrentDictionary(); + + private readonly DependencyContainer _dependencyContainer; + + internal TypesConcurrentDictionary(DependencyContainer dependencyContainer) + { + _dependencyContainer = dependencyContainer; + } + + /// + /// Represents a delegate to build an object with the parameters. + /// + /// The parameters. + /// The built object. + public delegate object ObjectConstructor(params object[] parameters); + + internal IEnumerable Resolve(Type resolveType, bool includeUnnamed) + { + var registrations = Keys.Where(tr => tr.Type == resolveType) + .Concat(GetParentRegistrationsForType(resolveType)).Distinct(); + + if (!includeUnnamed) + registrations = registrations.Where(tr => !string.IsNullOrEmpty(tr.Name)); + + return registrations.Select(registration => + ResolveInternal(registration, DependencyContainerResolveOptions.Default)); + } + + internal ObjectFactoryBase GetCurrentFactory(DependencyContainer.TypeRegistration registration) + { + TryGetValue(registration, out var current); + + return current; + } + + internal RegisterOptions Register(Type registerType, string name, ObjectFactoryBase factory) + => AddUpdateRegistration(new DependencyContainer.TypeRegistration(registerType, name), factory); + + internal RegisterOptions AddUpdateRegistration(DependencyContainer.TypeRegistration typeRegistration, ObjectFactoryBase factory) + { + this[typeRegistration] = factory; + + return new RegisterOptions(this, typeRegistration); + } + + internal bool RemoveRegistration(DependencyContainer.TypeRegistration typeRegistration) + => TryRemove(typeRegistration, out _); + + internal object ResolveInternal( + DependencyContainer.TypeRegistration registration, + DependencyContainerResolveOptions? options = null) + { + if (options == null) + options = DependencyContainerResolveOptions.Default; + + // Attempt container resolution + if (TryGetValue(registration, out var factory)) + { + try + { + return factory.GetObject(registration.Type, _dependencyContainer, options); + } + catch (DependencyContainerResolutionException) + { + throw; + } + catch (Exception ex) + { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + + // Attempt to get a factory from parent if we can + var bubbledObjectFactory = GetParentObjectFactory(registration); + if (bubbledObjectFactory != null) + { + try + { + return bubbledObjectFactory.GetObject(registration.Type, _dependencyContainer, options); + } + catch (DependencyContainerResolutionException) + { + throw; + } + catch (Exception ex) + { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + + // Fail if requesting named resolution and settings set to fail if unresolved + if (!string.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureAction.Fail) + throw new DependencyContainerResolutionException(registration.Type); + + // Attempted unnamed fallback container resolution if relevant and requested + if (!string.IsNullOrEmpty(registration.Name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) + { + if (TryGetValue(new DependencyContainer.TypeRegistration(registration.Type, string.Empty), out factory)) + { + try + { + return factory.GetObject(registration.Type, _dependencyContainer, options); + } + catch (DependencyContainerResolutionException) + { + throw; + } + catch (Exception ex) + { + throw new DependencyContainerResolutionException(registration.Type, ex); + } + } + } + + // Attempt unregistered construction if possible and requested + var isValid = (options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionAction.AttemptResolve) || + (registration.Type.IsGenericType && options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionAction.GenericsOnly); + + return isValid && !registration.Type.IsAbstract && !registration.Type.IsInterface + ? ConstructType(registration.Type, null, options) + : throw new DependencyContainerResolutionException(registration.Type); + } + + internal bool CanResolve( + DependencyContainer.TypeRegistration registration, + DependencyContainerResolveOptions? options = null) + { + if (options == null) + options = DependencyContainerResolveOptions.Default; + + var checkType = registration.Type; + var name = registration.Name; + + if (TryGetValue(new DependencyContainer.TypeRegistration(checkType, name), out var factory)) + { + if (factory.AssumeConstruction) + return true; + + if (factory.Constructor == null) + return GetBestConstructor(factory.CreatesType, options) != null; + + return CanConstruct(factory.Constructor, options); + } + + // Fail if requesting named resolution and settings set to fail if unresolved + // Or bubble up if we have a parent + if (!string.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureAction.Fail) + return _dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false; + + // Attempted unnamed fallback container resolution if relevant and requested + if (!string.IsNullOrEmpty(name) && options.NamedResolutionFailureAction == + DependencyContainerNamedResolutionFailureAction.AttemptUnnamedResolution) + { + if (TryGetValue(new DependencyContainer.TypeRegistration(checkType), out factory)) + { + if (factory.AssumeConstruction) + return true; + + return GetBestConstructor(factory.CreatesType, options) != null; + } + } + + // Check if type is an automatic lazy factory request or an IEnumerable + if (IsAutomaticLazyFactoryRequest(checkType) || registration.Type.IsIEnumerable()) + return true; + + // Attempt unregistered construction if possible and requested + // If we cant', bubble if we have a parent + if ((options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionAction.AttemptResolve) || + (checkType.IsGenericType && options.UnregisteredResolutionAction == + DependencyContainerUnregisteredResolutionAction.GenericsOnly)) + { + return (GetBestConstructor(checkType, options) != null) || + (_dependencyContainer.Parent?.RegisteredTypes.CanResolve(registration, options.Clone()) ?? false); + } + + // Bubble resolution up the container tree if we have a parent + return _dependencyContainer.Parent != null && _dependencyContainer.Parent.RegisteredTypes.CanResolve(registration, options.Clone()); + } + + internal object ConstructType( + Type implementationType, + ConstructorInfo constructor, + DependencyContainerResolveOptions? options = null) + { + var typeToConstruct = implementationType; + + if (constructor == null) + { + // Try and get the best constructor that we can construct + // if we can't construct any then get the constructor + // with the least number of parameters so we can throw a meaningful + // resolve exception + constructor = GetBestConstructor(typeToConstruct, options) ?? + GetTypeConstructors(typeToConstruct).LastOrDefault(); + } + + if (constructor == null) + throw new DependencyContainerResolutionException(typeToConstruct); + + var ctorParams = constructor.GetParameters(); + var args = new object?[ctorParams.Length]; + + for (var parameterIndex = 0; parameterIndex < ctorParams.Length; parameterIndex++) + { + var currentParam = ctorParams[parameterIndex]; + + try + { + args[parameterIndex] = options?.ConstructorParameters.GetValueOrDefault(currentParam.Name, ResolveInternal(new DependencyContainer.TypeRegistration(currentParam.ParameterType), options.Clone())); + } + catch (DependencyContainerResolutionException ex) + { + // If a constructor parameter can't be resolved + // it will throw, so wrap it and throw that this can't + // be resolved. + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } + catch (Exception ex) + { + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } + } + + try + { + return CreateObjectConstructionDelegateWithCache(constructor).Invoke(args); + } + catch (Exception ex) + { + throw new DependencyContainerResolutionException(typeToConstruct, ex); + } + } + + private static ObjectConstructor CreateObjectConstructionDelegateWithCache(ConstructorInfo constructor) + { + if (ObjectConstructorCache.TryGetValue(constructor, out var objectConstructor)) + return objectConstructor; + + // We could lock the cache here, but there's no real side + // effect to two threads creating the same ObjectConstructor + // at the same time, compared to the cost of a lock for + // every creation. + var constructorParams = constructor.GetParameters(); + var lambdaParams = Expression.Parameter(typeof(object[]), "parameters"); + var newParams = new Expression[constructorParams.Length]; + + for (var i = 0; i < constructorParams.Length; i++) + { + var paramsParameter = Expression.ArrayIndex(lambdaParams, Expression.Constant(i)); + + newParams[i] = Expression.Convert(paramsParameter, constructorParams[i].ParameterType); + } + + var newExpression = Expression.New(constructor, newParams); + + var constructionLambda = Expression.Lambda(typeof(ObjectConstructor), newExpression, lambdaParams); + + objectConstructor = (ObjectConstructor)constructionLambda.Compile(); + + ObjectConstructorCache[constructor] = objectConstructor; + return objectConstructor; + } + + private static IEnumerable GetTypeConstructors(Type type) + => type.GetConstructors().OrderByDescending(ctor => ctor.GetParameters().Length); + + private static bool IsAutomaticLazyFactoryRequest(Type type) + { + if (!type.IsGenericType) + return false; + + var genericType = type.GetGenericTypeDefinition(); + + // Just a func + if (genericType == typeof(Func<>)) + return true; + + // 2 parameter func with string as first parameter (name) + if (genericType == typeof(Func<,>) && type.GetGenericArguments()[0] == typeof(string)) + return true; + + // 3 parameter func with string as first parameter (name) and IDictionary as second (parameters) + return genericType == typeof(Func<,,>) && type.GetGenericArguments()[0] == typeof(string) && + type.GetGenericArguments()[1] == typeof(IDictionary); + } + + private ObjectFactoryBase? GetParentObjectFactory(DependencyContainer.TypeRegistration registration) + { + if (_dependencyContainer.Parent == null) + return null; + + return _dependencyContainer.Parent.RegisteredTypes.TryGetValue(registration, out var factory) + ? factory.GetFactoryForChildContainer(registration.Type, _dependencyContainer.Parent, _dependencyContainer) + : _dependencyContainer.Parent.RegisteredTypes.GetParentObjectFactory(registration); + } + + private ConstructorInfo? GetBestConstructor( + Type type, + DependencyContainerResolveOptions options) + => type.IsValueType ? null : GetTypeConstructors(type).FirstOrDefault(ctor => CanConstruct(ctor, options)); + + private bool CanConstruct( + MethodBase ctor, + DependencyContainerResolveOptions? options) + { + foreach (var parameter in ctor.GetParameters()) + { + if (string.IsNullOrEmpty(parameter.Name)) + return false; + + var isParameterOverload = options.ConstructorParameters.ContainsKey(parameter.Name); + + if (parameter.ParameterType.IsPrimitive && !isParameterOverload) + return false; + + if (!isParameterOverload && + !CanResolve(new DependencyContainer.TypeRegistration(parameter.ParameterType), options.Clone())) + return false; + } + + return true; + } + + private IEnumerable GetParentRegistrationsForType(Type resolveType) + => _dependencyContainer.Parent == null + ? Array.Empty() + : _dependencyContainer.Parent.RegisteredTypes.Keys.Where(tr => tr.Type == resolveType).Concat(_dependencyContainer.Parent.RegisteredTypes.GetParentRegistrationsForType(resolveType)); + } +} diff --git a/Swan/Diagnostics/RealtimeClock.cs b/Swan/Diagnostics/RealtimeClock.cs new file mode 100644 index 0000000..d4ebb08 --- /dev/null +++ b/Swan/Diagnostics/RealtimeClock.cs @@ -0,0 +1,143 @@ +namespace Swan.Diagnostics +{ + using System; + using System.Diagnostics; + using Threading; + + /// + /// A time measurement artifact. + /// + internal sealed class RealTimeClock : IDisposable + { + private readonly Stopwatch _chrono = new Stopwatch(); + private ISyncLocker? _locker = SyncLockerFactory.Create(useSlim: true); + private long _offsetTicks; + private double _speedRatio = 1.0d; + private bool _isDisposed; + + /// + /// Initializes a new instance of the class. + /// The clock starts paused and at the 0 position. + /// + public RealTimeClock() + { + Reset(); + } + + /// + /// Gets or sets the clock position. + /// + public TimeSpan Position + { + get + { + using (_locker?.AcquireReaderLock()) + { + return TimeSpan.FromTicks( + _offsetTicks + Convert.ToInt64(_chrono.Elapsed.Ticks * SpeedRatio)); + } + } + } + + /// + /// Gets a value indicating whether the clock is running. + /// + public bool IsRunning + { + get + { + using (_locker?.AcquireReaderLock()) + { + return _chrono.IsRunning; + } + } + } + + /// + /// Gets or sets the speed ratio at which the clock runs. + /// + public double SpeedRatio + { + get + { + using (_locker?.AcquireReaderLock()) + { + return _speedRatio; + } + } + set + { + using (_locker?.AcquireWriterLock()) + { + if (value < 0d) value = 0d; + + // Capture the initial position se we set it even after the Speed Ratio has changed + // this ensures a smooth position transition + var initialPosition = Position; + _speedRatio = value; + Update(initialPosition); + } + } + } + + /// + /// Sets a new position value atomically. + /// + /// The new value that the position property will hold. + public void Update(TimeSpan value) + { + using (_locker?.AcquireWriterLock()) + { + var resume = _chrono.IsRunning; + _chrono.Reset(); + _offsetTicks = value.Ticks; + if (resume) _chrono.Start(); + } + } + + /// + /// Starts or resumes the clock. + /// + public void Play() + { + using (_locker?.AcquireWriterLock()) + { + if (_chrono.IsRunning) return; + _chrono.Start(); + } + } + + /// + /// Pauses the clock. + /// + public void Pause() + { + using (_locker?.AcquireWriterLock()) + { + _chrono.Stop(); + } + } + + /// + /// Sets the clock position to 0 and stops it. + /// The speed ratio is not modified. + /// + public void Reset() + { + using (_locker?.AcquireWriterLock()) + { + _offsetTicks = 0; + _chrono.Reset(); + } + } + + /// + public void Dispose() + { + if (_isDisposed) return; + _isDisposed = true; + _locker?.Dispose(); + _locker = null; + } + } +} diff --git a/Swan/Extensions.MimeMessage.cs b/Swan/Extensions.MimeMessage.cs new file mode 100644 index 0000000..2f42f3d --- /dev/null +++ b/Swan/Extensions.MimeMessage.cs @@ -0,0 +1,56 @@ +namespace Swan +{ + using System; + using System.IO; + using System.Net.Mail; + using System.Reflection; + + /// + /// Extension methods. + /// + public static class SmtpExtensions + { + private static readonly BindingFlags PrivateInstanceFlags = BindingFlags.Instance | BindingFlags.NonPublic; + + /// + /// The raw contents of this MailMessage as a MemoryStream. + /// + /// The caller. + /// A MemoryStream with the raw contents of this MailMessage. + public static MemoryStream ToMimeMessage(this MailMessage @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var result = new MemoryStream(); + var mailWriter = MimeMessageConstants.MailWriterConstructor.Invoke(new object[] { result }); + MimeMessageConstants.SendMethod.Invoke( + @this, + PrivateInstanceFlags, + null, + MimeMessageConstants.IsRunningInDotNetFourPointFive ? new[] { mailWriter, true, true } : new[] { mailWriter, true }, + null); + + result = new MemoryStream(result.ToArray()); + MimeMessageConstants.CloseMethod.Invoke( + mailWriter, + PrivateInstanceFlags, + null, + Array.Empty(), + null); + result.Position = 0; + return result; + } + + internal static class MimeMessageConstants + { +#pragma warning disable DE0005 // API is deprecated + public static readonly Type MailWriter = typeof(SmtpClient).Assembly.GetType("System.Net.Mail.MailWriter"); +#pragma warning restore DE0005 // API is deprecated + public static readonly ConstructorInfo MailWriterConstructor = MailWriter.GetConstructor(PrivateInstanceFlags, null, new[] { typeof(Stream) }, null); + public static readonly MethodInfo CloseMethod = MailWriter.GetMethod("Close", PrivateInstanceFlags); + public static readonly MethodInfo SendMethod = typeof(MailMessage).GetMethod("Send", PrivateInstanceFlags); + public static readonly bool IsRunningInDotNetFourPointFive = SendMethod.GetParameters().Length == 3; + } + } +} \ No newline at end of file diff --git a/Swan/Extensions.Network.cs b/Swan/Extensions.Network.cs new file mode 100644 index 0000000..32ffef6 --- /dev/null +++ b/Swan/Extensions.Network.cs @@ -0,0 +1,58 @@ +namespace Swan +{ + using System; + using System.Linq; + using System.Net; + using System.Net.Sockets; + + /// + /// Provides various extension methods for networking-related tasks. + /// + public static class NetworkExtensions + { + /// + /// Determines whether the IP address is private. + /// + /// The IP address. + /// + /// True if the IP Address is private; otherwise, false. + /// + /// address. + public static bool IsPrivateAddress(this IPAddress @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + var octets = @this.ToString().Split(new[] { "." }, StringSplitOptions.RemoveEmptyEntries).Select(byte.Parse).ToArray(); + var is24Bit = octets[0] == 10; + var is20Bit = octets[0] == 172 && (octets[1] >= 16 && octets[1] <= 31); + var is16Bit = octets[0] == 192 && octets[1] == 168; + + return is24Bit || is20Bit || is16Bit; + } + + /// + /// Converts an IPv4 Address to its Unsigned, 32-bit integer representation. + /// + /// The address. + /// + /// A 32-bit unsigned integer converted from four bytes at a specified position in a byte array. + /// + /// address. + /// InterNetwork - address. + public static uint ToUInt32(this IPAddress @this) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + if (@this.AddressFamily != AddressFamily.InterNetwork) + throw new ArgumentException($"Address has to be of family '{nameof(AddressFamily.InterNetwork)}'", nameof(@this)); + + var addressBytes = @this.GetAddressBytes(); + if (BitConverter.IsLittleEndian) + Array.Reverse(addressBytes); + + return BitConverter.ToUInt32(addressBytes, 0); + } + } +} diff --git a/Swan/Extensions.WindowsServices.cs b/Swan/Extensions.WindowsServices.cs new file mode 100644 index 0000000..420f15d --- /dev/null +++ b/Swan/Extensions.WindowsServices.cs @@ -0,0 +1,89 @@ +namespace Swan +{ + using Logging; + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; +#if NET461 + using System.ServiceProcess; +#else + using Services; +#endif + + /// + /// Extension methods. + /// + public static class WindowsServicesExtensions + { + /// + /// Runs a service in console mode. + /// + /// The service to run. + /// The logger source. + /// this. + [Obsolete("This extension method will be removed in version 3.0")] + public static void RunInConsoleMode(this ServiceBase @this, string loggerSource = null) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + RunInConsoleMode(new[] { @this }, loggerSource); + } + + /// + /// Runs a set of services in console mode. + /// + /// The services to run. + /// The logger source. + /// this. + /// The ServiceBase class isn't available. + [Obsolete("This extension method will be removed in version 3.0")] + public static void RunInConsoleMode(this ServiceBase[] @this, string loggerSource = null) + { + if (@this == null) + throw new ArgumentNullException(nameof(@this)); + + const string onStartMethodName = "OnStart"; + const string onStopMethodName = "OnStop"; + + var onStartMethod = typeof(ServiceBase).GetMethod(onStartMethodName, + BindingFlags.Instance | BindingFlags.NonPublic); + var onStopMethod = typeof(ServiceBase).GetMethod(onStopMethodName, + BindingFlags.Instance | BindingFlags.NonPublic); + + if (onStartMethod == null || onStopMethod == null) + throw new InvalidOperationException("The ServiceBase class isn't available."); + + var serviceThreads = new List(); + "Starting services . . .".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name); + + foreach (var service in @this) + { + var thread = new Thread(() => + { + onStartMethod.Invoke(service, new object[] { Array.Empty() }); + $"Started service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name); + }); + + serviceThreads.Add(thread); + thread.Start(); + } + + "Press any key to stop all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name); + Terminal.ReadKey(true, true); + "Stopping services . . .".Info(SwanRuntime.EntryAssemblyName.Name); + + foreach (var service in @this) + { + onStopMethod.Invoke(service, null); + $"Stopped service '{service.GetType().Name}'".Info(loggerSource ?? service.GetType().Name); + } + + foreach (var thread in serviceThreads) + thread.Join(); + + "Stopped all services.".Info(loggerSource ?? SwanRuntime.EntryAssemblyName.Name); + } + } +} diff --git a/Swan/Messaging/IMessageHubMessage.cs b/Swan/Messaging/IMessageHubMessage.cs new file mode 100644 index 0000000..6cc2869 --- /dev/null +++ b/Swan/Messaging/IMessageHubMessage.cs @@ -0,0 +1,13 @@ +namespace Swan.Messaging +{ + /// + /// A Message to be published/delivered by Messenger. + /// + public interface IMessageHubMessage + { + /// + /// The sender of the message, or null if not supported by the message implementation. + /// + object Sender { get; } + } +} diff --git a/Swan/Messaging/IMessageHubSubscription.cs b/Swan/Messaging/IMessageHubSubscription.cs new file mode 100644 index 0000000..0248b85 --- /dev/null +++ b/Swan/Messaging/IMessageHubSubscription.cs @@ -0,0 +1,26 @@ +namespace Swan.Messaging +{ + /// + /// Represents a message subscription. + /// + public interface IMessageHubSubscription + { + /// + /// Token returned to the subscribed to reference this subscription. + /// + MessageHubSubscriptionToken SubscriptionToken { get; } + + /// + /// Whether delivery should be attempted. + /// + /// Message that may potentially be delivered. + /// true - ok to send, false - should not attempt to send. + bool ShouldAttemptDelivery(IMessageHubMessage message); + + /// + /// Deliver the message. + /// + /// Message to deliver. + void Deliver(IMessageHubMessage message); + } +} \ No newline at end of file diff --git a/Swan/Messaging/MessageHub.cs b/Swan/Messaging/MessageHub.cs new file mode 100644 index 0000000..7a1de9b --- /dev/null +++ b/Swan/Messaging/MessageHub.cs @@ -0,0 +1,442 @@ +// =============================================================================== +// TinyIoC - TinyMessenger +// +// A simple messenger/event aggregator. +// +// https://github.com/grumpydev/TinyIoC/blob/master/src/TinyIoC/TinyMessenger.cs +// =============================================================================== +// Copyright © Steven Robbins. All rights reserved. +// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY +// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT +// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +// FITNESS FOR A PARTICULAR PURPOSE. +// =============================================================================== + +namespace Swan.Messaging +{ + using System.Threading.Tasks; + using System; + using System.Collections.Generic; + using System.Linq; + + #region Message Types / Interfaces + + /// + /// Message proxy definition. + /// + /// A message proxy can be used to intercept/alter messages and/or + /// marshal delivery actions onto a particular thread. + /// + public interface IMessageHubProxy + { + /// + /// Delivers the specified message. + /// + /// The message. + /// The subscription. + void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription); + } + + /// + /// Default "pass through" proxy. + /// + /// Does nothing other than deliver the message. + /// + public sealed class MessageHubDefaultProxy : IMessageHubProxy + { + private MessageHubDefaultProxy() + { + // placeholder + } + + /// + /// Singleton instance of the proxy. + /// + public static MessageHubDefaultProxy Instance { get; } = new MessageHubDefaultProxy(); + + /// + /// Delivers the specified message. + /// + /// The message. + /// The subscription. + public void Deliver(IMessageHubMessage message, IMessageHubSubscription subscription) + => subscription.Deliver(message); + } + + #endregion + + #region Hub Interface + + /// + /// Messenger hub responsible for taking subscriptions/publications and delivering of messages. + /// + public interface IMessageHub + { + /// + /// Subscribe to a message type with the given destination and delivery action. + /// Messages will be delivered via the specified proxy. + /// + /// All messages of this type will be delivered. + /// + /// Type of message. + /// Action to invoke when message is delivered. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// MessageSubscription used to unsubscribing. + MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + bool useStrongReferences, + IMessageHubProxy proxy) + where TMessage : class, IMessageHubMessage; + + /// + /// Subscribe to a message type with the given destination and delivery action with the given filter. + /// Messages will be delivered via the specified proxy. + /// All references are held with WeakReferences + /// Only messages that "pass" the filter will be delivered. + /// + /// Type of message. + /// Action to invoke when message is delivered. + /// The message filter. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// + /// MessageSubscription used to unsubscribing. + /// + MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + Func messageFilter, + bool useStrongReferences, + IMessageHubProxy proxy) + where TMessage : class, IMessageHubMessage; + + /// + /// Unsubscribe from a particular message type. + /// + /// Does not throw an exception if the subscription is not found. + /// + /// Type of message. + /// Subscription token received from Subscribe. + void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) + where TMessage : class, IMessageHubMessage; + + /// + /// Publish a message to any subscribers. + /// + /// Type of message. + /// Message to deliver. + void Publish(TMessage message) + where TMessage : class, IMessageHubMessage; + + /// + /// Publish a message to any subscribers asynchronously. + /// + /// Type of message. + /// Message to deliver. + /// A task from Publish action. + Task PublishAsync(TMessage message) + where TMessage : class, IMessageHubMessage; + } + + #endregion + + #region Hub Implementation + + /// + /// + /// The following code describes how to use a MessageHub. Both the + /// subscription and the message sending are done in the same place but this is only for explanatory purposes. + /// + /// class Example + /// { + /// using Swan; + /// using Swan.Components; + /// + /// static void Main() + /// { + /// // using DependencyContainer to create an instance of MessageHub + /// var messageHub = DependencyContainer + /// .Current + /// .Resolve<IMessageHub>() as MessageHub; + /// + /// // create an instance of the publisher class + /// // which has a string as its content + /// var message = new MessageHubGenericMessage<string>(new object(), "SWAN"); + /// + /// // subscribe to the publisher's event + /// // and just print out the content which is a string + /// // a token is returned which can be used to unsubscribe later on + /// var token = messageHub + /// .Subscribe<MessageHubGenericMessage<string>>(m => m.Content.Info()); + /// + /// // publish the message described above which is + /// // the string 'SWAN' + /// messageHub.Publish(message); + /// + /// // unsuscribe, we will no longer receive any messages + /// messageHub.Unsubscribe<MessageHubGenericMessage<string>>(token); + /// + /// Terminal.Flush(); + /// } + /// + /// } + /// + /// + public sealed class MessageHub : IMessageHub + { + #region Private Types and Interfaces + + private readonly object _subscriptionsPadlock = new object(); + + private readonly Dictionary> _subscriptions = + new Dictionary>(); + + private class WeakMessageSubscription : IMessageHubSubscription + where TMessage : class, IMessageHubMessage + { + private readonly WeakReference _deliveryAction; + private readonly WeakReference _messageFilter; + + /// + /// Initializes a new instance of the class. + /// + /// The subscription token. + /// The delivery action. + /// The message filter. + /// subscriptionToken + /// or + /// deliveryAction + /// or + /// messageFilter. + public WeakMessageSubscription( + MessageHubSubscriptionToken subscriptionToken, + Action deliveryAction, + Func messageFilter) + { + SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); + _deliveryAction = new WeakReference(deliveryAction); + _messageFilter = new WeakReference(messageFilter); + } + + public MessageHubSubscriptionToken SubscriptionToken { get; } + + public bool ShouldAttemptDelivery(IMessageHubMessage message) + { + return _deliveryAction.IsAlive && _messageFilter.IsAlive && + ((Func) _messageFilter.Target).Invoke((TMessage) message); + } + + public void Deliver(IMessageHubMessage message) + { + if (_deliveryAction.IsAlive) + { + ((Action) _deliveryAction.Target).Invoke((TMessage) message); + } + } + } + + private class StrongMessageSubscription : IMessageHubSubscription + where TMessage : class, IMessageHubMessage + { + private readonly Action _deliveryAction; + private readonly Func _messageFilter; + + /// + /// Initializes a new instance of the class. + /// + /// The subscription token. + /// The delivery action. + /// The message filter. + /// subscriptionToken + /// or + /// deliveryAction + /// or + /// messageFilter. + public StrongMessageSubscription( + MessageHubSubscriptionToken subscriptionToken, + Action deliveryAction, + Func messageFilter) + { + SubscriptionToken = subscriptionToken ?? throw new ArgumentNullException(nameof(subscriptionToken)); + _deliveryAction = deliveryAction; + _messageFilter = messageFilter; + } + + public MessageHubSubscriptionToken SubscriptionToken { get; } + + public bool ShouldAttemptDelivery(IMessageHubMessage message) => _messageFilter.Invoke((TMessage) message); + + public void Deliver(IMessageHubMessage message) => _deliveryAction.Invoke((TMessage) message); + } + + #endregion + + #region Subscription dictionary + + private class SubscriptionItem + { + public SubscriptionItem(IMessageHubProxy proxy, IMessageHubSubscription subscription) + { + Proxy = proxy; + Subscription = subscription; + } + + public IMessageHubProxy Proxy { get; } + public IMessageHubSubscription Subscription { get; } + } + + #endregion + + #region Public API + + /// + /// Subscribe to a message type with the given destination and delivery action. + /// Messages will be delivered via the specified proxy. + /// + /// All messages of this type will be delivered. + /// + /// Type of message. + /// Action to invoke when message is delivered. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// MessageSubscription used to unsubscribing. + public MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + bool useStrongReferences = true, + IMessageHubProxy? proxy = null) + where TMessage : class, IMessageHubMessage + { + return Subscribe(deliveryAction, m => true, useStrongReferences, proxy); + } + + /// + /// Subscribe to a message type with the given destination and delivery action with the given filter. + /// Messages will be delivered via the specified proxy. + /// All references are held with WeakReferences + /// Only messages that "pass" the filter will be delivered. + /// + /// Type of message. + /// Action to invoke when message is delivered. + /// The message filter. + /// Use strong references to destination and deliveryAction. + /// Proxy to use when delivering the messages. + /// + /// MessageSubscription used to unsubscribing. + /// + public MessageHubSubscriptionToken Subscribe( + Action deliveryAction, + Func messageFilter, + bool useStrongReferences = true, + IMessageHubProxy? proxy = null) + where TMessage : class, IMessageHubMessage + { + if (deliveryAction == null) + throw new ArgumentNullException(nameof(deliveryAction)); + + if (messageFilter == null) + throw new ArgumentNullException(nameof(messageFilter)); + + lock (_subscriptionsPadlock) + { + if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) + { + currentSubscriptions = new List(); + _subscriptions[typeof(TMessage)] = currentSubscriptions; + } + + var subscriptionToken = new MessageHubSubscriptionToken(this, typeof(TMessage)); + + IMessageHubSubscription subscription; + if (useStrongReferences) + { + subscription = new StrongMessageSubscription( + subscriptionToken, + deliveryAction, + messageFilter); + } + else + { + subscription = new WeakMessageSubscription( + subscriptionToken, + deliveryAction, + messageFilter); + } + + currentSubscriptions.Add(new SubscriptionItem(proxy ?? MessageHubDefaultProxy.Instance, subscription)); + + return subscriptionToken; + } + } + + /// + public void Unsubscribe(MessageHubSubscriptionToken subscriptionToken) + where TMessage : class, IMessageHubMessage + { + if (subscriptionToken == null) + throw new ArgumentNullException(nameof(subscriptionToken)); + + lock (_subscriptionsPadlock) + { + if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) + return; + + var currentlySubscribed = currentSubscriptions + .Where(sub => ReferenceEquals(sub.Subscription.SubscriptionToken, subscriptionToken)) + .ToList(); + + currentlySubscribed.ForEach(sub => currentSubscriptions.Remove(sub)); + } + } + + /// + /// Publish a message to any subscribers. + /// + /// Type of message. + /// Message to deliver. + public void Publish(TMessage message) + where TMessage : class, IMessageHubMessage + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + List currentlySubscribed; + lock (_subscriptionsPadlock) + { + if (!_subscriptions.TryGetValue(typeof(TMessage), out var currentSubscriptions)) + return; + + currentlySubscribed = currentSubscriptions + .Where(sub => sub.Subscription.ShouldAttemptDelivery(message)) + .ToList(); + } + + currentlySubscribed.ForEach(sub => + { + try + { + sub.Proxy.Deliver(message, sub.Subscription); + } + catch + { + // Ignore any errors and carry on + } + }); + } + + /// + /// Publish a message to any subscribers asynchronously. + /// + /// Type of message. + /// Message to deliver. + /// A task with the publish. + public Task PublishAsync(TMessage message) + where TMessage : class, IMessageHubMessage + { + return Task.Run(() => Publish(message)); + } + + #endregion + } + + #endregion +} diff --git a/Swan/Messaging/MessageHubMessageBase.cs b/Swan/Messaging/MessageHubMessageBase.cs new file mode 100644 index 0000000..bdcdf6f --- /dev/null +++ b/Swan/Messaging/MessageHubMessageBase.cs @@ -0,0 +1,57 @@ +namespace Swan.Messaging +{ + using System; + + /// + /// Base class for messages that provides weak reference storage of the sender. + /// + public abstract class MessageHubMessageBase + : IMessageHubMessage + { + /// + /// Store a WeakReference to the sender just in case anyone is daft enough to + /// keep the message around and prevent the sender from being collected. + /// + private readonly WeakReference _sender; + + /// + /// Initializes a new instance of the class. + /// + /// The sender. + /// sender. + protected MessageHubMessageBase(object sender) + { + if (sender == null) + throw new ArgumentNullException(nameof(sender)); + + _sender = new WeakReference(sender); + } + + /// + public object Sender => _sender.Target; + } + + /// + /// Generic message with user specified content. + /// + /// Content type to store. + public class MessageHubGenericMessage + : MessageHubMessageBase + { + /// + /// Initializes a new instance of the class. + /// + /// The sender. + /// The content. + public MessageHubGenericMessage(object sender, TContent content) + : base(sender) + { + Content = content; + } + + /// + /// Contents of the message. + /// + public TContent Content { get; protected set; } + } +} diff --git a/Swan/Messaging/MessageHubSubscriptionToken.cs b/Swan/Messaging/MessageHubSubscriptionToken.cs new file mode 100644 index 0000000..4cc6a71 --- /dev/null +++ b/Swan/Messaging/MessageHubSubscriptionToken.cs @@ -0,0 +1,51 @@ +namespace Swan.Messaging +{ + using System; + + /// + /// Represents an active subscription to a message. + /// + public sealed class MessageHubSubscriptionToken + : IDisposable + { + private readonly WeakReference _hub; + private readonly Type _messageType; + + /// + /// Initializes a new instance of the class. + /// + /// The hub. + /// Type of the message. + /// hub. + /// messageType. + public MessageHubSubscriptionToken(IMessageHub hub, Type messageType) + { + if (hub == null) + { + throw new ArgumentNullException(nameof(hub)); + } + + if (!typeof(IMessageHubMessage).IsAssignableFrom(messageType)) + { + throw new ArgumentOutOfRangeException(nameof(messageType)); + } + + _hub = new WeakReference(hub); + _messageType = messageType; + } + + /// + public void Dispose() + { + if (_hub.IsAlive && _hub.Target is IMessageHub hub) + { + var unsubscribeMethod = typeof(IMessageHub).GetMethod(nameof(IMessageHub.Unsubscribe), + new[] {typeof(MessageHubSubscriptionToken)}); + unsubscribeMethod = unsubscribeMethod.MakeGenericMethod(_messageType); + unsubscribeMethod.Invoke(hub, new object[] {this}); + } + + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Swan/Net/Connection.cs b/Swan/Net/Connection.cs new file mode 100644 index 0000000..ff25bee --- /dev/null +++ b/Swan/Net/Connection.cs @@ -0,0 +1,886 @@ +namespace Swan.Net +{ + using Logging; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Net.Security; + using System.Net.Sockets; + using System.Security.Cryptography.X509Certificates; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Represents a network connection either on the server or on the client. It wraps a TcpClient + /// and its corresponding network streams. It is capable of working in 2 modes. Typically on the server side + /// you will need to enable continuous reading and events. On the client side you may want to disable continuous reading + /// and use the Read methods available. In continuous reading mode Read methods are not available and will throw + /// an invalid operation exceptions if they are used. + /// Continuous Reading Mode: Subscribe to data reception events, it runs a background thread, don't use Read methods + /// Manual Reading Mode: Data reception events are NEVER fired. No background threads are used. Use Read methods to receive data. + /// + /// + /// + /// The following code explains how to create a TCP server. + /// + /// using System.Text; + /// using Swan.Net; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new connection listener on a specific port + /// var connectionListener = new ConnectionListener(1337); + /// + /// // handle the OnConnectionAccepting event + /// connectionListener.OnConnectionAccepted += async (s, e) => + /// { + /// // create a new connection + /// using (var con = new Connection(e.Client)) + /// { + /// await con.WriteLineAsync("Hello world!"); + /// } + /// }; + /// + /// connectionListener.Start(); + /// Console.ReadLine)=ñ + /// } + /// } + /// + /// The following code describes how to create a TCP client. + /// + /// using System.Net.Sockets; + /// using System.Text; + /// using System.Threading.Tasks; + /// using Swan.Net; + /// + /// class Example + /// { + /// static async Task Main() + /// { + /// // create a new TcpClient object + /// var client = new TcpClient(); + /// + /// // connect to a specific address and port + /// client.Connect("localhost", 1337); + /// + /// //create a new connection with specific encoding, + /// //new line sequence and continuous reading disabled + /// using (var cn = new Connection(client, Encoding.UTF8, "\r\n", true, 0)) + /// { + /// var response = await cn.ReadTextAsync(); + /// } + /// } + /// } + /// + /// + public sealed class Connection : IDisposable + { + // New Line definitions for reading. This applies to both, events and read methods + private readonly string _newLineSequence; + + private readonly byte[] _newLineSequenceBytes; + private readonly char[] _newLineSequenceChars; + private readonly string[] _newLineSequenceLineSplitter; + private readonly byte[] _receiveBuffer; + private readonly TimeSpan _continuousReadingInterval = TimeSpan.FromMilliseconds(5); + private readonly Queue _readLineBuffer = new Queue(); + private readonly ManualResetEvent _writeDone = new ManualResetEvent(true); + + // Disconnect and Dispose + private bool _hasDisposed; + + private int _disconnectCalls; + + // Continuous Reading + private Thread _continuousReadingThread; + + private int _receiveBufferPointer; + + // Reading and writing + private Task _readTask; + + /// + /// Initializes a new instance of the class. + /// + /// The client. + /// The text encoding. + /// The new line sequence used for read and write operations. + /// if set to true [disable continuous reading]. + /// Size of the block. -- set to 0 or less to disable. + public Connection( + TcpClient client, + Encoding textEncoding, + string newLineSequence, + bool disableContinuousReading, + int blockSize) + { + // Setup basic properties + Id = Guid.NewGuid(); + TextEncoding = textEncoding; + + // Setup new line sequence + if (string.IsNullOrEmpty(newLineSequence)) + throw new ArgumentException("Argument cannot be null", nameof(newLineSequence)); + + _newLineSequence = newLineSequence; + _newLineSequenceBytes = TextEncoding.GetBytes(_newLineSequence); + _newLineSequenceChars = _newLineSequence.ToCharArray(); + _newLineSequenceLineSplitter = new[] { _newLineSequence }; + + // Setup Connection timers + ConnectionStartTimeUtc = DateTime.UtcNow; + DataReceivedLastTimeUtc = ConnectionStartTimeUtc; + DataSentLastTimeUtc = ConnectionStartTimeUtc; + + // Setup connection properties + RemoteClient = client; + LocalEndPoint = client.Client.LocalEndPoint as IPEndPoint; + NetworkStream = RemoteClient.GetStream(); + RemoteEndPoint = RemoteClient.Client.RemoteEndPoint as IPEndPoint; + + // Setup buffers + _receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2]; + ProtocolBlockSize = blockSize; + _receiveBufferPointer = 0; + + // Setup continuous reading mode if enabled + if (disableContinuousReading) return; + + ThreadPool.GetAvailableThreads(out var availableWorkerThreads, out _); + ThreadPool.GetMaxThreads(out var maxWorkerThreads, out _); + + var activeThreadPoolTreads = maxWorkerThreads - availableWorkerThreads; + + if (activeThreadPoolTreads < Environment.ProcessorCount / 4) + { + ThreadPool.QueueUserWorkItem(PerformContinuousReading, this); + } + else + { + new Thread(PerformContinuousReading) { IsBackground = true }.Start(); + } + } + + /// + /// Initializes a new instance of the class in continuous reading mode. + /// It uses UTF8 encoding, CRLF as a new line sequence and disables a protocol block size. + /// + /// The client. + public Connection(TcpClient client) + : this(client, Encoding.UTF8, "\r\n", false, 0) + { + // placeholder + } + + /// + /// Initializes a new instance of the class in continuous reading mode. + /// It uses UTF8 encoding, disables line sequences, and uses a protocol block size instead. + /// + /// The client. + /// Size of the block. + public Connection(TcpClient client, int blockSize) + : this(client, Encoding.UTF8, new string('\n', blockSize + 1), false, blockSize) + { + // placeholder + } + + #region Events + + /// + /// Occurs when the receive buffer has encounters a new line sequence, the buffer is flushed or the buffer is full. + /// + public event EventHandler DataReceived = (s, e) => { }; + + /// + /// Occurs when an error occurs while upgrading, sending, or receiving data in this client + /// + public event EventHandler ConnectionFailure = (s, e) => { }; + + /// + /// Occurs when a client is disconnected + /// + public event EventHandler ClientDisconnected = (s, e) => { }; + + #endregion + + #region Properties + + /// + /// Gets the unique identifier of this connection. + /// This field is filled out upon instantiation of this class. + /// + /// + /// The identifier. + /// + public Guid Id { get; } + + /// + /// Gets the active stream. Returns an SSL stream if the connection is secure, otherwise returns + /// the underlying NetworkStream. + /// + /// + /// The active stream. + /// + public Stream ActiveStream => SecureStream ?? NetworkStream as Stream; + + /// + /// Gets a value indicating whether the current connection stream is an SSL stream. + /// + /// + /// true if this instance is active stream secure; otherwise, false. + /// + public bool IsActiveStreamSecure => SecureStream != null; + + /// + /// Gets the text encoding for send and receive operations. + /// + /// + /// The text encoding. + /// + public Encoding TextEncoding { get; } + + /// + /// Gets the remote end point of this TCP connection. + /// + /// + /// The remote end point. + /// + public IPEndPoint RemoteEndPoint { get; } + + /// + /// Gets the local end point of this TCP connection. + /// + /// + /// The local end point. + /// + public IPEndPoint LocalEndPoint { get; } + + /// + /// Gets the remote client of this TCP connection. + /// + /// + /// The remote client. + /// + public TcpClient RemoteClient { get; private set; } + + /// + /// When in continuous reading mode, and if set to greater than 0, + /// a Data reception event will be fired whenever the amount of bytes + /// determined by this property has been received. Useful for fixed-length message protocols. + /// + /// + /// The size of the protocol block. + /// + public int ProtocolBlockSize { get; } + + /// + /// Gets a value indicating whether this connection is in continuous reading mode. + /// Remark: Whenever a disconnect event occurs, the background thread is terminated + /// and this property will return false whenever the reading thread is not active. + /// Therefore, even if continuous reading was not disabled in the constructor, this property + /// might return false. + /// + /// + /// true if this instance is continuous reading enabled; otherwise, false. + /// + public bool IsContinuousReadingEnabled => _continuousReadingThread != null; + + /// + /// Gets the start time at which the connection was started in UTC. + /// + /// + /// The connection start time UTC. + /// + public DateTime ConnectionStartTimeUtc { get; } + + /// + /// Gets the start time at which the connection was started in local time. + /// + /// + /// The connection start time. + /// + public DateTime ConnectionStartTime => ConnectionStartTimeUtc.ToLocalTime(); + + /// + /// Gets the duration of the connection. + /// + /// + /// The duration of the connection. + /// + public TimeSpan ConnectionDuration => DateTime.UtcNow.Subtract(ConnectionStartTimeUtc); + + /// + /// Gets the last time data was received at in UTC. + /// + /// + /// The data received last time UTC. + /// + public DateTime DataReceivedLastTimeUtc { get; private set; } + + /// + /// Gets how long has elapsed since data was last received. + /// + public TimeSpan DataReceivedIdleDuration => DateTime.UtcNow.Subtract(DataReceivedLastTimeUtc); + + /// + /// Gets the last time at which data was sent in UTC. + /// + /// + /// The data sent last time UTC. + /// + public DateTime DataSentLastTimeUtc { get; private set; } + + /// + /// Gets how long has elapsed since data was last sent. + /// + /// + /// The duration of the data sent idle. + /// + public TimeSpan DataSentIdleDuration => DateTime.UtcNow.Subtract(DataSentLastTimeUtc); + + /// + /// Gets a value indicating whether this connection is connected. + /// Remarks: This property polls the socket internally and checks if it is available to read data from it. + /// If disconnect has been called, then this property will return false. + /// + /// + /// true if this instance is connected; otherwise, false. + /// + public bool IsConnected + { + get + { + if (_disconnectCalls > 0) + return false; + + try + { + var socket = RemoteClient.Client; + var pollResult = !((socket.Poll(1000, SelectMode.SelectRead) + && (NetworkStream.DataAvailable == false)) || !socket.Connected); + + if (pollResult == false) + Disconnect(); + + return pollResult; + } + catch + { + Disconnect(); + return false; + } + } + } + + private NetworkStream NetworkStream { get; set; } + + private SslStream SecureStream { get; set; } + + #endregion + + #region Read Methods + + /// + /// Reads data from the remote client asynchronously and with the given timeout. + /// + /// The timeout. + /// The cancellation token. + /// A byte array containing the results of encoding the specified set of characters. + /// Read methods have been disabled because continuous reading is enabled. + /// Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} m. + public async Task ReadDataAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + { + if (IsContinuousReadingEnabled) + { + throw new InvalidOperationException( + "Read methods have been disabled because continuous reading is enabled."); + } + + if (RemoteClient == null) + { + throw new InvalidOperationException("An open connection is required"); + } + + var receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2]; + var receiveBuilder = new List(receiveBuffer.Length); + + try + { + var startTime = DateTime.UtcNow; + + while (receiveBuilder.Count <= 0) + { + if (DateTime.UtcNow.Subtract(startTime) >= timeout) + { + throw new TimeoutException( + $"Reading data from {ActiveStream} timed out in {timeout.TotalMilliseconds} ms"); + } + + if (_readTask == null) + _readTask = ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken); + + if (_readTask.Wait(_continuousReadingInterval)) + { + var bytesReceivedCount = _readTask.Result; + if (bytesReceivedCount > 0) + { + DataReceivedLastTimeUtc = DateTime.UtcNow; + var buffer = new byte[bytesReceivedCount]; + Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount); + receiveBuilder.AddRange(buffer); + } + + _readTask = null; + } + else + { + await Task.Delay(_continuousReadingInterval, cancellationToken).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + ex.Error(typeof(Connection).FullName, "Error while reading network stream data asynchronously."); + throw; + } + + return receiveBuilder.ToArray(); + } + + /// + /// Reads data asynchronously from the remote stream with a 5000 millisecond timeout. + /// + /// The cancellation token. + /// + /// A byte array containing the results the specified sequence of bytes. + /// + public Task ReadDataAsync(CancellationToken cancellationToken = default) + => ReadDataAsync(TimeSpan.FromSeconds(5), cancellationToken); + + /// + /// Asynchronously reads data as text with the given timeout. + /// + /// The timeout. + /// The cancellation token. + /// + /// A that contains the results of decoding the specified sequence of bytes. + /// + public async Task ReadTextAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + { + var buffer = await ReadDataAsync(timeout, cancellationToken).ConfigureAwait(false); + return buffer == null ? null : TextEncoding.GetString(buffer); + } + + /// + /// Asynchronously reads data as text with a 5000 millisecond timeout. + /// + /// The cancellation token. + /// + /// When this method completes successfully, it returns the contents of the file as a text string. + /// + public Task ReadTextAsync(CancellationToken cancellationToken = default) + => ReadTextAsync(TimeSpan.FromSeconds(5), cancellationToken); + + /// + /// Performs the same task as this method's overload but it defaults to a read timeout of 30 seconds. + /// + /// The cancellation token. + /// + /// A task that represents the asynchronous read operation. The value of the TResult parameter + /// contains the next line from the stream, or is null if all the characters have been read. + /// + public Task ReadLineAsync(CancellationToken cancellationToken = default) + => ReadLineAsync(TimeSpan.FromSeconds(30), cancellationToken); + + /// + /// Reads the next available line of text in queue. Return null when no text is read. + /// This method differs from the rest of the read methods because it keeps an internal + /// queue of lines that are read from the stream and only returns the one line next in the queue. + /// It is only recommended to use this method when you are working with text-based protocols + /// and the rest of the read methods are not called. + /// + /// The timeout. + /// The cancellation token. + /// A task with a string line from the queue. + /// Read methods have been disabled because continuous reading is enabled. + public async Task ReadLineAsync(TimeSpan timeout, CancellationToken cancellationToken = default) + { + if (IsContinuousReadingEnabled) + { + throw new InvalidOperationException( + "Read methods have been disabled because continuous reading is enabled."); + } + + if (_readLineBuffer.Count > 0) + return _readLineBuffer.Dequeue(); + + var builder = new StringBuilder(); + + while (true) + { + var text = await ReadTextAsync(timeout, cancellationToken).ConfigureAwait(false); + + if (string.IsNullOrEmpty(text)) + break; + + builder.Append(text); + + if (!text.EndsWith(_newLineSequence)) continue; + + var lines = builder.ToString().TrimEnd(_newLineSequenceChars) + .Split(_newLineSequenceLineSplitter, StringSplitOptions.None); + foreach (var item in lines) + _readLineBuffer.Enqueue(item); + + break; + } + + return _readLineBuffer.Count > 0 ? _readLineBuffer.Dequeue() : null; + } + + #endregion + + #region Write Methods + + /// + /// Writes data asynchronously. + /// + /// The buffer. + /// if set to true [force flush]. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public async Task WriteDataAsync(byte[] buffer, bool forceFlush, CancellationToken cancellationToken = default) + { + try + { + _writeDone.WaitOne(); + _writeDone.Reset(); + await ActiveStream.WriteAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + if (forceFlush) + await ActiveStream.FlushAsync(cancellationToken).ConfigureAwait(false); + + DataSentLastTimeUtc = DateTime.UtcNow; + } + finally + { + _writeDone.Set(); + } + } + + /// + /// Writes text asynchronously. + /// + /// The text. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteTextAsync(string text, CancellationToken cancellationToken = default) + => WriteTextAsync(text, TextEncoding, cancellationToken); + + /// + /// Writes text asynchronously. + /// + /// The text. + /// The encoding. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteTextAsync(string text, Encoding encoding, CancellationToken cancellationToken = default) + => WriteDataAsync(encoding.GetBytes(text), true, cancellationToken); + + /// + /// Writes a line of text asynchronously. + /// The new line sequence is added automatically at the end of the line. + /// + /// The line. + /// The encoding. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteLineAsync(string line, Encoding encoding, CancellationToken cancellationToken = default) + => WriteDataAsync(encoding.GetBytes($"{line}{_newLineSequence}"), true, cancellationToken); + + /// + /// Writes a line of text asynchronously. + /// The new line sequence is added automatically at the end of the line. + /// + /// The line. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public Task WriteLineAsync(string line, CancellationToken cancellationToken = default) + => WriteLineAsync(line, TextEncoding, cancellationToken); + + #endregion + + #region Socket Methods + + /// + /// Upgrades the active stream to an SSL stream if this connection object is hosted in the server. + /// + /// The server certificate. + /// true if the object is hosted in the server; otherwise, false. + public async Task UpgradeToSecureAsServerAsync(X509Certificate2 serverCertificate) + { + if (IsActiveStreamSecure) + return true; + + _writeDone.WaitOne(); + + SslStream? secureStream = null; + + try + { + secureStream = new SslStream(NetworkStream, true); + await secureStream.AuthenticateAsServerAsync(serverCertificate).ConfigureAwait(false); + SecureStream = secureStream; + return true; + } + catch (Exception ex) + { + ConnectionFailure(this, new ConnectionFailureEventArgs(ex)); + secureStream?.Dispose(); + + return false; + } + } + + /// + /// Upgrades the active stream to an SSL stream if this connection object is hosted in the client. + /// + /// The hostname. + /// The callback. + /// A tasks with true if the upgrade to SSL was successful; otherwise, false. + public async Task UpgradeToSecureAsClientAsync( + string? hostname = null, + RemoteCertificateValidationCallback? callback = null) + { + if (IsActiveStreamSecure) + return true; + + var secureStream = callback == null + ? new SslStream(NetworkStream, true) + : new SslStream(NetworkStream, true, callback); + + try + { + await secureStream.AuthenticateAsClientAsync(hostname ?? Network.HostName.ToLowerInvariant()).ConfigureAwait(false); + SecureStream = secureStream; + } + catch (Exception ex) + { + secureStream.Dispose(); + ConnectionFailure(this, new ConnectionFailureEventArgs(ex)); + return false; + } + + return true; + } + + /// + /// Disconnects this connection. + /// + public void Disconnect() + { + if (_disconnectCalls > 0) + return; + + _disconnectCalls++; + _writeDone.WaitOne(); + + try + { + ClientDisconnected(this, EventArgs.Empty); + } + catch + { + // ignore + } + + try + { +#if !NET461 + RemoteClient.Dispose(); + SecureStream?.Dispose(); + NetworkStream?.Dispose(); +#else + RemoteClient.Close(); + SecureStream?.Close(); + NetworkStream?.Close(); +#endif + } + finally + { + NetworkStream = null; + SecureStream = null; + RemoteClient = null; + _continuousReadingThread = null; + } + } + + #endregion + + #region Dispose + + /// + public void Dispose() + { + if (_hasDisposed) + return; + + // Release managed resources + Disconnect(); + _continuousReadingThread = null; + _writeDone.Dispose(); + + _hasDisposed = true; + } + + #endregion + + #region Continuous Read Methods + + private void RaiseReceiveBufferEvents(IEnumerable receivedData) + { + var moreAvailable = RemoteClient.Available > 0; + + foreach (var data in receivedData) + { + ProcessReceivedBlock(data, moreAvailable); + } + + // Check if we are left with some more stuff to handle + if (_receiveBufferPointer <= 0) + return; + + // Extract the segments split by newline terminated bytes + var sequences = _receiveBuffer.Skip(0).Take(_receiveBufferPointer).ToArray() + .Split(0, _newLineSequenceBytes); + + // Something really wrong happened + if (sequences.Count == 0) + throw new InvalidOperationException("Split function failed! This is terribly wrong!"); + + // We only have one sequence and it is not newline-terminated + // we don't have to do anything. + if (sequences.Count == 1 && sequences[0].EndsWith(_newLineSequenceBytes) == false) + return; + + // Process the events for each sequence + for (var i = 0; i < sequences.Count; i++) + { + var sequenceBytes = sequences[i]; + var isNewLineTerminated = sequences[i].EndsWith(_newLineSequenceBytes); + var isLast = i == sequences.Count - 1; + + if (isNewLineTerminated) + { + var eventArgs = new ConnectionDataReceivedEventArgs( + sequenceBytes, + ConnectionDataReceivedTrigger.NewLineSequenceEncountered, + isLast == false); + DataReceived(this, eventArgs); + } + + // Depending on the last segment determine what to do with the receive buffer + if (!isLast) continue; + + if (isNewLineTerminated) + { + // Simply reset the buffer pointer if the last segment was also terminated + _receiveBufferPointer = 0; + } + else + { + // If we have not received the termination sequence, then just shift the receive buffer to the left + // and adjust the pointer + Array.Copy(sequenceBytes, _receiveBuffer, sequenceBytes.Length); + _receiveBufferPointer = sequenceBytes.Length; + } + } + } + + private void ProcessReceivedBlock(byte data, bool moreAvailable) + { + _receiveBuffer[_receiveBufferPointer] = data; + _receiveBufferPointer++; + + // Block size reached + if (ProtocolBlockSize > 0 && _receiveBufferPointer >= ProtocolBlockSize) + { + SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BlockSizeReached); + return; + } + + // The receive buffer is full. Time to flush + if (_receiveBufferPointer >= _receiveBuffer.Length) + { + SendBuffer(moreAvailable, ConnectionDataReceivedTrigger.BufferFull); + } + } + + private void SendBuffer(bool moreAvailable, ConnectionDataReceivedTrigger trigger) + { + var eventBuffer = new byte[_receiveBuffer.Length]; + Array.Copy(_receiveBuffer, eventBuffer, eventBuffer.Length); + + DataReceived(this, + new ConnectionDataReceivedEventArgs( + eventBuffer, + trigger, + moreAvailable)); + _receiveBufferPointer = 0; + } + + private void PerformContinuousReading(object threadContext) + { + _continuousReadingThread = Thread.CurrentThread; + + // Check if the RemoteClient is still there + if (RemoteClient == null) return; + + var receiveBuffer = new byte[RemoteClient.ReceiveBufferSize * 2]; + + while (IsConnected && _disconnectCalls <= 0) + { + var doThreadSleep = false; + + try + { + if (_readTask == null) + _readTask = ActiveStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length); + + if (_readTask.Wait(_continuousReadingInterval)) + { + var bytesReceivedCount = _readTask.Result; + if (bytesReceivedCount > 0) + { + DataReceivedLastTimeUtc = DateTime.UtcNow; + var buffer = new byte[bytesReceivedCount]; + Array.Copy(receiveBuffer, 0, buffer, 0, bytesReceivedCount); + RaiseReceiveBufferEvents(buffer); + } + + _readTask = null; + } + else + { + doThreadSleep = _disconnectCalls <= 0; + } + } + catch (Exception ex) + { + ex.Log(nameof(PerformContinuousReading), "Continuous Read operation errored"); + } + finally + { + if (doThreadSleep) + Thread.Sleep(_continuousReadingInterval); + } + } + } + + #endregion + } +} diff --git a/Swan/Net/ConnectionDataReceivedTrigger.cs b/Swan/Net/ConnectionDataReceivedTrigger.cs new file mode 100644 index 0000000..e6d592d --- /dev/null +++ b/Swan/Net/ConnectionDataReceivedTrigger.cs @@ -0,0 +1,28 @@ +namespace Swan +{ + /// + /// Enumerates the possible causes of the DataReceived event occurring. + /// + public enum ConnectionDataReceivedTrigger + { + /// + /// The trigger was a forceful flush of the buffer + /// + Flush, + + /// + /// The new line sequence bytes were received + /// + NewLineSequenceEncountered, + + /// + /// The buffer was full + /// + BufferFull, + + /// + /// The block size reached + /// + BlockSizeReached, + } +} diff --git a/Swan/Net/ConnectionListener.cs b/Swan/Net/ConnectionListener.cs new file mode 100644 index 0000000..1c4b2bb --- /dev/null +++ b/Swan/Net/ConnectionListener.cs @@ -0,0 +1,253 @@ +namespace Swan.Net +{ + using System; + using System.Net; + using System.Net.Sockets; + using System.Threading; + using System.Threading.Tasks; + + /// + /// TCP Listener manager with built-in events and asynchronous functionality. + /// This networking component is typically used when writing server software. + /// + /// + public sealed class ConnectionListener : IDisposable + { + private readonly object _stateLock = new object(); + private TcpListener _listenerSocket; + private bool _cancellationPending; + private CancellationTokenSource _cancelListening; + private Task? _backgroundWorkerTask; + private bool _hasDisposed; + + #region Events + + /// + /// Occurs when a new connection requests a socket from the listener. + /// Set Cancel = true to prevent the TCP client from being accepted. + /// + public event EventHandler OnConnectionAccepting = (s, e) => { }; + + /// + /// Occurs when a new connection is accepted. + /// + public event EventHandler OnConnectionAccepted = (s, e) => { }; + + /// + /// Occurs when a connection fails to get accepted + /// + public event EventHandler OnConnectionFailure = (s, e) => { }; + + /// + /// Occurs when the listener stops. + /// + public event EventHandler OnListenerStopped = (s, e) => { }; + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The listen end point. + public ConnectionListener(IPEndPoint listenEndPoint) + { + Id = Guid.NewGuid(); + LocalEndPoint = listenEndPoint ?? throw new ArgumentNullException(nameof(listenEndPoint)); + } + + /// + /// Initializes a new instance of the class. + /// It uses the loopback address for listening. + /// + /// The listen port. + public ConnectionListener(int listenPort) + : this(new IPEndPoint(IPAddress.Loopback, listenPort)) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The listen address. + /// The listen port. + public ConnectionListener(IPAddress listenAddress, int listenPort) + : this(new IPEndPoint(listenAddress, listenPort)) + { + } + + /// + /// Finalizes an instance of the class. + /// + ~ConnectionListener() + { + Dispose(false); + } + + #endregion + + #region Public Properties + + /// + /// Gets the local end point on which we are listening. + /// + /// + /// The local end point. + /// + public IPEndPoint LocalEndPoint { get; } + + /// + /// Gets a value indicating whether this listener is active. + /// + /// + /// true if this instance is listening; otherwise, false. + /// + public bool IsListening => _backgroundWorkerTask != null; + + /// + /// Gets a unique identifier that gets automatically assigned upon instantiation of this class. + /// + /// + /// The unique identifier. + /// + public Guid Id { get; } + + #endregion + + #region Start and Stop + + /// + /// Starts the listener in an asynchronous, non-blocking fashion. + /// Subscribe to the events of this class to gain access to connected client sockets. + /// + /// Cancellation has already been requested. This listener is not reusable. + public void Start() + { + lock (_stateLock) + { + if (_backgroundWorkerTask != null) + { + return; + } + + if (_cancellationPending) + { + throw new InvalidOperationException( + "Cancellation has already been requested. This listener is not reusable."); + } + + _backgroundWorkerTask = DoWorkAsync(); + } + } + + /// + /// Stops the listener from receiving new connections. + /// This does not prevent the listener from . + /// + public void Stop() + { + lock (_stateLock) + { + _cancellationPending = true; + _listenerSocket?.Stop(); + _cancelListening?.Cancel(); + _backgroundWorkerTask?.Wait(); + _backgroundWorkerTask = null; + _cancellationPending = false; + } + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => LocalEndPoint.ToString(); + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + private void Dispose(bool disposing) + { + if (_hasDisposed) + return; + + if (disposing) + { + // Release managed resources + Stop(); + } + + _hasDisposed = true; + } + + /// + /// Continuously checks for client connections until the Close method has been called. + /// + /// A task that represents the asynchronous connection operation. + private async Task DoWorkAsync() + { + _cancellationPending = false; + _listenerSocket = new TcpListener(LocalEndPoint); + _listenerSocket.Start(); + _cancelListening = new CancellationTokenSource(); + + try + { + while (_cancellationPending == false) + { + try + { + var client = await Task.Run(() => _listenerSocket.AcceptTcpClientAsync(), _cancelListening.Token).ConfigureAwait(false); + var acceptingArgs = new ConnectionAcceptingEventArgs(client); + OnConnectionAccepting(this, acceptingArgs); + + if (acceptingArgs.Cancel) + { +#if !NET461 + client.Dispose(); +#else + client.Close(); +#endif + continue; + } + + OnConnectionAccepted(this, new ConnectionAcceptedEventArgs(client)); + } + catch (Exception ex) + { + OnConnectionFailure(this, new ConnectionFailureEventArgs(ex)); + } + } + + OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint)); + } + catch (ObjectDisposedException) + { + OnListenerStopped(this, new ConnectionListenerStoppedEventArgs(LocalEndPoint)); + } + catch (Exception ex) + { + OnListenerStopped(this, + new ConnectionListenerStoppedEventArgs(LocalEndPoint, _cancellationPending ? null : ex)); + } + finally + { + _backgroundWorkerTask = null; + _cancellationPending = false; + } + } + + #endregion + } +} diff --git a/Swan/Net/Dns/DnsClient.Interfaces.cs b/Swan/Net/Dns/DnsClient.Interfaces.cs new file mode 100644 index 0000000..1d301f1 --- /dev/null +++ b/Swan/Net/Dns/DnsClient.Interfaces.cs @@ -0,0 +1,62 @@ +namespace Swan.Net.Dns +{ + using System; + using System.Threading.Tasks; + using System.Collections.Generic; + + /// + /// DnsClient public interfaces. + /// + internal partial class DnsClient + { + public interface IDnsMessage + { + IList Questions { get; } + + int Size { get; } + byte[] ToArray(); + } + + public interface IDnsMessageEntry + { + DnsDomain Name { get; } + DnsRecordType Type { get; } + DnsRecordClass Class { get; } + + int Size { get; } + byte[] ToArray(); + } + + public interface IDnsResourceRecord : IDnsMessageEntry + { + TimeSpan TimeToLive { get; } + int DataLength { get; } + byte[] Data { get; } + } + + public interface IDnsRequest : IDnsMessage + { + int Id { get; set; } + DnsOperationCode OperationCode { get; set; } + bool RecursionDesired { get; set; } + } + + public interface IDnsResponse : IDnsMessage + { + int Id { get; set; } + IList AnswerRecords { get; } + IList AuthorityRecords { get; } + IList AdditionalRecords { get; } + bool IsRecursionAvailable { get; set; } + bool IsAuthorativeServer { get; set; } + bool IsTruncated { get; set; } + DnsOperationCode OperationCode { get; set; } + DnsResponseCode ResponseCode { get; set; } + } + + public interface IDnsRequestResolver + { + Task Request(DnsClientRequest request); + } + } +} diff --git a/Swan/Net/Dns/DnsClient.Request.cs b/Swan/Net/Dns/DnsClient.Request.cs new file mode 100644 index 0000000..e962844 --- /dev/null +++ b/Swan/Net/Dns/DnsClient.Request.cs @@ -0,0 +1,681 @@ +namespace Swan.Net.Dns +{ + using Formatters; + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading.Tasks; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Runtime.InteropServices; + using System.Text; + + /// + /// DnsClient Request inner class. + /// + internal partial class DnsClient + { + public class DnsClientRequest : IDnsRequest + { + private readonly IDnsRequestResolver _resolver; + private readonly IDnsRequest _request; + + public DnsClientRequest(IPEndPoint dns, IDnsRequest? request = null, IDnsRequestResolver? resolver = null) + { + Dns = dns; + _request = request == null ? new DnsRequest() : new DnsRequest(request); + _resolver = resolver ?? new DnsUdpRequestResolver(); + } + + public int Id + { + get => _request.Id; + set => _request.Id = value; + } + + public DnsOperationCode OperationCode + { + get => _request.OperationCode; + set => _request.OperationCode = value; + } + + public bool RecursionDesired + { + get => _request.RecursionDesired; + set => _request.RecursionDesired = value; + } + + public IList Questions => _request.Questions; + + public int Size => _request.Size; + + public IPEndPoint Dns { get; set; } + + public byte[] ToArray() => _request.ToArray(); + + public override string ToString() => _request.ToString(); + + /// + /// Resolves this request into a response using the provided DNS information. The given + /// request strategy is used to retrieve the response. + /// + /// Throw if a malformed response is received from the server. + /// Thrown if a IO error occurs. + /// Thrown if a the reading or writing to the socket fails. + /// The response received from server. + public async Task Resolve() + { + try + { + var response = await _resolver.Request(this).ConfigureAwait(false); + + if (response.Id != Id) + { + throw new DnsQueryException(response, "Mismatching request/response IDs"); + } + + if (response.ResponseCode != DnsResponseCode.NoError) + { + throw new DnsQueryException(response); + } + + return response; + } + catch (Exception e) + { + if (e is ArgumentException || e is SocketException) + throw new DnsQueryException("Invalid response", e); + + throw; + } + } + } + + public class DnsRequest : IDnsRequest + { + private static readonly Random Random = new Random(); + + private DnsHeader header; + + public DnsRequest() + { + Questions = new List(); + header = new DnsHeader + { + OperationCode = DnsOperationCode.Query, + Response = false, + Id = Random.Next(ushort.MaxValue), + }; + } + + public DnsRequest(IDnsRequest request) + { + header = new DnsHeader(); + Questions = new List(request.Questions); + + header.Response = false; + + Id = request.Id; + OperationCode = request.OperationCode; + RecursionDesired = request.RecursionDesired; + } + + public IList Questions { get; } + + public int Size => header.Size + Questions.Sum(q => q.Size); + + public int Id + { + get => header.Id; + set => header.Id = value; + } + + public DnsOperationCode OperationCode + { + get => header.OperationCode; + set => header.OperationCode = value; + } + + public bool RecursionDesired + { + get => header.RecursionDesired; + set => header.RecursionDesired = value; + } + + public byte[] ToArray() + { + UpdateHeader(); + using var result = new MemoryStream(Size); + + return result + .Append(header.ToArray()) + .Append(Questions.Select(q => q.ToArray())) + .ToArray(); + } + + public override string ToString() + { + UpdateHeader(); + + return Json.Serialize(this, true); + } + + private void UpdateHeader() + { + header.QuestionCount = Questions.Count; + } + } + + public class DnsTcpRequestResolver : IDnsRequestResolver + { + public async Task Request(DnsClientRequest request) + { + var tcp = new TcpClient(); + + try + { +#if !NET461 + await tcp.Client.ConnectAsync(request.Dns).ConfigureAwait(false); +#else + tcp.Client.Connect(request.Dns); +#endif + var stream = tcp.GetStream(); + var buffer = request.ToArray(); + var length = BitConverter.GetBytes((ushort)buffer.Length); + + if (BitConverter.IsLittleEndian) + Array.Reverse(length); + + await stream.WriteAsync(length, 0, length.Length).ConfigureAwait(false); + await stream.WriteAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + + buffer = new byte[2]; + await Read(stream, buffer).ConfigureAwait(false); + + if (BitConverter.IsLittleEndian) + Array.Reverse(buffer); + + buffer = new byte[BitConverter.ToUInt16(buffer, 0)]; + await Read(stream, buffer).ConfigureAwait(false); + + var response = DnsResponse.FromArray(buffer); + + return new DnsClientResponse(request, response, buffer); + } + finally + { +#if NET461 + tcp.Close(); +#else + tcp.Dispose(); +#endif + } + } + + private static async Task Read(Stream stream, byte[] buffer) + { + var length = buffer.Length; + var offset = 0; + int size; + + while (length > 0 && (size = await stream.ReadAsync(buffer, offset, length).ConfigureAwait(false)) > 0) + { + offset += size; + length -= size; + } + + if (length > 0) + { + throw new IOException("Unexpected end of stream"); + } + } + } + + public class DnsUdpRequestResolver : IDnsRequestResolver + { + private readonly IDnsRequestResolver _fallback; + + public DnsUdpRequestResolver(IDnsRequestResolver fallback) + { + _fallback = fallback; + } + + public DnsUdpRequestResolver() + { + _fallback = new DnsNullRequestResolver(); + } + + public async Task Request(DnsClientRequest request) + { + var udp = new UdpClient(); + var dns = request.Dns; + + try + { + udp.Client.SendTimeout = 7000; + udp.Client.ReceiveTimeout = 7000; +#if !NET461 + await udp.Client.ConnectAsync(dns).ConfigureAwait(false); +#else + udp.Client.Connect(dns); +#endif + + await udp.SendAsync(request.ToArray(), request.Size).ConfigureAwait(false); + + var bufferList = new List(); + + do + { + var tempBuffer = new byte[1024]; + var receiveCount = udp.Client.Receive(tempBuffer); + bufferList.AddRange(tempBuffer.Skip(0).Take(receiveCount)); + } + while (udp.Client.Available > 0 || bufferList.Count == 0); + + var buffer = bufferList.ToArray(); + var response = DnsResponse.FromArray(buffer); + + return response.IsTruncated + ? await _fallback.Request(request).ConfigureAwait(false) + : new DnsClientResponse(request, response, buffer); + } + finally + { +#if NET461 + udp.Close(); +#else + udp.Dispose(); +#endif + } + } + } + + public class DnsNullRequestResolver : IDnsRequestResolver + { + public Task Request(DnsClientRequest request) => throw new DnsQueryException("Request failed"); + } + + // 12 bytes message header + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct DnsHeader + { + public const int SIZE = 12; + + private ushort id; + + private byte flag0; + private byte flag1; + + // Question count: number of questions in the Question section + private ushort questionCount; + + // Answer record count: number of records in the Answer section + private ushort answerCount; + + // Authority record count: number of records in the Authority section + private ushort authorityCount; + + // Additional record count: number of records in the Additional section + private ushort addtionalCount; + + public int Id + { + get => id; + set => id = (ushort)value; + } + + public int QuestionCount + { + get => questionCount; + set => questionCount = (ushort)value; + } + + public int AnswerRecordCount + { + get => answerCount; + set => answerCount = (ushort)value; + } + + public int AuthorityRecordCount + { + get => authorityCount; + set => authorityCount = (ushort)value; + } + + public int AdditionalRecordCount + { + get => addtionalCount; + set => addtionalCount = (ushort)value; + } + + public bool Response + { + get => Qr == 1; + set => Qr = Convert.ToByte(value); + } + + public DnsOperationCode OperationCode + { + get => (DnsOperationCode)Opcode; + set => Opcode = (byte)value; + } + + public bool AuthorativeServer + { + get => Aa == 1; + set => Aa = Convert.ToByte(value); + } + + public bool Truncated + { + get => Tc == 1; + set => Tc = Convert.ToByte(value); + } + + public bool RecursionDesired + { + get => Rd == 1; + set => Rd = Convert.ToByte(value); + } + + public bool RecursionAvailable + { + get => Ra == 1; + set => Ra = Convert.ToByte(value); + } + + public DnsResponseCode ResponseCode + { + get => (DnsResponseCode)RCode; + set => RCode = (byte)value; + } + + public int Size => SIZE; + + // Query/Response Flag + private byte Qr + { + get => Flag0.GetBitValueAt(7); + set => Flag0 = Flag0.SetBitValueAt(7, 1, value); + } + + // Operation Code + private byte Opcode + { + get => Flag0.GetBitValueAt(3, 4); + set => Flag0 = Flag0.SetBitValueAt(3, 4, value); + } + + // Authorative Answer Flag + private byte Aa + { + get => Flag0.GetBitValueAt(2); + set => Flag0 = Flag0.SetBitValueAt(2, 1, value); + } + + // Truncation Flag + private byte Tc + { + get => Flag0.GetBitValueAt(1); + set => Flag0 = Flag0.SetBitValueAt(1, 1, value); + } + + // Recursion Desired + private byte Rd + { + get => Flag0.GetBitValueAt(0); + set => Flag0 = Flag0.SetBitValueAt(0, 1, value); + } + + // Recursion Available + private byte Ra + { + get => Flag1.GetBitValueAt(7); + set => Flag1 = Flag1.SetBitValueAt(7, 1, value); + } + + // Zero (Reserved) + private byte Z + { + get => Flag1.GetBitValueAt(4, 3); + set { } + } + + // Response Code + private byte RCode + { + get => Flag1.GetBitValueAt(0, 4); + set => Flag1 = Flag1.SetBitValueAt(0, 4, value); + } + + private byte Flag0 + { + get => flag0; + set => flag0 = value; + } + + private byte Flag1 + { + get => flag1; + set => flag1 = value; + } + + public static DnsHeader FromArray(byte[] header) => + header.Length < SIZE + ? throw new ArgumentException("Header length too small") + : header.ToStruct(0, SIZE); + + public byte[] ToArray() => this.ToBytes(); + + public override string ToString() + => Json.SerializeExcluding(this, true, nameof(Size)); + } + + public class DnsDomain : IComparable + { + private readonly string[] _labels; + + public DnsDomain(string domain) + : this(domain.Split('.')) + { + } + + public DnsDomain(string[] labels) + { + _labels = labels; + } + + public int Size => _labels.Sum(l => l.Length) + _labels.Length + 1; + + public static DnsDomain FromArray(byte[] message, int offset) + => FromArray(message, offset, out offset); + + public static DnsDomain FromArray(byte[] message, int offset, out int endOffset) + { + var labels = new List(); + var endOffsetAssigned = false; + endOffset = 0; + byte lengthOrPointer; + + while ((lengthOrPointer = message[offset++]) > 0) + { + // Two heighest bits are set (pointer) + if (lengthOrPointer.GetBitValueAt(6, 2) == 3) + { + if (!endOffsetAssigned) + { + endOffsetAssigned = true; + endOffset = offset + 1; + } + + ushort pointer = lengthOrPointer.GetBitValueAt(0, 6); + offset = (pointer << 8) | message[offset]; + + continue; + } + + if (lengthOrPointer.GetBitValueAt(6, 2) != 0) + { + throw new ArgumentException("Unexpected bit pattern in label length"); + } + + var length = lengthOrPointer; + var label = new byte[length]; + Array.Copy(message, offset, label, 0, length); + + labels.Add(label); + + offset += length; + } + + if (!endOffsetAssigned) + { + endOffset = offset; + } + + return new DnsDomain(labels.Select(l => l.ToText(Encoding.ASCII)).ToArray()); + } + + public static DnsDomain PointerName(IPAddress ip) + => new DnsDomain(FormatReverseIP(ip)); + + public byte[] ToArray() + { + var result = new byte[Size]; + var offset = 0; + + foreach (var l in _labels.Select(label => Encoding.ASCII.GetBytes(label))) + { + result[offset++] = (byte)l.Length; + l.CopyTo(result, offset); + + offset += l.Length; + } + + result[offset] = 0; + + return result; + } + + public override string ToString() + => string.Join(".", _labels); + + public int CompareTo(DnsDomain other) + => string.Compare(ToString(), other.ToString(), StringComparison.Ordinal); + + public override bool Equals(object obj) + => obj is DnsDomain domain && CompareTo(domain) == 0; + + public override int GetHashCode() => ToString().GetHashCode(); + + private static string FormatReverseIP(IPAddress ip) + { + var address = ip.GetAddressBytes(); + + if (address.Length == 4) + { + return string.Join(".", address.Reverse().Select(b => b.ToString())) + ".in-addr.arpa"; + } + + var nibbles = new byte[address.Length * 2]; + + for (int i = 0, j = 0; i < address.Length; i++, j = 2 * i) + { + var b = address[i]; + + nibbles[j] = b.GetBitValueAt(4, 4); + nibbles[j + 1] = b.GetBitValueAt(0, 4); + } + + return string.Join(".", nibbles.Reverse().Select(b => b.ToString("x"))) + ".ip6.arpa"; + } + } + + public class DnsQuestion : IDnsMessageEntry + { + private readonly DnsRecordType _type; + private readonly DnsRecordClass _klass; + + public static IList GetAllFromArray(byte[] message, int offset, int questionCount) => + GetAllFromArray(message, offset, questionCount, out offset); + + public static IList GetAllFromArray( + byte[] message, + int offset, + int questionCount, + out int endOffset) + { + IList questions = new List(questionCount); + + for (var i = 0; i < questionCount; i++) + { + questions.Add(FromArray(message, offset, out offset)); + } + + endOffset = offset; + return questions; + } + + public static DnsQuestion FromArray(byte[] message, int offset, out int endOffset) + { + var domain = DnsDomain.FromArray(message, offset, out offset); + var tail = message.ToStruct(offset, Tail.SIZE); + + endOffset = offset + Tail.SIZE; + + return new DnsQuestion(domain, tail.Type, tail.Class); + } + + public DnsQuestion( + DnsDomain domain, + DnsRecordType type = DnsRecordType.A, + DnsRecordClass klass = DnsRecordClass.IN) + { + Name = domain; + _type = type; + _klass = klass; + } + + public DnsDomain Name { get; } + + public DnsRecordType Type => _type; + + public DnsRecordClass Class => _klass; + + public int Size => Name.Size + Tail.SIZE; + + public byte[] ToArray() => + new MemoryStream(Size) + .Append(Name.ToArray()) + .Append(new Tail { Type = Type, Class = Class }.ToBytes()) + .ToArray(); + + public override string ToString() + => Json.SerializeOnly(this, true, nameof(Name), nameof(Type), nameof(Class)); + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Tail + { + public const int SIZE = 4; + + private ushort type; + private ushort klass; + + public DnsRecordType Type + { + get => (DnsRecordType)type; + set => type = (ushort)value; + } + + public DnsRecordClass Class + { + get => (DnsRecordClass)klass; + set => klass = (ushort)value; + } + } + } + } +} diff --git a/Swan/Net/Dns/DnsClient.ResourceRecords.cs b/Swan/Net/Dns/DnsClient.ResourceRecords.cs new file mode 100644 index 0000000..1ad6c38 --- /dev/null +++ b/Swan/Net/Dns/DnsClient.ResourceRecords.cs @@ -0,0 +1,419 @@ +namespace Swan.Net.Dns +{ + using Formatters; + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using System.Runtime.InteropServices; + + /// + /// DnsClient public methods. + /// + internal partial class DnsClient + { + public abstract class DnsResourceRecordBase : IDnsResourceRecord + { + private readonly IDnsResourceRecord _record; + + protected DnsResourceRecordBase(IDnsResourceRecord record) + { + _record = record; + } + + public DnsDomain Name => _record.Name; + + public DnsRecordType Type => _record.Type; + + public DnsRecordClass Class => _record.Class; + + public TimeSpan TimeToLive => _record.TimeToLive; + + public int DataLength => _record.DataLength; + + public byte[] Data => _record.Data; + + public int Size => _record.Size; + + protected virtual string[] IncludedProperties + => new[] {nameof(Name), nameof(Type), nameof(Class), nameof(TimeToLive), nameof(DataLength)}; + + public byte[] ToArray() => _record.ToArray(); + + public override string ToString() + => Json.SerializeOnly(this, true, IncludedProperties); + } + + public class DnsResourceRecord : IDnsResourceRecord + { + public DnsResourceRecord( + DnsDomain domain, + byte[] data, + DnsRecordType type, + DnsRecordClass klass = DnsRecordClass.IN, + TimeSpan ttl = default) + { + Name = domain; + Type = type; + Class = klass; + TimeToLive = ttl; + Data = data; + } + + public DnsDomain Name { get; } + + public DnsRecordType Type { get; } + + public DnsRecordClass Class { get; } + + public TimeSpan TimeToLive { get; } + + public int DataLength => Data.Length; + + public byte[] Data { get; } + + public int Size => Name.Size + Tail.SIZE + Data.Length; + + public static DnsResourceRecord FromArray(byte[] message, int offset, out int endOffset) + { + var domain = DnsDomain.FromArray(message, offset, out offset); + var tail = message.ToStruct(offset, Tail.SIZE); + + var data = new byte[tail.DataLength]; + + offset += Tail.SIZE; + Array.Copy(message, offset, data, 0, data.Length); + + endOffset = offset + data.Length; + + return new DnsResourceRecord(domain, data, tail.Type, tail.Class, tail.TimeToLive); + } + + public byte[] ToArray() => + new MemoryStream(Size) + .Append(Name.ToArray()) + .Append(new Tail() + { + Type = Type, + Class = Class, + TimeToLive = TimeToLive, + DataLength = Data.Length, + }.ToBytes()) + .Append(Data) + .ToArray(); + + public override string ToString() + { + return Json.SerializeOnly( + this, + true, + nameof(Name), + nameof(Type), + nameof(Class), + nameof(TimeToLive), + nameof(DataLength)); + } + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 2)] + private struct Tail + { + public const int SIZE = 10; + + private ushort type; + private ushort klass; + private uint ttl; + private ushort dataLength; + + public DnsRecordType Type + { + get => (DnsRecordType) type; + set => type = (ushort) value; + } + + public DnsRecordClass Class + { + get => (DnsRecordClass) klass; + set => klass = (ushort) value; + } + + public TimeSpan TimeToLive + { + get => TimeSpan.FromSeconds(ttl); + set => ttl = (uint) value.TotalSeconds; + } + + public int DataLength + { + get => dataLength; + set => dataLength = (ushort) value; + } + } + } + + public class DnsPointerResourceRecord : DnsResourceRecordBase + { + public DnsPointerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) + : base(record) + { + PointerDomainName = DnsDomain.FromArray(message, dataOffset); + } + + public DnsDomain PointerDomainName { get; } + + protected override string[] IncludedProperties + { + get + { + var temp = new List(base.IncludedProperties) {nameof(PointerDomainName)}; + return temp.ToArray(); + } + } + } + + public class DnsIPAddressResourceRecord : DnsResourceRecordBase + { + public DnsIPAddressResourceRecord(IDnsResourceRecord record) + : base(record) + { + IPAddress = new IPAddress(Data); + } + + public IPAddress IPAddress { get; } + + protected override string[] IncludedProperties + => new List(base.IncludedProperties) {nameof(IPAddress)}.ToArray(); + } + + public class DnsNameServerResourceRecord : DnsResourceRecordBase + { + public DnsNameServerResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) + : base(record) + { + NSDomainName = DnsDomain.FromArray(message, dataOffset); + } + + public DnsDomain NSDomainName { get; } + + protected override string[] IncludedProperties + => new List(base.IncludedProperties) {nameof(NSDomainName)}.ToArray(); + } + + public class DnsCanonicalNameResourceRecord : DnsResourceRecordBase + { + public DnsCanonicalNameResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) + : base(record) + { + CanonicalDomainName = DnsDomain.FromArray(message, dataOffset); + } + + public DnsDomain CanonicalDomainName { get; } + + protected override string[] IncludedProperties + => new List(base.IncludedProperties) {nameof(CanonicalDomainName)}.ToArray(); + } + + public class DnsMailExchangeResourceRecord : DnsResourceRecordBase + { + private const int PreferenceSize = 2; + + public DnsMailExchangeResourceRecord( + IDnsResourceRecord record, + byte[] message, + int dataOffset) + : base(record) + { + var preference = new byte[PreferenceSize]; + Array.Copy(message, dataOffset, preference, 0, preference.Length); + + if (BitConverter.IsLittleEndian) + { + Array.Reverse(preference); + } + + dataOffset += PreferenceSize; + + Preference = BitConverter.ToUInt16(preference, 0); + ExchangeDomainName = DnsDomain.FromArray(message, dataOffset); + } + + public int Preference { get; } + + public DnsDomain ExchangeDomainName { get; } + + protected override string[] IncludedProperties => new List(base.IncludedProperties) + { + nameof(Preference), + nameof(ExchangeDomainName), + }.ToArray(); + } + + public class DnsStartOfAuthorityResourceRecord : DnsResourceRecordBase + { + public DnsStartOfAuthorityResourceRecord(IDnsResourceRecord record, byte[] message, int dataOffset) + : base(record) + { + MasterDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); + ResponsibleDomainName = DnsDomain.FromArray(message, dataOffset, out dataOffset); + + var tail = message.ToStruct(dataOffset, Options.SIZE); + + SerialNumber = tail.SerialNumber; + RefreshInterval = tail.RefreshInterval; + RetryInterval = tail.RetryInterval; + ExpireInterval = tail.ExpireInterval; + MinimumTimeToLive = tail.MinimumTimeToLive; + } + + public DnsStartOfAuthorityResourceRecord( + DnsDomain domain, + DnsDomain master, + DnsDomain responsible, + long serial, + TimeSpan refresh, + TimeSpan retry, + TimeSpan expire, + TimeSpan minTtl, + TimeSpan ttl = default) + : base(Create(domain, master, responsible, serial, refresh, retry, expire, minTtl, ttl)) + { + MasterDomainName = master; + ResponsibleDomainName = responsible; + + SerialNumber = serial; + RefreshInterval = refresh; + RetryInterval = retry; + ExpireInterval = expire; + MinimumTimeToLive = minTtl; + } + + public DnsDomain MasterDomainName { get; } + + public DnsDomain ResponsibleDomainName { get; } + + public long SerialNumber { get; } + + public TimeSpan RefreshInterval { get; } + + public TimeSpan RetryInterval { get; } + + public TimeSpan ExpireInterval { get; } + + public TimeSpan MinimumTimeToLive { get; } + + protected override string[] IncludedProperties => new List(base.IncludedProperties) + { + nameof(MasterDomainName), + nameof(ResponsibleDomainName), + nameof(SerialNumber), + }.ToArray(); + + private static IDnsResourceRecord Create( + DnsDomain domain, + DnsDomain master, + DnsDomain responsible, + long serial, + TimeSpan refresh, + TimeSpan retry, + TimeSpan expire, + TimeSpan minTtl, + TimeSpan ttl) + { + var data = new MemoryStream(Options.SIZE + master.Size + responsible.Size); + var tail = new Options + { + SerialNumber = serial, + RefreshInterval = refresh, + RetryInterval = retry, + ExpireInterval = expire, + MinimumTimeToLive = minTtl, + }; + + data.Append(master.ToArray()).Append(responsible.ToArray()).Append(tail.ToBytes()); + + return new DnsResourceRecord(domain, data.ToArray(), DnsRecordType.SOA, DnsRecordClass.IN, ttl); + } + + [StructEndianness(Endianness.Big)] + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct Options + { + public const int SIZE = 20; + + private uint serialNumber; + private uint refreshInterval; + private uint retryInterval; + private uint expireInterval; + private uint ttl; + + public long SerialNumber + { + get => serialNumber; + set => serialNumber = (uint) value; + } + + public TimeSpan RefreshInterval + { + get => TimeSpan.FromSeconds(refreshInterval); + set => refreshInterval = (uint) value.TotalSeconds; + } + + public TimeSpan RetryInterval + { + get => TimeSpan.FromSeconds(retryInterval); + set => retryInterval = (uint) value.TotalSeconds; + } + + public TimeSpan ExpireInterval + { + get => TimeSpan.FromSeconds(expireInterval); + set => expireInterval = (uint) value.TotalSeconds; + } + + public TimeSpan MinimumTimeToLive + { + get => TimeSpan.FromSeconds(ttl); + set => ttl = (uint) value.TotalSeconds; + } + } + } + + private static class DnsResourceRecordFactory + { + public static IList GetAllFromArray( + byte[] message, + int offset, + int count, + out int endOffset) + { + var result = new List(count); + + for (var i = 0; i < count; i++) + { + result.Add(GetFromArray(message, offset, out offset)); + } + + endOffset = offset; + return result; + } + + private static IDnsResourceRecord GetFromArray(byte[] message, int offset, out int endOffset) + { + var record = DnsResourceRecord.FromArray(message, offset, out endOffset); + var dataOffset = endOffset - record.DataLength; + + return record.Type switch + { + DnsRecordType.A => (IDnsResourceRecord) new DnsIPAddressResourceRecord(record), + DnsRecordType.AAAA => new DnsIPAddressResourceRecord(record), + DnsRecordType.NS => new DnsNameServerResourceRecord(record, message, dataOffset), + DnsRecordType.CNAME => new DnsCanonicalNameResourceRecord(record, message, dataOffset), + DnsRecordType.SOA => new DnsStartOfAuthorityResourceRecord(record, message, dataOffset), + DnsRecordType.PTR => new DnsPointerResourceRecord(record, message, dataOffset), + DnsRecordType.MX => new DnsMailExchangeResourceRecord(record, message, dataOffset), + _ => record + }; + } + } + } +} diff --git a/Swan/Net/Dns/DnsClient.Response.cs b/Swan/Net/Dns/DnsClient.Response.cs new file mode 100644 index 0000000..5dcdb38 --- /dev/null +++ b/Swan/Net/Dns/DnsClient.Response.cs @@ -0,0 +1,215 @@ +namespace Swan.Net.Dns +{ + using Formatters; + using System; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.IO; + using System.Linq; + + /// + /// DnsClient Response inner class. + /// + internal partial class DnsClient + { + public class DnsClientResponse : IDnsResponse + { + private readonly DnsResponse _response; + private readonly byte[] _message; + + internal DnsClientResponse(DnsClientRequest request, DnsResponse response, byte[] message) + { + Request = request; + + _message = message; + _response = response; + } + + public DnsClientRequest Request { get; } + + public int Id + { + get { return _response.Id; } + set { } + } + + public IList AnswerRecords => _response.AnswerRecords; + + public IList AuthorityRecords => + new ReadOnlyCollection(_response.AuthorityRecords); + + public IList AdditionalRecords => + new ReadOnlyCollection(_response.AdditionalRecords); + + public bool IsRecursionAvailable + { + get { return _response.IsRecursionAvailable; } + set { } + } + + public bool IsAuthorativeServer + { + get { return _response.IsAuthorativeServer; } + set { } + } + + public bool IsTruncated + { + get { return _response.IsTruncated; } + set { } + } + + public DnsOperationCode OperationCode + { + get { return _response.OperationCode; } + set { } + } + + public DnsResponseCode ResponseCode + { + get { return _response.ResponseCode; } + set { } + } + + public IList Questions => new ReadOnlyCollection(_response.Questions); + + public int Size => _message.Length; + + public byte[] ToArray() => _message; + + public override string ToString() => _response.ToString(); + } + + public class DnsResponse : IDnsResponse + { + private DnsHeader _header; + + public DnsResponse( + DnsHeader header, + IList questions, + IList answers, + IList authority, + IList additional) + { + _header = header; + Questions = questions; + AnswerRecords = answers; + AuthorityRecords = authority; + AdditionalRecords = additional; + } + + public IList Questions { get; } + + public IList AnswerRecords { get; } + + public IList AuthorityRecords { get; } + + public IList AdditionalRecords { get; } + + public int Id + { + get => _header.Id; + set => _header.Id = value; + } + + public bool IsRecursionAvailable + { + get => _header.RecursionAvailable; + set => _header.RecursionAvailable = value; + } + + public bool IsAuthorativeServer + { + get => _header.AuthorativeServer; + set => _header.AuthorativeServer = value; + } + + public bool IsTruncated + { + get => _header.Truncated; + set => _header.Truncated = value; + } + + public DnsOperationCode OperationCode + { + get => _header.OperationCode; + set => _header.OperationCode = value; + } + + public DnsResponseCode ResponseCode + { + get => _header.ResponseCode; + set => _header.ResponseCode = value; + } + + public int Size + => _header.Size + + Questions.Sum(q => q.Size) + + AnswerRecords.Sum(a => a.Size) + + AuthorityRecords.Sum(a => a.Size) + + AdditionalRecords.Sum(a => a.Size); + + public static DnsResponse FromArray(byte[] message) + { + var header = DnsHeader.FromArray(message); + var offset = header.Size; + + if (!header.Response || header.QuestionCount == 0) + { + throw new ArgumentException("Invalid response message"); + } + + if (header.Truncated) + { + return new DnsResponse(header, + DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount), + new List(), + new List(), + new List()); + } + + return new DnsResponse(header, + DnsQuestion.GetAllFromArray(message, offset, header.QuestionCount, out offset), + DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AnswerRecordCount, out offset), + DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AuthorityRecordCount, out offset), + DnsResourceRecordFactory.GetAllFromArray(message, offset, header.AdditionalRecordCount, out offset)); + } + + public byte[] ToArray() + { + UpdateHeader(); + var result = new MemoryStream(Size); + + result + .Append(_header.ToArray()) + .Append(Questions.Select(q => q.ToArray())) + .Append(AnswerRecords.Select(a => a.ToArray())) + .Append(AuthorityRecords.Select(a => a.ToArray())) + .Append(AdditionalRecords.Select(a => a.ToArray())); + + return result.ToArray(); + } + + public override string ToString() + { + UpdateHeader(); + + return Json.SerializeOnly( + this, + true, + nameof(Questions), + nameof(AnswerRecords), + nameof(AuthorityRecords), + nameof(AdditionalRecords)); + } + + private void UpdateHeader() + { + _header.QuestionCount = Questions.Count; + _header.AnswerRecordCount = AnswerRecords.Count; + _header.AuthorityRecordCount = AuthorityRecords.Count; + _header.AdditionalRecordCount = AdditionalRecords.Count; + } + } + } +} \ No newline at end of file diff --git a/Swan/Net/Dns/DnsClient.cs b/Swan/Net/Dns/DnsClient.cs new file mode 100644 index 0000000..8cb5aca --- /dev/null +++ b/Swan/Net/Dns/DnsClient.cs @@ -0,0 +1,79 @@ +namespace Swan.Net.Dns +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Threading.Tasks; + + /// + /// DnsClient public methods. + /// + internal partial class DnsClient + { + private readonly IPEndPoint _dns; + private readonly IDnsRequestResolver _resolver; + + public DnsClient(IPEndPoint dns, IDnsRequestResolver? resolver = null) + { + _dns = dns; + _resolver = resolver ?? new DnsUdpRequestResolver(new DnsTcpRequestResolver()); + } + + public DnsClient(IPAddress ip, int port = Network.DnsDefaultPort, IDnsRequestResolver? resolver = null) + : this(new IPEndPoint(ip, port), resolver) + { + } + + public DnsClientRequest Create(IDnsRequest? request = null) + => new DnsClientRequest(_dns, request, _resolver); + + public async Task> Lookup(string domain, DnsRecordType type = DnsRecordType.A) + { + if (string.IsNullOrWhiteSpace(domain)) + throw new ArgumentNullException(nameof(domain)); + + if (type != DnsRecordType.A && type != DnsRecordType.AAAA) + { + throw new ArgumentException("Invalid record type " + type); + } + + var response = await Resolve(domain, type).ConfigureAwait(false); + var ips = response.AnswerRecords + .Where(r => r.Type == type) + .Cast() + .Select(r => r.IPAddress) + .ToList(); + + return ips.Count == 0 ? throw new DnsQueryException(response, "No matching records") : ips; + } + + public async Task Reverse(IPAddress ip) + { + if (ip == null) + throw new ArgumentNullException(nameof(ip)); + + var response = await Resolve(DnsDomain.PointerName(ip), DnsRecordType.PTR); + var ptr = response.AnswerRecords.FirstOrDefault(r => r.Type == DnsRecordType.PTR); + + return ptr == null + ? throw new DnsQueryException(response, "No matching records") + : ((DnsPointerResourceRecord) ptr).PointerDomainName.ToString(); + } + + public Task Resolve(string domain, DnsRecordType type) => + Resolve(new DnsDomain(domain), type); + + public Task Resolve(DnsDomain domain, DnsRecordType type) + { + var request = Create(); + var question = new DnsQuestion(domain, type); + + request.Questions.Add(question); + request.OperationCode = DnsOperationCode.Query; + request.RecursionDesired = true; + + return request.Resolve(); + } + } +} diff --git a/Swan/Net/Dns/DnsQueryException.cs b/Swan/Net/Dns/DnsQueryException.cs new file mode 100644 index 0000000..f07eeae --- /dev/null +++ b/Swan/Net/Dns/DnsQueryException.cs @@ -0,0 +1,37 @@ +namespace Swan.Net.Dns +{ + using System; + + /// + /// An exception thrown when the DNS query fails. + /// + /// + [Serializable] + public class DnsQueryException : Exception + { + internal DnsQueryException(string message) + : base(message) + { + } + + internal DnsQueryException(string message, Exception e) + : base(message, e) + { + } + + internal DnsQueryException(DnsClient.IDnsResponse response) + : this(response, Format(response)) + { + } + + internal DnsQueryException(DnsClient.IDnsResponse response, string message) + : base(message) + { + Response = response; + } + + internal DnsClient.IDnsResponse? Response { get; } + + private static string Format(DnsClient.IDnsResponse response) => $"Invalid response received with code {response.ResponseCode}"; + } +} diff --git a/Swan/Net/Dns/DnsQueryResult.cs b/Swan/Net/Dns/DnsQueryResult.cs new file mode 100644 index 0000000..31d164c --- /dev/null +++ b/Swan/Net/Dns/DnsQueryResult.cs @@ -0,0 +1,123 @@ +namespace Swan.Net.Dns +{ + using System.Collections.Generic; + + /// + /// Represents a response from a DNS server. + /// + public class DnsQueryResult + { + private readonly List _mAnswerRecords = new List(); + private readonly List _mAdditionalRecords = new List(); + private readonly List _mAuthorityRecords = new List(); + + /// + /// Initializes a new instance of the class. + /// + /// The response. + internal DnsQueryResult(DnsClient.IDnsResponse response) + : this() + { + Id = response.Id; + IsAuthoritativeServer = response.IsAuthorativeServer; + IsRecursionAvailable = response.IsRecursionAvailable; + IsTruncated = response.IsTruncated; + OperationCode = response.OperationCode; + ResponseCode = response.ResponseCode; + + if (response.AnswerRecords != null) + { + foreach (var record in response.AnswerRecords) + AnswerRecords.Add(new DnsRecord(record)); + } + + if (response.AuthorityRecords != null) + { + foreach (var record in response.AuthorityRecords) + AuthorityRecords.Add(new DnsRecord(record)); + } + + if (response.AdditionalRecords != null) + { + foreach (var record in response.AdditionalRecords) + AdditionalRecords.Add(new DnsRecord(record)); + } + } + + private DnsQueryResult() + { + } + + /// + /// Gets the identifier. + /// + /// + /// The identifier. + /// + public int Id { get; } + + /// + /// Gets a value indicating whether this instance is authoritative server. + /// + /// + /// true if this instance is authoritative server; otherwise, false. + /// + public bool IsAuthoritativeServer { get; } + + /// + /// Gets a value indicating whether this instance is truncated. + /// + /// + /// true if this instance is truncated; otherwise, false. + /// + public bool IsTruncated { get; } + + /// + /// Gets a value indicating whether this instance is recursion available. + /// + /// + /// true if this instance is recursion available; otherwise, false. + /// + public bool IsRecursionAvailable { get; } + + /// + /// Gets the operation code. + /// + /// + /// The operation code. + /// + public DnsOperationCode OperationCode { get; } + + /// + /// Gets the response code. + /// + /// + /// The response code. + /// + public DnsResponseCode ResponseCode { get; } + + /// + /// Gets the answer records. + /// + /// + /// The answer records. + /// + public IList AnswerRecords => _mAnswerRecords; + + /// + /// Gets the additional records. + /// + /// + /// The additional records. + /// + public IList AdditionalRecords => _mAdditionalRecords; + + /// + /// Gets the authority records. + /// + /// + /// The authority records. + /// + public IList AuthorityRecords => _mAuthorityRecords; + } +} diff --git a/Swan/Net/Dns/DnsRecord.cs b/Swan/Net/Dns/DnsRecord.cs new file mode 100644 index 0000000..78f19ac --- /dev/null +++ b/Swan/Net/Dns/DnsRecord.cs @@ -0,0 +1,208 @@ +namespace Swan.Net.Dns +{ + using System; + using System.Net; + using System.Text; + + /// + /// Represents a DNS record entry. + /// + public class DnsRecord + { + /// + /// Initializes a new instance of the class. + /// + /// The record. + internal DnsRecord(DnsClient.IDnsResourceRecord record) + : this() + { + Name = record.Name.ToString(); + Type = record.Type; + Class = record.Class; + TimeToLive = record.TimeToLive; + Data = record.Data; + + // PTR + PointerDomainName = (record as DnsClient.DnsPointerResourceRecord)?.PointerDomainName?.ToString(); + + // A + IPAddress = (record as DnsClient.DnsIPAddressResourceRecord)?.IPAddress; + + // NS + NameServerDomainName = (record as DnsClient.DnsNameServerResourceRecord)?.NSDomainName?.ToString(); + + // CNAME + CanonicalDomainName = (record as DnsClient.DnsCanonicalNameResourceRecord)?.CanonicalDomainName.ToString(); + + // MX + MailExchangerDomainName = (record as DnsClient.DnsMailExchangeResourceRecord)?.ExchangeDomainName.ToString(); + MailExchangerPreference = (record as DnsClient.DnsMailExchangeResourceRecord)?.Preference; + + // SOA + SoaMasterDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MasterDomainName.ToString(); + SoaResponsibleDomainName = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ResponsibleDomainName.ToString(); + SoaSerialNumber = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.SerialNumber; + SoaRefreshInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RefreshInterval; + SoaRetryInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.RetryInterval; + SoaExpireInterval = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.ExpireInterval; + SoaMinimumTimeToLive = (record as DnsClient.DnsStartOfAuthorityResourceRecord)?.MinimumTimeToLive; + } + + private DnsRecord() + { + // placeholder + } + + /// + /// Gets the name. + /// + /// + /// The name. + /// + public string Name { get; } + + /// + /// Gets the type. + /// + /// + /// The type. + /// + public DnsRecordType Type { get; } + + /// + /// Gets the class. + /// + /// + /// The class. + /// + public DnsRecordClass Class { get; } + + /// + /// Gets the time to live. + /// + /// + /// The time to live. + /// + public TimeSpan TimeToLive { get; } + + /// + /// Gets the raw data of the record. + /// + /// + /// The data. + /// + public byte[] Data { get; } + + /// + /// Gets the data text bytes in ASCII encoding. + /// + /// + /// The data text. + /// + public string DataText => Data == null ? string.Empty : Encoding.ASCII.GetString(Data); + + /// + /// Gets the name of the pointer domain. + /// + /// + /// The name of the pointer domain. + /// + public string PointerDomainName { get; } + + /// + /// Gets the ip address. + /// + /// + /// The ip address. + /// + public IPAddress IPAddress { get; } + + /// + /// Gets the name of the name server domain. + /// + /// + /// The name of the name server domain. + /// + public string NameServerDomainName { get; } + + /// + /// Gets the name of the canonical domain. + /// + /// + /// The name of the canonical domain. + /// + public string CanonicalDomainName { get; } + + /// + /// Gets the mail exchanger preference. + /// + /// + /// The mail exchanger preference. + /// + public int? MailExchangerPreference { get; } + + /// + /// Gets the name of the mail exchanger domain. + /// + /// + /// The name of the mail exchanger domain. + /// + public string MailExchangerDomainName { get; } + + /// + /// Gets the name of the soa master domain. + /// + /// + /// The name of the soa master domain. + /// + public string SoaMasterDomainName { get; } + + /// + /// Gets the name of the soa responsible domain. + /// + /// + /// The name of the soa responsible domain. + /// + public string SoaResponsibleDomainName { get; } + + /// + /// Gets the soa serial number. + /// + /// + /// The soa serial number. + /// + public long? SoaSerialNumber { get; } + + /// + /// Gets the soa refresh interval. + /// + /// + /// The soa refresh interval. + /// + public TimeSpan? SoaRefreshInterval { get; } + + /// + /// Gets the soa retry interval. + /// + /// + /// The soa retry interval. + /// + public TimeSpan? SoaRetryInterval { get; } + + /// + /// Gets the soa expire interval. + /// + /// + /// The soa expire interval. + /// + public TimeSpan? SoaExpireInterval { get; } + + /// + /// Gets the soa minimum time to live. + /// + /// + /// The soa minimum time to live. + /// + public TimeSpan? SoaMinimumTimeToLive { get; } + } +} diff --git a/Swan/Net/Dns/Enums.Dns.cs b/Swan/Net/Dns/Enums.Dns.cs new file mode 100644 index 0000000..8891993 --- /dev/null +++ b/Swan/Net/Dns/Enums.Dns.cs @@ -0,0 +1,172 @@ +// ReSharper disable InconsistentNaming +namespace Swan.Net.Dns +{ + /// + /// Enumerates the different DNS record types. + /// + public enum DnsRecordType + { + /// + /// A records + /// + A = 1, + + /// + /// Nameserver records + /// + NS = 2, + + /// + /// CNAME records + /// + CNAME = 5, + + /// + /// SOA records + /// + SOA = 6, + + /// + /// WKS records + /// + WKS = 11, + + /// + /// PTR records + /// + PTR = 12, + + /// + /// MX records + /// + MX = 15, + + /// + /// TXT records + /// + TXT = 16, + + /// + /// A records fot IPv6 + /// + AAAA = 28, + + /// + /// SRV records + /// + SRV = 33, + + /// + /// ANY records + /// + ANY = 255, + } + + /// + /// Enumerates the different DNS record classes. + /// + public enum DnsRecordClass + { + /// + /// IN records + /// + IN = 1, + + /// + /// ANY records + /// + ANY = 255, + } + + /// + /// Enumerates the different DNS operation codes. + /// + public enum DnsOperationCode + { + /// + /// Query operation + /// + Query = 0, + + /// + /// IQuery operation + /// + IQuery, + + /// + /// Status operation + /// + Status, + + /// + /// Notify operation + /// + Notify = 4, + + /// + /// Update operation + /// + Update, + } + + /// + /// Enumerates the different DNS query response codes. + /// + public enum DnsResponseCode + { + /// + /// No error + /// + NoError = 0, + + /// + /// No error + /// + FormatError, + + /// + /// Format error + /// + ServerFailure, + + /// + /// Server failure error + /// + NameError, + + /// + /// Name error + /// + NotImplemented, + + /// + /// Not implemented error + /// + Refused, + + /// + /// Refused error + /// + YXDomain, + + /// + /// YXRR error + /// + YXRRSet, + + /// + /// NXRR Set error + /// + NXRRSet, + + /// + /// Not authorized error + /// + NotAuth, + + /// + /// Not zone error + /// + NotZone, + } +} diff --git a/Swan/Net/Eventing.ConnectionListener.cs b/Swan/Net/Eventing.ConnectionListener.cs new file mode 100644 index 0000000..58c4910 --- /dev/null +++ b/Swan/Net/Eventing.ConnectionListener.cs @@ -0,0 +1,158 @@ +namespace Swan.Net +{ + using System; + using System.Net; + using System.Net.Sockets; + + /// + /// The event arguments for when connections are accepted. + /// + /// + public class ConnectionAcceptedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The client. + /// client. + public ConnectionAcceptedEventArgs(TcpClient client) + { + Client = client ?? throw new ArgumentNullException(nameof(client)); + } + + /// + /// Gets the client. + /// + /// + /// The client. + /// + public TcpClient Client { get; } + } + + /// + /// Occurs before a connection is accepted. Set the Cancel property to true to prevent the connection from being accepted. + /// + /// + public class ConnectionAcceptingEventArgs : ConnectionAcceptedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The client. + public ConnectionAcceptingEventArgs(TcpClient client) + : base(client) + { + } + + /// + /// Setting Cancel to true rejects the new TcpClient. + /// + /// + /// true if cancel; otherwise, false. + /// + public bool Cancel { get; set; } + } + + /// + /// Event arguments for when a server listener is started. + /// + /// + public class ConnectionListenerStartedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The listener end point. + /// listenerEndPoint. + public ConnectionListenerStartedEventArgs(IPEndPoint listenerEndPoint) + { + EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); + } + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public IPEndPoint EndPoint { get; } + } + + /// + /// Event arguments for when a server listener fails to start. + /// + /// + public class ConnectionListenerFailedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The listener end point. + /// The ex. + /// + /// listenerEndPoint + /// or + /// ex. + /// + public ConnectionListenerFailedEventArgs(IPEndPoint listenerEndPoint, Exception ex) + { + EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); + Error = ex ?? throw new ArgumentNullException(nameof(ex)); + } + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public IPEndPoint EndPoint { get; } + + /// + /// Gets the error. + /// + /// + /// The error. + /// + public Exception Error { get; } + } + + /// + /// Event arguments for when a server listener stopped. + /// + /// + public class ConnectionListenerStoppedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The listener end point. + /// The ex. + /// + /// listenerEndPoint + /// or + /// ex. + /// + public ConnectionListenerStoppedEventArgs(IPEndPoint listenerEndPoint, Exception? ex = null) + { + EndPoint = listenerEndPoint ?? throw new ArgumentNullException(nameof(listenerEndPoint)); + Error = ex; + } + + /// + /// Gets the end point. + /// + /// + /// The end point. + /// + public IPEndPoint EndPoint { get; } + + /// + /// Gets the error. + /// + /// + /// The error. + /// + public Exception? Error { get; } + } +} diff --git a/Swan/Net/Eventing.cs b/Swan/Net/Eventing.cs new file mode 100644 index 0000000..ab1fc15 --- /dev/null +++ b/Swan/Net/Eventing.cs @@ -0,0 +1,84 @@ +namespace Swan.Net +{ + using System; + using System.Text; + + /// + /// The event arguments for connection failure events. + /// + /// + public class ConnectionFailureEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The ex. + public ConnectionFailureEventArgs(Exception ex) + { + Error = ex; + } + + /// + /// Gets the error. + /// + /// + /// The error. + /// + public Exception Error { get; } + } + + /// + /// Event arguments for when data is received. + /// + /// + public class ConnectionDataReceivedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// The buffer. + /// The trigger. + /// if set to true [more available]. + public ConnectionDataReceivedEventArgs(byte[] buffer, ConnectionDataReceivedTrigger trigger, bool moreAvailable) + { + Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + Trigger = trigger; + HasMoreAvailable = moreAvailable; + } + + /// + /// Gets the buffer. + /// + /// + /// The buffer. + /// + public byte[] Buffer { get; } + + /// + /// Gets the cause as to why this event was thrown. + /// + /// + /// The trigger. + /// + public ConnectionDataReceivedTrigger Trigger { get; } + + /// + /// Gets a value indicating whether the receive buffer has more bytes available. + /// + /// + /// true if this instance has more available; otherwise, false. + /// + public bool HasMoreAvailable { get; } + + /// + /// Gets the string from buffer. + /// + /// The encoding. + /// + /// A that contains the results of decoding the specified sequence of bytes. + /// + /// encoding + public string GetStringFromBuffer(Encoding encoding) + => encoding?.GetString(Buffer).TrimEnd('\r', '\n') ?? throw new ArgumentNullException(nameof(encoding)); + } +} diff --git a/Swan/Net/JsonClient.cs b/Swan/Net/JsonClient.cs new file mode 100644 index 0000000..402ab32 --- /dev/null +++ b/Swan/Net/JsonClient.cs @@ -0,0 +1,418 @@ +namespace Swan.Net +{ + using Formatters; + using System; + using System.Collections.Generic; + using System.Net.Http; + using System.Net.Http.Headers; + using System.Security; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Represents a HttpClient with extended methods to use with JSON payloads + /// and bearer tokens authentication. + /// + public static class JsonClient + { + private const string JsonMimeType = "application/json"; + private const string FormType = "application/x-www-form-urlencoded"; + + private static readonly HttpClient HttpClient = new HttpClient(); + + /// + /// Post a object as JSON with optional authorization token. + /// + /// The type of response object. + /// The request URI. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested type. + /// + public static async Task Post( + Uri requestUri, + object payload, + string? authorization = null, + CancellationToken cancellationToken = default) + { + var jsonString = await PostString(requestUri, payload, authorization, cancellationToken) + .ConfigureAwait(false); + + return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; + } + + /// + /// Posts the specified URL. + /// + /// The request URI. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result as a collection of key/value pairs. + /// + public static async Task?> Post( + Uri requestUri, + object payload, + string? authorization = null, + CancellationToken cancellationToken = default) + { + var jsonString = await PostString(requestUri, payload, authorization, cancellationToken) + .ConfigureAwait(false); + + return string.IsNullOrWhiteSpace(jsonString) + ? default + : Json.Deserialize(jsonString) as IDictionary; + } + + /// + /// Posts the specified URL. + /// + /// The request URI. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + /// url. + /// Error POST JSON. + public static Task PostString( + Uri requestUri, + object payload, + string? authorization = null, + CancellationToken cancellationToken = default) + => SendAsync(HttpMethod.Post, requestUri, payload, authorization, cancellationToken); + + /// + /// Puts the specified URL. + /// + /// The type of response object. + /// The request URI. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested type. + /// + public static async Task Put( + Uri requestUri, + object payload, + string? authorization = null, + CancellationToken ct = default) + { + var jsonString = await PutString(requestUri, payload, authorization, ct) + .ConfigureAwait(false); + + return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; + } + + /// + /// Puts the specified URL. + /// + /// The request URI. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested collection of key/value pairs. + /// + public static async Task?> Put( + Uri requestUri, + object payload, + string? authorization = null, + CancellationToken cancellationToken = default) + { + var response = await Put(requestUri, payload, authorization, cancellationToken) + .ConfigureAwait(false); + + return response as IDictionary; + } + + /// + /// Puts as string. + /// + /// The request URI. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + /// url. + /// Error PUT JSON. + public static Task PutString( + Uri requestUri, + object payload, + string? authorization = null, + CancellationToken ct = default) => SendAsync(HttpMethod.Put, requestUri, payload, authorization, ct); + + /// + /// Gets as string. + /// + /// The request URI. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + /// url. + /// Error GET JSON. + public static Task GetString( + Uri requestUri, + string? authorization = null, + CancellationToken ct = default) + => GetString(requestUri, null, authorization, ct); + + /// + /// Gets the string. + /// + /// The URI. + /// The headers. + /// The authorization. + /// The ct. + /// + /// A task with a result of the requested string. + /// + public static async Task GetString( + Uri uri, + IDictionary>? headers, + string? authorization = null, + CancellationToken ct = default) + { + var response = await GetHttpContent(uri, ct, authorization, headers) + .ConfigureAwait(false); + + return await response.ReadAsStringAsync() + .ConfigureAwait(false); + } + + /// + /// Gets the specified URL and return the JSON data as object + /// with optional authorization token. + /// + /// The response type. + /// The request URI. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested type. + /// + public static async Task Get( + Uri requestUri, + string? authorization = null, + CancellationToken ct = default) + { + var jsonString = await GetString(requestUri, authorization, ct) + .ConfigureAwait(false); + + return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; + } + + /// + /// Gets the specified URL and return the JSON data as object + /// with optional authorization token. + /// + /// The response type. + /// The request URI. + /// The headers. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested type. + /// + public static async Task Get( + Uri requestUri, + IDictionary>? headers, + string? authorization = null, + CancellationToken ct = default) + { + var jsonString = await GetString(requestUri, headers, authorization, ct) + .ConfigureAwait(false); + + return !string.IsNullOrEmpty(jsonString) ? Json.Deserialize(jsonString) : default; + } + + /// + /// Gets the binary. + /// + /// The request URI. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested byte array. + /// + /// url. + /// Error GET Binary. + public static async Task GetBinary( + Uri requestUri, + string? authorization = null, + CancellationToken ct = default) + { + var response = await GetHttpContent(requestUri, ct, authorization) + .ConfigureAwait(false); + + return await response.ReadAsByteArrayAsync() + .ConfigureAwait(false); + } + + /// + /// Authenticate against a web server using Bearer Token. + /// + /// The request URI. + /// The username. + /// The password. + /// The cancellation token. + /// + /// A task with a Dictionary with authentication data. + /// + /// url + /// or + /// username. + /// Error Authenticating. + public static async Task?> Authenticate( + Uri requestUri, + string username, + string password, + CancellationToken ct = default) + { + if (string.IsNullOrWhiteSpace(username)) + throw new ArgumentNullException(nameof(username)); + + // ignore empty password for now + var content = $"grant_type=password&username={username}&password={password}"; + using var requestContent = new StringContent(content, Encoding.UTF8, FormType); + var response = await HttpClient.PostAsync(requestUri, requestContent, ct).ConfigureAwait(false); + + if (!response.IsSuccessStatusCode) + throw new SecurityException($"Error Authenticating. Status code: {response.StatusCode}."); + + var jsonPayload = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + return Json.Deserialize(jsonPayload) as IDictionary; + } + + /// + /// Posts the file. + /// + /// The request URI. + /// The buffer. + /// Name of the file. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + public static Task PostFileString( + Uri requestUri, + byte[] buffer, + string fileName, + string? authorization = null, + CancellationToken ct = default) => + PostString(requestUri, new { Filename = fileName, Data = buffer }, authorization, ct); + + /// + /// Posts the file. + /// + /// The response type. + /// The request URI. + /// The buffer. + /// Name of the file. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + public static Task PostFile( + Uri requestUri, + byte[] buffer, + string fileName, + string? authorization = null, + CancellationToken ct = default) => + Post(requestUri, new { Filename = fileName, Data = buffer }, authorization, ct); + + /// + /// Sends the asynchronous request. + /// + /// The method. + /// The request URI. + /// The payload. + /// The authorization. + /// The cancellation token. + /// + /// A task with a result of the requested string. + /// + /// requestUri. + /// Error {method} JSON. + public static async Task SendAsync( + HttpMethod method, + Uri requestUri, + object payload, + string? authorization = null, + CancellationToken ct = default) + { + using var response = await GetResponse(requestUri, authorization, null, payload, method, ct).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + throw new JsonRequestException( + $"Error {method} JSON", + (int)response.StatusCode, + await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + return await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); + } + + private static async Task GetHttpContent( + Uri uri, + CancellationToken ct, + string? authorization = null, + IDictionary>? headers = null) + { + var response = await GetResponse(uri, authorization, headers, ct: ct) + .ConfigureAwait(false); + + return response.IsSuccessStatusCode + ? response.Content + : throw new JsonRequestException("Error GET", (int)response.StatusCode); + } + + private static async Task GetResponse( + Uri uri, + string? authorization, + IDictionary>? headers, + object? payload = null, + HttpMethod? method = default, + CancellationToken ct = default) + { + if (uri == null) + throw new ArgumentNullException(nameof(uri)); + + using var requestMessage = new HttpRequestMessage(method ?? HttpMethod.Get, uri); + + if (!string.IsNullOrWhiteSpace(authorization)) + { + requestMessage.Headers.Authorization + = new AuthenticationHeaderValue("Bearer", authorization); + } + + if (headers != null) + { + foreach (var header in headers) + requestMessage.Headers.Add(header.Key, header.Value); + } + + if (payload != null && requestMessage.Method != HttpMethod.Get) + { + requestMessage.Content = new StringContent(Json.Serialize(payload), Encoding.UTF8, JsonMimeType); + } + + return await HttpClient.SendAsync(requestMessage, ct) + .ConfigureAwait(false); + } + } +} diff --git a/Swan/Net/JsonRequestException.cs b/Swan/Net/JsonRequestException.cs new file mode 100644 index 0000000..a2cd373 --- /dev/null +++ b/Swan/Net/JsonRequestException.cs @@ -0,0 +1,47 @@ +namespace Swan.Net +{ + using System; + + /// + /// Represents errors that occurs requesting a JSON file through HTTP. + /// + /// + [Serializable] + public class JsonRequestException + : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + /// The HTTP error code. + /// Content of the error. + public JsonRequestException(string message, int httpErrorCode = 500, string errorContent = null) + : base(message) + { + HttpErrorCode = httpErrorCode; + HttpErrorContent = errorContent; + } + + /// + /// Gets the HTTP error code. + /// + /// + /// The HTTP error code. + /// + public int HttpErrorCode { get; } + + /// + /// Gets the content of the HTTP error. + /// + /// + /// The content of the HTTP error. + /// + public string HttpErrorContent { get; } + + /// + public override string ToString() => string.IsNullOrEmpty(HttpErrorContent) + ? $"HTTP Response Status Code {HttpErrorCode} Error Message: {HttpErrorContent}" + : base.ToString(); + } +} diff --git a/Swan/Net/Network.cs b/Swan/Net/Network.cs new file mode 100644 index 0000000..613263b --- /dev/null +++ b/Swan/Net/Network.cs @@ -0,0 +1,328 @@ +namespace Swan.Net +{ + using Net.Dns; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Net; + using System.Net.Http; + using System.Net.NetworkInformation; + using System.Net.Sockets; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Provides miscellaneous network utilities such as a Public IP finder, + /// a DNS client to query DNS records of any kind, and an NTP client. + /// + public static class Network + { + /// + /// The DNS default port. + /// + public const int DnsDefaultPort = 53; + + /// + /// The NTP default port. + /// + public const int NtpDefaultPort = 123; + + /// + /// Gets the name of the host. + /// + /// + /// The name of the host. + /// + public static string HostName => IPGlobalProperties.GetIPGlobalProperties().HostName; + + /// + /// Gets the name of the network domain. + /// + /// + /// The name of the network domain. + /// + public static string DomainName => IPGlobalProperties.GetIPGlobalProperties().DomainName; + + #region IP Addresses and Adapters Information Methods + + /// + /// Gets the active IPv4 interfaces. + /// Only those interfaces with a valid unicast address and a valid gateway will be returned in the collection. + /// + /// + /// A collection of NetworkInterface/IPInterfaceProperties pairs + /// that represents the active IPv4 interfaces. + /// + public static Dictionary GetIPv4Interfaces() + { + // zero conf ip address + var zeroConf = new IPAddress(0); + + var adapters = NetworkInterface.GetAllNetworkInterfaces() + .Where(network => + network.OperationalStatus == OperationalStatus.Up + && network.NetworkInterfaceType != NetworkInterfaceType.Unknown + && network.NetworkInterfaceType != NetworkInterfaceType.Loopback) + .ToArray(); + + var result = new Dictionary(); + + foreach (var adapter in adapters) + { + var properties = adapter.GetIPProperties(); + if (properties == null + || properties.GatewayAddresses.Count == 0 + || properties.GatewayAddresses.All(gateway => Equals(gateway.Address, zeroConf)) + || properties.UnicastAddresses.Count == 0 + || properties.GatewayAddresses.All(address => Equals(address.Address, zeroConf)) + || properties.UnicastAddresses.Any(a => a.Address.AddressFamily == AddressFamily.InterNetwork) == + false) + continue; + + result[adapter] = properties; + } + + return result; + } + + /// + /// Retrieves the local ip addresses. + /// + /// if set to true [include loopback]. + /// An array of local ip addresses. + public static IPAddress[] GetIPv4Addresses(bool includeLoopback = true) => + GetIPv4Addresses(NetworkInterfaceType.Unknown, true, includeLoopback); + + /// + /// Retrieves the local ip addresses. + /// + /// Type of the interface. + /// if set to true [skip type filter]. + /// if set to true [include loopback]. + /// An array of local ip addresses. + public static IPAddress[] GetIPv4Addresses( + NetworkInterfaceType interfaceType, + bool skipTypeFilter = false, + bool includeLoopback = false) + { + var addressList = new List(); + var interfaces = NetworkInterface.GetAllNetworkInterfaces() + .Where(ni => +#if NET461 + ni.IsReceiveOnly == false && +#endif + (skipTypeFilter || ni.NetworkInterfaceType == interfaceType) && + ni.OperationalStatus == OperationalStatus.Up) + .ToArray(); + + foreach (var networkInterface in interfaces) + { + var properties = networkInterface.GetIPProperties(); + + if (properties.GatewayAddresses.All(g => g.Address.AddressFamily != AddressFamily.InterNetwork)) + continue; + + addressList.AddRange(properties.UnicastAddresses + .Where(i => i.Address.AddressFamily == AddressFamily.InterNetwork) + .Select(i => i.Address)); + } + + if (includeLoopback || interfaceType == NetworkInterfaceType.Loopback) + addressList.Add(IPAddress.Loopback); + + return addressList.ToArray(); + } + + /// + /// Gets the public IP address using ipify.org. + /// + /// The cancellation token. + /// A public IP address of the result produced by this Task. + public static async Task GetPublicIPAddressAsync(CancellationToken cancellationToken = default) + { + using var client = new HttpClient(); + var response = await client.GetAsync("https://api.ipify.org", cancellationToken).ConfigureAwait(false); + return IPAddress.Parse(await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + /// + /// Gets the configured IPv4 DNS servers for the active network interfaces. + /// + /// + /// A collection of NetworkInterface/IPInterfaceProperties pairs + /// that represents the active IPv4 interfaces. + /// + public static IPAddress[] GetIPv4DnsServers() + => GetIPv4Interfaces() + .Select(a => a.Value.DnsAddresses.Where(d => d.AddressFamily == AddressFamily.InterNetwork)) + .SelectMany(d => d) + .ToArray(); + + #endregion + + #region DNS and NTP Clients + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// An array of local ip addresses of the result produced by this task. + public static Task GetDnsHostEntryAsync(string fqdn) + { + var dnsServer = GetIPv4DnsServers().FirstOrDefault() ?? IPAddress.Parse("8.8.8.8"); + return GetDnsHostEntryAsync(fqdn, dnsServer, DnsDefaultPort); + } + + /// + /// Gets the DNS host entry (a list of IP addresses) for the domain name. + /// + /// The FQDN. + /// The DNS server. + /// The port. + /// + /// An array of local ip addresses of the result produced by this task. + /// + /// fqdn. + public static async Task GetDnsHostEntryAsync(string fqdn, IPAddress dnsServer, int port) + { + if (fqdn == null) + throw new ArgumentNullException(nameof(fqdn)); + + if (fqdn.IndexOf(".", StringComparison.Ordinal) == -1) + { + fqdn += "." + IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + + while (true) + { + if (!fqdn.EndsWith(".", StringComparison.OrdinalIgnoreCase)) break; + + fqdn = fqdn.Substring(0, fqdn.Length - 1); + } + + var client = new DnsClient(dnsServer, port); + var result = await client.Lookup(fqdn).ConfigureAwait(false); + return result.ToArray(); + } + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// The DNS server. + /// The port. + /// A that represents the current object. + public static Task GetDnsPointerEntryAsync(IPAddress query, IPAddress dnsServer, int port) + { + var client = new DnsClient(dnsServer, port); + return client.Reverse(query); + } + + /// + /// Gets the reverse lookup FQDN of the given IP Address. + /// + /// The query. + /// A that represents the current object. + public static Task GetDnsPointerEntryAsync(IPAddress query) + { + var client = new DnsClient(GetIPv4DnsServers().FirstOrDefault()); + return client.Reverse(query); + } + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// The DNS server. + /// The port. + /// Queries the DNS server for the specified record type of the result produced by this Task. + public static async Task QueryDnsAsync(string query, DnsRecordType recordType, IPAddress dnsServer, int port) + { + if (query == null) + throw new ArgumentNullException(nameof(query)); + + var client = new DnsClient(dnsServer, port); + var response = await client.Resolve(query, recordType).ConfigureAwait(false); + return new DnsQueryResult(response); + } + + /// + /// Queries the DNS server for the specified record type. + /// + /// The query. + /// Type of the record. + /// Queries the DNS server for the specified record type of the result produced by this Task. + public static Task QueryDnsAsync(string query, DnsRecordType recordType) => QueryDnsAsync(query, recordType, GetIPv4DnsServers().FirstOrDefault(), DnsDefaultPort); + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// The NTP server address. + /// The port. + /// The UTC time by querying from an NTP server of the result produced by this Task. + public static async Task GetNetworkTimeUtcAsync(IPAddress ntpServerAddress, int port = NtpDefaultPort) + { + if (ntpServerAddress == null) + throw new ArgumentNullException(nameof(ntpServerAddress)); + + // NTP message size - 16 bytes of the digest (RFC 2030) + var ntpData = new byte[48]; + + // Setting the Leap Indicator, Version Number and Mode values + ntpData[0] = 0x1B; // LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode) + + // The UDP port number assigned to NTP is 123 + var endPoint = new IPEndPoint(ntpServerAddress, port); + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + +#if !NET461 + await socket.ConnectAsync(endPoint).ConfigureAwait(false); +#else + socket.Connect(endPoint); +#endif + + socket.ReceiveTimeout = 3000; // Stops code hang if NTP is blocked + socket.Send(ntpData); + socket.Receive(ntpData); + socket.Dispose(); + + // Offset to get to the "Transmit Timestamp" field (time at which the reply + // departed the server for the client, in 64-bit timestamp format." + const byte serverReplyTime = 40; + + // Get the seconds part + ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime); + + // Get the seconds fraction + ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4); + + // Convert From big-endian to little-endian to match the platform + if (BitConverter.IsLittleEndian) + { + intPart = intPart.SwapEndianness(); + fractPart = intPart.SwapEndianness(); + } + + var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L); + + // The time is given in UTC + return new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds((long) milliseconds); + } + + /// + /// Gets the UTC time by querying from an NTP server. + /// + /// The NTP server, by default pool.ntp.org. + /// The port, by default NTP 123. + /// The UTC time by querying from an NTP server of the result produced by this Task. + public static async Task GetNetworkTimeUtcAsync(string ntpServerName = "pool.ntp.org", + int port = NtpDefaultPort) + { + var addresses = await GetDnsHostEntryAsync(ntpServerName).ConfigureAwait(false); + return await GetNetworkTimeUtcAsync(addresses.First(), port).ConfigureAwait(false); + } + + #endregion + } +} diff --git a/Swan/Net/Smtp/Enums.Smtp.cs b/Swan/Net/Smtp/Enums.Smtp.cs new file mode 100644 index 0000000..069bbff --- /dev/null +++ b/Swan/Net/Smtp/Enums.Smtp.cs @@ -0,0 +1,166 @@ +// ReSharper disable InconsistentNaming +namespace Swan.Net.Smtp +{ + /// + /// Enumerates all of the well-known SMTP command names. + /// + public enum SmtpCommandNames + { + /// + /// An unknown command + /// + Unknown, + + /// + /// The helo command + /// + HELO, + + /// + /// The ehlo command + /// + EHLO, + + /// + /// The quit command + /// + QUIT, + + /// + /// The help command + /// + HELP, + + /// + /// The noop command + /// + NOOP, + + /// + /// The rset command + /// + RSET, + + /// + /// The mail command + /// + MAIL, + + /// + /// The data command + /// + DATA, + + /// + /// The send command + /// + SEND, + + /// + /// The soml command + /// + SOML, + + /// + /// The saml command + /// + SAML, + + /// + /// The RCPT command + /// + RCPT, + + /// + /// The vrfy command + /// + VRFY, + + /// + /// The expn command + /// + EXPN, + + /// + /// The starttls command + /// + STARTTLS, + + /// + /// The authentication command + /// + AUTH, + } + + /// + /// Enumerates the reply code severities. + /// + public enum SmtpReplyCodeSeverities + { + /// + /// The unknown severity + /// + Unknown = 0, + + /// + /// The positive completion severity + /// + PositiveCompletion = 200, + + /// + /// The positive intermediate severity + /// + PositiveIntermediate = 300, + + /// + /// The transient negative severity + /// + TransientNegative = 400, + + /// + /// The permanent negative severity + /// + PermanentNegative = 500, + } + + /// + /// Enumerates the reply code categories. + /// + public enum SmtpReplyCodeCategories + { + /// + /// The unknown category + /// + Unknown = -1, + + /// + /// The syntax category + /// + Syntax = 0, + + /// + /// The information category + /// + Information = 1, + + /// + /// The connections category + /// + Connections = 2, + + /// + /// The unspecified a category + /// + UnspecifiedA = 3, + + /// + /// The unspecified b category + /// + UnspecifiedB = 4, + + /// + /// The system category + /// + System = 5, + } +} \ No newline at end of file diff --git a/Swan/Net/Smtp/SmtpClient.cs b/Swan/Net/Smtp/SmtpClient.cs new file mode 100644 index 0000000..bb0bfae --- /dev/null +++ b/Swan/Net/Smtp/SmtpClient.cs @@ -0,0 +1,388 @@ +namespace Swan.Net.Smtp +{ + using System.Threading; + using System; + using System.Linq; + using System.Net; + using System.Net.Sockets; + using System.Security; + using System.Text; + using System.Net.Security; + using System.Threading.Tasks; + using System.Collections.Generic; + using System.Net.Mail; + + /// + /// Represents a basic SMTP client that is capable of submitting messages to an SMTP server. + /// + /// + /// The following code explains how to send a simple e-mail. + /// + /// using System.Net.Mail; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new smtp client using google's smtp server + /// var client = new Swan.Net.Smtp.SmtpClient("smtp.gmail.com", 587); + /// + /// // send an email + /// client.SendMailAsync( + /// new MailMessage("sender@test.com", "recipient@test.cm", "Subject", "Body")); + /// } + /// } + /// + /// + /// The following code demonstrates how to sent an e-mail using a SmtpSessionState: + /// + /// using Swan.Net.Smtp; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new smtp client using google's smtp server + /// var client = new SmtpClient("smtp.gmail.com", 587); + /// + /// // create a new session state with a sender address + /// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; + /// + /// // add a recipient + /// session.Recipients.Add("recipient@test.cm"); + /// + /// // send + /// client.SendMailAsync(session); + /// } + /// } + /// + /// + /// The following code shows how to send an e-mail with an attachment using MimeKit: + /// + /// using MimeKit; + /// using Swan.Net.Smtp; + /// + /// class Example + /// { + /// static void Main() + /// { + /// // create a new smtp client using google's smtp server + /// var client = new SmtpClient("smtp.gmail.com", 587); + /// + /// // create a new session state with a sender address + /// var session = new SmtpSessionState { SenderAddress = "sender@test.com" }; + /// + /// // add a recipient + /// session.Recipients.Add("recipient@test.cm"); + /// + /// // load a file as an attachment + /// var attachment = new MimePart("image", "gif") + /// { + /// Content = new + /// MimeContent(File.OpenRead("meme.gif"), ContentEncoding.Default), + /// ContentDisposition = + /// new ContentDisposition(ContentDisposition.Attachment), + /// ContentTransferEncoding = ContentEncoding.Base64, + /// FileName = Path.GetFileName("meme.gif") + /// }; + /// + /// // send + /// client.SendMailAsync(session); + /// } + /// } + /// + /// + public class SmtpClient + { + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The port. + /// host. + public SmtpClient(string host, int port) + { + Host = host ?? throw new ArgumentNullException(nameof(host)); + Port = port; + ClientHostname = Network.HostName; + } + + /// + /// Gets or sets the credentials. No credentials will be used if set to null. + /// + /// + /// The credentials. + /// + public NetworkCredential Credentials { get; set; } + + /// + /// Gets the host. + /// + /// + /// The host. + /// + public string Host { get; } + + /// + /// Gets the port. + /// + /// + /// The port. + /// + public int Port { get; } + + /// + /// Gets or sets a value indicating whether the SSL is enabled. + /// If set to false, communication between client and server will not be secured. + /// + /// + /// true if [enable SSL]; otherwise, false. + /// + public bool EnableSsl { get; set; } + + /// + /// Gets or sets the name of the client that gets announced to the server. + /// + /// + /// The client hostname. + /// + public string ClientHostname { get; set; } + + /// + /// Sends an email message asynchronously. + /// + /// The message. + /// The session identifier. + /// The callback. + /// The cancellation token. + /// + /// A task that represents the asynchronous of send email operation. + /// + /// message. + public Task SendMailAsync( + MailMessage message, + string? sessionId = null, + RemoteCertificateValidationCallback? callback = null, + CancellationToken cancellationToken = default) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + var state = new SmtpSessionState + { + AuthMode = Credentials == null ? string.Empty : SmtpDefinitions.SmtpAuthMethods.Login, + ClientHostname = ClientHostname, + IsChannelSecure = EnableSsl, + SenderAddress = message.From.Address, + }; + + if (Credentials != null) + { + state.Username = Credentials.UserName; + state.Password = Credentials.Password; + } + + foreach (var recipient in message.To) + { + state.Recipients.Add(recipient.Address); + } + + state.DataBuffer.AddRange(message.ToMimeMessage().ToArray()); + + return SendMailAsync(state, sessionId, callback, cancellationToken); + } + + /// + /// Sends an email message using a session state object. + /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but + /// rather from the properties of this class. + /// + /// The state. + /// The session identifier. + /// The callback. + /// The cancellation token. + /// + /// A task that represents the asynchronous of send email operation. + /// + /// sessionState. + public Task SendMailAsync( + SmtpSessionState sessionState, + string? sessionId = null, + RemoteCertificateValidationCallback? callback = null, + CancellationToken cancellationToken = default) + { + if (sessionState == null) + throw new ArgumentNullException(nameof(sessionState)); + + return SendMailAsync(new[] { sessionState }, sessionId, callback, cancellationToken); + } + + /// + /// Sends an array of email messages using a session state object. + /// Credentials, Enable SSL and Client Hostname are NOT taken from the state object but + /// rather from the properties of this class. + /// + /// The session states. + /// The session identifier. + /// The callback. + /// The cancellation token. + /// + /// A task that represents the asynchronous of send email operation. + /// + /// sessionStates. + /// Could not upgrade the channel to SSL. + /// Defines an SMTP Exceptions class. + public async Task SendMailAsync( + IEnumerable sessionStates, + string? sessionId = null, + RemoteCertificateValidationCallback? callback = null, + CancellationToken cancellationToken = default) + { + if (sessionStates == null) + throw new ArgumentNullException(nameof(sessionStates)); + + using var tcpClient = new TcpClient(); + await tcpClient.ConnectAsync(Host, Port).ConfigureAwait(false); + + using var connection = new Connection(tcpClient, Encoding.UTF8, "\r\n", true, 1000); + var sender = new SmtpSender(sessionId); + + try + { + // Read the greeting message + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + + // EHLO 1 + await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false); + + // STARTTLS + if (EnableSsl) + { + sender.RequestText = $"{SmtpCommandNames.STARTTLS}"; + + await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + sender.ValidateReply(); + + if (await connection.UpgradeToSecureAsClientAsync(callback: callback).ConfigureAwait(false) == false) + throw new SecurityException("Could not upgrade the channel to SSL."); + } + + // EHLO 2 + await SendEhlo(sender, connection, cancellationToken).ConfigureAwait(false); + + // AUTH + if (Credentials != null) + { + var auth = new ConnectionAuth(connection, sender, Credentials); + await auth.AuthenticateAsync(cancellationToken).ConfigureAwait(false); + } + + foreach (var sessionState in sessionStates) + { + { + // MAIL FROM + sender.RequestText = $"{SmtpCommandNames.MAIL} FROM:<{sessionState.SenderAddress}>"; + + await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + sender.ValidateReply(); + } + + // RCPT TO + foreach (var recipient in sessionState.Recipients) + { + sender.RequestText = $"{SmtpCommandNames.RCPT} TO:<{recipient}>"; + + await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + sender.ValidateReply(); + } + + { + // DATA + sender.RequestText = $"{SmtpCommandNames.DATA}"; + + await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + sender.ValidateReply(); + } + + { + // CONTENT + var dataTerminator = sessionState.DataBuffer + .Skip(sessionState.DataBuffer.Count - 5) + .ToText(); + + sender.RequestText = $"Buffer ({sessionState.DataBuffer.Count} bytes)"; + + await connection.WriteDataAsync(sessionState.DataBuffer.ToArray(), true, cancellationToken).ConfigureAwait(false); + + if (!dataTerminator.EndsWith(SmtpDefinitions.SmtpDataCommandTerminator)) + await connection.WriteTextAsync(SmtpDefinitions.SmtpDataCommandTerminator, cancellationToken).ConfigureAwait(false); + + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + sender.ValidateReply(); + } + } + + { + // QUIT + sender.RequestText = $"{SmtpCommandNames.QUIT}"; + + await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false); + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + sender.ValidateReply(); + } + } + catch (Exception ex) + { + throw new SmtpException($"Could not send email - Session ID {sessionId}. {ex.Message}\r\n Last Request: {sender.RequestText}\r\n Last Reply: {sender.ReplyText}"); + } + } + + private async Task SendEhlo(SmtpSender sender, Connection connection, CancellationToken cancellationToken) + { + sender.RequestText = $"{SmtpCommandNames.EHLO} {ClientHostname}"; + + await connection.WriteLineAsync(sender.RequestText, cancellationToken).ConfigureAwait(false); + + do + { + sender.ReplyText = await connection.ReadLineAsync(cancellationToken).ConfigureAwait(false); + } + while (!sender.IsReplyOk); + + sender.ValidateReply(); + } + + private class ConnectionAuth + { + private readonly SmtpSender _sender; + private readonly Connection _connection; + private readonly NetworkCredential _credentials; + + public ConnectionAuth(Connection connection, SmtpSender sender, NetworkCredential credentials) + { + _connection = connection; + _sender = sender; + _credentials = credentials; + } + + public async Task AuthenticateAsync(CancellationToken ct) + { + _sender.RequestText = + $"{SmtpCommandNames.AUTH} {SmtpDefinitions.SmtpAuthMethods.Login} {Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.UserName))}"; + + await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false); + _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false); + _sender.ValidateReply(); + _sender.RequestText = Convert.ToBase64String(Encoding.UTF8.GetBytes(_credentials.Password)); + + await _connection.WriteLineAsync(_sender.RequestText, ct).ConfigureAwait(false); + _sender.ReplyText = await _connection.ReadLineAsync(ct).ConfigureAwait(false); + _sender.ValidateReply(); + } + } + } +} diff --git a/Swan/Net/Smtp/SmtpDefinitions.cs b/Swan/Net/Smtp/SmtpDefinitions.cs new file mode 100644 index 0000000..6b8fdad --- /dev/null +++ b/Swan/Net/Smtp/SmtpDefinitions.cs @@ -0,0 +1,29 @@ +namespace Swan.Net.Smtp +{ + /// + /// Contains useful constants and definitions. + /// + public static class SmtpDefinitions + { + /// + /// The string sequence that delimits the end of the DATA command. + /// + public const string SmtpDataCommandTerminator = "\r\n.\r\n"; + + /// + /// Lists the AUTH methods supported by default. + /// + public static class SmtpAuthMethods + { + /// + /// The plain method. + /// + public const string Plain = "PLAIN"; + + /// + /// The login method. + /// + public const string Login = "LOGIN"; + } + } +} diff --git a/Swan/Net/Smtp/SmtpSender.cs b/Swan/Net/Smtp/SmtpSender.cs new file mode 100644 index 0000000..61be247 --- /dev/null +++ b/Swan/Net/Smtp/SmtpSender.cs @@ -0,0 +1,60 @@ +namespace Swan.Net.Smtp +{ + using Logging; + using System; + using System.Linq; + using System.Net.Mail; + + /// + /// Use this class to store the sender session data. + /// + internal class SmtpSender + { + private readonly string _sessionId; + private string _requestText; + + public SmtpSender(string sessionId) + { + _sessionId = sessionId; + } + + public string RequestText + { + get => _requestText; + set + { + _requestText = value; + $" TX {_requestText}".Trace(typeof(SmtpClient), _sessionId); + } + } + + public string ReplyText { get; set; } + + public bool IsReplyOk => ReplyText.StartsWith("250 ", StringComparison.OrdinalIgnoreCase); + + public void ValidateReply() + { + if (ReplyText == null) + throw new SmtpException("There was no response from the server"); + + try + { + var response = SmtpServerReply.Parse(ReplyText); + $" RX {ReplyText} - {response.IsPositive}".Trace(typeof(SmtpClient), _sessionId); + + if (response.IsPositive) return; + + var responseContent = response.Content.Any() + ? string.Join(";", response.Content.ToArray()) + : string.Empty; + + throw new SmtpException((SmtpStatusCode)response.ReplyCode, responseContent); + } + catch (Exception ex) + { + if (!(ex is SmtpException)) + throw new SmtpException($"Could not parse server response: {ReplyText}"); + } + } + } +} \ No newline at end of file diff --git a/Swan/Net/Smtp/SmtpServerReply.cs b/Swan/Net/Smtp/SmtpServerReply.cs new file mode 100644 index 0000000..ae4b8c2 --- /dev/null +++ b/Swan/Net/Smtp/SmtpServerReply.cs @@ -0,0 +1,243 @@ +namespace Swan.Net.Smtp +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using System.Text; + + /// + /// Represents an SMTP server response object. + /// + public class SmtpServerReply + { + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The response code. + /// The status code. + /// The content. + public SmtpServerReply(int responseCode, string statusCode, params string[] content) + { + Content = new List(); + ReplyCode = responseCode; + EnhancedStatusCode = statusCode; + Content.AddRange(content); + IsValid = responseCode >= 200 && responseCode < 600; + ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; + ReplyCodeCategory = SmtpReplyCodeCategories.Unknown; + + if (!IsValid) return; + if (responseCode >= 200) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveCompletion; + if (responseCode >= 300) ReplyCodeSeverity = SmtpReplyCodeSeverities.PositiveIntermediate; + if (responseCode >= 400) ReplyCodeSeverity = SmtpReplyCodeSeverities.TransientNegative; + if (responseCode >= 500) ReplyCodeSeverity = SmtpReplyCodeSeverities.PermanentNegative; + if (responseCode >= 600) ReplyCodeSeverity = SmtpReplyCodeSeverities.Unknown; + + if (int.TryParse(responseCode.ToString(CultureInfo.InvariantCulture).Substring(1, 1), out var middleDigit)) + { + if (middleDigit >= 0 && middleDigit <= 5) + ReplyCodeCategory = (SmtpReplyCodeCategories) middleDigit; + } + } + + /// + /// Initializes a new instance of the class. + /// + public SmtpServerReply() + : this(0, string.Empty, string.Empty) + { + // placeholder + } + + /// + /// Initializes a new instance of the class. + /// + /// The response code. + /// The status code. + /// The content. + public SmtpServerReply(int responseCode, string statusCode, string content) + : this(responseCode, statusCode, new[] {content}) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The response code. + /// The content. + public SmtpServerReply(int responseCode, string content) + : this(responseCode, string.Empty, content) + { + } + + #endregion + + #region Pre-built responses (https://tools.ietf.org/html/rfc5321#section-4.2.2) + + /// + /// Gets the command unrecognized reply. + /// + public static SmtpServerReply CommandUnrecognized => + new SmtpServerReply(500, "Syntax error, command unrecognized"); + + /// + /// Gets the syntax error arguments reply. + /// + public static SmtpServerReply SyntaxErrorArguments => + new SmtpServerReply(501, "Syntax error in parameters or arguments"); + + /// + /// Gets the command not implemented reply. + /// + public static SmtpServerReply CommandNotImplemented => new SmtpServerReply(502, "Command not implemented"); + + /// + /// Gets the bad sequence of commands reply. + /// + public static SmtpServerReply BadSequenceOfCommands => new SmtpServerReply(503, "Bad sequence of commands"); + + /// + /// Gets the protocol violation reply. + /// = + public static SmtpServerReply ProtocolViolation => + new SmtpServerReply(451, "Requested action aborted: error in processing"); + + /// + /// Gets the system status bye reply. + /// + public static SmtpServerReply SystemStatusBye => + new SmtpServerReply(221, "Service closing transmission channel"); + + /// + /// Gets the system status help reply. + /// = + public static SmtpServerReply SystemStatusHelp => new SmtpServerReply(221, "Refer to RFC 5321"); + + /// + /// Gets the bad syntax command empty reply. + /// + public static SmtpServerReply BadSyntaxCommandEmpty => new SmtpServerReply(400, "Error: bad syntax"); + + /// + /// Gets the OK reply. + /// + public static SmtpServerReply Ok => new SmtpServerReply(250, "OK"); + + /// + /// Gets the authorization required reply. + /// + public static SmtpServerReply AuthorizationRequired => new SmtpServerReply(530, "Authorization Required"); + + #endregion + + #region Properties + + /// + /// Gets the response severity. + /// + public SmtpReplyCodeSeverities ReplyCodeSeverity { get; } + + /// + /// Gets the response category. + /// + public SmtpReplyCodeCategories ReplyCodeCategory { get; } + + /// + /// Gets the numeric response code. + /// + public int ReplyCode { get; } + + /// + /// Gets the enhanced status code. + /// + public string EnhancedStatusCode { get; } + + /// + /// Gets the content. + /// + public List Content { get; } + + /// + /// Returns true if the response code is between 200 and 599. + /// + public bool IsValid { get; } + + /// + /// Gets a value indicating whether this instance is positive. + /// + public bool IsPositive => ReplyCode >= 200 && ReplyCode <= 399; + + #endregion + + #region Methods + + /// + /// Parses the specified text into a Server Reply for thorough analysis. + /// + /// The text. + /// A new instance of SMTP server response object. + public static SmtpServerReply Parse(string text) + { + var lines = text.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries); + if (lines.Length == 0) return new SmtpServerReply(); + + var lastLineParts = lines.Last().Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries); + var enhancedStatusCode = string.Empty; + int.TryParse(lastLineParts[0], out var responseCode); + if (lastLineParts.Length > 1) + { + if (lastLineParts[1].Split('.').Length == 3) + enhancedStatusCode = lastLineParts[1]; + } + + var content = new List(); + + for (var i = 0; i < lines.Length; i++) + { + var splitChar = i == lines.Length - 1 ? " " : "-"; + + var lineParts = lines[i].Split(new[] {splitChar}, 2, StringSplitOptions.None); + var lineContent = lineParts.Last(); + if (string.IsNullOrWhiteSpace(enhancedStatusCode) == false) + lineContent = lineContent.Replace(enhancedStatusCode, string.Empty).Trim(); + + content.Add(lineContent); + } + + return new SmtpServerReply(responseCode, enhancedStatusCode, content.ToArray()); + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + var responseCodeText = ReplyCode.ToString(CultureInfo.InvariantCulture); + var statusCodeText = string.IsNullOrWhiteSpace(EnhancedStatusCode) + ? string.Empty + : $" {EnhancedStatusCode.Trim()}"; + if (Content.Count == 0) return $"{responseCodeText}{statusCodeText}"; + + var builder = new StringBuilder(); + + for (var i = 0; i < Content.Count; i++) + { + var isLastLine = i == Content.Count - 1; + + builder.Append(isLastLine + ? $"{responseCodeText}{statusCodeText} {Content[i]}" + : $"{responseCodeText}-{Content[i]}\r\n"); + } + + return builder.ToString(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Swan/Net/Smtp/SmtpSessionState.cs b/Swan/Net/Smtp/SmtpSessionState.cs new file mode 100644 index 0000000..9788362 --- /dev/null +++ b/Swan/Net/Smtp/SmtpSessionState.cs @@ -0,0 +1,158 @@ +namespace Swan.Net.Smtp +{ + using System.Collections.Generic; + + /// + /// Represents the state of an SMTP session associated with a client. + /// + public class SmtpSessionState + { + /// + /// Initializes a new instance of the class. + /// + public SmtpSessionState() + { + DataBuffer = new List(); + Reset(true); + ResetAuthentication(); + } + + #region Properties + + /// + /// Gets the contents of the data buffer. + /// + public List DataBuffer { get; protected set; } + + /// + /// Gets or sets a value indicating whether this instance has initiated. + /// + public bool HasInitiated { get; set; } + + /// + /// Gets or sets a value indicating whether the current session supports extensions. + /// + public bool SupportsExtensions { get; set; } + + /// + /// Gets or sets the client hostname. + /// + public string ClientHostname { get; set; } + + /// + /// Gets or sets a value indicating whether the session is currently receiving DATA. + /// + public bool IsInDataMode { get; set; } + + /// + /// Gets or sets the sender address. + /// + public string SenderAddress { get; set; } + + /// + /// Gets the recipients. + /// + public List Recipients { get; } = new List(); + + /// + /// Gets or sets the extended data supporting any additional field for storage by a responder implementation. + /// + public object ExtendedData { get; set; } + + #endregion + + #region AUTH State + + /// + /// Gets or sets a value indicating whether this instance is in authentication mode. + /// + public bool IsInAuthMode { get; set; } + + /// + /// Gets or sets the username. + /// + public string Username { get; set; } + + /// + /// Gets or sets the password. + /// + public string Password { get; set; } + + /// + /// Gets a value indicating whether this instance has provided username. + /// + public bool HasProvidedUsername => string.IsNullOrWhiteSpace(Username) == false; + + /// + /// Gets or sets a value indicating whether this instance is authenticated. + /// + public bool IsAuthenticated { get; set; } + + /// + /// Gets or sets the authentication mode. + /// + public string AuthMode { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is channel secure. + /// + public bool IsChannelSecure { get; set; } + + /// + /// Resets the authentication state. + /// + public void ResetAuthentication() + { + Username = string.Empty; + Password = string.Empty; + AuthMode = string.Empty; + IsInAuthMode = false; + IsAuthenticated = false; + } + + #endregion + + #region Methods + + /// + /// Resets the data mode to false, clears the recipients, the sender address and the data buffer. + /// + public void ResetEmail() + { + IsInDataMode = false; + Recipients.Clear(); + SenderAddress = string.Empty; + DataBuffer.Clear(); + } + + /// + /// Resets the state table entirely. + /// + /// if set to true [clear extension data]. + public void Reset(bool clearExtensionData) + { + HasInitiated = false; + SupportsExtensions = false; + ClientHostname = string.Empty; + ResetEmail(); + + if (clearExtensionData) + ExtendedData = null; + } + + /// + /// Creates a new object that is a copy of the current instance. + /// + /// A clone. + public virtual SmtpSessionState Clone() + { + var clonedState = this.CopyPropertiesToNew(new[] {nameof(DataBuffer)}); + clonedState.DataBuffer.AddRange(DataBuffer); + clonedState.Recipients.AddRange(Recipients); + + return clonedState; + } + + #endregion + } +} \ No newline at end of file diff --git a/Swan/ProcessResult.cs b/Swan/ProcessResult.cs new file mode 100644 index 0000000..c21e68f --- /dev/null +++ b/Swan/ProcessResult.cs @@ -0,0 +1,46 @@ +namespace Swan +{ + /// + /// Represents the text of the standard output and standard error + /// of a process, including its exit code. + /// + public class ProcessResult + { + /// + /// Initializes a new instance of the class. + /// + /// The exit code. + /// The standard output. + /// The standard error. + public ProcessResult(int exitCode, string standardOutput, string standardError) + { + ExitCode = exitCode; + StandardOutput = standardOutput; + StandardError = standardError; + } + + /// + /// Gets the exit code. + /// + /// + /// The exit code. + /// + public int ExitCode { get; } + + /// + /// Gets the text of the standard output. + /// + /// + /// The standard output. + /// + public string StandardOutput { get; } + + /// + /// Gets the text of the standard error. + /// + /// + /// The standard error. + /// + public string StandardError { get; } + } +} \ No newline at end of file diff --git a/Swan/ProcessRunner.cs b/Swan/ProcessRunner.cs new file mode 100644 index 0000000..db00888 --- /dev/null +++ b/Swan/ProcessRunner.cs @@ -0,0 +1,443 @@ +namespace Swan +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Provides methods to help create external processes, and efficiently capture the + /// standard error and standard output streams. + /// + public static class ProcessRunner + { + /// + /// Defines a delegate to handle binary data reception from the standard + /// output or standard error streams from a process. + /// + /// The process data. + /// The process. + public delegate void ProcessDataReceivedCallback(byte[] processData, Process process); + + /// + /// Runs the process asynchronously and if the exit code is 0, + /// returns all of the standard output text. If the exit code is something other than 0 + /// it returns the contents of standard error. + /// This method is meant to be used for programs that output a relatively small amount of text. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The cancellation token. + /// The type of the result produced by this Task. + /// + /// The following code explains how to run an external process using the + /// method. + /// + /// class Example + /// { + /// using System.Threading.Tasks; + /// using Swan; + /// + /// static async Task Main() + /// { + /// // execute a process and save its output + /// var data = await ProcessRunner. + /// GetProcessOutputAsync("dotnet", "--help"); + /// + /// // print the output + /// data.WriteLine(); + /// } + /// } + /// + /// + public static async Task GetProcessOutputAsync( + string filename, + string arguments = "", + string? workingDirectory = null, + CancellationToken cancellationToken = default) + { + var result = await GetProcessResultAsync(filename, arguments, workingDirectory, cancellationToken: cancellationToken).ConfigureAwait(false); + return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; + } + + /// + /// Runs the process asynchronously and if the exit code is 0, + /// returns all of the standard output text. If the exit code is something other than 0 + /// it returns the contents of standard error. + /// This method is meant to be used for programs that output a relatively small amount + /// of text using a different encoder. + /// + /// The filename. + /// The arguments. + /// The encoding. + /// The cancellation token. + /// + /// The type of the result produced by this Task. + /// + public static async Task GetProcessEncodedOutputAsync( + string filename, + string arguments = "", + Encoding? encoding = null, + CancellationToken cancellationToken = default) + { + var result = await GetProcessResultAsync(filename, arguments, null, encoding, cancellationToken).ConfigureAwait(false); + return result.ExitCode == 0 ? result.StandardOutput : result.StandardError; + } + + /// + /// Executes a process asynchronously and returns the text of the standard output and standard error streams + /// along with the exit code. This method is meant to be used for programs that output a relatively small + /// amount of text. + /// + /// The filename. + /// The arguments. + /// The cancellation token. + /// + /// Text of the standard output and standard error streams along with the exit code as a instance. + /// + /// filename. + public static Task GetProcessResultAsync( + string filename, + string arguments = "", + CancellationToken cancellationToken = default) => + GetProcessResultAsync(filename, arguments, null, Definitions.CurrentAnsiEncoding, cancellationToken); + + /// + /// Executes a process asynchronously and returns the text of the standard output and standard error streams + /// along with the exit code. This method is meant to be used for programs that output a relatively small + /// amount of text. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The encoding. + /// The cancellation token. + /// + /// Text of the standard output and standard error streams along with the exit code as a instance. + /// + /// filename. + /// + /// The following code describes how to run an external process using the method. + /// + /// class Example + /// { + /// using System.Threading.Tasks; + /// using Swan; + /// + /// static async Task Main() + /// { + /// // Execute a process asynchronously + /// var data = await ProcessRunner.GetProcessResultAsync("dotnet", "--help"); + /// + /// // print out the exit code + /// $"{data.ExitCode}".WriteLine(); + /// + /// // print out the output + /// data.StandardOutput.WriteLine(); + /// // and the error if exists + /// data.StandardError.Error(); + /// } + /// } + /// + public static async Task GetProcessResultAsync( + string filename, + string arguments, + string? workingDirectory, + Encoding? encoding = null, + CancellationToken cancellationToken = default) + { + if (filename == null) + throw new ArgumentNullException(nameof(filename)); + + if (encoding == null) + encoding = Definitions.CurrentAnsiEncoding; + + var standardOutputBuilder = new StringBuilder(); + var standardErrorBuilder = new StringBuilder(); + + var processReturn = await RunProcessAsync( + filename, + arguments, + workingDirectory, + (data, proc) => standardOutputBuilder.Append(encoding.GetString(data)), + (data, proc) => standardErrorBuilder.Append(encoding.GetString(data)), + encoding, + true, + cancellationToken) + .ConfigureAwait(false); + + return new ProcessResult(processReturn, standardOutputBuilder.ToString(), standardErrorBuilder.ToString()); + } + + /// + /// Runs an external process asynchronously, providing callbacks to + /// capture binary data from the standard error and standard output streams. + /// The callbacks contain a reference to the process so you can respond to output or + /// error streams by writing to the process' input stream. + /// The exit code (return value) will be -1 for forceful termination of the process. + /// + /// The filename. + /// The arguments. + /// The working directory. + /// The on output data. + /// The on error data. + /// The encoding. + /// if set to true the next data callback will wait until the current one completes. + /// The cancellation token. + /// + /// Value type will be -1 for forceful termination of the process. + /// + public static Task RunProcessAsync( + string filename, + string arguments, + string? workingDirectory, + ProcessDataReceivedCallback onOutputData, + ProcessDataReceivedCallback onErrorData, + Encoding encoding, + bool syncEvents = true, + CancellationToken cancellationToken = default) + { + if (filename == null) + throw new ArgumentNullException(nameof(filename)); + + return Task.Run(() => + { + // Setup the process and its corresponding start info + var process = new Process + { + EnableRaisingEvents = false, + StartInfo = new ProcessStartInfo + { + Arguments = arguments, + CreateNoWindow = true, + FileName = filename, + RedirectStandardError = true, + StandardErrorEncoding = encoding, + RedirectStandardOutput = true, + StandardOutputEncoding = encoding, + UseShellExecute = false, +#if NET461 + WindowStyle = ProcessWindowStyle.Hidden, +#endif + }, + }; + + if (!string.IsNullOrWhiteSpace(workingDirectory)) + process.StartInfo.WorkingDirectory = workingDirectory; + + // Launch the process and discard any buffered data for standard error and standard output + process.Start(); + process.StandardError.DiscardBufferedData(); + process.StandardOutput.DiscardBufferedData(); + + // Launch the asynchronous stream reading tasks + var readTasks = new Task[2]; + readTasks[0] = CopyStreamAsync( + process, + process.StandardOutput.BaseStream, + onOutputData, + syncEvents, + cancellationToken); + readTasks[1] = CopyStreamAsync( + process, + process.StandardError.BaseStream, + onErrorData, + syncEvents, + cancellationToken); + + try + { + // Wait for all tasks to complete + Task.WaitAll(readTasks, cancellationToken); + } + catch (TaskCanceledException) + { + // ignore + } + finally + { + // Wait for the process to exit + while (cancellationToken.IsCancellationRequested == false) + { + if (process.HasExited || process.WaitForExit(5)) + break; + } + + // Forcefully kill the process if it do not exit + try + { + if (process.HasExited == false) + process.Kill(); + } + catch + { + // swallow + } + } + + try + { + // Retrieve and return the exit code. + // -1 signals error + return process.HasExited ? process.ExitCode : -1; + } + catch + { + return -1; + } + }, cancellationToken); + } + + /// + /// Runs an external process asynchronously, providing callbacks to + /// capture binary data from the standard error and standard output streams. + /// The callbacks contain a reference to the process so you can respond to output or + /// error streams by writing to the process' input stream. + /// The exit code (return value) will be -1 for forceful termination of the process. + /// + /// The filename. + /// The arguments. + /// The on output data. + /// The on error data. + /// if set to true the next data callback will wait until the current one completes. + /// The cancellation token. + /// Value type will be -1 for forceful termination of the process. + /// + /// The following example illustrates how to run an external process using the + /// + /// method. + /// + /// class Example + /// { + /// using System.Diagnostics; + /// using System.Text; + /// using System.Threading.Tasks; + /// using Swan; + /// + /// static async Task Main() + /// { + /// // Execute a process asynchronously + /// var data = await ProcessRunner + /// .RunProcessAsync("dotnet", "--help", Print, Print); + /// + /// // flush all messages + /// Terminal.Flush(); + /// } + /// + /// // a callback to print both output or errors + /// static void Print(byte[] data, Process proc) => + /// Encoding.GetEncoding(0).GetString(data).WriteLine(); + /// } + /// + /// + public static Task RunProcessAsync( + string filename, + string arguments, + ProcessDataReceivedCallback onOutputData, + ProcessDataReceivedCallback onErrorData, + bool syncEvents = true, + CancellationToken cancellationToken = default) + => RunProcessAsync( + filename, + arguments, + null, + onOutputData, + onErrorData, + Definitions.CurrentAnsiEncoding, + syncEvents, + cancellationToken); + + /// + /// Copies the stream asynchronously. + /// + /// The process. + /// The source stream. + /// The on data callback. + /// if set to true [synchronize events]. + /// The cancellation token. + /// Total copies stream. + private static Task CopyStreamAsync( + Process process, + Stream baseStream, + ProcessDataReceivedCallback onDataCallback, + bool syncEvents, + CancellationToken ct) => + Task.Run(async () => + { + // define some state variables + var swapBuffer = new byte[2048]; // the buffer to copy data from one stream to the next + ulong totalCount = 0; // the total amount of bytes read + var hasExited = false; + + while (ct.IsCancellationRequested == false) + { + try + { + // Check if process is no longer valid + // if this condition holds, simply read the last bits of data available. + int readCount; // the bytes read in any given event + if (process.HasExited || process.WaitForExit(1)) + { + while (true) + { + try + { + readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct); + + if (readCount > 0) + { + totalCount += (ulong) readCount; + onDataCallback?.Invoke(swapBuffer.Skip(0).Take(readCount).ToArray(), process); + } + else + { + hasExited = true; + break; + } + } + catch + { + hasExited = true; + break; + } + } + } + + if (hasExited) break; + + // Try reading from the stream. < 0 means no read occurred. + readCount = await baseStream.ReadAsync(swapBuffer, 0, swapBuffer.Length, ct).ConfigureAwait(false); + + // When no read is done, we need to let is rest for a bit + if (readCount <= 0) + { + await Task.Delay(1, ct).ConfigureAwait(false); // do not hog CPU cycles doing nothing. + continue; + } + + totalCount += (ulong) readCount; + if (onDataCallback == null) continue; + + // Create the buffer to pass to the callback + var eventBuffer = swapBuffer.Skip(0).Take(readCount).ToArray(); + + // Create the data processing callback invocation + var eventTask = Task.Run(() => onDataCallback.Invoke(eventBuffer, process), ct); + + // wait for the event to process before the next read occurs + if (syncEvents) eventTask.Wait(ct); + } + catch + { + break; + } + } + + return totalCount; + }, ct); + } +} diff --git a/Swan/Services/ServiceBase.cs b/Swan/Services/ServiceBase.cs new file mode 100644 index 0000000..e790c14 --- /dev/null +++ b/Swan/Services/ServiceBase.cs @@ -0,0 +1,92 @@ +using System; + +#if !NET461 +namespace Swan.Services +{ + /// + /// Mimic a Windows ServiceBase class. Useful to keep compatibility with applications + /// running as services in OS different to Windows. + /// + [Obsolete("This abstract class will be removed in version 3.0")] + public abstract class ServiceBase + { + /// + /// Gets or sets a value indicating whether the service can be stopped once it has started. + /// + /// + /// true if this instance can stop; otherwise, false. + /// + public bool CanStop { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the service should be notified when the system is shutting down. + /// + /// + /// true if this instance can shutdown; otherwise, false. + /// + public bool CanShutdown { get; set; } + + /// + /// Gets or sets a value indicating whether the service can be paused and resumed. + /// + /// + /// true if this instance can pause and continue; otherwise, false. + /// + public bool CanPauseAndContinue { get; set; } + + /// + /// Gets or sets the exit code. + /// + /// + /// The exit code. + /// + public int ExitCode { get; set; } + + /// + /// Indicates whether to report Start, Stop, Pause, and Continue commands in the event log. + /// + /// + /// true if [automatic log]; otherwise, false. + /// + public bool AutoLog { get; set; } + + /// + /// Gets or sets the name of the service. + /// + /// + /// The name of the service. + /// + public string ServiceName { get; set; } + + /// + /// Stops the executing service. + /// + public void Stop() + { + if (!CanStop) return; + + CanStop = false; + OnStop(); + } + + /// + /// When implemented in a derived class, executes when a Start command is sent to the service by the Service Control Manager (SCM) + /// or when the operating system starts (for a service that starts automatically). Specifies actions to take when the service starts. + /// + /// The arguments. + protected virtual void OnStart(string[] args) + { + // do nothing + } + + /// + /// When implemented in a derived class, executes when a Stop command is sent to the service by the Service Control Manager (SCM). + /// Specifies actions to take when a service stops running. + /// + protected virtual void OnStop() + { + // do nothing + } + } +} +#endif diff --git a/Swan/Swan.csproj b/Swan/Swan.csproj new file mode 100644 index 0000000..cf7fb07 --- /dev/null +++ b/Swan/Swan.csproj @@ -0,0 +1,22 @@ + + + + Repeating code and reinventing the wheel is generally considered bad practice. At Unosquare we are committed to beautiful code and great software. Swan is a collection of classes and extension methods that we and other good developers have developed and evolved over the years. We found ourselves copying and pasting the same code for every project every time we started it. We decide to kill that cycle once and for all. This is the result of that idea. Our philosophy is that SWAN should have no external dependencies, it should be cross-platform, and it should be useful. + Copyright (c) 2016-2019 - Unosquare + Unosquare SWAN + netcoreapp3.0 + Swan + 2.4.2 + Unosquare + https://github.com/unosquare/swan/raw/master/swan-logo-32.png + https://github.com/unosquare/swan + https://raw.githubusercontent.com/unosquare/swan/master/LICENSE + best-practices netcore network objectmapper json-serialization + 8.0 + + + + + + + diff --git a/Swan/Threading/DelayProvider.cs b/Swan/Threading/DelayProvider.cs new file mode 100644 index 0000000..f1d1c23 --- /dev/null +++ b/Swan/Threading/DelayProvider.cs @@ -0,0 +1,141 @@ +namespace Swan.Threading +{ + using System; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Represents logic providing several delay mechanisms. + /// + /// + /// The following example shows how to implement delay mechanisms. + /// + /// using Swan.Threading; + /// + /// public class Example + /// { + /// public static void Main() + /// { + /// // using the ThreadSleep strategy + /// using (var delay = new DelayProvider(DelayProvider.DelayStrategy.ThreadSleep)) + /// { + /// // retrieve how much time was delayed + /// var time = delay.WaitOne(); + /// } + /// } + /// } + /// + /// + public sealed class DelayProvider : IDisposable + { + private readonly object _syncRoot = new object(); + private readonly Stopwatch _delayStopwatch = new Stopwatch(); + + private bool _isDisposed; + private IWaitEvent _delayEvent; + + /// + /// Initializes a new instance of the class. + /// + /// The strategy. + public DelayProvider(DelayStrategy strategy = DelayStrategy.TaskDelay) + { + Strategy = strategy; + } + + /// + /// Enumerates the different ways of providing delays. + /// + public enum DelayStrategy + { + /// + /// Using the Thread.Sleep(15) mechanism. + /// + ThreadSleep, + + /// + /// Using the Task.Delay(1).Wait mechanism. + /// + TaskDelay, + + /// + /// Using a wait event that completes in a background ThreadPool thread. + /// + ThreadPool, + } + + /// + /// Gets the selected delay strategy. + /// + public DelayStrategy Strategy { get; } + + /// + /// Creates the smallest possible, synchronous delay based on the selected strategy. + /// + /// The elapsed time of the delay. + public TimeSpan WaitOne() + { + lock (_syncRoot) + { + if (_isDisposed) return TimeSpan.Zero; + + _delayStopwatch.Restart(); + + switch (Strategy) + { + case DelayStrategy.ThreadSleep: + DelaySleep(); + break; + case DelayStrategy.TaskDelay: + DelayTask(); + break; + case DelayStrategy.ThreadPool: + DelayThreadPool(); + break; + } + + return _delayStopwatch.Elapsed; + } + } + + #region Dispose Pattern + + /// + public void Dispose() + { + lock (_syncRoot) + { + if (_isDisposed) return; + _isDisposed = true; + + _delayEvent?.Dispose(); + } + } + + #endregion + + #region Private Delay Mechanisms + + private static void DelaySleep() => Thread.Sleep(15); + + private static void DelayTask() => Task.Delay(1).Wait(); + + private void DelayThreadPool() + { + if (_delayEvent == null) + _delayEvent = WaitEventFactory.Create(isCompleted: true, useSlim: true); + + _delayEvent.Begin(); + ThreadPool.QueueUserWorkItem(s => + { + DelaySleep(); + _delayEvent.Complete(); + }); + + _delayEvent.Wait(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Swan/Threading/ThreadWorkerBase.cs b/Swan/Threading/ThreadWorkerBase.cs new file mode 100644 index 0000000..527a8f9 --- /dev/null +++ b/Swan/Threading/ThreadWorkerBase.cs @@ -0,0 +1,292 @@ +namespace Swan.Threading +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Provides a base implementation for application workers + /// that perform continuous, long-running tasks. This class + /// provides the ability to perform fine-grained control on these tasks. + /// + /// + public abstract class ThreadWorkerBase : WorkerBase + { + private readonly object _syncLock = new object(); + private readonly Thread _thread; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The thread priority. + /// The interval of cycle execution. + /// The cycle delay provide implementation. + protected ThreadWorkerBase(string name, ThreadPriority priority, TimeSpan period, IWorkerDelayProvider delayProvider) + : base(name, period) + { + DelayProvider = delayProvider; + _thread = new Thread(RunWorkerLoop) + { + IsBackground = true, + Priority = priority, + Name = name, + }; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The execution interval. + protected ThreadWorkerBase(string name, TimeSpan period) + : this(name, ThreadPriority.Normal, period, WorkerDelayProvider.Default) + { + // placeholder + } + + /// + /// Provides an implementation on a cycle delay provider. + /// + protected IWorkerDelayProvider DelayProvider { get; } + + /// + public override Task StartAsync() + { + lock (_syncLock) + { + if (WorkerState == WorkerState.Paused || WorkerState == WorkerState.Waiting) + return ResumeAsync(); + + if (WorkerState != WorkerState.Created) + return Task.FromResult(WorkerState); + + if (IsStopRequested) + return Task.FromResult(WorkerState); + + var task = QueueStateChange(StateChangeRequest.Start); + _thread.Start(); + return task; + } + } + + /// + public override Task PauseAsync() + { + lock (_syncLock) + { + if (WorkerState != WorkerState.Running && WorkerState != WorkerState.Waiting) + return Task.FromResult(WorkerState); + + return IsStopRequested ? Task.FromResult(WorkerState) : QueueStateChange(StateChangeRequest.Pause); + } + } + + /// + public override Task ResumeAsync() + { + lock (_syncLock) + { + if (WorkerState == WorkerState.Created) + return StartAsync(); + + if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting) + return Task.FromResult(WorkerState); + + return IsStopRequested ? Task.FromResult(WorkerState) : QueueStateChange(StateChangeRequest.Resume); + } + } + + /// + public override Task StopAsync() + { + lock (_syncLock) + { + if (WorkerState == WorkerState.Stopped || WorkerState == WorkerState.Created) + { + WorkerState = WorkerState.Stopped; + return Task.FromResult(WorkerState); + } + + return QueueStateChange(StateChangeRequest.Stop); + } + } + + /// + /// Suspends execution queues a new new cycle for execution. The delay is given in + /// milliseconds. When overridden in a derived class the wait handle will be set + /// whenever an interrupt is received. + /// + /// The remaining delay to wait for in the cycle. + /// Contains a reference to a task with the scheduled period delay. + /// The cancellation token to cancel waiting. + protected virtual void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) => + DelayProvider?.ExecuteCycleDelay(wantedDelay, delayTask, token); + + /// + protected override void OnDisposing() + { + lock (_syncLock) + { + if ((_thread.ThreadState & ThreadState.Unstarted) != ThreadState.Unstarted) + _thread.Join(); + } + } + + /// + /// Implements worker control, execution and delay logic in a loop. + /// + private void RunWorkerLoop() + { + while (WorkerState != WorkerState.Stopped && !IsDisposing && !IsDisposed) + { + CycleStopwatch.Restart(); + var interruptToken = CycleCancellation.Token; + var period = Period.TotalMilliseconds >= int.MaxValue ? -1 : Convert.ToInt32(Math.Floor(Period.TotalMilliseconds)); + var delayTask = Task.Delay(period, interruptToken); + var initialWorkerState = WorkerState; + + // Lock the cycle and capture relevant state valid for this cycle + CycleCompletedEvent.Reset(); + + // Process the tasks that are awaiting + if (ProcessStateChangeRequests()) + continue; + + try + { + if (initialWorkerState == WorkerState.Waiting && + !interruptToken.IsCancellationRequested) + { + // Mark the state as Running + WorkerState = WorkerState.Running; + + // Call the execution logic + ExecuteCycleLogic(interruptToken); + } + } + catch (Exception ex) + { + OnCycleException(ex); + } + finally + { + // Update the state + WorkerState = initialWorkerState == WorkerState.Paused + ? WorkerState.Paused + : WorkerState.Waiting; + + // Signal the cycle has been completed so new cycles can be executed + CycleCompletedEvent.Set(); + + if (!interruptToken.IsCancellationRequested) + { + var cycleDelay = ComputeCycleDelay(initialWorkerState); + if (cycleDelay == Timeout.Infinite) + delayTask = Task.Delay(Timeout.Infinite, interruptToken); + + ExecuteCycleDelay( + cycleDelay, + delayTask, + CycleCancellation.Token); + } + } + } + + ClearStateChangeRequests(); + WorkerState = WorkerState.Stopped; + } + + /// + /// Queues a transition in worker state for processing. Returns a task that can be awaited + /// when the operation completes. + /// + /// The request. + /// The awaitable task. + private Task QueueStateChange(StateChangeRequest request) + { + lock (_syncLock) + { + if (StateChangeTask != null) + return StateChangeTask; + + var waitingTask = new Task(() => + { + StateChangedEvent.Wait(); + lock (_syncLock) + { + StateChangeTask = null; + return WorkerState; + } + }); + + StateChangeTask = waitingTask; + StateChangedEvent.Reset(); + StateChangeRequests[request] = true; + waitingTask.Start(); + CycleCancellation.Cancel(); + + return waitingTask; + } + } + + /// + /// Processes the state change request by checking pending events and scheduling + /// cycle execution accordingly. The is also updated. + /// + /// Returns true if the execution should be terminated. false otherwise. + private bool ProcessStateChangeRequests() + { + lock (_syncLock) + { + var hasRequest = false; + var currentState = WorkerState; + + // Update the state in the given priority + if (StateChangeRequests[StateChangeRequest.Stop] || IsDisposing || IsDisposed) + { + hasRequest = true; + WorkerState = WorkerState.Stopped; + } + else if (StateChangeRequests[StateChangeRequest.Pause]) + { + hasRequest = true; + WorkerState = WorkerState.Paused; + } + else if (StateChangeRequests[StateChangeRequest.Start] || StateChangeRequests[StateChangeRequest.Resume]) + { + hasRequest = true; + WorkerState = WorkerState.Waiting; + } + + // Signals all state changes to continue + // as a command has been handled. + if (hasRequest) + { + ClearStateChangeRequests(); + OnStateChangeProcessed(currentState, WorkerState); + } + + return hasRequest; + } + } + + /// + /// Signals all state change requests to set. + /// + private void ClearStateChangeRequests() + { + lock (_syncLock) + { + // Mark all events as completed + StateChangeRequests[StateChangeRequest.Start] = false; + StateChangeRequests[StateChangeRequest.Pause] = false; + StateChangeRequests[StateChangeRequest.Resume] = false; + StateChangeRequests[StateChangeRequest.Stop] = false; + + StateChangedEvent.Set(); + CycleCompletedEvent.Set(); + } + } + } +} diff --git a/Swan/Threading/TimerWorkerBase.cs b/Swan/Threading/TimerWorkerBase.cs new file mode 100644 index 0000000..2175d7c --- /dev/null +++ b/Swan/Threading/TimerWorkerBase.cs @@ -0,0 +1,328 @@ +namespace Swan.Threading +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Provides a base implementation for application workers. + /// + /// + public abstract class TimerWorkerBase : WorkerBase + { + private readonly object _syncLock = new object(); + private readonly Timer _timer; + private bool _isTimerAlive = true; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The execution interval. + protected TimerWorkerBase(string name, TimeSpan period) + : base(name, period) + { + // Instantiate the timer that will be used to schedule cycles + _timer = new Timer( + ExecuteTimerCallback, + this, + Timeout.Infinite, + Timeout.Infinite); + } + + /// + public override Task StartAsync() + { + lock (_syncLock) + { + if (WorkerState == WorkerState.Paused || WorkerState == WorkerState.Waiting) + return ResumeAsync(); + + if (WorkerState != WorkerState.Created) + return Task.FromResult(WorkerState); + + if (IsStopRequested) + return Task.FromResult(WorkerState); + + var task = QueueStateChange(StateChangeRequest.Start); + Interrupt(); + return task; + } + } + + /// + public override Task PauseAsync() + { + lock (_syncLock) + { + if (WorkerState != WorkerState.Running && WorkerState != WorkerState.Waiting) + return Task.FromResult(WorkerState); + + if (IsStopRequested) + return Task.FromResult(WorkerState); + + var task = QueueStateChange(StateChangeRequest.Pause); + Interrupt(); + return task; + } + } + + /// + public override Task ResumeAsync() + { + lock (_syncLock) + { + if (WorkerState == WorkerState.Created) + return StartAsync(); + + if (WorkerState != WorkerState.Paused && WorkerState != WorkerState.Waiting) + return Task.FromResult(WorkerState); + + if (IsStopRequested) + return Task.FromResult(WorkerState); + + var task = QueueStateChange(StateChangeRequest.Resume); + Interrupt(); + return task; + } + } + + /// + public override Task StopAsync() + { + lock (_syncLock) + { + if (WorkerState == WorkerState.Stopped || WorkerState == WorkerState.Created) + { + WorkerState = WorkerState.Stopped; + return Task.FromResult(WorkerState); + } + + var task = QueueStateChange(StateChangeRequest.Stop); + Interrupt(); + return task; + } + } + + /// + /// Schedules a new cycle for execution. The delay is given in + /// milliseconds. Passing a delay of 0 means a new cycle should be executed + /// immediately. + /// + /// The delay. + protected void ScheduleCycle(int delay) + { + lock (_syncLock) + { + if (!_isTimerAlive) return; + _timer.Change(delay, Timeout.Infinite); + } + } + + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + lock (_syncLock) + { + if (!_isTimerAlive) return; + _isTimerAlive = false; + _timer.Dispose(); + } + } + + /// + /// Cancels the current token and schedules a new cycle immediately. + /// + private void Interrupt() + { + lock (_syncLock) + { + if (WorkerState == WorkerState.Stopped) + return; + + CycleCancellation.Cancel(); + ScheduleCycle(0); + } + } + + /// + /// Executes the worker cycle control logic. + /// This includes processing state change requests, + /// the execution of use cycle code, + /// and the scheduling of new cycles. + /// + private void ExecuteWorkerCycle() + { + CycleStopwatch.Restart(); + + lock (_syncLock) + { + if (IsDisposing || IsDisposed) + { + WorkerState = WorkerState.Stopped; + + // Cancel any awaiters + try { StateChangedEvent.Set(); } + catch { /* Ignore */ } + + return; + } + + // Prevent running another instance of the cycle + if (CycleCompletedEvent.IsSet == false) return; + + // Lock the cycle and capture relevant state valid for this cycle + CycleCompletedEvent.Reset(); + } + + var interruptToken = CycleCancellation.Token; + var initialWorkerState = WorkerState; + + // Process the tasks that are awaiting + if (ProcessStateChangeRequests()) + return; + + try + { + if (initialWorkerState == WorkerState.Waiting && + !interruptToken.IsCancellationRequested) + { + // Mark the state as Running + WorkerState = WorkerState.Running; + + // Call the execution logic + ExecuteCycleLogic(interruptToken); + } + } + catch (Exception ex) + { + OnCycleException(ex); + } + finally + { + // Update the state + WorkerState = initialWorkerState == WorkerState.Paused + ? WorkerState.Paused + : WorkerState.Waiting; + + lock (_syncLock) + { + // Signal the cycle has been completed so new cycles can be executed + CycleCompletedEvent.Set(); + + // Schedule a new cycle + ScheduleCycle(!interruptToken.IsCancellationRequested + ? ComputeCycleDelay(initialWorkerState) + : 0); + } + } + } + + /// + /// Represents the callback that is executed when the ticks. + /// + /// The state -- this contains the worker. + private void ExecuteTimerCallback(object state) => ExecuteWorkerCycle(); + + /// + /// Queues a transition in worker state for processing. Returns a task that can be awaited + /// when the operation completes. + /// + /// The request. + /// The awaitable task. + private Task QueueStateChange(StateChangeRequest request) + { + lock (_syncLock) + { + if (StateChangeTask != null) + return StateChangeTask; + + var waitingTask = new Task(() => + { + StateChangedEvent.Wait(); + lock (_syncLock) + { + StateChangeTask = null; + return WorkerState; + } + }); + + StateChangeTask = waitingTask; + StateChangedEvent.Reset(); + StateChangeRequests[request] = true; + waitingTask.Start(); + CycleCancellation.Cancel(); + + return waitingTask; + } + } + + /// + /// Processes the state change queue by checking pending events and scheduling + /// cycle execution accordingly. The is also updated. + /// + /// Returns true if the execution should be terminated. false otherwise. + private bool ProcessStateChangeRequests() + { + lock (_syncLock) + { + var currentState = WorkerState; + var hasRequest = false; + var schedule = 0; + + // Update the state according to request priority + if (StateChangeRequests[StateChangeRequest.Stop] || IsDisposing || IsDisposed) + { + hasRequest = true; + WorkerState = WorkerState.Stopped; + schedule = StateChangeRequests[StateChangeRequest.Stop] ? Timeout.Infinite : 0; + } + else if (StateChangeRequests[StateChangeRequest.Pause]) + { + hasRequest = true; + WorkerState = WorkerState.Paused; + schedule = Timeout.Infinite; + } + else if (StateChangeRequests[StateChangeRequest.Start] || StateChangeRequests[StateChangeRequest.Resume]) + { + hasRequest = true; + WorkerState = WorkerState.Waiting; + } + + // Signals all state changes to continue + // as a command has been handled. + if (hasRequest) + { + ClearStateChangeRequests(schedule, currentState, WorkerState); + } + + return hasRequest; + } + } + + /// + /// Signals all state change requests to set. + /// + /// The cycle schedule. + /// The previous worker state. + /// The new worker state. + private void ClearStateChangeRequests(int schedule, WorkerState oldState, WorkerState newState) + { + lock (_syncLock) + { + // Mark all events as completed + StateChangeRequests[StateChangeRequest.Start] = false; + StateChangeRequests[StateChangeRequest.Pause] = false; + StateChangeRequests[StateChangeRequest.Resume] = false; + StateChangeRequests[StateChangeRequest.Stop] = false; + + StateChangedEvent.Set(); + CycleCompletedEvent.Set(); + OnStateChangeProcessed(oldState, newState); + ScheduleCycle(schedule); + } + } + } +} diff --git a/Swan/Threading/WorkerBase.cs b/Swan/Threading/WorkerBase.cs new file mode 100644 index 0000000..ac3e681 --- /dev/null +++ b/Swan/Threading/WorkerBase.cs @@ -0,0 +1,240 @@ +namespace Swan.Threading +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Provides base infrastructure for Timer and Thread workers. + /// + /// + public abstract class WorkerBase : IWorker, IDisposable + { + // Since these are API property backers, we use interlocked to read from them + // to avoid deadlocked reads + private readonly object _syncLock = new object(); + + private readonly AtomicBoolean _isDisposed = new AtomicBoolean(); + private readonly AtomicBoolean _isDisposing = new AtomicBoolean(); + private readonly AtomicEnum _workerState = new AtomicEnum(WorkerState.Created); + private readonly AtomicTimeSpan _timeSpan; + + /// + /// Initializes a new instance of the class. + /// + /// The name. + /// The execution interval. + protected WorkerBase(string name, TimeSpan period) + { + Name = name; + _timeSpan = new AtomicTimeSpan(period); + + StateChangeRequests = new Dictionary(5) + { + [StateChangeRequest.Start] = false, + [StateChangeRequest.Pause] = false, + [StateChangeRequest.Resume] = false, + [StateChangeRequest.Stop] = false, + }; + } + + /// + /// Enumerates all the different state change requests. + /// + protected enum StateChangeRequest + { + /// + /// No state change request. + /// + None, + + /// + /// Start state change request + /// + Start, + + /// + /// Pause state change request + /// + Pause, + + /// + /// Resume state change request + /// + Resume, + + /// + /// Stop state change request + /// + Stop, + } + + /// + public string Name { get; } + + /// + public TimeSpan Period + { + get => _timeSpan.Value; + set => _timeSpan.Value = value; + } + + /// + public WorkerState WorkerState + { + get => _workerState.Value; + protected set => _workerState.Value = value; + } + + /// + public bool IsDisposed + { + get => _isDisposed.Value; + protected set => _isDisposed.Value = value; + } + + /// + public bool IsDisposing + { + get => _isDisposing.Value; + protected set => _isDisposing.Value = value; + } + + /// + /// Gets the default period of 15 milliseconds which is the default precision for timers. + /// + protected static TimeSpan DefaultPeriod { get; } = TimeSpan.FromMilliseconds(15); + + /// + /// Gets a value indicating whether stop has been requested. + /// This is useful to prevent more requests from being issued. + /// + protected bool IsStopRequested => StateChangeRequests[StateChangeRequest.Stop]; + + /// + /// Gets the cycle stopwatch. + /// + protected Stopwatch CycleStopwatch { get; } = new Stopwatch(); + + /// + /// Gets the state change requests. + /// + protected Dictionary StateChangeRequests { get; } + + /// + /// Gets the cycle completed event. + /// + protected ManualResetEventSlim CycleCompletedEvent { get; } = new ManualResetEventSlim(true); + + /// + /// Gets the state changed event. + /// + protected ManualResetEventSlim StateChangedEvent { get; } = new ManualResetEventSlim(true); + + /// + /// Gets the cycle logic cancellation owner. + /// + protected CancellationTokenOwner CycleCancellation { get; } = new CancellationTokenOwner(); + + /// + /// Gets or sets the state change task. + /// + protected Task? StateChangeTask { get; set; } + + /// + public abstract Task StartAsync(); + + /// + public abstract Task PauseAsync(); + + /// + public abstract Task ResumeAsync(); + + /// + public abstract Task StopAsync(); + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + lock (_syncLock) + { + if (IsDisposed || IsDisposing) return; + IsDisposing = true; + } + + // This also ensures the state change queue gets cleared + StopAsync().Wait(); + StateChangedEvent.Set(); + CycleCompletedEvent.Set(); + + OnDisposing(); + + CycleStopwatch.Stop(); + StateChangedEvent.Dispose(); + CycleCompletedEvent.Dispose(); + CycleCancellation.Dispose(); + + IsDisposed = true; + IsDisposing = false; + } + + /// + /// Handles the cycle logic exceptions. + /// + /// The exception that was thrown. + protected abstract void OnCycleException(Exception ex); + + /// + /// Represents the user defined logic to be executed on a single worker cycle. + /// Check the cancellation token continuously if you need responsive interrupts. + /// + /// The cancellation token. + protected abstract void ExecuteCycleLogic(CancellationToken cancellationToken); + + /// + /// This method is called automatically when is called. + /// Makes sure you release all resources within this call. + /// + protected abstract void OnDisposing(); + + /// + /// Called when a state change request is processed. + /// + /// The state before the change. + /// The new state. + protected virtual void OnStateChangeProcessed(WorkerState previousState, WorkerState newState) + { + // placeholder + } + + /// + /// Computes the cycle delay. + /// + /// Initial state of the worker. + /// The number of milliseconds to delay for. + protected int ComputeCycleDelay(WorkerState initialWorkerState) + { + var elapsedMillis = CycleStopwatch.ElapsedMilliseconds; + var period = Period; + var periodMillis = period.TotalMilliseconds; + var delayMillis = periodMillis - elapsedMillis; + + if (initialWorkerState == WorkerState.Paused || period == TimeSpan.MaxValue || delayMillis >= int.MaxValue) + return Timeout.Infinite; + + return elapsedMillis >= periodMillis ? 0 : Convert.ToInt32(Math.Floor(delayMillis)); + } + } +} diff --git a/Swan/Threading/WorkerDelayProvider.cs b/Swan/Threading/WorkerDelayProvider.cs new file mode 100644 index 0000000..32e99d2 --- /dev/null +++ b/Swan/Threading/WorkerDelayProvider.cs @@ -0,0 +1,151 @@ +namespace Swan.Threading +{ + using System; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + + /// + /// Represents a class that implements delay logic for thread workers. + /// + public static class WorkerDelayProvider + { + /// + /// Gets the default delay provider. + /// + public static IWorkerDelayProvider Default => TokenTimeout; + + /// + /// Provides a delay implementation which simply waits on the task and cancels on + /// the cancellation token. + /// + public static IWorkerDelayProvider Token => new TokenCancellableDelay(); + + /// + /// Provides a delay implementation which waits on the task and cancels on both, + /// the cancellation token and a wanted delay timeout. + /// + public static IWorkerDelayProvider TokenTimeout => new TokenTimeoutCancellableDelay(); + + /// + /// Provides a delay implementation which uses short sleep intervals of 5ms. + /// + public static IWorkerDelayProvider TokenSleep => new TokenSleepDelay(); + + /// + /// Provides a delay implementation which uses short delay intervals of 5ms and + /// a wait on the delay task in the final loop. + /// + public static IWorkerDelayProvider SteppedToken => new SteppedTokenDelay(); + + private class TokenCancellableDelay : IWorkerDelayProvider + { + public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) + { + if (wantedDelay == 0 || wantedDelay < -1) + return; + + // for wanted delays of less than 30ms it is not worth + // passing a timeout or a token as it only adds unnecessary + // overhead. + if (wantedDelay <= 30) + { + try { delayTask.Wait(token); } + catch { /* ignore */ } + return; + } + + // only wait on the cancellation token + // or until the task completes normally + try { delayTask.Wait(token); } + catch { /* ignore */ } + } + } + + private class TokenTimeoutCancellableDelay : IWorkerDelayProvider + { + public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) + { + if (wantedDelay == 0 || wantedDelay < -1) + return; + + // for wanted delays of less than 30ms it is not worth + // passing a timeout or a token as it only adds unnecessary + // overhead. + if (wantedDelay <= 30) + { + try { delayTask.Wait(token); } + catch { /* ignore */ } + return; + } + + try { delayTask.Wait(wantedDelay, token); } + catch { /* ignore */ } + } + } + + private class TokenSleepDelay : IWorkerDelayProvider + { + private readonly Stopwatch _elapsedWait = new Stopwatch(); + + public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) + { + _elapsedWait.Restart(); + + if (wantedDelay == 0 || wantedDelay < -1) + return; + + while (!token.IsCancellationRequested) + { + Thread.Sleep(5); + + if (wantedDelay != Timeout.Infinite && _elapsedWait.ElapsedMilliseconds >= wantedDelay) + break; + } + } + } + + private class SteppedTokenDelay : IWorkerDelayProvider + { + private const int StepMilliseconds = 15; + private readonly Stopwatch _elapsedWait = new Stopwatch(); + + public void ExecuteCycleDelay(int wantedDelay, Task delayTask, CancellationToken token) + { + _elapsedWait.Restart(); + + if (wantedDelay == 0 || wantedDelay < -1) + return; + + if (wantedDelay == Timeout.Infinite) + { + try { delayTask.Wait(wantedDelay, token); } + catch { /* Ignore cancelled tasks */ } + return; + } + + while (!token.IsCancellationRequested) + { + var remainingWaitTime = wantedDelay - Convert.ToInt32(_elapsedWait.ElapsedMilliseconds); + + // Exit for no remaining wait time + if (remainingWaitTime <= 0) + break; + + if (remainingWaitTime >= StepMilliseconds) + { + Task.Delay(StepMilliseconds, token).Wait(token); + } + else + { + try { delayTask.Wait(remainingWaitTime); } + catch { /* ignore cancellation of task exception */ } + } + + if (_elapsedWait.ElapsedMilliseconds >= wantedDelay) + break; + } + } + } + } +} diff --git a/Swan/ViewModelBase.cs b/Swan/ViewModelBase.cs new file mode 100644 index 0000000..7601871 --- /dev/null +++ b/Swan/ViewModelBase.cs @@ -0,0 +1,124 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +namespace Swan +{ + /// + /// A base class for implementing models that fire notifications when their properties change. + /// This class is ideal for implementing MVVM driven UIs. + /// + /// + public abstract class ViewModelBase : INotifyPropertyChanged + { + private readonly ConcurrentDictionary _queuedNotifications = new ConcurrentDictionary(); + private readonly bool _useDeferredNotifications; + + /// + /// Initializes a new instance of the class. + /// + /// Set to true to use deferred notifications in the background. + protected ViewModelBase(bool useDeferredNotifications) + { + _useDeferredNotifications = useDeferredNotifications; + } + + /// + /// Initializes a new instance of the class. + /// + protected ViewModelBase() + : this(false) + { + // placeholder + } + + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// Checks if a property already matches a desired value. Sets the property and + /// notifies listeners only when necessary. + /// Type of the property. + /// Reference to a property with both getter and setter. + /// Desired value for the property. + /// Name of the property used to notify listeners. This + /// value is optional and can be provided automatically when invoked from compilers that + /// support CallerMemberName. + /// An array of property names to notify in addition to notifying the changes on the current property name. + /// True if the value was changed, false if the existing value matched the + /// desired value. + protected bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = "", string[] notifyAlso = null) + { + if (EqualityComparer.Default.Equals(storage, value)) + return false; + + storage = value; + NotifyPropertyChanged(propertyName, notifyAlso); + return true; + } + + /// + /// Notifies one or more properties changed. + /// + /// The property names. + protected void NotifyPropertyChanged(params string[] propertyNames) => NotifyPropertyChanged(null, propertyNames); + + /// + /// Notifies one or more properties changed. + /// + /// The main property. + /// The auxiliary properties. + private void NotifyPropertyChanged(string mainProperty, string[] auxiliaryProperties) + { + // Queue property notification + if (string.IsNullOrWhiteSpace(mainProperty) == false) + _queuedNotifications[mainProperty] = true; + + // Set the state for notification properties + if (auxiliaryProperties != null) + { + foreach (var property in auxiliaryProperties) + { + if (string.IsNullOrWhiteSpace(property) == false) + _queuedNotifications[property] = true; + } + } + + // Depending on operation mode, either fire the notifications in the background + // or fire them immediately + if (_useDeferredNotifications) + Task.Run(NotifyQueuedProperties); + else + NotifyQueuedProperties(); + } + + /// + /// Notifies the queued properties and resets the property name to a non-queued stated. + /// + private void NotifyQueuedProperties() + { + // get a snapshot of property names. + var propertyNames = _queuedNotifications.Keys.ToArray(); + + // Iterate through the properties + foreach (var property in propertyNames) + { + // don't notify if we don't have a change + if (!_queuedNotifications[property]) continue; + + // notify and reset queued state to false + try { OnPropertyChanged(property); } + finally { _queuedNotifications[property] = false; } + } + } + + /// + /// Called when a property changes its backing value. + /// + /// Name of the property. + private void OnPropertyChanged(string propertyName) => + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName ?? string.Empty)); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/Definitions.cs b/Unosquare.RaspberryIO.Abstractions/Definitions.cs new file mode 100644 index 0000000..b1e368c --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/Definitions.cs @@ -0,0 +1,28 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + /// + /// Represents Definitions for GPIO information. + /// + public static class Definitions + { + private static readonly int[] GpioToPhysR1 = + { + 3, 5, -1, -1, 7, -1, -1, 26, 24, 21, 19, 23, -1, -1, 8, 10, -1, 11, 12, -1, -1, 13, 15, 16, 18, 22, -1, -1, -1, -1, -1, -1, + }; + + private static readonly int[] GpioToPhysR2 = + { + 27, 28, 3, 5, 7, 29, 31, 26, 24, 21, 19, 23, 32, 33, 8, 10, 36, 11, 12, 35, 38, 40, 15, 16, 18, 22, 37, 13, // P1 + 3, 4, 5, 6, // P5 + }; + + /// + /// BCMs to physical pin number. + /// + /// The rev. + /// The BCM pin. + /// The physical pin number. + public static int BcmToPhysicalPinNumber(BoardRevision rev, BcmPin bcmPin) => + rev == BoardRevision.Rev1 ? GpioToPhysR1[(int)bcmPin] : GpioToPhysR2[(int)bcmPin]; + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/Enums.cs b/Unosquare.RaspberryIO.Abstractions/Enums.cs new file mode 100644 index 0000000..0a25eed --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/Enums.cs @@ -0,0 +1,527 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + /// + /// Defines the SPI channel numbers. + /// + public enum SpiChannelNumber + { + /// + /// The channel 0 + /// + Channel0 = 0, + + /// + /// The channel 1 + /// + Channel1 = 1, + } + + /// + /// Defines the GPIO Pin values 0 for low, 1 for High. + /// + public enum GpioPinValue + { + /// + /// Digital high + /// + High = 1, + + /// + /// Digital low + /// + Low = 0, + } + + /// + /// The GPIO pin resistor mode. This is used on input pins so that their + /// lines are not floating. + /// + public enum GpioPinResistorPullMode + { + /// + /// Pull resistor not active. Line floating + /// + Off = 0, + + /// + /// Pull resistor sets a default value of 0 on no-connects + /// + PullDown = 1, + + /// + /// Pull resistor sets a default value of 1 on no-connects + /// + PullUp = 2, + } + + /// + /// Defines the different drive modes of a GPIO pin. + /// + public enum GpioPinDriveMode + { + /// + /// Input drive mode (perform reads) + /// + Input = 0, + + /// + /// Output drive mode (perform writes) + /// + Output = 1, + + /// + /// PWM output mode (only certain pins support this -- 2 of them at the moment) + /// + PwmOutput = 2, + + /// + /// GPIO Clock output mode (only a pin supports this at this time) + /// + GpioClock = 3, + + /// + /// The alt0 operating mode + /// + Alt0 = 4, + + /// + /// The alt1 operating mode + /// + Alt1 = 5, + + /// + /// The alt2 operating mode + /// + Alt2 = 6, + + /// + /// The alt3 operating mode + /// + Alt3 = 7, + } + + /// + /// Defines the different threading locking keys. + /// + public enum ThreadLockKey + { + /// + /// The lock 0 + /// + Lock0 = 0, + + /// + /// The lock 1 + /// + Lock1 = 1, + + /// + /// The lock 2 + /// + Lock2 = 2, + + /// + /// The lock 3 + /// + Lock3 = 3, + } + + /// + /// Defines the different edge detection modes for pin interrupts. + /// + public enum EdgeDetection + { + /// + /// Falling Edge + /// + FallingEdge, + + /// + /// Rising edge + /// + RisingEdge, + + /// + /// Both, falling and rising edges + /// + FallingAndRisingEdge, + } + + /// + /// The hardware revision of the board. + /// + public enum BoardRevision + { + /// + /// Revision 1 (the early Model A and B's). + /// + Rev1 = 1, + + /// + /// Revision 2 (everything else - it covers the B, B+ and CM). + /// + Rev2 = 2, + } + + /// + /// Defines the Header connectors available. + /// + public enum GpioHeader + { + /// + /// Not defined + /// + None, + + /// + /// P1 connector (main connector) + /// + P1, + + /// + /// P5 connector (auxiliary, not commonly used) + /// + P5, + } + + /// + /// Defines all the BCM Pin numbers available for the user. + /// + public enum BcmPin + { + /// + /// GPIO 0 + /// + Gpio00 = 0, + + /// + /// GPIO 1 + /// + Gpio01 = 1, + + /// + /// GPIO02 + /// + Gpio02 = 2, + + /// + /// GPIO 3 + /// + Gpio03 = 3, + + /// + /// GPIO 4 + /// + Gpio04 = 4, + + /// + /// GPIO 5 + /// + Gpio05 = 5, + + /// + /// GPIO 6 + /// + Gpio06 = 6, + + /// + /// GPIO 7 + /// + Gpio07 = 7, + + /// + /// GPIO 8 + /// + Gpio08 = 8, + + /// + /// GPIO 9 + /// + Gpio09 = 9, + + /// + /// GPIO 10 + /// + Gpio10 = 10, + + /// + /// GPIO 11 + /// + Gpio11 = 11, + + /// + /// GPIO 12 + /// + Gpio12 = 12, + + /// + /// GPIO 13 + /// + Gpio13 = 13, + + /// + /// GPIO 14 + /// + Gpio14 = 14, + + /// + /// GPIO 15 + /// + Gpio15 = 15, + + /// + /// GPIO 16 + /// + Gpio16 = 16, + + /// + /// GPIO 17 + /// + Gpio17 = 17, + + /// + /// GPIO 18 + /// + Gpio18 = 18, + + /// + /// GPIO 19 + /// + Gpio19 = 19, + + /// + /// GPIO 20 + /// + Gpio20 = 20, + + /// + /// GPIO 21 + /// + Gpio21 = 21, + + /// + /// GPIO 22 + /// + Gpio22 = 22, + + /// + /// GPIO 23 + /// + Gpio23 = 23, + + /// + /// GPIO 24 + /// + Gpio24 = 24, + + /// + /// GPIO 25 + /// + Gpio25 = 25, + + /// + /// GPIO 26 + /// + Gpio26 = 26, + + /// + /// GPIO 27 + /// + Gpio27 = 27, + + /// + /// GPIO 28 + /// + Gpio28 = 28, + + /// + /// GPIO 29 + /// + Gpio29 = 29, + + /// + /// GPIO 30 + /// + Gpio30 = 30, + + /// + /// GPIO 31 + /// + Gpio31 = 31, + } + + /// + /// Enumerates the different pins on the P1 Header. + /// Enumeration values correspond to the physical pin number. + /// + public enum P1 + { + /// + /// Header P1 Physical Pin 3. GPIO 0 for rev1 or GPIO 2 for rev2. + /// + Pin03 = 3, + + /// + /// Header P1 Physical Pin 5. GPIO 1 for rev1 or GPIO 3 for rev2. + /// + Pin05 = 5, + + /// + /// Header P1 Physical Pin 7. GPIO 4. + /// + Pin07 = 7, + + /// + /// Header P1 Physical Pin 11. GPIO 17. + /// + Pin11 = 11, + + /// + /// Header P1 Physical Pin 13. GPIO 21 for rev1 or GPIO 27 for rev2. + /// + Pin13 = 13, + + /// + /// Header P1 Physical Pin 15. GPIO 22. + /// + Pin15 = 15, + + /// + /// Header P1 Physical Pin 19. GPIO 10. + /// + Pin19 = 19, + + /// + /// Header P1 Physical Pin 21. GPIO 9. + /// + Pin21 = 21, + + /// + /// Header P1 Physical Pin 23. GPIO 11. + /// + Pin23 = 23, + + /// + /// Header P1 Physical Pin 27. GPIO 0. + /// + Pin27 = 27, + + /// + /// Header P1 Physical Pin 29. GPIO 5. + /// + Pin29 = 29, + + /// + /// Header P1 Physical Pin 31. GPIO 6. + /// + Pin31 = 31, + + /// + /// Header P1 Physical Pin 33. GPIO 13. + /// + Pin33 = 33, + + /// + /// Header P1 Physical Pin 35. GPIO 19. + /// + Pin35 = 35, + + /// + /// Header P1 Physical Pin 37. GPIO 26. + /// + Pin37 = 37, + + /// + /// Header P1 Physical Pin 8. GPIO 14. + /// + Pin08 = 8, + + /// + /// Header P1 Physical Pin 10. GPIO 15. + /// + Pin10 = 10, + + /// + /// Header P1 Physical Pin 12. GPIO 18. + /// + Pin12 = 12, + + /// + /// Header P1 Physical Pin 16. GPIO 23. + /// + Pin16 = 16, + + /// + /// Header P1 Physical Pin 18. GPIO 24. + /// + Pin18 = 18, + + /// + /// Header P1 Physical Pin 22. GPIO 25. + /// + Pin22 = 22, + + /// + /// Header P1 Physical Pin 24. GPIO 8. + /// + Pin24 = 24, + + /// + /// Header P1 Physical Pin 26. GPIO 7. + /// + Pin26 = 26, + + /// + /// Header P1 Physical Pin 28. GPIO 1. + /// + Pin28 = 28, + + /// + /// Header P1 Physical Pin 32. GPIO 12. + /// + Pin32 = 32, + + /// + /// Header P1 Physical Pin 36. GPIO 16. + /// + Pin36 = 36, + + /// + /// Header P1 Physical Pin 38. GPIO 20. + /// + Pin38 = 38, + + /// + /// Header P1 Physical Pin 40. GPIO 21. + /// + Pin40 = 40, + } + + /// + /// Enumerates the different pins on the P5 Header + /// as commonly referenced by Raspberry Pi documentation. + /// Enumeration values correspond to the physical pin number. + /// + public enum P5 + { + /// + /// Header P5 Physical Pin 3, GPIO 28. + /// + Pin03 = 3, + + /// + /// Header P5 Physical Pin 4, GPIO 29. + /// + Pin04 = 4, + + /// + /// Header P5 Physical Pin 5, GPIO 30. + /// + Pin05 = 5, + + /// + /// Header P5 Physical Pin 6, GPIO 31. + /// + Pin06 = 6, + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/IBootstrap.cs b/Unosquare.RaspberryIO.Abstractions/IBootstrap.cs new file mode 100644 index 0000000..0a3a9ed --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/IBootstrap.cs @@ -0,0 +1,13 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + /// + /// Interface for bootstrapping an implementation. + /// + public interface IBootstrap + { + /// + /// Bootstraps an implementation. + /// + void Bootstrap(); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/IGpioController.cs b/Unosquare.RaspberryIO.Abstractions/IGpioController.cs new file mode 100644 index 0000000..356c517 --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/IGpioController.cs @@ -0,0 +1,51 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + using System.Collections.Generic; + + /// + /// Interface for Raspberry Pi GPIO controller. + /// + /// + public interface IGpioController : IReadOnlyCollection + { + /// + /// Gets the with the specified BCM pin. + /// + /// + /// The . + /// + /// The BCM pin number. + /// A reference to the GPIO pin. + IGpioPin this[int bcmPinNumber] { get; } + + /// + /// Gets the with the specified BCM pin. + /// + /// + /// The . + /// + /// The BCM pin. + /// A reference to the GPIO pin. + IGpioPin this[BcmPin bcmPin] { get; } + + /// + /// Gets the with the specified pin number. + /// + /// + /// The . + /// + /// The pin number in header P1. + /// A reference to the GPIO pin. + IGpioPin this[P1 pinNumber] { get; } + + /// + /// Gets the with the specified pin number. + /// + /// + /// The . + /// + /// The pin number in header P5. + /// A reference to the GPIO pin. + IGpioPin this[P5 pinNumber] { get; } + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/IGpioPin.cs b/Unosquare.RaspberryIO.Abstractions/IGpioPin.cs new file mode 100644 index 0000000..cf435bc --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/IGpioPin.cs @@ -0,0 +1,105 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + using System; + + /// + /// Interface for GPIO Pin on a RaspberryPi board. + /// + public interface IGpioPin + { + /// + /// Gets the . + /// + /// + /// The pin number. + /// + BcmPin BcmPin { get; } + + /// + /// Gets the BCM chip (hardware) pin number. + /// + /// + /// The pin number. + /// + int BcmPinNumber { get; } + + /// + /// Gets the physical (header) pin number. + /// + int PhysicalPinNumber { get; } + + /// + /// Gets the pin's header (physical board) location. + /// + GpioHeader Header { get; } + + /// + /// Gets or sets the pin operating mode. + /// + /// + /// The pin mode. + /// + GpioPinDriveMode PinMode { get; set; } + + /// + /// This sets or gets the pull-up or pull-down resistor mode on the pin, which should be set as an input. + /// Unlike the Arduino, the BCM2835 has both pull-up an down internal resistors. + /// The parameter pud should be; PUD_OFF, (no pull up/down), PUD_DOWN (pull to ground) or PUD_UP (pull to 3.3v) + /// The internal pull up/down resistors have a value of approximately 50KΩ on the Raspberry Pi. + /// + GpioPinResistorPullMode InputPullMode { get; set; } + + /// + /// Gets or sets a value indicating whether this is value. + /// + /// + /// true if value; otherwise, false. + /// + bool Value { get; set; } + + /// + /// Reads the digital value on the pin as a boolean value. + /// + /// The state of the pin. + bool Read(); + + /// + /// Writes the specified bit value. + /// This method performs a digital write. + /// + /// if set to true [value]. + void Write(bool value); + + /// + /// Writes the specified pin value. + /// This method performs a digital write. + /// + /// The value. + void Write(GpioPinValue value); + + /// + /// Wait for specific pin status. + /// + /// status to check. + /// timeout to reach status. + /// true/false. + bool WaitForValue(GpioPinValue status, int timeOutMillisecond); + + /// + /// Registers the interrupt callback on the pin. Pin mode has to be set to Input. + /// + /// The edge detection. + /// The callback function. This function is called whenever + /// the interrupt occurs. + void RegisterInterruptCallback(EdgeDetection edgeDetection, Action callback); + + /// + /// Registers the interrupt callback on the pin. Pin mode has to be set to Input. + /// + /// The edge detection. + /// The callback function. This function is called whenever the interrupt occurs. + /// The function is passed the GPIO, the current level, and the current tick + /// (The number of microseconds since boot). + void RegisterInterruptCallback(EdgeDetection edgeDetection, Action callback); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/II2CBus.cs b/Unosquare.RaspberryIO.Abstractions/II2CBus.cs new file mode 100644 index 0000000..ffb7e5b --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/II2CBus.cs @@ -0,0 +1,39 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + using System.Collections.ObjectModel; + + /// + /// Interfaces the I2c bus on the Raspberry Pi. + /// + public interface II2CBus + { + /// + /// Gets the registered devices as a read only collection. + /// + ReadOnlyCollection Devices { get; } + + /// + /// Gets the with the specified device identifier. + /// + /// + /// The . + /// + /// The device identifier. + /// A reference to an I2C device. + II2CDevice this[int deviceId] { get; } + + /// + /// Gets the device by identifier. + /// + /// The device identifier. + /// The device reference. + II2CDevice GetDeviceById(int deviceId); + + /// + /// Adds a device to the bus by its Id. If the device is already registered it simply returns the existing device. + /// + /// The device identifier. + /// The device reference. + II2CDevice AddDevice(int deviceId); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/II2CDevice.cs b/Unosquare.RaspberryIO.Abstractions/II2CDevice.cs new file mode 100644 index 0000000..fee9ed2 --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/II2CDevice.cs @@ -0,0 +1,77 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + /// + /// Interfaces a device on the I2C Bus. + /// + public interface II2CDevice + { + /// + /// Gets the device identifier. + /// + /// + /// The device identifier. + /// + int DeviceId { get; } + + /// + /// Gets the standard POSIX file descriptor. + /// + /// + /// The file descriptor. + /// + int FileDescriptor { get; } + + /// + /// Reads a byte from the specified file descriptor. + /// + /// The byte from device. + byte Read(); + + /// + /// Reads a buffer of the specified length, one byte at a time. + /// + /// The length. + /// The byte array from device. + byte[] Read(int length); + + /// + /// Writes a byte of data the specified file descriptor. + /// + /// The data. + void Write(byte data); + + /// + /// Writes a set of bytes to the specified file descriptor. + /// + /// The data. + void Write(byte[] data); + + /// + /// These write an 8 or 16-bit data value into the device register indicated. + /// + /// The register. + /// The data. + void WriteAddressByte(int address, byte data); + + /// + /// These write an 8 or 16-bit data value into the device register indicated. + /// + /// The register. + /// The data. + void WriteAddressWord(int address, ushort data); + + /// + /// These read an 8 or 16-bit value from the device register indicated. + /// + /// The register. + /// The address byte from device. + byte ReadAddressByte(int address); + + /// + /// These read an 8 or 16-bit value from the device register indicated. + /// + /// The register. + /// The address word from device. + ushort ReadAddressWord(int address); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/ISpiBus.cs b/Unosquare.RaspberryIO.Abstractions/ISpiBus.cs new file mode 100644 index 0000000..4b38696 --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/ISpiBus.cs @@ -0,0 +1,48 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + /// + /// Interfaces a SPI Bus containing the 2 SPI channels. + /// + public interface ISpiBus + { + /// + /// Gets the default frequency. + /// + /// + /// The default frequency. + /// + int DefaultFrequency { get; } + + /// + /// Gets or sets the channel 0 frequency in Hz. + /// + /// + /// The channel0 frequency. + /// + int Channel0Frequency { get; set; } + + /// + /// Gets or sets the channel 1 frequency in Hz. + /// + /// + /// The channel1 frequency. + /// + int Channel1Frequency { get; set; } + + /// + /// Gets the SPI bus on channel 0. + /// + /// + /// The channel0. + /// + ISpiChannel Channel0 { get; } + + /// + /// Gets the SPI bus on channel 1. + /// + /// + /// The channel0. + /// + ISpiChannel Channel1 { get; } + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/ISpiChannel.cs b/Unosquare.RaspberryIO.Abstractions/ISpiChannel.cs new file mode 100644 index 0000000..48900ee --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/ISpiChannel.cs @@ -0,0 +1,43 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + /// + /// Interfaces a SPI buses on the GPIO. + /// + public interface ISpiChannel + { + /// + /// Gets the standard initialization file descriptor. + /// anything negative means error. + /// + /// + /// The file descriptor. + /// + int FileDescriptor { get; } + + /// + /// Gets the channel. + /// + int Channel { get; } + + /// + /// Gets the frequency. + /// + int Frequency { get; } + + /// + /// Sends data and simultaneously receives the data in the return buffer. + /// + /// The buffer. + /// The read bytes from the ring-style bus. + byte[] SendReceive(byte[] buffer); + + /// + /// Writes the specified buffer the the underlying FileDescriptor. + /// Do not use this method if you expect data back. + /// This method is efficient if used in a fire-and-forget scenario + /// like sending data over to those long RGB LED strips. + /// + /// The buffer. + void Write(byte[] buffer); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/ISystemInfo.cs b/Unosquare.RaspberryIO.Abstractions/ISystemInfo.cs new file mode 100644 index 0000000..3a18a89 --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/ISystemInfo.cs @@ -0,0 +1,26 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + using System; + + /// + /// Interface for system info. + /// + public interface ISystemInfo + { + /// + /// Gets the board revision (1 or 2). + /// + /// + /// The board revision. + /// + BoardRevision BoardRevision { get; } + + /// + /// Gets the library version. + /// + /// + /// The library version. + /// + Version LibraryVersion { get; } + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/IThreading.cs b/Unosquare.RaspberryIO.Abstractions/IThreading.cs new file mode 100644 index 0000000..f929222 --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/IThreading.cs @@ -0,0 +1,30 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + using System; + + /// + /// Interface to represent threading methods using interop. + /// + public interface IThreading + { + /// + /// Starts a new thread of execution which runs concurrently with your main program. + /// + /// The thread routine. + void StartThread(Action worker); + + /// + /// Starts a new thread of execution which runs concurrently with your main program. + /// + /// The thread routine. + /// A pointer to the user data. + /// A pointer to the new thread. + UIntPtr StartThreadEx(Action worker, UIntPtr userData); + + /// + /// Stops the thread pointed at by handle. + /// + /// A thread pointer returned by . + void StopThreadEx(UIntPtr handle); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/ITiming.cs b/Unosquare.RaspberryIO.Abstractions/ITiming.cs new file mode 100644 index 0000000..0216602 --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/ITiming.cs @@ -0,0 +1,40 @@ +namespace Unosquare.RaspberryIO.Abstractions +{ + /// + /// Interface for timing methods using interop. + /// + public interface ITiming + { + /// + /// This returns a number representing the number of milliseconds since system boot. + /// + /// The milliseconds since system boot. + uint Milliseconds { get; } + + /// + /// This returns a number representing the number of microseconds since system boot. + /// + /// The microseconds since system boot. + uint Microseconds { get; } + + /// + /// This causes program execution to pause for at least how long milliseconds. + /// Due to the multi-tasking nature of Linux it could be longer. + /// Note that the maximum delay is an unsigned 32-bit integer or approximately 49 days. + /// + /// The number of milliseconds to sleep. + void SleepMilliseconds(uint millis); + + /// + /// This causes program execution to pause for at least how long microseconds. + /// Due to the multi-tasking nature of Linux it could be longer. + /// Note that the maximum delay is an unsigned 32-bit integer microseconds or approximately 71 minutes. + /// Delays under 100 microseconds are timed using a hard-coded loop continually polling the system time, + /// Delays over 100 microseconds are done using the system nanosleep() function – + /// You may need to consider the implications of very short delays on the overall performance of the system, + /// especially if using threads. + /// + /// The number of microseconds to sleep. + void SleepMicroseconds(uint micros); + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/Native/HardwareException.cs b/Unosquare.RaspberryIO.Abstractions/Native/HardwareException.cs new file mode 100644 index 0000000..e11190b --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/Native/HardwareException.cs @@ -0,0 +1,71 @@ +namespace Unosquare.RaspberryIO.Abstractions.Native +{ + using System; + using System.Runtime.InteropServices; + + /// + /// Represents a low-level exception, typically thrown when return codes from a + /// low-level operation is non-zero or in some cases when it is less than zero. + /// + /// + public class HardwareException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The error code. + /// The component. + public HardwareException(int errorCode, string component) + : base($"A hardware exception occurred. Error Code: {errorCode}") + { + ExtendedMessage = null; + + try + { + ExtendedMessage = Standard.Strerror(errorCode); + } + catch + { + // Ignore + } + + ErrorCode = errorCode; + Component = component; + } + + /// + /// Gets the error code. + /// + /// + /// The error code. + /// + public int ErrorCode { get; } + + /// + /// Gets the component. + /// + /// + /// The component. + /// + public string Component { get; } + + /// + /// Gets the extended message (could be null). + /// + /// + /// The extended message. + /// + public string? ExtendedMessage { get; } + + /// + /// Throws a new instance of a hardware error by retrieving the last error number (errno). + /// + /// Name of the class. + /// Name of the method. + /// When an error thrown by an API call occurs. + public static void Throw(string className, string methodName) => throw new HardwareException(Marshal.GetLastWin32Error(), $"{className}.{methodName}"); + + /// + public override string ToString() => $"{nameof(HardwareException)}{(string.IsNullOrWhiteSpace(Component) ? string.Empty : $" on {Component}")}: ({ErrorCode}) - {Message}"; + } +} diff --git a/Unosquare.RaspberryIO.Abstractions/Native/Standard.cs b/Unosquare.RaspberryIO.Abstractions/Native/Standard.cs new file mode 100644 index 0000000..b671af3 --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/Native/Standard.cs @@ -0,0 +1,45 @@ +namespace Unosquare.RaspberryIO.Abstractions.Native +{ + using System; + using System.Runtime.InteropServices; + using System.Text; + + /// + /// Provides standard 'libc' calls using platform-invoke. + /// + public static class Standard + { + internal const string LibCLibrary = "libc"; + + #region LibC Calls + + /// + /// Strerrors the specified error. + /// + /// The error. + /// The error string. + public static string Strerror(int error) + { + if (Type.GetType("Mono.Runtime") == null) return Marshal.PtrToStringAnsi(StrError(error)); + + try + { + var buffer = new StringBuilder(256); + var result = Strerror(error, buffer, (ulong)buffer.Capacity); + return (result != -1) ? buffer.ToString() : null; + } + catch (Exception) + { + return null; + } + } + + [DllImport(LibCLibrary, EntryPoint = "strerror", SetLastError = true)] + private static extern IntPtr StrError(int errnum); + + [DllImport("MonoPosixHelper", EntryPoint = "Mono_Posix_Syscall_strerror_r", SetLastError = true)] + private static extern int Strerror(int error, [Out] StringBuilder buffer, ulong length); + + #endregion + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO.Abstractions/Unosquare.RaspberryIO.Abstractions.csproj b/Unosquare.RaspberryIO.Abstractions/Unosquare.RaspberryIO.Abstractions.csproj new file mode 100644 index 0000000..fd82b8c --- /dev/null +++ b/Unosquare.RaspberryIO.Abstractions/Unosquare.RaspberryIO.Abstractions.csproj @@ -0,0 +1,21 @@ + + + + The Raspberry Pi's IO Functionality in an easy-to use API for Mono/.NET Core + +This library enables developers to use the various Raspberry Pi's hardware modules including the Camera to capture images and video, the GPIO pins, and both, the SPI and I2C buses. + Unosquare (c) 2016-2019 + Unosquare Raspberry Abstractions + netcoreapp3.0 + Unosquare.Raspberry.Abstractions + Unosquare.Raspberry.Abstractions + 0.4.0 + Unosquare + https://github.com/unosquare/raspberryio/raw/master/logos/raspberryio-logo-32.png + https://github.com/unosquare/raspberryio + https://raw.githubusercontent.com/unosquare/raspberryio/master/LICENSE + Raspberry Pi GPIO Camera SPI I2C Embedded IoT Mono C# .NET + 8.0 + + + diff --git a/Unosquare.RaspberryIO.sln b/Unosquare.RaspberryIO.sln new file mode 100644 index 0000000..9541060 --- /dev/null +++ b/Unosquare.RaspberryIO.sln @@ -0,0 +1,49 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29215.179 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unosquare.RaspberryIO", "Unosquare.RaspberryIO\Unosquare.RaspberryIO.csproj", "{71BF0E4E-9BCF-40C2-AE1C-7F17FE6C82CB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unosquare.RaspberryIO.Abstractions", "Unosquare.RaspberryIO.Abstractions\Unosquare.RaspberryIO.Abstractions.csproj", "{01319087-49C3-4BC6-BA85-80A52005A9E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swan", "Swan\Swan.csproj", "{7C0BBEFC-0789-4AE4-A2D1-8382435B67A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swan.Lite", "Swan.Lite\Swan.Lite.csproj", "{13FA428C-D554-45C3-8EC7-8B0342DE9A30}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Unosquare.WiringPi", "Unosquare.WiringPi\Unosquare.WiringPi.csproj", "{7575A3B0-7ABB-4BFF-8B6C-E34F58A7F28F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {71BF0E4E-9BCF-40C2-AE1C-7F17FE6C82CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71BF0E4E-9BCF-40C2-AE1C-7F17FE6C82CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71BF0E4E-9BCF-40C2-AE1C-7F17FE6C82CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71BF0E4E-9BCF-40C2-AE1C-7F17FE6C82CB}.Release|Any CPU.Build.0 = Release|Any CPU + {01319087-49C3-4BC6-BA85-80A52005A9E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01319087-49C3-4BC6-BA85-80A52005A9E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01319087-49C3-4BC6-BA85-80A52005A9E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01319087-49C3-4BC6-BA85-80A52005A9E8}.Release|Any CPU.Build.0 = Release|Any CPU + {7C0BBEFC-0789-4AE4-A2D1-8382435B67A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7C0BBEFC-0789-4AE4-A2D1-8382435B67A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7C0BBEFC-0789-4AE4-A2D1-8382435B67A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7C0BBEFC-0789-4AE4-A2D1-8382435B67A0}.Release|Any CPU.Build.0 = Release|Any CPU + {13FA428C-D554-45C3-8EC7-8B0342DE9A30}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13FA428C-D554-45C3-8EC7-8B0342DE9A30}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13FA428C-D554-45C3-8EC7-8B0342DE9A30}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13FA428C-D554-45C3-8EC7-8B0342DE9A30}.Release|Any CPU.Build.0 = Release|Any CPU + {7575A3B0-7ABB-4BFF-8B6C-E34F58A7F28F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7575A3B0-7ABB-4BFF-8B6C-E34F58A7F28F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7575A3B0-7ABB-4BFF-8B6C-E34F58A7F28F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7575A3B0-7ABB-4BFF-8B6C-E34F58A7F28F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {284182C7-6434-490D-BFC0-4DB344A38C2A} + EndGlobalSection +EndGlobal diff --git a/Unosquare.RaspberryIO/BluetoothErrorException.cs b/Unosquare.RaspberryIO/BluetoothErrorException.cs new file mode 100644 index 0000000..7f746a4 --- /dev/null +++ b/Unosquare.RaspberryIO/BluetoothErrorException.cs @@ -0,0 +1,20 @@ +namespace Unosquare.RaspberryIO +{ + using System; + + /// + /// + /// Occurs when an exception is thrown in the Bluetooth component. + /// + public class BluetoothErrorException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message. + public BluetoothErrorException(string message) + : base(message) + { + } + } +} diff --git a/Unosquare.RaspberryIO/Camera/CameraColor.cs b/Unosquare.RaspberryIO/Camera/CameraColor.cs new file mode 100644 index 0000000..4e06b34 --- /dev/null +++ b/Unosquare.RaspberryIO/Camera/CameraColor.cs @@ -0,0 +1,134 @@ +namespace Unosquare.RaspberryIO.Camera +{ + using System; + using System.Linq; + using Swan; + + /// + /// A simple RGB color class to represent colors in RGB and YUV colorspaces. + /// + public class CameraColor + { + /// + /// Initializes a new instance of the class. + /// + /// The red. + /// The green. + /// The blue. + public CameraColor(int r, int g, int b) + : this(r, g, b, string.Empty) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The red. + /// The green. + /// The blue. + /// The well-known color name. + public CameraColor(int r, int g, int b, string name) + { + RGB = new[] { Convert.ToByte(r.Clamp(0, 255)), Convert.ToByte(g.Clamp(0, 255)), Convert.ToByte(b.Clamp(0, 255)) }; + + var y = (R * .299000f) + (G * .587000f) + (B * .114000f); + var u = (R * -.168736f) + (G * -.331264f) + (B * .500000f) + 128f; + var v = (R * .500000f) + (G * -.418688f) + (B * -.081312f) + 128f; + + YUV = new[] { (byte)y.Clamp(0, 255), (byte)u.Clamp(0, 255), (byte)v.Clamp(0, 255) }; + Name = name; + } + + #region Static Definitions + + /// + /// Gets the predefined white color. + /// + public static CameraColor White => new CameraColor(255, 255, 255, nameof(White)); + + /// + /// Gets the predefined red color. + /// + public static CameraColor Red => new CameraColor(255, 0, 0, nameof(Red)); + + /// + /// Gets the predefined green color. + /// + public static CameraColor Green => new CameraColor(0, 255, 0, nameof(Green)); + + /// + /// Gets the predefined blue color. + /// + public static CameraColor Blue => new CameraColor(0, 0, 255, nameof(Blue)); + + /// + /// Gets the predefined black color. + /// + public static CameraColor Black => new CameraColor(0, 0, 0, nameof(Black)); + + #endregion + + /// + /// Gets the well-known color name. + /// + public string Name { get; } + + /// + /// Gets the red byte. + /// + public byte R => RGB[0]; + + /// + /// Gets the green byte. + /// + public byte G => RGB[1]; + + /// + /// Gets the blue byte. + /// + public byte B => RGB[2]; + + /// + /// Gets the RGB byte array (3 bytes). + /// + public byte[] RGB { get; } + + /// + /// Gets the YUV byte array (3 bytes). + /// + public byte[] YUV { get; } + + /// + /// Returns a hexadecimal representation of the RGB byte array. + /// Preceded by 0x and all in lowercase. + /// + /// if set to true [reverse]. + /// A string. + public string ToRgbHex(bool reverse) + { + var data = RGB.ToArray(); + if (reverse) Array.Reverse(data); + return ToHex(data); + } + + /// + /// Returns a hexadecimal representation of the YUV byte array. + /// Preceded by 0x and all in lowercase. + /// + /// if set to true [reverse]. + /// A string. + public string ToYuvHex(bool reverse) + { + var data = YUV.ToArray(); + if (reverse) Array.Reverse(data); + return ToHex(data); + } + + /// + /// Returns a hexadecimal representation of the data byte array. + /// + /// The data. + /// A string. + private static string ToHex(byte[] data) => $"0x{BitConverter.ToString(data).Replace("-", string.Empty).ToLowerInvariant()}"; + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO/Camera/CameraController.cs b/Unosquare.RaspberryIO/Camera/CameraController.cs new file mode 100644 index 0000000..191535d --- /dev/null +++ b/Unosquare.RaspberryIO/Camera/CameraController.cs @@ -0,0 +1,209 @@ +namespace Unosquare.RaspberryIO.Camera +{ + using Swan; + using System; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + + /// + /// The Raspberry Pi's camera controller wrapping raspistill and raspivid programs. + /// This class is a singleton. + /// + public class CameraController : SingletonBase + { + #region Private Declarations + + private static readonly ManualResetEventSlim OperationDone = new ManualResetEventSlim(true); + private static readonly object SyncRoot = new object(); + private static CancellationTokenSource _videoTokenSource = new CancellationTokenSource(); + private static Task? _videoStreamTask; + + #endregion + + #region Properties + + /// + /// Gets a value indicating whether the camera module is busy. + /// + /// + /// true if this instance is busy; otherwise, false. + /// + public bool IsBusy => OperationDone.IsSet == false; + + #endregion + + #region Image Capture Methods + + /// + /// Captures an image asynchronously. + /// + /// The settings. + /// The ct. + /// The image bytes. + /// Cannot use camera module because it is currently busy. + public async Task CaptureImageAsync(CameraStillSettings settings, CancellationToken ct = default) + { + if (Instance.IsBusy) + throw new InvalidOperationException("Cannot use camera module because it is currently busy."); + + if (settings.CaptureTimeoutMilliseconds <= 0) + throw new ArgumentException($"{nameof(settings.CaptureTimeoutMilliseconds)} needs to be greater than 0"); + + try + { + OperationDone.Reset(); + + using var output = new MemoryStream(); + var exitCode = await ProcessRunner.RunProcessAsync( + settings.CommandName, + settings.CreateProcessArguments(), + (data, proc) => output.Write(data, 0, data.Length), + null, + true, + ct).ConfigureAwait(false); + + return exitCode != 0 ? Array.Empty() : output.ToArray(); + } + finally + { + OperationDone.Set(); + } + } + + /// + /// Captures an image. + /// + /// The settings. + /// The image bytes. + public byte[] CaptureImage(CameraStillSettings settings) => CaptureImageAsync(settings).GetAwaiter().GetResult(); + + /// + /// Captures a JPEG encoded image asynchronously at 90% quality. + /// + /// The width. + /// The height. + /// The ct. + /// The image bytes. + public Task CaptureImageJpegAsync(int width, int height, CancellationToken ct = default) + { + var settings = new CameraStillSettings + { + CaptureWidth = width, + CaptureHeight = height, + CaptureJpegQuality = 90, + CaptureTimeoutMilliseconds = 300, + }; + + return CaptureImageAsync(settings, ct); + } + + /// + /// Captures a JPEG encoded image at 90% quality. + /// + /// The width. + /// The height. + /// The image bytes. + public byte[] CaptureImageJpeg(int width, int height) => CaptureImageJpegAsync(width, height).GetAwaiter().GetResult(); + + #endregion + + #region Video Capture Methods + + /// + /// Opens the video stream with a timeout of 0 (running indefinitely) at 1080p resolution, variable bitrate and 25 FPS. + /// No preview is shown. + /// + /// The on data callback. + /// The on exit callback. + public void OpenVideoStream(Action onDataCallback, Action? onExitCallback = null) + { + var settings = new CameraVideoSettings + { + CaptureTimeoutMilliseconds = 0, + CaptureDisplayPreview = false, + CaptureWidth = 1920, + CaptureHeight = 1080, + }; + + OpenVideoStream(settings, onDataCallback, onExitCallback); + } + + /// + /// Opens the video stream with the supplied settings. Capture Timeout Milliseconds has to be 0 or greater. + /// + /// The settings. + /// The on data callback. + /// The on exit callback. + /// Cannot use camera module because it is currently busy. + /// CaptureTimeoutMilliseconds. + public void OpenVideoStream(CameraVideoSettings settings, Action onDataCallback, Action? onExitCallback = null) + { + if (Instance.IsBusy) + throw new InvalidOperationException("Cannot use camera module because it is currently busy."); + + if (settings.CaptureTimeoutMilliseconds < 0) + throw new ArgumentException($"{nameof(settings.CaptureTimeoutMilliseconds)} needs to be greater than or equal to 0"); + + try + { + OperationDone.Reset(); + _videoStreamTask = Task.Factory.StartNew(() => VideoWorkerDoWork(settings, onDataCallback, onExitCallback), _videoTokenSource.Token); + } + catch + { + OperationDone.Set(); + throw; + } + } + + /// + /// Closes the video stream of a video stream is open. + /// + public void CloseVideoStream() + { + lock (SyncRoot) + { + if (IsBusy == false) + return; + } + + if (_videoTokenSource.IsCancellationRequested == false) + { + _videoTokenSource.Cancel(); + _videoStreamTask?.Wait(); + } + + _videoTokenSource = new CancellationTokenSource(); + } + + private static async Task VideoWorkerDoWork( + CameraVideoSettings settings, + Action onDataCallback, + Action onExitCallback) + { + try + { + await ProcessRunner.RunProcessAsync( + settings.CommandName, + settings.CreateProcessArguments(), + (data, proc) => onDataCallback?.Invoke(data), + null, + true, + _videoTokenSource.Token).ConfigureAwait(false); + + onExitCallback?.Invoke(); + } + catch + { + // swallow + } + finally + { + Instance.CloseVideoStream(); + OperationDone.Set(); + } + } + #endregion + } +} diff --git a/Unosquare.RaspberryIO/Camera/CameraRect.cs b/Unosquare.RaspberryIO/Camera/CameraRect.cs new file mode 100644 index 0000000..505cdd8 --- /dev/null +++ b/Unosquare.RaspberryIO/Camera/CameraRect.cs @@ -0,0 +1,82 @@ +namespace Unosquare.RaspberryIO.Camera +{ + using System.Globalization; + using Swan; + + /// + /// Defines the Raspberry Pi camera's sensor ROI (Region of Interest). + /// + public struct CameraRect + { + /// + /// The default ROI which is the entire area. + /// + public static readonly CameraRect Default = new CameraRect { X = 0M, Y = 0M, W = 1.0M, H = 1.0M }; + + /// + /// Gets or sets the x in relative coordinates. (0.0 to 1.0). + /// + /// + /// The x. + /// + public decimal X { get; set; } + + /// + /// Gets or sets the y location in relative coordinates. (0.0 to 1.0). + /// + /// + /// The y. + /// + public decimal Y { get; set; } + + /// + /// Gets or sets the width in relative coordinates. (0.0 to 1.0). + /// + /// + /// The w. + /// + public decimal W { get; set; } + + /// + /// Gets or sets the height in relative coordinates. (0.0 to 1.0). + /// + /// + /// The h. + /// + public decimal H { get; set; } + + /// + /// Gets a value indicating whether this instance is equal to the default (The entire area). + /// + /// + /// true if this instance is default; otherwise, false. + /// + public bool IsDefault + { + get + { + Clamp(); + return X == Default.X && Y == Default.Y && W == Default.W && H == Default.H; + } + } + + /// + /// Clamps the members of this ROI to their minimum and maximum values. + /// + public void Clamp() + { + X = X.Clamp(0M, 1M); + Y = Y.Clamp(0M, 1M); + W = W.Clamp(0M, 1M - X); + H = H.Clamp(0M, 1M - Y); + } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => $"{X.ToString(CultureInfo.InvariantCulture)},{Y.ToString(CultureInfo.InvariantCulture)},{W.ToString(CultureInfo.InvariantCulture)},{H.ToString(CultureInfo.InvariantCulture)}"; + } +} diff --git a/Unosquare.RaspberryIO/Camera/CameraSettingsBase.cs b/Unosquare.RaspberryIO/Camera/CameraSettingsBase.cs new file mode 100644 index 0000000..0a5c456 --- /dev/null +++ b/Unosquare.RaspberryIO/Camera/CameraSettingsBase.cs @@ -0,0 +1,340 @@ +namespace Unosquare.RaspberryIO.Camera +{ + using System.Globalization; + using System.Text; + using Swan; + + /// + /// A base class to implement raspistill and raspivid wrappers + /// Full documentation available at + /// https://www.raspberrypi.org/documentation/raspbian/applications/camera.md. + /// + public abstract class CameraSettingsBase + { + /// + /// The Invariant Culture shorthand. + /// + protected static readonly CultureInfo Ci = CultureInfo.InvariantCulture; + + #region Capture Settings + + /// + /// Gets or sets the timeout milliseconds. + /// Default value is 5000 + /// Recommended value is at least 300 in order to let the light collectors open. + /// + public int CaptureTimeoutMilliseconds { get; set; } = 5000; + + /// + /// Gets or sets a value indicating whether or not to show a preview window on the screen. + /// + public bool CaptureDisplayPreview { get; set; } = false; + + /// + /// Gets or sets a value indicating whether a preview window is shown in full screen mode if enabled. + /// + public bool CaptureDisplayPreviewInFullScreen { get; set; } = true; + + /// + /// Gets or sets a value indicating whether video stabilization should be enabled. + /// + public bool CaptureVideoStabilizationEnabled { get; set; } = false; + + /// + /// Gets or sets the display preview opacity only if the display preview property is enabled. + /// + public byte CaptureDisplayPreviewOpacity { get; set; } = 255; + + /// + /// Gets or sets the capture sensor region of interest in relative coordinates. + /// + public CameraRect CaptureSensorRoi { get; set; } = CameraRect.Default; + + /// + /// Gets or sets the capture shutter speed in microseconds. + /// Default -1, Range 0 to 6000000 (equivalent to 6 seconds). + /// + public int CaptureShutterSpeedMicroseconds { get; set; } = -1; + + /// + /// Gets or sets the exposure mode. + /// + public CameraExposureMode CaptureExposure { get; set; } = CameraExposureMode.Auto; + + /// + /// Gets or sets the picture EV compensation. Default is 0, Range is -10 to 10 + /// Camera exposure compensation is commonly stated in terms of EV units; + /// 1 EV is equal to one exposure step (or stop), corresponding to a doubling of exposure. + /// Exposure can be adjusted by changing either the lens f-number or the exposure time; + /// which one is changed usually depends on the camera's exposure mode. + /// + public int CaptureExposureCompensation { get; set; } = 0; + + /// + /// Gets or sets the capture metering mode. + /// + public CameraMeteringMode CaptureMeteringMode { get; set; } = CameraMeteringMode.Average; + + /// + /// Gets or sets the automatic white balance mode. By default it is set to Auto. + /// + public CameraWhiteBalanceMode CaptureWhiteBalanceControl { get; set; } = CameraWhiteBalanceMode.Auto; + + /// + /// Gets or sets the capture white balance gain on the blue channel. Example: 1.25 + /// Only takes effect if White balance control is set to off. + /// Default is 0. + /// + public decimal CaptureWhiteBalanceGainBlue { get; set; } = 0M; + + /// + /// Gets or sets the capture white balance gain on the red channel. Example: 1.75 + /// Only takes effect if White balance control is set to off. + /// Default is 0. + /// + public decimal CaptureWhiteBalanceGainRed { get; set; } = 0M; + + /// + /// Gets or sets the dynamic range compensation. + /// DRC changes the images by increasing the range of dark areas, and decreasing the brighter areas. This can improve the image in low light areas. + /// + public CameraDynamicRangeCompensation CaptureDynamicRangeCompensation { get; set; } = + CameraDynamicRangeCompensation.Off; + + #endregion + + #region Image Properties + + /// + /// Gets or sets the width of the picture to take. + /// Less than or equal to 0 in either width or height means maximum resolution available. + /// + public int CaptureWidth { get; set; } = 640; + + /// + /// Gets or sets the height of the picture to take. + /// Less than or equal to 0 in either width or height means maximum resolution available. + /// + public int CaptureHeight { get; set; } = 480; + + /// + /// Gets or sets the picture sharpness. Default is 0, Range form -100 to 100. + /// + public int ImageSharpness { get; set; } = 0; + + /// + /// Gets or sets the picture contrast. Default is 0, Range form -100 to 100. + /// + public int ImageContrast { get; set; } = 0; + + /// + /// Gets or sets the picture brightness. Default is 50, Range form 0 to 100. + /// + public int ImageBrightness { get; set; } = 50; // from 0 to 100 + + /// + /// Gets or sets the picture saturation. Default is 0, Range form -100 to 100. + /// + public int ImageSaturation { get; set; } = 0; + + /// + /// Gets or sets the picture ISO. Default is -1 Range is 100 to 800 + /// The higher the value, the more light the sensor absorbs. + /// + public int ImageIso { get; set; } = -1; + + /// + /// Gets or sets the image capture effect to be applied. + /// + public CameraImageEffect ImageEffect { get; set; } = CameraImageEffect.None; + + /// + /// Gets or sets the color effect U coordinates. + /// Default is -1, Range is 0 to 255 + /// 128:128 should be effectively a monochrome image. + /// + public int ImageColorEffectU { get; set; } = -1; // 0 to 255 + + /// + /// Gets or sets the color effect V coordinates. + /// Default is -1, Range is 0 to 255 + /// 128:128 should be effectively a monochrome image. + /// + public int ImageColorEffectV { get; set; } = -1; // 0 to 255 + + /// + /// Gets or sets the image rotation. Default is no rotation. + /// + public CameraImageRotation ImageRotation { get; set; } = CameraImageRotation.None; + + /// + /// Gets or sets a value indicating whether the image should be flipped horizontally. + /// + public bool ImageFlipHorizontally { get; set; } + + /// + /// Gets or sets a value indicating whether the image should be flipped vertically. + /// + public bool ImageFlipVertically { get; set; } + + /// + /// Gets or sets the image annotations using a bitmask (or flags) notation. + /// Apply a bitwise OR to the enumeration to include multiple annotations. + /// + public CameraAnnotation ImageAnnotations { get; set; } = CameraAnnotation.None; + + /// + /// Gets or sets the image annotations text. + /// Text may include date/time placeholders by using the '%' character, as used by strftime. + /// Example: ABC %Y-%m-%d %X will output ABC 2015-10-28 20:09:33. + /// + public string ImageAnnotationsText { get; set; } = string.Empty; + + /// + /// Gets or sets the font size of the text annotations + /// Default is -1, range is 6 to 160. + /// + public int ImageAnnotationFontSize { get; set; } = -1; + + /// + /// Gets or sets the color of the text annotations. + /// + /// + /// The color of the image annotation font. + /// + public CameraColor? ImageAnnotationFontColor { get; set; } = null; + + /// + /// Gets or sets the background color for text annotations. + /// + /// + /// The image annotation background. + /// + public CameraColor? ImageAnnotationBackground { get; set; } = null; + + #endregion + + #region Interface + + /// + /// Gets the command file executable. + /// + public abstract string CommandName { get; } + + /// + /// Creates the process arguments. + /// + /// The string that represents the process arguments. + public virtual string CreateProcessArguments() + { + var sb = new StringBuilder(); + sb.Append("-o -"); // output to standard output as opposed to a file. + sb.Append($" -t {(CaptureTimeoutMilliseconds < 0 ? "0" : CaptureTimeoutMilliseconds.ToString(Ci))}"); + + // Basic Width and height + if (CaptureWidth > 0 && CaptureHeight > 0) + { + sb.Append($" -w {CaptureWidth.ToString(Ci)}"); + sb.Append($" -h {CaptureHeight.ToString(Ci)}"); + } + + // Display Preview + if (CaptureDisplayPreview) + { + if (CaptureDisplayPreviewInFullScreen) + sb.Append(" -f"); + + if (CaptureDisplayPreviewOpacity != byte.MaxValue) + sb.Append($" -op {CaptureDisplayPreviewOpacity.ToString(Ci)}"); + } + else + { + sb.Append(" -n"); // no preview + } + + // Picture Settings + if (ImageSharpness != 0) + sb.Append($" -sh {ImageSharpness.Clamp(-100, 100).ToString(Ci)}"); + + if (ImageContrast != 0) + sb.Append($" -co {ImageContrast.Clamp(-100, 100).ToString(Ci)}"); + + if (ImageBrightness != 50) + sb.Append($" -br {ImageBrightness.Clamp(0, 100).ToString(Ci)}"); + + if (ImageSaturation != 0) + sb.Append($" -sa {ImageSaturation.Clamp(-100, 100).ToString(Ci)}"); + + if (ImageIso >= 100) + sb.Append($" -ISO {ImageIso.Clamp(100, 800).ToString(Ci)}"); + + if (CaptureVideoStabilizationEnabled) + sb.Append(" -vs"); + + if (CaptureExposureCompensation != 0) + sb.Append($" -ev {CaptureExposureCompensation.Clamp(-10, 10).ToString(Ci)}"); + + if (CaptureExposure != CameraExposureMode.Auto) + sb.Append($" -ex {CaptureExposure.ToString().ToLowerInvariant()}"); + + if (CaptureWhiteBalanceControl != CameraWhiteBalanceMode.Auto) + sb.Append($" -awb {CaptureWhiteBalanceControl.ToString().ToLowerInvariant()}"); + + if (ImageEffect != CameraImageEffect.None) + sb.Append($" -ifx {ImageEffect.ToString().ToLowerInvariant()}"); + + if (ImageColorEffectU >= 0 && ImageColorEffectV >= 0) + { + sb.Append( + $" -cfx {ImageColorEffectU.Clamp(0, 255).ToString(Ci)}:{ImageColorEffectV.Clamp(0, 255).ToString(Ci)}"); + } + + if (CaptureMeteringMode != CameraMeteringMode.Average) + sb.Append($" -mm {CaptureMeteringMode.ToString().ToLowerInvariant()}"); + + if (ImageRotation != CameraImageRotation.None) + sb.Append($" -rot {((int)ImageRotation).ToString(Ci)}"); + + if (ImageFlipHorizontally) + sb.Append(" -hf"); + + if (ImageFlipVertically) + sb.Append(" -vf"); + + if (CaptureSensorRoi.IsDefault == false) + sb.Append($" -roi {CaptureSensorRoi}"); + + if (CaptureShutterSpeedMicroseconds > 0) + sb.Append($" -ss {CaptureShutterSpeedMicroseconds.Clamp(0, 6000000).ToString(Ci)}"); + + if (CaptureDynamicRangeCompensation != CameraDynamicRangeCompensation.Off) + sb.Append($" -drc {CaptureDynamicRangeCompensation.ToString().ToLowerInvariant()}"); + + if (CaptureWhiteBalanceControl == CameraWhiteBalanceMode.Off && + (CaptureWhiteBalanceGainBlue != 0M || CaptureWhiteBalanceGainRed != 0M)) + sb.Append($" -awbg {CaptureWhiteBalanceGainBlue.ToString(Ci)},{CaptureWhiteBalanceGainRed.ToString(Ci)}"); + + if (ImageAnnotationFontSize > 0) + { + sb.Append($" -ae {ImageAnnotationFontSize.Clamp(6, 160).ToString(Ci)}"); + sb.Append($",{(ImageAnnotationFontColor == null ? "0xff" : ImageAnnotationFontColor.ToYuvHex(true))}"); + + if (ImageAnnotationBackground != null) + { + ImageAnnotations |= CameraAnnotation.SolidBackground; + sb.Append($",{ImageAnnotationBackground.ToYuvHex(true)}"); + } + } + + if (ImageAnnotations != CameraAnnotation.None) + sb.Append($" -a {((int)ImageAnnotations).ToString(Ci)}"); + + if (string.IsNullOrWhiteSpace(ImageAnnotationsText) == false) + sb.Append($" -a \"{ImageAnnotationsText.Replace("\"", "'")}\""); + + return sb.ToString(); + } + + #endregion + } +} diff --git a/Unosquare.RaspberryIO/Camera/CameraStillSettings.cs b/Unosquare.RaspberryIO/Camera/CameraStillSettings.cs new file mode 100644 index 0000000..f384831 --- /dev/null +++ b/Unosquare.RaspberryIO/Camera/CameraStillSettings.cs @@ -0,0 +1,120 @@ +namespace Unosquare.RaspberryIO.Camera +{ + using System; + using System.Collections.Generic; + using System.Text; + using Swan; + + /// + /// Defines a wrapper for the raspistill program and its settings (command-line arguments). + /// + /// + public class CameraStillSettings : CameraSettingsBase + { + private int _rotate; + + /// + public override string CommandName => "raspistill"; + + /// + /// Gets or sets a value indicating whether the preview window (if enabled) uses native capture resolution + /// This may slow down preview FPS. + /// + public bool CaptureDisplayPreviewAtResolution { get; set; } = false; + + /// + /// Gets or sets the encoding format the hardware will use for the output. + /// + public CameraImageEncodingFormat CaptureEncoding { get; set; } = CameraImageEncodingFormat.Jpg; + + /// + /// Gets or sets the quality for JPEG only encoding mode. + /// Value ranges from 0 to 100. + /// + public int CaptureJpegQuality { get; set; } = 90; + + /// + /// Gets or sets a value indicating whether the JPEG encoder should add raw bayer metadata. + /// + public bool CaptureJpegIncludeRawBayerMetadata { get; set; } = false; + + /// + /// JPEG EXIF data + /// Keys and values must be already properly escaped. Otherwise the command will fail. + /// + public Dictionary CaptureJpegExtendedInfo { get; } = new Dictionary(); + + /// + /// Gets or sets a value indicating whether [horizontal flip]. + /// + /// + /// true if [horizontal flip]; otherwise, false. + /// + public bool HorizontalFlip { get; set; } = false; + + /// + /// Gets or sets a value indicating whether [vertical flip]. + /// + /// + /// true if [vertical flip]; otherwise, false. + /// + public bool VerticalFlip { get; set; } = false; + + /// + /// Gets or sets the rotation. + /// + /// Valid range 0-359. + public int Rotation + { + get => _rotate; + set + { + if (value < 0 || value > 359) + { + throw new ArgumentOutOfRangeException(nameof(value), "Valid range 0-359"); + } + + _rotate = value; + } + } + + /// + public override string CreateProcessArguments() + { + var sb = new StringBuilder(base.CreateProcessArguments()); + sb.Append($" -e {CaptureEncoding.ToString().ToLowerInvariant()}"); + + // JPEG Encoder specific arguments + if (CaptureEncoding == CameraImageEncodingFormat.Jpg) + { + sb.Append($" -q {CaptureJpegQuality.Clamp(0, 100).ToString(Ci)}"); + + if (CaptureJpegIncludeRawBayerMetadata) + sb.Append(" -r"); + + // JPEG EXIF data + if (CaptureJpegExtendedInfo.Count > 0) + { + foreach (var kvp in CaptureJpegExtendedInfo) + { + if (string.IsNullOrWhiteSpace(kvp.Key) || string.IsNullOrWhiteSpace(kvp.Value)) + continue; + + sb.Append($" -x \"{kvp.Key.Replace("\"", "'")}={kvp.Value.Replace("\"", "'")}\""); + } + } + } + + // Display preview settings + if (CaptureDisplayPreview && CaptureDisplayPreviewAtResolution) sb.Append(" -fp"); + + if (Rotation != 0) sb.Append($" -rot {Rotation}"); + + if (HorizontalFlip) sb.Append(" -hf"); + + if (VerticalFlip) sb.Append(" -vf"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO/Camera/CameraVideoSettings.cs b/Unosquare.RaspberryIO/Camera/CameraVideoSettings.cs new file mode 100644 index 0000000..c80c3ee --- /dev/null +++ b/Unosquare.RaspberryIO/Camera/CameraVideoSettings.cs @@ -0,0 +1,104 @@ +namespace Unosquare.RaspberryIO.Camera +{ + using System.Text; + + /// + /// Represents the raspivid camera settings for video capture functionality. + /// + /// + public class CameraVideoSettings : CameraSettingsBase + { + private int _length; + + /// + public override string CommandName => "raspivid"; + + /// + /// Use bits per second, so 10Mbits/s would be -b 10000000. For H264, 1080p30 a high quality bitrate would be 15Mbits/s or more. + /// Maximum bitrate is 25Mbits/s (-b 25000000), but much over 17Mbits/s won't show noticeable improvement at 1080p30. + /// Default -1. + /// + public int CaptureBitrate { get; set; } = -1; + + /// + /// Gets or sets the framerate. + /// Default 25, range 2 to 30. + /// + public int CaptureFramerate { get; set; } = 25; + + /// + /// Sets the intra refresh period (GoP) rate for the recorded video. H264 video uses a complete frame (I-frame) every intra + /// refresh period, from which subsequent frames are based. This option specifies the number of frames between each I-frame. + /// Larger numbers here will reduce the size of the resulting video, and smaller numbers make the stream less error-prone. + /// + public int CaptureKeyframeRate { get; set; } = 25; + + /// + /// Sets the initial quantisation parameter for the stream. Varies from approximately 10 to 40, and will greatly affect + /// the quality of the recording. Higher values reduce quality and decrease file size. Combine this setting with a + /// bitrate of 0 to set a completely variable bitrate. + /// + public int CaptureQuantisation { get; set; } = 23; + + /// + /// Gets or sets the profile. + /// Sets the H264 profile to be used for the encoding. + /// Default is Main mode. + /// + public CameraH264Profile CaptureProfile { get; set; } = CameraH264Profile.Main; + + /// + /// Forces the stream to include PPS and SPS headers on every I-frame. Needed for certain streaming cases + /// e.g. Apple HLS. These headers are small, so don't greatly increase the file size. + /// + /// + /// true if [interleave headers]; otherwise, false. + /// + public bool CaptureInterleaveHeaders { get; set; } = true; + + /// + /// Toggle fullscreen mode for video preview. + /// + public bool Fullscreen { get; set; } = false; + + /// + /// Specifies the path to save video files. + /// + public string VideoFileName { get; set; } + + /// + /// Video stream length in seconds. + /// + public int LengthInSeconds + { + get => _length; + set => _length = value * 1000; + } + + /// + /// Switch on an option to display the preview after compression. This will show any compression artefacts in the preview window. In normal operation, + /// the preview will show the camera output prior to being compressed. This option is not guaranteed to work in future releases. + /// + /// + /// true if [capture display preview encoded]; otherwise, false. + /// + public bool CaptureDisplayPreviewEncoded { get; set; } = false; + + /// + public override string CreateProcessArguments() + { + var sb = new StringBuilder(base.CreateProcessArguments()); + + if (Fullscreen) + sb.Append(" -f"); + + if (LengthInSeconds != 0) + sb.Append($" -t {LengthInSeconds}"); + + if (!string.IsNullOrEmpty(VideoFileName)) + sb.Append($" -o {VideoFileName}"); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO/Camera/Enums.cs b/Unosquare.RaspberryIO/Camera/Enums.cs new file mode 100644 index 0000000..f77ca6e --- /dev/null +++ b/Unosquare.RaspberryIO/Camera/Enums.cs @@ -0,0 +1,423 @@ +namespace Unosquare.RaspberryIO.Camera +{ + using System; + + /// + /// Defines the available encoding formats for the Raspberry Pi camera module. + /// + public enum CameraImageEncodingFormat + { + /// + /// The JPG + /// + Jpg, + + /// + /// The BMP + /// + Bmp, + + /// + /// The GIF + /// + Gif, + + /// + /// The PNG + /// + Png + } + + /// + /// Defines the different exposure modes for the Raspberry Pi's camera module. + /// + public enum CameraExposureMode + { + /// + /// The automatic + /// + Auto, + + /// + /// The night + /// + Night, + + /// + /// The night preview + /// + NightPreview, + + /// + /// The backlight + /// + Backlight, + + /// + /// The spotlight + /// + Spotlight, + + /// + /// The sports + /// + Sports, + + /// + /// The snow + /// + Snow, + + /// + /// The beach + /// + Beach, + + /// + /// The very long + /// + VeryLong, + + /// + /// The fixed FPS + /// + FixedFps, + + /// + /// The anti shake + /// + AntiShake, + + /// + /// The fireworks + /// + Fireworks + } + + /// + /// Defines the different AWB (Auto White Balance) modes for the Raspberry Pi's camera module. + /// + public enum CameraWhiteBalanceMode + { + /// + /// No white balance + /// + Off, + + /// + /// The automatic + /// + Auto, + + /// + /// The sun + /// + Sun, + + /// + /// The cloud + /// + Cloud, + + /// + /// The shade + /// + Shade, + + /// + /// The tungsten + /// + Tungsten, + + /// + /// The fluorescent + /// + Fluorescent, + + /// + /// The incandescent + /// + Incandescent, + + /// + /// The flash + /// + Flash, + + /// + /// The horizon + /// + Horizon + } + + /// + /// Defines the available image effects for the Raspberry Pi's camera module. + /// + public enum CameraImageEffect + { + /// + /// No effect + /// + None, + + /// + /// The negative + /// + Negative, + + /// + /// The solarise + /// + Solarise, + + /// + /// The whiteboard + /// + Whiteboard, + + /// + /// The blackboard + /// + Blackboard, + + /// + /// The sketch + /// + Sketch, + + /// + /// The denoise + /// + Denoise, + + /// + /// The emboss + /// + Emboss, + + /// + /// The oil paint + /// + OilPaint, + + /// + /// The hatch + /// + Hatch, + + /// + /// Graphite Pen + /// + GPen, + + /// + /// The pastel + /// + Pastel, + + /// + /// The water colour + /// + WaterColour, + + /// + /// The film + /// + Film, + + /// + /// The blur + /// + Blur, + + /// + /// The saturation + /// + Saturation, + + /// + /// The solour swap + /// + SolourSwap, + + /// + /// The washed out + /// + WashedOut, + + /// + /// The colour point + /// + ColourPoint, + + /// + /// The colour balance + /// + ColourBalance, + + /// + /// The cartoon + /// + Cartoon + } + + /// + /// Defines the different metering modes for the Raspberry Pi's camera module. + /// + public enum CameraMeteringMode + { + /// + /// The average + /// + Average, + + /// + /// The spot + /// + Spot, + + /// + /// The backlit + /// + Backlit, + + /// + /// The matrix + /// + Matrix + } + + /// + /// Defines the different image rotation modes for the Raspberry Pi's camera module. + /// + public enum CameraImageRotation + { + /// + /// No rerotation + /// + None = 0, + + /// + /// 90 Degrees + /// + Degrees90 = 90, + + /// + /// 180 Degrees + /// + Degrees180 = 180, + + /// + /// 270 degrees + /// + Degrees270 = 270 + } + + /// + /// Defines the different DRC (Dynamic Range Compensation) modes for the Raspberry Pi's camera module + /// Helpful for low light photos. + /// + public enum CameraDynamicRangeCompensation + { + /// + /// The off setting + /// + Off, + + /// + /// The low + /// + Low, + + /// + /// The medium + /// + Medium, + + /// + /// The high + /// + High + } + + /// + /// Defines the bit-wise mask flags for the available annotation elements for the Raspberry Pi's camera module. + /// + [Flags] + public enum CameraAnnotation + { + /// + /// The none + /// + None = 0, + + /// + /// The time + /// + Time = 4, + + /// + /// The date + /// + Date = 8, + + /// + /// The shutter settings + /// + ShutterSettings = 16, + + /// + /// The caf settings + /// + CafSettings = 32, + + /// + /// The gain settings + /// + GainSettings = 64, + + /// + /// The lens settings + /// + LensSettings = 128, + + /// + /// The motion settings + /// + MotionSettings = 256, + + /// + /// The frame number + /// + FrameNumber = 512, + + /// + /// The solid background + /// + SolidBackground = 1024 + } + + /// + /// Defines the different H.264 encoding profiles to be used when capturing video. + /// + public enum CameraH264Profile + { + /// + /// BP: Primarily for lower-cost applications with limited computing resources, + /// this profile is used widely in videoconferencing and mobile applications. + /// + Baseline, + + /// + /// MP: Originally intended as the mainstream consumer profile for broadcast + /// and storage applications, the importance of this profile faded when the High profile was developed for those applications. + /// + Main, + + /// + /// HiP: The primary profile for broadcast and disc storage applications, particularly + /// for high-definition television applications (this is the profile adopted into HD DVD and Blu-ray Disc, for example). + /// + High + } +} diff --git a/Unosquare.RaspberryIO/Computer/AudioSettings.cs b/Unosquare.RaspberryIO/Computer/AudioSettings.cs new file mode 100644 index 0000000..206cd88 --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/AudioSettings.cs @@ -0,0 +1,113 @@ +namespace Unosquare.RaspberryIO.Computer +{ + using Swan; + using System; + using System.Linq; + using System.Threading.Tasks; + + /// + /// Settings for audio device. + /// + public class AudioSettings : SingletonBase + { + private const string DefaultControlName = "PCM"; + private const int DefaultCardNumber = 0; + + private readonly string[] _errorMess = { "Invalid", "Unable" }; + + /// + /// Gets the current audio state. + /// + /// The card number. + /// Name of the control. + /// An object. + /// Invalid command, card number or control name. + public async Task GetState(int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) + { + var volumeInfo = await ProcessRunner.GetProcessOutputAsync("amixer", $"-c {cardNumber} get {controlName}").ConfigureAwait(false); + + var lines = volumeInfo.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); + + if (!lines.Any()) + throw new InvalidOperationException("Invalid command."); + + if (_errorMess.Any(x => lines[0].Contains(x))) + throw new InvalidOperationException(lines[0]); + + var volumeLine = lines + .FirstOrDefault(x => x.Trim() + .StartsWith("Mono:", StringComparison.OrdinalIgnoreCase)); + + if (volumeLine == null) + throw new InvalidOperationException("Unexpected output from 'amixer'."); + + var sections = volumeLine.Split(new[] { ' ' }, + StringSplitOptions.RemoveEmptyEntries); + + var level = int.Parse(sections[3].Substring(1, sections[3].Length - 3), + System.Globalization.NumberFormatInfo.InvariantInfo); + + var decibels = float.Parse(sections[4].Substring(1, sections[4].Length - 4), + System.Globalization.NumberFormatInfo.InvariantInfo); + + var isMute = sections[5].Equals("[off]", + StringComparison.CurrentCultureIgnoreCase); + + return new AudioState(cardNumber, controlName, level, decibels, isMute); + } + + /// + /// Sets the volume percentage. + /// + /// The percentage level. + /// The card number. + /// Name of the control. + /// A representing the asynchronous operation. + /// Invalid card number or control name. + public Task SetVolumePercentage(int level, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) => + SetAudioCommand($"{level}%", cardNumber, controlName); + + /// + /// Sets the volume by decibels. + /// + /// The decibels. + /// The card number. + /// Name of the control. + /// A representing the asynchronous operation. + /// Invalid card number or control name. + public Task SetVolumeByDecibels(float decibels, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) => + SetAudioCommand($"{decibels}dB", cardNumber, controlName); + + /// + /// Increments the volume by decibels. + /// + /// The decibels to increment or decrement. + /// The card number. + /// Name of the control. + /// A representing the asynchronous operation. + /// Invalid card number or control name. + public Task IncrementVolume(float decibels, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) => + SetAudioCommand($"{decibels}dB{(decibels < 0 ? "-" : "+")}", cardNumber, controlName); + + /// + /// Toggles the mute state. + /// + /// if set to true, mutes the audio. + /// The card number. + /// Name of the control. + /// A representing the asynchronous operation. + /// Invalid card number or control name. + public Task ToggleMute(bool mute, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) => + SetAudioCommand(mute ? "mute" : "unmute", cardNumber, controlName); + + private static async Task SetAudioCommand(string command, int cardNumber = DefaultCardNumber, string controlName = DefaultControlName) + { + var taskResult = await ProcessRunner.GetProcessOutputAsync("amixer", $"-q -c {cardNumber} -- set {controlName} {command}").ConfigureAwait(false); + + if (!string.IsNullOrWhiteSpace(taskResult)) + throw new InvalidOperationException(taskResult.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries).First()); + + return taskResult; + } + } +} diff --git a/Unosquare.RaspberryIO/Computer/AudioState.cs b/Unosquare.RaspberryIO/Computer/AudioState.cs new file mode 100644 index 0000000..ab71777 --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/AudioState.cs @@ -0,0 +1,64 @@ +namespace Unosquare.RaspberryIO.Computer +{ + /// + /// Manage the volume of any sound device. + /// + public readonly struct AudioState + { + /// + /// Initializes a new instance of the struct. + /// + /// The card number. + /// Name of the control. + /// The volume level in percentaje. + /// The volume level in decibels. + /// if set to true the audio is mute. + public AudioState(int cardNumber, string controlName, int level, float decibels, bool isMute) + { + CardNumber = cardNumber; + ControlName = controlName; + Level = level; + Decibels = decibels; + IsMute = isMute; + } + + /// + /// Gets the card number. + /// + public int CardNumber { get; } + + /// + /// Gets the name of the current control. + /// + public string ControlName { get; } + + /// + /// Gets the volume level in percentage. + /// + public int Level { get; } + + /// + /// Gets the volume level in decibels. + /// + public float Decibels { get; } + + /// + /// Gets a value indicating whether the audio is mute. + /// + public bool IsMute { get; } + + /// + /// Returns a that represents the audio state. + /// + /// + /// A that represents the audio state. + /// + public override string ToString() => + "Device information: \n" + + $">> Name: {ControlName}\n" + + $">> Card number: {CardNumber}\n" + + $">> Volume (%): {Level}%\n" + + $">> Volume (dB): {Decibels:0.00}dB\n" + + $">> Mute: [{(IsMute ? "Off" : "On")}]\n\n"; + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO/Computer/Bluetooth.cs b/Unosquare.RaspberryIO/Computer/Bluetooth.cs new file mode 100644 index 0000000..beee700 --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/Bluetooth.cs @@ -0,0 +1,271 @@ +namespace Unosquare.RaspberryIO.Computer +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Swan; + + /// + /// Represents the Bluetooth information. + /// + public class Bluetooth : SingletonBase + { + private const string BcCommand = "bluetoothctl"; + + /// + /// Turns on the Bluetooth adapter. + /// + /// The cancellation token. + /// + /// Returns true or false depending if the controller was turned on. + /// + /// Failed to power on:. + public async Task PowerOn(CancellationToken cancellationToken = default) + { + try + { + var output = await ProcessRunner.GetProcessOutputAsync(BcCommand, "power on", null, cancellationToken) + .ConfigureAwait(false); + return output.Contains("succeeded"); + } + catch (Exception ex) + { + throw new BluetoothErrorException($"Failed to power on: {ex.Message}"); + } + } + + /// + /// Turns off the bluetooth adapter. + /// + /// The cancellation token. + /// + /// Returns true or false depending if the controller was turned off. + /// + /// Failed to power off:. + public async Task PowerOff(CancellationToken cancellationToken = default) + { + try + { + var output = await ProcessRunner.GetProcessOutputAsync(BcCommand, "power off", null, cancellationToken) + .ConfigureAwait(false); + return output.Contains("succeeded"); + } + catch (Exception ex) + { + throw new BluetoothErrorException($"Failed to power off: {ex.Message}"); + } + } + + /// + /// Gets the list of detected devices. + /// + /// The cancellation token. + /// + /// Returns the list of detected devices. + /// + /// Failed to retrieve devices:. + public async Task> ListDevices(CancellationToken cancellationToken = default) + { + try + { + using var cancellationTokenSource = new CancellationTokenSource(3000); + await ProcessRunner.GetProcessOutputAsync(BcCommand, "scan on", null, cancellationTokenSource.Token) + .ConfigureAwait(false); + await ProcessRunner.GetProcessOutputAsync(BcCommand, "scan off", null, cancellationToken) + .ConfigureAwait(false); + var devices = await ProcessRunner.GetProcessOutputAsync(BcCommand, "devices", null, cancellationToken) + .ConfigureAwait(false); + return devices.Trim().Split('\n').Select(x => x.Trim()); + } + catch (Exception ex) + { + throw new BluetoothErrorException($"Failed to retrieve devices: {ex.Message}"); + } + } + + /// + /// Gets the list of bluetooth controllers. + /// + /// The cancellation token. + /// + /// Returns the list of bluetooth controllers. + /// + /// Failed to retrieve controllers:. + public async Task> ListControllers(CancellationToken cancellationToken = default) + { + try + { + var controllers = await ProcessRunner.GetProcessOutputAsync(BcCommand, "list", null, cancellationToken) + .ConfigureAwait(false); + return controllers.Trim().Split('\n').Select(x => x.Trim()); + } + catch (Exception ex) + { + throw new BluetoothErrorException($"Failed to retrieve controllers: {ex.Message}"); + } + } + + /// + /// Pairs a specific device with a specific controller. + /// + /// The mac address of the controller that will be used to pair. + /// The mac address of the device that will be paired. + /// The cancellation token. + /// + /// Returns true or false if the pair was successfully. + /// + /// Failed to Pair:. + public async Task Pair( + string controllerAddress, + string deviceAddress, + CancellationToken cancellationToken = default) + { + try + { + // Selects the controller to pair. Once you select the controller, all controller-related commands will apply to it for three minutes. + await ProcessRunner + .GetProcessOutputAsync(BcCommand, $"select {controllerAddress}", null, cancellationToken) + .ConfigureAwait(false); + + // Makes the controller visible to other devices. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable on", null, cancellationToken) + .ConfigureAwait(false); + + // Readies the controller for pairing. Remember that you have three minutes after running this command to pair. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "pairable on", null, cancellationToken) + .ConfigureAwait(false); + + // Pairs the device with the controller. + var result = await ProcessRunner + .GetProcessOutputAsync(BcCommand, $"pair {deviceAddress}", null, cancellationToken) + .ConfigureAwait(false); + + // Hides the controller from other Bluetooth devices. Otherwise, any device that can detect it has access to it, leaving a major security hole. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable off", null, cancellationToken) + .ConfigureAwait(false); + + return result.Contains("Paired: yes"); + } + catch (Exception ex) + { + throw new BluetoothErrorException($"Failed to Pair: {ex.Message}"); + } + } + + /// + /// Performs a connection of a given controller with a given device. + /// + /// The mac address of the controller that will be used to make the connection. + /// The mac address of the device that will be connected. + /// The cancellation token. + /// + /// Returns true or false if the connection was successfully. + /// + /// Failed to connect:. + public async Task Connect( + string controllerAddress, + string deviceAddress, + CancellationToken cancellationToken = default) + { + try + { + // Selects the controller to pair. Once you select the controller, all controller-related commands will apply to it for three minutes. + await ProcessRunner + .GetProcessOutputAsync(BcCommand, $"select {controllerAddress}", null, cancellationToken) + .ConfigureAwait(false); + + // Makes the controller visible to other devices. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable on", null, cancellationToken) + .ConfigureAwait(false); + + // Readies the controller for pairing. Remember that you have three minutes after running this command to pair. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "pairable on", null, cancellationToken) + .ConfigureAwait(false); + + // Readies the device for pairing. + var result = await ProcessRunner + .GetProcessOutputAsync(BcCommand, $"connect {deviceAddress}", null, cancellationToken) + .ConfigureAwait(false); + + // Hides the controller from other Bluetooth devices. Otherwise, any device that can detect it has access to it, leaving a major security hole. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable off", null, cancellationToken) + .ConfigureAwait(false); + + return result.Contains("Connected: yes"); + } + catch (Exception ex) + { + throw new BluetoothErrorException($"Failed to connect: {ex.Message}"); + } + } + + /// + /// Sets the device to re-pair automatically when it is turned on, which eliminates the need to pair all over again. + /// + /// The mac address of the controller will be used. + /// The mac address of the device will be added to the trust list devices. + /// The cancellation token. + /// + /// Returns true or false if the operation was successful. + /// + /// Failed to add to trust devices list:. + public async Task Trust( + string controllerAddress, + string deviceAddress, + CancellationToken cancellationToken = default) + { + try + { + // Selects the controller to pair. Once you select the controller, all controller-related commands will apply to it for three minutes. + await ProcessRunner + .GetProcessOutputAsync(BcCommand, $"select {controllerAddress}", null, cancellationToken) + .ConfigureAwait(false); + + // Makes the controller visible to other devices. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable on", null, cancellationToken) + .ConfigureAwait(false); + + // Readies the controller for pairing. Remember that you have three minutes after running this command to pair. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "pairable on", null, cancellationToken) + .ConfigureAwait(false); + + // Sets the device to re-pair automatically when it is turned on, which eliminates the need to pair all over again. + var result = await ProcessRunner + .GetProcessOutputAsync(BcCommand, $"trust {deviceAddress}", null, cancellationToken) + .ConfigureAwait(false); + + // Hides the controller from other Bluetooth devices. Otherwise, any device that can detect it has access to it, leaving a major security hole. + await ProcessRunner.GetProcessOutputAsync(BcCommand, "discoverable off", null, cancellationToken) + .ConfigureAwait(false); + + return result.Contains("Trusted: yes"); + } + catch (Exception ex) + { + throw new BluetoothErrorException($"Failed to add to trust devices list: {ex.Message}"); + } + } + + /// + /// Displays information about a particular device. + /// + /// The mac address of the device which info will be retrieved. + /// The cancellation token. + /// + /// Returns the device info. + /// + /// Failed to retrieve info for {deviceAddress}. + public async Task DeviceInfo(string deviceAddress, CancellationToken cancellationToken = default) + { + var info = await ProcessRunner + .GetProcessOutputAsync(BcCommand, $"info {deviceAddress}", null, cancellationToken) + .ConfigureAwait(false); + + return !string.IsNullOrEmpty(info) + ? info + : throw new BluetoothErrorException($"Failed to retrieve info for {deviceAddress}"); + } + } +} diff --git a/Unosquare.RaspberryIO/Computer/DsiDisplay.cs b/Unosquare.RaspberryIO/Computer/DsiDisplay.cs new file mode 100644 index 0000000..cd49aaf --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/DsiDisplay.cs @@ -0,0 +1,71 @@ +namespace Unosquare.RaspberryIO.Computer +{ + using System.Globalization; + using System.IO; + using Swan; + + /// + /// The Official Raspberry Pi 7-inch touch display from the foundation + /// Some docs available here: + /// http://forums.pimoroni.com/t/official-7-raspberry-pi-touch-screen-faq/959. + /// + public class DsiDisplay : SingletonBase + { + private const string BacklightFilename = "/sys/class/backlight/rpi_backlight/bl_power"; + private const string BrightnessFilename = "/sys/class/backlight/rpi_backlight/brightness"; + + /// + /// Prevents a default instance of the class from being created. + /// + private DsiDisplay() + { + // placeholder + } + + /// + /// Gets a value indicating whether the Pi Foundation Display files are present. + /// + /// + /// true if this instance is present; otherwise, false. + /// + public bool IsPresent => File.Exists(BrightnessFilename); + + /// + /// Gets or sets the brightness of the DSI display via filesystem. + /// + /// + /// The brightness. + /// + public byte Brightness + { + get => + IsPresent + ? byte.TryParse(File.ReadAllText(BrightnessFilename).Trim(), out var brightness) ? brightness : (byte)0 : + (byte)0; + set + { + if (IsPresent) + File.WriteAllText(BrightnessFilename, value.ToString(CultureInfo.InvariantCulture)); + } + } + + /// + /// Gets or sets a value indicating whether the backlight of the DSI display on. + /// This operation is performed via the file system. + /// + /// + /// true if this instance is backlight on; otherwise, false. + /// + public bool IsBacklightOn + { + get => + IsPresent && (int.TryParse(File.ReadAllText(BacklightFilename).Trim(), out var value) && + value == 0); + set + { + if (IsPresent) + File.WriteAllText(BacklightFilename, value ? "0" : "1"); + } + } + } +} diff --git a/Unosquare.RaspberryIO/Computer/NetworkAdapterInfo.cs b/Unosquare.RaspberryIO/Computer/NetworkAdapterInfo.cs new file mode 100644 index 0000000..661a357 --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/NetworkAdapterInfo.cs @@ -0,0 +1,40 @@ +namespace Unosquare.RaspberryIO.Computer +{ + using System.Net; + + /// + /// Represents a Network Adapter. + /// + public class NetworkAdapterInfo + { + /// + /// Gets the name. + /// + public string Name { get; internal set; } + + /// + /// Gets the IP V4 address. + /// + public IPAddress IPv4 { get; internal set; } + + /// + /// Gets the IP V6 address. + /// + public IPAddress IPv6 { get; internal set; } + + /// + /// Gets the name of the access point. + /// + public string AccessPointName { get; internal set; } + + /// + /// Gets the MAC (Physical) address. + /// + public string MacAddress { get; internal set; } + + /// + /// Gets a value indicating whether this instance is wireless. + /// + public bool IsWireless { get; internal set; } + } +} diff --git a/Unosquare.RaspberryIO/Computer/NetworkSettings.cs b/Unosquare.RaspberryIO/Computer/NetworkSettings.cs new file mode 100644 index 0000000..9d88ff7 --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/NetworkSettings.cs @@ -0,0 +1,281 @@ +namespace Unosquare.RaspberryIO.Computer +{ + using Swan; + using Swan.Logging; + using Swan.Net; + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Threading.Tasks; + + /// + /// Represents the network information. + /// + public class NetworkSettings : SingletonBase + { + private const string EssidTag = "ESSID:"; + + /// + /// Gets the local machine Host Name. + /// + public string HostName => Network.HostName; + + /// + /// Retrieves the wireless networks. + /// + /// The adapter. + /// A list of WiFi networks. + public Task> RetrieveWirelessNetworks(string adapter) => RetrieveWirelessNetworks(new[] { adapter }); + + /// + /// Retrieves the wireless networks. + /// + /// The adapters. + /// A list of WiFi networks. + public async Task> RetrieveWirelessNetworks(string[] adapters = null) + { + var result = new List(); + + foreach (var networkAdapter in adapters ?? (await RetrieveAdapters()).Where(x => x.IsWireless).Select(x => x.Name)) + { + var wirelessOutput = await ProcessRunner.GetProcessOutputAsync("iwlist", $"{networkAdapter} scanning").ConfigureAwait(false); + var outputLines = + wirelessOutput.Split('\n') + .Select(x => x.Trim()) + .Where(x => string.IsNullOrWhiteSpace(x) == false) + .ToArray(); + + for (var i = 0; i < outputLines.Length; i++) + { + var line = outputLines[i]; + + if (line.StartsWith(EssidTag) == false) continue; + + var network = new WirelessNetworkInfo + { + Name = line.Replace(EssidTag, string.Empty).Replace("\"", string.Empty) + }; + + while (true) + { + if (i + 1 >= outputLines.Length) break; + + // should look for two lines before the ESSID acording to the scan + line = outputLines[i - 2]; + + if (!line.StartsWith("Quality=")) continue; + network.Quality = line.Replace("Quality=", string.Empty); + break; + } + + while (true) + { + if (i + 1 >= outputLines.Length) break; + + // should look for a line before the ESSID acording to the scan + line = outputLines[i - 1]; + + if (!line.StartsWith("Encryption key:")) continue; + network.IsEncrypted = line.Replace("Encryption key:", string.Empty).Trim() == "on"; + break; + } + + if (result.Any(x => x.Name == network.Name) == false) + result.Add(network); + } + } + + return result + .OrderBy(x => x.Name) + .ToList(); + } + + /// + /// Setups the wireless network. + /// + /// Name of the adapter. + /// The network ssid. + /// The password (8 characters as minimum length). + /// The 2-letter country code in uppercase. Default is US. + /// True if successful. Otherwise, false. + public async Task SetupWirelessNetwork(string adapterName, string networkSsid, string password = null, string countryCode = "US") + { + // TODO: Get the country where the device is located to set 'country' param in payload var + var payload = $"country={countryCode}\nctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev\nupdate_config=1\n"; + + if (!string.IsNullOrWhiteSpace(password) && password.Length < 8) + throw new InvalidOperationException("The password must be at least 8 characters length."); + + payload += string.IsNullOrEmpty(password) + ? $"network={{\n\tssid=\"{networkSsid}\"\n\tkey_mgmt=NONE\n\t}}\n" + : $"network={{\n\tssid=\"{networkSsid}\"\n\tpsk=\"{password}\"\n\t}}\n"; + try + { + File.WriteAllText("/etc/wpa_supplicant/wpa_supplicant.conf", payload); + await ProcessRunner.GetProcessOutputAsync("pkill", "-f wpa_supplicant"); + await ProcessRunner.GetProcessOutputAsync("ifdown", adapterName); + await ProcessRunner.GetProcessOutputAsync("ifup", adapterName); + } + catch (Exception ex) + { + ex.Log(nameof(NetworkSettings)); + return false; + } + + return true; + } + + /// + /// Retrieves the network adapters. + /// + /// A list of network adapters. + public async Task> RetrieveAdapters() + { + const string hWaddr = "HWaddr "; + const string ether = "ether "; + + var result = new List(); + var interfacesOutput = await ProcessRunner.GetProcessOutputAsync("ifconfig"); + var wlanOutput = (await ProcessRunner.GetProcessOutputAsync("iwconfig")) + .Split('\n') + .Where(x => x.Contains("no wireless extensions.") == false) + .ToArray(); + + var outputLines = interfacesOutput.Split('\n').Where(x => string.IsNullOrWhiteSpace(x) == false).ToArray(); + + for (var i = 0; i < outputLines.Length; i++) + { + // grab the current line + var line = outputLines[i]; + + // skip if the line is indented + if (char.IsLetterOrDigit(line[0]) == false) + continue; + + // Read the line as an adapter + var adapter = new NetworkAdapterInfo + { + Name = line.Substring(0, line.IndexOf(' ')).TrimEnd(':') + }; + + // Parse the MAC address in old version of ifconfig; it comes in the first line + if (line.IndexOf(hWaddr, StringComparison.Ordinal) >= 0) + { + var startIndexHwd = line.IndexOf(hWaddr, StringComparison.Ordinal) + hWaddr.Length; + adapter.MacAddress = line.Substring(startIndexHwd, 17).Trim(); + } + + // Parse the info in lines other than the first + for (var j = i + 1; j < outputLines.Length; j++) + { + // Get the contents of the indented line + var indentedLine = outputLines[j]; + + // We have hit the next adapter info + if (char.IsLetterOrDigit(indentedLine[0])) + { + i = j - 1; + break; + } + + // Parse the MAC address in new versions of ifconfig; it no longer comes in the first line + if (indentedLine.IndexOf(ether, StringComparison.Ordinal) >= 0 && string.IsNullOrWhiteSpace(adapter.MacAddress)) + { + var startIndexHwd = indentedLine.IndexOf(ether, StringComparison.Ordinal) + ether.Length; + adapter.MacAddress = indentedLine.Substring(startIndexHwd, 17).Trim(); + } + + // Parse the IPv4 Address + GetIPv4(indentedLine, adapter); + + // Parse the IPv6 Address + GetIPv6(indentedLine, adapter); + + // we have hit the end of the output in an indented line + if (j >= outputLines.Length - 1) + i = outputLines.Length; + } + + // Retrieve the wireless LAN info + var wlanInfo = wlanOutput.FirstOrDefault(x => x.StartsWith(adapter.Name)); + + if (wlanInfo != null) + { + adapter.IsWireless = true; + var essidParts = wlanInfo.Split(new[] { EssidTag }, StringSplitOptions.RemoveEmptyEntries); + if (essidParts.Length >= 2) + { + adapter.AccessPointName = essidParts[1].Replace("\"", string.Empty).Trim(); + } + } + + // Add the current adapter to the result + result.Add(adapter); + } + + return result.OrderBy(x => x.Name).ToList(); + } + + /// + /// Retrieves the current network adapter. + /// + /// The name of the current network adapter. + public static async Task GetCurrentAdapterName() + { + var result = await ProcessRunner.GetProcessOutputAsync("route").ConfigureAwait(false); + var defaultLine = result.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .FirstOrDefault(l => l.StartsWith("default", StringComparison.OrdinalIgnoreCase)); + + return defaultLine?.Trim().Substring(defaultLine.LastIndexOf(" ", StringComparison.OrdinalIgnoreCase) + 1); + } + + /// + /// Retrieves current wireless connected network name. + /// + /// The connected network name. + public Task GetWirelessNetworkName() => ProcessRunner.GetProcessOutputAsync("iwgetid", "-r"); + + private static void GetIPv4(string indentedLine, NetworkAdapterInfo adapter) + { + var addressText = ParseOutputTagFromLine(indentedLine, "inet addr:") ?? + ParseOutputTagFromLine(indentedLine, "inet "); + + if (addressText == null) return; + if (IPAddress.TryParse(addressText, out var outValue)) + adapter.IPv4 = outValue; + } + + private static void GetIPv6(string indentedLine, NetworkAdapterInfo adapter) + { + var addressText = ParseOutputTagFromLine(indentedLine, "inet6 addr:") ?? + ParseOutputTagFromLine(indentedLine, "inet6 "); + + if (addressText == null) return; + + if (IPAddress.TryParse(addressText, out var outValue)) + adapter.IPv6 = outValue; + } + + private static string ParseOutputTagFromLine(string indentedLine, string tagName) + { + if (indentedLine.IndexOf(tagName, StringComparison.Ordinal) < 0) + return null; + + var startIndex = indentedLine.IndexOf(tagName, StringComparison.Ordinal) + tagName.Length; + var builder = new StringBuilder(1024); + for (var c = startIndex; c < indentedLine.Length; c++) + { + var currentChar = indentedLine[c]; + if (!char.IsPunctuation(currentChar) && !char.IsLetterOrDigit(currentChar)) + break; + + builder.Append(currentChar); + } + + return builder.ToString(); + } + } +} diff --git a/Unosquare.RaspberryIO/Computer/OsInfo.cs b/Unosquare.RaspberryIO/Computer/OsInfo.cs new file mode 100644 index 0000000..d8d85cd --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/OsInfo.cs @@ -0,0 +1,46 @@ +namespace Unosquare.RaspberryIO.Computer +{ + /// + /// Represents the OS Information. + /// + public class OsInfo + { + /// + /// System name. + /// + public string SysName { get; set; } + + /// + /// Node name. + /// + public string NodeName { get; set; } + + /// + /// Release level. + /// + public string Release { get; set; } + + /// + /// Version level. + /// + public string Version { get; set; } + + /// + /// Hardware level. + /// + public string Machine { get; set; } + + /// + /// Domain name. + /// + public string DomainName { get; set; } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() => $"{SysName} {Release} {Version}"; + } +} diff --git a/Unosquare.RaspberryIO/Computer/PiVersion.cs b/Unosquare.RaspberryIO/Computer/PiVersion.cs new file mode 100644 index 0000000..c748509 --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/PiVersion.cs @@ -0,0 +1,394 @@ +namespace Unosquare.RaspberryIO.Computer +{ + /// + /// Defines the board revision codes of the different versions of the Raspberry Pi + /// http://www.raspberrypi-spy.co.uk/2012/09/checking-your-raspberry-pi-board-version/. + /// https://www.raspberrypi.org/documentation/hardware/raspberrypi/revision-codes/README.md. + /// + public enum PiVersion + { + /// + /// The unknown version + /// + Unknown = 0, + + /// + /// The model B Rev1 + /// + ModelBRev1 = 0x0002, + + /// + /// The model B Rev1 ECN0001 + /// + ModelBRev1ECN0001 = 0x0003, + + /// + /// The model B Rev2 Sony + /// + ModelBRev2x04 = 0x0004, + + /// + /// The model B Rev2 Qisda + /// + ModelBRev2x05 = 0x0005, + + /// + /// The model B Rev2 Egoman + /// + ModelBRev2x06 = 0x0006, + + /// + /// The model A Rev2 Egoman + /// + ModelAx07 = 0x0007, + + /// + /// The model A Rev2 Sony + /// + ModelAx08 = 0x0008, + + /// + /// The model A Rev2 Qisda + /// + ModelAx09 = 0x0009, + + /// + /// The model B Rev2 (512MB) Egoman + /// + ModelBRev2x0d = 0x000d, + + /// + /// The model B Rev2 (512MB) Sony + /// + ModelBRev2x0e = 0x000e, + + /// + /// The model B Rev2 (512MB) Egoman + /// + ModelBRev2x0f = 0x000f, + + /// + /// The model B+ Rev1 Sony + /// + ModelBPlus0x10 = 0x0010, + + /// + /// The compute module 1 Sony + /// + ComputeModule0x11 = 0x0011, + + /// + /// The model A+ Rev1.1 Sony + /// + ModelAPlus0x12 = 0x0012, + + /// + /// The model B+ Rev1.2 Embest + /// + ModelBPlus0x13 = 0x0013, + + /// + /// The compute module 1 Embest + /// + ComputeModule0x14 = 0x0014, + + /// + /// The model A+ Rev1.1 Embest + /// + ModelAPlus0x15 = 0x0015, + + /// + /// The model A+ Rev1.1 (512MB) Sony + /// + ModelAPlus1v1Sony = 900021, + + /// + /// The model B+ Rev1.2 sony + /// + ModelBPlus1v2Sony = 900032, + + /// + /// The Pi Zero Rev1.2 Sony + /// + PiZero1v2 = 0x900092, + + /// + /// The Pi Zero Rev1.3 SOny + /// + PiZero1v3 = 0x900093, + + /// + /// The Pi Zero W Rev1.1 + /// + PiZeroW = 0x9000c1, + + /// + /// The Pi 3 model A+ Sony + /// + Pi3ModelAPlus = 0x9020e0, + + /// + /// The Pi Zero Rev1.2 Embest + /// + PiZero1v2Embest = 0x920092, + + /// + /// The Pi Zero Rev1.3 Embest + /// + PiZero1v3Embest = 0x920093, + + /// + /// The Pi 2 model B Rev1.0 Sony + /// + Pi2ModelB1v0Sony = 0xa01040, + + /// + /// The Pi 2 model B Rev1.1 Sony + /// + Pi2ModelB1v1Sony = 0xa01041, + + /// + /// The Pi 3 model B Rev1.2 Sony + /// + Pi3ModelBSony = 0xa02082, + + /// + /// The compute module 3 Rev1.0 Sony + /// + ComputeModule3Sony = 0xa020a0, + + /// + /// The Pi 3 model B+ Rev1.3 Sony + /// + Pi3ModelBPlusSony = 0xa020d3, + + /// + /// The Pi 2 model B Rev1.1 Embest + /// + Pi2ModelB1v1Embest = 0xa21041, + + /// + /// The Pi 2 model B Rev1.2 Embest + /// + Pi2ModelB1v2 = 0xa22042, + + /// + /// The Pi 3 model B Rev1.2 Embest + /// + Pi3ModelBEmbest = 0xa22082, + + /// + /// The compute module 3 Rev1.0 Embest + /// + ComputeModule3Embest = 0xa220a0, + + /// + /// The Pi 3 model B Rev1.2 Sony Japan + /// + Pi3ModelBSonyJapan = 0xa32082, + + /// + /// The Pi 3 model B Rev1.2 Stadium + /// + Pi3ModelBStadium = 0xa52082, + + /// + /// The compute module 3+ Rev 1.0 Sony + /// + ComputeModule3PlusSony = 0xa02100, + + /// + /// The Pi 4 model B 1GB, Sony + /// + Pi4ModelB1Gb = 0xa03111, + + /// + /// The Pi 4 model B 2GB, Sony + /// + Pi4ModelB2Gb = 0xb03111, + + /// + /// The Pi 4 model B 4GB, Sony + /// + Pi4ModelB4Gb = 0xc03111, + } + + /// + /// Defines the board model accordingly to new-style revision codes. + /// + public enum BoardModel + { + /// + /// Model A + /// + ModelA = 0, + + /// + /// Model B + /// + ModelB = 1, + + /// + /// Model A+ + /// + ModelAPlus = 2, + + /// + /// Model B+ + /// + ModelBPlus = 3, + + /// + /// Model 2B + /// + Model2B = 4, + + /// + /// Alpha (early prototype) + /// + Alpha = 5, + + /// + /// Compute Module 1 + /// + CM1 = 6, + + /// + /// Model 3B + /// + Model3B = 8, + + /// + /// Model Zero + /// + Zero = 9, + + /// + /// Compute Module 3 + /// + CM3 = 0xa, + + /// + /// Model Zero W + /// + ZeroW = 0xc, + + /// + /// Model 3B+ + /// + Model3BPlus = 0xd, + + /// + /// Model 3A+ + /// + Model3APlus = 0xe, + + /// + /// Reserved (Internal use only) + /// + InternalUse = 0xf, + + /// + /// Compute Module 3+ + /// + CM3Plus = 0x10, + + /// + /// Model 4B + /// + Model4B = 0x11, + } + + /// + /// Defines the processor model accordingly to new-style revision codes. + /// + public enum ProcessorModel + { + /// + /// The BCMM2835 processor. + /// + BCM2835, + + /// + /// The BCMM2836 processor. + /// + BCM2836, + + /// + /// The BCMM2837 processor. + /// + BCM2837, + + /// + /// The BCM2711 processor. + /// + BCM2711, + } + + /// + /// Defines the manufacturer accordingly to new-style revision codes. + /// + public enum Manufacturer + { + /// + /// Sony UK + /// + SonyUK, + + /// + /// Egoman + /// + Egoman, + + /// + /// Embest + /// + Embest, + + /// + /// Sony Japan + /// + SonyJapan, + + /// + /// Embest + /// + Embest2, + + /// + /// Stadium + /// + Stadium, + } + + /// + /// Defines the memory size accordingly to new-style revision codes. + /// + public enum MemorySize + { + /// + /// 256 MB + /// + Memory256, + + /// + /// 512 MB + /// + Memory512, + + /// + /// 1 GB + /// + Memory1024, + + /// + /// 2 GB + /// + Memory2048, + + /// + /// 4 GB + /// + Memory4096, + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO/Computer/SystemInfo.cs b/Unosquare.RaspberryIO/Computer/SystemInfo.cs new file mode 100644 index 0000000..fdce8ba --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/SystemInfo.cs @@ -0,0 +1,390 @@ +namespace Unosquare.RaspberryIO.Computer +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Reflection; + using Abstractions; + using Native; + using Swan; + using Swan.DependencyInjection; + + /// + /// Retrieves the RaspberryPI System Information. + /// + /// http://raspberry-pi-guide.readthedocs.io/en/latest/system.html. + /// + public sealed class SystemInfo : SingletonBase + { + private const string CpuInfoFilePath = "/proc/cpuinfo"; + private const string MemInfoFilePath = "/proc/meminfo"; + private const string UptimeFilePath = "/proc/uptime"; + + private const int NewStyleCodesMask = 0x800000; + + private BoardModel _boardModel; + private ProcessorModel _processorModel; + private Manufacturer _manufacturer; + private MemorySize _memorySize; + + /// + /// Prevents a default instance of the class from being created. + /// + /// Could not initialize the GPIO controller. + private SystemInfo() + { + #region Obtain and format a property dictionary + + var properties = + typeof(SystemInfo) + .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where( + p => + p.CanWrite && p.CanRead && + (p.PropertyType == typeof(string) || p.PropertyType == typeof(string[]))) + .ToArray(); + var propDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var prop in properties) + { + propDictionary[prop.Name.Replace(" ", string.Empty).ToLowerInvariant().Trim()] = prop; + } + + #endregion + + #region Extract CPU information + + if (File.Exists(CpuInfoFilePath)) + { + var cpuInfoLines = File.ReadAllLines(CpuInfoFilePath); + + foreach (var line in cpuInfoLines) + { + var lineParts = line.Split(new[] { ':' }, 2); + if (lineParts.Length != 2) + continue; + + var propertyKey = lineParts[0].Trim().Replace(" ", string.Empty); + var propertyStringValue = lineParts[1].Trim(); + + if (!propDictionary.ContainsKey(propertyKey)) continue; + + var property = propDictionary[propertyKey]; + if (property.PropertyType == typeof(string)) + { + property.SetValue(this, propertyStringValue); + } + else if (property.PropertyType == typeof(string[])) + { + var propertyArrayValue = propertyStringValue.Split(' '); + property.SetValue(this, propertyArrayValue); + } + } + } + + #endregion + + ExtractMemoryInfo(); + ExtractBoardVersion(); + ExtractOS(); + } + + /// + /// Gets the library version. + /// + public Version LibraryVersion { get; private set; } + + /// + /// Gets the OS information. + /// + /// + /// The os information. + /// + public OsInfo OperatingSystem { get; set; } + + /// + /// Gets the Raspberry Pi version. + /// + public PiVersion RaspberryPiVersion { get; set; } + + /// + /// Gets the board revision (1 or 2). + /// + /// + /// The wiring pi board revision. + /// + public int BoardRevision { get; set; } + + /// + /// Gets the number of processor cores. + /// + public int ProcessorCount => int.TryParse(Processor, out var outIndex) ? outIndex + 1 : 0; + + /// + /// Gets the installed ram in bytes. + /// + public int InstalledRam { get; private set; } + + /// + /// Gets a value indicating whether this CPU is little endian. + /// + public bool IsLittleEndian => BitConverter.IsLittleEndian; + + /// + /// Gets the CPU model name. + /// + public string ModelName { get; private set; } + + /// + /// Gets a list of supported CPU features. + /// + public string[] Features { get; private set; } + + /// + /// Gets the CPU implementer hex code. + /// + public string CpuImplementer { get; private set; } + + /// + /// Gets the CPU architecture code. + /// + public string CpuArchitecture { get; private set; } + + /// + /// Gets the CPU variant code. + /// + public string CpuVariant { get; private set; } + + /// + /// Gets the CPU part code. + /// + public string CpuPart { get; private set; } + + /// + /// Gets the CPU revision code. + /// + public string CpuRevision { get; private set; } + + /// + /// Gets the hardware model number. + /// + public string Hardware { get; private set; } + + /// + /// Gets the hardware revision number. + /// + public string Revision { get; private set; } + + /// + /// Gets the revision number (accordingly to new-style revision codes). + /// + public int RevisionNumber { get; set; } + + /// + /// Gets the board model (accordingly to new-style revision codes). + /// + /// /// This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}. + public BoardModel BoardModel => + NewStyleRevisionCodes ? + _boardModel : + throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead."); + + /// + /// Gets processor model (accordingly to new-style revision codes). + /// + /// /// This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}. + public ProcessorModel ProcessorModel => + NewStyleRevisionCodes ? + _processorModel : + throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead."); + + /// + /// Gets the manufacturer of the board (accordingly to new-style revision codes). + /// + /// This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}. + public Manufacturer Manufacturer => + NewStyleRevisionCodes ? + _manufacturer : + throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead."); + + /// + /// Gets the size of the memory (accordingly to new-style revision codes). + /// + /// This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)}. + public MemorySize MemorySize => + NewStyleRevisionCodes ? + _memorySize : + throw new InvalidOperationException($"This board does not support new-style revision codes. Use {nameof(RaspberryPiVersion)} property instead."); + + /// + /// Gets the serial number. + /// + public string Serial { get; private set; } + + /// + /// Gets the system up-time (in seconds). + /// + public double Uptime + { + get + { + try + { + if (File.Exists(UptimeFilePath) == false) return 0; + var parts = File.ReadAllText(UptimeFilePath).Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length >= 1 && float.TryParse(parts[0], out var result)) + return result; + } + catch + { + /* Ignore */ + } + + return 0; + } + } + + /// + /// Gets the uptime in TimeSpan. + /// + public TimeSpan UptimeTimeSpan => TimeSpan.FromSeconds(Uptime); + + /// + /// Indicates if the board uses the new-style revision codes. + /// + private bool NewStyleRevisionCodes { get; set; } + + /// + /// Placeholder for processor index. + /// + private string Processor { get; set; } + + /// + /// Returns a that represents this instance. + /// + /// + /// A that represents this instance. + /// + public override string ToString() + { + var properties = typeof(SystemInfo).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(p => p.CanRead && ( + p.PropertyType == typeof(string) || + p.PropertyType == typeof(string[]) || + p.PropertyType == typeof(int) || + p.PropertyType == typeof(bool) || + p.PropertyType == typeof(TimeSpan))) + .ToArray(); + + var propertyValues2 = new List + { + "System Information", + $"\t{nameof(LibraryVersion),-22}: {LibraryVersion}", + $"\t{nameof(RaspberryPiVersion),-22}: {RaspberryPiVersion}", + }; + + foreach (var property in properties) + { + if (property.PropertyType != typeof(string[])) + { + propertyValues2.Add($"\t{property.Name,-22}: {property.GetValue(this)}"); + } + else if (property.GetValue(this) is string[] allValues) + { + var concatValues = string.Join(" ", allValues); + propertyValues2.Add($"\t{property.Name,-22}: {concatValues}"); + } + } + + return string.Join(Environment.NewLine, propertyValues2.ToArray()); + } + + private void ExtractOS() + { + try + { + Standard.Uname(out var unameInfo); + + OperatingSystem = new OsInfo + { + DomainName = unameInfo.DomainName, + Machine = unameInfo.Machine, + NodeName = unameInfo.NodeName, + Release = unameInfo.Release, + SysName = unameInfo.SysName, + Version = unameInfo.Version, + }; + } + catch + { + OperatingSystem = new OsInfo(); + } + } + + private void ExtractBoardVersion() + { + var hasSysInfo = DependencyContainer.Current.CanResolve(); + + try + { + if (string.IsNullOrWhiteSpace(Revision) == false && + int.TryParse( + Revision.ToUpperInvariant(), + NumberStyles.HexNumber, + CultureInfo.InvariantCulture, + out var boardVersion)) + { + RaspberryPiVersion = PiVersion.Unknown; + if (Enum.IsDefined(typeof(PiVersion), boardVersion)) + RaspberryPiVersion = (PiVersion)boardVersion; + + if ((boardVersion & NewStyleCodesMask) == NewStyleCodesMask) + { + NewStyleRevisionCodes = true; + RevisionNumber = boardVersion & 0xF; + _boardModel = (BoardModel)((boardVersion >> 4) & 0xFF); + _processorModel = (ProcessorModel)((boardVersion >> 12) & 0xF); + _manufacturer = (Manufacturer)((boardVersion >> 16) & 0xF); + _memorySize = (MemorySize)((boardVersion >> 20) & 0x7); + } + } + + if (hasSysInfo) + BoardRevision = (int)DependencyContainer.Current.Resolve().BoardRevision; + } + catch + { + /* Ignore */ + } + + if (hasSysInfo) + LibraryVersion = DependencyContainer.Current.Resolve().LibraryVersion; + } + + private void ExtractMemoryInfo() + { + if (!File.Exists(MemInfoFilePath)) return; + + var memInfoLines = File.ReadAllLines(MemInfoFilePath); + + foreach (var line in memInfoLines) + { + var lineParts = line.Split(new[] { ':' }, 2); + if (lineParts.Length != 2) + continue; + + if (lineParts[0].ToLowerInvariant().Trim().Equals("memtotal") == false) + continue; + + var memKb = lineParts[1].ToLowerInvariant().Trim().Replace("kb", string.Empty).Trim(); + + if (!int.TryParse(memKb, out var parsedMem)) continue; + InstalledRam = parsedMem * 1024; + break; + } + } + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO/Computer/WirelessNetworkInfo.cs b/Unosquare.RaspberryIO/Computer/WirelessNetworkInfo.cs new file mode 100644 index 0000000..9e41d24 --- /dev/null +++ b/Unosquare.RaspberryIO/Computer/WirelessNetworkInfo.cs @@ -0,0 +1,23 @@ +namespace Unosquare.RaspberryIO.Computer +{ + /// + /// Represents a wireless network information. + /// + public class WirelessNetworkInfo + { + /// + /// Gets the ESSID of the Wireless network. + /// + public string Name { get; internal set; } + + /// + /// Gets the network quality. + /// + public string Quality { get; internal set; } + + /// + /// Gets a value indicating whether this instance is encrypted. + /// + public bool IsEncrypted { get; internal set; } + } +} diff --git a/Unosquare.RaspberryIO/Native/Standard.cs b/Unosquare.RaspberryIO/Native/Standard.cs new file mode 100644 index 0000000..cd15d80 --- /dev/null +++ b/Unosquare.RaspberryIO/Native/Standard.cs @@ -0,0 +1,17 @@ +namespace Unosquare.RaspberryIO.Native +{ + using System.Runtime.InteropServices; + + internal static class Standard + { + internal const string LibCLibrary = "libc"; + + /// + /// Fills in the structure with information about the system. + /// + /// The name. + /// The result. + [DllImport(LibCLibrary, EntryPoint = "uname", SetLastError = true)] + public static extern int Uname(out SystemName name); + } +} diff --git a/Unosquare.RaspberryIO/Native/SystemName.cs b/Unosquare.RaspberryIO/Native/SystemName.cs new file mode 100644 index 0000000..b9f5903 --- /dev/null +++ b/Unosquare.RaspberryIO/Native/SystemName.cs @@ -0,0 +1,47 @@ +namespace Unosquare.RaspberryIO.Native +{ + using System.Runtime.InteropServices; + + /// + /// OS uname structure. + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] + public struct SystemName + { + /// + /// System name. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] + public string SysName; + + /// + /// Node name. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] + public string NodeName; + + /// + /// Release level. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] + public string Release; + + /// + /// Version level. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] + public string Version; + + /// + /// Hardware level. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] + public string Machine; + + /// + /// Domain name. + /// + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 65)] + public string DomainName; + } +} \ No newline at end of file diff --git a/Unosquare.RaspberryIO/Pi.cs b/Unosquare.RaspberryIO/Pi.cs new file mode 100644 index 0000000..2500924 --- /dev/null +++ b/Unosquare.RaspberryIO/Pi.cs @@ -0,0 +1,141 @@ +namespace Unosquare.RaspberryIO +{ + using Abstractions; + using Camera; + using Computer; + using Swan; + using Swan.DependencyInjection; + using System; + using System.Threading.Tasks; + + /// + /// Our main character. Provides access to the Raspberry Pi's GPIO, system and board information and Camera. + /// + public static class Pi + { + private const string MissingDependenciesMessage = "You need to load a valid assembly (WiringPi or PiGPIO)."; + private static readonly object SyncLock = new object(); + private static bool _isInit; + private static SystemInfo _info; + + /// + /// Initializes static members of the class. + /// + static Pi() + { + lock (SyncLock) + { + Camera = CameraController.Instance; + PiDisplay = DsiDisplay.Instance; + Audio = AudioSettings.Instance; + Bluetooth = Bluetooth.Instance; + } + } + + /// + /// Provides information on this Raspberry Pi's CPU and form factor. + /// + public static SystemInfo Info => _info ??= SystemInfo.Instance; + + /// + /// Provides access to the Raspberry Pi's GPIO as a collection of GPIO Pins. + /// + public static IGpioController Gpio => + ResolveDependency(); + + /// + /// Provides access to the 2-channel SPI bus. + /// + public static ISpiBus Spi => + ResolveDependency(); + + /// + /// Provides access to the functionality of the i2c bus. + /// + public static II2CBus I2C => + ResolveDependency(); + + /// + /// Provides access to timing functionality. + /// + public static ITiming Timing => + ResolveDependency(); + + /// + /// Provides access to threading functionality. + /// + public static IThreading Threading => + ResolveDependency(); + + /// + /// Provides access to the official Raspberry Pi Camera. + /// + public static CameraController Camera { get; } + + /// + /// Provides access to the official Raspberry Pi 7-inch DSI Display. + /// + public static DsiDisplay PiDisplay { get; } + + /// + /// Provides access to Raspberry Pi ALSA sound card driver. + /// + public static AudioSettings Audio { get; } + + /// + /// Provides access to Raspberry Pi Bluetooth driver. + /// + public static Bluetooth Bluetooth { get; } + + /// + /// Restarts the Pi. Must be running as SU. + /// + /// The process result. + public static Task RestartAsync() => ProcessRunner.GetProcessResultAsync("reboot"); + + /// + /// Restarts the Pi. Must be running as SU. + /// + /// The process result. + public static ProcessResult Restart() => RestartAsync().GetAwaiter().GetResult(); + + /// + /// Halts the Pi. Must be running as SU. + /// + /// The process result. + public static Task ShutdownAsync() => ProcessRunner.GetProcessResultAsync("halt"); + + /// + /// Halts the Pi. Must be running as SU. + /// + /// The process result. + public static ProcessResult Shutdown() => ShutdownAsync().GetAwaiter().GetResult(); + + /// + /// Initializes an Abstractions implementation. + /// + /// An implementation of . + public static void Init() + where T : IBootstrap + { + lock (SyncLock) + { + if (_isInit) return; + + Activator.CreateInstance().Bootstrap(); + _isInit = true; + } + } + + private static T ResolveDependency() + where T : class + { + if (!_isInit) + throw new InvalidOperationException($"You must first initialize {nameof(Pi)} referencing a valid {nameof(IBootstrap)} implementation."); + + return DependencyContainer.Current.CanResolve() + ? DependencyContainer.Current.Resolve() + : throw new InvalidOperationException(MissingDependenciesMessage); + } + } +} diff --git a/Unosquare.RaspberryIO/Unosquare.RaspberryIO.csproj b/Unosquare.RaspberryIO/Unosquare.RaspberryIO.csproj new file mode 100644 index 0000000..5638c85 --- /dev/null +++ b/Unosquare.RaspberryIO/Unosquare.RaspberryIO.csproj @@ -0,0 +1,26 @@ + + + + The Raspberry Pi's IO Functionality in an easy-to use API for Mono/.NET Core + +This library enables developers to use the various Raspberry Pi's hardware modules including the Camera to capture images and video, the GPIO pins, and both, the SPI and I2C buses. + Unosquare (c) 2016-2019 + Unosquare Raspberry IO + netcoreapp3.0 + Unosquare.RaspberryIO + Unosquare.Raspberry.IO + 0.26.0 + Unosquare + https://github.com/unosquare/raspberryio/raw/master/logos/raspberryio-logo-32.png + https://github.com/unosquare/raspberryio + https://raw.githubusercontent.com/unosquare/raspberryio/master/LICENSE + Raspberry Pi GPIO Camera SPI I2C Embedded IoT Mono C# .NET + 8.0 + + + + + + + + diff --git a/Unosquare.WiringPi/BootstrapWiringPi.cs b/Unosquare.WiringPi/BootstrapWiringPi.cs new file mode 100644 index 0000000..6c70067 --- /dev/null +++ b/Unosquare.WiringPi/BootstrapWiringPi.cs @@ -0,0 +1,30 @@ +namespace Unosquare.WiringPi +{ + using RaspberryIO.Abstractions; + using Swan.DependencyInjection; + + /// + /// Represents the Bootstrap class to extract resources. + /// + /// + public class BootstrapWiringPi : IBootstrap + { + private static readonly object SyncLock = new object(); + + /// + public void Bootstrap() + { + lock (SyncLock) + { + Resources.EmbeddedResources.ExtractAll(); + + DependencyContainer.Current.Register(new GpioController()); + DependencyContainer.Current.Register(new SpiBus()); + DependencyContainer.Current.Register(new I2CBus()); + DependencyContainer.Current.Register(new SystemInfo()); + DependencyContainer.Current.Register(new Timing()); + DependencyContainer.Current.Register(new Threading()); + } + } + } +} diff --git a/Unosquare.WiringPi/Enums.cs b/Unosquare.WiringPi/Enums.cs new file mode 100644 index 0000000..5e5c6b3 --- /dev/null +++ b/Unosquare.WiringPi/Enums.cs @@ -0,0 +1,289 @@ +namespace Unosquare.WiringPi +{ + using System; + + /// + /// Defines all the available Wiring Pi Pin Numbers. + /// + public enum WiringPiPin + { + /// + /// Unknown WiringPi pin. + /// + Unknown = -1, + + /// + /// WiringPi pin 0. + /// + Pin00 = 0, + + /// + /// WiringPi pin 1. + /// + Pin01 = 1, + + /// + /// WiringPi pin 2. + /// + Pin02 = 2, + + /// + /// WiringPi pin 3. + /// + Pin03 = 3, + + /// + /// WiringPi pin 4. + /// + Pin04 = 4, + + /// + /// WiringPi pin 5. + /// + Pin05 = 5, + + /// + /// WiringPi pin 6. + /// + Pin06 = 6, + + /// + /// WiringPi pin 7. + /// + Pin07 = 7, + + /// + /// WiringPi pin 8. + /// + Pin08 = 8, + + /// + /// WiringPi pin 9. + /// + Pin09 = 9, + + /// + /// WiringPi pin 10. + /// + Pin10 = 10, + + /// + /// WiringPi pin 11. + /// + Pin11 = 11, + + /// + /// WiringPi pin 12. + /// + Pin12 = 12, + + /// + /// WiringPi pin 13. + /// + Pin13 = 13, + + /// + /// WiringPi pin 14. + /// + Pin14 = 14, + + /// + /// WiringPi pin 15. + /// + Pin15 = 15, + + /// + /// WiringPi pin 16. + /// + Pin16 = 16, + + /// + /// WiringPi pin 17. + /// + Pin17 = 17, + + /// + /// WiringPi pin 18. + /// + Pin18 = 18, + + /// + /// WiringPi pin 19. + /// + Pin19 = 19, + + /// + /// WiringPi pin 20. + /// + Pin20 = 20, + + /// + /// WiringPi pin 21. + /// + Pin21 = 21, + + /// + /// WiringPi pin 22. + /// + Pin22 = 22, + + /// + /// WiringPi pin 23. + /// + Pin23 = 23, + + /// + /// WiringPi pin 24. + /// + Pin24 = 24, + + /// + /// WiringPi pin 25. + /// + Pin25 = 25, + + /// + /// WiringPi pin 26. + /// + Pin26 = 26, + + /// + /// WiringPi pin 27. + /// + Pin27 = 27, + + /// + /// WiringPi pin 28. + /// + Pin28 = 28, + + /// + /// WiringPi pin 29. + /// + Pin29 = 29, + + /// + /// WiringPi pin 30. + /// + Pin30 = 30, + + /// + /// WiringPi pin 31. + /// + Pin31 = 31, + } + + /// + /// Defines the different pin capabilities. + /// + [Flags] + public enum PinCapability + { + /// + /// General Purpose capability: Digital and Analog Read/Write + /// + GP = 0x01, + + /// + /// General Purpose Clock (not PWM) + /// + GPCLK = 0x02, + + /// + /// i2c data channel + /// + I2CSDA = 0x04, + + /// + /// i2c clock channel + /// + I2CSCL = 0x08, + + /// + /// SPI Master Out, Slave In channel + /// + SPIMOSI = 0x10, + + /// + /// SPI Master In, Slave Out channel + /// + SPIMISO = 0x20, + + /// + /// SPI Clock channel + /// + SPICLK = 0x40, + + /// + /// SPI Chip Select Channel + /// + SPICS = 0x80, + + /// + /// UART Request to Send Channel + /// + UARTRTS = 0x100, + + /// + /// UART Transmit Channel + /// + UARTTXD = 0x200, + + /// + /// UART Receive Channel + /// + UARTRXD = 0x400, + + /// + /// Hardware Pule Width Modulation + /// + PWM = 0x800, + } + + /// + /// The PWM mode. + /// + public enum PwmMode + { + /// + /// PWM pulses are sent using mark-sign patterns (old school) + /// + MarkSign = 0, + + /// + /// PWM pulses are sent as a balanced signal (default, newer mode) + /// + Balanced = 1, + } + + /// + /// Defines GPIO controller initialization modes. + /// + internal enum ControllerMode + { + /// + /// The not initialized + /// + NotInitialized, + + /// + /// The direct with wiring pi pins + /// + DirectWithWiringPiPins, + + /// + /// The direct with BCM pins + /// + DirectWithBcmPins, + + /// + /// The direct with header pins + /// + DirectWithHeaderPins, + + /// + /// The file stream with hardware pins + /// + FileStreamWithHardwarePins, + } +} diff --git a/Unosquare.WiringPi/GpioController.cs b/Unosquare.WiringPi/GpioController.cs new file mode 100644 index 0000000..cc30710 --- /dev/null +++ b/Unosquare.WiringPi/GpioController.cs @@ -0,0 +1,582 @@ +namespace Unosquare.WiringPi +{ + using Native; + using RaspberryIO.Abstractions; + using Swan; + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Threading.Tasks; + + /// + /// Represents the Raspberry Pi GPIO controller + /// as an IReadOnlyCollection of GpioPins. + /// + /// Low level operations are accomplished by using the Wiring Pi library. + /// + public sealed class GpioController : IGpioController + { + #region Private Declarations + + private const string WiringPiCodesEnvironmentVariable = "WIRINGPI_CODES"; + private static readonly object SyncRoot = new object(); + private readonly List _pins; + + #endregion + + #region Constructors and Initialization + + /// + /// Initializes static members of the class. + /// + static GpioController() + { + var wiringPiEdgeDetection = new Dictionary + { + {EdgeDetection.FallingEdge, 21}, + {EdgeDetection.RisingEdge, 1}, + {EdgeDetection.FallingAndRisingEdge, 3}, + }; + + WiringPiEdgeDetectionMapping = new ReadOnlyDictionary(wiringPiEdgeDetection); + } + + /// + /// Initializes a new instance of the class. + /// + /// Unable to initialize the GPIO controller. + internal GpioController() + { + if (_pins != null) + return; + + if (IsInitialized == false) + { + var initResult = Initialize(ControllerMode.DirectWithBcmPins); + if (initResult == false) + throw new Exception("Unable to initialize the GPIO controller."); + } + + _pins = new List + { + GpioPin.Pin00.Value, + GpioPin.Pin01.Value, + GpioPin.Pin02.Value, + GpioPin.Pin03.Value, + GpioPin.Pin04.Value, + GpioPin.Pin05.Value, + GpioPin.Pin06.Value, + GpioPin.Pin07.Value, + GpioPin.Pin08.Value, + GpioPin.Pin09.Value, + GpioPin.Pin10.Value, + GpioPin.Pin11.Value, + GpioPin.Pin12.Value, + GpioPin.Pin13.Value, + GpioPin.Pin14.Value, + GpioPin.Pin15.Value, + GpioPin.Pin16.Value, + GpioPin.Pin17.Value, + GpioPin.Pin18.Value, + GpioPin.Pin19.Value, + GpioPin.Pin20.Value, + GpioPin.Pin21.Value, + GpioPin.Pin22.Value, + GpioPin.Pin23.Value, + GpioPin.Pin24.Value, + GpioPin.Pin25.Value, + GpioPin.Pin26.Value, + GpioPin.Pin27.Value, + GpioPin.Pin28.Value, + GpioPin.Pin29.Value, + GpioPin.Pin30.Value, + GpioPin.Pin31.Value, + }; + + var headerP1 = new Dictionary(_pins.Count); + var headerP5 = new Dictionary(_pins.Count); + foreach (var pin in _pins) + { + if (pin.PhysicalPinNumber < 0) + continue; + + var header = pin.Header == GpioHeader.P1 ? headerP1 : headerP5; + header[pin.PhysicalPinNumber] = pin; + } + + HeaderP1 = new ReadOnlyDictionary(headerP1); + HeaderP5 = new ReadOnlyDictionary(headerP5); + } + + /// + /// Determines if the underlying GPIO controller has been initialized properly. + /// + /// + /// true if the controller is properly initialized; otherwise, false. + /// + public static bool IsInitialized + { + get + { + lock (SyncRoot) + { + return Mode != ControllerMode.NotInitialized; + } + } + } + + /// + /// Gets the wiring pi edge detection mapping. + /// + internal static ReadOnlyDictionary WiringPiEdgeDetectionMapping { get; } + + /// + /// + /// Gets the number of registered pins in the controller. + /// + public int Count => Pins.Count; + + /// + /// Gets or sets the initialization mode. + /// + private static ControllerMode Mode { get; set; } = ControllerMode.NotInitialized; + + #endregion + + #region Pin Addressing + + /// + /// Gets the PWM base frequency (in Hz). + /// + public int PwmBaseFrequency => 19200000; + + /// + /// Gets a red-only collection of all pins. + /// + public ReadOnlyCollection Pins => new ReadOnlyCollection(_pins); + + /// + /// Provides all the pins on Header P1 of the Pi as a lookup by physical header pin number. + /// This header is the main header and it is the one commonly used. + /// + public ReadOnlyDictionary HeaderP1 { get; } + + /// + /// Provides all the pins on Header P5 of the Pi as a lookup by physical header pin number. + /// This header is the secondary header and it is rarely used. + /// + public ReadOnlyDictionary HeaderP5 { get; } + + #endregion + + #region Individual Pin Properties + + /// + /// Provides direct access to Pin known as BCM0. + /// + public GpioPin Pin00 => GpioPin.Pin00.Value; + + /// + /// Provides direct access to Pin known as BCM1. + /// + public GpioPin Pin01 => GpioPin.Pin01.Value; + + /// + /// Provides direct access to Pin known as BCM2. + /// + public GpioPin Pin02 => GpioPin.Pin02.Value; + + /// + /// Provides direct access to Pin known as BCM3. + /// + public GpioPin Pin03 => GpioPin.Pin03.Value; + + /// + /// Provides direct access to Pin known as BCM4. + /// + public GpioPin Pin04 => GpioPin.Pin04.Value; + + /// + /// Provides direct access to Pin known as BCM5. + /// + public GpioPin Pin05 => GpioPin.Pin05.Value; + + /// + /// Provides direct access to Pin known as BCM6. + /// + public GpioPin Pin06 => GpioPin.Pin06.Value; + + /// + /// Provides direct access to Pin known as BCM7. + /// + public GpioPin Pin07 => GpioPin.Pin07.Value; + + /// + /// Provides direct access to Pin known as BCM8. + /// + public GpioPin Pin08 => GpioPin.Pin08.Value; + + /// + /// Provides direct access to Pin known as BCM9. + /// + public GpioPin Pin09 => GpioPin.Pin09.Value; + + /// + /// Provides direct access to Pin known as BCM10. + /// + public GpioPin Pin10 => GpioPin.Pin10.Value; + + /// + /// Provides direct access to Pin known as BCM11. + /// + public GpioPin Pin11 => GpioPin.Pin11.Value; + + /// + /// Provides direct access to Pin known as BCM12. + /// + public GpioPin Pin12 => GpioPin.Pin12.Value; + + /// + /// Provides direct access to Pin known as BCM13. + /// + public GpioPin Pin13 => GpioPin.Pin13.Value; + + /// + /// Provides direct access to Pin known as BCM14. + /// + public GpioPin Pin14 => GpioPin.Pin14.Value; + + /// + /// Provides direct access to Pin known as BCM15. + /// + public GpioPin Pin15 => GpioPin.Pin15.Value; + + /// + /// Provides direct access to Pin known as BCM16. + /// + public GpioPin Pin16 => GpioPin.Pin16.Value; + + /// + /// Provides direct access to Pin known as BCM17. + /// + public GpioPin Pin17 => GpioPin.Pin17.Value; + + /// + /// Provides direct access to Pin known as BCM18. + /// + public GpioPin Pin18 => GpioPin.Pin18.Value; + + /// + /// Provides direct access to Pin known as BCM19. + /// + public GpioPin Pin19 => GpioPin.Pin19.Value; + + /// + /// Provides direct access to Pin known as BCM20. + /// + public GpioPin Pin20 => GpioPin.Pin20.Value; + + /// + /// Provides direct access to Pin known as BCM21. + /// + public GpioPin Pin21 => GpioPin.Pin21.Value; + + /// + /// Provides direct access to Pin known as BCM22. + /// + public GpioPin Pin22 => GpioPin.Pin22.Value; + + /// + /// Provides direct access to Pin known as BCM23. + /// + public GpioPin Pin23 => GpioPin.Pin23.Value; + + /// + /// Provides direct access to Pin known as BCM24. + /// + public GpioPin Pin24 => GpioPin.Pin24.Value; + + /// + /// Provides direct access to Pin known as BCM25. + /// + public GpioPin Pin25 => GpioPin.Pin25.Value; + + /// + /// Provides direct access to Pin known as BCM26. + /// + public GpioPin Pin26 => GpioPin.Pin26.Value; + + /// + /// Provides direct access to Pin known as BCM27. + /// + public GpioPin Pin27 => GpioPin.Pin27.Value; + + /// + /// Provides direct access to Pin known as BCM28 (available on Header P5). + /// + public GpioPin Pin28 => GpioPin.Pin28.Value; + + /// + /// Provides direct access to Pin known as BCM29 (available on Header P5). + /// + public GpioPin Pin29 => GpioPin.Pin29.Value; + + /// + /// Provides direct access to Pin known as BCM30 (available on Header P5). + /// + public GpioPin Pin30 => GpioPin.Pin30.Value; + + /// + /// Provides direct access to Pin known as BCM31 (available on Header P5). + /// + public GpioPin Pin31 => GpioPin.Pin31.Value; + + #endregion + + #region Indexers + + /// + public IGpioPin this[BcmPin bcmPin] => Pins[(int)bcmPin]; + + /// + public IGpioPin this[int bcmPinNumber] + { + get + { + if (!Enum.IsDefined(typeof(BcmPin), bcmPinNumber)) + throw new IndexOutOfRangeException($"Pin {bcmPinNumber} is not registered in the GPIO controller."); + + return Pins[bcmPinNumber]; + } + } + + /// + public IGpioPin this[P1 pinNumber] => HeaderP1[(int)pinNumber]; + + /// + public IGpioPin this[P5 pinNumber] => HeaderP5[(int)pinNumber]; + + /// + /// Gets the with the specified Wiring Pi pin number. + /// + /// + /// The . + /// + /// The pin number. + /// A reference to the GPIO pin. + public GpioPin this[WiringPiPin pinNumber] + { + get + { + if (pinNumber == WiringPiPin.Unknown) + throw new InvalidOperationException("You can not get an unknown WiringPi pin."); + + return Pins.First(p => p.WiringPiPinNumber == pinNumber); + } + } + + #endregion + + #region Pin Group Methods (Read, Write, Pad Drive) + + /// + /// This sets the “strength” of the pad drivers for a particular group of pins. + /// There are 3 groups of pins and the drive strength is from 0 to 7. + /// Do not use this unless you know what you are doing. + /// + /// The group. + /// The value. + public void SetPadDrive(int group, int value) + { + lock (SyncRoot) + { + WiringPi.SetPadDrive(group, value); + } + } + + /// + /// This sets the “strength” of the pad drivers for a particular group of pins. + /// There are 3 groups of pins and the drive strength is from 0 to 7. + /// Do not use this unless you know what you are doing. + /// + /// The group. + /// The value. + /// The awaitable task. + public Task SetPadDriveAsync(int group, int value) => + Task.Run(() => SetPadDrive(group, value)); + + /// + /// This writes the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to set all 8 bits at once to a particular value, + /// although it still takes two write operations to the Pi’s GPIO hardware. + /// + /// The value. + /// PinMode. + public void WriteByte(byte value) + { + lock (SyncRoot) + { + if (this.Skip(0).Take(8).Any(p => p.PinMode != GpioPinDriveMode.Output)) + { + throw new InvalidOperationException( + $"All first 8 pins (0 to 7) need their {nameof(GpioPin.PinMode)} to be set to {GpioPinDriveMode.Output}"); + } + + WiringPi.DigitalWriteByte(value); + } + } + + /// + /// This writes the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to set all 8 bits at once to a particular value, + /// although it still takes two write operations to the Pi’s GPIO hardware. + /// + /// The value. + /// The awaitable task. + public Task WriteByteAsync(byte value) => + Task.Run(() => WriteByte(value)); + + /// + /// This reads the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to get all 8 bits at once to a particular value. + /// Please note this function is undocumented and unsupported. + /// + /// A byte from the GPIO. + /// PinMode. + public byte ReadByte() + { + lock (SyncRoot) + { + if (this.Skip(0).Take(8).Any(p => + p.PinMode != GpioPinDriveMode.Input && p.PinMode != GpioPinDriveMode.Output)) + { + throw new InvalidOperationException( + $"All first 8 pins (0 to 7) need their {nameof(GpioPin.PinMode)} to be set to {GpioPinDriveMode.Input} or {GpioPinDriveMode.Output}"); + } + + return (byte)WiringPi.DigitalReadByte(); + } + } + + /// + /// This reads the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to get all 8 bits at once to a particular value. + /// Please note this function is undocumented and unsupported. + /// + /// A byte from the GPIO. + public Task ReadByteAsync() => + Task.Run(ReadByte); + + #endregion + + #region IReadOnlyCollection Implementation + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// + /// A that can be used to iterate through the collection. + /// + public IEnumerator GetEnumerator() => Pins.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => Pins.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => Pins.GetEnumerator(); + + #endregion + + #region Helper and Init Methods + + /// + /// Converts the Wirings Pi pin number to the BCM pin number. + /// + /// The wiring pi pin number. + /// The converted pin. + internal static int WiringPiToBcmPinNumber(int wiringPiPinNumber) + { + lock (SyncRoot) + { + return WiringPi.WpiPinToGpio(wiringPiPinNumber); + } + } + + /// + /// Converts the Physical (Header) pin number to BCM pin number. + /// + /// The header pin number. + /// The converted pin. + internal static int HaderToBcmPinNumber(int headerPinNumber) + { + lock (SyncRoot) + { + return WiringPi.PhysPinToGpio(headerPinNumber); + } + } + + /// + /// Initializes the controller given the initialization mode and pin numbering scheme. + /// + /// The mode. + /// True when successful. + /// + /// This library does not support the platform. + /// + /// Library was already Initialized. + /// The init mode is invalid. + private bool Initialize(ControllerMode mode) + { + if (SwanRuntime.OS != Swan.OperatingSystem.Unix) + throw new PlatformNotSupportedException("This library does not support the platform"); + + lock (SyncRoot) + { + if (IsInitialized) + throw new InvalidOperationException($"Cannot call {nameof(Initialize)} more than once."); + + Environment.SetEnvironmentVariable(WiringPiCodesEnvironmentVariable, "1"); + int setupResult; + + switch (mode) + { + case ControllerMode.DirectWithWiringPiPins: + { + setupResult = WiringPi.WiringPiSetup(); + break; + } + + case ControllerMode.DirectWithBcmPins: + { + setupResult = WiringPi.WiringPiSetupGpio(); + break; + } + + case ControllerMode.DirectWithHeaderPins: + { + setupResult = WiringPi.WiringPiSetupPhys(); + break; + } + + case ControllerMode.FileStreamWithHardwarePins: + { + setupResult = WiringPi.WiringPiSetupSys(); + break; + } + + default: + { + throw new ArgumentException($"'{mode}' is not a valid initialization mode."); + } + } + + Mode = setupResult == 0 ? mode : ControllerMode.NotInitialized; + return IsInitialized; + } + } + #endregion + + } +} diff --git a/Unosquare.WiringPi/GpioPin.Factory.cs b/Unosquare.WiringPi/GpioPin.Factory.cs new file mode 100644 index 0000000..2cffb47 --- /dev/null +++ b/Unosquare.WiringPi/GpioPin.Factory.cs @@ -0,0 +1,200 @@ +namespace Unosquare.WiringPi +{ + using RaspberryIO.Abstractions; + using System; + + public partial class GpioPin + { + internal static readonly Lazy Pin00 = new Lazy(() => new GpioPin(BcmPin.Gpio00) + { + Capabilities = PinCapability.GP | PinCapability.I2CSDA, + Name = $"BCM 0 {(SystemInfo.GetBoardRevision() == BoardRevision.Rev1 ? "(SDA)" : "(ID_SD)")}", + }); + + internal static readonly Lazy Pin01 = new Lazy(() => new GpioPin(BcmPin.Gpio01) + { + Capabilities = PinCapability.GP | PinCapability.I2CSCL, + Name = $"BCM 1 {(SystemInfo.GetBoardRevision() == BoardRevision.Rev1 ? "(SCL)" : "(ID_SC)")}", + }); + + internal static readonly Lazy Pin02 = new Lazy(() => new GpioPin(BcmPin.Gpio02) + { + Capabilities = PinCapability.GP | PinCapability.I2CSDA, + Name = "BCM 2 (SDA)", + }); + + internal static readonly Lazy Pin03 = new Lazy(() => new GpioPin(BcmPin.Gpio03) + { + Capabilities = PinCapability.GP | PinCapability.I2CSCL, + Name = "BCM 3 (SCL)", + }); + + internal static readonly Lazy Pin04 = new Lazy(() => new GpioPin(BcmPin.Gpio04) + { + Capabilities = PinCapability.GP | PinCapability.GPCLK, + Name = "BCM 4 (GPCLK0)", + }); + + internal static readonly Lazy Pin05 = new Lazy(() => new GpioPin(BcmPin.Gpio05) + { + Capabilities = PinCapability.GP, + Name = "BCM 5", + }); + + internal static readonly Lazy Pin06 = new Lazy(() => new GpioPin(BcmPin.Gpio06) + { + Capabilities = PinCapability.GP, + Name = "BCM 6", + }); + + internal static readonly Lazy Pin07 = new Lazy(() => new GpioPin(BcmPin.Gpio07) + { + Capabilities = PinCapability.GP | PinCapability.SPICS, + Name = "BCM 7 (CE1)", + }); + + internal static readonly Lazy Pin08 = new Lazy(() => new GpioPin(BcmPin.Gpio08) + { + Capabilities = PinCapability.GP | PinCapability.SPICS, + Name = "BCM 8 (CE0)", + }); + + internal static readonly Lazy Pin09 = new Lazy(() => new GpioPin(BcmPin.Gpio09) + { + Capabilities = PinCapability.GP | PinCapability.SPIMISO, + Name = "BCM 9 (MISO)", + }); + + internal static readonly Lazy Pin10 = new Lazy(() => new GpioPin(BcmPin.Gpio10) + { + Capabilities = PinCapability.GP | PinCapability.SPIMOSI, + Name = "BCM 10 (MOSI)", + }); + + internal static readonly Lazy Pin11 = new Lazy(() => new GpioPin(BcmPin.Gpio11) + { + Capabilities = PinCapability.GP | PinCapability.SPICLK, + Name = "BCM 11 (SCLCK)", + }); + + internal static readonly Lazy Pin12 = new Lazy(() => new GpioPin(BcmPin.Gpio12) + { + Capabilities = PinCapability.GP | PinCapability.PWM, + Name = "BCM 12 (PWM0)", + }); + + internal static readonly Lazy Pin13 = new Lazy(() => new GpioPin(BcmPin.Gpio13) + { + Capabilities = PinCapability.GP | PinCapability.PWM, + Name = "BCM 13 (PWM1)", + }); + + internal static readonly Lazy Pin14 = new Lazy(() => new GpioPin(BcmPin.Gpio14) + { + Capabilities = PinCapability.UARTTXD, + Name = "BCM 14 (TXD)", + }); + + internal static readonly Lazy Pin15 = new Lazy(() => new GpioPin(BcmPin.Gpio15) + { + Capabilities = PinCapability.UARTRXD, + Name = "BCM 15 (RXD)", + }); + + internal static readonly Lazy Pin16 = new Lazy(() => new GpioPin(BcmPin.Gpio16) + { + Capabilities = PinCapability.GP, + Name = "BCM 16", + }); + + internal static readonly Lazy Pin17 = new Lazy(() => new GpioPin(BcmPin.Gpio17) + { + Capabilities = PinCapability.GP | PinCapability.UARTRTS, + Name = "BCM 17", + }); + + internal static readonly Lazy Pin18 = new Lazy(() => new GpioPin(BcmPin.Gpio18) + { + Capabilities = PinCapability.GP | PinCapability.PWM, + Name = "BCM 18 (PWM0)", + }); + + internal static readonly Lazy Pin19 = new Lazy(() => new GpioPin(BcmPin.Gpio19) + { + Capabilities = PinCapability.GP | PinCapability.PWM | PinCapability.SPIMISO, + Name = "BCM 19 (MISO)", + }); + + internal static readonly Lazy Pin20 = new Lazy(() => new GpioPin(BcmPin.Gpio20) + { + Capabilities = PinCapability.GP | PinCapability.SPIMOSI, + Name = "BCM 20 (MOSI)", + }); + + internal static readonly Lazy Pin21 = new Lazy(() => new GpioPin(BcmPin.Gpio21) + { + Capabilities = PinCapability.GP | PinCapability.SPICLK, + Name = $"BCM 21{(SystemInfo.GetBoardRevision() == BoardRevision.Rev1 ? string.Empty : " (SCLK)")}", + }); + + internal static readonly Lazy Pin22 = new Lazy(() => new GpioPin(BcmPin.Gpio22) + { + Capabilities = PinCapability.GP, + Name = "BCM 22", + }); + + internal static readonly Lazy Pin23 = new Lazy(() => new GpioPin(BcmPin.Gpio23) + { + Capabilities = PinCapability.GP, + Name = "BCM 23", + }); + + internal static readonly Lazy Pin24 = new Lazy(() => new GpioPin(BcmPin.Gpio24) + { + Capabilities = PinCapability.GP, + Name = "BCM 24", + }); + + internal static readonly Lazy Pin25 = new Lazy(() => new GpioPin(BcmPin.Gpio25) + { + Capabilities = PinCapability.GP, + Name = "BCM 25", + }); + + internal static readonly Lazy Pin26 = new Lazy(() => new GpioPin(BcmPin.Gpio26) + { + Capabilities = PinCapability.GP, + Name = "BCM 26", + }); + + internal static readonly Lazy Pin27 = new Lazy(() => new GpioPin(BcmPin.Gpio27) + { + Capabilities = PinCapability.GP, + Name = "BCM 27", + }); + + internal static readonly Lazy Pin28 = new Lazy(() => new GpioPin(BcmPin.Gpio28) + { + Capabilities = PinCapability.GP | PinCapability.I2CSDA, + Name = "BCM 28 (SDA)", + }); + + internal static readonly Lazy Pin29 = new Lazy(() => new GpioPin(BcmPin.Gpio29) + { + Capabilities = PinCapability.GP | PinCapability.I2CSCL, + Name = "BCM 29 (SCL)", + }); + + internal static readonly Lazy Pin30 = new Lazy(() => new GpioPin(BcmPin.Gpio30) + { + Capabilities = PinCapability.GP, + Name = "BCM 30", + }); + + internal static readonly Lazy Pin31 = new Lazy(() => new GpioPin(BcmPin.Gpio31) + { + Capabilities = PinCapability.GP, + Name = "BCM 31", + }); + } +} \ No newline at end of file diff --git a/Unosquare.WiringPi/GpioPin.cs b/Unosquare.WiringPi/GpioPin.cs new file mode 100644 index 0000000..e65ecf9 --- /dev/null +++ b/Unosquare.WiringPi/GpioPin.cs @@ -0,0 +1,654 @@ +namespace Unosquare.WiringPi +{ + using System; + using System.Threading.Tasks; + using Native; + using RaspberryIO.Abstractions; + using RaspberryIO.Abstractions.Native; + using Swan.Diagnostics; + using Definitions = RaspberryIO.Abstractions.Definitions; + + /// + /// Represents a GPIO Pin, its location and its capabilities. + /// Full pin reference available here: + /// http://pinout.xyz/pinout/pin31_gpio6 and http://wiringpi.com/pins/. + /// + public sealed partial class GpioPin : IGpioPin + { + #region Property Backing + + private static readonly int[] GpioToWiringPi; + + private static readonly int[] GpioToWiringPiR1 = + { + 8, 9, -1, -1, 7, -1, -1, 11, 10, 13, 12, 14, -1, -1, 15, 16, -1, 0, 1, -1, -1, 2, 3, 4, 5, 6, -1, -1, -1, -1, -1, -1, + }; + + private static readonly int[] GpioToWiringPiR2 = + { + 30, 31, 8, 9, 7, 21, 22, 11, 10, 13, 12, 14, 26, 23, 15, 16, 27, 0, 1, 24, 28, 29, 3, 4, 5, 6, 25, 2, 17, 18, 19, 20, + }; + + private readonly object _syncLock = new object(); + private GpioPinDriveMode _pinMode; + private GpioPinResistorPullMode _resistorPullMode; + private int _pwmRegister; + private PwmMode _pwmMode = PwmMode.Balanced; + private uint _pwmRange = 1024; + private int _pwmClockDivisor = 1; + private int _softPwmValue = -1; + private int _softToneFrequency = -1; + + #endregion + + #region Constructor + + static GpioPin() + { + GpioToWiringPi = SystemInfo.GetBoardRevision() == + BoardRevision.Rev1 ? GpioToWiringPiR1 : GpioToWiringPiR2; + } + + /// + /// Initializes a new instance of the class. + /// + /// The BCM pin number. + private GpioPin(BcmPin bcmPinNumber) + { + BcmPin = bcmPinNumber; + BcmPinNumber = (int)bcmPinNumber; + + WiringPiPinNumber = BcmToWiringPiPinNumber(bcmPinNumber); + PhysicalPinNumber = Definitions.BcmToPhysicalPinNumber(SystemInfo.GetBoardRevision(), bcmPinNumber); + Header = (BcmPinNumber >= 28 && BcmPinNumber <= 31) ? GpioHeader.P5 : GpioHeader.P1; + } + + #endregion + + #region Pin Properties + + /// + public BcmPin BcmPin { get; } + + /// + public int BcmPinNumber { get; } + + /// + public int PhysicalPinNumber { get; } + + /// + /// Gets the WiringPi Pin number. + /// + public WiringPiPin WiringPiPinNumber { get; } + + /// + public GpioHeader Header { get; } + + /// + /// Gets the friendly name of the pin. + /// + public string Name { get; private set; } + + /// + /// Gets the hardware mode capabilities of this pin. + /// + public PinCapability Capabilities { get; private set; } + + /// + public bool Value + { + get => Read(); + set => Write(value); + } + + #endregion + + #region Hardware-Specific Properties + + /// + /// Thrown when a pin does not support the given operation mode. + public GpioPinDriveMode PinMode + { + get => _pinMode; + + set + { + lock (_syncLock) + { + var mode = value; + if ((mode == GpioPinDriveMode.GpioClock && !HasCapability(PinCapability.GPCLK)) || + (mode == GpioPinDriveMode.PwmOutput && !HasCapability(PinCapability.PWM)) || + (mode == GpioPinDriveMode.Input && !HasCapability(PinCapability.GP)) || + (mode == GpioPinDriveMode.Output && !HasCapability(PinCapability.GP))) + { + throw new NotSupportedException( + $"Pin {BcmPinNumber} '{Name}' does not support mode '{mode}'. Pin capabilities are limited to: {Capabilities}"); + } + + WiringPi.PinMode(BcmPinNumber, (int)mode); + _pinMode = mode; + } + } + } + + /// + /// Gets the interrupt callback. Returns null if no interrupt + /// has been registered. + /// + public InterruptServiceRoutineCallback InterruptCallback { get; private set; } + + /// + /// Gets the interrupt edge detection mode. + /// + public EdgeDetection InterruptEdgeDetection { get; private set; } + + /// + /// Determines whether the specified capability has capability. + /// + /// The capability. + /// + /// true if the specified capability has capability; otherwise, false. + /// + public bool HasCapability(PinCapability capability) => + (Capabilities & capability) == capability; + + #endregion + + #region Hardware PWM Members + + /// + public GpioPinResistorPullMode InputPullMode + { + get => PinMode == GpioPinDriveMode.Input ? _resistorPullMode : GpioPinResistorPullMode.Off; + + set + { + lock (_syncLock) + { + if (PinMode != GpioPinDriveMode.Input) + { + _resistorPullMode = GpioPinResistorPullMode.Off; + throw new InvalidOperationException( + $"Unable to set the {nameof(InputPullMode)} for pin {BcmPinNumber} because operating mode is {PinMode}." + + $" Setting the {nameof(InputPullMode)} is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}"); + } + + WiringPi.PullUpDnControl(BcmPinNumber, (int)value); + _resistorPullMode = value; + } + } + } + + /// + /// Gets or sets the PWM register. + /// + /// + /// The PWM register. + /// + public int PwmRegister + { + get => _pwmRegister; + + set + { + lock (_syncLock) + { + if (!HasCapability(PinCapability.PWM)) + { + _pwmRegister = 0; + + throw new NotSupportedException( + $"Pin {BcmPinNumber} '{Name}' does not support mode '{GpioPinDriveMode.PwmOutput}'. Pin capabilities are limited to: {Capabilities}"); + } + + WiringPi.PwmWrite(BcmPinNumber, value); + _pwmRegister = value; + } + } + } + + /// + /// The PWM generator can run in 2 modes – “balanced” and “mark:space”. The mark:space mode is traditional, + /// however the default mode in the Pi is “balanced”. + /// + /// + /// The PWM mode. + /// + /// When pin mode is not set a Pwn output. + public PwmMode PwmMode + { + get => PinMode == GpioPinDriveMode.PwmOutput ? _pwmMode : PwmMode.Balanced; + + set + { + lock (_syncLock) + { + if (!HasCapability(PinCapability.PWM)) + { + _pwmMode = PwmMode.Balanced; + + throw new NotSupportedException( + $"Pin {BcmPinNumber} '{Name}' does not support mode '{GpioPinDriveMode.PwmOutput}'. Pin capabilities are limited to: {Capabilities}"); + } + + WiringPi.PwmSetMode((int)value); + _pwmMode = value; + } + } + } + + /// + /// This sets the range register in the PWM generator. The default is 1024. + /// + /// + /// The PWM range. + /// + /// When pin mode is not set to PWM output. + public uint PwmRange + { + get => PinMode == GpioPinDriveMode.PwmOutput ? _pwmRange : 0; + + set + { + lock (_syncLock) + { + if (!HasCapability(PinCapability.PWM)) + { + _pwmRange = 1024; + + throw new NotSupportedException( + $"Pin {BcmPinNumber} '{Name}' does not support mode '{GpioPinDriveMode.PwmOutput}'. Pin capabilities are limited to: {Capabilities}"); + } + + WiringPi.PwmSetRange(value); + _pwmRange = value; + } + } + } + + /// + /// Gets or sets the PWM clock divisor. + /// + /// + /// The PWM clock divisor. + /// + /// When pin mode is not set to PWM output. + public int PwmClockDivisor + { + get => PinMode == GpioPinDriveMode.PwmOutput ? _pwmClockDivisor : 0; + + set + { + lock (_syncLock) + { + if (!HasCapability(PinCapability.PWM)) + { + _pwmClockDivisor = 1; + + throw new NotSupportedException( + $"Pin {BcmPinNumber} '{Name}' does not support mode '{GpioPinDriveMode.PwmOutput}'. Pin capabilities are limited to: {Capabilities}"); + } + + WiringPi.PwmSetClock(value); + _pwmClockDivisor = value; + } + } + } + + #endregion + + #region Software Tone Members + + /// + /// Gets a value indicating whether this instance is in software based tone generator mode. + /// + /// + /// true if this instance is in soft tone mode; otherwise, false. + /// + public bool IsInSoftToneMode => _softToneFrequency >= 0; + + /// + /// Gets or sets the soft tone frequency. 0 to 5000 Hz is typical. + /// + /// + /// The soft tone frequency. + /// + /// When soft tones cannot be initialized on the pin. + public int SoftToneFrequency + { + get => _softToneFrequency; + + set + { + lock (_syncLock) + { + if (IsInSoftToneMode == false) + { + var setupResult = WiringPi.SoftToneCreate(BcmPinNumber); + if (setupResult != 0) + { + throw new InvalidOperationException( + $"Unable to initialize soft tone on pin {BcmPinNumber}. Error Code: {setupResult}"); + } + } + + WiringPi.SoftToneWrite(BcmPinNumber, value); + _softToneFrequency = value; + } + } + } + + #endregion + + #region Software PWM Members + + /// + /// Gets a value indicating whether this pin is in software based PWM mode. + /// + /// + /// true if this instance is in soft PWM mode; otherwise, false. + /// + public bool IsInSoftPwmMode => _softPwmValue >= 0; + + /// + /// Gets or sets the software PWM value on the pin. + /// + /// + /// The soft PWM value. + /// + /// StartSoftPwm. + public int SoftPwmValue + { + get => _softPwmValue; + + set + { + lock (_syncLock) + { + if (IsInSoftPwmMode && value >= 0) + { + WiringPi.SoftPwmWrite(BcmPinNumber, value); + _softPwmValue = value; + } + else + { + throw new InvalidOperationException($"Software PWM requires a call to {nameof(StartSoftPwm)}."); + } + } + } + } + + /// + /// Gets the software PWM range used upon starting the PWM. + /// + public int SoftPwmRange { get; private set; } = -1; + + /// + /// Starts the software based PWM on this pin. + /// + /// The value. + /// The range. + /// When the pin does not suppoert PWM. + /// StartSoftPwm + /// or. + public void StartSoftPwm(int value, int range) + { + lock (_syncLock) + { + if (!HasCapability(PinCapability.GP)) + throw new NotSupportedException($"Pin {BcmPinNumber} does not support software PWM"); + + if (IsInSoftPwmMode) + throw new InvalidOperationException($"{nameof(StartSoftPwm)} has already been called."); + + var startResult = WiringPi.SoftPwmCreate(BcmPinNumber, value, range); + + if (startResult == 0) + { + _softPwmValue = value; + SoftPwmRange = range; + } + else + { + throw new InvalidOperationException( + $"Could not start software based PWM on pin {BcmPinNumber}. Error code: {startResult}"); + } + } + } + + #endregion + + #region Output Mode (Write) Members + + /// + public void Write(GpioPinValue value) + { + lock (_syncLock) + { + if (PinMode != GpioPinDriveMode.Output) + { + throw new InvalidOperationException( + $"Unable to write to pin {BcmPinNumber} because operating mode is {PinMode}." + + $" Writes are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Output}"); + } + + WiringPi.DigitalWrite(BcmPinNumber, (int)value); + } + } + + /// + /// Writes the value asynchronously. + /// + /// The value. + /// The awaitable task. + public Task WriteAsync(GpioPinValue value) => Task.Run(() => { Write(value); }); + + /// + /// Writes the specified bit value. + /// This method performs a digital write. + /// + /// if set to true [value]. + public void Write(bool value) + => Write(value ? GpioPinValue.High : GpioPinValue.Low); + + /// + /// Writes the specified bit value. + /// This method performs a digital write. + /// + /// The value. + /// + /// The awaitable task. + /// + public Task WriteAsync(bool value) => Task.Run(() => { Write(value); }); + + /// + /// Writes the specified value. 0 for low, any other value for high + /// This method performs a digital write. + /// + /// The value. + public void Write(int value) => Write(value != 0 ? GpioPinValue.High : GpioPinValue.Low); + + /// + /// Writes the specified value. 0 for low, any other value for high + /// This method performs a digital write. + /// + /// The value. + /// The awaitable task. + public Task WriteAsync(int value) => Task.Run(() => { Write(value); }); + + /// + /// Writes the specified value as an analog level. + /// You will need to register additional analog modules to enable this function for devices such as the Gertboard. + /// + /// The value. + public void WriteLevel(int value) + { + lock (_syncLock) + { + if (PinMode != GpioPinDriveMode.Output) + { + throw new InvalidOperationException( + $"Unable to write to pin {BcmPinNumber} because operating mode is {PinMode}." + + $" Writes are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Output}"); + } + + WiringPi.AnalogWrite(BcmPinNumber, value); + } + } + + /// + /// Writes the specified value as an analog level. + /// You will need to register additional analog modules to enable this function for devices such as the Gertboard. + /// + /// The value. + /// The awaitable task. + public Task WriteLevelAsync(int value) => Task.Run(() => { WriteLevel(value); }); + + #endregion + + #region Input Mode (Read) Members + + /// + /// Wait for specific pin status. + /// + /// status to check. + /// timeout to reach status. + /// true/false. + public bool WaitForValue(GpioPinValue status, int timeOutMillisecond) + { + if (PinMode != GpioPinDriveMode.Input) + { + throw new InvalidOperationException( + $"Unable to read from pin {BcmPinNumber} because operating mode is {PinMode}." + + $" Reads are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}"); + } + + var hrt = new HighResolutionTimer(); + hrt.Start(); + do + { + if (ReadValue() == status) + return true; + } + while (hrt.ElapsedMilliseconds <= timeOutMillisecond); + + return false; + } + + /// + /// Reads the digital value on the pin as a boolean value. + /// + /// The state of the pin. + public bool Read() + { + lock (_syncLock) + { + if (PinMode != GpioPinDriveMode.Input && PinMode != GpioPinDriveMode.Output) + { + throw new InvalidOperationException( + $"Unable to read from pin {BcmPinNumber} because operating mode is {PinMode}." + + $" Reads are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input} or {GpioPinDriveMode.Output}"); + } + + return WiringPi.DigitalRead(BcmPinNumber) != 0; + } + } + + /// + /// Reads the digital value on the pin as a boolean value. + /// + /// The state of the pin. + public Task ReadAsync() => Task.Run(Read); + + /// + /// Reads the digital value on the pin as a High or Low value. + /// + /// The state of the pin. + public GpioPinValue ReadValue() + => Read() ? GpioPinValue.High : GpioPinValue.Low; + + /// + /// Reads the digital value on the pin as a High or Low value. + /// + /// The state of the pin. + public Task ReadValueAsync() => Task.Run(ReadValue); + + /// + /// Reads the analog value on the pin. + /// This returns the value read on the supplied analog input pin. You will need to register + /// additional analog modules to enable this function for devices such as the Gertboard, + /// quick2Wire analog board, etc. + /// + /// The analog level. + /// When the pin mode is not configured as an input. + public int ReadLevel() + { + lock (_syncLock) + { + if (PinMode != GpioPinDriveMode.Input) + { + throw new InvalidOperationException( + $"Unable to read from pin {BcmPinNumber} because operating mode is {PinMode}." + + $" Reads are only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}"); + } + + return WiringPi.AnalogRead(BcmPinNumber); + } + } + + /// + /// Reads the analog value on the pin. + /// This returns the value read on the supplied analog input pin. You will need to register + /// additional analog modules to enable this function for devices such as the Gertboard, + /// quick2Wire analog board, etc. + /// + /// The analog level. + public Task ReadLevelAsync() => Task.Run(ReadLevel); + + #endregion + + #region Interrupts + + /// + /// callback. + public void RegisterInterruptCallback(EdgeDetection edgeDetection, Action callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + if (PinMode != GpioPinDriveMode.Input) + { + throw new InvalidOperationException( + $"Unable to {nameof(RegisterInterruptCallback)} for pin {BcmPinNumber} because operating mode is {PinMode}." + + $" Calling {nameof(RegisterInterruptCallback)} is only allowed if {nameof(PinMode)} is set to {GpioPinDriveMode.Input}"); + } + + lock (_syncLock) + { + var isrCallback = new InterruptServiceRoutineCallback(callback); + var registerResult = WiringPi.WiringPiISR(BcmPinNumber, GetWiringPiEdgeDetection(edgeDetection), isrCallback); + if (registerResult == 0) + { + InterruptEdgeDetection = edgeDetection; + InterruptCallback = isrCallback; + } + else + { + HardwareException.Throw(nameof(GpioPin), nameof(RegisterInterruptCallback)); + } + } + } + + /// + public void RegisterInterruptCallback(EdgeDetection edgeDetection, Action callback) => + throw new NotSupportedException("WiringPi does only support a simple interrupt callback that has no parameters."); + + internal static WiringPiPin BcmToWiringPiPinNumber(BcmPin pin) => + (WiringPiPin)GpioToWiringPi[(int)pin]; + + private static int GetWiringPiEdgeDetection(EdgeDetection edgeDetection) => + GpioController.WiringPiEdgeDetectionMapping[edgeDetection]; + + #endregion + } +} diff --git a/Unosquare.WiringPi/I2CBus.cs b/Unosquare.WiringPi/I2CBus.cs new file mode 100644 index 0000000..99dad15 --- /dev/null +++ b/Unosquare.WiringPi/I2CBus.cs @@ -0,0 +1,72 @@ +namespace Unosquare.WiringPi +{ + using Native; + using RaspberryIO.Abstractions; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + + /// + /// + /// A simple wrapper for the I2c bus on the Raspberry Pi. + /// + public class I2CBus : II2CBus + { + // TODO: It would be nice to integrate i2c device detection. + private static readonly object SyncRoot = new object(); + private readonly Dictionary _devices = new Dictionary(); + + /// + public ReadOnlyCollection Devices + { + get + { + lock (SyncRoot) + return new ReadOnlyCollection(_devices.Values.ToArray()); + } + } + + /// + public II2CDevice this[int deviceId] => GetDeviceById(deviceId); + + /// + public II2CDevice GetDeviceById(int deviceId) + { + lock (SyncRoot) + return _devices[deviceId]; + } + + /// + /// When the device file descriptor is not found. + public II2CDevice AddDevice(int deviceId) + { + lock (SyncRoot) + { + if (_devices.ContainsKey(deviceId)) + return _devices[deviceId]; + + var fileDescriptor = SetupFileDescriptor(deviceId); + if (fileDescriptor < 0) + throw new KeyNotFoundException($"Device with id {deviceId} could not be registered with the I2C bus. Error Code: {fileDescriptor}."); + + var device = new I2CDevice(deviceId, fileDescriptor); + _devices[deviceId] = device; + return device; + } + } + + /// + /// This initializes the I2C system with your given device identifier. + /// The ID is the I2C number of the device and you can use the i2cdetect program to find this out. + /// wiringPiI2CSetup() will work out which revision Raspberry Pi you have and open the appropriate device in /dev. + /// The return value is the standard Linux filehandle, or -1 if any error – in which case, you can consult errno as usual. + /// + /// The device identifier. + /// The Linux file handle. + private static int SetupFileDescriptor(int deviceId) + { + lock (SyncRoot) + return WiringPi.WiringPiI2CSetup(deviceId); + } + } +} diff --git a/Unosquare.WiringPi/I2CDevice.cs b/Unosquare.WiringPi/I2CDevice.cs new file mode 100644 index 0000000..ae4af68 --- /dev/null +++ b/Unosquare.WiringPi/I2CDevice.cs @@ -0,0 +1,181 @@ +namespace Unosquare.WiringPi +{ + using Native; + using RaspberryIO.Abstractions; + using RaspberryIO.Abstractions.Native; + using System; + using System.Threading.Tasks; + + /// + /// Represents a device on the I2C Bus. + /// + public class I2CDevice : II2CDevice + { + private readonly object _syncLock = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// The device identifier. + /// The file descriptor. + internal I2CDevice(int deviceId, int fileDescriptor) + { + DeviceId = deviceId; + FileDescriptor = fileDescriptor; + } + + /// + public int DeviceId { get; } + + /// + public int FileDescriptor { get; } + + /// + public byte Read() + { + lock (_syncLock) + { + var result = WiringPi.WiringPiI2CRead(FileDescriptor); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(Read)); + return (byte)result; + } + } + + /// + /// Reads a byte from the specified file descriptor. + /// + /// The byte from device. + public Task ReadAsync() => Task.Run(Read); + + /// + /// Reads a buffer of the specified length, one byte at a time. + /// + /// The length. + /// The byte array from device. + public byte[] Read(int length) + { + lock (_syncLock) + { + var buffer = new byte[length]; + for (var i = 0; i < length; i++) + { + var result = WiringPi.WiringPiI2CRead(FileDescriptor); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(Read)); + buffer[i] = (byte)result; + } + + return buffer; + } + } + + /// + /// Reads a buffer of the specified length, one byte at a time. + /// + /// The length. + /// The byte array from device. + public Task ReadAsync(int length) => Task.Run(() => Read(length)); + + /// + /// Writes a byte of data the specified file descriptor. + /// + /// The data. + public void Write(byte data) + { + lock (_syncLock) + { + var result = WiringPi.WiringPiI2CWrite(FileDescriptor, data); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(Write)); + } + } + + /// + /// Writes a byte of data the specified file descriptor. + /// + /// The data. + /// The awaitable task. + public Task WriteAsync(byte data) => Task.Run(() => { Write(data); }); + + /// + /// Writes a set of bytes to the specified file descriptor. + /// + /// The data. + public void Write(byte[] data) + { + lock (_syncLock) + { + foreach (var b in data) + { + var result = WiringPi.WiringPiI2CWrite(FileDescriptor, b); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(Write)); + } + } + } + + /// + /// Writes a set of bytes to the specified file descriptor. + /// + /// The data. + /// The awaitable task. + public Task WriteAsync(byte[] data) => Task.Run(() => { Write(data); }); + + /// + /// These write an 8 or 16-bit data value into the device register indicated. + /// + /// The register. + /// The data. + public void WriteAddressByte(int address, byte data) + { + lock (_syncLock) + { + var result = WiringPi.WiringPiI2CWriteReg8(FileDescriptor, address, data); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(WriteAddressByte)); + } + } + + /// + /// These write an 8 or 16-bit data value into the device register indicated. + /// + /// The register. + /// The data. + public void WriteAddressWord(int address, ushort data) + { + lock (_syncLock) + { + var result = WiringPi.WiringPiI2CWriteReg16(FileDescriptor, address, data); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(WriteAddressWord)); + } + } + + /// + /// These read an 8 or 16-bit value from the device register indicated. + /// + /// The register. + /// The address byte from device. + public byte ReadAddressByte(int address) + { + lock (_syncLock) + { + var result = WiringPi.WiringPiI2CReadReg8(FileDescriptor, address); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(ReadAddressByte)); + + return (byte)result; + } + } + + /// + /// These read an 8 or 16-bit value from the device register indicated. + /// + /// The register. + /// The address word from device. + public ushort ReadAddressWord(int address) + { + lock (_syncLock) + { + var result = WiringPi.WiringPiI2CReadReg16(FileDescriptor, address); + if (result < 0) HardwareException.Throw(nameof(I2CDevice), nameof(ReadAddressWord)); + + return Convert.ToUInt16(result); + } + } + } +} diff --git a/Unosquare.WiringPi/Native/Delegates.cs b/Unosquare.WiringPi/Native/Delegates.cs new file mode 100644 index 0000000..16f9934 --- /dev/null +++ b/Unosquare.WiringPi/Native/Delegates.cs @@ -0,0 +1,12 @@ +namespace Unosquare.WiringPi.Native +{ + /// + /// A delegate defining a callback for an Interrupt Service Routine. + /// + public delegate void InterruptServiceRoutineCallback(); + + /// + /// Defines the body of a thread worker. + /// + public delegate void ThreadWorker(); +} diff --git a/Unosquare.WiringPi/Native/SysCall.cs b/Unosquare.WiringPi/Native/SysCall.cs new file mode 100644 index 0000000..d33248d --- /dev/null +++ b/Unosquare.WiringPi/Native/SysCall.cs @@ -0,0 +1,19 @@ +namespace Unosquare.WiringPi.Native +{ + using System; + using System.Runtime.InteropServices; + + internal static class SysCall + { + internal const string LibCLibrary = "libc"; + + [DllImport(LibCLibrary, EntryPoint = "chmod", SetLastError = true)] + public static extern int Chmod(string filename, uint mode); + + [DllImport(LibCLibrary, EntryPoint = "strtol", SetLastError = true)] + public static extern int StringToInteger(string numberString, IntPtr endPointer, int numberBase); + + [DllImport(LibCLibrary, EntryPoint = "write", SetLastError = true)] + public static extern int Write(int fd, byte[] buffer, int count); + } +} diff --git a/Unosquare.WiringPi/Native/WiringPi.I2C.cs b/Unosquare.WiringPi/Native/WiringPi.I2C.cs new file mode 100644 index 0000000..56a73d3 --- /dev/null +++ b/Unosquare.WiringPi/Native/WiringPi.I2C.cs @@ -0,0 +1,74 @@ +namespace Unosquare.WiringPi.Native +{ + using System.Runtime.InteropServices; + + public partial class WiringPi + { + /// + /// Simple device read. Some devices present data when you read them without having to do any register transactions. + /// + /// The fd. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiI2CRead", SetLastError = true)] + public static extern int WiringPiI2CRead(int fd); + + /// + /// These read an 8-bit value from the device register indicated. + /// + /// The fd. + /// The reg. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiI2CReadReg8", SetLastError = true)] + public static extern int WiringPiI2CReadReg8(int fd, int reg); + + /// + /// These read a 16-bit value from the device register indicated. + /// + /// The fd. + /// The reg. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiI2CReadReg16", SetLastError = true)] + public static extern int WiringPiI2CReadReg16(int fd, int reg); + + /// + /// Simple device write. Some devices accept data this way without needing to access any internal registers. + /// + /// The fd. + /// The data. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiI2CWrite", SetLastError = true)] + public static extern int WiringPiI2CWrite(int fd, int data); + + /// + /// These write an 8-bit data value into the device register indicated. + /// + /// The fd. + /// The reg. + /// The data. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiI2CWriteReg8", SetLastError = true)] + public static extern int WiringPiI2CWriteReg8(int fd, int reg, int data); + + /// + /// These write a 16-bit data value into the device register indicated. + /// + /// The fd. + /// The reg. + /// The data. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiI2CWriteReg16", SetLastError = true)] + public static extern int WiringPiI2CWriteReg16(int fd, int reg, int data); + + /// + /// This initializes the I2C system with your given device identifier. + /// The ID is the I2C number of the device and you can use the i2cdetect program to find this out. wiringPiI2CSetup() + /// will work out which revision Raspberry Pi you have and open the appropriate device in /dev. + /// The return value is the standard Linux filehandle, or -1 if any error – in which case, you can consult errno as usual. + /// E.g. the popular MCP23017 GPIO expander is usually device Id 0x20, so this is the number you would pass into wiringPiI2CSetup(). + /// + /// The dev identifier. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiI2CSetup", SetLastError = true)] + public static extern int WiringPiI2CSetup(int devId); + } +} diff --git a/Unosquare.WiringPi/Native/WiringPi.SerialPort.cs b/Unosquare.WiringPi/Native/WiringPi.SerialPort.cs new file mode 100644 index 0000000..5ceb73d --- /dev/null +++ b/Unosquare.WiringPi/Native/WiringPi.SerialPort.cs @@ -0,0 +1,69 @@ +namespace Unosquare.WiringPi.Native +{ + using System.Runtime.InteropServices; + + public partial class WiringPi + { + /// + /// This opens and initialises the serial device and sets the baud rate. It sets the port into “raw” mode (character at a time and no translations), + /// and sets the read timeout to 10 seconds. The return value is the file descriptor or -1 for any error, in which case errno will be set as appropriate. + /// The wiringSerial library is intended to provide simplified control – suitable for most applications, however if you need advanced control + /// – e.g. parity control, modem control lines (via a USB adapter, there are none on the Pi’s on-board UART!) and so on, + /// then you need to do some of this the old fashioned way. + /// + /// The device. + /// The baud. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "serialOpen", SetLastError = true)] + public static extern int SerialOpen(string device, int baud); + + /// + /// Closes the device identified by the file descriptor given. + /// + /// The fd. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "serialClose", SetLastError = true)] + public static extern int SerialClose(int fd); + + /// + /// Sends the single byte to the serial device identified by the given file descriptor. + /// + /// The fd. + /// The c. + [DllImport(WiringPiLibrary, EntryPoint = "serialPutchar", SetLastError = true)] + public static extern void SerialPutchar(int fd, byte c); + + /// + /// Sends the nul-terminated string to the serial device identified by the given file descriptor. + /// + /// The fd. + /// The s. + [DllImport(WiringPiLibrary, EntryPoint = "serialPuts", SetLastError = true)] + public static extern void SerialPuts(int fd, string s); + + /// + /// Returns the number of characters available for reading, or -1 for any error condition, + /// in which case errno will be set appropriately. + /// + /// The fd. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "serialDataAvail", SetLastError = true)] + public static extern int SerialDataAvail(int fd); + + /// + /// Returns the next character available on the serial device. + /// This call will block for up to 10 seconds if no data is available (when it will return -1). + /// + /// The fd. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "serialGetchar", SetLastError = true)] + public static extern int SerialGetchar(int fd); + + /// + /// This discards all data received, or waiting to be send down the given device. + /// + /// The fd. + [DllImport(WiringPiLibrary, EntryPoint = "serialFlush", SetLastError = true)] + public static extern void SerialFlush(int fd); + } +} \ No newline at end of file diff --git a/Unosquare.WiringPi/Native/WiringPi.Shift.cs b/Unosquare.WiringPi/Native/WiringPi.Shift.cs new file mode 100644 index 0000000..80cc913 --- /dev/null +++ b/Unosquare.WiringPi/Native/WiringPi.Shift.cs @@ -0,0 +1,36 @@ +namespace Unosquare.WiringPi.Native +{ + using System.Runtime.InteropServices; + + public partial class WiringPi + { + #region WiringPi - Shift Library + + /// + /// This shifts an 8-bit data value in with the data appearing on the dPin and the clock being sent out on the cPin. + /// Order is either LSBFIRST or MSBFIRST. The data is sampled after the cPin goes high. + /// (So cPin high, sample data, cPin low, repeat for 8 bits) The 8-bit value is returned by the function. + /// + /// The d pin. + /// The c pin. + /// The order. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "shiftIn", SetLastError = true)] + public static extern byte ShiftIn(byte dPin, byte cPin, byte order); + + /// + /// The shifts an 8-bit data value val out with the data being sent out on dPin and the clock being sent out on the cPin. + /// order is as above. Data is clocked out on the rising or falling edge – ie. dPin is set, then cPin is taken high then low + /// – repeated for the 8 bits. + /// + /// The d pin. + /// The c pin. + /// The order. + /// The value. + [DllImport(WiringPiLibrary, EntryPoint = "shiftOut", SetLastError = true)] + public static extern void ShiftOut(byte dPin, byte cPin, byte order, byte val); + + #endregion + + } +} diff --git a/Unosquare.WiringPi/Native/WiringPi.SoftPwm.cs b/Unosquare.WiringPi/Native/WiringPi.SoftPwm.cs new file mode 100644 index 0000000..b45bc26 --- /dev/null +++ b/Unosquare.WiringPi/Native/WiringPi.SoftPwm.cs @@ -0,0 +1,64 @@ +namespace Unosquare.WiringPi.Native +{ + using System.Runtime.InteropServices; + + public partial class WiringPi + { + #region WiringPi - Soft PWM (https://github.com/WiringPi/WiringPi/blob/master/wiringPi/softPwm.h) + + /// + /// This creates a software controlled PWM pin. You can use any GPIO pin and the pin numbering will be that of the wiringPiSetup() + /// function you used. Use 100 for the pwmRange, then the value can be anything from 0 (off) to 100 (fully on) for the given pin. + /// The return value is 0 for success. Anything else and you should check the global errno variable to see what went wrong. + /// + /// The pin. + /// The initial value. + /// The PWM range. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "softPwmCreate", SetLastError = true)] + public static extern int SoftPwmCreate(int pin, int initialValue, int pwmRange); + + /// + /// This updates the PWM value on the given pin. The value is checked to be in-range and pins that haven’t previously + /// been initialized via softPwmCreate will be silently ignored. + /// + /// The pin. + /// The value. + [DllImport(WiringPiLibrary, EntryPoint = "softPwmWrite", SetLastError = true)] + public static extern void SoftPwmWrite(int pin, int value); + + /// + /// This function is undocumented. + /// + /// The pin. + [DllImport(WiringPiLibrary, EntryPoint = "softPwmStop", SetLastError = true)] + public static extern void SoftPwmStop(int pin); + + /// + /// This creates a software controlled tone pin. You can use any GPIO pin and the pin numbering will be that of the wiringPiSetup() function you used. + /// The return value is 0 for success. Anything else and you should check the global errno variable to see what went wrong. + /// + /// The pin. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "softToneCreate", SetLastError = true)] + public static extern int SoftToneCreate(int pin); + + /// + /// This function is undocumented. + /// + /// The pin. + [DllImport(WiringPiLibrary, EntryPoint = "softToneStop", SetLastError = true)] + public static extern void SoftToneStop(int pin); + + /// + /// This updates the tone frequency value on the given pin. The tone will be played until you set the frequency to 0. + /// + /// The pin. + /// The freq. + [DllImport(WiringPiLibrary, EntryPoint = "softToneWrite", SetLastError = true)] + public static extern void SoftToneWrite(int pin, int freq); + + #endregion + + } +} diff --git a/Unosquare.WiringPi/Native/WiringPi.Spi.cs b/Unosquare.WiringPi/Native/WiringPi.Spi.cs new file mode 100644 index 0000000..6e1cab1 --- /dev/null +++ b/Unosquare.WiringPi/Native/WiringPi.Spi.cs @@ -0,0 +1,53 @@ +namespace Unosquare.WiringPi.Native +{ + using System.Runtime.InteropServices; + + public partial class WiringPi + { + #region WiringPi - SPI Library Calls + + /// + /// This function is undocumented. + /// + /// The channel. + /// Unknown. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSPIGetFd", SetLastError = true)] + public static extern int WiringPiSPIGetFd(int channel); + + /// + /// This performs a simultaneous write/read transaction over the selected SPI bus. Data that was in your buffer is overwritten by data returned from the SPI bus. + /// That’s all there is in the helper library. It is possible to do simple read and writes over the SPI bus using the standard read() and write() system calls though – + /// write() may be better to use for sending data to chains of shift registers, or those LED strings where you send RGB triplets of data. + /// Devices such as A/D and D/A converters usually need to perform a concurrent write/read transaction to work. + /// + /// The channel. + /// The data. + /// The length. + /// The result. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSPIDataRW", SetLastError = true)] + public static extern int WiringPiSPIDataRW(int channel, byte[] data, int len); + + /// + /// This function is undocumented. + /// + /// The channel. + /// The speed. + /// The mode. + /// Unkown. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSPISetupMode", SetLastError = true)] + public static extern int WiringPiSPISetupMode(int channel, int speed, int mode); + + /// + /// This is the way to initialize a channel (The Pi has 2 channels; 0 and 1). The speed parameter is an integer + /// in the range 500,000 through 32,000,000 and represents the SPI clock speed in Hz. + /// The returned value is the Linux file-descriptor for the device, or -1 on error. If an error has happened, you may use the standard errno global variable to see why. + /// + /// The channel. + /// The speed. + /// The Linux file descriptor for the device or -1 for error. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSPISetup", SetLastError = true)] + public static extern int WiringPiSPISetup(int channel, int speed); + + #endregion + } +} diff --git a/Unosquare.WiringPi/Native/WiringPi.cs b/Unosquare.WiringPi/Native/WiringPi.cs new file mode 100644 index 0000000..866f924 --- /dev/null +++ b/Unosquare.WiringPi/Native/WiringPi.cs @@ -0,0 +1,376 @@ +namespace Unosquare.WiringPi.Native +{ + using System.Runtime.InteropServices; + + /// + /// Provides native C WiringPi Library function call wrappers + /// All credit for the native library goes to the author of http://wiringpi.com/ + /// The wrappers were written based on https://github.com/WiringPi/WiringPi/blob/master/wiringPi/wiringPi.h. + /// + public partial class WiringPi + { + internal const string WiringPiLibrary = "libwiringPi.so.2.50"; + + #region WiringPi - Core Functions (https://github.com/WiringPi/WiringPi/blob/master/wiringPi/wiringPi.h) + + /// + /// This initialises wiringPi and assumes that the calling program is going to be using the wiringPi pin numbering scheme. + /// This is a simplified numbering scheme which provides a mapping from virtual pin numbers 0 through 16 to the real underlying Broadcom GPIO pin numbers. + /// See the pins page for a table which maps the wiringPi pin number to the Broadcom GPIO pin number to the physical location on the edge connector. + /// This function needs to be called with root privileges. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSetup", SetLastError = true)] + public static extern int WiringPiSetup(); + + /// + /// This initialises wiringPi but uses the /sys/class/gpio interface rather than accessing the hardware directly. + /// This can be called as a non-root user provided the GPIO pins have been exported before-hand using the gpio program. + /// Pin numbering in this mode is the native Broadcom GPIO numbers – the same as wiringPiSetupGpio() above, + /// so be aware of the differences between Rev 1 and Rev 2 boards. + /// + /// Note: In this mode you can only use the pins which have been exported via the /sys/class/gpio interface before you run your program. + /// You can do this in a separate shell-script, or by using the system() function from inside your program to call the gpio program. + /// Also note that some functions have no effect when using this mode as they’re not currently possible to action unless called with root privileges. + /// (although you can use system() to call gpio to set/change modes if needed). + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSetupSys", SetLastError = true)] + public static extern int WiringPiSetupSys(); + + /// + /// This is identical to wiringPiSetup, however it allows the calling programs to use the Broadcom GPIO + /// pin numbers directly with no re-mapping. + /// As above, this function needs to be called with root privileges, and note that some pins are different + /// from revision 1 to revision 2 boards. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSetupGpio", SetLastError = true)] + public static extern int WiringPiSetupGpio(); + + /// + /// Identical to wiringPiSetup, however it allows the calling programs to use the physical pin numbers on the P1 connector only. + /// This function needs to be called with root privileges. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiSetupPhys", SetLastError = true)] + public static extern int WiringPiSetupPhys(); + + /// + /// This function is undocumented. + /// + /// The pin. + /// The mode. + [DllImport(WiringPiLibrary, EntryPoint = "pinModeAlt", SetLastError = true)] + public static extern void PinModeAlt(int pin, int mode); + + /// + /// This sets the mode of a pin to either INPUT, OUTPUT, PWM_OUTPUT or GPIO_CLOCK. + /// Note that only wiringPi pin 1 (BCM_GPIO 18) supports PWM output and only wiringPi pin 7 (BCM_GPIO 4) + /// supports CLOCK output modes. + /// + /// This function has no effect when in Sys mode. If you need to change the pin mode, then you can + /// do it with the gpio program in a script before you start your program. + /// + /// The pin. + /// The mode. + [DllImport(WiringPiLibrary, EntryPoint = "pinMode", SetLastError = true)] + public static extern void PinMode(int pin, int mode); + + /// + /// This sets the pull-up or pull-down resistor mode on the given pin, which should be set as an input. + /// Unlike the Arduino, the BCM2835 has both pull-up and down internal resistors. The parameter pud should be; PUD_OFF, + /// (no pull up/down), PUD_DOWN (pull to ground) or PUD_UP (pull to 3.3v) The internal pull up/down resistors + /// have a value of approximately 50KΩ on the Raspberry Pi. + /// + /// This function has no effect on the Raspberry Pi’s GPIO pins when in Sys mode. + /// If you need to activate a pull-up/pull-down, then you can do it with the gpio program in a script before you start your program. + /// + /// The pin. + /// The pud. + [DllImport(WiringPiLibrary, EntryPoint = "pullUpDnControl", SetLastError = true)] + public static extern void PullUpDnControl(int pin, int pud); + + /// + /// This function returns the value read at the given pin. It will be HIGH or LOW (1 or 0) depending on the logic level at the pin. + /// + /// The pin. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "digitalRead", SetLastError = true)] + public static extern int DigitalRead(int pin); + + /// + /// Writes the value HIGH or LOW (1 or 0) to the given pin which must have been previously set as an output. + /// WiringPi treats any non-zero number as HIGH, however 0 is the only representation of LOW. + /// + /// The pin. + /// The value. + [DllImport(WiringPiLibrary, EntryPoint = "digitalWrite", SetLastError = true)] + public static extern void DigitalWrite(int pin, int value); + + /// + /// Writes the value to the PWM register for the given pin. The Raspberry Pi has one + /// on-board PWM pin, pin 1 (BMC_GPIO 18, Phys 12) and the range is 0-1024. + /// Other PWM devices may have other PWM ranges. + /// This function is not able to control the Pi’s on-board PWM when in Sys mode. + /// + /// The pin. + /// The value. + [DllImport(WiringPiLibrary, EntryPoint = "pwmWrite", SetLastError = true)] + public static extern void PwmWrite(int pin, int value); + + /// + /// This returns the value read on the supplied analog input pin. You will need to + /// register additional analog modules to enable this function for devices such as the Gertboard, quick2Wire analog board, etc. + /// + /// The pin. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "analogRead", SetLastError = true)] + public static extern int AnalogRead(int pin); + + /// + /// This writes the given value to the supplied analog pin. You will need to register additional + /// analog modules to enable this function for devices such as the Gertboard. + /// + /// The pin. + /// The value. + [DllImport(WiringPiLibrary, EntryPoint = "analogWrite", SetLastError = true)] + public static extern void AnalogWrite(int pin, int value); + + /// + /// This returns the board revision of the Raspberry Pi. It will be either 1 or 2. Some of the BCM_GPIO pins changed number and + /// function when moving from board revision 1 to 2, so if you are using BCM_GPIO pin numbers, then you need to be aware of the differences. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "piBoardRev", SetLastError = true)] + public static extern int PiBoardRev(); + + /// + /// This function is undocumented. + /// + /// The model. + /// The memory. + /// The maker. + /// The over volted. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "piBoardId", SetLastError = true)] + public static extern int PiBoardId(ref int model, ref int mem, ref int maker, ref int overVolted); + + /// + /// This returns the BCM_GPIO pin number of the supplied wiringPi pin. It takes the board revision into account. + /// + /// The w pi pin. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "wpiPinToGpio", SetLastError = true)] + public static extern int WpiPinToGpio(int wPiPin); + + /// + /// This returns the BCM_GPIO pin number of the supplied physical pin on the P1 connector. + /// + /// The physical pin. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "physPinToGpio", SetLastError = true)] + public static extern int PhysPinToGpio(int physPin); + + /// + /// This sets the “strength” of the pad drivers for a particular group of pins. + /// There are 3 groups of pins and the drive strength is from 0 to 7. Do not use this unless you know what you are doing. + /// + /// The group. + /// The value. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "setPadDrive", SetLastError = true)] + public static extern int SetPadDrive(int group, int value); + + /// + /// Undocumented function. + /// + /// The pin. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "getAlt", SetLastError = true)] + public static extern int GetAlt(int pin); + + /// + /// Undocumented function. + /// + /// The pin. + /// The freq. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "pwmToneWrite", SetLastError = true)] + public static extern int PwmToneWrite(int pin, int freq); + + /// + /// This writes the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to set all 8 bits at once to a particular value, although it still takes two write operations to the Pi’s GPIO hardware. + /// + /// The value. + [DllImport(WiringPiLibrary, EntryPoint = "digitalWriteByte", SetLastError = true)] + public static extern void DigitalWriteByte(int value); + + /// + /// This writes the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to set all 8 bits at once to a particular value, although it still takes two write operations to the Pi’s GPIO hardware. + /// + /// The value. + [DllImport(WiringPiLibrary, EntryPoint = "digitalWriteByte2", SetLastError = true)] + public static extern void DigitalWriteByte2(int value); + + /// + /// Undocumented function + /// This reads the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to get all 8 bits at once to a particular value. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "digitalReadByte", SetLastError = true)] + public static extern uint DigitalReadByte(); + + /// + /// Undocumented function + /// This reads the 8-bit byte supplied to the first 8 GPIO pins. + /// It’s the fastest way to get all 8 bits at once to a particular value. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "digitalReadByte2", SetLastError = true)] + public static extern uint DigitalReadByte2(); + + /// + /// The PWM generator can run in 2 modes – “balanced” and “mark:space”. The mark:space mode is traditional, + /// however the default mode in the Pi is “balanced”. You can switch modes by supplying the parameter: PWM_MODE_BAL or PWM_MODE_MS. + /// + /// The mode. + [DllImport(WiringPiLibrary, EntryPoint = "pwmSetMode", SetLastError = true)] + public static extern void PwmSetMode(int mode); + + /// + /// This sets the range register in the PWM generator. The default is 1024. + /// + /// The range. + [DllImport(WiringPiLibrary, EntryPoint = "pwmSetRange", SetLastError = true)] + public static extern void PwmSetRange(uint range); + + /// + /// This sets the divisor for the PWM clock. + /// Note: The PWM control functions can not be used when in Sys mode. + /// To understand more about the PWM system, you’ll need to read the Broadcom ARM peripherals manual. + /// + /// The divisor. + [DllImport(WiringPiLibrary, EntryPoint = "pwmSetClock", SetLastError = true)] + public static extern void PwmSetClock(int divisor); + + /// + /// Undocumented function. + /// + /// The pin. + /// The freq. + [DllImport(WiringPiLibrary, EntryPoint = "gpioClockSet", SetLastError = true)] + public static extern void GpioClockSet(int pin, int freq); + + /// + /// This function registers a function to received interrupts on the specified pin. + /// The edgeType parameter is either INT_EDGE_FALLING, INT_EDGE_RISING, INT_EDGE_BOTH or INT_EDGE_SETUP. + /// If it is INT_EDGE_SETUP then no initialisation of the pin will happen – it’s assumed that you have already setup the pin elsewhere + /// (e.g. with the gpio program), but if you specify one of the other types, then the pin will be exported and initialised as specified. + /// This is accomplished via a suitable call to the gpio utility program, so it need to be available. + /// The pin number is supplied in the current mode – native wiringPi, BCM_GPIO, physical or Sys modes. + /// This function will work in any mode, and does not need root privileges to work. + /// The function will be called when the interrupt triggers. When it is triggered, it’s cleared in the dispatcher before calling your function, + /// so if a subsequent interrupt fires before you finish your handler, then it won’t be missed. (However it can only track one more interrupt, + /// if more than one interrupt fires while one is being handled then they will be ignored) + /// This function is run at a high priority (if the program is run using sudo, or as root) and executes concurrently with the main program. + /// It has full access to all the global variables, open file handles and so on. + /// + /// The pin. + /// The mode. + /// The method. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "wiringPiISR", SetLastError = true)] + public static extern int WiringPiISR(int pin, int mode, InterruptServiceRoutineCallback method); + + /// + /// This function creates a thread which is another function in your program previously declared using the PI_THREAD declaration. + /// This function is then run concurrently with your main program. An example may be to have this function wait for an interrupt while + /// your program carries on doing other tasks. The thread can indicate an event, or action by using global variables to + /// communicate back to the main program, or other threads. + /// + /// The method. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "piThreadCreate", SetLastError = true)] + public static extern int PiThreadCreate(ThreadWorker method); + + /// + /// These allow you to synchronise variable updates from your main program to any threads running in your program. keyNum is a number from 0 to 3 and represents a key. + /// When another process tries to lock the same key, it will be stalled until the first process has unlocked the same key. + /// You may need to use these functions to ensure that you get valid data when exchanging data between your main program and a thread + /// – otherwise it’s possible that the thread could wake-up halfway during your data copy and change the data – + /// so the data you end up copying is incomplete, or invalid. See the wfi.c program in the examples directory for an example. + /// + /// The key. + [DllImport(WiringPiLibrary, EntryPoint = "piLock", SetLastError = true)] + public static extern void PiLock(int key); + + /// + /// These allow you to synchronise variable updates from your main program to any threads running in your program. keyNum is a number from 0 to 3 and represents a key. + /// When another process tries to lock the same key, it will be stalled until the first process has unlocked the same key. + /// You may need to use these functions to ensure that you get valid data when exchanging data between your main program and a thread + /// – otherwise it’s possible that the thread could wake-up halfway during your data copy and change the data – + /// so the data you end up copying is incomplete, or invalid. See the wfi.c program in the examples directory for an example. + /// + /// The key. + [DllImport(WiringPiLibrary, EntryPoint = "piUnlock", SetLastError = true)] + public static extern void PiUnlock(int key); + + /// + /// This attempts to shift your program (or thread in a multi-threaded program) to a higher priority + /// and enables a real-time scheduling. The priority parameter should be from 0 (the default) to 99 (the maximum). + /// This won’t make your program go any faster, but it will give it a bigger slice of time when other programs are running. + /// The priority parameter works relative to others – so you can make one program priority 1 and another priority 2 + /// and it will have the same effect as setting one to 10 and the other to 90 (as long as no other + /// programs are running with elevated priorities) + /// The return value is 0 for success and -1 for error. If an error is returned, the program should then consult the errno global variable, as per the usual conventions. + /// Note: Only programs running as root can change their priority. If called from a non-root program then nothing happens. + /// + /// The priority. + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "piHiPri", SetLastError = true)] + public static extern int PiHiPri(int priority); + + /// + /// This causes program execution to pause for at least howLong milliseconds. + /// Due to the multi-tasking nature of Linux it could be longer. + /// Note that the maximum delay is an unsigned 32-bit integer or approximately 49 days. + /// + /// The how long. + [DllImport(WiringPiLibrary, EntryPoint = "delay", SetLastError = true)] + public static extern void Delay(uint howLong); + + /// + /// This causes program execution to pause for at least howLong microseconds. + /// Due to the multi-tasking nature of Linux it could be longer. + /// Note that the maximum delay is an unsigned 32-bit integer microseconds or approximately 71 minutes. + /// Delays under 100 microseconds are timed using a hard-coded loop continually polling the system time, + /// Delays over 100 microseconds are done using the system nanosleep() function – You may need to consider the implications + /// of very short delays on the overall performance of the system, especially if using threads. + /// + /// The how long. + [DllImport(WiringPiLibrary, EntryPoint = "delayMicroseconds", SetLastError = true)] + public static extern void DelayMicroseconds(uint howLong); + + /// + /// This returns a number representing the number of milliseconds since your program called one of the wiringPiSetup functions. + /// It returns an unsigned 32-bit number which wraps after 49 days. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "millis", SetLastError = true)] + public static extern uint Millis(); + + /// + /// This returns a number representing the number of microseconds since your program called one of + /// the wiringPiSetup functions. It returns an unsigned 32-bit number which wraps after approximately 71 minutes. + /// + /// The result code. + [DllImport(WiringPiLibrary, EntryPoint = "micros", SetLastError = true)] + public static extern uint Micros(); + + #endregion + } +} \ No newline at end of file diff --git a/Unosquare.WiringPi/Resources/EmbeddedResources.cs b/Unosquare.WiringPi/Resources/EmbeddedResources.cs new file mode 100644 index 0000000..efd28eb --- /dev/null +++ b/Unosquare.WiringPi/Resources/EmbeddedResources.cs @@ -0,0 +1,65 @@ +namespace Unosquare.WiringPi.Resources +{ + using Native; + using System; + using System.Collections.ObjectModel; + using System.IO; + using System.Reflection; + + /// + /// Provides access to embedded assembly files. + /// + internal static class EmbeddedResources + { + /// + /// Initializes static members of the class. + /// + static EmbeddedResources() + { + ResourceNames = + new ReadOnlyCollection(typeof(EmbeddedResources).Assembly.GetManifestResourceNames()); + } + + /// + /// Gets the resource names. + /// + /// + /// The resource names. + /// + public static ReadOnlyCollection ResourceNames { get; } + + /// + /// Extracts all the file resources to the specified base path. + /// + public static void ExtractAll() + { + var basePath = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + var executablePermissions = SysCall.StringToInteger("0777", IntPtr.Zero, 8); + + foreach (var resourceName in ResourceNames) + { + var filename = resourceName.Substring($"{typeof(EmbeddedResources).Namespace}.".Length); + var targetPath = Path.Combine(basePath, filename); + if (File.Exists(targetPath)) return; + + using (var stream = typeof(EmbeddedResources).Assembly + .GetManifestResourceStream(resourceName)) + { + using (var outputStream = File.OpenWrite(targetPath)) + { + stream?.CopyTo(outputStream); + } + + try + { + SysCall.Chmod(targetPath, (uint)executablePermissions); + } + catch + { + /* Ignore */ + } + } + } + } + } +} \ No newline at end of file diff --git a/Unosquare.WiringPi/Resources/gpio b/Unosquare.WiringPi/Resources/gpio new file mode 100644 index 0000000000000000000000000000000000000000..7d42480e286e569922da3742ffea1cae1738c548 GIT binary patch literal 36860 zcmeI5e|%h3o%hcqX$c{vDHN!s!WD{Jph>1}fC3egHvOe(8{3o?DdyKqlUb6?I5X)F zjcb(x6^aCDS(LJgYu#lPma28F3U1Z9R$s;K?)DLuW!EXYtA7?TRp7dN_)}!YRTctPRUcTMq^@)5evnr2#Oqx2PbjY0m=rTAsQ zO61}6tFa|ee2}T^NVHUT#L7DoUEQ0?o70``>nk$JiaCg^j%-M8@$%JT8I^~8)+HO0 z0r4ci@V`H@{Xf6D=_hww`0oFBZqu*5VObIGK#O+~$ToO%8oN;9}r#(p^( za|YoOpwtN`fF}YxByU{>bl5B=3=l1yvxx-mjahLdItD%7Z$n_ zCBQMrPTTm=|DH5)?yv5dcEO7qUVr+=(kE7yf9CP`JbvwjGj`uK9J%y& zyZ`NXpZrnf^7p*_(R(rzFa7q!C#HUL?=SEB%WHqK;hBefEq3B{jO7)X76b%fxOehYAralr#mqQ(9sPhON4 ze{r7tS>gqk>@CXE|8$=IkMrms<(0>UdGrH$`kxCD11r6&^U_eds_^SUC;EquHsK~Q7KQF!K^6Y;# zPrf)$UY=PJSAA9cSNg~E;ja2*QOnsuOA-Nc%CKm5#-fWt?nmdwhE91>ES#Q2Ao?hPE8P7m; zElbAYH65h7v6IACHg~nf12%YNd_zK31NHgI=5%an%(TU`iWGRa93z<#3On5?n|dlC zRM)&Y*_|EZ`dHn1Ty_q~oj_Cm+C2L1+dPj%tc|yHw~bIXNty7KGVyGEbF4O<*r3eF zW{1mx&801Blj`p1Se>fvT9E9@rjs3t-9Xj(7F;>oA`5c4bk)jm?bpUPr~#xBi<(>G zz5{LXbhbrBRAUR=XVPF&+4eM<(LyY06ryRw)9J2cBbDBqRmGx}&PrjHf}ILotx#)v zb83{|>ht595?PZ;(F(Kc&H7|2-eoe`bXRLa%5=ozDYChV)^A;qQS4(TnTD^dxt`J4oWM*v9yg2Ymd;<$IJ-g%tv|X7 zSf}cr)~f|>-Rjf9D%O|z>)AjrFcZY^X}?p4?FFk~e|XEcWh$;m7( zwO*Ob;VgwHqT z6=4>#uL^6!Lw%pm=Bh}T4MuTw`Oj;{o0p2WJaxQipgdPJ(3d-V;J$(4li$ug*~EnP zPxl=?|3}-8K6&!VToB$H3O^PK?+%4`g~B^R;q9UD)=;=76wZdi9ieb66kZn!H-y4< zp>S;|JTDZk3WaBe!qY?HX`%4sP`ES{E^^`XD4$n{{qj-xR49BR6h0OTKOYJo355@Z z!Usa(Cqv=Aq3~m&@a|A}S17zA6y6>RZw-ZeLg8#E+z|@LLg96xa6>3u7Yf&g!t+Am zs!(`lC_FtBo)!vE4uwlY;i6FZ)z?Gi9}1rcg^z{8&xgWCLg7P%Pp&BcxoPSjT=-wh zj-H&@my6W*58gFA{1T(n5JOhU0K88^KYt@KuYbsRx91|}rlGBcrlhEPXYSy|eYqmj zKiH@^srsXIvyy#|SMKaEG?YB*crzT2Zd&q?<6Y`_3^gUPwH5yR#H+49nl)zXCg@H0 zaWT9sI1Vl`{X?Gtw?j9BV_^Bv1ilr#4txt(diR0rq1S-xz^lPaz^lNu;P->2cL#Vb z^fGW2SU!mVF>sK_D#E6$e{c%&KfJDT>L0opUg?c}Ia9VXR}9~H59CfEJA=HeMCO&< zdvuz)_vj}S_8!SiD|;k&?CieWOX%zd&w~FfxDqTIhY24;rnH;jO^M!nw9LErXeo9~ z^xj-CwkE@?NA@VX?*dOAcqG^V@RxcD_l2TK_rK7A{CoH?&g&nHp;w46C!l3> z2lXU=z2pDP@!11d{yfJYaQx`medB=herxIep$Yi=6DQkWWTvVeO(u@Y_-XNJM_bhv z9Pgmh-3;&4JAFO*CSP8Lcg*p=?RdX|cf|3&;dmd3n34mIw^uyU*bHy4<2~W@`r++% zye~Q)dzq3Qj`s!e%Ic5O2}-s)Uca+93@__=54*VY3QS4N@g8)%N$?sR?|#R-0bZ@+ zZFfBGiQ4`Ug@2dhZ7ra@ygQCkmqSe5FBX-3Iv0uFezd6U6UWAzRfE4D9{#5B28PUg z`f>$LcMhn(mR2u3dTM)LZhXzwfua;`l=iE(nRYfGr;ioTr;XW}d$zzlr+!{cJG{=x zpTmzr2PX+O;e)%7YB!58-7Xl{vLbE)0y}< z4*DV4Cw~gBBFr7>A(kg4fe*hW9%Jkqr{A<7x14xyn0{0BxQ4U(a{q-LrCWDlUruR# zIF#;HE?te~EEz}AepYF}N!}~}r?koYKuKS2zsq}4>0mEXz4d6dX&CxTSHEgg@@aL* zr?QYwze3NLHA6dTFFUYz3VWZXp0^YJC1Kh8Ikd*SOI;edP&;^@cF+@|x3iAh1g*KK zw9q`aWGZce^q*3Gspl&C;#|ku=XhJ-&2YT0ibp@@zE;T;$NPJy_f2@kj`w%s(T=|X z@0EA>Y3z1-&%itGczurdAMl=ayw5rw4Wi_rs-@n0o z$noxUyi1sSw>jR&UEFuT+vIpX;%%uv%9g66-SIX$z2DK7*E(L>@lL^8;&`2o$Niy_ zxsKQFct3+T!|_@j@2BvlINk>x@897SJKoKX_fPO%nc|mUgLq#3QSKp?9Cy4GPVXLg z&pO_F9q(iC4m#dK$J+pJpW{_KUV^bP7&nUSxUn;L1?}V)uQQ&|Ha=TuUeY)hp}iC{ z=kyR?V^MYL>6`~tI6GSy4~x+IJIbE>(mz_esV}#78?Xdu2Z~v1U`y@no#<#xIOOQP z^gY=;=;+;!KH%tw(T~u^OGrz7Mq};WyR*ob{5^Dm?{AUw^M}s1 z_`|PKrug%2boVc&zpnAeh`({^;um@9AN*@a|K01uCH-gg<;tOpqKskKFdkzm<6#W@ z!WV%1@I^knFJh+lM*a1N=P^zap2k=?1wED9l;|V5!sr6V&Q(JfMa%@4*M|c_M?=H%r z#A6<#d_N^!%Enap58hW`?ye^OKRTOR3d}dF&DNt=U_T!L!JCHSFCwsnM-j7O` zzL>$L+QlJ!V~hEto(+Auo;&+;^^1V@tWg<{B!3M*mA2|s>30!dVe#ZcDZB(co}3P8 zyk-!)dl>%)!Sd}Pr3Za8zKxep__qt*JhcmGwYMF#@43*b_cvP)oT~EI?h)o`tp|Sp z+VG1z$mib|n3q(KT2t;jkGw`{kDlKKhOnzP@E~|O`HG?c*XXP6jdyQuJi1$n6X>gc zrGGd02K4_O{N7^o(nNd=>gm1kg7D*nmtZpu9*@pp@SEjhCiy6&41;_eBp=TgQ3v?N z+GOy^tC~;Na z#i6$L>p!p;pxvkB3w5hDJO$gTPmgx94!(HjIv%%iO6nZ%Y{ygEpXYdII^J3IU)8(X z|0|^N4bq-Q-Y-G^Q_620xD@}W54%TWUcfik=7{q&apc2)GY`v->fvYb{uaHUJeD(7 ze%|GYwm7wjwx%|#HvAO20j+$g4eygpbY$-o;T^;+tC9~pb6d{^n+I|i;g{rPVA)c< zFQWVV%Ko7kI&(9??d(q$0IhwwPn!a}CR>31Zt@lQ_C?MA*#9bVKManT{-HgDnb$^i zzK%`?dOPKCEomu_Ci+0m6fz#7t2n;^YdqsN)yVj80-nl7<$f{gwuEec1Do@(r#wA* zj-RKWJ6rz&_MB|DlO1)kBj6$@+wNr2mk+9ow)L@ZETwRbEVL#JEf<(cy)2a zlrD+b=eK*nPXhaa13>J(tY(2tzyS1i@Uy`5_rV7q0-guFI&^_uzzN{wtr0Vs`)+%Q za}X$9%BmV@0(2kt;JS$EC0*Ua)%{!Dv)zPjZ*{~R0-pp7>C6STE{m9%3rH7yVj+GA z13Q*S%wxd175D%&)MFnQz-|oO4w!U-*#n+7KVptya}N;1RzL9{1?JV_19%s0HVNLpc+^L)C1dqT67MB=dC0R)B|gQb)?$_ZU<(rLI%VD zez9ZP!KX-TH~#Jg4giONb>wLYaFXy2;_o878#spFrR4J!a20qi@GLsVfGqMd=o82c ze!L1^t2E$8DgQd~VIYNUEwGnx2YKrSUcrYR(fFf!TL)eO>_s*cC;|qsogxo=fmeYj z^4GZ+K8^6B*y{ye0iFeRquT>C0mq0tS7l25mLk)7XHCSsG^Om~+!?fE^((c>tllwi`(xb5QY*PB}nJ}iC4T;;vP#rYukh_iRX*^4{b8t_RcD>~QDgY@NRJ-Bp^?Cs1= zb28~mrn;EnWK~WkeaYs7Yn^O~lSyB)>%nFZ^*7KzR8;oJu?h0k(0 zD>clk(Z1Y8=zbK~#`rq#9L6&6WcZUk)>P5^1`50fxL>$(=%V4u$i=@OesLdTN-^Da z3h}3)XSlZ{S(zWUdgl&T4r=Tc=$el1^fGkIY6fa1^$&jT;*Sq}r)>4 zMUN<*p|UMcO&Q=GVI->g`#=u*_$#FGGuCd>HRS!(-%-{J%74D@@Uo+(w}0?;=uJ(m z!LhTc9@xVEhxi4lE!+b#Fa9<1qUr~a@wAP3p7jWG{V(CEt)IcYo_~{^@;ejS-{CE@6nUDKXrT;WxAbl@l||%jIexNEB~td z2W2Bl*~rFcoehe&WG!)(KU>D$j@)FGA8r3II;!LEf;A^<{5%Nu_3$6tWls0|PWS6z z>Gju~Uhi8@?{TMh1ich?it#amk6PEQL}u#y2mcYQ@xVG z5P6tdOde9?VR{tb>mSJ7iCq10JaPT}+?&%l^)UQESAL#!>DRk5x)=F&<*BKEaGUAN zy<6$j*B|9kY{@um=^o+)>|7;2X`cmOX=q(4zRF7cUsEsSXGm%8f+zp$osX)ES7=8w z;4N^w46t{`JKECQ%gYFYuice2M z9~6x*JYFtQ`Zu9lX0{x?R(7K|9i?rbtM;}-X{LDQ0$=y9u9HpTo&5MykM_t%w+>uu z`MtSHu*xE)c$BehpR9T6cu)Zv9+JW!1`GUUOUU<6S z{y|rd9`Y%qul!ayKg!_gxyC|h;Zu|Pa)0CE#fT?b`-tz0hYwn-A9K9@j;A?DYl=e8 zU;7`7^yR*bOzBK@>Fgtp>|d#L%Gk@|^LwZ(m1)0|O%KVgMz)={bCUMG)up%FrN<-n zlC0zPIiBh}=6IiVJbofj(%^VI9goM~CAE&%>v+mjmE+yxcsw4rd7`ZhMJVGbYt=7ejNT&@Q>SeK)ulJ4_N!Xxrf2Z`(d>i*ABi6Pi_D6 z;Ju3D-JjblIc@5TPPPmDkYuW7?CpWpK1DvLPcrrnZnbglwfkM^%jT!RP4IR9pvWUG z>j2R^ocvDk6xOcO0Ifx}RzCn71dg$u)q35o)uE4w2WZW!HL=#b2U*8H4?YQ~zgCG) zyWIvZBCgu6#yquO=7o||$^-d3X?=Ylmv(-{z;;AF z`hgQOB4*;B@mmYRj{+n8z25Kd@iF~W82DHyXjR(_6 zPyKK%SiBjozb}PWf7G7zi!NR>@xuMsVDm6C#h>BgpQN8C-nGz&UHG#u{sQ73asB8U z;!#Ie!aLx2dmYb%x7YEWa6GbJvfJ^jKg4ZteK6=#+vsbLp|5;wL05iUpnTZzWT$hL>)0de;-PH!t%?Xw4+U~Ewx$@X?|1AKoR@b`dgp+D{9ikm+UsQ;_) zH!U37W(VMDTu8xh2Q~p}+oj+cfcn11fvtoKL;aua>zKK#h%_}8sei9y+#CvpV7>-i>SIlioaaH;I@{7rtu2;az>_T*hp6>Ht9 zw)O1GzTb=+Q{}^%#ItJ~{JYNiF!MA%DE*P~v0nFq!{x*MFYM~xW{ke7GAbgiL8ZmL z@l}+O#sTr3cf95BsvYkc@hJN`cu~juSI1inPiq3jecJK(d0L6q1mYcZyruAr<9*NZ z^qf^=f%LxZc>E-+WWe#h;dl$-X`GPWUh(jmJ#oo?$9ux@_*q}c9>@Eli_1^$O8On| z3yya!yk5uacRa=2;&=}`9zT~WNjctwj(0V@Cda$q@%Y`0ofkCTt8E$DmexB8AAQ*m zYfcdTQ%487&*jmSzl}>gY4Cnx|Ko77LQ2&|^-VUh$YwvWHYum-7skQ4k(kcU@USBSXzh$Ze zbPG&>q|o$FT0%aj!B=0`ysEz5=KQ=Fd2U93s?rG75-3NA`d9&h~t8Fh(2?+2vvS7`y)qyPALh3tV<_()P{Iouhr?Q^!wW zUweJ|9_$(CI{R0ERX&FOeo!`A7d1HjdY9LkPX8isI6q$}J+&RBEt|F<=J{|4`lA#3 zhmM0^X0A|rQ%G-QtRXIA?3l6mbLgsGY`HNOm9kFXD+aibUu-n&IPAQM|n{jea@v*|0d~lo+h34ymYpmrk;N6(%J9QNs*4-e>U|G{s`XI z8s@3@?|6aPZ%AXN#*72-$9esFRy5eo`r!T0<7aIhXd-MTJ&;=l9WmWQ8h7hw!Q&Z; zp4+|`x)40RzCWTeUWuL|UR3eUc_4SA?809pzS=JQ>tw6`&ZC-_%Al{74&h?La}@s2 zju*yZBVslVnU6n^)0*pfwXG(ldD9U08U5cDHT6f%XzHIN|0|q*?V~RtEMBE}HCqSv z!ZYXW$W4RReD^5yI|$3KJHd=Y_MXKK_~%0Rg0&A`OWEq4!rAb?MH}2g_$PLC?a;cZ9OC<^4nWJ*4vkufL`DsK=+kc zo6#=nhjj1fOovOsjN9B#sUPC!;3dTlPjL8b@HqI@`0zaG%>zFNmQUpU#VYVmz)|oK z@J#T(f@gpagLVJ%$6yb92t18vRJ-tJJ2%)5-NJlBcpl;Bftk=ffaVChpKRhD18Hj= zTxi}uw6z{w-#)cgyhbY{_@c+w1TzN*cN+D!==Y&tW{$Z!E6@{|5XEWLff%0yY7MfN88ZJHWb!GoAZ4 z?eACr*WAnRHb`?F{q|mLtp(o$t^w@!2yxcM`wT@Ta@`E7PLfkKwlnlRbV{Lb#0mR4IF_2hl05e}wU3 zwS4?+L3H8KVs9bscqQ}ms@o&c{s&0s_ELBS@c2FC1Io`7@}oBV=ag+PX|2J|cJOL& zxa{>i&&yrecOYMeyb1h1u;PD!_<@h2%lQ57mJn||{uD*OH)QW!o7LyiDyEho=N_Pa zCNm9NCx1;JhSph>4(ebVup0>KWIy58c-|e%!PNhl`TakM{|jIS?aQtmBDW8ig5KOE zw1fP$LyhuB8<+!DU(-Bz3j20%kz$YD)R$XA+Zo?<-@v$fplQ)SWDD)Ew}0?!q_L=Z z&!F}b8jn^g6CmyY~-*`%ivd6nH=LSAPw*4+`$c+D9v z+n&gH^$D-thU1=Z!jlOvZX?GkN<8o8WVhE9kH=&w-Q7jBxR-9uq!Q(=$*%Q@Hjnh% z(#@UT4D6(n$?WA`b63pE#5>}xS!rVO5_ zrQ_C6I^KfO3YKZctj;vI#jo=&%h)`5^YEfFF}w8!?-ne2EzRArbaOWDCAz%#wN!F! z>{eop#&Jb3603wV43`rp)HrPDafXbujqQn)mt0R$N=}KV0l%zWZlHBca;$}T>uuf` ztsuI=&lpG2TC>C(o9_AqiNV$*y>ZE7lnqN}R&# zN@i`b#Hlu$hF8AXyDUa9FFn=bNOD0kp>m3EN^wfh<1D69h+}LcHYz!5SJ~RpoXJ!w z+m*gnzHJ@-^A^`HUEw(^oCu7k*K;5-NGRWqj9nJ1j3v^PjSe3Ek-wuhN0g{yXVzO! zdy=b{xf0{(o=J3Z;1D}HB*29Gm&NE4f2JdZgt5wlE-6h@le@sT%QOPe44YaU2Vp! z=QN}OEy=9)T|T1uKRfD2p6R*Ba#iu=>qz|3L4p&i-6@X~r9nN7_8Ykva>~UYjXGLC zR`ZRk#&mIcV#zoHo<7Bt7PB@ zMLC;1mL}7P8e3XBXI~SYtFVSnUxy*DJkVDdU5%}Q?wX}5mo8`IYOGy2e>EFAe{2tq zS~ELxyxWe4!Q_Z9s-qe^iKUtF(wc(b#dO!OImNIc173N%9iY~8v>OxI=0pc`Z&wT@ z|FTM|1A4gCZ1z6XonR8Nr2Zv{4uye%LD%F z9JH57uFr1NB(pJ*ZTD*CuUb*Jx?y46&EE1A-kO?~D{GcF++4xCKSsUtvbGSIs>_LJ zYp=wtWZlrhf$Pm$chvgD!976}6xn5&nO<fc{25ehWVRdqd-1>2aXY)PA(M)$r6Jo~erjE-y;v1NH z?G)GC+8WPf$hZ7h$tp!-Y^FQLLMcuyrdc#;b{eZYQXHf=yW%Q4CgjzZU2k|?)|X&? zm0uUv+r?9Ba~G+_c-KQZ(bBEOPgV`Al61*$V*O@6U!M5gs$RQ*WAg0GsA*|6$D5VB zx;T0B7ROooGrW3SkcziU6D+;@lG$> zGBTAn(E9x`XjykAtMb+s!0i<(yn44ca84u6@Jmq)gUjdAo9rSB zelX`V*s|v1truL(5k_O^=={%8$Tshm?)B?wg&AIZLTPNQ@anXKvahgUjqTNU#CbJ^ zEh61ykbVSBk#G)rbBO}cw6lH2fx>=wQ(c-RB|JkVD{jZ8ye4>Q#9<>U~f48N{trYhS{tQ zc})moLRZITTM_LEcHs>7t#o?T=aQw1mw2t+>2$m+>&ftpSrxNuE~gyF>ea1SGgj^D zXtWc(ww83rS150uHeBpu{JCEFhkKp3z~_2h-JQ%8UZ-nUQT4Q0tSLtJwTZ4oHo-cb zO0dIN@U9b2yUdX_o2Cm*er+uq$E3e;G<|PgXwBjKvGGm)7}4`vVBqHZbh6Wns=#Jn zb(PYkt-1M~xtX4DI8oFBC%jyI3xKwF$@SN?2xq?T{+qWFPUE9OG=B@Pv) zg|3U|uPB3JZ;59&#u>V!wmDZtuf1Aj5VJ4b3EQz8uPvQqvlHanFT=1zTj}UbZ~k+vn5Fw%VWCF>a*@^~{(` z1<&jX(cYG6QI^uYd`V-xZCPsQcD3N{5-=~62P2VRpUun=@s5u2Olvxk@<(EufbAmM zn9C`3kEHE2dK->yC!PoG?#TG}d)nijnprg!*%hc8IW$YN=-Jp3F4v7ML2;&AHiha9 zswdrI%P%^6t?C5Iuy?iPxw`8%2G*{i@v2qZv8S>-ldjCPXe)1Q*bfGFZ2^>T2o}*k z**=E6rM#`DeAcaAMTPER*~_ZkQXU(_yoD|}()F%OHFF~?bE|jDEgpR0-a1CvSxDVV z3ueh)X$!n^W6KSHl*~L;cx6b*_Lxe)Cv9x8yEdA%Uhm}Z-cDn}?_8C(Z?UD)#%fGf zTss@X3r%qD22Ey++@DG-H6L(u#$2_fjePhhIJ9?)RsjEq!^%J|MF|lZqm1>jW zc0v0+U0z0})JjdM;Rs_Vu(6WUOfAL?hpuAUwMgzwTYLtX=KwZb2G*s!ygW^|yh3o1 z3y%ZyjMwrffO*Dj&IZ)CWRvGQ_8p_V_Q>#iI?FpB%rh1H9#ZLWKi_a(&c2hB*A7{o zcRDQjn_ZL8JqI`!nEV#wp=L@y$ZzF=X%0^Z&jhM~c|a{t2Q&cdfEXZo2ap9yfgWHh zApUk>2e1p+4JiI&z+T`<-~b@KL%c}E`up8!q)uL6o!#QS^S>Q##FWMCRF{Vm3u ziCz?_0_Fj=KpoHktOL9r+lcI6`2l^|oS6SNpf#B|Jt3=81yfp@ScGR2@OlW8D`a_| zi(}>GS7m%mMCq6NiGX;omb*7vm)z(n?v>@b{J#MzztV5)r8YxJ-qt>GyI?0-kLv^f42N)yR7{Q@@wtQng1utud{dMpESR7XfVp_pDMpM zzQyvZF=qa3fP0c}rERM}bQYa6m(32de0?$!485q zjY@n)srpf^nYZMla&C+9EtKaQ6t_$m-1P^FTL0~$xZ=OS((6IST>uys$w@!ZqfDv0r|kH>YCG26NR0e+=Rt zPHhc0oWTp~z){E78ot82#x!iIl_GMk=mxXW(Y`!t*ZLLSEQe=1JjdZGhp$rnWh+)K zwRq{O6^?(kXcB&(S+Fol&;!r1*!L@HUJ3A|kbZ@{Hz}qG8b_x7HmB4Y%obLzT?2<+R z!vqx8cO-;)SF**e4tF}N??~vm!;Fa$qvsCWtNm})>AQ1!mQV!f8N&f+JyQ_P*@@)d zNW|!w#opqGeO58Oh!=N4xA9Oz&noo1L(eI6={bktg^(m=UcnhXThOxxKB-`$6CyTz zvNU3!JygTf^9Makd3|^|xd%SaEs~1MnTX^x=>OYG-^UBCD)RaND+FaA+|PM~#S0c( z=gqJ`ic9b_m$?;lDx&3AUztuNW?k-6rltbj=6G{U!bGMM9Cx)DKfX9}W~8LxoT76h zlOyLvE-0K5c}HYw!L-OFZ%{S*{-Lfy+X86{BeY*Hy(6@3DMoF}*H>%fy-Bn!U4^#A z`EAap^^}ZvthqK(XusyicQ;08wIbTh2(2e`ew!2K^}j;4?F$#$mdp1_Xd^zYp27D@ zcpsFmLfb?9KH}4QLdP3Y=rg(s?bAO0n~^^4KlSV2Ign4Q)$`pC*B%RPi}(AMFIUU= zzuV%|>M8y=9(~&NDEg95>j@zJm-8#S3T=<_zxU$PdLlS_6^?wZ(B~D>vwhEey2$); zcnQIve)OdBebD+&rhiS)#~1DQFnzkr(lbZKCSM+uhn{{)Kk!dIp4*Efm(r8{xuN(w zLv#V$j+TGhE+5sGe-DM|LPVk)Liz(Cx%5wj=rZ(0&j{(4UNP4G=!@PGvcESZm;RF>nj_Vg-W$?yx^k>PsSvGWOrl>6>DSL3tKSi#Repy#8|7aU zY<DPs5$)CS^tbFD* zqqO8(L$vJg2+@)cglNf6glNgD=Z@Nwd`XCwd{2m$d|!x`e9El8k*A#PZ5QGtw*U3=*>bsYsk6gu^-U>O4ylp`n*$pq`j#A`M!z$-m|nKtg!Ee4%&m{{m@!#`||T``wG@G zTInnO+L8IN)XJBH=zk#UHvCtAJcs_zcL_!={Wj8y(4^Zxyj_h*)o-u0pQlGo+Y>+T6b> zd~qKA0cgFOx|+DM_vd-?k3#Ewwb+v1mnZ)mw9b47@+b1-2lD8D$)lgiqxIrL&3{4p zzmzBcZ61B5HnHwf{@)R5k7cyScjw8k&ZC#)(eXTba~^$99=$6>d!#=vuYSLTd|#-1 z_dy5q)1-n6%=bg_<^TVJ*7{r7oWyu{EKmO1Jo?PM{&s#IeR&>zO&+}jTH|38>8XFM z&6Bs}(SMOg^X-x`{h>Ecelh+A`w!XwJaWwsI~Z3*Kc1)mHE6B>`kg=fp~L;{V4nU@ z^5~yIkM_qY=2v<0BK@e-T`KP~Xsws_vo(`H=Rs?|?XO%l#p_hm9|VXyxZ0^5}!mny>e{^!^oE>(i8@ zpUcxf0j>3E(Enb}lb=~kM0Y7YUgBU5F`ozdSxI>Z`@3@r&N9EwEB{N-*ZO?GjZc?D zYrmi^fy%EETI&Oqzi9u5=6nUVvaPkXvN_#Z&Y_0xP30VU(p`=A^_6ogD=OUo(}`Qt z*;y4yGd*UeH~%S&B{0TsvpO0##+z?zT;KJlu-S!8{ffP!)&2%wZys261f4qJ{Ss1%$2d~Y z%L@D-VEY!XF`0OCy0yK&nRDzZ{#0BuHySmRTqet5E4<>z-|oZJjoHpd&UkdiGklyk zmTYY6Nb;-l#uz7HGmXvNn+zu)QXTxhD^@Wpdd=K9r`KvEi-|@ZK;7KP!KL(OzU#~H z;2LAyot>MB5~3P)o@k7!`-MGjxsd%XG*z2WT&63Mt4MIs%9>>h8y7CG)%Ul@Cdo&bYj0j&vux>tu@d{aZqvB9ZpHkX zy2cfY7Oh&?(8&42x`i@fzgygz>9#qwr`pDTeLMWMX?;$)GgVzZ`dMQ*oC^zcV#$T} zyxp68NW3MJ8TpR5{or%hhKegphd*8&OV&v2zS$hs;)~7thPMtJhuN{Did$!bR>iaW z@uL6X=&>sGoV*XtYWrW}Zj5snwK@27_Zs_6X!{N5a72vyk}=;nZ){ZVI1Ei4ZA{rD z{nQuOGyRm1UfQ6-^uJ9Vw&*@P9cJZ!wcLG!I;`S<&N|HTzhWKc1ljYyZGPG>dyoCR zdFV^)ekNo7H{C}HO5YEczg-)`>4YlFe(apyM?w0PZNKInR`$Qs9p?Pem$bt|%FG`1 z9K*BU@D6jF2i9rl!kF*YkNIRg?q!mV3_&qIaIf!>hb=WOU4i4VL{}rnfMcq%Ebs9M zYrx|3<6#a}J^I;o_x12Ga=JIaPIce2x8EEOi$gt9y`D2*!8hvjKd~N;MK$ns^DxJK zGCj=ldx8#`@gr5+EHxhcr+63%T!T&U7529J1f7LfoaBE~-f!Ceh~j^8J)A)J)A9Br z=-qnZ4ukgS*Xx5;c$5Ej0mk^i1No)7#dZ=>cRhBcCI{3S>>DKl&(+c0_u|72G&VLY zTM&$nOB@m1o@%xqv$vx{;Ev7hVp8W5^AgCJSaQBSmpDep4&kvR1src4Fr1>+SQ z8ZXZ_w>e6usav`e9kKF6%-FNlyo;&bRKzxSVb90ev@dZ->BkU_$kOo+>HC2crCMQI zT}75tZKguAAEKlk3M*<>E~}u2#5S3Vc)LIK`Re|x=c@-{MX}b?N$W{-XQGwVlUVhw zF-DpS9YbVZHP&`>*1d#)BPnnB(z8R|`e2!6^!%p*f_Hn5T$1U!6wne;dV21VLf{b& zF0JEq=|{(bo}L%=qBq@2{6B$SEhO*Iw9gCm9BD6lx*Za1d!#4ObBMD5rK{&l(_!`k zT5b}>>gegsO@Ov!($llfy{s?wj5D~TtBZH{k36T;^QdV&F!(gQAYH{5+y>~mrowto zwFkXw!b(>>`KF7b#70l6#MASulTI(NCp}y-w*%6f59nEHlqK&gI3+zj9SZce6PB+^ zN9*eu98AOH$Q7h}7o0%Pe@^P(!wD3nCs5u3J?($yv6!#d!$Nlj_8x)`^mJchd$sR( zzTRIsJ>^&X*B$5`=4E!l71;Y57}!gF0vv1PELo#(PtUG(1$tj5taN2h_t!em3uLfH z!rvl%HlQlieYE{uqcT`lPw{>L=q#V~bpQ6i8|eKTa-}Oh-IqCto<}&i0v`ros4k?Z z=hMg0n?gkKbV*P3tLIaJJ=>7kc+nh2F6jw=0R(!wpH}*z)7P(_U&*f0)xK~FdM5}6 zmt?wr2gqJj1ef$K^@H}WWP$?zU`yLUdtHxSWk7(Xqs!|0LHoA@y~Mlv7ULZG;d3_ATpj>ScvuTlPJ2h1HZ3@->Fq$}QiKL1oYuVJmKLjE<@d<^=4< z9BV?*idc=I5X}T4PlnH!loc8G#3<`xU!zqRu!AeB?8qpw8Y`I47_odg)>8wlSytXY zD;gSKaDCOVH?J)lXSHMv3D!meQ2OCk@iLh(g)oh9J3+rY(!lxmPX3+liQnbHh2HIn z&oKCxJZZDQCD=KHc_zIIIG<2WSV*WL)Dr3m4TO6MO9&4T+}~1=e)g ztcdwN0$fd4Ls(0AjL=5VZyn)D!c&B=5%hbSuz~Pzgc#xLgl`bON%%J5JA_RH{k~85 z55i``3k3J~qCtLO;17XY-Av#%5B?H(2jNx14np12Kka|_jJc0}F?ik`Z`{A&s_&ok z?z{cB-2L9vOP>1v1M|0CdF3B2d$M#~aQshmS9g5;;A;~ue17pKcm8Ql;;tXv_r1UV z_lz%Jz39hlHy>U7{U5yl`R2OOU&~7l{PxSUuKDGW4_Z%q<&QrYbjFiwht5BL&t1Xy z{uKJt1-F;KfBBm?4JfSp*1(a8{(Bbu{>`iW|G4WPxqquETY78q&$r+D@Zcl{?#jg``2iGo;`Kw$FICndTh`3 z!rO+97*zB^Q@`6TyWzDJJ##-PvhTla*&lB!C|dDg=QZ!XH@tRTpUWQq@-OqR&;8rv zraRU=dDXAK@_yB$KM(#(=;u%VeV2dkb-(_``A3^BT6*KNkNd{Hvh1~s@2G!p#>Wlc z_`#8HfA8g6pV;<;C;oinp~44~JI?u^FJE`bPZs}l-Oi^U_~wmAE-t(1&Si5p{Ic<> zbC%8iQP#!Zshqy{g^AA|m^$Rl8=n1O$eb_zcKEu+|GMnqz{D-DO&z-Y#{U0x_O>k_ zZEqa*m&E&vKbev9(>YJ?d1~lCe$zO5(u8mJegA(}bme^ehl$ZgMon2$`$qqvLwDcy z;ExV8ymWqd`Hf%M%J}@}-|HpmG{4o?L6*KMuOWYtZ}s>b)cI!+|NcCkbv*v1oD=i& z(nJme=7f%414``M*Uaa!a$XOH>142Pj3O`d;P!SgERPl zBSU^iGxGaqwExqL^1sN4Kao*?PzFCQWt4xVmtChnFJ#o;lcArNGxEQYQGb{x9s>Vw zPh9;!nh|f!pm$nEe;>`r|7k{iQ--}IGxYH~`i`)W&^lp|)sHm>>pT_#8W)QH0{OHu zq03Gb{}uTMzie5BCjTD#UpULMMw|HKlv{s|Wra=o6|}dNi8w_5K&!v?0PU4k_O^E? z^mnt6IBe?2sP{U=q9*aT}iIco7!K>l-{{7UGR)b+Oi6#6WK zUzc7f{EwcIUh}Db1@bCfX<1tgy<5pIBY&ugKTUq*G1ge7KQ}?Yv^u@sQ2jB`-!jg! zwwv;U$*-PcS<d7 z(W|4qRTI+dF~y%Ho_9lU{zUj)-@^LY)E`QI1^l_~&w-zPp8h=^fZl7i6`}k<=CiL$ zzVP2+>c0)4t;jde$uF?VpjSZqn1s{6Kal?%i?q!q{(Jb{cwaAnhv`r4b?G&g_`Qtw zHhJ`QEA4H#xtIS9+1T5xv>#Uav&dih1==w5vuQ8zuf6U6kn)AIEen<$emjJ*xIqJJgzXO#EW&xihI=+h-9e=+s*s6W)iFDL(CQ?Gs>g3k20 zz4kSf{4JjT)stU)3;ZBAwKtjk>J`2G*tFjQzir4#`IjKS=jQdcAA{dr9{qd*y$v4v zH&Jii2;@OM)&EfPsOara6ZzY(?xpt=%0*FL#Pnx9@$DY_S_*$FS%jAuetrThyp}$i z`18OGZ}g7G5b`O-o~Y)ue+~3Te+3&b@p|}g^_0Jk_`ExixyerfUzhzGeKaGdBNHrZ zr^z2gd{TrqO#4yl@ATCFfcECG*_dX^ucTZQ|1;6Vqtve*-^hC(rS0F#g zI{aTsM>c!pdk^upM|=I7{Mvd?f69>ezH<08^=CmS#KxK87vtfjQ8_#f!! zCj8fS$|*jW^v1E!F!ldc@>`VNODX>h=@a^pamueCK5bSnKffTo&BM>{(MJG#a{coc ziLXT1uDln)S0VPW(a_tV{ymOOoA_q<%6r{*{H6TCT=<YCZ}XU?r`XsB9HY28~z ziMi3L8_KJy7u8i-i|ZGfI&*3lHPj!^pHkVdsBYo-nu^MLtG4RK+NzpK<@ePrYOv)7(5<)XD(|gYSYgefsrpLgRa0c<%-Ie1)mF}&H4_GusPb;}6Dnsd znhW7^HRW{`Qz{p`=uH(W*-%q$-CJ7~ty(y(Mon9_^X{wfjn!8+M9V8C)Kx96v}k{9 zb%RxV?}Dk74Y$CqHJ1uVefE43j<(uY?vhed2b&Akh?=aZnp@RSUOffnSk?8FmGcn;s7b!l(Fbg$k#ba9GuJ7bM$^gO z!sG5kfgYmXxKmmQBQi^ju_h`g$yF{_&*Q z^c8kbEU{>zQ$`G>+r6b~b{!TmyJlfU{X`TkZOIC2=2XCx!xZ&?}BLz zo#S#un~qjp6iQRh3uYra?Yu_Cl-)$)9x%? zYC?HK`PjwyEt7d;rKvTqYEHvV3{t_{a8dQM=T%n7Q0*bNoE%81@1Zl>JnO>uPQxS-=&jNarV9xrv% zi1CgOa0{6Dl*+lIGRu%Q{0eVkdL(#iItiIo-NX~?I$Ss&Vov$&UMEQNY5v@CTwlp% zrZydA8vB$7mge;-1JfAi$7+- zOsiz}n;or_kHs-npr2U_YKM;sr&+{eXV0%+w7{CzuxP~aigY3~A^%$tA2n-)DOfkJ zAv`jjT~RmNL8dxmh(W;=Tvs`JvE{4_teI~9t;~Z}3pE-VaA_LM6%{Zxrv~-Ro=+zJ zd12*j#tlP*lp5xc*)EMaCPx-4HRTm|Mysx0 zf^T$uW?gv|16#|J<7*h#Xi2bcoOIK;@iRwUT6!ECF^a{O^YdAL{!9NWOlPj%6rafp zIHj_jf72y;|Lv`<`rcTYayDr`OLg7<`W!E%(k%3x9F=n0(=)CA|NM795(pAPgo3K7 z{+!BMBg{Frq@ZU{C+26JkYDpp`2kP9&pHcQsvjn-T)U1Hd$u*&q@~;Qd&=7u+Z(4m z!0_Wcp08(&XOe!zK%Lbqt}_n7Rs-7%TyNk817ikmH1IhCHyOCuz%2%DHE_Fu9R}_+ z@O1;VhgbW%4cuek+Xn76FmB*J0}mK@(7?k69x?E!fyWHA*t@BnfPpy%YEP&1pn(Mj z78+P=V93A{1BV(o%)qdLr3Q{NaI}FD11A_b(ZERtMh%>1;B*5k44h|RwSlz;HW;|X zz@-K@8rWjsN&{CJ*lJ*#f$I(2U|`I^jRrnv;3fk%8@R>5tp;v4u*1Ne2EK0KE(3QP zxW~Y^4cu#B+`xSX9x(8rfrkw|V&G8&j~QtFm(jO@IR@q#7&NfJz(NB@GcHb#Syn`l zu{J@lALk~5U+2t4kcXpDK_1Lb6CBK0fFS3gGXy!uC=9rwcrN&Cpd`y3Fgy3!2*sD1aVZ2f}9_;2%e6A6Z}v7k>DBlBS8jcn;?e> z>jinpy+QCS&Nc*#IIj_`=WI~$+ngZ@a_+TR@FmV11<&T3Rj`rsCBf%8#}(wfY^UHi zIBOGJ#+jmE2mVN~m@^E)CeCUEIULz5*v46p;8!`H53;qZC7u`6C|upa=fMLW{MaWR z`Frq#9(7gV@Eym_uvOT_&yK5*Mslz;JZBd zP7l7_gKzQRn>_eN55B>Jw|VeY9=yeaFZJLJ9=zIvS9tIl9(nf`9Jj#|A>zVSwvhV1ugM-cmRRzPZ8Z=kKw|Fseg$X9qp+Z)X(c!6IVM{ls-f@?4Lv*M?=#Jx25bL5P5x{_C^KUQ(W0i*g?sz zEp?c*p_Em*5tMh!ew}oiHhv4703Xxf;}~neVgJJVgrK%^cHh$3KNw2{C^wUS1j5gx zCL)K4w6SAAEb-WmSL18*cf_suGpTW(FN>E(+Ee5GW%0ibjwN3m6HBbUG#M}HXis$v zjwRYoOT^WO!=YV?qt>oO&hFKnl0)sB5UHgmQ^$le*NLL{Z{yK$0o9ztJCqM{kzqj>SH+a(t$jnXpj9L(mGak=0sO_ z=7nR4f|6%a!{}F*g^W8^r#^xw*}CEm&EVsyeB>@Y)sVMkt!>q!ryf6RdEl^-?Wx1? zC0Ui!Ys@^J>QlQWPO0Qlcw_~byOg#MBj=oa`1ZRpToeEH-+Ml=%A%b=qRf#g?Wz8e zSRyOBCeAjsD=V$@#9wGreP^qgn~ffpU}xAu;zjD_U`xvTQ%_IP)($)CL>ylpU^3k(MMc z|F1ngxh~IaD~qh~yAJ<|y=Wf%+TkNkyb>O-rVizc0<>jES~{&zYbsENEQE)DooYtU zFNPmW_;T};M=56mS-R({4}R+S=@U!(uUU~NkRg0p(~?%%ia6WiqS}FJS*d^LoBm4| zGd$_%m~@{N%jf8#Xu|MbUqc#wYfk+OekFX8bk{ZtZQ4Qp@*!tKFV90iOZ4GQvd0en z99!fb#%9I8`1<>?v|f}a9^Q`14yPq$OO~}f^<~Kt+B+@d(;a@FNV#oqCOu%VSM*)mPp_s6j zGlq$brvtzkp@q;uXrtUx!cq2idBl$Z3yALm9%G-liLjDzm=Gc@rZ_MJobmUbuEV!o z^8PA~!`fKF*NDDbk#SipaXT{DdPaL{Yj*E=+(AD#eXBi{8$)iJ6n$jkTSO zwLm!1>5sI;H#46FNSkoFY%_*s#F{Ou*_mfl=LRFoq0qL&nrl~3?jgd;loftBwMxEq z%}WQ={#BF_y^DvS=fK&?i{HTiU9{B@xZ-|N$n*EztlItK*%(Qjx4q16E` z>*16YZ-!>tPaa^**wNJoF1{j`*c50_4G8=33v1$~@Z+|*o^*HITuOS3F|x9t*AJg* zc$dFu@T3nk>1V1>$keBPiI=eH7reXgW5)4l!021{sPJPm+e2jvSr{N$4MG-)}W zcK`IZw7t@9?h)o9$yfg4kV)s*F*oi>-*3`6Zp_`~Nl%z`jv0#_-mMdPf6DOg)0&9> zIXaV^Lus9@CEudY%2)r7=ZDD;J)C-&{4!5_KQ--rgia=T(tl#oCErn=^dFmaj$s{} z@wsh;;6ZI5-&&W)%sgaVMczTreELm8?~k(F>PzdeQsdv?v%niY^p^M1%dPgLKWNf}^wST0$x(fE?8faY{kfcWj^`hjIpsY% z8Ah1`5AAt|_AvTpdD3T@bZkQNw$nGYmzJ@SFFGuQ-Y(`e+2^J7XQ#n02H$RQ`GYM6 zzW{s_bDnFH6HPnP?Rro8IFo*w(XGpaGj^m~`uoX0db-^7v@9f>Ufx*abxripyu{O1 zsVDtHPg}kE!`3BRW6QsXmNN#Bz3teNWMbNamtT{=kGb0I*C0bvw!GVuo@de-7y0n) zN1lG{CJUXO4)4H+bgB8mRaL z@KQnvLFG1)wt=upd4y6z3E{a0>MUknChQ_CAAl z5kfHBp5pko)7e94U2Z?VU+eG48vBI20#9BEc`ejy=}7NAv{wm`@5i^>tO=>#YWF_+6#V$-;PBs$aCRvoPG8%o_gH{l3tK{Z{BP>~Ko5w(+GL zbd<30o!aBdHaq^x9)x{ah?{Z-qM>u&mcfUv~V zH}R*rNA_4jy0^d6iBE?&rTIdvDT066Ukop!*}K~IWBbGYSfURyl3dhB*0#CyGkF-> zu&ngnQ~Orccjfdx<7&%8wiSh^ylVE@z+>n?Y|3TIOZBxzacG3vvqJD247H!GHL2Py zf!0>)H#PCf6`K`ncza5+RrxUytJCctexT@pX(I@Yz}Uz4??q;IG?uu$$ofG06{{?k zc%8H#6Tj$z_Cpun-F|30@$JY!at&CGjvSU)yHlH>RnqYoy1gT@F-{-LV$WLVt%_TT zRr_0uvb*1#zqPy1{Lgh~J+PwtwFh46w!)948m;`~G1gQ|pclOQ%x=jv5LytfM7%&V0`Q!FyOZkaE|Sujm~H>#pZkv8#br1b!SH>rBMhLyr7rNVYL0LHln6 zdhzd$GG+cuU;NC$(!F6Y(i$W%+JU-r*Tbuq-DL66@4h4W!;%z zUiw}Qhk}3(dIQi=-x1KUXDbq&Z+dSp$==GhV zam=?fQA&Ow+Cm>%kj?u2;?a>uq&YQ;yu(c|9hdR+aul$IXI`%pHo#{PyB2># zu_cXJ)~&f?!n>0}?4psrNQbVyYfcTS9DYmZ3-hS&=$QIehxV~=rN?r|-c+ABavuKJ z{?meX{M^wy5(7s!C#>Pi_Cxms`%^m~z&%C=-6y!Za~|T7ecpI+V|H_Arc>bKvzTeO6xs^iwjr*vNSYi<6x4QC^4=ExyFjEq3Dck$B-j_Zcx6rc2E{o@IeyFn~(c?}0BE8J|U-=Ee|noHLdp&GD_n_oV!jU3o=0+mXDY>Q5m< zwrlQ5<15A4S6?}U@^QtIJlBmLb-|72HA|LCXuwU4z$5Ea=dr_JVj227eIz=Jf7NFHJ220hocIglBMM#7ET+Ao>_dz3lk$x5eT zPK2=<)gFAq{;XhjJiz(Z&yR9m$yv{G+M;|;u;a-T^GetD2W@K}b65f4=yKa?g??)= z-G*#Neop$=*a(K!rZiTIgKJZP@VHL3Q`)hXd0?5wBYk->6&zU1T);j$81ws?(^?`; z37rjS9HYwvqsrhD-ZUR&(Py;K6%4IQWm%8yUkZ z_V6dWl&|eiT>KYsZW4w!`RP!&J&U$2##$EhU7tT-KXx>pWWCU}6y6KqeHw5*zE^U( z(uyTN{0q-H;Bizm&9}}PbH?IFLh)qIdCjT7DadFlU#qn#8@9T z#uC?{g9elD=%8GCfyqg(vf!^KC!YrTfE-okMyIk@puakcDCBG~JCt6-NY1~budkZ^ zuH&;Fk3e_mY^`C^V}-*MgsM0z`KSTPTE+;r*w~R8MqT$T|WXHXnw&4ojHj9bEZ_(GLClE zBIC%Kcwf%za-y1#7A3ERzk(2RH?(C>f0X<=dy_ozCGSjwcliGGrTdLe_A89+MK$NvN=N>TS`r=%B{8()X?jNBo=J%2ndDiNDZ_3KKlIXpE$sanJIB|f`^k6n zHcU?Iya~H)qg*J~tM^>`^Ce^|$hb@X!J!*X;vW(@tev;?^nCXpPFic~=fLUI&o%AH z?#1UW(zQksKP})oODr{QR)7zqj><`ouaK{O%=b1y>z}s+7F@Ap>)&!v!p#w+Gc2Lzwl|a{m||(wATVx zX3$tF5rrR!hC zo?o@G_fhIU<^1fw>~L%c`hH96H$BJA!rqC0gY*~G9(#YMJo@auRL57Hw=C`(X?!OBvY9ol{N;E4 z%rhqAFMkA&F0S_6c~5>)KEgeFk?&_rCx%9`Uu;1>FoZu3vQ{|=J>g0Z5!c#u|DSuh z-d=2570eye!o72>*2D4>tR?Zklan?ykyVlEe49ENHydwjiI-UIsdLY;K4^wN`HY=G z=3UDC8)fpylYf|sjfJ7#abaI;UIphfv~vKvlpk~aVn!ddAJUr5VlMZWJ+^-*de^*e zh1WS}NaCv`)RsCAem0S}DVRQ6vO4G&=R^zXpH&P09nGm&a2Y)Ce1)`4^uL7s82kK2 zYfw`2e*<;1K9}|<)$ki)eRWsKvgF1Pb**Oh#!dK}b#lK5`lFLws z{*>f{$ltr(kI?5J{?(Bi?TIh;md<+LVm{FPHkq~se+xV?TspYjnKQCOf7(BqeWlL9 z_K;o|`pJ80i}91xn$|b@@%_QcKTY^1H=DiFlq6?L_-N-`u25s}J^I5qN`9X70B|@k z3pfdw4ZH>D1BQWqAZx3_Q6t`hhpEo~sLY%t59??pZAS77@KOveUi!pY6HVT*U-~;2 zKJq6ohnH3RucIHDlZWCzWm}!ddmL#RZ_q!!hf^PvuRWYQKW>BHBj`r-ow=%mzT%I4 z!&~z?M!Q6XWcdS z@%`JQjNh{CPWczcJ@&&hx!PE=t*klqOZpmPTn)B3y9lyZ2sXv>pUEZTe!+g_iBx`= zI~3Fjut$H5vZ||iTWxb{hbJDmr+L4|*UQL&{fya{I^(eL_vrN%%E|^bF15bXzENkV zs&m{1;Poxax@S0HWHs?VR_b-kPrxmAaL>DpGb-b+-FrL>(3AYXBjb)|QaiYt(|Z1n zcnF#Nb%36wtcj1msyx0Se14p-(_t6m<6i%qw*>kbG^e{Xk&OjS-8Wi${wwhkXzt)1 zkZ6t_IoqL`lR?wH`?K|4zL6M;B}X7nE7X{>!cES;YCF5g1{-Tekw64?G zFJT_SU+4SKF>+3nXx`#^te!>M=u9>}6xpvto;lF*N7n7vUcl0`%i5OHmER;;YrQ_Q z{d7AN%lC)Zoyqw$&pF!Y`_$y~8Vh08kIY+Ia~6UJgX{@d_kEqZdB}3>c-E<*@tuLt ztF*~}oaY5vBVrqV=Ggo|xFt26vO0TUU7ov&wZ46EOQ-sj$3Az*Z7rPz_+`yyqL)Xz z{*TtQ-iLqhM_{7r>PyHWG!U{0S%iBC9Mg5t|E`4ujvl&tu!%7AeM8ds%_g!RD&gKw z3Ht^0_fGY7P;l98-QW zTE4~Y-LWV9!P!k_WqHy-mXG#OGPu7E{n~gx8~F^?B@~cRFF6R zvb1eAV^=}a3nI;_dE`wpbJT0lx{3Hv(PYl}FQA@1x@&LgL1h?QiJ_G1HsyW-^se<) z?j=+1T*~b;<$eG>h%fNcd(o6DpxisA-1mU5XO#PnDVIyR-KN~Lz>OK@+D$o&az8WW zo&+{#lzYOI`?H=|;$ucFWE5gldR7?$>ONgTEprNMnFyhgizL2ap{2ErzN=F~s3uGz zG!RxX7wws0S!KWpgc;xwjLr@7!1vzG+Me_vYo1-SKRT?tnr2<-c}~L^E*eUnbSPgS zc?Qr|u<@k>u3w)){g3HmV^w;cwQoLio}n!{pNh7|*{9LI-q0>Fv{yn~-=&FQLx;_J zZPa9ZE@wcR4|T7r1)qx;a7pYH0!(aPE+TY6V%Jd9*P5c7# z9Df^j{p}OR-#&?*PfcEeO!`wszVbZU*@b*|xv~r4Q_pVh%nLSk?ikbDS-=`N7+#jz z9Az(9o8BY$FJr#s%uQ#Ofe?Er=1#4#+MwxU%m_b2^x5-A=ug1-zR

doaAwQ zZp@_dxT|PAXCv1z&)hsqeY%cD%$yTI4c-p?tM1D&%evyi| zTy$OsP6e(2>Rc&6S$OKY_NwuneXkwgDf!(_{yy0zIC9I~Yw)d<-!qo|7Uf4!|9bLn z2Kpi~Kj#fa+XuF%R-#uu50PK|KF^+HD}gf3{jjU=k*DXMlI>XROLq4CKeE;`_P3UN z7rz!<^L&s!eGcoTv#pNgnb++|+8w)+6>*+LUe}z|x{tFZ=U%RSz3ApJpKeRMnaE?_ zyiW8eqrHRjh7;FbUuoIgKPWKiTDM$8UJhgMQlQGZy65>oQs*>>Bkif50S^L^TURdW zuL9%5b+;><_{+dO#J2-&;@W?_P8_*)X&>vH(*QYd>dGSj1z=y`W?+BdzXLatzX|9k z{$1dD;?DuwfZqc4BYh*V5Af^2M$&Z_HGufP0d+?_cERVY_}t4atM9c|$ImXY`lDbg zrw>oLbF9#_eXK0SIsLGKN^WdnH`6xI>PuYTB|M!ooI*nILf-Hv%}?k@7(mD-oJNo# z`v`f203pYT$C9)E*weN3PwZ#R`qvKWT>H(G&bF^aw(dGvV^=o92u~oZL|^ixdkfu2 z{@NE^3?1Dq4^vk6%JqEp`2Fw*>rLvrcf_@S(la)#r{)=2qB9o!WcSQ4)}8G7I^9o6dsw->5j|(it~)bts6wA(kDFVj4kQ2(raDphx3bQA`n^qP3Hb> z(v9XMi+qh=_t~?`Pj|}lYyrEMjzmZ2Zn7bNkU7p;-FX9LT>a!RKQ*#O*WA@;H^<-n zi05$gnNKp(?<0MqNw>`%DffQVLDDtP9Z!FyIj**~&&ui8z}=nIshl|c27v_C>kmKT zP4>y&C{=)sYF#fM#l9(36n`_73!mDX>G{YW+OXz_BC>mbhp%%Ow5OlP{bbpESbN>o z%)x8ogC!SqDciT88Fl25K1cW*xW)YuE(L50;lBfM&N%6J7W|4Itx>baHt)}guS$LJ zQF^_>aNp*hM>}Us(A{V93XCrH(Fe8ndz1b-r4t_p?_uW3VQQE4^54ndPFe5z_HU{a zN%QNL51H~Gk*DXM-tr%wq`YiQ^i$+jWRy>yq6IXN zd%hjX2KNI4^8=CHjQ=>Hm9T+;Z6!zH>nMkWtd$D1kfQSYI#-Fnyd z@}cLPvdt`)2IEaU1j27R_Nn=FHuc?ScP{Tf>g0uJmwMJ`v^|Zw-u5Ij@w`3sX5v(J zulLl|e4%+lzMw|^iAM2v@(;1Zbo6?)(vUTa?rri_TDLvyq_w8*0!sIqQ@r}WlX2c? z%1j0po}|pJl<_l1ZlA(C^#u7X+3+Ql*ZP~aP_FJhICB?k_aJRdrA{y-O>{O>zm?$3 z5zvtTlOOvWwAR;hUrg(9Yjx_!hqZ1=`@NmyX?(3CZAq9t0?%(Xj_;zLRmM3qd!YvM zvYQ@?l1(H`3pcLHVJQ{BnDe>8SIdyCtXS&<3x zL#*3u>S;b`Ctr6O+vai)v4Xoi^qJ=zujsh|v|p5u=J~YaTl6g4I#17TrzEU_kN-e^ z)!8QuYnX{HtUrLbU64pvO*J}s? zBka|p%_)_WuhUsoR?{Qd;I-t4z0-eqQ+lPr9C04x^8Gp|o#hO&Pa_ zG^f3x}{XY#|-?>At-IkGd$_e6>d zrymGPmxJkt#;a_?8or$Klk{5Y^24kZp?es5vMJ4jt<-BR<6cQEXK!U`+ZsZeYg@8m zuT71RO>uT8oBG3t$ZqVq{kGDaJ)l*zn7ZCH`sK)4@;zO2;YDj3{$`2e=WkPa@kxGv z<%53#cmR2fV$Y|w=)76HYlIAo(TVmO^ygXWV+`}|D}!Ibj+QyLvl8vEnrl1v8`Pe3 zqVeqs%h}_i_c%7-=o>y9`(qqhoU6IdN7SAl z8gtc_`pDVhH=K6UdmC#wcQxT#e(c8|_H;dG=-FY)M)XW&N-_``8}Gxt!Ga+7<4Tc( zXhvSK?MU;$K!|lVbaR+Le+S)y$nuoKYq*tjRh=JGm-lG2cX7)VjE^NQ6%STH0$V@} zF@I3~qmKHbI(Ag;J9k?h+P*UGP)&^`Le%qFcO=>KabCUL&I)x5)O|Ve|DV|F2{dR^ zysF>gPc+o_0BG3IP!1YXaA~rX%BmJJPe3at@I?~VIMK34X$n(IK)R7@O6VhvOM@uS~`XA7SgXIvx;Yod=AC7K<$knpB`!hMI=O1jb-Y9V17o3vxM|Ro)qnmW! zpK$!StDC};l#~5S24A5sg+_{r+e zn7Xn7{B57I*pBQn>W2NV${w_D_tmC#@?Fug?sU?;I?=P2>oat6u16=FgKV?ViR5?< zybH(wCM92uFX<>qJOHkD$tR(sNx}3SBK>IYJ~E_3`<6s;urVba`Aq%gw8PkN^dfpq zroTGBcJ)+PX{6dUD1m zy5an8N1m&j=<%_~T^7a-I?~u13oW0~k$3D#M|<%dpQd*k+LlW@L+``H->2yPHE4Rr zVywG&EINA-qj%Y%tLI0+r=j0z=vwy=q+^XmXN`uACs^&y_>(Ws!8aeXcP0vutMuEK zx~GF{94ekA-oorfJ^Ic;&u^peGNbb_W5gMc*!szQrRyhy$lKXRM)4_O?ug}5;Aj4HE zS=<+f-)#Eq^c_AoQD6N&Z2B#pPUts%7GLVW&H}}ge7*RRUvc@e3|~(F;cK1gziiIg zV?=h?x|18^nX&5SS-(oTyt=ya>STq7ufyZvsf;@CBKhpZ?zF!3HO3O(qMY^)%xy1z z9v`(5-cE~Vbw42{nD|~+rb&ig(&umv- z4gCGeUBZ(3{hfw%n24R6?{ zcw@Znu;9%qOWU;glfAn9oj#+NzYf|++tGBVOjeX}6SsuGqz&Old|!lk@UC{pE=mO zY~2~dF~&bM`cX!C>Pu$+SIO6Up&#}B0=|+ly^?Xal5wXw#GOwx)~rY@(O~t?CwXP* z`6Srd!n3J==0W3aF*GG7w{49%@p3xl-7&eql+lfCJ!I$>4|~s zzO!AXIjZ%5=AVMrR@U=l<9W5MsWbTQP9Z)#h_3CjpV@ZVih+gr1+57kKM1eU(qMYS9VvwB#~0+?aCw7i+PCNMow7w$ZVHwa`+& zc=;;0e52ZU6kIkUd<^uIFMKq(TjnZo@u)E{j5%EEAI+tbRiB_OTX*&d1!H50hoP6b zH>uV96V|%u+UC>`A0A zcaFH3K90gxMM+w45|<-=@BNtIn6b@)VzbWEu%JIWnCNN(!W=gHIAvU}Es z43lTlAKj1AS&%#KaD>#rtmV0v+Zqkkz3(R%xH^ z>}TL#Yplm;H%sfG(7QYrY)^e7jIS$gX3uQJ-M&3Y#L*x962FqIWd42e%p9MC4rFsa zc(VVd3nmf`yN-`I}j&F#keR?Hr z{J?rQv3US@5c{zwCb+zwjL+r_KDD0c_+RqbAU-?T1F#ekS zvr5knI(;M3Ht)z7xr*OwXydc^`cej8lHb=d_$vJr`L)28GtWlGb^6NS4ZYw)7*|d4 z>`;1bD*tus2R$F`4)1a7MQ0+6|L1;7nx5}S{<2fegN_b}Hw|h|1hGrsz@~)kxT!eq zoC!TEh#u|YlXji(SSU>G=fC2fF$bV67{Ft;yzn!PW7nC?DWm#~*~> zO}sb;o$-AVe~#5zAMSb2Z#6pSJ3(w&<^9G+S5aq+(Q&QS^Pam#_m*ikbY;I*AeI=D z$Gi8RLT^b%*#Q}4HIDYex3h<9Opi&))sI}qQ8yAX=S#KG&VJat=1q;!OVQU*#%K;c z+V$C1n6VIQ+b_EhbM7afEBb%{vyw{x5*F4 zpFT-?DZW(xMgG(Z;bZWrrQG{by?8k7)3@-fEJRt^o7xI%UKn)0wM4!PzBR}Dt$})u zQ?ra(9^Xy)WNx`&d}ghcT}U42`x+Xx#-noR#fMrS)FyE zp7(S%CO=Y&O`k`QFS&qlAwj<7Vgjvn@m#cP?XYF6U6&zU;LjU;4|tQoe*x~u1Kp#m#4vc!oX?)onLjlz z7156fcA@VpM9_`mh^$GpSIvz08_ z@s=phYS<6tK6=Ta9Bj?1ZB1PYf6n<(?bx{5Izn3?(H3~}OyHl1EBy=Le*wSmKid!8 z|7iQ62fo*SXzA1Khl(F-PxWnO>_yV+?g-^{4`gD$&E2hE=+kX+cTCS6_FufU+jn!@ zp}^g3hjJchJCr@Y?NF`7T$$_dPF}OMyWcgw?g=-K?9RHoq`S`pmv{I1?-kuy|2?`p zw_jd&-+qnV6QR46I|ZfiD<1ugPwf|99sT$YNbbr*JINQ%UGMkue2(GSiluowi}+FM z-XHt$J*zC8RzTX~FnoNsPj~QYyZa{m^&ohg3%_H}-r9Zr-J`qn;5YZ0wnG#8wI3>= z5B<*ebq6nQJCyaG7r^TU-P!Xm==ML*ulqF0_~!TPPWLN|v@5$S@0RbsqPviKMdVv& zXLX-(ciW*G&mP{L{Xi4nGr5p?MRQGdxBr?eyK^7))8CcddGNIheOsJE`NL1`=S{Y( zjtBnUZQZ})5C`eGI-k-!#QHv0=V$i)Zyo|KvhHs?R7@Yvf!A}-Za?%Ib5GX%hq`a* z7w*0d+30;W%}aAhxB5NQEt>9pmiy>S2ldY5bc=Tm7>kZAv9e7=cOG%)mEQTplyl~p z$Tw4_{NXXnKbXAr+^o|hk%-NsQl3LHua72t^mgy_3L8DV2w!$^g>yzGo&JaT3^XRR zuF*Jk)*KP$6zuk1bo@E;o&62-!TpRkjTOyT`N(V-?z2cUK3p2Qzt+mpB+ zz20z5`=Q{~eY%g3_KyqqB(|NoCm}rtug>Z|M%vLiI}<;>S@G=dt;9!T4_eQRrmk%K z@TfhBpYbi7teZ=^Ti?j)e&uYH^>rVnZ0hc<-LGQ%7T?*Czwi6>dDw^DeHnH8P+q<> z=i(vViAO1O_XXX7r&}1y7j}Oh`3}0d<}oO`3kJk~d7qD!0kbprW%*8M8Fl)Y*luX2|X_mdAZMbdpMam~jxoV%U-QH;|`t`x(#of&2KDY;ZtLBIZ-uX`LLy{xoN6 zJ)wEqeuupd^Yd_ItueuRHzC{LEI9GI{pmFU{w`r(#Qj_1(!)^hTW<*>Gs}xLwFK|8Mm|n8 zakrr*#6HTJ(D_1WL&86hZwK`oq;+FaggIkGxVf|S{EoQZE$bLDJHF0u#qD>}cN6s9 z?%&X*?k({iY*#T*-+RzETeR2Sd8uV>CJbiMaP@S==*n4Vm9e&G56D#CF z+oXS2e>m%6-Ic35HhRBSzF*&os3sl=YpT_#!nP4%E7e1;>FY0f>aY~orU zH&RyXe(4<@Xg$ihavA3}%lOW;xo@E~gCAt$xtp+^pl5%F(WPulI+LBLUFqP*w59t% z`{3=b4>or)cPFf}XHsw1w*Qp#*H`=0Hh1(x-v1$jJ$aqJb{XhcUyyc{`H()>D z_>K!zhjwI(YD;&jo-yr7Hs1E;WwiJB|DnA;roDbnd)$GdExy~snj~%ehw(G+I1sLP z)t`IiaF6a^6(bWX#(oIjnH%HTf^}E2oM%G0wXCaKxeo*XIvcKmw)7y{zjWG32b|nX}`Vs?oQp0*0)~l=+E|LThkMK+o?$W>YFQ8BhSdnc$T^O-IPE2ZfXoP z^e#a`kar0}>30h3@KkJRT>RRrn#b9papU@4+dNKZ^4w7<`fLBG^1W|lg&&RUPFD^= zXO8u(P5Jgrf;C@JzrgaiuWvA!Q?Vkh`_q3NaEIQP|867aMKQuzr$v#S^#&H$$!p$8 zYYHYJ>@9gA0e^H*rT_{s70M54ty5uJ{4#$Llfjy=D zn&!Zud{iWTmiZ(47&K$VdzKkQ=OM;`Gj1uXvp4O37b7blI`{%|wSsNziMi_*ygsfo zv+GG)3{;x#>WqEQ?5~iI#*f+*jWFLD8Xk=0U**tv0vb<1V?tbYL}#R1&(m%l?Ot+{ zHiwd?_BI3E?~K*LdnVuI)IaAW^$Sh?nDU%Hwxyl{&(hk}rQ_kf%Je_ip~amW_DSMh zeGu;f(sg%<{`u2$$sO9K7Sd;I?1|K|l;rl$eg1Pw@5uAcyqVKnSz!z84M>L`ZRa2S zl=|;czlJvUk&j(;Ip4@>MCPsN*x@psVN&mWj?bLT{7Sm^qMLbLQ@F~ySu+1*JqZ7YU^atwd ztX6vy+D_k+Z&zo zp*Zo5M!sp-m^#XRAGf`O*zsoO=W3vAdM|jC5JuMNd(7;k(QzL04et+h`a@&m*&Xy5 zzPR(4!cTK&S^C50b*xSely7l(=5D3Q%O=fTpM4fx|7Xzs8#2kHdzdtruGX%ykD&Z- zndASM7r7e&&wZeq9d`Cm4$tD%;TeAO%2ucLLC4LP9V+j)q~)=$44V1xv-r;X48Gqy z3E!{&FZu3!3ci1M628Cpf8g8q8GN^&gzq*F-=8H9`!nc1d=k3<>Y?jA&*i zqpj&XwpJr!HOBf7J3LU!J}Sbqu@(p${ImVH!7$Hn3CFDd`cj_A% zHhn83uDJ<+m&^*q5+U{;-TTt>|L5Vekh$Ck4}33^XYSAot#+Q#2jHE3N@rmRSs57) zrFgxwr=H4wB1%fnCV z3|QY{cJ};A!yh_n+~aoFK$_RIA6ZO4bpK-NB>iZ+)FXzwKhq)Fm(E zp!00;r1vgz^qdfRNp315TYQhYqv@;OO?UgL^f1rl^$p!TXli_D?-9f{G&a5QAF^Ou zbsi@moCoXqY>;t*d=ll3Js=C}xM_!yvhfqsCZ43+d!BLwEankts{hpy&eA$~#vkF1 z8Ttxh7a9}O;a~3(4N;#<+MIi}TI*_VX4n=vyhq@JK1ilvgsRC=x@aY@57E!QgOVeOOP)L%&)57Bh{gt9*GH5 z<~C#E24t!4OiP~lIrfN=PCLA3pVBx((U|~iKknKqk9Sh#iyZm!H$!~1=jBCb-Cr`i zh!5$_4u8yeo0=p&U$ne!U+8IjzxJnPv7~(33}mhONOHbevWv!&dTu5C=>D(b%Hy63 zyr=n5Uwn*{LC~aar$0Ksa?|rX>AOe|alo?ooKM%;Qzy~cBxtx}`Y^g4`+MHi3hSJR zbB|i|ZuAX*iE*kQjCKw~2HIcRe53WR1NcsvWnENQ#u;De21k|;nsP%Zr~RlEc?)?> zbH1mQW6CU68T>eNPHs?T%->N{JU|)ECz3<8;dub*&UYOREuAIFe`(FUmv`)Pu)lHG z)!5&|hnc^sJu>fh{zgtL>CJb2yT-lN7pGA_Shm`kgY^9dS6_diu58>}S7%CY{%=e^ z`tI6^U)afktO8R1eqQZU&!=4`s0-;a=1uiO)|LH$dmg8xjM&j z3zw)&@3h#|T>qVIu>_t|GmDM0TaW!=^bZOZ|gC zediQk_lW4lJJYa@kX2AvIzWw*WW8QHk>GTWPx z(O%>f?JfF@zD+(wTVpfY+Kkt7<*0q2jqF^1Y$4OTR32Nx1u%y6uHHDOZOjJ5v5lhF_iaXzc1f5zh7z*;9F;H8mAiX8mBWoylAZJUJzr}8K)NfJeeU! zt#w^q^tX?^wl5#&=9OgR1>v*s6ugc;1+S&2;I%Wu#=N{*8T@#8{lxkEWxZp#0tHqR zTzfd#I38$t`80d~B>vfBd2dFW5$5{OYVTvGe3WzXlg~G$8GX;3Z#pvC+l@hdR(rqA zXwPPTTkM(N5~pZyV@7*rv}f+~C#G>1#tQzzIlG*658ootyYVH>DXWoZlC`XBWvirJ z0?fvqZJh5pJj}$NGU2M@6|h1GpXC zD*vd~?7X+q7kaXLodxM#0DVt$DPzIi50x8P=sZe)H_}bN+mo*Qf;y+nftGDOxql*Y z=UpV$lm+xn<4pCPxsWn0jW0k$>qW_F8*ut?+uB^rH*`yED~!x1612};35*aV*Cxra zBNVrS<>TzoxWo%{G zPkgjSFwr!C=Xq8v-ZW->$TqkQ-gKHtzm9Z++u%+8P5QN@8{7tO$}{PsNjJC+-qg>e zUqiaVZSbaClYTYn2DibR`kM5sNH@3*-jrj~N0DxD8@#EHNxzbGgWKRu*(Uu8(hY8d zH)Wah%Sku54c-(m>7}F_+y-y*oAi;S8{7tO@|pAzq#N7@Z?aALaMBHKgE#59spK3c z-QYI3z6;DVvvJOOk2RVyE*=}XYe3Altwz7{2BOEop;?_OKa%J7z2$iJZ^w7|?AVL! zaT#Zm^{wYzKkMv8>=&{*=XlKW$90AzIzG~|^W^eB@GOM6DMxXjEGzx9kNvCZY8Hm()YU8qT~=FHGyAgHwTr42 z&Z%Jzv#ii1*M%;qXXynDeX(ZIg%zP$mGupwxivL&MR8tbsIsoErcSiyR4uFs)hucV z-8=8TOR0SUEmI{luY7T3Xkkr5Xm)vZbtOe;y=IP6_ui_ys)ci-RZ}Y)7S-m4LUR@^ zoZSFB^}|B-HKCg>To5XsRa4hMnb3VTi|Rt~H@B{Q0X)p9sjCcCL9?#BeqLpLsC?ml z_m(@&I_-LTTtiD$^)OYrFtn&%q%NzdTzuKw+Nzobl?yHnO{=>vRKKXAW{f&qM>s(W z#CvJg)IuXO+nR_V?=7#Zq^NakO=yT49}=oY(42cz-AG%L%IQJF)gd?cYDcb9jQriY zZv5nR-P}-pgXESbVU4SqyQn@i#LY}s4Gmpb6Pj1KBver~7crGLP^Y}EL4vO{ZL%IQ zb=?$);n2{Anwn7k0+jXn4Bo5D>l;eP&nvG(-gT8%hlX5GKZL0_93CP5fgD+$*x$7M zO+meN3nhBDC0MVJyg!}y*)`QQ3&kQljFYY2RB?64?c)go@Z{fGd2i^>^82nn-cG0r zBfKCyYWn|c@B4$}IIc6BA}m7&B|{SIz&0z{lAr`ClOPrZDNv>pfB;A^2E-KrinPOc zG1#32*4$s(oh1N=aTpcLhH|vSiy@niXq!nWn@-VXMy4G)VR9@*&e1lL<0aIYcV*|4 z3Uen{DT$KMSGt6b@_qed=gn|st5Wwz>Pjv3c7O9;zkdC?r@Lo*@QBI;AJm`*>k`3I z1?^ytL*L|zIa(H%Fg#2Ky<7E40pwC^A!k=1+pIp^Fh8_yucc3#^ktl_AF%0wO}E%IYg0W|Q7&h2ve%}%|9X3qR!-8EOX71l)|0gLC9V9V zXRq7-Pg*^aw%+6{=`}w{dD)R_{s2t9qgt+4j8zYKg}OF=qMJQ*KaDo&d)WlYrpJ_W z69hW;p>h`7m3(QNSJM;7eOdHwVryb^|C?^+OXW-GtVH>MUoChQ-2VAsKH)FsYkD%t z`k1WpoM}+$O?8AfJnn&V#tVE7g>seskFJFZ6VPR-5WprVpG2%I6!%o~fwpT7r7+f2 z6*DUmHCqnmX$M!&&Z$Y=jzqDJnRM1q^se8$5jM!e0udeuFoV|g1Ulu-g{8zonO(eq!3Acnq}6&!PxShJ zYb>jcv1r>+EP9nhH|w}RI-Vldc`#x4&;c>F=JIssL^+$w8ycC4vNe}Cy>nu8GRo-W z#rly8e4{U4g(o$?RQpjpQ-VcJl}k(P|IxW}(JLV>&SLAu{mA}Od7Mwef|$6VjI4W-F9gcNiw%jqxdqZcf9eT!-}_h7A($IXSQq!(>=J7|5=O{I2X zD}5lJ;rhkZo)_4b1a?QIIbRRzI=4ugYUtZidzzA2cBM4dA+LR>7hm4iYB8K2{ou@)FE|3U4Z@Gi-{|EH~2YM&(>=j z7LqKAM%Om<4Xju9)%YLi3(0h?fK5KibZzdsuhs=y&vUO&ON4V@s^(*xjfELyIEU*U z9@gXvboQ}c@tWBgMXzNr33*_t#&|JL*YvWfA3C_P)u!+6Tc5<%=6)?NUm40~tDMxT z{-gDL)zwqM%7a;p4?ym+=CAqHyjMt6@X%q6oG5ukU$SYwS+9<=+!=7)jwqn%>{IAn zDq(cr%wgJ!E=YJ*yKbyedz;9qdBqBzvtFepBWKRTUZ+VVv?pu?JJ(CW%?ye4iGlSS z`x-{YUjP_+)V-nCO|3IL+CV>qiFNynfnU-Om)eAU0yGbwFdH5tmlB>C0F#d{} zO5es_&R8yQ_QB@5+4JgrP4Lvr^;&h)7S4y2Om1M)1N|o5l2o(BN^$`ATt$ri`JmpH zM19#>a$vTvS5@Z&{on-IsJ@SSAnj+`KVWfxAFg|g8fIEe`q*EVegtybFD z$4M+(&EW3RDOQZv!|N7!-2$&$;B^bcEpQAUZ}lSb_33UzrfHkL#8`78l5dRJ;(iTK~s-dm}<69GrzyB|g#=bxK{pzxRe@27)?}f?fR>;(Ws7`K{$$z^A}`H_}!_{8>b~9oO+r zzsx#bLb?nk@I7Ri`?1#=UWU#tTtWL@kFVR{wT=NdZvL(5!DsMI@#~>s0p9n#&)QrQLXqwqyG5Ex0yuO>ym!{m(Um>%|Z2f383H{!!uEScPxC^!mfK zhwB#C4z5#N%eeM%t>IW>aCZLpViUHNy0E=WU#~Ctjnin7)<}?}r~%2BOu9?h^=R# z8)5=+9pV7uR>VQX5yUaXal{njG~!;weTXxNS;Tq70%8R*KwLyTgt&~jf_MV)6yg~~ z)_)f1dBkTCS^q_(R}jC0$oj7#zKHlTV#`-TTeW-*Wq%X)L^^_WEz&E$p;R~0ZlwK4 z51xSDNT2;I^hP?3G==oqXP`IIl_#J#(nClKNDD{_NTdTZ;d+g82lmb+RyR=s7_ zkFD-p^|n=Szh&*Jdw8qe6t3GVtM#bFyl=Ty>zyHp9H@5xEY-0Ad26M)QeY$jl+BlQKuKEos>*q{r_P|V6>$?V-&l3^z z4#HT44H;JJS=;QD4d(1^_R0qHE`vO~4~0)SjkzagSglt?^fC4d2H&mI?r%8Up|Sf} z-I&qLmDQL@tts_|7WMb9j3MXR!7kI_Cmvr)T?{C6w8uJ;zgf;5tTo$R5 ze>ui~L-6AkXavW9T-5hPP#t#qTo!cd^Sr=c64>!4I3RT}?Q!dJtMFy ze^}73f=+wWj`)|xwSD-kV?w)n0U3u^Vb-L4K4+QGrrr!zhn;*rJDKoi^$uhmUWHu@ z?eF@Rv+skB71y1;P6+vp!`YTlfbPOXMj%wFIr6b?d$QQgX!NcfdlXpOa3E* z&h{M__%Qg1x8k~M-z7mO|5brM0e)i1-`b;I$NO|auzVA5|Sqx7skh2b?bo<_Dr>CX$y@-GX__TLbg^o|FkvmN`nbTP&kRNBhXf{ld|Q;B62gZAf~TAXOGjnqWo6{ zro5}EDE&o&NgsGPN*@%M^dkbZJ;wwl{pH=!^7D^`nDny(v;4CHlit230pLY}A7goe zkFvbLpJw?lgyp};@&dCdhwT1@>+A1R`Yj|fbA9r@3wUx&N({^Kh8*M!^Ex4`d0WB(Bwzvq6= zahUtRe_x2n`n}NuU`T^t<&)@?4>y7Lv8=SpbV&yvpW_@b~ru>5flYUrW(qEdvj}CTy z_Cov?)PKiU^l*Ycx9k2?-wO3zOZ|Xbkx<`zfm;Qp{C1Q#;dZsYNqZ8aKJu^0IDV^- zz@$&d@NpskT}|Zg6a1r~6Q741w?4FIWAzEF>l2vv+b1yTDL-0%Mqtv<#_+z2&VF~R zsTRlsMgsep`=wpj_kAE}j3Tt}4DlrmG|mzKNr+1nWP=+o=S2NWpcCV@LZkgNIak8i zyC5+2KPoWWe@tM~$L6Bt_XolxDJ@F*gj(Ye}~fT{VyT@Sd;di z67_uwbm9@nV}G9lei69e;;#ZH=0o~L;H<#Ze_dWHgP%6_XP~>_?7LOaSs!r#CRg7X z-~o#%{{>*VU%T=FC$UX^w~74c1)cJVrTpprp?tRI2(XjS`kohm%kR*Pg*5))b3b-wEJj7PG#AQixgK=`wzFXjA{#q`nJ+&iaTifZx@3sDeJRnDt!) zK4CHIJN#&fS>Ip{KRUFje*xVE*I(0u&iaVYfQj|7{4$NJLz}ww4K5!(FCP)~Qz%c| z3pSRgyz{_=7E`|)0#p9UI(~F$Q|(RUpAq!8gHF5-{FL7f{2kzaiz#p40WAzaZE8~! zd09bcd197x?L8;xyuZ&2Oup6y{OHi8ex`~1c0s3n;%V?Z`DX-uXOr^h1fA_A9ueio z7ESGJ-&B+GDM6>cEguZ|f4&KSyP#`*f%jrcar$=uLRg;mm=~Dx&k8)>ME-d}XM2cc zdkP0b`PC-n13~9_BF>_S)9Xd|BC{%y}gG+I&u4D7)OT=^%=+`hS|~n4)uB9PT*FHe;YU<@E3u* z1wI4ZEAW?r2L%2q@Swn710ECjJa9_j3&48?einE};ETZX0)Gp*BJiIAFADr$fe!)0 zT)KVF0Y4`2e+ND+@OObfEHE7DsAYlSxIq1iz~2KtBJke;e@x)N2Rf^4AY0vWlQ$OOfLcb&bA}u=){94 zNBeTVX+IL~C%C?i0XsVNSq2`n{9KPO0~ajjdbjnXc&4?O`mB5mKRUFjcQw)Hw4hTT zVwQ9IwESv_sn0&(8B5pmtDsZAj-w%+`USvF9`(B<@CfSb`j656U-xTTS^Sv(7!;Ut zvMV87w-4jlgbwvE^F|Ze)K%~k58wjje-C&s@If*DFC7c{Ss%|WO=wfs!B6bUzwwP&%RU7e zqI~MtLw=U$Ii(40>TTdBZV~)@pA6~b=Q*bdZR&pT6T9)h7lg}#uJsdmEBJX{S^=N4 z-{5g8&W<;ptD4ZJc7dN*%0DgWl+W{6$DaW|@k_Yiq?4cLxF)oz1K=l?@>@R> z(#g;BUlZEY{7*E{|@S`C)^5B_FICqK{mO=wg1fS*|MpAmHO^ZNkDzZv|*l0WfVA)WkP0(1N> zpA6~5D_2(83mxibAx~pFzwIHu8t#|KpB2ydY0!x;;JSOhzw)#dhy7(0bQj$Ee@fJU z5cD$we_Zh6U*dV?Qv$p4FMQ5PY*P(W;1cQ7&z-09E2yVX-r-gDeBG5M|Cd2`=hL@f zSNt{Li;$lH6UWatfLUL=#oq>Y=fiC8HDIJhy6ZZLAU>B&b{lr3Fx(B~+f0NGYAJW$W^ZbT(puCNOp1`$T0+Zhd zJ^|*H)rP+U%=f{ppXI;)W@Kz2|03vH(cTv1Nk1Hu_e2ao1)N=6RU>Cu8*UF}!@Y(~k17wt_xBh&I{w ze>cYex4_55_`DILx2?hb17n@B`n?1A(2&`mP@gVfp1-fP^dxX+Ce(Ku@VPIT@q8!d zuU)`=f4$Z6r-5CdzK;T*`B2zDzX;6p%ucKSQx34b1c50%+vV3HqJ5wkR;m|4Ix$34C0P$1eb1 zgt6WI_j@t^OEJv-^ik-`=PlMx2c|BtzW+;kJ*dOp-?!f3sN_fA0`vTQ8hMugN#N@v z*h5(S0Pym*(Eh`~JWua#G5ouLj|%-~fnA{d!!i5`V18fV?0;8><2L$yF2;W@hA#oH z>p^=^jQV~DczGY55iI^5@LA}4-r|1(b|Hbxh1mUa8|F8j=M93!{(md*2{B&pjm>{s zV|W~x=U<&DPknX+^ZlZ;=PWSKvk%$+sR19Ih0d1$qrfiEUY`Z#d7W$D=VSEW1?Kkz zZvJ^TM*lW2zn@tPS=8sRWAq!q`*44ui#5Hy9kTi`PO+~e|J#9izUJhw19pM>J;xs# zm>rAJ-v@lTWcKHjHxGPP*s~PlUk2v+r@KE_fUgL9J_*e8mK5ruyrtOqJ_Wi9)aM*9 z&r7?JC;vs@t@Uuecn;WoKSBC;fqC9@!ScTdy!_E{{rP9$PT0%ow}v;h4Xp3Sflp(- zJ#X)abus!7Fu%ue_vgbgdIp&9U)^|l6u2`R_Roia`8^fH>-IbjJR{n-0?hMLH~yXi z=JWit9dAzqyFh)PiQzv7=J!-5L8CqXCosS7;B86#cfdSPU9s|7AcWtKIDOkF)COkP z#&AFINzwm1f%$z^ua)-*@HFm+ttiX-3c&oH#>rm<=J|7%rGEsN-%p*h_}73>3HhG| zc7fTa@x${>KJQZg1>hAiUtNjue-D`71D!%y^1lLn8TaE+i(9c?pPCQXySK*H@Abg^ z{%fzT@7*!_`(wBQd|K%LOELN};Fb^J{ z19w%!@sI)L_hT@z);9n?2m4^z#{Ar&ej!%=W56BYclJ2}+$qM>S7Q8s9>dnbO@4O6 zTqd)@s}|S8El+)M{T#gH;0x{f+=hV-Jw5jSX5g+HvP zUyBi4OnYbW3UtzBE=bpAboHe|=R~8Ga%P^spX!z2c|Wtirvmq5@T8$#u+d-2gkKbd zWW$A$k?fVcLU~U6_OVq(J?=yl5^SbisA0YYlUD1MV9YCJ3x3r#s7E_Kf*U2T*7Ja( zd!p&`j@0(4(Qyl==JUD0=7$UW;pis>Bl!c3BIBcvH1fmala2h;=(NqJ7K$nOlL*n! zxFhYTKr~>lBkmp<9)FJp#$Q~|mQ5QsHN4ZXCltJKY|qx%juf0&FoovOe-b8yi!r+oIzL>^A6Ts8ymDGfP-SykX?Kx@O^QJKR2hAP^TkDrZnr^?r=+nonLZRmR7 zFCs(r=+Z&a<4-yT_d6!19rmQR4NZ+29Oks^9m@{)OJ+b$Y3Cdmloqmw^bj4(r>c3K zo1U*?aO&&?E5}Xb1~c4R;5vM|zz+)(8WD;1yb^N3cM7@6xd7c3=8Rlb@)uJ161*-{ z_t%tmH=+VvBSfJH-BH;x%-TXZ_!y}*yi0&yTpFQMKCfV0S2Z9jM+%qQk|f*GP{S(vUySWuH8h+=>_gm$*pIjgakF}4TN1HPO$;mJTLa>)a}DIQdkv(} zcm%U%CKxX-jOFL%_jvh${xTY)3U+p0+;I6JpJU6b)A3c1&!81*ww~)5=-W6Tym^3S zp^_hUo=$4O*>VF;8Lu4T%9!!}A+89oIO6Iy<2MA}>t;vkUsJ)E3Z3BWEJ5e6f=TT< z#L}jMs-%rr_acIQL^mtKg!W53Vf~4SLPOToh`0nU1h zQlwxi11~D@o+7yPB{)_Qm!`DO6>(vL_XHiX+;j~NTjJMxoAA-`;Z1xft6iKAOv z?|AH!GUhT^-MF_2r@VAFf1sA%h?Tv^7i}oqo`6;ru0@oRm_~0E^-D<*GpI`_YV&47~i>V zINjINuXjV`Oyo+6K9&^RlHz|!sCw{IC@&ZXOR0K5uTCnHD|-9=s<$9n%ss3Z7*!~H z**&T72D9^rAy)ES5DO1sU!&{J6f06rI-ObcXi=>4AB-vyRD*Is@)q-01F_-SIX#gM zw_>mzH)7ND{l%?J4TdOJ0$I?sXBQm9Vx80L5E$u03MDZ}a a`MEqvY@Pj>1_ha1 zp~FJRe>A=c=bD>^LbjKy*wkV+ql+#WtPr^&GP#-`lxuWLCu_s)<}FR= zo5hF6QEs6E+%J)PKDR(msIpYPoCzY*U0>0sYqq= z&6m;Yr8N3Vssfr{Iv}rzdwAOtHWl=<9Gy%th3W-ImU0H$4iB#ID`@8!?0QQ(~I#Y#BN zyi&%G40f{~Z0F1l9mY8$xQ7LO+p~qTKEqGNh7O>+-45PVRjG^b9gNgUClAt^+wPLv2&T3yR5Y_@3_HQ55fTg z%VWygj=hcEfkSibx3OAQBXhcTI4m`)g|4Is#oM=ZVfM5mZHnH zTX#NC&PV#Xad%8(VyW(|K04_w=m%gulS6K!T8u2+P`i%lg?5q-DAC8}!OwP$?J+La z4e5C5$Ft{6L)#kA7IY0V-qVNmRC%CQy1Cd*lkyn6_L{xsu6kgX0ykPQBhhaoJ^V|{ zy2`Ah?Z$A1#q}(Qw>Mr<7T{jUkBr+!>$L6gwg4QQ963kJlJ+T4caqt0xmRD2{g!@i zGfd%2ord`}L9eHnk@%EhI9)R|qQ_mOpMwoCSHTdJnqX2Y#WsyqKNFd*YkqW%*ZV6x zV;G6C$-Iy6=5XU8f#=Um#5(awy06g`F_H%Cl2Wr)uj7rCR5-}6`(n$W>ZvUi18){7 z_O&{lcR4uS^n38avuC!RFJ#x}vq~d46raa4%2Ej>O^RnnO>z4H3DTfd{Q~(-rh>8D z!&iN(2Tut2<;zBp%6#BgJ^s9Ty=ZvMn?=Lp@{r_V%Z)0_C~Zp3;sK%u_ABCfTlEZ0 zPV}&$`B^-K)ZA<4ADO-b960V$w;*1(k?+f0Xt9|dr1-ZR%jE0C`+P?J{T%++fqaYu zh&)y#pTc=8;~-vGlSW>qI7U)~h{WU@#Cvf@`gd~y;&m9qFMPo0dg-e}M-W}TEYG+b zk-puSrw1c zzpTIpC-;wmDVOr-Go$MpIBOH + /// The SPI Bus containing the 2 SPI channels. + /// + public class SpiBus : ISpiBus + { + /// + public int Channel0Frequency { get; set; } + + /// + public int Channel1Frequency { get; set; } + + /// + public int DefaultFrequency => 8000000; + + /// + public ISpiChannel Channel0 + { + get + { + if (Channel0Frequency == 0) + Channel0Frequency = DefaultFrequency; + + return SpiChannel.Retrieve(SpiChannelNumber.Channel0, Channel0Frequency); + } + } + + /// + public ISpiChannel Channel1 + { + get + { + if (Channel1Frequency == 0) + Channel1Frequency = DefaultFrequency; + + return SpiChannel.Retrieve(SpiChannelNumber.Channel1, Channel1Frequency); + } + } + } +} diff --git a/Unosquare.WiringPi/SpiChannel.cs b/Unosquare.WiringPi/SpiChannel.cs new file mode 100644 index 0000000..b85f01d --- /dev/null +++ b/Unosquare.WiringPi/SpiChannel.cs @@ -0,0 +1,130 @@ +namespace Unosquare.WiringPi +{ + using Native; + using RaspberryIO.Abstractions; + using RaspberryIO.Abstractions.Native; + using Swan; + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + ///

+ /// Provides access to using the SPI buses on the GPIO. + /// SPI is a bus that works like a ring shift register + /// The number of bytes pushed is equal to the number of bytes received. + /// + public sealed class SpiChannel : ISpiChannel + { + /// + /// The minimum frequency of a SPI Channel. + /// + public const int MinFrequency = 500000; + + /// + /// The maximum frequency of a SPI channel. + /// + public const int MaxFrequency = 32000000; + + private static readonly object SyncRoot = new object(); + private static readonly Dictionary Buses = new Dictionary(); + private readonly object _syncLock = new object(); + + /// + /// Initializes a new instance of the class. + /// + /// The channel. + /// The frequency. + private SpiChannel(SpiChannelNumber channel, int frequency) + { + lock (SyncRoot) + { + Frequency = frequency.Clamp(MinFrequency, MaxFrequency); + Channel = (int)channel; + FileDescriptor = WiringPi.WiringPiSPISetup((int)channel, Frequency); + + if (FileDescriptor < 0) + { + HardwareException.Throw(nameof(SpiChannel), channel.ToString()); + } + } + } + + /// + public int FileDescriptor { get; } + + /// + public int Channel { get; } + + /// + public int Frequency { get; } + + /// + public byte[] SendReceive(byte[] buffer) + { + if (buffer == null || buffer.Length == 0) + return null; + + lock (_syncLock) + { + var spiBuffer = new byte[buffer.Length]; + Array.Copy(buffer, spiBuffer, buffer.Length); + + var result = WiringPi.WiringPiSPIDataRW(Channel, spiBuffer, spiBuffer.Length); + if (result < 0) HardwareException.Throw(nameof(SpiChannel), nameof(SendReceive)); + + return spiBuffer; + } + } + + /// + /// Sends data and simultaneously receives the data in the return buffer. + /// + /// The buffer. + /// + /// The read bytes from the ring-style bus. + /// + public Task SendReceiveAsync(byte[] buffer) => Task.Run(() => SendReceive(buffer)); + + /// + public void Write(byte[] buffer) + { + lock (_syncLock) + { + var result = SysCall.Write(FileDescriptor, buffer, buffer.Length); + + if (result < 0) + HardwareException.Throw(nameof(SpiChannel), nameof(Write)); + } + } + + /// + /// Writes the specified buffer the the underlying FileDescriptor. + /// Do not use this method if you expect data back. + /// This method is efficient if used in a fire-and-forget scenario + /// like sending data over to those long RGB LED strips. + /// + /// The buffer. + /// The awaitable task. + public Task WriteAsync(byte[] buffer) => Task.Run(() => { Write(buffer); }); + + /// + /// Retrieves the spi bus. If the bus channel is not registered it sets it up automatically. + /// If it had been previously registered, then the bus is simply returned. + /// + /// The channel. + /// The frequency. + /// The usable SPI channel. + internal static ISpiChannel Retrieve(SpiChannelNumber channel, int frequency) + { + lock (SyncRoot) + { + if (Buses.ContainsKey(channel)) + return Buses[channel]; + + var newBus = new SpiChannel(channel, frequency); + Buses[channel] = newBus; + return newBus; + } + } + } +} diff --git a/Unosquare.WiringPi/SystemInfo.cs b/Unosquare.WiringPi/SystemInfo.cs new file mode 100644 index 0000000..55f0938 --- /dev/null +++ b/Unosquare.WiringPi/SystemInfo.cs @@ -0,0 +1,45 @@ +namespace Unosquare.WiringPi +{ + using Native; + using RaspberryIO.Abstractions; + using System; + + /// + /// Represents the WiringPi system info. + /// + /// + public class SystemInfo : ISystemInfo + { + private static readonly object Lock = new object(); + private static bool _revGetted; + private static BoardRevision _boardRevision = BoardRevision.Rev2; + + /// + public BoardRevision BoardRevision => GetBoardRevision(); + + /// + public Version LibraryVersion + { + get + { + var libParts = WiringPi.WiringPiLibrary.Split('.'); + var major = int.Parse(libParts[libParts.Length - 2]); + var minor = int.Parse(libParts[libParts.Length - 1]); + return new Version(major, minor); + } + } + + internal static BoardRevision GetBoardRevision() + { + lock (Lock) + { + if (_revGetted) return _boardRevision; + var val = WiringPi.PiBoardRev(); + _boardRevision = val == 1 ? BoardRevision.Rev1 : BoardRevision.Rev2; + _revGetted = true; + } + + return _boardRevision; + } + } +} diff --git a/Unosquare.WiringPi/Threading.cs b/Unosquare.WiringPi/Threading.cs new file mode 100644 index 0000000..ada771e --- /dev/null +++ b/Unosquare.WiringPi/Threading.cs @@ -0,0 +1,72 @@ +namespace Unosquare.WiringPi +{ + using Native; + using RaspberryIO.Abstractions; + using RaspberryIO.Abstractions.Native; + using Swan; + using System; + + /// + /// Use this class to access threading methods using interop. + /// + /// + public class Threading : IThreading + { + /// + /// This attempts to shift your program (or thread in a multi-threaded program) to a higher priority and + /// enables a real-time scheduling. The priority parameter should be from 0 (the default) to 99 (the maximum). + /// This won’t make your program go any faster, but it will give it a bigger slice of time when other programs + /// are running. The priority parameter works relative to others – so you can make one program priority 1 and + /// another priority 2 and it will have the same effect as setting one to 10 and the other to 90 + /// (as long as no other programs are running with elevated priorities). + /// + /// The priority. + public void SetThreadPriority(int priority) + { + priority = priority.Clamp(0, 99); + var result = WiringPi.PiHiPri(priority); + if (result < 0) HardwareException.Throw(nameof(Timing), nameof(SetThreadPriority)); + } + + /// + /// These allow you to synchronize variable updates from your main program to any threads running in your program. + /// keyNum is a number from 0 to 3 and represents a “key”. When another process tries to lock the same key, + /// it will be stalled until the first process has unlocked the same key. + /// + /// The key. + public void Lock(ThreadLockKey key) => WiringPi.PiLock((int)key); + + /// + /// These allow you to synchronize variable updates from your main program to any threads running in your program. + /// keyNum is a number from 0 to 3 and represents a “key”. When another process tries to lock the same key, + /// it will be stalled until the first process has unlocked the same key. + /// + /// The key. + public void Unlock(ThreadLockKey key) => WiringPi.PiUnlock((int)key); + + /// + /// + /// This is really nothing more than a simplified interface to the Posix threads mechanism that Linux supports. + /// See the manual pages on Posix threads (man pthread) if you need more control over them. + /// + /// worker. + public void StartThread(Action worker) + { + if (worker == null) + throw new ArgumentNullException(nameof(worker)); + + var result = WiringPi.PiThreadCreate(new ThreadWorker(worker)); + + if (result != 0) + HardwareException.Throw(nameof(Timing), nameof(StartThread)); + } + + /// + public UIntPtr StartThreadEx(Action worker, UIntPtr userData) => + throw new NotSupportedException("WiringPi does only support a simple thread callback that has no parameters."); + + /// + public void StopThreadEx(UIntPtr handle) => + throw new NotSupportedException("WiringPi does not support stopping threads."); + } +} diff --git a/Unosquare.WiringPi/Timing.cs b/Unosquare.WiringPi/Timing.cs new file mode 100644 index 0000000..de36444 --- /dev/null +++ b/Unosquare.WiringPi/Timing.cs @@ -0,0 +1,36 @@ +namespace Unosquare.WiringPi +{ + using Native; + using RaspberryIO.Abstractions; + + /// + /// Provides access to timing and threading properties and methods. + /// + public class Timing : ITiming + { + /// + /// + /// This returns a number representing the number of milliseconds since your program + /// initialized the GPIO controller. + /// It returns an unsigned 32-bit number which wraps after 49 days. + /// + public uint Milliseconds => WiringPi.Millis(); + + /// + /// + /// This returns a number representing the number of microseconds since your + /// program initialized the GPIO controller + /// It returns an unsigned 32-bit number which wraps after approximately 71 minutes. + /// + public uint Microseconds => WiringPi.Micros(); + + /// + public static void Sleep(uint millis) => WiringPi.Delay(millis); + + /// + public void SleepMilliseconds(uint millis) => Sleep(millis); + + /// + public void SleepMicroseconds(uint micros) => WiringPi.DelayMicroseconds(micros); + } +} diff --git a/Unosquare.WiringPi/Unosquare.WiringPi.csproj b/Unosquare.WiringPi/Unosquare.WiringPi.csproj new file mode 100644 index 0000000..0525f7f --- /dev/null +++ b/Unosquare.WiringPi/Unosquare.WiringPi.csproj @@ -0,0 +1,27 @@ + + + + This library uses WiringPi to enables developers to use the various Raspberry Pi's hardware modules including the Camera to capture images and video, the GPIO pins, and both, the SPI and I2C buses. + Unosquare (c) 2016-2019 + netcoreapp3.0 + Unosquare.WiringPi + Unosquare.WiringPi + 0.4.2 + Unosquare + https://github.com/unosquare/wiringpi-dotnet/raw/master/logos/raspberryio-logo-32.png + https://github.com/unosquare/wiringpi-dotnet + https://raw.githubusercontent.com/unosquare/wiringpi-dotnet/master/LICENSE + Raspberry Pi GPIO Camera SPI I2C Embedded IoT Mono C# .NET wiringPi + 7.3 + + + + + + + + + + + +