├── .gitignore ├── LICENSE ├── PluginHost.Extensions ├── Collections │ └── IEnumerableExtensions.cs ├── Comparers │ └── GenericComparer.cs ├── Enums │ └── EnumExtensions.cs ├── Functional │ ├── Either.cs │ ├── Option.cs │ └── Pipes.cs ├── IO │ └── StreamExtensions.cs ├── Numeric │ └── NumberExtensions.cs ├── PluginHost.Extensions.csproj ├── Properties │ └── AssemblyInfo.cs ├── Text │ ├── FormatStringTokenParser.cs │ └── StringExtensions.cs └── Time │ └── DateTimeExtensions.cs ├── PluginHost.Heartbeat ├── HeartbeatTask.cs ├── PluginHost.Heartbeat.csproj └── Properties │ └── AssemblyInfo.cs ├── PluginHost.Interface ├── Logging │ └── ILogger.cs ├── PluginHost.Interface.csproj ├── Properties │ └── AssemblyInfo.cs ├── Shell │ ├── Command.cs │ ├── IShellCommand.cs │ └── ShellInput.cs ├── Tasks │ ├── IEventBus.cs │ ├── ITask.cs │ ├── ObserverTask.cs │ ├── ScheduledTask.cs │ └── Tick.cs └── packages.config ├── PluginHost.sln ├── PluginHost ├── App.Debug.config ├── App.Release.config ├── App.config ├── Application.cs ├── Configuration │ ├── Config.cs │ ├── Elements │ │ ├── PathElement.cs │ │ └── PathsElement.cs │ └── IConfig.cs ├── Dependencies │ ├── DependencyInjector.cs │ ├── ExportChangeType.cs │ └── ExportChangedEventArgs.cs ├── Helpers │ └── DirectoryWatcher │ │ ├── DirectoryWatcher.cs │ │ ├── FileChangedEvent.cs │ │ └── FileChangedEventArgs.cs ├── Logging │ ├── ConsoleLogger.cs │ ├── EventLogLogger.cs │ └── LogLevel.cs ├── PluginHost.csproj ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── Shell │ ├── CommandParser.cs │ ├── CommandShell.cs │ └── Commands │ │ ├── ClearCommand.cs │ │ ├── ExitCommand.cs │ │ ├── HelpCommand.cs │ │ ├── ListTasksCommand.cs │ │ └── StartCommand.cs ├── Tasks │ ├── EventBus.cs │ ├── EventLoop.cs │ ├── IEventLoop.cs │ ├── ITaskManager.cs │ ├── TaskManager.cs │ └── TaskMetadata.cs └── packages.config └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Roslyn cache directories 20 | *.ide/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | # Build Results of an ATL Project 31 | [Dd]ebugPS/ 32 | [Rr]eleasePS/ 33 | dlldata.c 34 | 35 | *_i.c 36 | *_p.c 37 | *_i.h 38 | *.ilk 39 | *.meta 40 | *.obj 41 | *.pch 42 | *.pdb 43 | *.pgc 44 | *.pgd 45 | *.rsp 46 | *.sbr 47 | *.tlb 48 | *.tli 49 | *.tlh 50 | *.tmp 51 | *.tmp_proj 52 | *.log 53 | *.vspscc 54 | *.vssscc 55 | .builds 56 | *.pidb 57 | *.svclog 58 | *.scc 59 | 60 | # Visual C++ cache files 61 | ipch/ 62 | *.aps 63 | *.ncb 64 | *.opensdf 65 | *.sdf 66 | *.cachefile 67 | 68 | # Visual Studio profiler 69 | *.psess 70 | *.vsp 71 | *.vspx 72 | 73 | # ReSharper is a .NET coding add-in 74 | _ReSharper*/ 75 | *.[Rr]e[Ss]harper 76 | *.DotSettings.user 77 | 78 | # JustCode is a .NET coding addin-in 79 | .JustCode 80 | 81 | # TeamCity is a build add-in 82 | _TeamCity* 83 | 84 | # DotCover is a Code Coverage Tool 85 | *.dotCover 86 | 87 | # NCrunch 88 | _NCrunch_* 89 | .*crunch*.local.xml 90 | 91 | # MightyMoose 92 | *.mm.* 93 | AutoTest.Net/ 94 | 95 | # Web workbench (sass) 96 | .sass-cache/ 97 | 98 | # Installshield output folder 99 | [Ee]xpress/ 100 | 101 | # DocProject is a documentation generator add-in 102 | DocProject/buildhelp/ 103 | DocProject/Help/*.HxT 104 | DocProject/Help/*.HxC 105 | DocProject/Help/*.hhc 106 | DocProject/Help/*.hhk 107 | DocProject/Help/*.hhp 108 | DocProject/Help/Html2 109 | DocProject/Help/html 110 | 111 | # Click-Once directory 112 | publish/ 113 | 114 | # Publish Web Output 115 | *.[Pp]ublish.xml 116 | *.azurePubxml 117 | ## TODO: Comment the next line if you want to checkin your 118 | ## web deploy settings but do note that will include unencrypted 119 | ## passwords 120 | ##*.pubxml 121 | 122 | # NuGet Packages 123 | packages/* 124 | *.nupkg 125 | ## TODO: If the tool you use requires repositories.config 126 | ## uncomment the next line 127 | #!packages/repositories.config 128 | 129 | # Enable "build/" folder in the NuGet Packages folder since 130 | # NuGet packages use it for MSBuild targets. 131 | # This line needs to be after the ignore of the build folder 132 | # (and the packages folder if the line above has been uncommented) 133 | !packages/build/ 134 | 135 | # Windows Azure Build Output 136 | csx/ 137 | *.build.csdef 138 | 139 | # Windows Store app package directory 140 | AppPackages/ 141 | 142 | # Others 143 | *.Cache 144 | ClientBin/ 145 | [Ss]tyle[Cc]op.* 146 | ~$* 147 | *~ 148 | *.dbmdl 149 | *.dbproj.schemaview 150 | ##*.publishsettings 151 | 152 | # RIA/Silverlight projects 153 | Generated_Code/ 154 | 155 | # Backup & report files from converting an old project file 156 | # to a newer Visual Studio version. Backup files are not needed, 157 | # because we have git ;-) 158 | _UpgradeReport_Files/ 159 | Backup*/ 160 | UpgradeLog*.XML 161 | UpgradeLog*.htm 162 | 163 | # SQL Server files 164 | *.mdf 165 | *.ldf 166 | 167 | # Business Intelligence projects 168 | *.rdl.data 169 | *.bim.layout 170 | *.bim_*.settings 171 | 172 | # Microsoft Fakes 173 | FakesAssemblies/ 174 | 175 | Comps/ 176 | 177 | 178 | # =========================================================================== 179 | # General 180 | # =========================================================================== 181 | 182 | # Packages # 183 | # it's better to unpack these files and commit the raw source 184 | # git has its own built in compression methods 185 | # ============================================= 186 | *.7z 187 | *.dmg 188 | *.gz 189 | *.iso 190 | *.jar 191 | *.rar 192 | *.tar 193 | *.zip 194 | 195 | # Logs and databases # 196 | # ============================================= 197 | *.log 198 | *.sql 199 | *.sqlite 200 | log.txt* 201 | 202 | # OS generated files # 203 | # ============================================= 204 | .DS_Store 205 | .DS_Store? 206 | *.DS* 207 | ._.DS_Store 208 | .DS_Store 209 | ._* 210 | .Spotlight-V100 211 | .Trashes 212 | ehthumbs.db 213 | Thumbs.db 214 | 215 | # Specific Files & Directories # 216 | # ============================================= 217 | **/elmah_logs/* 218 | 219 | # Front-End # 220 | # ============================================= 221 | Keymaker/Content/web 222 | .sass-cache/ 223 | .tmp/ 224 | npm-debug.log 225 | _static/web 226 | docs/ 227 | .idea/ 228 | GISStuff/ 229 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Paul Schoenfelder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Collections/IEnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | 5 | namespace PluginHost.Extensions.Collections 6 | { 7 | public static class IEnumerableExtensions 8 | { 9 | /// 10 | /// Determine if the source collection is null or empty. 11 | /// 12 | /// The collection to check 13 | public static bool IsEmpty(this IEnumerable @this) 14 | { 15 | if (@this == null) 16 | return true; 17 | 18 | if (!@this.Any()) 19 | return true; 20 | 21 | return false; 22 | } 23 | 24 | /// 25 | /// Filter out elements that match the predicate. 26 | /// 27 | /// A boolean function to match elements against. 28 | public static IEnumerable Reject(this IEnumerable @this, Func predicate) 29 | { 30 | return @this.Where(x => false == predicate(x)); 31 | } 32 | 33 | /// 34 | /// Returns a shuffled version of the source enumerable. 35 | /// 36 | public static IEnumerable Shuffle(this IEnumerable @this) 37 | { 38 | var rand = new Random(); 39 | return @this.OrderBy(x => rand.Next()); 40 | } 41 | 42 | /// 43 | /// Map an action over each element in a collection 44 | /// 45 | /// The type of the items in the collection 46 | /// The collection 47 | /// The mapping function 48 | /// The collection 49 | public static IEnumerable Map(this IEnumerable @this, Action func) 50 | { 51 | var result = @this.ToList(); 52 | foreach (var item in result) 53 | { 54 | func(item); 55 | } 56 | 57 | return result; 58 | } 59 | 60 | /// 61 | /// Map a transformation function over each element in a collection 62 | /// 63 | /// The source type of items in the collection 64 | /// The target type of items in the collection 65 | /// The collection 66 | /// The mapping function 67 | /// An IEnumerable of transformed items 68 | public static IEnumerable Map(this IEnumerable @this, Func func) 69 | { 70 | foreach (var item in @this) 71 | { 72 | yield return func(item); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Comparers/GenericComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace PluginHost.Extensions.Comparers 6 | { 7 | /// 8 | /// Factory class for GenericComparers. 9 | /// 10 | /// The type to create a GenericComparer for 11 | public static class GenericComparer 12 | { 13 | /// 14 | /// Creates a new GenericComparer for type T, using the value extracted 15 | /// via the provided extractor Func. You can optionally define whether 16 | /// to compare in ascending (default), or descending (reverse) order. 17 | /// 18 | /// The type of the value used in the comparison 19 | /// An expression defining how to extract the comparison value from each instance of T 20 | /// True if this comparer will compare in ascending (normal) or descending (reverse) order. 21 | /// A strongly-typed GenericComparer 22 | public static GenericComparer Create(Func extractor, bool inAscendingOrder = true) 23 | where U : IComparable, IComparable 24 | { 25 | return new GenericComparer(extractor, inAscendingOrder); 26 | } 27 | } 28 | 29 | /// 30 | /// Defines a comparer for some type T, given an expression defining 31 | /// how to compare two instances of T. See the examples for more detail. 32 | /// 33 | /// 34 | /// Suppose you have a type called Person, containing a FirstName and LastName. 35 | /// Out of the box, C# does not know how to compare two instances of Person, so how 36 | /// then do you sort a list of Persons? You could implement the 37 | /// or interfaces, but that's tedious if you have many such 38 | /// types, or even worse, if you want to sort Persons in more than one way! 39 | /// 40 | /// GenericComparer does two things for you: First, it allows you to create an 41 | /// / for any type you wish on the fly, even those which already implement those 42 | /// interfaces. You do so by providing an expression which defines how to get the value for the 43 | /// comparison. Secondly, it allows you to easily create multiple comparers over the same type, as 44 | /// well as control the direction of the comparison (in addition to the standard ascending order, 45 | /// you can specify descending order as well, i.e. 10 comes before 5 instead of 5 before 10) 46 | /// 47 | /// To see this in action, we'll use the Person example from up above. I want to sort a collection of Persons 48 | /// in reverse (descending) order, and I want to sort the list based on their full name, a property which does 49 | /// not exist, but which can be expressed easily enough. All I have to do is the following: 50 | /// 51 | /// Func{Person, string} fullName = (p) => string.Format("{0} {1}", p.FirstName, p.LastName); 52 | /// var byDescendingFullName = GenericComparer{Person}.Create(fullName, false); 53 | /// var people = new [] { 54 | /// new Person("Andrew Smith"), 55 | /// new Person("Paul Schoenfelder"), 56 | /// new Person("Paul Anderson") 57 | /// }; 58 | /// people.Sort(byDescendingFullName); 59 | /// var expected = new [] { "Paul Schoenfelder", "Paul Anderson", "Andrew Smith" }; 60 | /// Assert.IsTrue(expected.SequenceEqual(people.Select(fullName))); 61 | /// 62 | /// Used in combination with MultiComparer{T}, you can get an immense amount of power over how you 63 | /// sort collections! 64 | /// 65 | /// The type to compare 66 | /// The type of the value used to perform the comparison 67 | public sealed class GenericComparer : IComparer, IComparer, IEqualityComparer 68 | where U : IComparable, IComparable 69 | { 70 | private readonly Func _extractor; 71 | private readonly bool _inAscendingOrder = true; 72 | 73 | /// 74 | /// Creates a new GenericComparer 75 | /// 76 | /// 77 | /// An expression which extracts the value to use for comparing instances of type . 78 | /// 79 | /// 80 | /// Determines whether to use the default behavior when performing comparisons (ascending order) 81 | /// Defaults to true (sort ascending) if not overridden. 82 | /// 83 | public GenericComparer(Func extractor, bool inAscendingOrder = true) 84 | { 85 | _inAscendingOrder = inAscendingOrder; 86 | _extractor = extractor; 87 | } 88 | 89 | public int Compare(T x, T y) 90 | { 91 | var valX = _extractor(x); 92 | var valY = _extractor(y); 93 | 94 | var result = valX.CompareTo(valY); 95 | if (_inAscendingOrder) 96 | return result; 97 | else 98 | return result * -1; 99 | } 100 | 101 | int IComparer.Compare(object x, object y) 102 | { 103 | if ((x is T) == false) 104 | throw new ArgumentException(string.Format("x is not an instance of type {0}", typeof(T).Name)); 105 | if ((y is T) == false) 106 | throw new ArgumentException(string.Format("y is not an instance of type {0}", typeof(T).Name)); 107 | 108 | return Compare((T) x, (T) y); 109 | } 110 | 111 | public bool Equals(T x, T y) 112 | { 113 | var valX = _extractor(x); 114 | var valY = _extractor(y); 115 | 116 | return valX.Equals(valY); 117 | } 118 | 119 | public int GetHashCode(T obj) 120 | { 121 | var val = _extractor(obj); 122 | if (val == null) 123 | return 0; 124 | 125 | return val.GetHashCode(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Enums/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PluginHost.Extensions.Enums 4 | { 5 | public static class EnumExtensions 6 | { 7 | public static string GetName(this T enumeration) 8 | { 9 | return Enum.GetName(typeof(T), enumeration); 10 | } 11 | 12 | public static T ToEnum(this string name) where T : struct 13 | { 14 | var enumType = typeof(T).Name; 15 | T enumeration; 16 | bool result = Enum.TryParse(name, true, out enumeration); 17 | if (result) 18 | { 19 | return enumeration; 20 | } 21 | else 22 | { 23 | var format = "Invalid {0} name provided: {1}"; 24 | throw new ArgumentException(string.Format(format, enumType, name), "name"); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /PluginHost.Extensions/Functional/Either.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace PluginHost.Extensions.Functional 5 | { 6 | /// 7 | /// Defines the result type of an operation that can either be an error or success value. 8 | /// This is intended for use in places where error handling should be deferred to a higher 9 | /// level. This can reduce the amount of boilerplate required at each level of the API, as 10 | /// only the top level (say a controller) needs to determine what to do with the failure case. 11 | /// 12 | /// Example Usage (failable operation): 13 | /// 14 | /// public static Either> GetStuff() 15 | /// { 16 | /// try 17 | /// { 18 | /// return _db.Stuff.Where(s => s.Things.Contains("foo")).ToList(); 19 | /// } 20 | /// catch (Exception ex) 21 | /// { 22 | /// return new SomeError("Unable to get stuff: " + ex.Message); 23 | /// } 24 | /// } 25 | /// 26 | /// Example Usage (top level consumer): 27 | /// 28 | /// [HttpGet] 29 | /// public HttpResponseMessage GetStuff() 30 | /// { 31 | /// return _provider 32 | /// .GetStuff() 33 | /// .Map(ConvertToViewModel) 34 | /// .Case( 35 | /// error => BadResponse(error.Message), 36 | /// model => OkResponse(model) 37 | /// ); 38 | /// } 39 | /// 40 | /// The above is a very simplistic example, but the point is that each level of your 41 | /// API can transform the potential success value on the way up (via Map), or return 42 | /// a value representing a higher-level error, and each level only has to be concerned 43 | /// with the errors that itself produces. 44 | /// 45 | /// Type of the Error value 46 | /// Type of the Success value 47 | public sealed class Either 48 | { 49 | private readonly bool _isError = false; 50 | private readonly TError _error; 51 | private readonly TSuccess _success; 52 | 53 | public bool IsError { get { return _isError; } } 54 | /// 55 | /// Only for use when absolutely necessary. Use one of the accessor functions 56 | /// in all other cases. 57 | /// 58 | public TError Error { get { return _error; }} 59 | /// 60 | /// Only for use when absolutely necessary. Use one of the accessor functions 61 | /// in all other cases. 62 | /// 63 | public TSuccess Success { get { return _success; }} 64 | 65 | private Either(TError error) 66 | { 67 | _isError = true; 68 | _error = error; 69 | } 70 | 71 | private Either(TSuccess value) 72 | { 73 | _isError = false; 74 | _success = value; 75 | } 76 | 77 | /// 78 | /// Check the type of the value held and invoke the matching handler function 79 | /// 80 | /// The result type of the operation 81 | /// When the Either contains an Error, this function is called. 82 | /// When the Either contains Success, this function is called. 83 | public U Case(Func onError, Func onSuccess) 84 | { 85 | if (_isError) 86 | { 87 | if (onError == null) 88 | throw new ArgumentNullException("onError cannot be null"); 89 | 90 | return onError(_error); 91 | } 92 | else 93 | { 94 | if (onSuccess == null) 95 | throw new ArgumentNullException("onSuccess cannot be null"); 96 | 97 | return onSuccess(_success); 98 | } 99 | } 100 | 101 | /// 102 | /// Check the type of the value held and invoke the matching handler function 103 | /// 104 | /// When the Either contains an Error, this action is called. 105 | /// When the Either contains Success, this action is called. 106 | public void Case(Action onError, Action onSuccess) 107 | { 108 | if (_isError) 109 | { 110 | if (onError == null) 111 | throw new ArgumentNullException("onError cannot be null"); 112 | onError(_error); 113 | } 114 | else 115 | { 116 | if (onSuccess == null) 117 | throw new ArgumentNullException("onSuccess cannot be null"); 118 | onSuccess(_success); 119 | } 120 | } 121 | 122 | /// 123 | /// Async version of Case, where both error and success handlers return a Task which 124 | /// should be executed to unwrap the result value. 125 | /// 126 | public async Task CaseAsync(Func> onError, Func> onSuccess) 127 | { 128 | if (_isError) 129 | { 130 | if (onError == null) 131 | throw new ArgumentNullException("onError cannot be null"); 132 | 133 | return await onError(_error); 134 | } 135 | else 136 | { 137 | if (onSuccess == null) 138 | throw new ArgumentNullException("onSuccess cannot be null"); 139 | 140 | return await onSuccess(_success); 141 | } 142 | } 143 | 144 | /// 145 | /// A variation of CaseAsync, where the success case requires some expensive operation to obtain, 146 | /// which we wish to be delegated to another thread. 147 | /// 148 | public Task CaseAsync(Func onError, Func> onSuccess) 149 | { 150 | return CaseAsync(e => Task.FromResult(onError(e)), onSuccess); 151 | } 152 | 153 | /// 154 | /// A variation of CaseAsync, where the error case requires some expensive operation to be performed, 155 | /// such as logging to a database, which we wish to be delegated to another thread. 156 | /// 157 | public Task CaseAsync(Func> onError, Func onSuccess) 158 | { 159 | return CaseAsync(onError, s => Task.FromResult(onSuccess(s))); 160 | } 161 | 162 | /// 163 | /// Async version of Case, where both error and success cases do not return any value, 164 | /// but require some operation to be performed to handle each case. 165 | /// 166 | public Task CaseAsync(Action onError, Action onSuccess) 167 | { 168 | if (_isError) 169 | { 170 | if (onError == null) 171 | throw new ArgumentNullException("onError cannot be null"); 172 | return Task.Run(() => onError(_error)); 173 | } 174 | else 175 | { 176 | if (onSuccess == null) 177 | throw new ArgumentNullException("onSuccess cannot be null"); 178 | return Task.Run(() => onSuccess(_success)); 179 | } 180 | } 181 | 182 | /// 183 | /// Map a transformation over the Either, which will be applied if the Either 184 | /// contains a Success value. If the Either contains an Error value, then the 185 | /// Error value will be propagated. 186 | /// 187 | /// The type of the Error value 188 | /// The type of the current Success value 189 | /// The type of the transformed Success value 190 | /// A function which takes a TSuccess and transforms it to a new value. 191 | /// A new IEither, where the success type is the result type of the map operation. 192 | public Either Map(Func mapper) 193 | { 194 | if (_isError) 195 | { 196 | return new Either(_error); 197 | } 198 | else 199 | { 200 | return new Either(mapper(_success)); 201 | } 202 | } 203 | 204 | /// 205 | /// Map an asynchronous transformation over the Either, which is only applied if the 206 | /// Either contains a success value. If the Either contains an Error, the Error value 207 | /// is propogated. 208 | /// 209 | public async Task> MapAsync(Func> mapper) 210 | { 211 | if (_isError) 212 | { 213 | return new Either(_error); 214 | } 215 | else 216 | { 217 | var result = await mapper(_success); 218 | return new Either(result); 219 | } 220 | } 221 | 222 | /// 223 | /// Map a transformation over the Either, which will be applied if the Either 224 | /// contains a Success value. If the Either contains an Error value, then the Error 225 | /// value will be propagated. This will flatten a nested Either, so that you do not 226 | /// have to do nested Map/Case calls. 227 | /// 228 | /// The new Success type 229 | /// A function which takes a TSuccess and transforms it to a new value. 230 | public Either FlatMap(Func> mapper) 231 | { 232 | if (_isError) 233 | { 234 | return new Either(_error); 235 | } 236 | else 237 | { 238 | return mapper(_success); 239 | } 240 | } 241 | 242 | /// 243 | /// Map an asynchronous transformation over the Either, which will be applied if the Either 244 | /// contains a Success value. If the Either contains an Error value, then the Error 245 | /// value will be propogated. This will flatten a nested Either, so that you do not 246 | /// have to do nested Map/Case calls. 247 | /// 248 | public async Task> FlatMapAsync(Func>> mapper) 249 | { 250 | if (_isError) 251 | { 252 | return new Either(_error); 253 | } 254 | else 255 | { 256 | return await mapper(_success); 257 | } 258 | } 259 | 260 | /// 261 | /// Allow implicit conversion from TError to Either 262 | /// 263 | /// An instance of TError 264 | public static implicit operator Either(TError error) 265 | { 266 | return new Either(error); 267 | } 268 | 269 | /// 270 | /// Allow implicit conversion from TSuccess to Either 271 | /// 272 | /// An instance of TSuccess 273 | public static implicit operator Either(TSuccess value) 274 | { 275 | return new Either(value); 276 | } 277 | } 278 | 279 | public static class AsyncEitherExtensions 280 | { 281 | /// 282 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 283 | /// to Map. The primary difference between this and MapAsync is that it will not execute 284 | /// the mapping function asynchronously. Best used when you want to map simple transformations 285 | /// over the result of an expensive operation which returns an Either. 286 | /// 287 | public static async Task> Map(this Task> awaitable, Func mapper) 288 | { 289 | var result = await awaitable; 290 | return result.Map(mapper); 291 | } 292 | 293 | /// 294 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 295 | /// to MapAsync on that result. In this way you can use Task-wrapped Either's the same 296 | /// way you do the non-wrapped version. 297 | /// 298 | public static async Task> MapAsync(this Task> awaitable, Func> mapper) 299 | { 300 | var result = await awaitable; 301 | return await result.MapAsync(mapper); 302 | } 303 | 304 | /// 305 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 306 | /// to FlatMap. The primary difference between this and FlatMapAsync is that it will not 307 | /// execute the mapping function asynchronously. Best used when you want to map simple transformations 308 | /// over the result of an expensive operation which returns an Either. 309 | /// 310 | public static async Task> FlatMap(this Task> awaitable, Func> mapper) 311 | { 312 | var result = await awaitable; 313 | return result.FlatMap(mapper); 314 | } 315 | 316 | /// 317 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 318 | /// to FlatMapAsync on that result. In this way you can use Task-wrapped Either's the same 319 | /// way you do the non-wrapped version. 320 | /// 321 | public static async Task> FlatMapAsync(this Task> awaitable, Func>> mapper) 322 | { 323 | var result = await awaitable; 324 | return await result.FlatMapAsync(mapper); 325 | } 326 | 327 | /// 328 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 329 | /// to Case. In this way you can use Task-wrapped Either's the same way you do the 330 | /// non-wrapped version. 331 | /// 332 | public static async Task Case(this Task> awaitable, Func onError, Func onSuccess) 333 | { 334 | var result = await awaitable; 335 | return result.Case(onError, onSuccess); 336 | } 337 | 338 | /// 339 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 340 | /// to Case. In this way you can use Task-wrapped Either's the same way you do the 341 | /// non-wrapped version. 342 | /// 343 | public static async Task Case(this Task> awaitable, Action onError, Action onSuccess) 344 | { 345 | var result = await awaitable; 346 | result.Case(onError, onSuccess); 347 | } 348 | 349 | /// 350 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 351 | /// to CaseAsync on that result. In this way you can use Task-wrapped Either's the same 352 | /// way you do the non-wrapped version. 353 | /// 354 | public static async Task CaseAsync(this Task> awaitable, Func> onError, Func> onSuccess) 355 | { 356 | var result = await awaitable; 357 | return await result.CaseAsync(onError, onSuccess); 358 | } 359 | 360 | /// 361 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 362 | /// to CaseAsync on that result. In this way you can use Task-wrapped Either's the same 363 | /// way you do the non-wrapped version. 364 | /// 365 | public static async Task CaseAsync(this Task> awaitable, Func> onError, Func onSuccess) 366 | { 367 | var result = await awaitable; 368 | return await result.CaseAsync(onError, onSuccess); 369 | } 370 | 371 | /// 372 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 373 | /// to CaseAsync on that result. In this way you can use Task-wrapped Either's the same 374 | /// way you do the non-wrapped version. 375 | /// 376 | public static async Task CaseAsync(this Task> awaitable, Func onError, Func> onSuccess) 377 | { 378 | var result = await awaitable; 379 | return await result.CaseAsync(onError, onSuccess); 380 | } 381 | 382 | /// 383 | /// Given a Task-wrapped Either, chain the result of awaiting that Task into a call 384 | /// to CaseAsync on that result. In this way you can use Task-wrapped Either's the same 385 | /// way you do the non-wrapped version. 386 | /// 387 | public static async Task CaseAsync(this Task> awaitable, Action onError, Action onSuccess) 388 | { 389 | var result = await awaitable; 390 | await result.CaseAsync(onError, onSuccess); 391 | } 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Functional/Option.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PluginHost.Extensions.Functional 4 | { 5 | public static class Option 6 | { 7 | public static Option ToOption(this T @this) 8 | { 9 | if (@this == null) 10 | return Option.None; 11 | else 12 | return Option.Some(@this); 13 | } 14 | } 15 | 16 | [Serializable] 17 | public sealed class Option : IEquatable> 18 | { 19 | private readonly T _value; 20 | private readonly bool _hasValue; 21 | 22 | /// 23 | /// Default empty instance. 24 | /// 25 | public static readonly Option None = new Option(default(T), false); 26 | 27 | /// 28 | /// Returns a non-empty Option. If a null value is provided, 29 | /// an argument exception is thrown. 30 | /// 31 | /// A value of type T 32 | /// 33 | public static Option Some(T value) 34 | { 35 | return new Option(value); 36 | } 37 | 38 | private Option(T item, bool hasValue) 39 | { 40 | _value = item; 41 | _hasValue = hasValue; 42 | } 43 | 44 | private Option(T value) : this(value, true) 45 | { 46 | if (value == null) throw new ArgumentNullException("value"); 47 | } 48 | 49 | /// 50 | /// Gets the underlying value, if it is available 51 | /// 52 | public T Value 53 | { 54 | get 55 | { 56 | if (_hasValue == false) 57 | throw new InvalidOperationException("Accessed Option.Value when HasValue is false. Use Option.GetValueOrDefault instead of Option.Value"); 58 | return _value; 59 | } 60 | } 61 | 62 | /// 63 | /// Gets a value indicating whether this instance has value. 64 | /// 65 | public bool HasValue 66 | { 67 | get { return _hasValue; } 68 | } 69 | 70 | /// 71 | /// Get the stored value, or the default value for it's type 72 | /// 73 | /// 74 | public T GetValueOrDefault() 75 | { 76 | return _hasValue ? _value : default(T); 77 | } 78 | 79 | /// 80 | /// Get the stored value, or return the provided default value 81 | /// 82 | public T GetValueOrDefault(T @default) 83 | { 84 | return _hasValue ? _value : @default; 85 | } 86 | 87 | /// 88 | /// Get the stored value, or the provided default if the Option[T] is None 89 | /// 90 | public T GetValueOrDefault(Func @default) 91 | { 92 | return _hasValue ? _value : @default(); 93 | } 94 | 95 | /// 96 | /// Apply an action to the value, if present 97 | /// 98 | /// 99 | /// 100 | public Option Apply(Action action) 101 | { 102 | if (_hasValue) 103 | action(_value); 104 | return this; 105 | } 106 | 107 | /// 108 | /// Select a new value from the value of the Option, if it exists 109 | /// otherwise provides an instance of Option.None 110 | /// 111 | public Option Select(Func selector) 112 | { 113 | if (_hasValue == false) 114 | return Option.None; 115 | else 116 | { 117 | var selected = selector(_value); 118 | if (selected == null) 119 | return Option.None; 120 | else 121 | return new Option(selected); 122 | } 123 | } 124 | 125 | /// 126 | /// Determines whether the provided Option is equal to the current Option 127 | /// 128 | public bool Equals(Option option) 129 | { 130 | if (ReferenceEquals(null, option)) return false; 131 | if (ReferenceEquals(this, option)) return true; 132 | 133 | if (_hasValue != option._hasValue) return false; 134 | if (_hasValue == false) return true; 135 | return _value.Equals(option._value); 136 | } 137 | 138 | /// 139 | /// Determines whether the provided object is equal to the current Option. 140 | /// 141 | public override bool Equals(object obj) 142 | { 143 | if (ReferenceEquals(null, obj)) return false; 144 | if (ReferenceEquals(this, obj)) return true; 145 | 146 | var maybe = obj as Option; 147 | if (maybe == null) return false; 148 | return Equals(maybe); 149 | } 150 | 151 | public override int GetHashCode() 152 | { 153 | if (_hasValue) 154 | { 155 | // 41 is just an odd prime, the likelihood of encountering it is not high in comparison to 156 | // 0 for example. We just want a good seed value, and 41 is my choice :) 157 | return 41*_value.GetHashCode(); 158 | } 159 | else 160 | { 161 | return 0; 162 | } 163 | } 164 | 165 | public static bool operator ==(Option left, Option right) 166 | { 167 | return Equals(left, right); 168 | } 169 | 170 | public static bool operator !=(Option left, Option right) 171 | { 172 | return !Equals(left, right); 173 | } 174 | 175 | /// 176 | /// Performs an implicit conversion from T to Option[T] 177 | /// 178 | public static implicit operator Option(T item) 179 | { 180 | if (item == null) 181 | return Option.None; 182 | else 183 | return new Option(item); 184 | } 185 | 186 | /// 187 | /// Performs an explicit conversion from Option[T] to T 188 | /// 189 | public static explicit operator T(Option item) 190 | { 191 | if (item == null) throw new ArgumentNullException("item"); 192 | 193 | return item.HasValue ? item.Value : default(T); 194 | } 195 | 196 | /// 197 | /// Returns a string representing the Option's value 198 | /// 199 | public override string ToString() 200 | { 201 | if (_hasValue) 202 | return "Some<" + _value + ">"; 203 | 204 | return "None"; 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Functional/Pipes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PluginHost.Extensions.Functional 4 | { 5 | public static class Pipes 6 | { 7 | public static T Pipe(this U result, Func func) 8 | { 9 | return func(result); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PluginHost.Extensions/IO/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Text; 3 | 4 | namespace PluginHost.Extensions.IO 5 | { 6 | public static class StreamExtensions 7 | { 8 | public static byte[] ReadAllBytes(this Stream stream) 9 | { 10 | using (var reader = new BinaryReader(stream)) 11 | { 12 | var bytes = reader.ReadBytes((int) stream.Length); 13 | return bytes; 14 | } 15 | } 16 | 17 | public static string ReadAsText(this Stream stream) 18 | { 19 | var bytes = ReadAllBytes(stream); 20 | return Encoding.UTF8.GetString(bytes); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Numeric/NumberExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PluginHost.Extensions.Numeric 4 | { 5 | public static class NumberExtensions 6 | { 7 | /// 8 | /// Attempts to convert a string to an integer. 9 | /// Returns a nullable int. Should only be null if the string 10 | /// does not contain a number which can be converted to an int. 11 | /// 12 | /// The string to parse for a number. 13 | /// int? 14 | public static int? SafeConvertInteger(this string numString) 15 | { 16 | int result; 17 | var valid = Int32.TryParse(numString, out result); 18 | if (valid) 19 | return result; 20 | else 21 | return null; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /PluginHost.Extensions/PluginHost.Extensions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {653947DE-DFC6-4327-9AAA-C9EB494BFFF2} 8 | Library 9 | Properties 10 | PluginHost.Extensions 11 | PluginHost.Extensions 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PluginHost.Extensions")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PluginHost.Extensions")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b9ff814b-bafa-49bc-8b8c-2d2d17e59993")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Text/FormatStringTokenParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | 5 | namespace PluginHost.Extensions.Text 6 | { 7 | [DebuggerDisplay("( {Token} )")] 8 | public struct FormattingToken 9 | { 10 | public string Token { get; set; } 11 | public int ArgsIndex { get; set; } 12 | public int StartIndex { get; set; } 13 | public int EndIndex { get; set; } 14 | public int Length { get { return (EndIndex - StartIndex) + 1; } } 15 | } 16 | 17 | public static class FormatStringTokenParser 18 | { 19 | private const short ZERO = 48; 20 | 21 | public static IEnumerable ExtractFormatTokens(this string @this) 22 | { 23 | if (string.IsNullOrWhiteSpace(@this)) 24 | return new FormattingToken[0]; 25 | 26 | var tokens = new List(); 27 | 28 | var insideToken = false; 29 | var startIndex = -1; 30 | var tokenParts = new char[2] {'0','0'}; 31 | var places = tokenParts.Length; 32 | 33 | for (var i = 0; i < @this.Length; i++) 34 | { 35 | if (insideToken) 36 | { 37 | switch (@this[i]) 38 | { 39 | case '}': 40 | // This was just a string with {} in it 41 | if (i - startIndex == 1) 42 | break; 43 | 44 | // We've gathered a complete token 45 | tokens.Add(new FormattingToken() { 46 | StartIndex = startIndex, 47 | EndIndex = i, 48 | ArgsIndex = UnpackInteger(tokenParts), 49 | Token = @this.Substring(startIndex, (i - startIndex) + 1) 50 | }); 51 | break; 52 | case '0': 53 | case '1': 54 | case '2': 55 | case '3': 56 | case '4': 57 | case '5': 58 | case '6': 59 | case '7': 60 | case '8': 61 | case '9': 62 | places--; 63 | tokenParts[places] = @this[i]; 64 | continue; 65 | default: 66 | break; 67 | } 68 | 69 | startIndex = -1; 70 | tokenParts[0] = (char) ZERO; 71 | tokenParts[1] = (char) ZERO; 72 | places = tokenParts.Length; 73 | insideToken = false; 74 | } 75 | else 76 | { 77 | switch (@this[i]) 78 | { 79 | case '{': 80 | startIndex = i; 81 | insideToken = true; 82 | continue; 83 | default: 84 | continue; 85 | } 86 | } 87 | } 88 | 89 | return tokens; 90 | } 91 | 92 | /// 93 | /// Given a number where it's individual digits are stored as elements in a char array: 94 | /// (i.e. 105 == new char[] { 1, 0, 5 }). This function extracts the integer value back out, 95 | /// by iterating over each digit, adding it's value to the accumulator using the formula: 96 | /// 97 | /// value = (digit * 10^(arrayLength - (index + 1))) 98 | /// 99 | /// 100 | private static int UnpackInteger(this char[] chars) 101 | { 102 | if (chars.Length == 0) return 0; 103 | 104 | int result = 0; 105 | for (int i = 0; i < chars.Length; i++) 106 | { 107 | int factor = (int) Math.Pow(10, chars.Length - (i + 1)); 108 | result += (factor * (chars[i] - 48)); 109 | } 110 | 111 | return result; 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /PluginHost.Extensions/Text/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace PluginHost.Extensions.Text 8 | { 9 | public static class StringExtensions 10 | { 11 | /// 12 | /// Determine if two strings are equal, optionally ignoring casing. 13 | /// The invariant culture is used for comparisons. 14 | /// 15 | /// A string 16 | /// The string to compare with 17 | /// True to ignore casing in the two strings during comparison 18 | /// 19 | public static bool IsEqualTo(this string s, string s2, bool ignoreCase = true) 20 | { 21 | var comparison = ignoreCase 22 | ? StringComparison.InvariantCultureIgnoreCase 23 | : StringComparison.InvariantCulture; 24 | return String.Compare(s, s2, comparison) == 0; 25 | } 26 | 27 | /// 28 | /// Returns true if the string starts with any of the strings passed in the array. 29 | /// 30 | /// the string to test 31 | /// Array of strings to test for .StartsWith 32 | /// true if the string starts with any item passed in the array. 33 | public static bool StartsWithAny(this string s, IEnumerable stringsItCouldStartWith) 34 | { 35 | var result = false; 36 | var listOfItemsToTest = new List(stringsItCouldStartWith); 37 | var i = 0; 38 | while (!result && i < listOfItemsToTest.Count) 39 | { 40 | result = s.StartsWith(listOfItemsToTest[i]); 41 | i++; 42 | } 43 | 44 | return result; 45 | } 46 | 47 | public static bool Contains(this string s, string compareValue, StringComparison comparisonMethod) 48 | { 49 | return s.IndexOf(compareValue, comparisonMethod) > -1; 50 | } 51 | 52 | /// 53 | /// Will collapse multiple spaces into one, and trim any trailing whitespace 54 | /// 55 | public static string CollapseAndTrim(this string s) 56 | { 57 | return Regex.Replace(s, @"\s+", " ").Trim(); 58 | } 59 | 60 | public static string EnsurePostfix(this string s, string post) 61 | { 62 | if (string.IsNullOrWhiteSpace(s) || string.IsNullOrWhiteSpace(post)) return s; 63 | return s.EndsWith(post) ? s : s + post; 64 | } 65 | 66 | /// 67 | /// Ensures a given string has a specific prefix. 68 | /// Will check if already exists, if not, add it. 69 | /// 70 | /// String to check prefix on 71 | /// Prefix to ensure is there 72 | /// string with prefix 73 | public static string EnsurePrefix(this string s, string prefix) 74 | { 75 | if (String.IsNullOrWhiteSpace(s)) return s; 76 | if (!s.StartsWith(prefix)) 77 | { 78 | s = prefix + s; 79 | } 80 | return s; 81 | } 82 | 83 | /// 84 | /// Returns the string split into individual lines. 85 | /// 86 | /// The string to split. 87 | /// An array of all the lines in the string. 88 | public static string[] Lines(this string s) 89 | { 90 | return s.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries); 91 | } 92 | 93 | /// 94 | /// Wraps the string at the given column index. 95 | /// 96 | /// The string to process. 97 | /// The column at which to wrap the string. 98 | /// A stream of strings representing the wrapped lines. String.Length is <= column. 99 | public static IEnumerable WordWrapAt(this string s, int column) 100 | { 101 | char[] whitespaceCharacters = { ' ', '\n', '\t', '\r' }; 102 | 103 | foreach (var line in s.Lines()) 104 | { 105 | if (line.Length <= column) 106 | { 107 | yield return line; 108 | } 109 | else 110 | { 111 | // the line is longer than the allowed column width, so we start 112 | // at the column index, then search backwards for the last whitespace 113 | // char; we return that string, then advance the marker and repeat 114 | // if there is no whitespace char, then just return the whole string 115 | int mark = 0; 116 | for (int i = line.LastIndexOfAny(whitespaceCharacters, column); 117 | i < line.Length && i >= 0 && mark < i; 118 | mark = i, i = line.LastIndexOfAny(whitespaceCharacters, Math.Min(i + column - 1, line.Length - 1))) 119 | { 120 | yield return line.Substring(mark, i - mark); 121 | } 122 | if (mark < line.Length) 123 | { 124 | yield return line.Substring(mark, line.Length - mark); 125 | } 126 | } 127 | } 128 | } 129 | 130 | public static string ValueOrDefault(this string s, string @default) 131 | { 132 | return string.IsNullOrWhiteSpace(s) ? @default : s; 133 | } 134 | 135 | /// 136 | /// Duplicates a given string `count` many times. 137 | /// 138 | public static string Duplicate(this string s, int count) 139 | { 140 | return String.Join("", Enumerable.Range(0, count).Select(i => s).ToArray()); 141 | } 142 | 143 | public static string Indent(this string s, int tabs) 144 | { 145 | var lines = s.Lines(); 146 | var sb = new StringBuilder(); 147 | foreach (var line in lines) 148 | { 149 | sb.AppendFormat("{0}{1}{2}", Duplicate("\t", tabs), line, Environment.NewLine); 150 | } 151 | return sb.ToString(); 152 | } 153 | 154 | public static string Inject(this string s, params object[] args) 155 | { 156 | return string.Format(s, args); 157 | } 158 | 159 | public static string StripHtml(this string s) 160 | { 161 | var pattern = @"(<[^>]+/?>)|(<[^>]+>[^<]*]+>)"; 162 | var stripHtml = new Regex(pattern, RegexOptions.CultureInvariant | RegexOptions.Multiline); 163 | 164 | return stripHtml.Replace(s, ""); 165 | } 166 | 167 | public static string ToUrl(this string s) 168 | { 169 | var bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(s); 170 | s = System.Text.Encoding.ASCII.GetString(bytes); 171 | s = s.ToLower(); 172 | s = Regex.Replace(s, @"[^a-z0-9\s-]", ""); // Remove all non valid chars 173 | s = Regex.Replace(s, @"\s+", " ").Trim(); // convert multiple spaces into one space 174 | s = Regex.Replace(s, @"\s", "-"); // Replace spaces by dashes 175 | return s; 176 | } 177 | 178 | public static string Capitalize(this string s) 179 | { 180 | if (string.IsNullOrWhiteSpace(s)) 181 | return s; 182 | 183 | var first = Convert.ToInt32(s[0]); 184 | char upper; 185 | if (first >= 97 && first <= 122) 186 | upper = Convert.ToChar(first - 32); 187 | else 188 | upper = s[0]; 189 | 190 | return string.Format("{0}{1}", upper, s.Substring(1, s.Length - 1)); 191 | } 192 | 193 | /// 194 | /// Extract a string slice from a StringBuilder. This can be useful to check for 195 | /// trailing characters, etc. 196 | /// 197 | /// The StringBuilder to slice from. 198 | /// The index to start slicing from. 199 | /// 200 | /// The length of the slice. If no value is given, or the value is negative, 201 | /// the entire string starting from the startIndex will be returned. 202 | /// 203 | public static string Slice(this StringBuilder s, int startIndex, int length = -1) 204 | { 205 | bool infinite = length < 0 || length == s.Length; 206 | 207 | // We're asking for the whole string 208 | if (startIndex <= 0 && infinite) 209 | return s.ToString(); 210 | // There is no string to slice, return empty 211 | if (startIndex > s.Length - 1) 212 | return string.Empty; 213 | 214 | // Handle infinite slices where the slice is less than 500 characters. 215 | // 500 characters is roughly where StringBuilder performance begins to 216 | // overtake raw string concatenation. 217 | var str = ""; 218 | if (infinite && s.Length < 500) 219 | { 220 | for (var i = startIndex; i < s.Length; i++) 221 | str += s[i]; 222 | return str; 223 | } 224 | 225 | var sb = new StringBuilder(string.Empty); 226 | // Since this is an infinite slice, we don't need the extra 227 | // conditinal branching in the inner loop. 228 | if (infinite) 229 | { 230 | for (var i = startIndex; i < s.Length; i++) 231 | { 232 | sb.Append(s[i]); 233 | } 234 | return sb.ToString(); 235 | } 236 | // Loop until we've collected the entire slice requested. 237 | else 238 | { 239 | var chars = 0; 240 | for (var i = startIndex; i < s.Length; i++) 241 | { 242 | if (chars < length) 243 | { 244 | sb.Append(s[i]); 245 | chars++; 246 | continue; 247 | } 248 | 249 | break; 250 | } 251 | 252 | return sb.ToString(); 253 | } 254 | } 255 | 256 | /// 257 | /// Remove occurrances of a given trailing string from the current StringBuilder. 258 | /// 259 | /// The current StringBuilder 260 | /// The string to remove (if it exists) 261 | public static StringBuilder RemoveTrailing(this StringBuilder s, string trailing) 262 | { 263 | if (string.IsNullOrWhiteSpace(trailing)) 264 | return s; 265 | 266 | var startIndex = s.Length - trailing.Length; 267 | if (startIndex < 0) 268 | return s; 269 | 270 | var length = trailing.Length; 271 | if (s.Slice(startIndex, length).Equals(trailing)) 272 | { 273 | return s.Remove(startIndex, length); 274 | } 275 | else return s; 276 | } 277 | } 278 | } -------------------------------------------------------------------------------- /PluginHost.Extensions/Time/DateTimeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using Microsoft.SqlServer.Server; 7 | 8 | namespace PluginHost.Extensions.Time 9 | { 10 | public static class DateTimeExtensions 11 | { 12 | private static readonly DateTimeFormatInfo _formatInfo = DateTimeFormatInfo.InvariantInfo; 13 | private static readonly DateTimeStyles _formatStyle = DateTimeStyles.None; 14 | 15 | public struct Formats 16 | { 17 | public const string ISO_8601 = "yyyy-MM-ddTHH:mm:ssK"; 18 | } 19 | 20 | /// 21 | /// The first moment of the Unix epoch. (1/1/1970) 22 | /// 23 | public static DateTime Epoch 24 | { 25 | get { return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); } 26 | } 27 | 28 | /// 29 | /// Returns the humanized relative difference between 30 | /// the provided date and right now. 31 | /// 32 | /// Examples: 10 minutes ago, 45 seconds from now, 2 years ago 33 | /// 34 | public static string Humanize(this DateTime date) 35 | { 36 | var now = DateTime.UtcNow; 37 | var shifted = date.Kind == DateTimeKind.Utc || date.Kind == DateTimeKind.Unspecified 38 | ? date 39 | : date.ToUniversalTime(); 40 | var timeSpan = now.Subtract(shifted); 41 | 42 | var interval = "0"; 43 | var magnitude = "seconds"; 44 | var modifer = now.Ticks > shifted.Ticks ? "ago" : "from now"; 45 | var format = "{0} {1} {2}"; 46 | 47 | if (timeSpan.TotalSeconds < 60) 48 | { 49 | magnitude = "seconds"; 50 | interval = timeSpan.TotalSeconds.ToString("#"); 51 | } 52 | else if (timeSpan.TotalMinutes < 60) 53 | { 54 | magnitude = "minutes"; 55 | interval = timeSpan.TotalMinutes.ToString("#"); 56 | } 57 | else if (timeSpan.TotalHours < 24) 58 | { 59 | magnitude = "hours"; 60 | interval = timeSpan.TotalHours.ToString("#"); 61 | } 62 | else if (timeSpan.TotalDays < 365) 63 | { 64 | magnitude = "days"; 65 | interval = timeSpan.TotalDays.ToString("#"); 66 | } 67 | else 68 | { 69 | magnitude = "years"; 70 | interval = (now.Year - shifted.Year).ToString("#"); 71 | } 72 | 73 | return string.Format(format, interval, magnitude, modifer); 74 | } 75 | 76 | /// 77 | /// Converts a date time to an ISO 8601 formatted string, using the DateTimeKind 78 | /// to format the timezone appropriately. Kind defaults to Local. 79 | /// 80 | /// Example: 2007-08-13T16:48:01+0300 81 | /// 82 | public static string ToISO8601(this DateTime date, DateTimeKind kind = DateTimeKind.Local) 83 | { 84 | var kinded = new DateTime(date.Ticks, kind); 85 | return kinded.ToString(Formats.ISO_8601); 86 | } 87 | 88 | /// 89 | /// Converts a date time to an ISO 8601 formatted string, shifting the date 90 | /// to UTC/Zulu. 91 | /// 92 | /// Example: 2007-08-13T16:48:01Z 93 | /// 94 | public static string ToISO8601z(this DateTime date) 95 | { 96 | return ToISO8601(date.ToUniversalTime(), DateTimeKind.Utc); 97 | } 98 | 99 | /// 100 | /// Returns the date suffix (e.g., 'st', 'nd', 'rd', 'th') as a string 101 | /// 102 | public static string GetDaySuffix(this DateTime d) 103 | { 104 | switch (d.Day) 105 | { 106 | case 31: case 21: case 1: return "st"; 107 | case 22: case 2: return "nd"; 108 | case 23: case 3: return "rd"; 109 | default: return "th"; 110 | } 111 | } 112 | 113 | /// 114 | /// Attempt to parse a DateTime from a string, and return either the successfully 115 | /// parsed value, or use the provided default or DateTime.MinValue if no default was 116 | /// provided. 117 | /// 118 | /// The date string to parse 119 | /// The default date to use. If this isn't provided, DateTime.MinValue is used. 120 | public static DateTime ParseOrDefault(string date, DateTime? @default = null) 121 | { 122 | var result = DateTime.MinValue; 123 | bool parsed = DateTime.TryParse(date, out result); 124 | if (parsed) 125 | return result; 126 | else 127 | return @default.HasValue ? @default.Value : DateTime.MinValue; 128 | } 129 | 130 | /// 131 | /// Parses a date string as an ISO8601-formatted DateTime. If the format is incorrect, 132 | /// or the input string is invalid, DateTime.MinValue will be returned. 133 | /// 134 | /// The date string to parse. 135 | public static DateTime ParseISO8601(string date, DateTime? @default = null) 136 | { 137 | if (string.IsNullOrWhiteSpace(date)) 138 | return @default.HasValue ? @default.Value : DateTime.MinValue; 139 | 140 | DateTime parsed; 141 | var valid = DateTime.TryParseExact(date, Formats.ISO_8601, _formatInfo, _formatStyle, out parsed); 142 | 143 | if (valid) 144 | return parsed; 145 | 146 | return @default.HasValue ? @default.Value : DateTime.MinValue; 147 | } 148 | 149 | public static bool IsFuture(this DateTime @this, bool utc = true) 150 | { 151 | return @this.Ticks > (utc ? DateTime.UtcNow : DateTime.Now).Ticks; 152 | } 153 | 154 | public static bool IsPast(this DateTime @this, bool utc = true) 155 | { 156 | // We consider the current tick to be in the past, since the comparison will take at least one tick 157 | return @this.Ticks <= (utc ? DateTime.UtcNow : DateTime.Now).Ticks; 158 | } 159 | 160 | /// 161 | /// Allows us to use natural language to describe future points in time 162 | /// Example: 163 | /// 3.Minutes().From().Now(); // Where .Minutes() returns a TimeSpan 164 | /// or 165 | /// var oneMinutes = TimeSpan.FromMinutes(1); 166 | /// var nextRun = oneMinute.From().Now(); 167 | /// 168 | public static From From(this TimeSpan @this) 169 | { 170 | return new From(@this); 171 | } 172 | 173 | /// 174 | /// Determine whether a date falls within the range defined by two dates. You can specify the 175 | /// granularity of the DateTime comparison using the DateTimeGranularity enum. 176 | /// 177 | /// Examples: 178 | /// augustEightAtTwo.WithinRange(augustEightAtFive, augustNine, DateTimeGranularity.Day) #=> true 179 | /// augustEightAtTwo.WithinRange(augustEightAtFive, augustNine, DateTimeGranularity.Hour) #=> false 180 | /// 181 | /// The date to check 182 | /// The date marking the beginning of the range 183 | /// The date marking the end of the range 184 | /// The granularity in time units to use when doing comparisons. 185 | public static bool WithinRange(this DateTime date, DateTime start, DateTime end, DateTimeGranularity granularity = DateTimeGranularity.Second) 186 | { 187 | DateTime rounded = date; 188 | switch (granularity) 189 | { 190 | case DateTimeGranularity.Minute: 191 | rounded = date.RoundNearest(1.Minutes()); 192 | return rounded >= start.RoundNearest(1.Minutes()) && rounded <= end.RoundNearest(1.Minutes()); 193 | case DateTimeGranularity.Hour: 194 | rounded = date.RoundNearest(1.Hours()); 195 | return rounded >= start.RoundNearest(1.Hours()) && rounded <= end.RoundNearest(1.Hours()); 196 | case DateTimeGranularity.Day: 197 | rounded = date.RoundNearest(1.Days()); 198 | return rounded >= start.RoundNearest(1.Days()) && rounded <= end.RoundNearest(1.Days()); 199 | case DateTimeGranularity.Second: 200 | default: 201 | rounded = date.RoundNearest(1.Seconds()); 202 | return rounded >= start.RoundNearest(1.Seconds()) && rounded <= end.RoundNearest(1.Seconds()); 203 | } 204 | } 205 | 206 | /// 207 | /// Round a date up to the nearest interval specified by the TimeSpan. 208 | /// Returns a new DateTime object. 209 | /// 210 | /// Usage: 211 | /// > var date = new DateTime(2014, 8, 14, 12, 26, 59, 59); 212 | /// > date.RoundUp(5.Minutes()) 213 | /// DateTime(2014, 8, 14, 12, 30, 0, 0) 214 | /// > date.RoundUp(1.Minutes()) 215 | /// DateTime(2014, 8, 14, 12, 27, 0, 0) 216 | /// 217 | /// The date to round. 218 | /// The timespan defining the interval to round by. 219 | /// DateTime 220 | public static DateTime RoundUp(this DateTime date, TimeSpan time) 221 | { 222 | return new DateTime(((date.Ticks + time.Ticks - 1)/time.Ticks)*time.Ticks); 223 | } 224 | 225 | /// 226 | /// Round a date to the nearest interval specified by the TimeSpan. 227 | /// Returns a new DateTime object. 228 | /// 229 | /// Usage: 230 | /// > var date = new DateTime(2014, 8, 14, 12, 26, 59, 59); 231 | /// > date.RoundNearest(5.Minutes()) 232 | /// DateTime(2014, 8, 14, 12, 25, 0, 0) 233 | /// > date.RoundNearest(1.Hours()) 234 | /// DateTime(2014, 8, 14, 12, 0, 0, 0) 235 | /// 236 | /// The date to round. 237 | /// The timespan defining the interval to round by. 238 | /// DateTime 239 | public static DateTime RoundNearest(this DateTime date, TimeSpan time) 240 | { 241 | int f = 0; 242 | double m = (double) (date.Ticks%time.Ticks)/time.Ticks; 243 | if (m >= 0.5) 244 | f = 1; 245 | return new DateTime(((date.Ticks / time.Ticks) + f) * time.Ticks); 246 | } 247 | 248 | /// 249 | /// Get the smallest DateTime value allowed by SQL Server. 250 | /// 251 | /// Useful in comparisons when validating whether a DateTime 252 | /// is valid for storing in SQL. 253 | /// 254 | /// DateTime 255 | public static DateTime MinSqlValue() 256 | { 257 | return DateTime.MinValue.AddYears(1752); 258 | } 259 | 260 | /// 261 | /// Returns a TimeSpan representing `num` milliseconds. 262 | /// 263 | /// The number of milliseconds 264 | /// TimeSpan 265 | public static TimeSpan Milliseconds(this int num) 266 | { 267 | return new TimeSpan(0, 0, 0, 0, num); 268 | } 269 | 270 | /// 271 | /// Returns a TimeSpan representing `num` seconds. 272 | /// 273 | /// The number of seconds 274 | /// TimeSpan 275 | public static TimeSpan Seconds(this int num) 276 | { 277 | return new TimeSpan(0, 0, num); 278 | } 279 | 280 | /// 281 | /// Returns a TimeSpan representing `num` minutes. 282 | /// 283 | /// The number of minutes 284 | /// TimeSpan 285 | public static TimeSpan Minutes(this int num) 286 | { 287 | return new TimeSpan(0, num, 0); 288 | } 289 | 290 | /// 291 | /// Returns a TimeSpan representing `num` hours. 292 | /// 293 | /// The number of hours 294 | /// TimeSpan 295 | public static TimeSpan Hours(this int num) 296 | { 297 | return new TimeSpan(num, 0, 0); 298 | } 299 | 300 | /// 301 | /// Returns a TimeSpan representing `num` days. 302 | /// 303 | /// The number of days 304 | /// TimeSpan 305 | public static TimeSpan Days(this int num) 306 | { 307 | return new TimeSpan(num, 0, 0, 0); 308 | } 309 | } 310 | 311 | /// 312 | /// Defines valid units of time which can be used 313 | /// when fuzzily comparing dates. For example, 314 | /// when you want to consider two dates equivalent 315 | /// if they occur on the same day, or within the same 316 | /// hour. 317 | /// 318 | public enum DateTimeGranularity 319 | { 320 | Second, 321 | Minute, 322 | Hour, 323 | Day 324 | } 325 | 326 | public class From 327 | { 328 | private readonly TimeSpan _time; 329 | 330 | public DateTime Now() 331 | { 332 | return DateTime.Now.Add(_time); 333 | } 334 | 335 | public DateTime UtcNow() 336 | { 337 | return DateTime.UtcNow.Add(_time); 338 | } 339 | 340 | public From(TimeSpan time) 341 | { 342 | _time = time; 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /PluginHost.Heartbeat/HeartbeatTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using PluginHost.Extensions.Time; 4 | using PluginHost.Interface.Tasks; 5 | using PluginHost.Interface.Logging; 6 | 7 | namespace PluginHost.Heartbeat 8 | { 9 | public class HeartbeatTask : ScheduledTask 10 | { 11 | public override bool IsInitialized { get; protected set; } 12 | protected override IEventBus EventBus { get; set; } 13 | protected override ILogger Logger { get; set; } 14 | 15 | public HeartbeatTask(ILogger logger, IEventBus eventBus) 16 | : base("Heartbeat", 5.Seconds(), quiet: true) 17 | { 18 | Logger = logger; 19 | EventBus = eventBus; 20 | } 21 | 22 | public override void Init() 23 | { 24 | IsInitialized = true; 25 | Logger.Info("Heartbeat plugin loaded!"); 26 | } 27 | 28 | protected override void Execute() 29 | { 30 | var isoDate = DateTime.UtcNow.ToISO8601z(); 31 | Logger.Info("{0} - All systems normal.", isoDate); 32 | } 33 | 34 | protected override void Kill(bool brutalKill) 35 | { 36 | var isoDate = DateTime.UtcNow.ToISO8601z(); 37 | Logger.Info("{0} - Heartbeat stopped."); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /PluginHost.Heartbeat/PluginHost.Heartbeat.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {58513877-F118-4FEC-ABE3-6A34D0111BF8} 8 | Library 9 | Properties 10 | PluginHost.Heartbeat 11 | PluginHost.Heartbeat 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {653947de-dfc6-4327-9aaa-c9eb494bfff2} 48 | PluginHost.Extensions 49 | 50 | 51 | {19b4a837-8e9a-47dc-8a59-1c8a39c42537} 52 | PluginHost.Interface 53 | 54 | 55 | 56 | 63 | -------------------------------------------------------------------------------- /PluginHost.Heartbeat/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PluginHost.Heartbeat")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PluginHost.Heartbeat")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("f28a11ce-47a4-49ec-8d10-e51986e30f3b")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PluginHost.Interface/Logging/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PluginHost.Interface.Logging 4 | { 5 | public interface ILogger 6 | { 7 | void Trace(string message); 8 | void Trace(string message, params object[] args); 9 | 10 | void Info(string message); 11 | void Info(string message, params object[] args); 12 | 13 | void Success(string message); 14 | void Success(string message, params object[] args); 15 | 16 | void Warn(string message); 17 | void Warn(string message, params object[] args); 18 | 19 | void Alert(string message); 20 | void Alert(string message, params object[] args); 21 | 22 | void Error(string message); 23 | void Error(string message, params object[] args); 24 | void Error(Exception ex); 25 | void Error(Exception ex, string message, params object[] args); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PluginHost.Interface/PluginHost.Interface.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {19B4A837-8E9A-47DC-8A59-1C8A39C42537} 8 | Library 9 | Properties 10 | PluginHost.Interface 11 | PluginHost.Interface 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | ..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll 37 | 38 | 39 | ..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll 40 | 41 | 42 | ..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll 43 | 44 | 45 | ..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | {653947de-dfc6-4327-9aaa-c9eb494bfff2} 68 | PluginHost.Extensions 69 | 70 | 71 | 72 | 73 | 74 | 75 | 82 | -------------------------------------------------------------------------------- /PluginHost.Interface/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PluginHost.Interface")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PluginHost.Interface")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("b93b1ba7-d288-4b6f-9e2e-ef890a0fbbf1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PluginHost.Interface/Shell/Command.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | 5 | namespace PluginHost.Interface.Shell 6 | { 7 | public abstract class Command : IShellCommand 8 | { 9 | public string Name { get; private set; } 10 | public string Description { get; private set; } 11 | 12 | protected Command(string name, string description = "No description.") 13 | { 14 | Name = name; 15 | Description = description; 16 | } 17 | 18 | public abstract bool CanExecute(ShellInput input); 19 | public abstract void Execute(params string[] arguments); 20 | 21 | protected void WriteColor(ConsoleColor color, string text) 22 | { 23 | Console.ForegroundColor = color; 24 | Console.Write(text); 25 | Console.ResetColor(); 26 | } 27 | 28 | protected void WriteColor(ConsoleColor color, string format, params object[] args) 29 | { 30 | Console.ForegroundColor = color; 31 | Console.Write(format, args); 32 | Console.ResetColor(); 33 | } 34 | 35 | protected static object CoerceArgument(Type requiredType, string inputValue) 36 | { 37 | var requiredTypeCode = Type.GetTypeCode(requiredType); 38 | var exceptionMessage = string.Format( 39 | "Cannnot coerce the input argument {0} to required type {1}", 40 | inputValue, 41 | requiredType.Name 42 | ); 43 | 44 | object result = null; 45 | switch (requiredTypeCode) 46 | { 47 | case TypeCode.String: 48 | result = inputValue; 49 | break; 50 | 51 | case TypeCode.Int16: 52 | short number16; 53 | if (Int16.TryParse(inputValue, out number16)) 54 | result = number16; 55 | else 56 | throw new ArgumentException(exceptionMessage); 57 | break; 58 | 59 | case TypeCode.Int32: 60 | int number32; 61 | if (Int32.TryParse(inputValue, out number32)) 62 | result = number32; 63 | else 64 | throw new ArgumentException(exceptionMessage); 65 | break; 66 | 67 | case TypeCode.Int64: 68 | long number64; 69 | if (Int64.TryParse(inputValue, out number64)) 70 | result = number64; 71 | else 72 | throw new ArgumentException(exceptionMessage); 73 | break; 74 | 75 | case TypeCode.Boolean: 76 | bool trueFalse; 77 | if (bool.TryParse(inputValue, out trueFalse)) 78 | result = trueFalse; 79 | else 80 | throw new ArgumentException(exceptionMessage); 81 | break; 82 | 83 | case TypeCode.Byte: 84 | byte byteValue; 85 | if (byte.TryParse(inputValue, out byteValue)) 86 | result = byteValue; 87 | else 88 | throw new ArgumentException(exceptionMessage); 89 | break; 90 | 91 | case TypeCode.Char: 92 | char charValue; 93 | if (char.TryParse(inputValue, out charValue)) 94 | result = charValue; 95 | else 96 | throw new ArgumentException(exceptionMessage); 97 | break; 98 | 99 | case TypeCode.DateTime: 100 | DateTime dateValue; 101 | if (DateTime.TryParse(inputValue, out dateValue)) 102 | result = dateValue; 103 | else 104 | throw new ArgumentException(exceptionMessage); 105 | break; 106 | 107 | case TypeCode.Decimal: 108 | decimal decimalValue; 109 | if (Decimal.TryParse(inputValue, out decimalValue)) 110 | result = decimalValue; 111 | else 112 | throw new ArgumentException(exceptionMessage); 113 | break; 114 | 115 | case TypeCode.Double: 116 | double doubleValue; 117 | if (Double.TryParse(inputValue, out doubleValue)) 118 | result = doubleValue; 119 | else 120 | throw new ArgumentException(exceptionMessage); 121 | break; 122 | 123 | case TypeCode.Single: 124 | Single singleValue; 125 | if (Single.TryParse(inputValue, out singleValue)) 126 | result = singleValue; 127 | else 128 | throw new ArgumentException(exceptionMessage); 129 | break; 130 | 131 | case TypeCode.UInt16: 132 | UInt16 uInt16Value; 133 | if (UInt16.TryParse(inputValue, out uInt16Value)) 134 | result = uInt16Value; 135 | else 136 | throw new ArgumentException(exceptionMessage); 137 | break; 138 | 139 | case TypeCode.UInt32: 140 | UInt32 uInt32Value; 141 | if (UInt32.TryParse(inputValue, out uInt32Value)) 142 | result = uInt32Value; 143 | else 144 | throw new ArgumentException(exceptionMessage); 145 | break; 146 | 147 | case TypeCode.UInt64: 148 | UInt64 uInt64Value; 149 | if (UInt64.TryParse(inputValue, out uInt64Value)) 150 | result = uInt64Value; 151 | else 152 | throw new ArgumentException(exceptionMessage); 153 | break; 154 | 155 | default: 156 | throw new ArgumentException(exceptionMessage); 157 | } 158 | return result; 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /PluginHost.Interface/Shell/IShellCommand.cs: -------------------------------------------------------------------------------- 1 | namespace PluginHost.Interface.Shell 2 | { 3 | public interface IShellCommand 4 | { 5 | /// 6 | /// The name of this command 7 | /// 8 | string Name { get; } 9 | /// 10 | /// Describes this commands functionality 11 | /// 12 | string Description { get; } 13 | /// 14 | /// Given a user input string, this method determines whether or 15 | /// not this command instance can handle that input. 16 | /// 17 | /// The command + args entered by the user 18 | /// Boolean 19 | bool CanExecute(ShellInput input); 20 | /// 21 | /// Given a user input string, this method executes the command 22 | /// based on that input. 23 | /// 24 | /// The arguments provided by the user 25 | void Execute(params string[] args); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /PluginHost.Interface/Shell/ShellInput.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace PluginHost.Interface.Shell 4 | { 5 | public struct ShellInput 6 | { 7 | public bool IsValid { get; set; } 8 | public string Command { get; set; } 9 | public List Arguments { get; set; } 10 | 11 | public static ShellInput Valid(string command) 12 | { 13 | return new ShellInput() 14 | { 15 | IsValid = true, 16 | Command = command, 17 | Arguments = new List() 18 | }; 19 | } 20 | 21 | public static ShellInput Invalid(string input) 22 | { 23 | return new ShellInput() 24 | { 25 | IsValid = false, 26 | Command = input, 27 | Arguments = new List() 28 | }; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PluginHost.Interface/Tasks/IEventBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PluginHost.Interface.Tasks 4 | { 5 | /// 6 | /// Defines the client API for an observable event publisher. 7 | /// 8 | public interface IEventBus : IDisposable 9 | { 10 | /// 11 | /// Subscribe to a type of event. 12 | /// 13 | /// The type of event to subscribe to 14 | /// The subscribing instance 15 | /// 16 | /// The subscription as an IDisposable. 17 | /// Disposing the subscription will unsubscribe the subscriber from further events. 18 | /// 19 | IDisposable Subscribe(IObserver subscriber) 20 | where TEvent : class; 21 | /// 22 | /// Subscribe to a type of event and apply a eventFilter over the observable stream of events 23 | /// to further refine what events are received by the subscriber. 24 | /// 25 | /// The type of event to subscribe to 26 | /// The subscribing instance 27 | /// 28 | /// A function which takes in an observable, and returns a new one with additional 29 | /// filters applied to it. Use this to refine what events are received by the subscriber. 30 | /// 31 | /// 32 | /// The subscription as an IDisposable. 33 | /// Disposing the subscription will unsubscribe the subscriber from further events. 34 | /// 35 | IDisposable Subscribe(IObserver subscriber, Func, IObservable> eventFilter) 36 | where TEvent : class; 37 | /// 38 | /// Publish a message to all subscribers of the given type of event. 39 | /// 40 | /// The type of event 41 | /// The event object to push to all subscribers. 42 | void Publish(TEvent @event) 43 | where TEvent : class; 44 | /// 45 | /// Stop the publisher from publishing further messages. It is recommended that 46 | /// publishers call the OnCompleted event of all subscriptions to ensure that 47 | /// subscribers are able to clean up their resources and dispose their subscriptions. 48 | /// 49 | void Stop(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /PluginHost.Interface/Tasks/ITask.cs: -------------------------------------------------------------------------------- 1 | namespace PluginHost.Interface.Tasks 2 | { 3 | public interface ITask 4 | { 5 | /// 6 | /// Has this task been initialized? 7 | /// 8 | bool IsInitialized { get; } 9 | /// 10 | /// Has this task been started? 11 | /// 12 | bool IsStarted { get; } 13 | /// 14 | /// Is this task currently executing? 15 | /// 16 | bool IsExecuting { get; } 17 | /// 18 | /// Initializes this task for execution 19 | /// 20 | void Init(); 21 | /// 22 | /// Start executing this task 23 | /// 24 | void Start(); 25 | /// 26 | /// Stop executing this task. 27 | /// 28 | /// Whether task execution should be brutally killed or not 29 | void Stop(bool brutalKill); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /PluginHost.Interface/Tasks/ObserverTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using PluginHost.Extensions.Time; 4 | using PluginHost.Interface.Logging; 5 | 6 | namespace PluginHost.Interface.Tasks 7 | { 8 | /// 9 | /// The abstract base class for tasks which should be executed based on some event. 10 | /// This type of task is useful for responding to events published by other components 11 | /// of the system, such as receipt of a remote command. 12 | /// 13 | /// The type of event to subscribe to 14 | public abstract class ObserverTask : IObserver, ITask 15 | where T : class 16 | { 17 | private bool _shuttingDown = false; 18 | private IDisposable _subscription; 19 | private readonly string _description; 20 | private readonly bool _quiet; 21 | 22 | public abstract bool IsInitialized { get; protected set; } 23 | public bool IsStarted { get; private set; } 24 | public bool IsExecuting { get; private set; } 25 | protected abstract IEventBus EventBus { get; set; } 26 | protected abstract ILogger Logger { get; set; } 27 | 28 | /// 29 | /// Initializes a new observer task 30 | /// 31 | /// The text description for this task 32 | /// Whether or not the base class should do any logging 33 | protected ObserverTask(string description, bool quiet = false) 34 | { 35 | _description = description; 36 | _quiet = quiet; 37 | } 38 | 39 | /// 40 | /// Initialize this task for execution 41 | /// 42 | public abstract void Init(); 43 | 44 | /// 45 | /// Start executing this task 46 | /// 47 | public virtual void Start() 48 | { 49 | IsStarted = true; 50 | // Subscribe to events of type T 51 | _subscription = EventBus.Subscribe(this); 52 | } 53 | 54 | /// 55 | /// Executes the workload for this scheduled task. 56 | /// 57 | /// 58 | protected abstract void Execute(); 59 | 60 | /// 61 | /// Called when this task should be stopped. 62 | /// Ensure you call base.Stop in your implementation, to make 63 | /// sure the instance is fully cleaned up. 64 | /// 65 | /// Whether the task should be killed or not 66 | public virtual void Stop(bool brutalKill) 67 | { 68 | if (_shuttingDown) 69 | return; 70 | 71 | IsStarted = false; 72 | IsExecuting = false; 73 | 74 | _shuttingDown = true; 75 | _subscription.Dispose(); 76 | 77 | Kill(brutalKill); 78 | } 79 | 80 | /// 81 | /// Called when this task is being shutdown. 82 | /// 83 | /// Whether this shutdown should be expedited or not. 84 | protected abstract void Kill(bool brutalKill); 85 | 86 | /// 87 | /// Called for every event in the event stream this task subscribes to 88 | /// 89 | /// The value of the event 90 | public void OnNext(T value) 91 | { 92 | if (_shuttingDown) 93 | return; 94 | 95 | if (!_quiet) 96 | { 97 | var isoDate = DateTime.UtcNow.ToISO8601z(); 98 | Logger.Info("{0} - Executing: {1}", isoDate, _description); 99 | } 100 | 101 | IsExecuting = true; 102 | Execute(); 103 | IsExecuting = false; 104 | } 105 | 106 | /// 107 | /// Called for any error produced by the event stream 108 | /// 109 | /// 110 | public void OnError(Exception error) 111 | { 112 | Logger.Error(error); 113 | } 114 | 115 | /// 116 | /// Called when the event stream stops publishing 117 | /// 118 | public void OnCompleted() 119 | { 120 | Stop(true); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /PluginHost.Interface/Tasks/ScheduledTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Linq; 3 | 4 | using PluginHost.Extensions.Time; 5 | using PluginHost.Interface.Logging; 6 | 7 | namespace PluginHost.Interface.Tasks 8 | { 9 | /// 10 | /// The abstract base class for tasks which should be executed on some interval. 11 | /// Tasks of this type are executed like cron jobs/scheduled tasks, and can be used 12 | /// for heartbeats, backup jobs, indexing jobs, clean up, etc. 13 | /// 14 | public abstract class ScheduledTask : IObserver, ITask 15 | { 16 | private bool _shuttingDown = false; 17 | private IDisposable _subscription; 18 | private readonly TimeSpan _interval; 19 | private readonly string _description; 20 | private readonly bool _quiet; 21 | 22 | public abstract bool IsInitialized { get; protected set; } 23 | public bool IsStarted { get; private set; } 24 | public bool IsExecuting { get; private set; } 25 | protected abstract IEventBus EventBus { get; set; } 26 | protected abstract ILogger Logger { get; set; } 27 | 28 | /// 29 | /// Initializes a new scheduled task. 30 | /// 31 | /// The text description for this task 32 | /// The interval on which this task will execute 33 | /// Whether or not the base class should do any logging 34 | protected ScheduledTask(string description, TimeSpan interval, bool quiet = false) 35 | { 36 | _description = description; 37 | _interval = interval; 38 | _quiet = quiet; 39 | } 40 | 41 | /// 42 | /// Initialize this task for execution 43 | /// 44 | public abstract void Init(); 45 | 46 | /// 47 | /// Start executing this task 48 | /// 49 | public virtual void Start() 50 | { 51 | IsStarted = true; 52 | // Subscribe to Tick events, throttled by the interval 53 | // this scheduled task executes on. 54 | _subscription = EventBus.Subscribe(this, ticks => ticks.Sample(_interval)); 55 | } 56 | 57 | /// 58 | /// Executes the workload for this scheduled task. 59 | /// 60 | protected abstract void Execute(); 61 | 62 | /// 63 | /// Called when this task should be stopped. 64 | /// 65 | /// Whether the task should be killed or not 66 | public virtual void Stop(bool brutalKill) 67 | { 68 | if (_shuttingDown) 69 | return; 70 | 71 | IsStarted = false; 72 | IsExecuting = false; 73 | 74 | _shuttingDown = true; 75 | _subscription.Dispose(); 76 | 77 | Kill(brutalKill); 78 | } 79 | 80 | /// 81 | /// Called when this task is being shutdown. 82 | /// 83 | /// Whether this shutdown should be expedited or not. 84 | protected abstract void Kill(bool brutalKill); 85 | 86 | /// 87 | /// Called for every event in the event stream this task has subscribed to 88 | /// 89 | /// The value of the event 90 | public void OnNext(Tick value) 91 | { 92 | if (_shuttingDown) 93 | return; 94 | 95 | if (!_quiet) 96 | { 97 | var isoDate = new DateTime(value.CurrentTicks).ToISO8601z(); 98 | Logger.Info("{0} - Executing scheduled task: {1}", isoDate, _description); 99 | } 100 | 101 | IsExecuting = true; 102 | Execute(); 103 | IsExecuting = false; 104 | } 105 | 106 | /// 107 | /// Called when the event stream produces an error 108 | /// 109 | /// 110 | public void OnError(Exception error) 111 | { 112 | Logger.Error(error); 113 | } 114 | 115 | /// 116 | /// Called when the event stream stops publishing 117 | /// 118 | public void OnCompleted() 119 | { 120 | Stop(true); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /PluginHost.Interface/Tasks/Tick.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace PluginHost.Interface.Tasks 4 | { 5 | /// 6 | /// Used as an event type for scheduled tasks. 7 | /// 8 | public class Tick 9 | { 10 | /// 11 | /// The number of ticks representing the date and time 12 | /// that this tick was generated. 13 | /// 14 | public long CurrentTicks { get; private set; } 15 | 16 | public Tick() 17 | { 18 | CurrentTicks = DateTime.UtcNow.Ticks; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /PluginHost.Interface/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PluginHost.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginHost", "PluginHost\PluginHost.csproj", "{80D7D5EF-F856-4A55-840B-0D07E7FD1718}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginHost.Interface", "PluginHost.Interface\PluginHost.Interface.csproj", "{19B4A837-8E9A-47DC-8A59-1C8A39C42537}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginHost.Extensions", "PluginHost.Extensions\PluginHost.Extensions.csproj", "{653947DE-DFC6-4327-9AAA-C9EB494BFFF2}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginHost.Heartbeat", "PluginHost.Heartbeat\PluginHost.Heartbeat.csproj", "{58513877-F118-4FEC-ABE3-6A34D0111BF8}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Documentation", "Documentation", "{4DA93C22-1545-486C-A916-812A1AFCDD70}" 15 | ProjectSection(SolutionItems) = preProject 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {80D7D5EF-F856-4A55-840B-0D07E7FD1718}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {80D7D5EF-F856-4A55-840B-0D07E7FD1718}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {80D7D5EF-F856-4A55-840B-0D07E7FD1718}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {80D7D5EF-F856-4A55-840B-0D07E7FD1718}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {19B4A837-8E9A-47DC-8A59-1C8A39C42537}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {19B4A837-8E9A-47DC-8A59-1C8A39C42537}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {19B4A837-8E9A-47DC-8A59-1C8A39C42537}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {19B4A837-8E9A-47DC-8A59-1C8A39C42537}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {653947DE-DFC6-4327-9AAA-C9EB494BFFF2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {653947DE-DFC6-4327-9AAA-C9EB494BFFF2}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {653947DE-DFC6-4327-9AAA-C9EB494BFFF2}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {653947DE-DFC6-4327-9AAA-C9EB494BFFF2}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {58513877-F118-4FEC-ABE3-6A34D0111BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {58513877-F118-4FEC-ABE3-6A34D0111BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {58513877-F118-4FEC-ABE3-6A34D0111BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {58513877-F118-4FEC-ABE3-6A34D0111BF8}.Release|Any CPU.Build.0 = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(SolutionProperties) = preSolution 43 | HideSolutionNode = FALSE 44 | EndGlobalSection 45 | EndGlobal 46 | -------------------------------------------------------------------------------- /PluginHost/App.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PluginHost/App.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /PluginHost/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /PluginHost/Application.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | using PluginHost.Tasks; 8 | using PluginHost.Dependencies; 9 | using PluginHost.Configuration; 10 | using PluginHost.Extensions.Time; 11 | using PluginHost.Interface.Tasks; 12 | using PluginHost.Interface.Logging; 13 | 14 | namespace PluginHost 15 | { 16 | internal class Application 17 | { 18 | private ILogger _logger; 19 | private IDisposable _exportsChangedSubscription; 20 | 21 | internal ITaskManager Tasks; 22 | 23 | public void Init() 24 | { 25 | // Initialize directories 26 | if (!Config.Current.Paths.LocalStorage.Info.Exists) 27 | Config.Current.Paths.LocalStorage.Info.Create(); 28 | if (!Config.Current.Paths.Plugins.Info.Exists) 29 | Config.Current.Paths.Plugins.Info.Create(); 30 | 31 | _logger = DependencyInjector.Current.Resolve(); 32 | Tasks = DependencyInjector.Current.Resolve(); 33 | 34 | _exportsChangedSubscription = DependencyInjector.Current.ExportChanged 35 | .Where(e => e.Metadata.ContainsKey(TaskManager.TaskNameMetadataKey)) 36 | .Subscribe(UpdateTasks); 37 | } 38 | 39 | public void Start() 40 | { 41 | Tasks.Start(); 42 | } 43 | 44 | public void Stop() 45 | { 46 | try 47 | { 48 | _logger.Info("Shutting down..."); 49 | 50 | // Shutdown task engine and wait 5 seconds to give 51 | // the processes time to clean up 52 | Tasks.Shutdown(); 53 | 54 | _logger.Success("Goodbye!"); 55 | } 56 | catch (Exception ex) 57 | { 58 | _logger.Error(ex); 59 | } 60 | } 61 | 62 | private void UpdateTasks(ExportChangedEventArgs e) 63 | { 64 | var taskMeta = e.Metadata[TaskManager.TaskNameMetadataKey] as TaskMetadata; 65 | if (taskMeta == null) 66 | return; 67 | 68 | var taskName = taskMeta.Name; 69 | switch (e.Type) 70 | { 71 | case ExportChangeType.Added: 72 | var task = ResolveTask(taskName); 73 | Tasks.AddTask(taskName, task); 74 | break; 75 | case ExportChangeType.Removed: 76 | default: 77 | Tasks.RemoveTask(taskName); 78 | break; 79 | } 80 | } 81 | 82 | private ITask ResolveTask(string taskName) 83 | { 84 | var task = DependencyInjector.Current 85 | .Resolve>(meta => 86 | { 87 | if (!meta.ContainsKey(TaskManager.TaskNameMetadataKey)) 88 | return false; 89 | 90 | var taskMeta = meta[TaskManager.TaskNameMetadataKey] as TaskMetadata; 91 | if (taskMeta == null || taskMeta.Name == null) 92 | return false; 93 | return taskMeta.Name.Equals(taskName); 94 | }); 95 | 96 | return task; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /PluginHost/Configuration/Config.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Configuration; 3 | using System.IO; 4 | 5 | using PluginHost.Configuration.Elements; 6 | 7 | namespace PluginHost.Configuration 8 | { 9 | public class Config : ConfigurationSection, IConfig 10 | { 11 | private static Config _cached; 12 | 13 | /// 14 | /// Get the current configuration, by loading via ConfigurationManager. 15 | /// The config is cached on first load, so subsequent requests will hit 16 | /// the cached instance. 17 | /// 18 | public static Config Current 19 | { 20 | get 21 | { 22 | if (_cached == null) 23 | _cached = (Config) ConfigurationManager.GetSection("pluginHost"); 24 | return _cached; 25 | } 26 | } 27 | 28 | [ConfigurationProperty("paths")] 29 | public PathsElement Paths 30 | { 31 | get { return this["paths"] as PathsElement; } 32 | set { this["paths"] = value; } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /PluginHost/Configuration/Elements/PathElement.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Configuration; 3 | 4 | namespace PluginHost.Configuration.Elements 5 | { 6 | public interface IPathElement 7 | { 8 | string Name { get; set; } 9 | string Location { get; set; } 10 | DirectoryInfo Info { get; } 11 | } 12 | 13 | public class PathElement : ConfigurationElement, IPathElement 14 | { 15 | /// 16 | /// The name of this path 17 | /// 18 | [ConfigurationProperty("name", IsRequired = true)] 19 | public string Name 20 | { 21 | get { return (string) this["name"]; } 22 | set { this["name"] = value; } 23 | } 24 | 25 | /// 26 | /// The string representation of the path 27 | /// 28 | [ConfigurationProperty("location", DefaultValue = "LocalStorage", IsRequired = true)] 29 | public string Location 30 | { 31 | get { return (string) this["location"]; } 32 | set { this["location"] = value; } 33 | } 34 | 35 | /// 36 | /// The directory info for this path 37 | /// 38 | public DirectoryInfo Info 39 | { 40 | get 41 | { 42 | return new DirectoryInfo(Location); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /PluginHost/Configuration/Elements/PathsElement.cs: -------------------------------------------------------------------------------- 1 | using System.Configuration; 2 | 3 | namespace PluginHost.Configuration.Elements 4 | { 5 | public interface IPathsElement 6 | { 7 | PathElement Plugins { get; } 8 | PathElement LocalStorage { get; } 9 | } 10 | 11 | [ConfigurationCollection(typeof(PathElement), AddItemName = "path", CollectionType = ConfigurationElementCollectionType.BasicMap)] 12 | public class PathsElement : ConfigurationElementCollection, IPathsElement 13 | { 14 | public override ConfigurationElementCollectionType CollectionType 15 | { 16 | get { return ConfigurationElementCollectionType.BasicMap; } 17 | } 18 | 19 | protected override string ElementName 20 | { 21 | get { return "path"; } 22 | } 23 | 24 | protected override ConfigurationElement CreateNewElement() 25 | { 26 | return new PathElement(); 27 | } 28 | 29 | protected override object GetElementKey(ConfigurationElement element) 30 | { 31 | return (element as PathElement).Name; 32 | } 33 | 34 | public PathElement this[int index] 35 | { 36 | get { return (PathElement) base.BaseGet(index); } 37 | set 38 | { 39 | if (base.BaseGet(index) != null) 40 | { 41 | base.BaseRemoveAt(index); 42 | } 43 | base.BaseAdd(index, value); 44 | } 45 | } 46 | 47 | public PathElement this[string name] 48 | { 49 | get { return (PathElement) base.BaseGet(name); } 50 | } 51 | 52 | /// 53 | /// Path where plugins are located. 54 | /// 55 | public PathElement Plugins 56 | { 57 | get { return this["plugins"]; } 58 | } 59 | 60 | /// 61 | /// Path where locally stored files should be kept. 62 | /// 63 | public PathElement LocalStorage 64 | { 65 | get { return this["localStorage"]; } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /PluginHost/Configuration/IConfig.cs: -------------------------------------------------------------------------------- 1 | using PluginHost.Configuration.Elements; 2 | 3 | namespace PluginHost.Configuration 4 | { 5 | public interface IConfig 6 | { 7 | /// 8 | /// All directory path configuration for this application 9 | /// 10 | PathsElement Paths { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /PluginHost/Dependencies/DependencyInjector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using System.Reactive.Concurrency; 7 | using System.Reactive.Linq; 8 | using System.Reactive.Subjects; 9 | using System.ComponentModel.Composition; 10 | using System.ComponentModel.Composition.Hosting; 11 | using System.ComponentModel.Composition.Registration; 12 | 13 | using PluginHost.Tasks; 14 | using PluginHost.Helpers; 15 | using PluginHost.Configuration; 16 | using PluginHost.Extensions.Functional; 17 | using PluginHost.Interface.Shell; 18 | using PluginHost.Interface.Tasks; 19 | using PluginHost.Interface.Logging; 20 | 21 | namespace PluginHost.Dependencies 22 | { 23 | public class DependencyInjector : IDisposable 24 | { 25 | private readonly CompositionContainer _container; 26 | private readonly RegistrationBuilder _conventions; 27 | private readonly DirectoryCatalog _directoryCatalog; 28 | private readonly AssemblyCatalog _assemblyCatalog; 29 | private readonly DirectoryWatcher _watcher; 30 | private readonly Task _watcherTask; 31 | private readonly IDisposable _subscription; 32 | 33 | private static DependencyInjector _current; 34 | public static DependencyInjector Current 35 | { 36 | get { return _current ?? (_current = new DependencyInjector()); } 37 | } 38 | 39 | public Subject ExportChanged { get; private set; } 40 | 41 | private DependencyInjector() 42 | { 43 | ExportChanged = new Subject(); 44 | _conventions = GetConventions(); 45 | 46 | var assembly = Assembly.GetExecutingAssembly(); 47 | var directory = Config.Current.Paths.Plugins.Location; 48 | var filter = @"*.dll"; 49 | 50 | // Create catalogs 51 | _directoryCatalog = new DirectoryCatalog(directory, filter, _conventions); 52 | _assemblyCatalog = new AssemblyCatalog(assembly, _conventions); 53 | 54 | // Initialize container 55 | _container = InitContainer(); 56 | _container.ExportsChanged += OnExportsChanged; 57 | 58 | // Start file watcher for the directory catalog 59 | // so that dependencies can be loaded at runtime 60 | var logger = Resolve(); 61 | _watcher = new DirectoryWatcher(logger, directory, filter); 62 | _subscription = _watcher.Events 63 | .Where(e => e.Type == FileChangedEvent.Ready || e.Type == FileChangedEvent.Removed) 64 | .ObserveOn(new EventLoopScheduler()) 65 | .Subscribe(OnDependencyChanged); 66 | _watcherTask = _watcher.Watch(); 67 | } 68 | 69 | public void Inject(T instance) 70 | { 71 | _container.ComposeParts(instance); 72 | } 73 | 74 | public Lazy LazyResolve() 75 | { 76 | return _container.GetExport(); 77 | } 78 | 79 | public Lazy LazyResolve(Func predicate) 80 | { 81 | return _container 82 | .GetExports() 83 | .SingleOrDefault(export => predicate(export.Metadata)); 84 | } 85 | 86 | public T Resolve() 87 | { 88 | return LazyResolve() 89 | .ToOption() 90 | .Select(export => export.Value) 91 | .GetValueOrDefault(); 92 | } 93 | 94 | public T Resolve(Func predicate) 95 | { 96 | return LazyResolve(predicate) 97 | .ToOption() 98 | .Select(export => export.Value) 99 | .GetValueOrDefault(); 100 | } 101 | 102 | public IEnumerable> LazyResolveMany() 103 | { 104 | return _container.GetExports(); 105 | } 106 | 107 | public IEnumerable> LazyResolveMany() 108 | { 109 | return _container.GetExports(); 110 | } 111 | 112 | public IEnumerable> LazyResolveMany(Func predicate) 113 | { 114 | return _container.GetExports().Where(export => predicate(export.Metadata)); 115 | } 116 | 117 | public IEnumerable ResolveMany() 118 | { 119 | return LazyResolveMany().Select(x => x.Value); 120 | } 121 | 122 | public IEnumerable ResolveMany() 123 | { 124 | return LazyResolveMany().Select(x => x.Value); 125 | } 126 | 127 | public IEnumerable ResolveMany(Func predicate) 128 | { 129 | return LazyResolveMany(predicate).Select(x => x.Value); 130 | } 131 | 132 | public void Dispose() 133 | { 134 | if (_subscription != null) 135 | _subscription.Dispose(); 136 | if (_watcher != null) 137 | _watcher.Dispose(); 138 | if (_container != null) 139 | _container.Dispose(); 140 | } 141 | 142 | private CompositionContainer InitContainer() 143 | { 144 | return new CompositionContainer( 145 | new AggregateCatalog(_assemblyCatalog, _directoryCatalog), 146 | CompositionOptions.IsThreadSafe 147 | ); 148 | } 149 | 150 | private RegistrationBuilder GetConventions() 151 | { 152 | var builder = new RegistrationBuilder(); 153 | builder.ForType() 154 | .SetCreationPolicy(CreationPolicy.Shared) 155 | .Export(); 156 | builder.ForTypesDerivedFrom() 157 | .SetCreationPolicy(CreationPolicy.Shared) 158 | .Export(); 159 | builder.ForTypesDerivedFrom() 160 | .SetCreationPolicy(CreationPolicy.Shared) 161 | .Export(); 162 | builder.ForTypesDerivedFrom() 163 | .SetCreationPolicy(CreationPolicy.Shared) 164 | .Export(); 165 | builder.ForType() 166 | .SetCreationPolicy(CreationPolicy.Shared) 167 | .Export(); 168 | builder.ForTypesDerivedFrom() 169 | .Export(b => 170 | b.AddMetadata(TaskManager.TaskNameMetadataKey, t => new TaskMetadata(t.Name)) 171 | ); 172 | builder.ForTypesDerivedFrom() 173 | .Export(); 174 | 175 | return builder; 176 | } 177 | 178 | private void OnDependencyChanged(FileChangedEventArgs e) 179 | { 180 | _directoryCatalog.Refresh(); 181 | } 182 | 183 | private void OnExportsChanged(object sender, ExportsChangeEventArgs e) 184 | { 185 | foreach (var removed in e.RemovedExports) 186 | { 187 | ExportChanged.OnNext(ExportChangedEventArgs.Removed(removed)); 188 | } 189 | foreach (var added in e.AddedExports) 190 | { 191 | ExportChanged.OnNext(ExportChangedEventArgs.Added(added)); 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /PluginHost/Dependencies/ExportChangeType.cs: -------------------------------------------------------------------------------- 1 | namespace PluginHost.Dependencies 2 | { 3 | /// 4 | /// Defines the type of change that has occurred 5 | /// for a given export. 6 | /// 7 | public enum ExportChangeType 8 | { 9 | Added, 10 | Removed 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /PluginHost/Dependencies/ExportChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.Composition.Primitives; 3 | 4 | namespace PluginHost.Dependencies 5 | { 6 | /// 7 | /// Defines the metadata for when new or removed exports are 8 | /// detected at runtime. Currently used as the event args type 9 | /// for the DependencyInjector.ExportChanged event stream. 10 | /// 11 | public struct ExportChangedEventArgs 12 | { 13 | public ExportChangeType Type { get; set; } 14 | public string ContractName { get; set; } 15 | public IDictionary Metadata { get; set; } 16 | 17 | public static ExportChangedEventArgs Added(ExportDefinition export) 18 | { 19 | return new ExportChangedEventArgs() 20 | { 21 | Type = ExportChangeType.Added, 22 | ContractName = export.ContractName, 23 | Metadata = export.Metadata 24 | }; 25 | } 26 | 27 | public static ExportChangedEventArgs Removed(ExportDefinition export) 28 | { 29 | return new ExportChangedEventArgs() 30 | { 31 | Type = ExportChangeType.Removed, 32 | ContractName = export.ContractName, 33 | Metadata = export.Metadata 34 | }; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /PluginHost/Helpers/DirectoryWatcher/DirectoryWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | using System.Threading.Tasks; 6 | 7 | using PluginHost.Extensions.Time; 8 | using PluginHost.Interface.Logging; 9 | 10 | namespace PluginHost.Helpers 11 | { 12 | /// 13 | /// Defines a disposable file system observer specifically designed 14 | /// to monitor a directory path for changes to files matching a given 15 | /// filter. 16 | /// 17 | /// Once created, call Watch to start a background task which will 18 | /// monitor the given directory indefinitely. In order to listen to 19 | /// events published by the watcher, you must subscribe to the event 20 | /// stream via the Events property. You can filter that subscription 21 | /// for specific events, or listen to all of them. 22 | /// 23 | /// Currently, DirectoryWatcher only publishes two types of FileChangedEvents, 24 | /// Ready, and Removed. Created and Changed occur before a file is necessarily 25 | /// accessible, and because of this, DirectoryWatcher handles those event types 26 | /// internally, monitoring files that change until they are available for use, at 27 | /// which point the Ready event is published. Removals are effective immediately. 28 | /// 29 | /// DirectoryWatcher is not meant to be reused - create it, call Watch, and 30 | /// then call Dispose (or wrap in a using) once done with it. Dispose is the 31 | /// only way to shutdown and release the watcher task. 32 | /// 33 | public class DirectoryWatcher : IDisposable 34 | { 35 | private const int ERROR_SHARING_VIOLATION = 32; 36 | private const int ERROR_LOCK_VIOLATION = 33; 37 | private const NotifyFilters NOTIFY_FILTERS = 38 | NotifyFilters.CreationTime | 39 | NotifyFilters.LastWrite | 40 | NotifyFilters.Size; 41 | 42 | 43 | private string _path; 44 | private string _filter; 45 | private bool _isDisposed; 46 | private FileSystemWatcher _watcher; 47 | private IDisposable _eventObserver; 48 | // This is different than the public Events Subject, 49 | // because it receives all events, unbuffered and unfiltered. 50 | // Events receives the buffered/filtered event stream which is 51 | // more suitable for observers. 52 | private Subject _events; 53 | private ILogger _logger; 54 | 55 | public Subject Events; 56 | 57 | public DirectoryWatcher(ILogger logger, string path, string filter = "*.*") 58 | { 59 | if (string.IsNullOrWhiteSpace(path)) 60 | throw new ArgumentException("Path cannot be null or empty!"); 61 | 62 | _logger = logger; 63 | 64 | if (Path.IsPathRooted(path)) 65 | _path = path; 66 | else 67 | _path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path); 68 | 69 | if (string.IsNullOrWhiteSpace(filter)) 70 | filter = "*.*"; 71 | 72 | _filter = filter; 73 | _watcher = new FileSystemWatcher(); 74 | _events = new Subject(); 75 | Events = new Subject(); 76 | 77 | _watcher.Path = _path; 78 | _watcher.Filter = _filter; 79 | _watcher.NotifyFilter = NOTIFY_FILTERS; 80 | _watcher.Created += new FileSystemEventHandler(OnFileSystemEvent); 81 | _watcher.Changed += new FileSystemEventHandler(OnFileSystemEvent); 82 | } 83 | 84 | /// 85 | /// Starts a new background task which handles monitoring the provided 86 | /// directory path for file system events which effect files matching 87 | /// the provided filter. 88 | /// 89 | /// Task 90 | public async Task Watch() 91 | { 92 | if (_isDisposed) 93 | throw new Exception("Attempted to use FileObserver after it was disposed!"); 94 | 95 | // Subscribe to Created and Changed events, but throttled by a short 96 | // period of time to prevent checking the file's availability when it's 97 | // already a given that it will fail. We will always get at least one event 98 | // when throttling. OnChange is bound as the handler for this subscription. 99 | _eventObserver = _events 100 | .Where(ev => ev.Type == FileChangedEvent.Created || 101 | ev.Type == FileChangedEvent.Changed || 102 | ev.Type == FileChangedEvent.Removed) 103 | .Throttle(50.Milliseconds()) 104 | .Subscribe(OnChange); 105 | 106 | 107 | // Start watching 108 | _watcher.EnableRaisingEvents = true; 109 | 110 | _logger.Info("Watching for dependencies in {0} with filter {1}", _path, _filter); 111 | 112 | // Wait until either a timeout occurs, or _events.OnCompleted is called, 113 | // which is done by OnReady when a Ready event is received. 114 | await _events 115 | .Where(ev => ev.Type == FileChangedEvent.Ready || ev.Type == FileChangedEvent.Removed) 116 | .ForEachAsync(PublishEvent); 117 | } 118 | 119 | /// 120 | /// Should be called when this DirectoryWatcher is no longer needed. 121 | /// It will stop the watcher, notify subscribers that it is shutting 122 | /// down, and clean up it's internals. 123 | /// 124 | public void Dispose() 125 | { 126 | if (_isDisposed) 127 | return; 128 | 129 | _watcher.EnableRaisingEvents = false; 130 | 131 | if (_eventObserver != null) 132 | _eventObserver.Dispose(); 133 | 134 | // Signal observers that we're done publishing events 135 | _events.OnCompleted(); 136 | Events.OnCompleted(); 137 | 138 | _watcher.Dispose(); 139 | _watcher = null; 140 | 141 | _isDisposed = true; 142 | } 143 | 144 | /// 145 | /// Called when the internal FileSystemWatcher sends an event notification 146 | /// 147 | /// The current FileSystemWatcher state 148 | /// The FileSystemEventArgs associated with this event 149 | private void OnFileSystemEvent(object state, FileSystemEventArgs e) 150 | { 151 | _events.OnNext(FileChangedEventArgs.Map(e)); 152 | } 153 | 154 | /// 155 | /// Publishes a FileChangedEvent to the internal event stream 156 | /// on removal, or on change as long as the file is available. 157 | /// 158 | /// The FileChangedEventArgs for this event 159 | private void OnChange(FileChangedEventArgs e) 160 | { 161 | if (e.Type == FileChangedEvent.Removed) 162 | { 163 | _events.OnNext(e); 164 | } 165 | else if (!IsFileLocked(e.Path)) 166 | { 167 | // File is fully ready 168 | e.Type = FileChangedEvent.Ready; 169 | _events.OnNext(e); 170 | } 171 | } 172 | 173 | /// 174 | /// Called when a change event occurs and the file was 175 | /// either removed, or was created/changed and is available for use. 176 | /// This publishes an event to the public event stream. 177 | /// 178 | /// The FileChangedEventArgs for this event 179 | private void PublishEvent(FileChangedEventArgs e) 180 | { 181 | Events.OnNext(e); 182 | } 183 | 184 | /// 185 | /// Checks to see if a file is currently locked, which tells 186 | /// us whether or not this file is still being written to, or 187 | /// otherwise in use by the process making the change. 188 | /// 189 | /// The path to the file to check 190 | /// Boolean 191 | private static bool IsFileLocked(string file) 192 | { 193 | FileStream stream = null; 194 | 195 | // Attempt to open the file with full permissions and no sharing allowed. 196 | // This ensures that not only can we open the file, but that nobody else has it open either. 197 | try 198 | { 199 | stream = File.Open(file, FileMode.Open, FileAccess.ReadWrite, FileShare.None); 200 | } 201 | catch (IOException ex) 202 | { 203 | // We could get exceptions for any number of reasons, but the file is only "locked" 204 | // if one of the following lower-level error codes is given. 205 | if (ex.HResult == ERROR_LOCK_VIOLATION || ex.HResult == ERROR_SHARING_VIOLATION) 206 | return true; 207 | } 208 | finally 209 | { 210 | if (stream != null) 211 | stream.Close(); 212 | } 213 | 214 | return false; 215 | } 216 | } 217 | } -------------------------------------------------------------------------------- /PluginHost/Helpers/DirectoryWatcher/FileChangedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace PluginHost.Helpers 2 | { 3 | /// 4 | /// Defines the core types of file change events we care about. 5 | /// Used in conjunction with DirectoryWatcher. 6 | /// 7 | public enum FileChangedEvent 8 | { 9 | Changed = 0, 10 | Created, 11 | Removed, 12 | Ready, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /PluginHost/Helpers/DirectoryWatcher/FileChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace PluginHost.Helpers 4 | { 5 | /// 6 | /// A cleaner version of FileSystemEventArgs used in 7 | /// conjunction with DirectoryWatcher. 8 | /// 9 | public struct FileChangedEventArgs 10 | { 11 | public FileChangedEvent Type { get; set; } 12 | public string Path { get; set; } 13 | 14 | public static FileChangedEventArgs Map(FileSystemEventArgs e) 15 | { 16 | var type = FileChangedEvent.Changed; 17 | switch (e.ChangeType) 18 | { 19 | case WatcherChangeTypes.Changed: 20 | type = FileChangedEvent.Changed; 21 | break; 22 | case WatcherChangeTypes.Created: 23 | type = FileChangedEvent.Created; 24 | break; 25 | case WatcherChangeTypes.Deleted: 26 | type = FileChangedEvent.Removed; 27 | break; 28 | default: 29 | break; 30 | } 31 | 32 | return new FileChangedEventArgs() { 33 | Path = e.FullPath, 34 | Type = type 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PluginHost/Logging/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | using PluginHost.Extensions.Comparers; 6 | using PluginHost.Extensions.Enums; 7 | using PluginHost.Extensions.Text; 8 | using PluginHost.Interface.Logging; 9 | 10 | namespace PluginHost.Logging 11 | { 12 | public class ConsoleLogger : ILogger 13 | { 14 | private static class ConsoleColors 15 | { 16 | public static readonly ConsoleColor Trace = ConsoleColor.Gray; 17 | public static readonly ConsoleColor Info = ConsoleColor.Cyan; 18 | public static readonly ConsoleColor Success = ConsoleColor.Green; 19 | public static readonly ConsoleColor Warn = ConsoleColor.Yellow; 20 | public static readonly ConsoleColor Alert = ConsoleColor.Magenta; 21 | public static readonly ConsoleColor Error = ConsoleColor.Red; 22 | } 23 | 24 | private static readonly GenericComparer _tokenComparer = 25 | GenericComparer.Create(t => t.Token); 26 | 27 | public void Trace(string message) 28 | { 29 | WriteOutput(LogLevel.TRACE, ConsoleColors.Trace, message); 30 | } 31 | 32 | public void Trace(string message, params object[] args) 33 | { 34 | WriteOutput(LogLevel.TRACE, ConsoleColors.Trace, message, args); 35 | } 36 | 37 | public void Info(string message) 38 | { 39 | WriteOutput(LogLevel.INFO, ConsoleColors.Info, message); 40 | } 41 | 42 | public void Info(string message, params object[] args) 43 | { 44 | WriteOutput(LogLevel.INFO, ConsoleColors.Info, message, args); 45 | } 46 | 47 | public void Success(string message) 48 | { 49 | WriteOutput(LogLevel.SUCCESS, ConsoleColors.Success, message); 50 | } 51 | 52 | public void Success(string message, params object[] args) 53 | { 54 | WriteOutput(LogLevel.SUCCESS, ConsoleColors.Success, message, args); 55 | } 56 | 57 | public void Warn(string message) 58 | { 59 | WriteOutput(LogLevel.WARN, ConsoleColors.Warn, message); 60 | } 61 | 62 | public void Warn(string message, params object[] args) 63 | { 64 | WriteOutput(LogLevel.WARN, ConsoleColors.Warn, message, args); 65 | } 66 | 67 | public void Alert(string message) 68 | { 69 | WriteOutput(LogLevel.ALERT, ConsoleColors.Alert, message); 70 | } 71 | 72 | public void Alert(string message, params object[] args) 73 | { 74 | WriteOutput(LogLevel.ALERT, ConsoleColors.Alert, message, args); 75 | } 76 | 77 | public void Error(string message) 78 | { 79 | WriteOutput(LogLevel.ERROR, ConsoleColors.Error, message); 80 | } 81 | 82 | public void Error(string message, params object[] args) 83 | { 84 | WriteOutput(LogLevel.ERROR, ConsoleColors.Error, message, args); 85 | } 86 | 87 | public void Error(Exception ex) 88 | { 89 | var formatted = FormatException(ex); 90 | WriteOutput(LogLevel.ERROR, ConsoleColors.Error, formatted); 91 | } 92 | 93 | public void Error(Exception ex, string message, params object[] args) 94 | { 95 | WriteOutput(LogLevel.ERROR, ConsoleColors.Error, message, args); 96 | 97 | var formatted = FormatException(ex); 98 | WriteOutput(LogLevel.ERROR, ConsoleColors.Error, formatted); 99 | } 100 | 101 | private void WriteOutput(LogLevel level, ConsoleColor color, string output, params object[] args) 102 | { 103 | var logLevel = level.GetName(); 104 | 105 | Console.ForegroundColor = color; 106 | 107 | // Prepend log level to each line of the output 108 | var lines = output.Split(new[] {'\r', '\n'}, StringSplitOptions.RemoveEmptyEntries); 109 | foreach (var line in lines) 110 | { 111 | var formattedLine = FormatLine(line, args); 112 | Console.WriteLine("{0,-15}{1}", "[" + logLevel + "]", formattedLine); 113 | } 114 | 115 | Console.ResetColor(); 116 | } 117 | 118 | private string FormatLine(string line, object[] args) 119 | { 120 | if (args == null || !args.Any()) 121 | return line; 122 | 123 | var tokens = line.ExtractFormatTokens(); 124 | foreach (var token in tokens.Distinct(_tokenComparer)) 125 | { 126 | line = line.Replace(token.Token, args[token.ArgsIndex].ToString()); 127 | } 128 | 129 | return line; 130 | } 131 | 132 | private string FormatException(Exception ex) 133 | { 134 | var builder = new StringBuilder(); 135 | builder.AppendFormat("[EXCEPTION] {0}", ex.Message); 136 | builder.AppendLine(); 137 | 138 | 139 | return FormatException(ex.InnerException, builder); 140 | } 141 | 142 | private string FormatException(Exception ex, StringBuilder builder, int depth = 1) 143 | { 144 | if (ex == null) 145 | return builder.ToString(); 146 | 147 | builder.AppendFormat("[EXCEPTION (INNER {0})] {1}", depth, ex.Message); 148 | builder.AppendLine(); 149 | builder = AppendStackTrace(builder, ex.StackTrace); 150 | 151 | return FormatException(ex.InnerException, builder, ++depth); 152 | } 153 | 154 | private StringBuilder AppendStackTrace(StringBuilder builder, string stackTrace) 155 | { 156 | // Indent the stack trace to provide better visual structure 157 | var lines = stackTrace 158 | .Split(new [] {'\r','\n'}, StringSplitOptions.RemoveEmptyEntries); 159 | foreach (var stackLine in lines) 160 | { 161 | builder.AppendFormat("\t{0}", stackLine); 162 | builder.AppendLine(); 163 | } 164 | 165 | return builder; 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /PluginHost/Logging/EventLogLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Diagnostics; 4 | using System.Reflection; 5 | 6 | using PluginHost.Interface.Logging; 7 | 8 | namespace PluginHost.Logging 9 | { 10 | /// 11 | /// Logs to the Windows Event Log, under the Application log as PluginHost. 12 | /// 13 | public class EventLogLogger : ILogger 14 | { 15 | // The event log to write to. There is really no good 16 | // reason to use anything other than the application log. 17 | private const string EVENT_LOG = "Application"; 18 | // The name of the event source representing events from 19 | // this application. Defaults to the name of this application's 20 | // entry assembly/executable, otherwise uses the name provided 21 | // via the secondary constructor. 22 | private readonly string _eventSource; 23 | 24 | public EventLogLogger() 25 | : this(Assembly.GetEntryAssembly().GetName().Name) {} 26 | 27 | public EventLogLogger(string eventSourceName) 28 | { 29 | if (string.IsNullOrWhiteSpace(eventSourceName)) 30 | throw new ArgumentException("EventLogLogger: Event source name cannot be null or empty!"); 31 | 32 | _eventSource = eventSourceName; 33 | 34 | InitializeEventLog(); 35 | } 36 | 37 | public void Trace(string message) 38 | { 39 | WriteOutput(LogLevel.TRACE, message); 40 | } 41 | 42 | public void Trace(string message, params object[] args) 43 | { 44 | WriteOutput(LogLevel.TRACE, message, args); 45 | } 46 | 47 | public void Info(string message) 48 | { 49 | WriteOutput(LogLevel.INFO, message); 50 | } 51 | 52 | public void Info(string message, params object[] args) 53 | { 54 | WriteOutput(LogLevel.INFO, message, args); 55 | } 56 | 57 | public void Success(string message) 58 | { 59 | WriteOutput(LogLevel.SUCCESS, message); 60 | } 61 | 62 | public void Success(string message, params object[] args) 63 | { 64 | WriteOutput(LogLevel.SUCCESS, message, args); 65 | } 66 | 67 | public void Warn(string message) 68 | { 69 | WriteOutput(LogLevel.WARN, message); 70 | } 71 | 72 | public void Warn(string message, params object[] args) 73 | { 74 | WriteOutput(LogLevel.WARN, message, args); 75 | } 76 | 77 | public void Alert(string message) 78 | { 79 | WriteOutput(LogLevel.ALERT, message); 80 | } 81 | 82 | public void Alert(string message, params object[] args) 83 | { 84 | WriteOutput(LogLevel.ALERT, message, args); 85 | } 86 | 87 | public void Error(string message) 88 | { 89 | WriteOutput(LogLevel.ERROR, message); 90 | } 91 | 92 | public void Error(string message, params object[] args) 93 | { 94 | WriteOutput(LogLevel.ERROR, message, args); 95 | } 96 | 97 | public void Error(Exception ex) 98 | { 99 | var formatted = FormatException(ex); 100 | WriteOutput(LogLevel.ERROR, formatted); 101 | } 102 | 103 | public void Error(Exception ex, string message, params object[] args) 104 | { 105 | var formatted = FormatException(ex); 106 | var joined = string.Format(message, args) + Environment.NewLine + formatted; 107 | WriteOutput(LogLevel.ERROR, joined); 108 | } 109 | 110 | private void WriteOutput(LogLevel level, string output, params object[] args) 111 | { 112 | var formatted = string.Format(output, args); 113 | 114 | try 115 | { 116 | switch (level) 117 | { 118 | case LogLevel.TRACE: 119 | case LogLevel.INFO: 120 | case LogLevel.SUCCESS: 121 | var infoType = EventLogEntryType.Information; 122 | EventLog.WriteEntry(_eventSource, formatted, infoType, (int) level, (short) level); 123 | break; 124 | case LogLevel.WARN: 125 | case LogLevel.ALERT: 126 | var warnType = EventLogEntryType.Warning; 127 | EventLog.WriteEntry(_eventSource, formatted, warnType, (int) level, (short) level); 128 | break; 129 | case LogLevel.ERROR: 130 | case LogLevel.EXCEPTION: 131 | default: 132 | var errorType = EventLogEntryType.Error; 133 | EventLog.WriteEntry(_eventSource, formatted, errorType, (int) level, (short) level); 134 | break; 135 | } 136 | } 137 | catch (Exception ex) 138 | { 139 | // Write to the trace listener that this failed, just in case anything is listening 140 | System.Diagnostics.Trace.TraceError("EventLogLogger: Failed to write entry - {0}", ex.Message); 141 | } 142 | } 143 | 144 | private string FormatException(Exception ex) 145 | { 146 | var builder = new StringBuilder(); 147 | builder.AppendFormat("[EXCEPTION] {0}", ex.Message); 148 | builder.AppendLine(); 149 | 150 | 151 | return FormatException(ex.InnerException, builder); 152 | } 153 | 154 | private string FormatException(Exception ex, StringBuilder builder, int depth = 1) 155 | { 156 | if (ex == null) 157 | return builder.ToString(); 158 | 159 | builder.AppendFormat("[EXCEPTION (INNER {0})] {1}", depth, ex.Message); 160 | builder.AppendLine(); 161 | builder = AppendStackTrace(builder, ex.StackTrace); 162 | 163 | return FormatException(ex.InnerException, builder, ++depth); 164 | } 165 | 166 | private StringBuilder AppendStackTrace(StringBuilder builder, string stackTrace) 167 | { 168 | // Indent the stack trace to provide better visual structure 169 | var lines = stackTrace 170 | .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); 171 | foreach (var stackLine in lines) 172 | { 173 | builder.AppendFormat("\t{0}", stackLine); 174 | builder.AppendLine(); 175 | } 176 | 177 | return builder; 178 | } 179 | 180 | private void InitializeEventLog() 181 | { 182 | if (!EventLog.SourceExists(_eventSource)) 183 | EventLog.CreateEventSource(_eventSource, EVENT_LOG); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /PluginHost/Logging/LogLevel.cs: -------------------------------------------------------------------------------- 1 | namespace PluginHost.Logging 2 | { 3 | public enum LogLevel 4 | { 5 | TRACE, 6 | INFO, 7 | SUCCESS, 8 | WARN, 9 | ALERT, 10 | ERROR, 11 | EXCEPTION 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PluginHost/PluginHost.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {80D7D5EF-F856-4A55-840B-0D07E7FD1718} 8 | Exe 9 | Properties 10 | PluginHost 11 | PluginHost 12 | v4.5 13 | 512 14 | 15 | 16 | AnyCPU 17 | true 18 | full 19 | false 20 | bin\Debug\ 21 | DEBUG;TRACE 22 | prompt 23 | 4 24 | 25 | 26 | AnyCPU 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll 42 | 43 | 44 | ..\packages\Rx-Interfaces.2.2.5\lib\net45\System.Reactive.Interfaces.dll 45 | 46 | 47 | ..\packages\Rx-Linq.2.2.5\lib\net45\System.Reactive.Linq.dll 48 | 49 | 50 | ..\packages\Rx-PlatformServices.2.2.5\lib\net45\System.Reactive.PlatformServices.dll 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Code 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | App.config 96 | 97 | 98 | App.config 99 | 100 | 101 | 102 | 103 | 104 | {653947de-dfc6-4327-9aaa-c9eb494bfff2} 105 | PluginHost.Extensions 106 | 107 | 108 | {19b4a837-8e9a-47dc-8a59-1c8a39c42537} 109 | PluginHost.Interface 110 | 111 | 112 | 113 | 114 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | $(TargetFileName).config 130 | 131 | 132 | 133 | 134 | 135 | 136 | $(_DeploymentApplicationDir)$(TargetName)$(TargetExt).config$(_DeploymentFileMappingExtension) 137 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /PluginHost/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | using PluginHost.Shell; 6 | using PluginHost.Extensions.Time; 7 | 8 | namespace PluginHost 9 | { 10 | class Program 11 | { 12 | internal static Application App; 13 | internal static CommandShell Shell; 14 | private static CancellationTokenSource _tokenSource; 15 | 16 | /// 17 | /// Main entry point for the application 18 | /// 19 | /// 20 | static void Main(string[] args) 21 | { 22 | Task t = MainAsync(args); 23 | t.Wait(); 24 | } 25 | 26 | /// 27 | /// Handles application start on another thread, only called from Main 28 | /// 29 | /// 30 | /// 31 | async static Task MainAsync(string[] args) 32 | { 33 | _tokenSource = new CancellationTokenSource(); 34 | 35 | App = new Application(); 36 | App.Init(); 37 | 38 | Shell = new CommandShell(_tokenSource.Token); 39 | await Shell.Start(); 40 | } 41 | 42 | public static void Shutdown() 43 | { 44 | if (_tokenSource != null) 45 | _tokenSource.Cancel(throwOnFirstException: false); 46 | if (App != null) 47 | App.Stop(); 48 | if (Shell != null) 49 | Shell.Shutdown(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PluginHost/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("PluginHost")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("PluginHost")] 13 | [assembly: AssemblyCopyright("Copyright © 2014")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("94ba5b7a-c1e5-40a6-817a-7b5ac54a9bd0")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /PluginHost/Shell/CommandParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Text.RegularExpressions; 5 | 6 | using PluginHost.Dependencies; 7 | using PluginHost.Interface.Shell; 8 | 9 | namespace PluginHost.Shell 10 | { 11 | public class CommandParser 12 | { 13 | private static readonly Regex _commandPattern = new Regex(@"(?x) 14 | ^([A-Za-z]{1}[0-9A-Za-z_-]+) # Command name, at least one alpha, followed by alphanumeric, _ or - 15 | (?:(?:\s+?(?:(?:""([^""]*)"")|(\S*)))+)? # followed by any number of quoted/unquoted arguments 16 | ", RegexOptions.Singleline); 17 | 18 | public IEnumerable Parse(string input) 19 | { 20 | var parsed = ParseInput(input); 21 | if (!parsed.IsValid) 22 | yield break; 23 | 24 | var handlers = new List(); 25 | var available = GetCommands(); 26 | foreach (var command in available) 27 | { 28 | if (command.Value.CanExecute(parsed)) 29 | yield return command.Value; 30 | } 31 | } 32 | 33 | public ShellInput ParseInput(string input) 34 | { 35 | if (!_commandPattern.IsMatch(input)) 36 | return ShellInput.Invalid(input); 37 | 38 | var match = _commandPattern.Match(input); 39 | var command = match.Groups[1].Value; 40 | var args = GatherArguments(match).ToArray(); 41 | 42 | if (string.IsNullOrWhiteSpace(command)) 43 | return ShellInput.Invalid(input); 44 | 45 | if (args.Length == 0) 46 | return ShellInput.Valid(command); 47 | 48 | var result = ShellInput.Valid(command); 49 | result.Arguments.AddRange(args); 50 | 51 | return result; 52 | } 53 | 54 | private IEnumerable> GetCommands() 55 | { 56 | return DependencyInjector.Current 57 | .LazyResolveMany(); 58 | } 59 | 60 | private static IEnumerable GatherArguments(Match match) 61 | { 62 | // Gather quoted arguments 63 | var quoted = new Capture[match.Groups[2].Captures.Count]; 64 | match.Groups[2].Captures.CopyTo(quoted, 0); 65 | // Gather unquoted arguments 66 | var unquoted = new Capture[match.Groups[3].Captures.Count]; 67 | match.Groups[3].Captures.CopyTo(unquoted, 0); 68 | // Join them together and order them as they appear in the input 69 | return quoted.Concat(unquoted) 70 | .OrderBy(c => c.Index) 71 | .Select(c => c.Value); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /PluginHost/Shell/CommandShell.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace PluginHost.Shell 9 | { 10 | public class CommandShell 11 | { 12 | private const string PROMPT = "pluginhost> "; 13 | 14 | private readonly CancellationToken _token; 15 | private readonly CommandParser _parser; 16 | 17 | private bool _shuttingDown = false; 18 | 19 | public CommandShell(CancellationToken token) 20 | { 21 | _token = token; 22 | _parser = new CommandParser(); 23 | 24 | _token.Register(Shutdown); 25 | } 26 | 27 | public async Task Start() 28 | { 29 | // Make sure CTRL+C is treated as an escape sequence 30 | Console.TreatControlCAsInput = false; 31 | Console.CancelKeyPress += (sender, e) => Shutdown(); 32 | 33 | Console.Clear(); 34 | 35 | await Task.Run(()=>Loop(), _token); 36 | } 37 | 38 | public void Shutdown() 39 | { 40 | _shuttingDown = true; 41 | } 42 | 43 | /// 44 | /// Loops indefinitely, listening to user input. 45 | /// When the user types Enter or CTRL+C, the app is shut down. 46 | /// If Shutdown is called directly, the thread the loop executes on 47 | /// is cancelled. 48 | /// 49 | private void Loop() 50 | { 51 | Console.Write(PROMPT); 52 | 53 | // Since C# has no tail-recursion, use goto to emulate 54 | // infinite recursion to keep the loop going. 55 | readkey: 56 | if (_token.IsCancellationRequested || _shuttingDown) 57 | return; 58 | // If no key is available, loop every 100ms until one is. 59 | // This lets us cancel out of the loop if Shutdown is called 60 | // with no user input 61 | if (!Console.KeyAvailable) 62 | { 63 | Task.Delay(100, _token); 64 | goto readkey; 65 | } 66 | 67 | // Get command entered by user 68 | var input = Console.ReadLine(); 69 | 70 | // If the input is empty, just print the prompt 71 | if (string.IsNullOrWhiteSpace(input)) 72 | { 73 | Console.Write(Environment.NewLine); 74 | Console.Write(PROMPT); 75 | goto readkey; 76 | } 77 | 78 | var handlers = _parser.Parse(input).ToArray(); 79 | // Write an empty line for better readability 80 | Console.WriteLine(); 81 | // If there is more than one command which can handle the given 82 | // input, prompt the user to select the command they wish to execute 83 | if (handlers.Length > 1) 84 | { 85 | // Ambiguous input, ask the user to clarify 86 | Console.ForegroundColor = ConsoleColor.Yellow; 87 | Console.WriteLine("Command is ambiguous, select one of the following:"); 88 | var options = string.Join(", ", handlers.Select(h => h.Name)); 89 | Console.WriteLine(options); 90 | Console.ResetColor(); 91 | 92 | Console.Write(PROMPT); 93 | var selection = Console.ReadLine(); 94 | var selected = handlers.FirstOrDefault(h => h.Name.Equals(selection)); 95 | if (selected != null) 96 | selected.Execute(); 97 | else 98 | { 99 | Console.ForegroundColor = ConsoleColor.Red; 100 | Console.WriteLine("Invalid command selection!"); 101 | Console.ResetColor(); 102 | } 103 | } 104 | else if (handlers.Length == 1) 105 | { 106 | var command = handlers.First(); 107 | command.Execute(input); 108 | } 109 | else 110 | { 111 | Console.ForegroundColor = ConsoleColor.Red; 112 | Console.WriteLine("No such command."); 113 | Console.ResetColor(); 114 | } 115 | Console.Write(Environment.NewLine); 116 | Console.Write(PROMPT); 117 | 118 | goto readkey; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /PluginHost/Shell/Commands/ClearCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using PluginHost.Interface.Shell; 4 | 5 | namespace PluginHost.Shell.Commands 6 | { 7 | public class ClearCommand : Command 8 | { 9 | public ClearCommand() : base("clear", "Clears the screen.") {} 10 | 11 | public override bool CanExecute(ShellInput input) 12 | { 13 | if (StringComparer.InvariantCultureIgnoreCase.Equals("clear", input.Command)) 14 | return true; 15 | return false; 16 | } 17 | 18 | public override void Execute(params string[] arguments) 19 | { 20 | Console.Clear(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PluginHost/Shell/Commands/ExitCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using PluginHost.Interface.Shell; 4 | 5 | namespace PluginHost.Shell.Commands 6 | { 7 | public class ExitCommand : Command 8 | { 9 | public ExitCommand() : base("exit", "Exits the application.") {} 10 | 11 | public override bool CanExecute(ShellInput input) 12 | { 13 | if (StringComparer.InvariantCultureIgnoreCase.Equals("exit", input.Command)) 14 | return true; 15 | return false; 16 | } 17 | 18 | public override void Execute(params string[] arguments) 19 | { 20 | Program.Shutdown(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /PluginHost/Shell/Commands/HelpCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using PluginHost.Dependencies; 5 | using PluginHost.Extensions.Collections; 6 | using PluginHost.Interface.Shell; 7 | 8 | namespace PluginHost.Shell.Commands 9 | { 10 | public class HelpCommand : Command 11 | { 12 | public HelpCommand() : base("help", "List available commands.") {} 13 | 14 | public override bool CanExecute(ShellInput input) 15 | { 16 | if (StringComparer.InvariantCultureIgnoreCase.Equals("help", input.Command)) 17 | return true; 18 | return false; 19 | } 20 | 21 | public override void Execute(params string[] arguments) 22 | { 23 | var commands = DependencyInjector.Current 24 | .ResolveMany() 25 | .ToArray(); 26 | 27 | if (commands.Length == 0) 28 | { 29 | Console.WriteLine("There are no commands available."); 30 | } 31 | else 32 | { 33 | WriteColor(ConsoleColor.Magenta, "The following commands are available:"); 34 | Console.WriteLine(Environment.NewLine); 35 | commands.Map(command => 36 | { 37 | Console.WriteLine("{0,-10}{1}", command.Name, command.Description); 38 | }); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PluginHost/Shell/Commands/ListTasksCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using PluginHost.Extensions.Collections; 4 | using PluginHost.Interface.Shell; 5 | 6 | namespace PluginHost.Shell.Commands 7 | { 8 | public class ListTasksCommand : Command 9 | { 10 | public ListTasksCommand() : base("tasks", "List available tasks and their status.") {} 11 | 12 | public override bool CanExecute(ShellInput input) 13 | { 14 | if (StringComparer.InvariantCultureIgnoreCase.Equals("tasks", input.Command)) 15 | return true; 16 | return false; 17 | } 18 | 19 | public override void Execute(params string[] args) 20 | { 21 | var available = Program.App.Tasks.AvailableTasks.ToArray(); 22 | if (available.Length > 0) 23 | { 24 | available.Map(t => 25 | { 26 | Console.Write(t.Key); 27 | Console.Write("\t"); 28 | if (t.Value.IsStarted) 29 | WriteSuccess("Started"); 30 | else 31 | WriteNominal("Stopped"); 32 | Console.Write(", "); 33 | if (t.Value.IsExecuting) 34 | WriteSuccess("Executing"); 35 | else 36 | WriteNominal("Idle"); 37 | Console.Write(Environment.NewLine); 38 | }); 39 | } 40 | else 41 | { 42 | Console.WriteLine("There are no tasks currently available."); 43 | } 44 | } 45 | 46 | private void WriteSuccess(string statusText) 47 | { 48 | WriteColor(ConsoleColor.Green, statusText); 49 | } 50 | 51 | private void WriteNominal(string statusText) 52 | { 53 | WriteColor(ConsoleColor.Yellow, statusText); 54 | } 55 | 56 | private void WriteError(string statusText) 57 | { 58 | WriteColor(ConsoleColor.Red, statusText); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /PluginHost/Shell/Commands/StartCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using PluginHost.Interface.Shell; 4 | 5 | namespace PluginHost.Shell.Commands 6 | { 7 | public class StartCommand : Command 8 | { 9 | public StartCommand() : base("start", "Starts task execution.") {} 10 | 11 | 12 | public override bool CanExecute(ShellInput input) 13 | { 14 | if (StringComparer.InvariantCultureIgnoreCase.Equals("start", input.Command)) 15 | return true; 16 | return false; 17 | } 18 | 19 | public override void Execute(params string[] arguments) 20 | { 21 | if (!Program.App.Tasks.IsStarted) 22 | { 23 | Program.App.Tasks.Start(); 24 | } 25 | else 26 | { 27 | if (arguments.Length > 0) 28 | { 29 | foreach (var taskName in arguments) 30 | { 31 | Program.App.Tasks.InitTask(taskName); 32 | Program.App.Tasks.StartTask(taskName); 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /PluginHost/Tasks/EventBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Composition; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Subjects; 5 | using System.Reactive.Concurrency; 6 | using System.Reactive.Disposables; 7 | 8 | using PluginHost.Interface.Tasks; 9 | 10 | namespace PluginHost.Tasks 11 | { 12 | /// 13 | /// This class is responsible for publishing events. It does so via Reactive Extension's 14 | /// Subject class, which behaves like a proxy. Observers subscribe to a specific kind of event, 15 | /// and when Publish is called with that type of event, the Subject handles proxying that event 16 | /// to all the subscribers. 17 | /// 18 | /// Every subscription is automatically set up with a filter over the type 19 | /// of the event object, so that those observers only receive messages of that type. There is an 20 | /// overloaded version of Subscribe that allows further refinement of the events of that type when 21 | /// needed. 22 | /// 23 | /// For example, subscribing to the Tick event by default will cause you to receive one 24 | /// message per second, one for every Tick. If you wanted to only receive a Tick once per minute though, 25 | /// you could pass a filter expression which throttles the subscription by an interval. Throttle is 26 | /// an extension method on IObservable, and ensures that you receive no more than one event of that type 27 | /// in a given interval. There are many filters available, and what you need will depend on the situation, 28 | /// see Rx's documentation for details on what is available and how to use them. 29 | /// 30 | public class EventBus : IEventBus 31 | { 32 | /// 33 | /// The Subject is both an observer and observable which will proxy 34 | /// events piped to it on to all it's subscribers. 35 | /// 36 | private Subject _subject; 37 | private bool _shuttingDown = false; 38 | private bool _disposed = false; 39 | 40 | public EventBus() 41 | { 42 | _subject = new Subject(); 43 | } 44 | 45 | public void Publish(TEvent @event) where TEvent : class 46 | { 47 | if (_shuttingDown) 48 | return; 49 | 50 | _subject.OnNext(@event); 51 | } 52 | 53 | public void Stop() 54 | { 55 | _shuttingDown = true; 56 | // This will execute all subscriber's OnCompleted callbacks, 57 | // which by convention is called whenever a publisher is done 58 | // publishing events. 59 | _subject.OnCompleted(); 60 | } 61 | 62 | public void Dispose() 63 | { 64 | if (_disposed) 65 | return; 66 | 67 | if (!_shuttingDown) 68 | Stop(); 69 | 70 | _subject.Dispose(); 71 | _disposed = true; 72 | } 73 | 74 | public IDisposable Subscribe(IObserver subscriber) 75 | where TEvent : class 76 | { 77 | return GetSubscription(subscriber); 78 | } 79 | 80 | public IDisposable Subscribe(IObserver subscriber, Func, IObservable> eventFilter) 81 | where TEvent : class 82 | { 83 | return GetSubscription(subscriber, eventFilter); 84 | } 85 | 86 | private IDisposable GetSubscription( 87 | IObserver subscriber, 88 | Func, IObservable> eventFilter = null) 89 | { 90 | // If we're shutting down, ignore new subscriptions 91 | if (_shuttingDown) 92 | return Disposable.Empty; 93 | 94 | var eventSource = _subject 95 | // Filter events by object type and cast 96 | .Where(ev => ev.GetType() == typeof(TEvent)) 97 | .Cast(); 98 | 99 | // Allow the subscriber to select more precisely what events they care about 100 | IObservable filteredEventSource; 101 | if (eventFilter != null) 102 | filteredEventSource = eventFilter(eventSource); 103 | else 104 | filteredEventSource = eventSource; 105 | 106 | return filteredEventSource 107 | // Execute all subscriber callbacks on a new thread 108 | .ObserveOn(new EventLoopScheduler()) 109 | // Return the subscription disposable to the subscriber, 110 | // so that they may unsubscribe themsleves 111 | .Subscribe(subscriber); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /PluginHost/Tasks/EventLoop.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.Composition; 3 | using System.Reactive.Linq; 4 | using System.Threading; 5 | 6 | using PluginHost.Extensions.Time; 7 | using PluginHost.Interface.Tasks; 8 | using PluginHost.Interface.Logging; 9 | 10 | namespace PluginHost.Tasks 11 | { 12 | /// 13 | /// The EventLoop class is used to drive scheduled background tasks in the application. 14 | /// It does this by publishing a Tick event once per second via the EventBus. 15 | /// 16 | /// By subscribing to the Tick event, tasks can schedule themselves for execution. 17 | /// Use subscription throttling to control how often your task is executed. 18 | /// 19 | /// The ScheduledTask abstract class already wraps up the behavior for tasks of this type, 20 | /// simply implement it, and provide the base constructor with the TimeSpan defining the 21 | /// interval to execute on. See that class for implementation instructions. 22 | /// 23 | /// 24 | public sealed class EventLoop : IEventLoop 25 | { 26 | private bool _started = false; 27 | private bool _shuttingDown = false; 28 | 29 | private IDisposable _subscription; 30 | private CancellationToken _cancelToken; 31 | private ILogger _logger; 32 | private IEventBus _eventBus; 33 | 34 | public EventLoop(ILogger logger, IEventBus eventBus) 35 | { 36 | _logger = logger; 37 | _eventBus = eventBus; 38 | } 39 | 40 | /// 41 | /// Starts the event loop 42 | /// 43 | public void Start() 44 | { 45 | if (_started) 46 | throw new Exception("Invalid call to EventLoop.Start when already started."); 47 | _started = true; 48 | 49 | Init(); 50 | } 51 | 52 | /// 53 | /// An alternate start method which allows us to proactively stop execution of 54 | /// the event loop via a CancellationToken. 55 | /// 56 | /// The cancellation token to watch 57 | public void Start(CancellationToken cancellationToken) 58 | { 59 | if (_started) 60 | throw new Exception("Invalid call to EventLoop.Start when already started."); 61 | _started = true; 62 | 63 | // Enable external cancellation 64 | _cancelToken = cancellationToken; 65 | _cancelToken.Register(() => Stop(true)); 66 | 67 | Init(); 68 | } 69 | 70 | private void Init() 71 | { 72 | _subscription = Observable 73 | // Once per second 74 | .Interval(1.Seconds()) 75 | // While enabled 76 | .DoWhile(IsEnabled) 77 | // Create a Tick 78 | .Select(_ => new Tick()) 79 | // And publish it to the EventBus 80 | .Subscribe(_eventBus.Publish); 81 | 82 | _logger.Info("EventLoop running!"); 83 | } 84 | 85 | private void HandleError(Exception ex) 86 | { 87 | _logger.Error(ex); 88 | } 89 | 90 | public void Stop(bool immediate) 91 | { 92 | if (_shuttingDown) 93 | return; 94 | _shuttingDown = true; 95 | 96 | _logger.Warn("EventLoop shutting down!"); 97 | 98 | if (_subscription != null) 99 | _subscription.Dispose(); 100 | } 101 | 102 | private bool IsEnabled() 103 | { 104 | return (_cancelToken == null || !_cancelToken.IsCancellationRequested) && 105 | !_shuttingDown; 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /PluginHost/Tasks/IEventLoop.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace PluginHost.Tasks 4 | { 5 | public interface IEventLoop 6 | { 7 | /// 8 | /// Starts the event loop 9 | /// 10 | void Start(); 11 | 12 | /// 13 | /// An alternate start method which allows us to proactively stop execution of 14 | /// the event loop via a CancellationToken. 15 | /// 16 | /// The cancellation token to watch 17 | void Start(CancellationToken cancellationToken); 18 | 19 | void Stop(bool immediate); 20 | } 21 | } -------------------------------------------------------------------------------- /PluginHost/Tasks/ITaskManager.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Collections.Generic; 3 | 4 | using PluginHost.Interface.Tasks; 5 | 6 | namespace PluginHost.Tasks 7 | { 8 | public interface ITaskManager 9 | { 10 | /// 11 | /// Whether or not the task manager instance is started 12 | /// 13 | bool IsStarted { get; } 14 | /// 15 | /// A queryable collection of tasks known to this task manager 16 | /// 17 | IQueryable> AvailableTasks { get; } 18 | /// 19 | /// Add a new task, and optionally initialize/start it. 20 | /// 21 | /// The name of the new task 22 | /// The new task 23 | /// Whether or not to initialize this task when added 24 | /// Whether or not to start this task when added 25 | void AddTask(string taskName, ITask task, bool init = true, bool start = true); 26 | /// 27 | /// Remove a task from this task manager 28 | /// 29 | /// 30 | void RemoveTask(string taskName); 31 | /// 32 | /// Start the task manager and associated tasks 33 | /// 34 | void Start(); 35 | /// 36 | /// Stop the task manager and associated tasks 37 | /// 38 | void Shutdown(); 39 | /// 40 | /// Initialize a given task 41 | /// 42 | /// The name of the task to initialize 43 | void InitTask(string taskName); 44 | /// 45 | /// Start a given task 46 | /// 47 | /// The name of the task to start 48 | void StartTask(string taskName); 49 | /// 50 | /// Stop a given task 51 | /// 52 | /// The name of the task to stop 53 | void StopTask(string taskName); 54 | } 55 | } -------------------------------------------------------------------------------- /PluginHost/Tasks/TaskManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | using PluginHost.Interface.Tasks; 7 | using PluginHost.Interface.Logging; 8 | 9 | namespace PluginHost.Tasks 10 | { 11 | /// 12 | /// Primary entry point for all plugin-based tasks in the PluginHost application. 13 | /// 14 | public class TaskManager : ITaskManager 15 | { 16 | public const string TaskNameMetadataKey = "TaskName"; 17 | 18 | private bool _started = false; 19 | private bool _shuttingDown = false; 20 | private IEventLoop _eventLoop; 21 | private ILogger _logger; 22 | private IDictionary _tasks; 23 | 24 | public bool IsStarted { get { return _started; } } 25 | 26 | public TaskManager(IEnumerable>> tasks, ILogger logger) 27 | { 28 | _logger = logger; 29 | _tasks = new ConcurrentDictionary(); 30 | 31 | foreach (var task in tasks) 32 | { 33 | if (task.Metadata.ContainsKey(TaskManager.TaskNameMetadataKey)) 34 | { 35 | var taskMeta = task.Metadata[TaskManager.TaskNameMetadataKey] as TaskMetadata; 36 | if (taskMeta == null || taskMeta.Name == null) 37 | continue; 38 | 39 | AddTask(taskMeta.Name, task.Value, init: false, start: false); 40 | } 41 | } 42 | } 43 | 44 | public IQueryable> AvailableTasks 45 | { 46 | get { return _tasks.AsQueryable(); } 47 | } 48 | 49 | public void AddTask(string taskName, ITask task, bool init = true, bool start = true) 50 | { 51 | try 52 | { 53 | if (_tasks.ContainsKey(taskName)) 54 | { 55 | _logger.Warn("Attempted to add a task ({0}) which is already being managed!", taskName); 56 | _logger.Warn("Request to add new task ({0}) has been denied.", taskName); 57 | return; 58 | } 59 | 60 | _logger.Alert("Adding new task ({0})...", taskName); 61 | 62 | _tasks.Add(taskName, task); 63 | 64 | if (_started) 65 | { 66 | if (init) { InitTask(taskName); } 67 | if (start) { StartTask(taskName); } 68 | } 69 | } 70 | catch (Exception ex) 71 | { 72 | _logger.Error(ex); 73 | _logger.Warn("Non-fatal exception occurred while adding new task. All systems normal."); 74 | } 75 | } 76 | 77 | public void RemoveTask(string taskName) 78 | { 79 | try 80 | { 81 | if (!_tasks.ContainsKey(taskName)) 82 | { 83 | _logger.Warn("Removal of task ({0}) was requested, but is not known to the task manager!"); 84 | return; 85 | } 86 | 87 | _logger.Alert("Removal of task ({0}) has been requested!"); 88 | 89 | if (_started) 90 | { 91 | StopTask(taskName); 92 | } 93 | 94 | _tasks.Remove(taskName); 95 | _logger.Success("Task ({0}) has been removed successfully."); 96 | } 97 | catch (Exception ex) 98 | { 99 | _logger.Error(ex); 100 | _logger.Alert("Non-fatal exception occurred, all systems normal."); 101 | } 102 | } 103 | 104 | public void Start() 105 | { 106 | if (_shuttingDown) 107 | { 108 | _logger.Warn("Cannot start TaskManager, as shutdown is in progress."); 109 | return; 110 | } 111 | if (_started) 112 | return; 113 | 114 | _started = true; 115 | 116 | // Start event loop 117 | _eventLoop = Dependencies.DependencyInjector.Current.Resolve(); 118 | _eventLoop.Start(); 119 | 120 | // Load and execute tasks 121 | Run(); 122 | } 123 | 124 | public void Shutdown() 125 | { 126 | _logger.Warn("TaskManager shutting down!"); 127 | 128 | if (!_started || _shuttingDown) 129 | return; 130 | 131 | // Set flags 132 | _shuttingDown = true; 133 | _started = false; 134 | 135 | // Stop publishing events 136 | _eventLoop.Stop(true); 137 | 138 | // Stop all tasks 139 | foreach (var _task in _tasks) 140 | { 141 | _task.Value.Stop(brutalKill: true); 142 | } 143 | } 144 | 145 | public void InitTask(string taskName) 146 | { 147 | if (!_tasks.ContainsKey(taskName)) 148 | return; 149 | 150 | var task = _tasks[taskName]; 151 | if (task.IsInitialized) 152 | return; 153 | try 154 | { 155 | _logger.Info("Initializing task ({0})...", taskName); 156 | task.Init(); 157 | } 158 | catch (Exception ex) 159 | { 160 | _logger.Error(ex); 161 | _logger.Warn("Non-fatal exception occurred while initializing task. All systems normal."); 162 | } 163 | } 164 | 165 | public void StartTask(string taskName) 166 | { 167 | if (!_tasks.ContainsKey(taskName)) 168 | return; 169 | 170 | var task = _tasks[taskName]; 171 | if (task.IsStarted) 172 | return; 173 | 174 | try 175 | { 176 | _logger.Info("Starting task ({0})...", taskName); 177 | task.Start(); 178 | _logger.Success("Task ({0}) has been started.", taskName); 179 | } 180 | catch (Exception ex) 181 | { 182 | _logger.Error(ex); 183 | _logger.Warn("Non-fatal exception occurred while starting task. All systems normal."); 184 | } 185 | } 186 | 187 | public void StopTask(string taskName) 188 | { 189 | if (!_tasks.ContainsKey(taskName)) 190 | return; 191 | 192 | var task = _tasks[taskName]; 193 | if (!task.IsStarted) 194 | return; 195 | 196 | try 197 | { 198 | _logger.Info("Stopping task ({0})...", taskName); 199 | task.Stop(brutalKill: true); 200 | } 201 | catch (Exception ex) 202 | { 203 | _logger.Error(ex); 204 | _logger.Warn("Non-fatal exception occurred while stopping task. All systems normal."); 205 | } 206 | } 207 | 208 | private void Run() 209 | { 210 | if (_shuttingDown) 211 | return; 212 | 213 | var taskNames = _tasks.Select(t => t.Key).ToArray(); 214 | 215 | // Initialize tasks 216 | foreach (var task in taskNames) 217 | { 218 | InitTask(task); 219 | } 220 | 221 | _logger.Success("All tasks have been initialized."); 222 | 223 | // Start task execution 224 | foreach (var task in taskNames) 225 | { 226 | StartTask(task); 227 | } 228 | 229 | _logger.Success("All tasks have been started."); 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /PluginHost/Tasks/TaskMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace PluginHost.Tasks 2 | { 3 | public class TaskMetadata 4 | { 5 | public string Name { get; set; } 6 | 7 | public TaskMetadata() {} 8 | public TaskMetadata(string name) 9 | { 10 | Name = name; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PluginHost/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PluginHost 2 | 3 | This project is a proof-of-concept for an application which provides 4 | runtime extensibility via plugins. This application is written in C#, 5 | using .NET v4.5, and is composed of the following components: 6 | 7 | ## Usage 8 | 9 | Simply clone the repository, open in Visual Studio and build the solution. Make sure 10 | PluginHost is set as the startup project, and run the debugger. From here you can type 11 | `help` and press Enter to execute the command and view a list of available commands. 12 | 13 | By default, PluginHost has no tasks bundled, so we need to add one. I've added the 14 | PluginHost.Heartbeat project for this purpose. To test the runtime detection and execution 15 | of tasks, perform the following steps while PluginHost is running: 16 | 17 | - In the console, type `start` followed by Enter 18 | - Go to the PluginHost.Hearbeat project, locate the `bin\Debug` folder, and copy the 19 | `PluginHost.Hearbeat.dll` file to `PluginHost\bin\Debug`. 20 | 21 | You should see logging messages in the console reflecting the detection of the new assembly 22 | and associated tasks, and start up messages for the Heartbeat task. After a few seconds you 23 | should start seeing the heartbeat message logged to the console. 24 | 25 | To exit, just type `exit` and press Enter. 26 | 27 | ## Components 28 | 29 | ### PluginHost 30 | 31 | This project contains the core application logic for the command shell, 32 | dependency management, configuration, and task management. It also contains 33 | the default set of shell commands, and the default ILogger implementations. 34 | 35 | ### PluginHost.Interface 36 | 37 | This project contains shared interfaces necessary for plugins to extend 38 | the command shell, reference and add logging modules, add new tasks, and 39 | reference the parent application's configuration. 40 | 41 | ### PluginHost.Extensions 42 | 43 | This project contains shared common code extensions: a generic comparer 44 | implementation, extension methods for collections, enums, streams, numbers, 45 | strings, and datetime/timespans. 46 | 47 | ### PluginHost.Heartbeat 48 | 49 | An example task which acts as a simple heartbeat by logging an event every 50 | 5 seconds. 51 | 52 | ## Dependency Injection 53 | 54 | Injection is handled via the Managed Extensibility Framework (MEF). The 55 | configuration for how dependencies are imported/exported is defined via 56 | convention in the PluginHost.Dependencies.DependencyInjector class. It 57 | works like so: 58 | 59 | - On application startup, MEF loads a catalog of exported dependencies, 60 | based on the configured conventions, by loading types from the PluginHost 61 | assembly, as well as any assemblies in the `plugins` path as defined in 62 | `App.config`. 63 | - In addition to this behavior, it has been extended with a custom directory 64 | watcher which monitors that plugin directory, and tells MEF to reload the 65 | catalog when files are added or removed from that directory. An event is published 66 | which can be subscribed to in order to act on new/removed dependencies (this 67 | is done for implementations of ITask for example). 68 | - To import an instance of a dependency, calling one of the Resolve variations 69 | on the `DependencyInjector` class will fetch the type information for the dependency, 70 | find the constructor the the largest number of parameters, and attempt to fulfill the 71 | requirements of that constructor based on available exports. In this way, dependencies 72 | can be automatically injected into instances on creation. 73 | - If you have an instance of an object that is not tracked by MEF, but for which public 74 | properties of exported types exist, you can inject those properties by using the `Inject` 75 | method of the `DependencyInjector` class. 76 | 77 | ## Command Shell 78 | 79 | The shell is a simple REPL-style interface, which provides a prompt, waits for 80 | user input, and responds to that input by looking up commands which can handle the 81 | given input and executing that command. 82 | 83 | Commands all implement the IShellCommand interface, and expose two core methods, 84 | `CanExecute` and `Execute`. When user input is received by the shell, it is parsed 85 | into command text and a list of arguments by the CommandParser, and then commands are 86 | resolved via MEF, and each one is asked whether it can handle the given input. If only a 87 | single command responds, it is executed immediately. If more than one command can handle 88 | the input, the user is prompted to choose which command they wish to execute, and then the 89 | command is executed. 90 | 91 | Some default commands are provided directly in the PluginHost project: `exit`, `clear`, 92 | `start`, `tasks`, and `help`. New commands can be provided by extending `IShellCommand`, 93 | and adding the containing assembly to the plugins directory. 94 | 95 | ## Tasks 96 | 97 | Tasks all implement the `ITask` interface, and are loaded at startup via MEF. In addition, 98 | any tasks added via new assemblies in the plugins directory will be added during runtime, 99 | and automatically started if `TaskManager` is running. 100 | 101 | Tasks can execute on a schedule (via `ScheduledTask`), in response to an event (via `ObserverTask`), 102 | or based on their own behavior entirely, as either one-off tasks, or with their own lifecycle management. 103 | 104 | ## Logging 105 | 106 | Logging is exposed via the `ILogger` interface, and contains two default implementations, `ConsoleLogger`, 107 | and `EventLogLogger`, both of which do pretty much what you'd expect. You can add new loggers by creating 108 | an implementation of `ILogger`, and adding the containing assembly to the plugins directory. Right now no 109 | work has yet been done on providing a single interface for writing to all loggers, you basically have to 110 | import all of them and write to all of them, or choose one to log with and import just that one. 111 | 112 | ## License 113 | 114 | MIT --------------------------------------------------------------------------------