├── .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