├── .github └── workflows │ └── test.yml ├── .gitignore ├── Directory.Packages.props ├── QuartzNetWebConsole.Tests ├── DummyTrigger.cs ├── LimitedListTests.cs ├── PaginationInfoTests.cs ├── QuartzNetWebConsole.Tests.csproj ├── ViewsTests.cs └── job_scheduling_data_2_0.xsd ├── QuartzNetWebConsole.Views ├── EnumerableExtensions.vb ├── GroupWithStatus.vb ├── Helpers.vb ├── JobWithContext.vb ├── LogEntry.vb ├── PaginationInfo.vb ├── QuartzNetWebConsole.Views.vbproj ├── TriggerWithState.vb ├── TriggersByJobModel.vb ├── Views.vb ├── X.vb └── job_scheduling_data_2_0.xsd ├── QuartzNetWebConsole.sln ├── QuartzNetWebConsole ├── AbstractLogger.cs ├── Controllers │ ├── IndexController.cs │ ├── JobGroupController.cs │ ├── LogController.cs │ ├── SchedulerController.cs │ ├── StaticController.cs │ ├── TriggerGroupController.cs │ └── TriggersByJobController.cs ├── EnumerableExtensions.cs ├── ILogger.cs ├── MemoryLogger.cs ├── QuartzNetWebConsole.csproj ├── Resources │ ├── styles.css │ └── time-toggle.js ├── Routing.cs ├── Setup.cs ├── Utils │ ├── Extensions.cs │ ├── ISchedulerWrapper.cs │ ├── LimitedList.cs │ ├── Owin.cs │ ├── QueryStringParser.cs │ ├── RelativeUri.cs │ ├── Response.cs │ └── SchedulerWrapper.cs └── job_scheduling_data_2_0.xsd ├── README.md ├── SampleApp ├── HelloJob.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── SampleApp.csproj ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── flake.lock ├── flake.nix └── license.txt /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | check: 9 | runs-on: ubuntu-24.04 10 | permissions: 11 | id-token: "write" 12 | contents: "read" 13 | steps: 14 | - uses: actions/checkout@v4.2.2 15 | - uses: DeterminateSystems/nix-installer-action@v16 16 | with: 17 | source-url: "https://github.com/DeterminateSystems/nix-installer/releases/download/v3.0.0/nix-installer-x86_64-linux" 18 | - uses: DeterminateSystems/magic-nix-cache-action@v9 19 | - name: Unit tests 20 | run: nix develop --command dotnet test 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.bak 3 | *.user 4 | *.cache 5 | */bin 6 | */obj 7 | _ReSharper.* 8 | */Build/* 9 | packages/* 10 | .nuget/nuget.exe 11 | *.nupkg 12 | *.sln.ide 13 | QuartzNetWebConsole.Web/*.dll 14 | QuartzNetWebConsole/*.dll 15 | [Nn][Uu][Gg][Ee][Tt].[Ee][Xx][Ee] 16 | .idea -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Tests/DummyTrigger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Quartz; 3 | 4 | namespace QuartzNetWebConsole.Tests { 5 | public class DummyTrigger : ITrigger { 6 | public int CompareTo(ITrigger other) { 7 | throw new NotImplementedException(); 8 | } 9 | 10 | public TriggerBuilder GetTriggerBuilder() { 11 | throw new NotImplementedException(); 12 | } 13 | 14 | public IScheduleBuilder GetScheduleBuilder() { 15 | throw new NotImplementedException(); 16 | } 17 | 18 | public bool GetMayFireAgain() { 19 | throw new NotImplementedException(); 20 | } 21 | 22 | public DateTimeOffset? GetNextFireTimeUtc() { 23 | return NextFireTimeUtc; 24 | } 25 | 26 | public DateTimeOffset? GetPreviousFireTimeUtc() { 27 | throw new NotImplementedException(); 28 | } 29 | 30 | public DateTimeOffset? GetFireTimeAfter(DateTimeOffset? afterTime) { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public ITrigger Clone() 35 | { 36 | throw new NotImplementedException(); 37 | } 38 | 39 | public DateTimeOffset? NextFireTimeUtc { get; set; } 40 | public TriggerKey Key { get; set; } 41 | public JobKey JobKey { get; set; } 42 | public string Description { get; set; } 43 | public string CalendarName { get; set; } 44 | public JobDataMap JobDataMap { get; set; } 45 | public DateTimeOffset? FinalFireTimeUtc { get; set; } 46 | public int MisfireInstruction { get; set; } 47 | public DateTimeOffset? EndTimeUtc { get; set; } 48 | public DateTimeOffset StartTimeUtc { get; set; } 49 | public int Priority { get; set; } 50 | public bool HasMillisecondPrecision { get; set; } 51 | } 52 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole.Tests/LimitedListTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | using QuartzNetWebConsole.Utils; 5 | 6 | namespace QuartzNetWebConsole.Tests { 7 | public class LimitedListTests { 8 | [Fact] 9 | public void tt() { 10 | var b = new LimitedList(5); 11 | foreach (var i in Enumerable.Range(0, 1000)) 12 | b.Add(i); 13 | var a = b.ToArray(); 14 | Assert.Equal(5, a.Length); 15 | Assert.Equal(999, a[0]); 16 | Assert.Equal(998, a[1]); 17 | Assert.Equal(997, a[2]); 18 | Assert.Equal(996, a[3]); 19 | Assert.Equal(995, a[4]); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole.Tests/PaginationInfoTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Xunit; 3 | using QuartzNetWebConsole.Views; 4 | 5 | namespace QuartzNetWebConsole.Tests { 6 | public class PaginationInfoTests { 7 | [Fact] 8 | public void tt() { 9 | var p = new PaginationInfo( 10 | firstItemIndex: 350, 11 | pageSize: 25, 12 | pageSlide: 2, 13 | totalItemCount: 398, 14 | pageUrl: ""); 15 | Assert.Equal(16, p.LastPage); 16 | Assert.Equal(15, p.CurrentPage); 17 | Assert.True(p.HasNextPage); 18 | var pages = p.Pages.ToArray(); 19 | Assert.Equal(5, pages.Length); 20 | Assert.Equal(12, pages[0]); 21 | Assert.Equal(13, pages[1]); 22 | Assert.Equal(14, pages[2]); 23 | Assert.Equal(15, pages[3]); 24 | Assert.Equal(16, pages[4]); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole.Tests/QuartzNetWebConsole.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Library 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Tests/ViewsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | using Quartz; 5 | using QuartzNetWebConsole.Views; 6 | 7 | namespace QuartzNetWebConsole.Tests { 8 | public class ViewsTests { 9 | [Fact] 10 | public void TrClassAlt() { 11 | var x = Views.Views.SchedulerCalendars(new[] { 12 | Helpers.KV("one", "1"), 13 | Helpers.KV("two", "2"), 14 | Helpers.KV("three", "3"), 15 | Helpers.KV("four", "4"), 16 | }); 17 | Helpers.StripeTrs(x); 18 | //Console.WriteLine(x.ToString()); 19 | var trs = x.Descendants("tr") 20 | .Skip(1) 21 | .WhereOdd(); 22 | foreach (var tr in trs) { 23 | Console.WriteLine(tr.ToString()); 24 | Assert.Equal("alt", tr.Attribute("class").Value); 25 | } 26 | } 27 | 28 | [Fact] 29 | public void TriggerTable() { 30 | var trigger = new DummyTrigger { 31 | Key = new TriggerKey("myCronTrigger", "DEFAULT"), 32 | JobKey = new JobKey("someJob", "DEFAULT"), 33 | }; 34 | var triggers = new[] { 35 | new TriggerWithState(trigger, TriggerState.Normal), 36 | }; 37 | var x = Views.Views.TriggerTable(triggers, "/", highlight: "DEFAULT.myCronTrigger"); 38 | var tr = x.Descendants().First(e => { 39 | var id = e.Attribute("id"); 40 | if (id == null) 41 | return false; 42 | return id.Value == "DEFAULT.myCronTrigger"; 43 | }); 44 | Assert.Equal("highlight", tr.Attribute("class").Value); 45 | //var html = x.MakeHTMLCompatible(); 46 | //Console.WriteLine(html); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole.Tests/job_scheduling_data_2_0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | Root level node 12 | 13 | 14 | 15 | 16 | 17 | Commands to be executed before scheduling the jobs and triggers in this file. 18 | 19 | 20 | 21 | 22 | Directives to be followed while scheduling the jobs and triggers in this file. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Version of the XML Schema instance 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Delete all jobs, if any, in the identified group. "*" can be used to identify all groups. Will also result in deleting all triggers related to the jobs. 47 | 48 | 49 | 50 | 51 | Delete all triggers, if any, in the identified group. "*" can be used to identify all groups. Will also result in deletion of related jobs that are non-durable. 52 | 53 | 54 | 55 | 56 | Delete the identified job if it exists (will also result in deleting all triggers related to it). 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Delete the identified trigger if it exists (will also result in deletion of related jobs that are non-durable). 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Whether the existing scheduling data (with same identifiers) will be overwritten. If false, and ignore-duplicates is not false, and jobs or triggers with the same names already exist as those in the file, an error will occur. 84 | 85 | 86 | 87 | 88 | If true (and overwrite-existing-data is false) then any job/triggers encountered in this file that have names that already exist in the scheduler will be ignored, and no error will be produced. 89 | 90 | 91 | 92 | 93 | If true trigger's start time is calculated based on earlier run time instead of fixed value. Trigger's start time must be undefined for this to work. 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Define a JobDetail 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Define a JobDataMap 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Define a JobDataMap entry 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | Define a Trigger 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Common Trigger definitions 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | Define a SimpleTrigger 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | Define a CronTrigger 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | Define a DateIntervalTrigger 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | Cron expression (see JavaDoc for examples) 220 | 221 | Special thanks to Chris Thatcher (thatcher@butterfly.net) for the regular expression! 222 | 223 | Regular expressions are not my strong point but I believe this is complete, 224 | with the caveat that order for expressions like 3-0 is not legal but will pass, 225 | and month and day names must be capitalized. 226 | If you want to examine the correctness look for the [\s] to denote the 227 | seperation of individual regular expressions. This is how I break them up visually 228 | to examine them: 229 | 230 | SECONDS: 231 | ( 232 | ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) 233 | | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) 234 | | ([\?]) 235 | | ([\*]) 236 | ) [\s] 237 | MINUTES: 238 | ( 239 | ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) 240 | | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) 241 | | ([\?]) 242 | | ([\*]) 243 | ) [\s] 244 | HOURS: 245 | ( 246 | ((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?) 247 | | (([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3])) 248 | | ([\?]) 249 | | ([\*]) 250 | ) [\s] 251 | DAY OF MONTH: 252 | ( 253 | ((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?) 254 | | (([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?) 255 | | (L(-[0-9])?) 256 | | (L(-[1-2][0-9])?) 257 | | (L(-[3][0-1])?) 258 | | (LW) 259 | | ([1-9]W) 260 | | ([1-3][0-9]W) 261 | | ([\?]) 262 | | ([\*]) 263 | )[\s] 264 | MONTH: 265 | ( 266 | ((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?) 267 | | (([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2])) 268 | | (((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?) 269 | | ((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)) 270 | | ([\?]) 271 | | ([\*]) 272 | )[\s] 273 | DAY OF WEEK: 274 | ( 275 | (([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?) 276 | | ([1-7]/([1-7])) 277 | | (((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?) 278 | | ((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?) 279 | | (([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))(L|LW)?) 280 | | (([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?) 281 | | ([\?]) 282 | | ([\*]) 283 | ) 284 | YEAR (OPTIONAL): 285 | ( 286 | [\s]? 287 | ([\*])? 288 | | ((19[7-9][0-9])|(20[0-9][0-9]))? 289 | | (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))? 290 | | ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)? 291 | ) 292 | 293 | 294 | 295 | 297 | 298 | 299 | 300 | 301 | 302 | Number of times to repeat the Trigger (-1 for indefinite) 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | Simple Trigger Misfire Instructions 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | Cron Trigger Misfire Instructions 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | Date Interval Trigger Misfire Instructions 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | Interval Units 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/EnumerableExtensions.vb: -------------------------------------------------------------------------------- 1 | Imports System.Runtime.CompilerServices 2 | 3 | Public Module EnumerableExtensions 4 | 5 | Public Function WhereOdd(Of T)(s As IEnumerable(Of T)) As IEnumerable(Of T) 6 | Return s.Select(Function(e, i) New With {.e = e, .i = i}). 7 | Where(Function(e) e.i Mod 2 <> 0). 8 | Select(Function(e) e.e) 9 | End Function 10 | 11 | 12 | Public Function WhereEven(Of T)(s As IEnumerable(Of T)) As IEnumerable(Of T) 13 | Return s.Select(Function(e, i) New With {.e = e, .i = i}). 14 | Where(Function(e) e.i Mod 2 = 0). 15 | Select(Function(e) e.e) 16 | End Function 17 | End Module 18 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/GroupWithStatus.vb: -------------------------------------------------------------------------------- 1 | Public Class GroupWithStatus 2 | Public ReadOnly Name As String 3 | Public ReadOnly Paused As Boolean 4 | 5 | Public Sub New(name As String, paused As Boolean) 6 | Me.Name = name 7 | Me.Paused = paused 8 | End Sub 9 | End Class 10 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/Helpers.vb: -------------------------------------------------------------------------------- 1 | Public Module Helpers 2 | Public Function SimpleForm(action As String, button As String) As XElement 3 | Return _ 4 |
> 5 | /> 6 |
7 | End Function 8 | 9 | Public ReadOnly Stylesheet As XElement() = _ 10 | 11 | 12 | 13 | 14 | .Elements.ToArray() 15 | 16 | Public Function YesNo(b As Boolean) As String 17 | Return If(b, "Yes", "No") 18 | End Function 19 | 20 | Public Function KV(Of K, V)(key As K, value As V) As KeyValuePair(Of K, V) 21 | Return New KeyValuePair(Of K, V)(key, value) 22 | End Function 23 | 24 | Public Sub StripeTrs(xml As XElement) 25 | For Each table In xml... 26 | Dim t = table 27 | Dim trs = From x In t... 28 | trs = trs.Skip(1).WhereOdd() 29 | For Each tr In trs 30 | Dim clas = tr.Attribute("class") 31 | If clas IsNot Nothing Then 32 | clas.SetValue(clas.Value + " " + "alt") 33 | Else 34 | tr.Add(New XAttribute("class", "alt")) 35 | End If 36 | Next 37 | Next 38 | End Sub 39 | 40 | Public Function XHTML(e As XElement) As XDocument 41 | StripeTrs(e) 42 | Return e.MakeHTML5Doc() 43 | End Function 44 | 45 | Public Function IfNullable(Of T)(value As Boolean?, ifNull As T, ifTrue As T, ifFalse As T) As T 46 | If Not value.HasValue Then 47 | Return ifNull 48 | End If 49 | Return If(value.Value, ifTrue, ifFalse) 50 | End Function 51 | End Module 52 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/JobWithContext.vb: -------------------------------------------------------------------------------- 1 | Imports Quartz 2 | 3 | Public Class JobWithContext 4 | Public ReadOnly Job As IJobDetail 5 | Public ReadOnly JobContext As IJobExecutionContext 6 | Public ReadOnly Interruptible As Boolean 7 | 8 | Public Sub New(job As IJobDetail, jobContext As IJobExecutionContext, interruptible As Boolean) 9 | If job Is Nothing Then 10 | Throw New ArgumentNullException("job") 11 | End If 12 | Me.Job = job 13 | Me.JobContext = jobContext 14 | Me.Interruptible = interruptible 15 | End Sub 16 | End Class 17 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/LogEntry.vb: -------------------------------------------------------------------------------- 1 | Public Class LogEntry 2 | Public ReadOnly Timestamp As DateTimeOffset 3 | Public ReadOnly Description As String 4 | 5 | Public Sub New(timestamp As DateTimeOffset, description As String) 6 | Me.Timestamp = timestamp 7 | Me.Description = description 8 | End Sub 9 | 10 | Public Sub New(description As String) 11 | Me.Description = description 12 | Timestamp = DateTimeOffset.Now 13 | End Sub 14 | End Class 15 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/PaginationInfo.vb: -------------------------------------------------------------------------------- 1 | Public Class PaginationInfo 2 | Public ReadOnly PageSlide As Integer 3 | Public ReadOnly PageSize As Integer 4 | Public ReadOnly TotalItemCount As Integer 5 | Public ReadOnly PageUrl As String 6 | Public ReadOnly FirstItemIndex As Integer 7 | 8 | Public Sub New(pageSlide As Integer, pageSize As Integer, totalItemCount As Integer, pageUrl As String, firstItemIndex As Integer) 9 | Me.PageSlide = pageSlide 10 | Me.PageSize = pageSize 11 | Me.TotalItemCount = totalItemCount 12 | Me.PageUrl = pageUrl 13 | Me.FirstItemIndex = firstItemIndex 14 | End Sub 15 | 16 | Public Sub New(pageSize As Integer, totalItemCount As Integer, pageUrl As String, firstItemIndex As Integer) 17 | Me.new(2, pageSize, totalItemCount, pageUrl, firstItemIndex) 18 | End Sub 19 | 20 | Public ReadOnly Property CurrentPage As Integer 21 | Get 22 | Return FirstItemIndex \ PageSize + 1 23 | End Get 24 | End Property 25 | 26 | Public ReadOnly Property LastPage As Integer 27 | Get 28 | Return CInt(Math.Floor((CDec(TotalItemCount) - 1) / PageSize)) + 1 29 | End Get 30 | End Property 31 | 32 | Public ReadOnly Property HasNextPage As Boolean 33 | Get 34 | Return CurrentPage < LastPage 35 | End Get 36 | End Property 37 | 38 | Public ReadOnly Property HasPrevPage As Boolean 39 | Get 40 | Return CurrentPage > 1 41 | End Get 42 | End Property 43 | 44 | Public ReadOnly Property NextPageUrl As String 45 | Get 46 | Return If(HasNextPage, PageUrlFor(CurrentPage + 1), Nothing) 47 | End Get 48 | End Property 49 | 50 | Public ReadOnly Property PrevPageUrl As String 51 | Get 52 | Return If(HasPrevPage, PageUrlFor(CurrentPage - 1), Nothing) 53 | End Get 54 | End Property 55 | 56 | Public ReadOnly Property LastItemIndex As Integer 57 | Get 58 | Return Math.Min(FirstItemIndex + PageSize - 1, TotalItemCount) 59 | End Get 60 | End Property 61 | 62 | Public Function PageUrlFor(page As Integer) As String 63 | Dim start = (page - 1) * PageSize 64 | Return PageUrl.Replace("!0", start.ToString()) 65 | End Function 66 | 67 | Public ReadOnly Property Pages As IEnumerable(Of Integer) 68 | Get 69 | Dim pageCount = LastPage 70 | Dim pageFrom = Math.Max(1, CurrentPage - PageSlide) 71 | Dim pageTo = Math.Min(pageCount, CurrentPage + PageSlide) 72 | pageFrom = Math.Max(1, Math.Min(pageTo - 2 * PageSlide, pageFrom)) 73 | pageTo = Math.Min(pageCount, Math.Max(pageFrom + 2 * PageSlide, pageTo)) 74 | Return Enumerable.Range(pageFrom, pageTo - pageFrom + 1) 75 | End Get 76 | End Property 77 | End Class 78 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/QuartzNetWebConsole.Views.vbproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Library 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/TriggerWithState.vb: -------------------------------------------------------------------------------- 1 | Imports Quartz 2 | 3 | Public Class TriggerWithState 4 | Public ReadOnly Trigger As ITrigger 5 | Public ReadOnly State As TriggerState 6 | 7 | Public Sub New(trigger As ITrigger, state As TriggerState) 8 | Me.Trigger = trigger 9 | Me.State = state 10 | End Sub 11 | 12 | Public ReadOnly Property IsPaused As Boolean 13 | Get 14 | Return State = TriggerState.Paused 15 | End Get 16 | End Property 17 | End Class 18 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/TriggersByJobModel.vb: -------------------------------------------------------------------------------- 1 | Public Class TriggersByJobModel 2 | Public ReadOnly Triggers As IEnumerable(Of TriggerWithState) 3 | Public ReadOnly ThisUrl As String 4 | Public ReadOnly Group As String 5 | Public ReadOnly Job As String 6 | Public ReadOnly Highlight As String 7 | 8 | Public Sub New(triggers As IEnumerable(Of TriggerWithState), thisUrl As String, group As String, job As String, highlight As String) 9 | Me.Triggers = triggers 10 | Me.ThisUrl = thisUrl 11 | Me.Group = group 12 | Me.Job = job 13 | Me.Highlight = highlight 14 | End Sub 15 | End Class 16 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/Views.vb: -------------------------------------------------------------------------------- 1 | Imports System.Net 2 | Imports Quartz 3 | Imports Quartz.Impl.Triggers 4 | 5 | Imports CronExpressionDescriptor 6 | 7 | Public Module Views 8 | Public Function Pager(pagination As PaginationInfo) As XElement 9 | Return _ 10 | 27 | End Function 28 | 29 | Public Function Log(logs As IEnumerable(Of LogEntry), pagination As PaginationInfo, thisUrl As String) As XElement 30 | Return _ 31 | 32 | 33 | Quartz.Net Console - Log 34 | <%= Stylesheet %> 35 | 36 | 37 | 38 | Index 39 |

Scheduler log

40 |
41 | 42 | 43 | 44 | 45 | <%= From e In logs 46 | Select 47 | 48 | 49 | 50 | 51 | %> 52 |
Date / TimeDescription
<%= e.Timestamp %><%= X.Raw(e.Description) %>
53 |
54 | <%= Pager(pagination) %> 55 | 56 | 57 | End Function 58 | 59 | Public Function LogRSS(thisUrl As String, logs As IEnumerable(Of LogEntry)) As XElement 60 | Return _ 61 | 62 | 63 | Quartz.NET log 64 | Quartz.NET log 65 | <%= thisUrl %> 66 | <%= DateTimeOffset.Now.ToString("R") %> 67 | <%= From r In logs 68 | Select 69 | 70 | <%= r.Timestamp %> 71 | <%= r.Description %> 72 | <%= r.Timestamp.ToString("R") %> 73 | 74 | %> 75 | 76 | 77 | End Function 78 | 79 | Public Function SchedulerStatus(schedulerName As String, inStandby As Boolean, metadata As SchedulerMetaData) As XElement 80 | Return _ 81 |
82 |

Scheduler name: <%= schedulerName %>

83 |
84 | Job store: <%= metadata.JobStoreType %>
85 | Supports persistence: <%= YesNo(metadata.JobStoreSupportsPersistence) %>
86 | Number of jobs executed: <%= metadata.NumberOfJobsExecuted %>
87 | Running since: <%= metadata.RunningSince %>
88 | Status: <%= If(inStandby, "stand-by", "running") %> 89 |
90 | View log 91 |
92 |
93 | <%= SimpleForm("scheduler.ashx?method=Shutdown", "Shut down") %> 94 | <%= If(inStandby, 95 | SimpleForm("scheduler.ashx?method=Start", "Start"), 96 | SimpleForm("scheduler.ashx?method=Standby", "Stand by")) %> 97 | <%= SimpleForm("scheduler.ashx?method=PauseAll", "Pause all triggers") %> 98 | <%= SimpleForm("scheduler.ashx?method=ResumeAll", "Resume all triggers") %> 99 |
100 |
101 | 102 | End Function 103 | 104 | Public Function SchedulerListeners(listeners As IEnumerable(Of ISchedulerListener)) As XElement 105 | Return _ 106 |
107 |

Scheduler listeners

108 | 109 | 110 | 111 | 112 | <%= From l In listeners 113 | Select 114 | 115 | 116 | 117 | %> 118 |
Type
<%= l.GetType() %>
119 |
120 | End Function 121 | 122 | Public Function SchedulerCalendars(calendars As ICollection(Of KeyValuePair(Of String, String))) As XElement 123 | Return _ 124 |
125 |

Calendars

126 | <%= If(calendars.Count = 0, 127 | No calendars, 128 | 129 | 130 | 131 | 132 | 133 | <%= From cal In calendars 134 | Select 135 | 136 | 137 | 138 | 139 | %> 140 |
NameDescription
<%= cal.Key %><%= cal.Value %>
) %> 141 |
142 | 143 | End Function 144 | 145 | Public Function SchedulerEntityGroups(entity As String) As Func(Of IEnumerable(Of GroupWithStatus), XElement) 146 | Return Function(groups) _ 147 |
148 |

<%= entity %> groups

149 | 150 | 151 | 152 | 153 | 154 | 155 | <%= From group In groups 156 | Select 157 | 158 | 162 | 168 | 169 | %> 170 |
NameStatus
159 | 163 | <%= IfNullable(group.Paused, 164 | ifNull:=, 165 | ifTrue:=SimpleForm("scheduler.ashx?method=Resume" & entity & "Group&groupName=" + group.Name, "Resume"), 166 | ifFalse:=SimpleForm("scheduler.ashx?method=Pause" & entity & "Group&groupName=" + group.Name, "Pause")) %> 167 |
171 |
172 | End Function 173 | 174 | Public ReadOnly SchedulerJobGroups As Func(Of IEnumerable(Of GroupWithStatus), XElement) = SchedulerEntityGroups("Job") 175 | 176 | Public ReadOnly SchedulerTriggerGroups As Func(Of IEnumerable(Of GroupWithStatus), XElement) = SchedulerEntityGroups("Trigger") 177 | 178 | Public Function GlobalEntityListeners(entity As String) As Func(Of ICollection(Of KeyValuePair(Of String, Type)), XElement) 179 | Return Function(listeners) _ 180 |
181 |

Global <%= entity %> listeners

182 | <%= If(listeners.Count = 0, 183 | No <%= entity %> listeners, 184 | 185 | 186 | 187 | 188 | 189 | <%= From listener In listeners 190 | Select 191 | 192 | 193 | 196 | 197 | %> 198 |
Name
<%= listener.Key %> 194 | <%= SimpleForm("scheduler.ashx?method=RemoveGlobal" & entity & "Listener&name=" & listener.Key, "Delete") %> 195 |
) %> 199 |
200 | End Function 201 | 202 | Public ReadOnly GlobalJobListeners As Func(Of ICollection(Of KeyValuePair(Of String, Type)), XElement) = GlobalEntityListeners("Job") 203 | 204 | Public ReadOnly GlobalTriggerListeners As Func(Of ICollection(Of KeyValuePair(Of String, Type)), XElement) = GlobalEntityListeners("Trigger") 205 | 206 | Public Function IndexPage(schedulerName As String, 207 | inStandby As Boolean, 208 | listeners As IReadOnlyCollection(Of ISchedulerListener), 209 | metadata As SchedulerMetaData, 210 | triggerGroups As IReadOnlyCollection(Of GroupWithStatus), 211 | jobGroups As IReadOnlyCollection(Of GroupWithStatus), 212 | calendars As IReadOnlyCollection(Of KeyValuePair(Of String, String)), 213 | jobListeners As IReadOnlyCollection(Of KeyValuePair(Of String, Type)), 214 | triggerListeners As IReadOnlyCollection(Of KeyValuePair(Of String, Type))) As XElement 215 | Return _ 216 | 217 | 218 | Quartz.Net Console 219 | <%= Stylesheet %> 220 | 221 | 222 | <%= SchedulerStatus(schedulerName, inStandby, metadata) %> 223 | <%= SchedulerListeners(listeners) %> 224 | <%= SchedulerCalendars(calendars) %> 225 |
226 | <%= SchedulerJobGroups(jobGroups) %> 227 | <%= SchedulerTriggerGroups(triggerGroups) %> 228 |
229 | <%= GlobalJobListeners(jobListeners) %> 230 | <%= GlobalTriggerListeners(triggerListeners) %> 231 | 232 | 233 | End Function 234 | 235 | Public Function TriggerGroup(group As String, paused As Boolean?, thisUrl As String, highlight As String, triggers As IEnumerable(Of TriggerWithState)) As XElement 236 | Dim schedulerOp = Function(method As String) "scheduler.ashx?method=" + method + 237 | "&groupName=" + group + 238 | "&next=" + WebUtility.UrlEncode(thisUrl) 239 | Return _ 240 | 241 | 242 | Quartz.Net console - Trigger group <%= group %> 243 | <%= Stylesheet %> 244 | 245 | 246 | Index 247 |

Trigger group <%= group %>

248 | Status: <%= IfNullable(paused, ifNull:="N/A", ifTrue:="paused", ifFalse:="started") %> 249 | <%= IfNullable(paused, 250 | ifNull:=, 251 | ifTrue:=SimpleForm(schedulerOp("ResumeTriggerGroup"), "Resume this trigger group"), 252 | ifFalse:=SimpleForm(schedulerOp("PauseTriggerGroup"), "Pause this trigger group")) %> 253 |
254 |

Triggers

255 | <%= TriggerTable(triggers, thisUrl, highlight) %> 256 | 257 | 258 | End Function 259 | 260 | Public Function JobGroup(group As String, paused As Boolean?, highlight As String, thisUrl As String, jobs As IEnumerable(Of JobWithContext)) As XElement 261 | Dim schedulerOp = Function(method As String) "scheduler.ashx?method=" + method + 262 | "&groupName=" + group + 263 | "&next=" + WebUtility.UrlEncode(thisUrl) 264 | Return _ 265 | 266 | 267 | Quartz.Net console - Job group <%= group %> 268 | <%= Stylesheet %> 269 | 270 | 271 | Index 272 |

Job group <%= group %>

273 | Status: <%= IfNullable(paused, ifNull:="N/A", ifTrue:="paused", ifFalse:="started") %> 274 | <%= If(paused, 275 | SimpleForm(schedulerOp("ResumeJobGroup"), "Resume this job group"), 276 | SimpleForm(schedulerOp("PauseJobGroup"), "Pause this job group")) %> 277 |
278 |

Jobs

279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | <%= From j In jobs 292 | Let op = Function(method As String) "scheduler.ashx?method=" + method + 293 | "&jobName=" + j.Job.Key.Name + 294 | "&groupName=" + j.Job.Key.Group + 295 | "&next=" + WebUtility.UrlEncode(thisUrl) 296 | Select 297 | 298 | class=<%= If(highlight = j.Job.Key.ToString(), "highlight", "") %>> 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 315 | 316 | %> 317 |
NameDescriptionTypeDurablePersist data after executionConcurrent execution disallowedRequests recoveryRunning since
<%= j.Job.Key.Name %><%= j.Job.Description %><%= j.Job.JobType %><%= YesNo(j.Job.Durable) %><%= YesNo(j.Job.PersistJobDataAfterExecution) %><%= YesNo(j.Job.ConcurrentExecutionDisallowed) %><%= YesNo(j.Job.RequestsRecovery) %><%= If(j.JobContext IsNot Nothing, j.JobContext.FireTimeUtc, Nothing) %> 308 | 310 | <%= SimpleForm(op("PauseJob"), "Pause") %> 311 | <%= SimpleForm(op("ResumeJob"), "Resume") %> 312 | <%= SimpleForm(op("TriggerJob"), "Trigger") %> 313 | <%= If(j.Interruptible, SimpleForm(op("Interrupt"), "Interrupt"), Nothing) %> 314 |
318 | 319 | 320 | End Function 321 | 322 | Public Function TriggersByJob(model As TriggersByJobModel) As XElement 323 | Return _ 324 | 325 | 326 | Quartz.Net console - Triggers for job <%= model.Group %>.<%= model.Job %> 327 | <%= Stylesheet %> 328 | 329 | 330 | Index 331 |

Triggers for job <%= model.Group %>.<%= model.Job %>

332 |
333 |

Triggers

334 | <%= TriggerTable(model.Triggers, model.ThisUrl, model.Highlight) %> 335 | 336 | 337 | End Function 338 | 339 | Public Function TriggerTable(triggers As IEnumerable(Of TriggerWithState), thisUrl As String, highlight As String) As XElement 340 | Return _ 341 | If(triggers Is Nothing, 342 | Not available, 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | <%= From tr In triggers 363 | Let trigger = tr.Trigger 364 | Let high = highlight = trigger.Key.ToString() 365 | Let simpleTrigger = TryCast(trigger, SimpleTriggerImpl) 366 | Let cronTrigger = TryCast(trigger, CronTriggerImpl) 367 | Let op = Function(method As String) "scheduler.ashx?method=" & method & 368 | "&triggerName=" + trigger.Key.Name + 369 | "&groupName=" + trigger.Key.Group + 370 | "&next=" + WebUtility.UrlEncode(thisUrl) 371 | Select 372 | 373 | class=<%= If(highlight = trigger.Key.ToString(), "highlight", "") %>> 374 | 375 | 376 | 377 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 402 | 403 | 404 | 411 | %> 412 |
NameDescriptionPriorityJob groupJob nameStart time UTCEnd time UTCFinal fire time UTCNext fire time UTCRepeat countRepeat intervalTimes triggeredCronCalendarState
<%= trigger.Key.Name %><%= trigger.Description %><%= trigger.Priority %> 378 | > 384 | <%= trigger.JobKey.Group %> 385 | 386 | <%= trigger.StartTimeUtc %><%= trigger.EndTimeUtc %><%= trigger.FinalFireTimeUtc %><%= trigger.GetNextFireTimeUtc() %><%= If(simpleTrigger IsNot Nothing, simpleTrigger.RepeatCount.ToString, "") %><%= If(simpleTrigger IsNot Nothing, simpleTrigger.RepeatInterval.ToString, "") %><%= If(simpleTrigger IsNot Nothing, simpleTrigger.TimesTriggered.ToString, "") %> 395 | <%= If(cronTrigger IsNot Nothing, 396 | > 397 | <%= X.SpacesToNbsp(cronTrigger.CronExpressionString) %> 398 | , 399 | X.NoElements) 400 | %> 401 | <%= trigger.CalendarName %><%= tr.State %> 405 | <%= If(tr.IsPaused, 406 | SimpleForm(op("ResumeTrigger"), "Resume"), 407 | SimpleForm(op("PauseTrigger"), "Pause")) %> 408 | 409 | <%= SimpleForm(op("UnscheduleJob"), "Delete") %> 410 |
) 413 | End Function 414 | 415 | End Module 416 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/X.vb: -------------------------------------------------------------------------------- 1 | Imports System 2 | Imports System.Collections.Generic 3 | Imports System.IO 4 | Imports System.Linq 5 | Imports System.Text 6 | Imports System.Xml 7 | Imports System.Xml.Linq 8 | 9 | Public Module X 10 | Public ReadOnly XHTML1_0_Transitional_Doctype As XDocumentType = New XDocumentType("html", "-//W3C//DTD XHTML 1.0 Transitional//EN", "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd", Nothing) 11 | 12 | Public ReadOnly XHTML1_0_Strict_Doctype As XDocumentType = New XDocumentType("html", "-//W3C//DTD XHTML 1.0 Strict//EN", "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd", Nothing) 13 | 14 | Public ReadOnly HTML5_Doctype As XDocumentType = New XDocumentType("html", Nothing, Nothing, Nothing) 15 | 16 | Public ReadOnly nbsp As String = "\u00A0" 17 | 18 | Public ReadOnly raquo As String = "»" 19 | 20 | Public ReadOnly laquo As String = "«" 21 | 22 | Public ReadOnly rsaquo As String = ChrW(8250) 23 | 24 | Public ReadOnly lsaquo As String = ChrW(8249) 25 | 26 | Public ReadOnly copy As String = "©" 27 | 28 | Public ReadOnly amp As String = "&" 29 | 30 | Public ReadOnly lt As String = "<" 31 | 32 | Public ReadOnly gt As String = ">" 33 | 34 | Public ReadOnly XHTML_Namespace As XNamespace = XNamespace.[Get]("http://www.w3.org/1999/xhtml") 35 | 36 | Private ReadOnly emptyElems As HashSet(Of String) = New HashSet(Of String)() From {"area", "base", "basefont", "br", "col", "command", "frame", "hr", "img", "input", "isindex", "keygen", "link", "meta", "param", "source", "track", "wbr"} 37 | 38 | Public ReadOnly NoElements As IEnumerable(Of XElement) = Enumerable.Empty(Of XElement)() 39 | 40 | Public ReadOnly NoNodes As IEnumerable(Of XNode) = Enumerable.Empty(Of XNode)() 41 | 42 | Public Function A(name As String, value As String) As XAttribute 43 | Return New XAttribute(XName.[Get](name), value) 44 | End Function 45 | 46 | Public Function E(name As String, ParamArray content As Object()) As XElement 47 | Return New XElement(XName.[Get](name), content) 48 | End Function 49 | 50 | Public Function T(text As String) As XNode 51 | Return New XText(text) 52 | End Function 53 | 54 | Public Function Raw(xml As String) As XNode() 55 | Try 56 | Return XDocument.Parse("" + xml + "", LoadOptions.PreserveWhitespace).Document.Root.Nodes().ToArray 57 | Catch ex as Exception 58 | Throw New Exception($"Error parsing '{xml}'") 59 | End Try 60 | End Function 61 | 62 | 63 | Public Function Alter(e As XElement, pred As Func(Of Boolean), a As Action(Of XElement)) As XElement 64 | If pred() Then 65 | a(e) 66 | End If 67 | Return e 68 | End Function 69 | 70 | Public Function IsEmptyElement(elementName As String) As Boolean 71 | Return emptyElems.Contains(elementName) 72 | End Function 73 | 74 | 75 | Public Function IsEmptyElement(element As XElement) As Boolean 76 | Return X.IsEmptyElement(element.Name.LocalName) 77 | End Function 78 | 79 | 80 | Public Function FixEmptyElements(n As XNode) As XNode 81 | Dim e = TryCast(n, XElement) 82 | If e Is Nothing Then 83 | Return n 84 | End If 85 | Dim isEmptyElem = e.IsEmptyElement() 86 | If isEmptyElem AndAlso Not e.IsEmpty Then 87 | Return New XElement(e.Name, e.Attributes()) 88 | End If 89 | Dim children = e.Nodes().Select(AddressOf FixEmptyElements) 90 | If Not isEmptyElem AndAlso e.IsEmpty Then 91 | Return New XElement(e.Name, {e.Attributes(), New XText(""), children}) 92 | End If 93 | Return New XElement(e.Name, {e.Attributes(), children}) 94 | End Function 95 | 96 | 97 | Public Function ApplyNamespace(n As XNode, ns As XNamespace) As XNode 98 | Dim e = TryCast(n, XElement) 99 | If e IsNot Nothing Then 100 | Dim arg_58_0 = ns + e.Name.LocalName 101 | Dim children = e.Nodes().Select(Function(x As XNode) x.ApplyNamespace(ns)) 102 | Return New XElement(arg_58_0, {e.Attributes(), children}) 103 | End If 104 | Return n 105 | End Function 106 | 107 | 108 | Public Function MakeHTMLCompatible(n As XNode) As XNode 109 | Return n.ApplyNamespace(X.XHTML_Namespace).FixEmptyElements() 110 | End Function 111 | 112 | 113 | Public Function MakeHTML5Doc(root As XElement) As XDocument 114 | Return New XDocument({X.HTML5_Doctype, root.MakeHTMLCompatible()}) 115 | End Function 116 | 117 | Public Function CreateXmlWriter(output As Stream) As XmlWriter 118 | Dim settings = New XmlWriterSettings() With {.OmitXmlDeclaration = True, .ConformanceLevel = ConformanceLevel.Fragment, .NewLineHandling = NewLineHandling.None, .Encoding = New UTF8Encoding(False)} 119 | Return XmlWriter.Create(output, settings) 120 | End Function 121 | 122 | 123 | Public Sub WriteToStream(n As XNode, output As Stream) 124 | If n Is Nothing Then 125 | Return 126 | End If 127 | Using xmlwriter = X.CreateXmlWriter(output) 128 | n.FixEmptyElements().WriteTo(xmlwriter) 129 | End Using 130 | End Sub 131 | 132 | 133 | Public Sub WriteToStream(nodes As IEnumerable(Of XNode), output As Stream) 134 | If nodes Is Nothing Then 135 | Return 136 | End If 137 | Dim root = TryCast(<%= nodes %>.FixEmptyElements(), XElement) 138 | Using xmlwriter = X.CreateXmlWriter(output) 139 | Using enumerator = root.Nodes().GetEnumerator() 140 | While enumerator.MoveNext() 141 | enumerator.Current.WriteTo(xmlwriter) 142 | End While 143 | End Using 144 | End Using 145 | End Sub 146 | 147 | 148 | Public Function WriteToString(nodes As IEnumerable(Of XNode)) As String 149 | If nodes Is Nothing Then 150 | Return "" 151 | End If 152 | Dim [string] As String 153 | Using ms = New MemoryStream() 154 | nodes.WriteToStream(ms) 155 | [string] = Encoding.UTF8.GetString(ms.ToArray()) 156 | End Using 157 | Return [string] 158 | End Function 159 | 160 | ' 161 | ' Public Sub WriteToResponse(nodes As IEnumerable(Of XNode)) 162 | ' Dim ctx As HttpContext = HttpContext.Current 163 | ' If ctx Is Nothing Then 164 | ' Throw New Exception("No current HttpContext") 165 | ' End If 166 | ' nodes.WriteToStream(ctx.Response.OutputStream) 167 | ' End Sub 168 | 169 | ' 170 | ' Public Sub WriteToResponse(elements As IEnumerable(Of XElement)) 171 | ' Dim arg_1C_1 As Func(Of XElement, XNode) 172 | 'If(arg_1C_1 = X.CS$<>9__CachedAnonymousMethodDelegate4) Is Nothing Then 173 | ' arg_1C_1 = (X.CS$<>9__CachedAnonymousMethodDelegate4 = (Function(x As XElement) x)) 174 | ' End If 175 | ' elements.[Select](arg_1C_1).WriteToResponse() 176 | ' End Sub 177 | 178 | Public Function IsNullOrWhiteSpace(value As String) As Boolean 179 | If value IsNot Nothing Then 180 | For i As Integer = 0 To value.Length - 1 181 | If Not Char.IsWhiteSpace(value(i)) Then 182 | Return False 183 | End If 184 | Next 185 | End If 186 | Return True 187 | End Function 188 | 189 | 190 | Public Function IsWhiteSpace(n As XNode) As Boolean 191 | Dim t As XText = TryCast(n, XText) 192 | Return t IsNot Nothing AndAlso X.IsNullOrWhiteSpace(t.Value) 193 | End Function 194 | 195 | 196 | Public Function Trim(nodes As IEnumerable(Of XNode)) As IEnumerable(Of XNode) 197 | Return nodes.SkipWhile(AddressOf IsWhiteSpace).Reverse().SkipWhile(AddressOf IsWhiteSpace).Reverse() 198 | End Function 199 | 200 | Public Function SpacesToNbsp(s As String) As String 201 | If s Is Nothing Then 202 | Return Nothing 203 | End If 204 | Return s.Replace(" ", " ") 205 | End Function 206 | 207 | Public Function Javascript(content As String) As XElement 208 | Dim cdata = New XCData("*/" + content + "/*") 209 | Dim begin = New XText("/*") 210 | Dim [end] = New XText("*/") 211 | Return X.E("script", {X.A("type", "text/javascript"), begin, cdata, [end]}) 212 | End Function 213 | 214 | Public Function Javascript(content As XCData) As XElement 215 | Return X.Javascript(content.Value) 216 | End Function 217 | 218 | Public Function SelectOption(options As IEnumerable(Of XElement), value As String) As IEnumerable(Of XElement) 219 | Return options.Select(Function(e As XElement) 220 | Dim valueAtt = e.Attribute("value") 221 | If valueAtt Is Nothing Then 222 | Return e 223 | End If 224 | If valueAtt.Value <> value Then 225 | Return e 226 | End If 227 | Dim expr_31 = New XElement(e) 228 | expr_31.Add(X.A("selected", "selected")) 229 | Return expr_31 230 | End Function) 231 | End Function 232 | 233 | Public Function UnselectOption([option] As XElement) As XElement 234 | Return [option].RemoveAttr("selected") 235 | End Function 236 | 237 | 238 | Public Function RemoveChildNodes(element As XElement) As XElement 239 | Dim expr_06 = New XElement(element) 240 | expr_06.RemoveNodes() 241 | Return expr_06 242 | End Function 243 | 244 | 245 | Public Function RemoveAttr(element As XElement) As XElement 246 | Dim expr_06 = New XElement(element) 247 | expr_06.RemoveAttributes() 248 | Return expr_06 249 | End Function 250 | 251 | 252 | Public Function RemoveAttr(element As XElement, attribute As String) As XElement 253 | Dim arg_30_0 = element.RemoveAttr() 254 | Dim attr = element.Attributes().Where(Function(a As XAttribute) a.Name.LocalName <> attribute).ToArray() 255 | arg_30_0.Add(attr) 256 | Return arg_30_0 257 | End Function 258 | 259 | 260 | Public Function AttributeValue(element As XElement, attr As String) As String 261 | Dim a As XAttribute = element.Attribute(attr) 262 | If a Is Nothing Then 263 | Return Nothing 264 | End If 265 | Return a.Value 266 | End Function 267 | 268 | 269 | Public Function Match(Of T)(node As XNode, cdata As Func(Of XCData, T), comment As Func(Of XComment, T), text As Func(Of XText, T), instruction As Func(Of XProcessingInstruction, T), element As Func(Of XElement, T)) As T 270 | Dim ncdata = TryCast(node, XCData) 271 | If ncdata IsNot Nothing Then 272 | Return cdata(ncdata) 273 | End If 274 | Dim ncomment = TryCast(node, XComment) 275 | If ncomment IsNot Nothing Then 276 | Return comment(ncomment) 277 | End If 278 | Dim ntext = TryCast(node, XText) 279 | If ntext IsNot Nothing Then 280 | Return text(ntext) 281 | End If 282 | Dim ninstruction = TryCast(node, XProcessingInstruction) 283 | If ninstruction IsNot Nothing Then 284 | Return instruction(ninstruction) 285 | End If 286 | Dim nelement = TryCast(node, XElement) 287 | If nelement IsNot Nothing Then 288 | Return element(nelement) 289 | End If 290 | Throw New Exception("Unknown node type " + node.[GetType]().ToString()) 291 | End Function 292 | End Module 293 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.Views/job_scheduling_data_2_0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | Root level node 12 | 13 | 14 | 15 | 16 | 17 | Commands to be executed before scheduling the jobs and triggers in this file. 18 | 19 | 20 | 21 | 22 | Directives to be followed while scheduling the jobs and triggers in this file. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Version of the XML Schema instance 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Delete all jobs, if any, in the identified group. "*" can be used to identify all groups. Will also result in deleting all triggers related to the jobs. 47 | 48 | 49 | 50 | 51 | Delete all triggers, if any, in the identified group. "*" can be used to identify all groups. Will also result in deletion of related jobs that are non-durable. 52 | 53 | 54 | 55 | 56 | Delete the identified job if it exists (will also result in deleting all triggers related to it). 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Delete the identified trigger if it exists (will also result in deletion of related jobs that are non-durable). 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Whether the existing scheduling data (with same identifiers) will be overwritten. If false, and ignore-duplicates is not false, and jobs or triggers with the same names already exist as those in the file, an error will occur. 84 | 85 | 86 | 87 | 88 | If true (and overwrite-existing-data is false) then any job/triggers encountered in this file that have names that already exist in the scheduler will be ignored, and no error will be produced. 89 | 90 | 91 | 92 | 93 | If true trigger's start time is calculated based on earlier run time instead of fixed value. Trigger's start time must be undefined for this to work. 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Define a JobDetail 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Define a JobDataMap 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Define a JobDataMap entry 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | Define a Trigger 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Common Trigger definitions 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | Define a SimpleTrigger 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | Define a CronTrigger 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | Define a DateIntervalTrigger 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | Cron expression (see JavaDoc for examples) 220 | 221 | Special thanks to Chris Thatcher (thatcher@butterfly.net) for the regular expression! 222 | 223 | Regular expressions are not my strong point but I believe this is complete, 224 | with the caveat that order for expressions like 3-0 is not legal but will pass, 225 | and month and day names must be capitalized. 226 | If you want to examine the correctness look for the [\s] to denote the 227 | seperation of individual regular expressions. This is how I break them up visually 228 | to examine them: 229 | 230 | SECONDS: 231 | ( 232 | ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) 233 | | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) 234 | | ([\?]) 235 | | ([\*]) 236 | ) [\s] 237 | MINUTES: 238 | ( 239 | ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) 240 | | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) 241 | | ([\?]) 242 | | ([\*]) 243 | ) [\s] 244 | HOURS: 245 | ( 246 | ((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?) 247 | | (([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3])) 248 | | ([\?]) 249 | | ([\*]) 250 | ) [\s] 251 | DAY OF MONTH: 252 | ( 253 | ((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?) 254 | | (([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?) 255 | | (L(-[0-9])?) 256 | | (L(-[1-2][0-9])?) 257 | | (L(-[3][0-1])?) 258 | | (LW) 259 | | ([1-9]W) 260 | | ([1-3][0-9]W) 261 | | ([\?]) 262 | | ([\*]) 263 | )[\s] 264 | MONTH: 265 | ( 266 | ((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?) 267 | | (([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2])) 268 | | (((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?) 269 | | ((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)) 270 | | ([\?]) 271 | | ([\*]) 272 | )[\s] 273 | DAY OF WEEK: 274 | ( 275 | (([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?) 276 | | ([1-7]/([1-7])) 277 | | (((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?) 278 | | ((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?) 279 | | (([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))(L|LW)?) 280 | | (([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?) 281 | | ([\?]) 282 | | ([\*]) 283 | ) 284 | YEAR (OPTIONAL): 285 | ( 286 | [\s]? 287 | ([\*])? 288 | | ((19[7-9][0-9])|(20[0-9][0-9]))? 289 | | (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))? 290 | | ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)? 291 | ) 292 | 293 | 294 | 295 | 297 | 298 | 299 | 300 | 301 | 302 | Number of times to repeat the Trigger (-1 for indefinite) 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | Simple Trigger Misfire Instructions 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | Cron Trigger Misfire Instructions 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | Date Interval Trigger Misfire Instructions 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | Interval Units 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | -------------------------------------------------------------------------------- /QuartzNetWebConsole.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}") = "QuartzNetWebConsole", "QuartzNetWebConsole\QuartzNetWebConsole.csproj", "{72A7A322-38DD-47DD-8948-6ED6E2F9DC5D}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuartzNetWebConsole.Tests", "QuartzNetWebConsole.Tests\QuartzNetWebConsole.Tests.csproj", "{05021F00-C4D1-45C3-92EC-92C467B74F67}" 9 | EndProject 10 | Project("{F184B08F-C81C-45F6-A57F-5ABD9991F28F}") = "QuartzNetWebConsole.Views", "QuartzNetWebConsole.Views\QuartzNetWebConsole.Views.vbproj", "{7961AB01-1549-4340-8D3D-F0DA43B8C84F}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleApp", "SampleApp\SampleApp.csproj", "{894DEE25-8251-4FEE-B357-7EBB8D463734}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {72A7A322-38DD-47DD-8948-6ED6E2F9DC5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {72A7A322-38DD-47DD-8948-6ED6E2F9DC5D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {72A7A322-38DD-47DD-8948-6ED6E2F9DC5D}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {72A7A322-38DD-47DD-8948-6ED6E2F9DC5D}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {05021F00-C4D1-45C3-92EC-92C467B74F67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {05021F00-C4D1-45C3-92EC-92C467B74F67}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {05021F00-C4D1-45C3-92EC-92C467B74F67}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {05021F00-C4D1-45C3-92EC-92C467B74F67}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {7961AB01-1549-4340-8D3D-F0DA43B8C84F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {7961AB01-1549-4340-8D3D-F0DA43B8C84F}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {7961AB01-1549-4340-8D3D-F0DA43B8C84F}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {7961AB01-1549-4340-8D3D-F0DA43B8C84F}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {894DEE25-8251-4FEE-B357-7EBB8D463734}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {894DEE25-8251-4FEE-B357-7EBB8D463734}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {894DEE25-8251-4FEE-B357-7EBB8D463734}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {894DEE25-8251-4FEE-B357-7EBB8D463734}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /QuartzNetWebConsole/AbstractLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Quartz; 9 | using QuartzNetWebConsole.Views; 10 | 11 | namespace QuartzNetWebConsole { 12 | public abstract class AbstractLogger : ILogger { 13 | 14 | public virtual void JobExecutionVetoed(IJobExecutionContext context) { 15 | } 16 | 17 | public virtual void TriggerComplete(ITrigger trigger, IJobExecutionContext context, SchedulerInstruction triggerInstructionCode) { 18 | } 19 | 20 | public virtual Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = new CancellationToken()) 21 | { 22 | return Task.CompletedTask; 23 | } 24 | 25 | public virtual Task JobExecutionVetoed(IJobExecutionContext context, CancellationToken cancellationToken = new CancellationToken()) 26 | { 27 | return Task.CompletedTask; 28 | } 29 | 30 | public virtual Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, 31 | CancellationToken cancellationToken = new CancellationToken()) 32 | { 33 | return Task.CompletedTask; 34 | } 35 | 36 | public virtual Task TriggerFired(ITrigger trigger, IJobExecutionContext context, 37 | CancellationToken cancellationToken = new CancellationToken()) 38 | { 39 | return Task.CompletedTask; 40 | } 41 | 42 | public virtual Task VetoJobExecution(ITrigger trigger, IJobExecutionContext context, 43 | CancellationToken cancellationToken = new CancellationToken()) 44 | { 45 | return Task.FromResult(false); 46 | } 47 | 48 | public virtual Task TriggerMisfired(ITrigger trigger, CancellationToken cancellationToken = new CancellationToken()) 49 | { 50 | return Task.CompletedTask; 51 | } 52 | 53 | public virtual Task TriggerComplete(ITrigger trigger, IJobExecutionContext context, SchedulerInstruction triggerInstructionCode, 54 | CancellationToken cancellationToken = new CancellationToken()) 55 | { 56 | return Task.CompletedTask; 57 | } 58 | 59 | string IJobListener.Name { 60 | get { return "QuartzNetWebConsole.Logger"; } 61 | } 62 | 63 | string ITriggerListener.Name { 64 | get { return "QuartzNetWebConsole.Logger"; } 65 | } 66 | 67 | public abstract IEnumerator GetEnumerator(); 68 | 69 | IEnumerator IEnumerable.GetEnumerator() { 70 | return GetEnumerator(); 71 | } 72 | 73 | public abstract Expression Expression { get; } 74 | public abstract Type ElementType { get; } 75 | public abstract IQueryProvider Provider { get; } 76 | public abstract void Add(string msg); 77 | 78 | public virtual Task JobScheduled(ITrigger trigger, CancellationToken cancellationToken = new CancellationToken()) 79 | { 80 | return Task.CompletedTask; 81 | } 82 | 83 | public virtual Task JobUnscheduled(TriggerKey triggerKey, CancellationToken cancellationToken = new CancellationToken()) 84 | { 85 | return Task.CompletedTask; 86 | } 87 | 88 | public virtual Task TriggerFinalized(ITrigger trigger, CancellationToken cancellationToken = new CancellationToken()) 89 | { 90 | return Task.CompletedTask; 91 | } 92 | 93 | public virtual Task TriggerPaused(TriggerKey triggerKey, CancellationToken cancellationToken = new CancellationToken()) 94 | { 95 | return Task.CompletedTask; 96 | } 97 | 98 | public virtual Task TriggersPaused(string? triggerGroup, CancellationToken cancellationToken = new CancellationToken()) 99 | { 100 | return Task.CompletedTask; 101 | } 102 | 103 | public virtual Task TriggerResumed(TriggerKey triggerKey, CancellationToken cancellationToken = new CancellationToken()) 104 | { 105 | return Task.CompletedTask; 106 | } 107 | 108 | public virtual Task TriggersResumed(string? triggerGroup, CancellationToken cancellationToken = new CancellationToken()) 109 | { 110 | return Task.CompletedTask; 111 | } 112 | 113 | public virtual Task JobAdded(IJobDetail jobDetail, CancellationToken cancellationToken = new CancellationToken()) 114 | { 115 | return Task.CompletedTask; 116 | } 117 | 118 | public virtual Task JobDeleted(JobKey jobKey, CancellationToken cancellationToken = new CancellationToken()) 119 | { 120 | return Task.CompletedTask; 121 | } 122 | 123 | public virtual Task JobPaused(JobKey jobKey, CancellationToken cancellationToken = new CancellationToken()) 124 | { 125 | return Task.CompletedTask; 126 | } 127 | 128 | public virtual Task JobInterrupted(JobKey jobKey, CancellationToken cancellationToken = new CancellationToken()) 129 | { 130 | return Task.CompletedTask; 131 | } 132 | 133 | public virtual Task JobsPaused(string jobGroup, CancellationToken cancellationToken = new CancellationToken()) 134 | { 135 | return Task.CompletedTask; 136 | } 137 | 138 | public virtual Task JobResumed(JobKey jobKey, CancellationToken cancellationToken = new CancellationToken()) 139 | { 140 | return Task.CompletedTask; 141 | } 142 | 143 | public virtual Task JobsResumed(string jobGroup, CancellationToken cancellationToken = new CancellationToken()) 144 | { 145 | return Task.CompletedTask; 146 | } 147 | 148 | public virtual Task SchedulerError(string msg, SchedulerException cause, 149 | CancellationToken cancellationToken = new CancellationToken()) 150 | { 151 | return Task.CompletedTask; 152 | } 153 | 154 | public virtual Task SchedulerInStandbyMode(CancellationToken cancellationToken = new CancellationToken()) 155 | { 156 | return Task.CompletedTask; 157 | } 158 | 159 | public virtual Task SchedulerStarted(CancellationToken cancellationToken = new CancellationToken()) 160 | { 161 | return Task.CompletedTask; 162 | } 163 | 164 | public virtual Task SchedulerStarting(CancellationToken cancellationToken = new CancellationToken()) 165 | { 166 | return Task.CompletedTask; 167 | } 168 | 169 | public virtual Task SchedulerShutdown(CancellationToken cancellationToken = new CancellationToken()) 170 | { 171 | return Task.CompletedTask; 172 | } 173 | 174 | public virtual Task SchedulerShuttingdown(CancellationToken cancellationToken = new CancellationToken()) 175 | { 176 | return Task.CompletedTask; 177 | } 178 | 179 | public virtual Task SchedulingDataCleared(CancellationToken cancellationToken = new CancellationToken()) 180 | { 181 | return Task.CompletedTask; 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Controllers/IndexController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using System.Xml.Linq; 5 | using QuartzNetWebConsole.Utils; 6 | using QuartzNetWebConsole.Views; 7 | 8 | namespace QuartzNetWebConsole.Controllers { 9 | public class IndexController { 10 | 11 | public static async Task Execute(Func getScheduler) { 12 | var scheduler = getScheduler(); 13 | var triggerGroups = await (await scheduler.GetTriggerGroupNames()) 14 | .Traverse(async t => new GroupWithStatus(t, await scheduler.IsTriggerGroupPaused(t))); 15 | 16 | var jobGroups = await (await scheduler.GetJobGroupNames()) 17 | .Traverse(async j => new GroupWithStatus(j, await scheduler.IsJobGroupPaused(j))); 18 | 19 | var calendars = await (await scheduler.GetCalendarNames()) 20 | .Traverse(async name => Helpers.KV(name, (await scheduler.GetCalendar(name)).Description)); 21 | 22 | var jobListeners = scheduler.ListenerManager.GetJobListeners() 23 | .Select(j => Helpers.KV(j.Name, j.GetType())) 24 | .ToArray(); 25 | 26 | var triggerListeners = scheduler.ListenerManager.GetTriggerListeners() 27 | .Select(j => Helpers.KV(j.Name, j.GetType())) 28 | .ToArray(); 29 | 30 | var view = Views.Views.IndexPage( 31 | schedulerName: scheduler.SchedulerName, 32 | inStandby: scheduler.InStandbyMode, 33 | listeners: scheduler.ListenerManager.GetSchedulerListeners(), 34 | metadata: await scheduler.GetMetaData(), 35 | triggerGroups: triggerGroups, 36 | jobGroups: jobGroups, 37 | calendars: calendars, 38 | jobListeners: jobListeners, 39 | triggerListeners: triggerListeners); 40 | return new Response.XDocumentResponse(Helpers.XHTML(view)); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Controllers/JobGroupController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Quartz; 5 | using Quartz.Impl.Matchers; 6 | using QuartzNetWebConsole.Utils; 7 | using QuartzNetWebConsole.Views; 8 | 9 | namespace QuartzNetWebConsole.Controllers { 10 | public class JobGroupController { 11 | public static async Task Execute(RelativeUri uri, Func getScheduler) { 12 | var scheduler = getScheduler(); 13 | var querystring = QueryStringParser.ParseQueryString(uri.Query); 14 | 15 | var group = querystring["group"]; 16 | var jobNames = await scheduler.GetJobKeys(GroupMatcher.GroupEquals(group)); 17 | var runningJobs = await scheduler.GetCurrentlyExecutingJobs(); 18 | var jobs = await jobNames.Traverse(async j => { 19 | var job = await scheduler.GetJobDetail(j); 20 | // TODO apparently interruptible is not a thing any more? 21 | //var interruptible = typeof (IInterruptableJob).IsAssignableFrom(job.JobType); 22 | var jobContext = runningJobs.FirstOrDefault(r => r.JobDetail.Key.ToString() == job.Key.ToString()); 23 | return new JobWithContext(job, jobContext, false); 24 | }); 25 | var paused = await scheduler.IsJobGroupPaused(group); 26 | var highlight = querystring["highlight"]; 27 | var view = Views.Views.JobGroup(group, paused, highlight, uri.PathAndQuery, jobs); 28 | return new Response.XDocumentResponse(Helpers.XHTML(view)); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Controllers/LogController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using System.Xml.Linq; 8 | using QuartzNetWebConsole.Utils; 9 | using QuartzNetWebConsole.Views; 10 | 11 | namespace QuartzNetWebConsole.Controllers { 12 | public class LogController { 13 | private static IQueryable logsQ { 14 | get { 15 | return Setup.Logger ?? Enumerable.Empty().AsQueryable(); 16 | } 17 | } 18 | 19 | public static int DefaultPageSize { get; set; } 20 | 21 | static LogController() { 22 | DefaultPageSize = 25; 23 | } 24 | 25 | public static async Task Execute(RelativeUri url) 26 | { 27 | await Task.CompletedTask; 28 | var thisUrl = url.PathAndQuery.Split('?')[0]; 29 | var qs = url.ParseQueryString(); 30 | var pageSize = GetPageSize(qs); 31 | var pagination = new PaginationInfo( 32 | firstItemIndex: GetStartIndex(qs), 33 | pageSize: pageSize, 34 | totalItemCount: logsQ.Count(), 35 | pageUrl: "log.ashx?start=!0&max=" + pageSize); 36 | var logs = logsQ.Skip(pagination.FirstItemIndex).Take(pagination.PageSize).ToList(); 37 | var v = GetView(qs.AllKeys); 38 | var view = v.Value(logs, pagination, thisUrl); 39 | return new Response.XDocumentResponse(view, v.Key); 40 | } 41 | 42 | public static KeyValuePair, PaginationInfo, string, XDocument>> GetView(IEnumerable qs) { 43 | if (qs.Contains("rss")) 44 | return Helpers.KV("application/rss+xml", RSSView); 45 | return Helpers.KV("text/html", XHTMLView); 46 | } 47 | 48 | public static readonly Func, PaginationInfo, string, XDocument> XHTMLView = 49 | (entries, pagination, url) => Helpers.XHTML(Views.Views.Log(entries, pagination, url)); 50 | 51 | public static readonly Func, PaginationInfo, string, XDocument> RSSView = 52 | (entries, pagination, url) => new XDocument(Views.Views.LogRSS(url, entries)); 53 | 54 | public static int GetPageSize(NameValueCollection nv) { 55 | try { 56 | return int.Parse(nv["max"]); 57 | } catch { 58 | return DefaultPageSize; 59 | } 60 | } 61 | 62 | public static int GetStartIndex(NameValueCollection nv) { 63 | try { 64 | return int.Parse(nv["start"]); 65 | } catch { 66 | return 0; 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Controllers/SchedulerController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | using System.Web; 9 | using QuartzNetWebConsole.Utils; 10 | 11 | namespace QuartzNetWebConsole.Controllers { 12 | public class SchedulerController { 13 | private static readonly MethodInfo[] methods = typeof(SchedulerWrapper).GetMethods(); 14 | 15 | public class MethodParameters { 16 | public readonly MethodInfo method; 17 | public readonly string redirect; 18 | public readonly IEnumerable parameters; 19 | 20 | public MethodParameters(MethodInfo method, string redirect, IEnumerable parameters) { 21 | this.method = method; 22 | this.redirect = redirect; 23 | this.parameters = parameters; 24 | } 25 | } 26 | 27 | public static Func MatchParameters(IEnumerable parameterNames) { 28 | return m => m.GetParameters() 29 | .Select(p => p.Name.ToLowerInvariant()) 30 | .ToSet() 31 | .SetEquals(parameterNames); 32 | } 33 | 34 | public static MethodParameters GetMethodParameters(NameValueCollection qs) { 35 | var methodName = qs["method"].ToLowerInvariant(); 36 | var redirect = qs["next"] ?? "index.ashx"; 37 | var parameterNames = qs.AllKeys 38 | .Except(new[] { "method", "next" }) 39 | .Select(a => a.ToLowerInvariant()) 40 | .ToArray(); 41 | var method = methods 42 | .Where(n => n.Name.ToLowerInvariant() == methodName) 43 | .Where(MatchParameters(parameterNames)) 44 | .FirstOrDefault(); 45 | 46 | if (method == null) 47 | throw new Exception("Method not found: " + methodName); 48 | 49 | var parameters = method.GetParameters() 50 | .Select(p => Convert(qs[p.Name], p.ParameterType)) 51 | .ToArray(); 52 | return new MethodParameters(method, redirect, parameters); 53 | } 54 | 55 | public static async Task Execute(RelativeUri url, Func getScheduler) 56 | { 57 | await Task.CompletedTask; 58 | var scheduler = getScheduler(); 59 | var p = GetMethodParameters(url.ParseQueryString()); 60 | p.method.Invoke(scheduler, p.parameters.ToArray()); 61 | return new Response.RedirectResponse(p.redirect); 62 | } 63 | 64 | public static object Convert(string s, Type t) { 65 | var converter = TypeDescriptor.GetConverter(t); 66 | if (converter != null && converter.CanConvertFrom(typeof (string))) 67 | return converter.ConvertFromInvariantString(s); 68 | throw new Exception(string.Format("Can't convert to type '{0}'", t)); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Controllers/StaticController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | using QuartzNetWebConsole.Utils; 6 | 7 | namespace QuartzNetWebConsole.Controllers { 8 | public class StaticController { 9 | private static readonly Assembly assembly = typeof(StaticController).Assembly; 10 | 11 | public static async Task Execute(RelativeUri url) 12 | { 13 | await Task.CompletedTask; 14 | var querystring = url.ParseQueryString(); 15 | var resource = querystring["r"]; 16 | resource = string.Format("{0}.Resources.{1}", assembly.FullName.Split(',')[0], resource); 17 | var content = ReadResource(resource); 18 | return new Response.ContentResponse(content: content, contentType: querystring["t"]); 19 | } 20 | 21 | public static string ReadResource(string name) { 22 | using (var stream = assembly.GetManifestResourceStream(name)) 23 | using (var reader = new StreamReader(stream)) { 24 | return reader.ReadToEnd(); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Controllers/TriggerGroupController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Quartz; 4 | using Quartz.Impl; 5 | using Quartz.Impl.Matchers; 6 | using QuartzNetWebConsole.Utils; 7 | using QuartzNetWebConsole.Views; 8 | using System.Collections.Generic; 9 | using System.Threading.Tasks; 10 | using System.Xml.Linq; 11 | 12 | namespace QuartzNetWebConsole.Controllers { 13 | public class TriggerGroupController { 14 | private static async Task> GetTriggers(ISchedulerWrapper scheduler, string group) { 15 | var triggerKeys = await scheduler.GetTriggerKeys(GroupMatcher.GroupEquals(group)); 16 | if (triggerKeys == null) 17 | return null; 18 | 19 | return await triggerKeys.Traverse(async t => 20 | { 21 | var trigger = await scheduler.GetTrigger(t); 22 | var state = await scheduler.GetTriggerState(t); 23 | return new TriggerWithState(trigger, state); 24 | }); 25 | } 26 | 27 | public static async Task Execute(RelativeUri url, Func getScheduler) { 28 | var scheduler = getScheduler(); 29 | var qs = url.ParseQueryString(); 30 | var highlight = qs["highlight"]; 31 | var group = qs["group"]; 32 | var triggers = await GetTriggers(scheduler, group); 33 | var paused = await scheduler.IsTriggerGroupPaused(group); 34 | var v = Views.Views.TriggerGroup(group, paused, url.PathAndQuery, highlight, triggers); 35 | return new Response.XDocumentResponse(Helpers.XHTML(v)); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Controllers/TriggersByJobController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Quartz; 4 | using QuartzNetWebConsole.Utils; 5 | using QuartzNetWebConsole.Views; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace QuartzNetWebConsole.Controllers { 10 | public class TriggersByJobController { 11 | private static async Task> GetTriggers(ISchedulerWrapper scheduler, JobKey jobKey) { 12 | var triggers = await scheduler.GetTriggersOfJob(jobKey); 13 | if (triggers == null) 14 | return null; 15 | 16 | return await triggers.Traverse(async t => 17 | { 18 | var state = await scheduler.GetTriggerState(t.Key); 19 | return new TriggerWithState(t, state); 20 | }); 21 | } 22 | 23 | public static async Task Execute(RelativeUri url, Func getScheduler) { 24 | var scheduler = getScheduler(); 25 | var querystring = url.ParseQueryString(); 26 | var highlight = querystring["highlight"]; 27 | var group = querystring["group"]; 28 | var job = querystring["job"]; 29 | var jobKey = new JobKey(job, group); 30 | var triggers = await GetTriggers(scheduler, jobKey); 31 | var m = new TriggersByJobModel(triggers, url.PathAndQuery, group, job, highlight); 32 | return new Response.XDocumentResponse(Helpers.XHTML(Views.Views.TriggersByJob(m))); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace QuartzNetWebConsole { 4 | internal static class EnumerableExtensions { 5 | public static HashSet ToSet(this IEnumerable l) { 6 | return new HashSet(l); 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Quartz; 4 | using QuartzNetWebConsole.Views; 5 | 6 | namespace QuartzNetWebConsole { 7 | public interface ILogger: ISchedulerListener, IJobListener, ITriggerListener, IQueryable { 8 | void Add(string msg); 9 | } 10 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/MemoryLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Net; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using System.Web; 9 | using Quartz; 10 | using QuartzNetWebConsole.Utils; 11 | using QuartzNetWebConsole.Views; 12 | 13 | namespace QuartzNetWebConsole { 14 | /// 15 | /// Fixed-capacity in-memory logger. 16 | /// 17 | public class MemoryLogger : AbstractLogger { 18 | private readonly LimitedList entries; 19 | private readonly string partialQuartzConsoleUrl; 20 | 21 | public MemoryLogger(int capacity, string partialQuartzConsoleUrl) : this(capacity) { 22 | this.partialQuartzConsoleUrl = partialQuartzConsoleUrl; 23 | } 24 | 25 | public MemoryLogger(int capacity) { 26 | entries = new LimitedList(capacity); 27 | } 28 | 29 | public override void Add(string msg) { 30 | entries.Add(new LogEntry(msg)); 31 | } 32 | 33 | public override Task JobScheduled(ITrigger trigger, CancellationToken cancellationToken = new CancellationToken()) 34 | { 35 | var desc = string.Format("Job {0} scheduled with trigger {1}", DescribeJob(trigger.JobKey.Group, trigger.JobKey.Name), Describe(trigger)); 36 | entries.Add(new LogEntry(desc)); 37 | return Task.CompletedTask; 38 | } 39 | 40 | public override Task TriggerFinalized(ITrigger trigger, CancellationToken cancellationToken = new CancellationToken()) 41 | { 42 | entries.Add(new LogEntry("Trigger finalized: " + Describe(trigger))); 43 | return Task.CompletedTask; 44 | } 45 | 46 | public override Task SchedulerError(string msg, SchedulerException cause, 47 | CancellationToken cancellationToken = new CancellationToken()) 48 | { 49 | entries.Add(new LogEntry(string.Format("Scheduler error:
{0}

{1}
", 50 | WebUtility.HtmlEncode(msg), 51 | WebUtility.HtmlEncode(cause.ToString())))); 52 | return Task.CompletedTask; 53 | } 54 | 55 | public override Task SchedulerShutdown(CancellationToken cancellationToken = new CancellationToken()) 56 | { 57 | entries.Add(new LogEntry("Scheduler shutdown")); 58 | return Task.CompletedTask; 59 | } 60 | 61 | public override Task JobToBeExecuted(IJobExecutionContext context, CancellationToken cancellationToken = new CancellationToken()) 62 | { 63 | entries.Add(new LogEntry("Job to be executed: " + Describe(context))); 64 | return Task.CompletedTask; 65 | } 66 | 67 | public override void JobExecutionVetoed(IJobExecutionContext context) { 68 | entries.Add(new LogEntry("Job execution vetoed: " + Describe(context))); 69 | } 70 | 71 | public override Task JobWasExecuted(IJobExecutionContext context, JobExecutionException? jobException, 72 | CancellationToken cancellationToken = new CancellationToken()) 73 | { 74 | var description = "Job was executed: " + Describe(context); 75 | if (jobException != null) 76 | description += string.Format("
with exception:
{0}
", WebUtility.HtmlEncode(jobException.ToString())); 77 | entries.Add(new LogEntry(description)); 78 | return Task.CompletedTask; 79 | } 80 | 81 | public override Task TriggerFired(ITrigger trigger, IJobExecutionContext context, 82 | CancellationToken cancellationToken = new CancellationToken()) 83 | { 84 | entries.Add(new LogEntry("Job fired: " + Describe(context))); 85 | return Task.CompletedTask; 86 | } 87 | 88 | public override Task TriggerMisfired(ITrigger trigger, CancellationToken cancellationToken = new CancellationToken()) 89 | { 90 | entries.Add(new LogEntry("Job misfired: " + Describe(trigger))); 91 | return Task.CompletedTask; 92 | } 93 | 94 | public override void TriggerComplete(ITrigger trigger, IJobExecutionContext context, SchedulerInstruction triggerInstructionCode) { 95 | entries.Add(new LogEntry("Job complete: " + Describe(context))); 96 | } 97 | 98 | private string DescribeJob(string group, string name) { 99 | if (group == null && name == null) 100 | return "All"; 101 | return string.Format("{0}.{1}", LinkJobGroup(group), LinkJob(group, name)); 102 | } 103 | 104 | private string Link(string href, string text) { 105 | return string.Format("{2}", partialQuartzConsoleUrl, WebUtility.HtmlEncode(href), WebUtility.HtmlEncode(text)); 106 | } 107 | 108 | private string LinkTriggerGroup(string group) { 109 | return Link(string.Format("triggerGroup.ashx?group={0}", group), group); 110 | } 111 | 112 | private string LinkJobGroup(string group) { 113 | return Link(string.Format("jobGroup.ashx?group={0}", group), group); 114 | } 115 | 116 | private string LinkTrigger(string group, string name) { 117 | return Link(string.Format("triggerGroup.ashx?group={0}&highlight={0}.{1}#{0}.{1}", group, name), name); 118 | } 119 | 120 | private string LinkJob(string group, string name) { 121 | return Link(string.Format("jobGroup.ashx?group={0}&highlight={0}.{1}#{0}.{1}", group, name), name); 122 | } 123 | 124 | private string Describe(ITrigger trigger) { 125 | return string.Format("{0}.{1}", LinkTriggerGroup(trigger.Key.Group), LinkTrigger(trigger.Key.Group, trigger.Key.Name)); 126 | } 127 | 128 | private string Describe(IJobExecutionContext context) { 129 | var job = context.JobDetail; 130 | return string.Format("{0}.{1} (trigger {2})", LinkJobGroup(job.Key.Group), LinkJob(job.Key.Group, job.Key.Name), Describe(context.Trigger)); 131 | } 132 | 133 | private string DescribeTrigger(string group, string name) { 134 | if (group == null && name == null) 135 | return "All"; 136 | return string.Format("{0}.{1}", LinkTriggerGroup(group), LinkTrigger(group, name)); 137 | } 138 | 139 | 140 | public override IEnumerator GetEnumerator() { 141 | return entries.GetEnumerator(); 142 | } 143 | 144 | public override Expression Expression { 145 | get { return entries.AsQueryable().Expression; } 146 | } 147 | 148 | public override Type ElementType { 149 | get { return typeof (LogEntry); } 150 | } 151 | 152 | public override IQueryProvider Provider { 153 | get { return entries.AsQueryable().Provider; } 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/QuartzNetWebConsole.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Library 6 | latest 7 | true 8 | https://github.com/mausch/QuartzNetWebConsole 9 | 1.0.2 10 | Mauricio Scheffer 11 | https://raw.githubusercontent.com/mausch/QuartzNetWebConsole/master/license.txt 12 | true 13 | 14 | 15 | 16 | 17 | $(TargetsForTfmSpecificBuildOutput);CopyProjectReferencesToPackage 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /QuartzNetWebConsole/Resources/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | line-height: 140%; 3 | font-size: 0.8em; 4 | } 5 | div.group { 6 | padding: 7px; 7 | float: left; 8 | width: 45%; 9 | margin: 10px; 10 | } 11 | form { 12 | float: right; 13 | margin: 5px; 14 | } 15 | h2 { 16 | font-size: 130%; 17 | margin: 0px; 18 | padding: 5px; 19 | margin-bottom: 8px; 20 | border-bottom: solid 1px #444; 21 | background-color: #f5f5f5; 22 | } 23 | table, th, td { 24 | border: solid 1px #444; 25 | border-collapse: collapse; 26 | } 27 | th, td { 28 | padding: 2px 5px; 29 | } 30 | th { 31 | background-color: #eaeaea; 32 | } 33 | tr.alt { 34 | background-color: #f6f6f6; 35 | } 36 | tr.highlight { 37 | background-color: #FFFFCC; 38 | } 39 | table a { 40 | text-decoration: none; 41 | } 42 | table a:hover { 43 | text-decoration: underline; 44 | } 45 | 46 | /* pagination */ 47 | .pagination 48 | { 49 | margin: 5px; 50 | float: left; 51 | } 52 | .pagination a, .pagination span 53 | { 54 | border: solid 1px #888888; 55 | padding: 2px 5px; 56 | margin: 2px; 57 | } 58 | .pagination a 59 | { 60 | text-decoration: none; 61 | } 62 | .pagination a:hover 63 | { 64 | background-color: #FFFFCC; 65 | } 66 | .pagination .currentPage 67 | { 68 | font-weight: bold; 69 | background-color: #888888; 70 | color: White; 71 | } 72 | .pagination .disabledPage 73 | { 74 | color: #aaaaaa; 75 | } 76 | /* page size */ 77 | .pagesize 78 | { 79 | text-align: right; 80 | } 81 | .pagesize span 82 | { 83 | font-weight: bold; 84 | } 85 | -------------------------------------------------------------------------------- /QuartzNetWebConsole/Resources/time-toggle.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | const timeToggle = document.getElementById('time-toggle'); 3 | let isUTC = true; 4 | const originalTimes = new Map(); 5 | 6 | function convertTime(timeElement) { 7 | const originalTime = originalTimes.get(timeElement); 8 | if (!originalTime) { 9 | return; 10 | } 11 | 12 | const date = new Date(Date.parse(originalTime)); 13 | if (isNaN(date.getTime())) { 14 | return; 15 | } 16 | timeElement.textContent = (function() { 17 | if (isUTC) { 18 | return date.toISOString(); 19 | } else { 20 | return date.toLocaleString(); 21 | } 22 | })(); 23 | } 24 | 25 | function toggleTime() { 26 | isUTC = !isUTC; 27 | const timeElements = document.querySelectorAll('.datetime'); 28 | 29 | for (const timeElement of timeElements) { 30 | if (!originalTimes.has(timeElement)) { 31 | originalTimes.set(timeElement, timeElement.textContent); 32 | } 33 | convertTime(timeElement); 34 | } 35 | } 36 | 37 | timeToggle.addEventListener('click', toggleTime); 38 | }); 39 | -------------------------------------------------------------------------------- /QuartzNetWebConsole/Routing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using QuartzNetWebConsole.Controllers; 5 | using QuartzNetWebConsole.Utils; 6 | 7 | using Route = System.Collections.Generic.KeyValuePair>>; 8 | 9 | namespace QuartzNetWebConsole { 10 | public class Routing { 11 | 12 | private static ISchedulerWrapper GetSchedulerWrapper() { 13 | return new SchedulerWrapper(Setup.Scheduler()); 14 | } 15 | 16 | public static readonly IReadOnlyList Routes = 17 | new[] { 18 | Route("jobgroup", url => JobGroupController.Execute(url, GetSchedulerWrapper)), 19 | Route("index", _ => IndexController.Execute(GetSchedulerWrapper)), 20 | Route("log", LogController.Execute), 21 | Route("scheduler", ctx => SchedulerController.Execute(ctx, GetSchedulerWrapper)), 22 | Route("static", StaticController.Execute), 23 | Route("triggerGroup", ctx => TriggerGroupController.Execute(ctx, GetSchedulerWrapper)), 24 | Route("triggersByJob", ctx => TriggersByJobController.Execute(ctx, GetSchedulerWrapper)), 25 | 26 | }; 27 | 28 | private static Route Route(string path, Func> action) { 29 | return new Route(path, action); 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /QuartzNetWebConsole/Setup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Quartz; 6 | using QuartzNetWebConsole.Utils; 7 | 8 | namespace QuartzNetWebConsole { 9 | public static class Setup { 10 | /// 11 | /// What Quartz.NET scheduler should the web console use. 12 | /// 13 | public static Func Scheduler { get; set; } 14 | 15 | private static ILogger logger; 16 | 17 | /// 18 | /// Optional logger to attach to the web console 19 | /// 20 | public static ILogger Logger { 21 | get { return logger; } 22 | set { 23 | var scheduler = Scheduler(); 24 | if (logger != null) { 25 | IJobListener jobListener = logger; 26 | ITriggerListener triggerListener = logger; 27 | scheduler.ListenerManager.RemoveJobListener(jobListener.Name); 28 | scheduler.ListenerManager.RemoveTriggerListener(triggerListener.Name); 29 | scheduler.ListenerManager.RemoveSchedulerListener(logger); 30 | } 31 | if (value != null) { 32 | scheduler.ListenerManager.AddJobListener(value); 33 | //scheduler.ListenerManager.AddJobListenerMatcher() 34 | scheduler.ListenerManager.AddTriggerListener(value); 35 | scheduler.ListenerManager.AddSchedulerListener(value); 36 | } 37 | logger = value; 38 | } 39 | } 40 | 41 | static Setup() { 42 | Scheduler = () => { throw new Exception("Define QuartzNetWebConsole.Setup.Scheduler"); }; 43 | } 44 | 45 | public delegate Task AppFunc(IDictionary env); 46 | 47 | public static Func< 48 | Func, Task>, 49 | Func, Task> 50 | > Owin(string basePath, Func scheduler) { 51 | Setup.Scheduler = scheduler; 52 | return app => env => { 53 | var pathAndQuery = env.GetOwinRelativeUri(); 54 | if (!pathAndQuery.Path.StartsWith(basePath)) 55 | return app(env); 56 | 57 | var route = Routing.Routes 58 | .FirstOrDefault(x => pathAndQuery.Path.Replace(basePath, "").Split('.')[0].EndsWith(x.Key, StringComparison.InvariantCultureIgnoreCase)); 59 | 60 | if (route.Key is null) 61 | { 62 | return app(env); 63 | } 64 | 65 | // TODO don't block async 66 | var response = route.Value(pathAndQuery).Result.EvaluateResponse(); 67 | 68 | return response(env); 69 | }; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace QuartzNetWebConsole.Utils 7 | { 8 | public static class Extensions 9 | { 10 | /// 11 | /// Map each element of a structure to a Task, evaluate these tasks from left to right, and collect the results. 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static Task> Traverse(this IEnumerable source, Func> f) { 19 | return source.Select(f).Sequence(); 20 | } 21 | 22 | /// 23 | /// Evaluate each Task from left to right and collect the results. 24 | /// https://msdn.microsoft.com/en-us/library/hh194766.aspx 25 | /// 26 | /// 27 | /// 28 | /// 29 | public static async Task> Sequence(this IEnumerable> tasks) 30 | { 31 | var results = new List(); 32 | foreach (var t in tasks) 33 | { 34 | results.Add(await t); 35 | } 36 | return results; 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/ISchedulerWrapper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Quartz; 4 | using Quartz.Impl.Matchers; 5 | 6 | namespace QuartzNetWebConsole.Utils { 7 | public interface ISchedulerWrapper { 8 | void Shutdown(); 9 | void Start(); 10 | void Standby(); 11 | void PauseAll(); 12 | void ResumeAll(); 13 | void ResumeJobGroup(string groupName); 14 | void ResumeTriggerGroup(string groupName); 15 | void PauseJobGroup(string groupName); 16 | void PauseTriggerGroup(string groupName); 17 | void RemoveGlobalJobListener(string name); 18 | void RemoveGlobalTriggerListener(string name); 19 | void DeleteJob(string jobName, string groupName); 20 | void PauseJob(string jobName, string groupName); 21 | void ResumeJob(string jobName, string groupName); 22 | void TriggerJob(string jobName, string groupName); 23 | void Interrupt(string jobName, string groupName); 24 | void ResumeTrigger(string triggerName, string groupName); 25 | void PauseTrigger(string triggerName, string groupName); 26 | void UnscheduleJob(string triggerName, string groupName); 27 | Task> GetTriggerKeys(GroupMatcher matcher); 28 | Task> GetJobKeys(GroupMatcher matcher); 29 | Task IsJobGroupPaused(string groupName); 30 | Task IsTriggerGroupPaused(string groupName); 31 | Task> GetCurrentlyExecutingJobs(); 32 | Task GetJobDetail(JobKey key); 33 | Task> GetTriggerGroupNames(); 34 | Task> GetJobGroupNames(); 35 | Task> GetCalendarNames(); 36 | IListenerManager ListenerManager { get; } 37 | string SchedulerName { get; } 38 | bool InStandbyMode { get; } 39 | Task GetCalendar(string name); 40 | Task GetMetaData(); 41 | Task> GetTriggersOfJob(JobKey jobKey); 42 | Task GetTrigger(TriggerKey triggerKey); 43 | Task GetTriggerState(TriggerKey triggerKey); 44 | } 45 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/LimitedList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace QuartzNetWebConsole.Utils { 7 | /// 8 | /// List with fixed capacity, FIFO removal 9 | /// 10 | /// 11 | public class LimitedList : IEnumerable { 12 | private readonly LinkedList list = new LinkedList(); 13 | private readonly int capacity; 14 | 15 | public LimitedList(int capacity) { 16 | this.capacity = capacity; 17 | } 18 | 19 | [MethodImpl(MethodImplOptions.Synchronized)] 20 | public void Add(T e) { 21 | list.AddFirst(e); 22 | if (list.Count > capacity) 23 | list.RemoveLast(); 24 | } 25 | 26 | public IEnumerator GetEnumerator() { 27 | return list.GetEnumerator(); 28 | } 29 | 30 | IEnumerator IEnumerable.GetEnumerator() { 31 | return GetEnumerator(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/Owin.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace QuartzNetWebConsole.Utils { 9 | public static class Owin { 10 | public static string GetOwinRequestPath(this IDictionary env) { 11 | return (string)env["owin.RequestPath"]; 12 | } 13 | 14 | public static string GetOwinRequestQueryString(this IDictionary env) { 15 | return (string)env["owin.RequestQueryString"]; 16 | } 17 | 18 | public static RelativeUri GetOwinRelativeUri(this IDictionary env) { 19 | return new RelativeUri(env.GetOwinRequestPath() + "?" + env.GetOwinRequestQueryString()); 20 | } 21 | 22 | public static Stream GetOwinResponseBody(this IDictionary env) { 23 | return (Stream) env["owin.ResponseBody"]; 24 | } 25 | 26 | public static IDictionary GetOwinResponseHeaders(this IDictionary env) { 27 | return (IDictionary) env["owin.ResponseHeaders"]; 28 | } 29 | 30 | public static void SetOwinContentType(this IDictionary env, string contentType, string charset) { 31 | if (string.IsNullOrEmpty(contentType)) 32 | throw new ArgumentNullException("contentType"); 33 | if (string.IsNullOrEmpty(charset)) 34 | throw new ArgumentNullException("charset"); 35 | env.GetOwinResponseHeaders()["Content-Type"] = new[] {contentType + ";charset=" + charset}; 36 | } 37 | 38 | public static void SetOwinContentLength(this IDictionary env, long length) { 39 | env.GetOwinResponseHeaders()["Content-Length"] = new[] {length.ToString()}; 40 | } 41 | 42 | public static void SetOwinStatusCode(this IDictionary env, int statusCode) { 43 | env["owin.ResponseStatusCode"] = statusCode; 44 | } 45 | 46 | private static readonly Encoding encoding = Encoding.UTF8; 47 | 48 | public static Setup.AppFunc EvaluateResponse(this Response response) { 49 | return env => response.Match( 50 | content: async x => { 51 | env.SetOwinContentType(contentType: x.ContentType, charset: encoding.BodyName); 52 | var content = Encoding.UTF8.GetBytes(x.Content); 53 | env.SetOwinContentLength(content.Length); 54 | await env.GetOwinResponseBody().WriteAsync(content, 0, content.Length); 55 | }, 56 | xdoc: async x => { 57 | var eval = new Response.ContentResponse(content: x.Content.ToString(), contentType: x.ContentType).EvaluateResponse(); 58 | await eval(env); 59 | }, 60 | redirect: async x => { 61 | env.SetOwinStatusCode(302); 62 | env.GetOwinResponseHeaders()["Location"] = new[] { x.Location }; 63 | await Task.Yield(); 64 | }); 65 | } 66 | 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/QueryStringParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Specialized; 3 | using System.Linq; 4 | using System.Net; 5 | 6 | namespace QuartzNetWebConsole.Utils { 7 | public static class QueryStringParser { 8 | public static NameValueCollection ParseQueryString(string q) { 9 | if (string.IsNullOrEmpty(q)) 10 | return new NameValueCollection(); 11 | var query = q.Split('&').Select(x => x.Split('=').Select(WebUtility.UrlDecode).ToArray()); 12 | var r = new NameValueCollection(); 13 | foreach (var kv in query) { 14 | var value = kv.Length < 2 ? "" : kv[1]; 15 | r.Add(kv[0], value); 16 | } 17 | return r; 18 | } 19 | 20 | public static NameValueCollection ParseQueryString(this RelativeUri uri) { 21 | return ParseQueryString(uri.Query); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/RelativeUri.cs: -------------------------------------------------------------------------------- 1 | namespace QuartzNetWebConsole.Utils { 2 | public sealed class RelativeUri { 3 | public readonly string PathAndQuery; 4 | 5 | public RelativeUri(string pathAndQuery) { 6 | PathAndQuery = pathAndQuery; 7 | } 8 | 9 | public string Query { 10 | get { 11 | var parts = PathAndQuery.Split('?'); 12 | if (parts.Length < 2) 13 | return ""; 14 | return parts[1]; 15 | } 16 | } 17 | 18 | public string Path { 19 | get { 20 | return PathAndQuery.Split('?')[0]; 21 | } 22 | } 23 | 24 | public override string ToString() { 25 | return PathAndQuery; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/Response.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Xml.Linq; 3 | 4 | namespace QuartzNetWebConsole.Utils { 5 | public abstract class Response { 6 | private Response() { } 7 | 8 | public abstract T Match(Func content, Func xdoc, Func redirect); 9 | 10 | public void MatchAction(Action content, Action xdoc, Action redirect) { 11 | Match(x => { 12 | content(x); 13 | return null; 14 | }, x => { 15 | xdoc(x); 16 | return null; 17 | }, x => { 18 | redirect(x); 19 | return null; 20 | }); 21 | } 22 | 23 | public sealed class ContentResponse : Response { 24 | public readonly string Content; 25 | public readonly string ContentType; 26 | 27 | public ContentResponse(string content, string contentType) { 28 | Content = content; 29 | ContentType = contentType; 30 | } 31 | 32 | public override T Match(Func content, Func xdoc, Func redirect) { 33 | return content(this); 34 | } 35 | } 36 | 37 | public sealed class XDocumentResponse : Response { 38 | public readonly XDocument Content; 39 | public readonly string ContentType; 40 | 41 | public XDocumentResponse(XDocument content, string contentType) { 42 | Content = content; 43 | ContentType = contentType; 44 | } 45 | 46 | public XDocumentResponse(XDocument content): this(content, "text/html") {} 47 | 48 | public override T Match(Func content, Func xdoc, Func redirect) { 49 | return xdoc(this); 50 | } 51 | } 52 | 53 | public sealed class RedirectResponse : Response { 54 | public readonly string Location; 55 | 56 | public RedirectResponse(string location) { 57 | Location = location; 58 | } 59 | 60 | public override T Match(Func content, Func xdoc, Func redirect) { 61 | return redirect(this); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /QuartzNetWebConsole/Utils/SchedulerWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Quartz; 6 | using Quartz.Impl.Matchers; 7 | 8 | namespace QuartzNetWebConsole.Utils { 9 | public class SchedulerWrapper : ISchedulerWrapper { 10 | private readonly IScheduler scheduler; 11 | 12 | public SchedulerWrapper(IScheduler scheduler) { 13 | this.scheduler = scheduler; 14 | } 15 | 16 | public void Shutdown() { 17 | scheduler.Shutdown(); 18 | } 19 | 20 | public void Start() { 21 | scheduler.Start(); 22 | } 23 | 24 | public void Standby() { 25 | scheduler.Standby(); 26 | } 27 | 28 | public void PauseAll() { 29 | scheduler.PauseAll(); 30 | } 31 | 32 | public void ResumeAll() { 33 | scheduler.ResumeAll(); 34 | } 35 | 36 | public void ResumeJobGroup(string groupName) { 37 | scheduler.ResumeJobs(GroupMatcher.GroupEquals(groupName)); 38 | } 39 | 40 | public void ResumeTriggerGroup(string groupName) { 41 | scheduler.ResumeTriggers(GroupMatcher.GroupEquals(groupName)); 42 | } 43 | 44 | public void PauseJobGroup(string groupName) { 45 | scheduler.PauseJobs(GroupMatcher.GroupEquals(groupName)); 46 | } 47 | 48 | public void PauseTriggerGroup(string groupName) { 49 | scheduler.PauseTriggers(GroupMatcher.GroupEquals(groupName)); 50 | } 51 | 52 | public void RemoveGlobalJobListener(string name) { 53 | scheduler.ListenerManager.RemoveJobListener(name); 54 | } 55 | 56 | public void RemoveGlobalTriggerListener(string name) { 57 | scheduler.ListenerManager.RemoveTriggerListener(name); 58 | } 59 | 60 | public void DeleteJob(string jobName, string groupName) { 61 | scheduler.DeleteJob(new JobKey(jobName, groupName)); 62 | } 63 | 64 | public void PauseJob(string jobName, string groupName) { 65 | scheduler.PauseJob(new JobKey(jobName, groupName)); 66 | } 67 | 68 | public void ResumeJob(string jobName, string groupName) { 69 | scheduler.ResumeJob(new JobKey(jobName, groupName)); 70 | } 71 | 72 | public void TriggerJob(string jobName, string groupName) { 73 | scheduler.TriggerJob(new JobKey(jobName, groupName)); 74 | } 75 | 76 | public void Interrupt(string jobName, string groupName) { 77 | scheduler.Interrupt(new JobKey(jobName, groupName)); 78 | } 79 | 80 | public void ResumeTrigger(string triggerName, string groupName) { 81 | scheduler.ResumeTrigger(new TriggerKey(triggerName, groupName)); 82 | } 83 | 84 | public void PauseTrigger(string triggerName, string groupName) { 85 | scheduler.PauseTrigger(new TriggerKey(triggerName, groupName)); 86 | } 87 | 88 | public void UnscheduleJob(string triggerName, string groupName) { 89 | scheduler.UnscheduleJob(new TriggerKey(triggerName, groupName)); 90 | } 91 | 92 | public async Task> GetTriggerKeys(GroupMatcher matcher) 93 | { 94 | return await scheduler.GetTriggerKeys(matcher); 95 | } 96 | 97 | public async Task> GetJobKeys(GroupMatcher matcher) 98 | { 99 | return await scheduler.GetJobKeys(matcher); 100 | } 101 | 102 | public async Task IsJobGroupPaused(string groupName) { 103 | try { 104 | return await scheduler.IsJobGroupPaused(groupName); 105 | } catch (NotImplementedException) { 106 | return false; 107 | } 108 | } 109 | 110 | public async Task IsTriggerGroupPaused(string groupName) { 111 | try { 112 | return await scheduler.IsTriggerGroupPaused(groupName); 113 | } catch (NotImplementedException) { 114 | return false; 115 | } 116 | } 117 | 118 | public async Task> GetCurrentlyExecutingJobs() { 119 | return await scheduler.GetCurrentlyExecutingJobs(); 120 | } 121 | 122 | public async Task GetJobDetail(JobKey key) { 123 | return await scheduler.GetJobDetail(key); 124 | } 125 | 126 | public async Task> GetTriggerGroupNames() { 127 | return await scheduler.GetTriggerGroupNames(); 128 | } 129 | 130 | public async Task> GetJobGroupNames() { 131 | return await scheduler.GetJobGroupNames(); 132 | } 133 | 134 | public async Task> GetCalendarNames() { 135 | return await scheduler.GetCalendarNames(); 136 | } 137 | 138 | public IListenerManager ListenerManager { 139 | get { 140 | return scheduler.ListenerManager; 141 | } 142 | } 143 | 144 | public async Task GetCalendar(string name) { 145 | return await scheduler.GetCalendar(name); 146 | } 147 | 148 | public async Task GetMetaData() { 149 | return await scheduler.GetMetaData(); 150 | } 151 | 152 | public async Task> GetTriggersOfJob(JobKey jobKey) { 153 | try { 154 | return await scheduler.GetTriggersOfJob(jobKey); 155 | } catch (NotImplementedException) { 156 | return null; 157 | } 158 | } 159 | 160 | public async Task GetTrigger(TriggerKey triggerKey) { 161 | return await scheduler.GetTrigger(triggerKey); 162 | } 163 | 164 | public async Task GetTriggerState(TriggerKey triggerKey) { 165 | return await scheduler.GetTriggerState(triggerKey); 166 | } 167 | 168 | public string SchedulerName { 169 | get { 170 | return scheduler.SchedulerName; 171 | } 172 | } 173 | 174 | public bool InStandbyMode { 175 | get { 176 | return scheduler.InStandbyMode; 177 | } 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /QuartzNetWebConsole/job_scheduling_data_2_0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | 11 | Root level node 12 | 13 | 14 | 15 | 16 | 17 | Commands to be executed before scheduling the jobs and triggers in this file. 18 | 19 | 20 | 21 | 22 | Directives to be followed while scheduling the jobs and triggers in this file. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | Version of the XML Schema instance 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Delete all jobs, if any, in the identified group. "*" can be used to identify all groups. Will also result in deleting all triggers related to the jobs. 47 | 48 | 49 | 50 | 51 | Delete all triggers, if any, in the identified group. "*" can be used to identify all groups. Will also result in deletion of related jobs that are non-durable. 52 | 53 | 54 | 55 | 56 | Delete the identified job if it exists (will also result in deleting all triggers related to it). 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | Delete the identified trigger if it exists (will also result in deletion of related jobs that are non-durable). 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | Whether the existing scheduling data (with same identifiers) will be overwritten. If false, and ignore-duplicates is not false, and jobs or triggers with the same names already exist as those in the file, an error will occur. 84 | 85 | 86 | 87 | 88 | If true (and overwrite-existing-data is false) then any job/triggers encountered in this file that have names that already exist in the scheduler will be ignored, and no error will be produced. 89 | 90 | 91 | 92 | 93 | If true trigger's start time is calculated based on earlier run time instead of fixed value. Trigger's start time must be undefined for this to work. 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | Define a JobDetail 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Define a JobDataMap 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | Define a JobDataMap entry 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | Define a Trigger 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | Common Trigger definitions 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | Define a SimpleTrigger 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | Define a CronTrigger 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | Define a DateIntervalTrigger 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | Cron expression (see JavaDoc for examples) 220 | 221 | Special thanks to Chris Thatcher (thatcher@butterfly.net) for the regular expression! 222 | 223 | Regular expressions are not my strong point but I believe this is complete, 224 | with the caveat that order for expressions like 3-0 is not legal but will pass, 225 | and month and day names must be capitalized. 226 | If you want to examine the correctness look for the [\s] to denote the 227 | seperation of individual regular expressions. This is how I break them up visually 228 | to examine them: 229 | 230 | SECONDS: 231 | ( 232 | ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) 233 | | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) 234 | | ([\?]) 235 | | ([\*]) 236 | ) [\s] 237 | MINUTES: 238 | ( 239 | ((([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?,)*([0-9]|[0-5][0-9])(-([0-9]|[0-5][0-9]))?) 240 | | (([\*]|[0-9]|[0-5][0-9])/([0-9]|[0-5][0-9])) 241 | | ([\?]) 242 | | ([\*]) 243 | ) [\s] 244 | HOURS: 245 | ( 246 | ((([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?,)*([0-9]|[0-1][0-9]|[2][0-3])(-([0-9]|[0-1][0-9]|[2][0-3]))?) 247 | | (([\*]|[0-9]|[0-1][0-9]|[2][0-3])/([0-9]|[0-1][0-9]|[2][0-3])) 248 | | ([\?]) 249 | | ([\*]) 250 | ) [\s] 251 | DAY OF MONTH: 252 | ( 253 | ((([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?,)*([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(-([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1]))?(C)?) 254 | | (([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])/([1-9]|[0][1-9]|[1-2][0-9]|[3][0-1])(C)?) 255 | | (L(-[0-9])?) 256 | | (L(-[1-2][0-9])?) 257 | | (L(-[3][0-1])?) 258 | | (LW) 259 | | ([1-9]W) 260 | | ([1-3][0-9]W) 261 | | ([\?]) 262 | | ([\*]) 263 | )[\s] 264 | MONTH: 265 | ( 266 | ((([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?,)*([1-9]|0[1-9]|1[0-2])(-([1-9]|0[1-9]|1[0-2]))?) 267 | | (([1-9]|0[1-9]|1[0-2])/([1-9]|0[1-9]|1[0-2])) 268 | | (((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?,)*(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(-(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?) 269 | | ((JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)/(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)) 270 | | ([\?]) 271 | | ([\*]) 272 | )[\s] 273 | DAY OF WEEK: 274 | ( 275 | (([1-7](-([1-7]))?,)*([1-7])(-([1-7]))?) 276 | | ([1-7]/([1-7])) 277 | | (((MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?,)*(MON|TUE|WED|THU|FRI|SAT|SUN)(-(MON|TUE|WED|THU|FRI|SAT|SUN))?(C)?) 278 | | ((MON|TUE|WED|THU|FRI|SAT|SUN)/(MON|TUE|WED|THU|FRI|SAT|SUN)(C)?) 279 | | (([1-7]|(MON|TUE|WED|THU|FRI|SAT|SUN))(L|LW)?) 280 | | (([1-7]|MON|TUE|WED|THU|FRI|SAT|SUN)#([1-7])?) 281 | | ([\?]) 282 | | ([\*]) 283 | ) 284 | YEAR (OPTIONAL): 285 | ( 286 | [\s]? 287 | ([\*])? 288 | | ((19[7-9][0-9])|(20[0-9][0-9]))? 289 | | (((19[7-9][0-9])|(20[0-9][0-9]))/((19[7-9][0-9])|(20[0-9][0-9])))? 290 | | ((((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?,)*((19[7-9][0-9])|(20[0-9][0-9]))(-((19[7-9][0-9])|(20[0-9][0-9])))?)? 291 | ) 292 | 293 | 294 | 295 | 297 | 298 | 299 | 300 | 301 | 302 | Number of times to repeat the Trigger (-1 for indefinite) 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | Simple Trigger Misfire Instructions 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | Cron Trigger Misfire Instructions 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | Date Interval Trigger Misfire Instructions 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | Interval Units 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Embeddable web admin for the Quartz.NET scheduler 2 | 3 | [Nuget package](https://www.nuget.org/packages/QuartzNetWebConsole/). 4 | 5 | [An old blog post that's still relevant about what the project does](http://bugsquash.blogspot.com/2010/06/embeddable-quartznet-web-consoles.html). 6 | 7 | To set this up in your ASP.NET Core (or any OWIN-compatible framework really), add to your Startup: 8 | 9 | ``` 10 | app.UseOwin(m => 11 | { 12 | m(Setup.Owin("/quartz/", () => Program.Scheduler)); 13 | Setup.Logger = new MemoryLogger(100, "/quartz/"); 14 | }); 15 | ``` 16 | 17 | This repo also has a [reference sample app](SampleApp). -------------------------------------------------------------------------------- /SampleApp/HelloJob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Quartz; 5 | 6 | namespace SampleApp { 7 | /// 8 | /// A sample dummy job 9 | /// 10 | [DisallowConcurrentExecution] 11 | public class HelloJob : IJob { 12 | public async Task Execute(IJobExecutionContext context) 13 | { 14 | await Task.Delay(5000); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /SampleApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | using Quartz; 10 | using Quartz.Impl; 11 | 12 | namespace SampleApp 13 | { 14 | public class Program 15 | { 16 | public static IScheduler Scheduler { get; private set; } 17 | 18 | public static async Task Main(string[] args) 19 | { 20 | // First, initialize Quartz.NET as usual. In this sample app I'll configure Quartz.NET by code. 21 | var schedulerFactory = new StdSchedulerFactory(); 22 | Scheduler = await schedulerFactory.GetScheduler(); 23 | await Scheduler.Start(); 24 | 25 | // I'll add some global listeners 26 | //scheduler.ListenerManager.AddJobListener(new GlobalJobListener()); 27 | //scheduler.ListenerManager.AddTriggerListener(new GlobalTriggerListener()); 28 | 29 | // A sample trigger and job 30 | var trigger = TriggerBuilder.Create() 31 | .WithIdentity("myTrigger") 32 | .WithSchedule(DailyTimeIntervalScheduleBuilder.Create() 33 | .WithIntervalInSeconds(6)) 34 | .StartNow() 35 | .Build(); 36 | var job = new JobDetailImpl("myJob", null, typeof(HelloJob)); 37 | await Scheduler.ScheduleJob(job, trigger); 38 | 39 | // A cron trigger and job 40 | var cron = TriggerBuilder.Create() 41 | .WithIdentity("myCronTrigger") 42 | .ForJob(job.Key) 43 | .WithCronSchedule("0/10 * * * * ?") // every 10 seconds 44 | .Build(); 45 | 46 | await Scheduler.ScheduleJob(cron); 47 | 48 | // A dummy calendar 49 | //scheduler.AddCalendar("myCalendar", new DummyCalendar { Description = "dummy calendar" }, false, false); 50 | 51 | await CreateHostBuilder(args).Build().RunAsync(); 52 | } 53 | 54 | public static IHostBuilder CreateHostBuilder(string[] args) => 55 | Host.CreateDefaultBuilder(args) 56 | .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); 57 | } 58 | } -------------------------------------------------------------------------------- /SampleApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:60733", 7 | "sslPort": 44360 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "SampleApp": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": false, 22 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SampleApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using QuartzNetWebConsole; 11 | 12 | namespace SampleApp 13 | { 14 | public class Startup 15 | { 16 | // This method gets called by the runtime. Use this method to add services to the container. 17 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 18 | public void ConfigureServices(IServiceCollection services) 19 | { 20 | } 21 | 22 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 23 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 24 | { 25 | if (env.IsDevelopment()) 26 | { 27 | app.UseDeveloperExceptionPage(); 28 | } 29 | 30 | app.UseRouting(); 31 | 32 | app.UseEndpoints(endpoints => 33 | { 34 | endpoints.MapGet("/", async context => { await context.Response.WriteAsync("Hello World!"); }); 35 | }); 36 | 37 | app.UseOwin(m => 38 | { 39 | m(Setup.Owin("/quartz/", () => Program.Scheduler)); 40 | Setup.Logger = new MemoryLogger(100, "/quartz/"); 41 | }); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /SampleApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /SampleApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-parts": { 4 | "inputs": { 5 | "nixpkgs-lib": "nixpkgs-lib" 6 | }, 7 | "locked": { 8 | "lastModified": 1717285511, 9 | "narHash": "sha256-iKzJcpdXih14qYVcZ9QC9XuZYnPc6T8YImb6dX166kw=", 10 | "owner": "hercules-ci", 11 | "repo": "flake-parts", 12 | "rev": "2a55567fcf15b1b1c7ed712a2c6fadaec7412ea8", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "hercules-ci", 17 | "repo": "flake-parts", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1741851582, 24 | "narHash": "sha256-cPfs8qMccim2RBgtKGF+x9IBCduRvd/N5F4nYpU0TVE=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "6607cf789e541e7873d40d3a8f7815ea92204f32", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs-lib": { 38 | "locked": { 39 | "lastModified": 1717284937, 40 | "narHash": "sha256-lIbdfCsf8LMFloheeE6N31+BMIeixqyQWbSr2vk79EQ=", 41 | "type": "tarball", 42 | "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" 43 | }, 44 | "original": { 45 | "type": "tarball", 46 | "url": "https://github.com/NixOS/nixpkgs/archive/eb9ceca17df2ea50a250b6b27f7bf6ab0186f198.tar.gz" 47 | } 48 | }, 49 | "root": { 50 | "inputs": { 51 | "flake-parts": "flake-parts", 52 | "nixpkgs": "nixpkgs" 53 | } 54 | } 55 | }, 56 | "root": "root", 57 | "version": 7 58 | } 59 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "QuartzNetWebConsole"; 3 | 4 | inputs = { 5 | flake-parts.url = "github:hercules-ci/flake-parts"; 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 7 | }; 8 | 9 | outputs = inputs@{ flake-parts, ... }: 10 | flake-parts.lib.mkFlake { inherit inputs; } { 11 | imports = [ 12 | # To import a flake module 13 | # 1. Add foo to inputs 14 | # 2. Add foo as a parameter to the outputs function 15 | # 3. Add here: foo.flakeModule 16 | 17 | ]; 18 | systems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin" ]; 19 | perSystem = { config, self', inputs', pkgs, system, ... }: { 20 | # Per-system attributes can be defined here. The self' and inputs' 21 | # module parameters provide easy access to attributes of the same 22 | # system. 23 | 24 | # Equivalent to inputs'.nixpkgs.legacyPackages.hello; 25 | # packages.default = pkgs.hello; 26 | devShells.default = pkgs.mkShell { 27 | buildInputs = [ 28 | pkgs.dotnet-sdk_8 29 | ]; 30 | }; 31 | }; 32 | flake = { 33 | # The usual flake attributes can be defined here, including system- 34 | # agnostic ones like nixosModule and system-enumerating ones, although 35 | # those are more easily expressed in perSystem. 36 | 37 | }; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Quartz.Net web console 2 | 3 | Copyright 2010 Mauricio Scheffer 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | 18 | 19 | Apache License 20 | Version 2.0, January 2004 21 | http://www.apache.org/licenses/ 22 | 23 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 24 | 25 | 1. Definitions. 26 | 27 | "License" shall mean the terms and conditions for use, reproduction, 28 | and distribution as defined by Sections 1 through 9 of this document. 29 | 30 | "Licensor" shall mean the copyright owner or entity authorized by 31 | the copyright owner that is granting the License. 32 | 33 | "Legal Entity" shall mean the union of the acting entity and all 34 | other entities that control, are controlled by, or are under common 35 | control with that entity. For the purposes of this definition, 36 | "control" means (i) the power, direct or indirect, to cause the 37 | direction or management of such entity, whether by contract or 38 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 39 | outstanding shares, or (iii) beneficial ownership of such entity. 40 | 41 | "You" (or "Your") shall mean an individual or Legal Entity 42 | exercising permissions granted by this License. 43 | 44 | "Source" form shall mean the preferred form for making modifications, 45 | including but not limited to software source code, documentation 46 | source, and configuration files. 47 | 48 | "Object" form shall mean any form resulting from mechanical 49 | transformation or translation of a Source form, including but 50 | not limited to compiled object code, generated documentation, 51 | and conversions to other media types. 52 | 53 | "Work" shall mean the work of authorship, whether in Source or 54 | Object form, made available under the License, as indicated by a 55 | copyright notice that is included in or attached to the work 56 | (an example is provided in the Appendix below). 57 | 58 | "Derivative Works" shall mean any work, whether in Source or Object 59 | form, that is based on (or derived from) the Work and for which the 60 | editorial revisions, annotations, elaborations, or other modifications 61 | represent, as a whole, an original work of authorship. For the purposes 62 | of this License, Derivative Works shall not include works that remain 63 | separable from, or merely link (or bind by name) to the interfaces of, 64 | the Work and Derivative Works thereof. 65 | 66 | "Contribution" shall mean any work of authorship, including 67 | the original version of the Work and any modifications or additions 68 | to that Work or Derivative Works thereof, that is intentionally 69 | submitted to Licensor for inclusion in the Work by the copyright owner 70 | or by an individual or Legal Entity authorized to submit on behalf of 71 | the copyright owner. For the purposes of this definition, "submitted" 72 | means any form of electronic, verbal, or written communication sent 73 | to the Licensor or its representatives, including but not limited to 74 | communication on electronic mailing lists, source code control systems, 75 | and issue tracking systems that are managed by, or on behalf of, the 76 | Licensor for the purpose of discussing and improving the Work, but 77 | excluding communication that is conspicuously marked or otherwise 78 | designated in writing by the copyright owner as "Not a Contribution." 79 | 80 | "Contributor" shall mean Licensor and any individual or Legal Entity 81 | on behalf of whom a Contribution has been received by Licensor and 82 | subsequently incorporated within the Work. 83 | 84 | 2. Grant of Copyright License. Subject to the terms and conditions of 85 | this License, each Contributor hereby grants to You a perpetual, 86 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 87 | copyright license to reproduce, prepare Derivative Works of, 88 | publicly display, publicly perform, sublicense, and distribute the 89 | Work and such Derivative Works in Source or Object form. 90 | 91 | 3. Grant of Patent License. Subject to the terms and conditions of 92 | this License, each Contributor hereby grants to You a perpetual, 93 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 94 | (except as stated in this section) patent license to make, have made, 95 | use, offer to sell, sell, import, and otherwise transfer the Work, 96 | where such license applies only to those patent claims licensable 97 | by such Contributor that are necessarily infringed by their 98 | Contribution(s) alone or by combination of their Contribution(s) 99 | with the Work to which such Contribution(s) was submitted. If You 100 | institute patent litigation against any entity (including a 101 | cross-claim or counterclaim in a lawsuit) alleging that the Work 102 | or a Contribution incorporated within the Work constitutes direct 103 | or contributory patent infringement, then any patent licenses 104 | granted to You under this License for that Work shall terminate 105 | as of the date such litigation is filed. 106 | 107 | 4. Redistribution. You may reproduce and distribute copies of the 108 | Work or Derivative Works thereof in any medium, with or without 109 | modifications, and in Source or Object form, provided that You 110 | meet the following conditions: 111 | 112 | (a) You must give any other recipients of the Work or 113 | Derivative Works a copy of this License; and 114 | 115 | (b) You must cause any modified files to carry prominent notices 116 | stating that You changed the files; and 117 | 118 | (c) You must retain, in the Source form of any Derivative Works 119 | that You distribute, all copyright, patent, trademark, and 120 | attribution notices from the Source form of the Work, 121 | excluding those notices that do not pertain to any part of 122 | the Derivative Works; and 123 | 124 | (d) If the Work includes a "NOTICE" text file as part of its 125 | distribution, then any Derivative Works that You distribute must 126 | include a readable copy of the attribution notices contained 127 | within such NOTICE file, excluding those notices that do not 128 | pertain to any part of the Derivative Works, in at least one 129 | of the following places: within a NOTICE text file distributed 130 | as part of the Derivative Works; within the Source form or 131 | documentation, if provided along with the Derivative Works; or, 132 | within a display generated by the Derivative Works, if and 133 | wherever such third-party notices normally appear. The contents 134 | of the NOTICE file are for informational purposes only and 135 | do not modify the License. You may add Your own attribution 136 | notices within Derivative Works that You distribute, alongside 137 | or as an addendum to the NOTICE text from the Work, provided 138 | that such additional attribution notices cannot be construed 139 | as modifying the License. 140 | 141 | You may add Your own copyright statement to Your modifications and 142 | may provide additional or different license terms and conditions 143 | for use, reproduction, or distribution of Your modifications, or 144 | for any such Derivative Works as a whole, provided Your use, 145 | reproduction, and distribution of the Work otherwise complies with 146 | the conditions stated in this License. 147 | 148 | 5. Submission of Contributions. Unless You explicitly state otherwise, 149 | any Contribution intentionally submitted for inclusion in the Work 150 | by You to the Licensor shall be under the terms and conditions of 151 | this License, without any additional terms or conditions. 152 | Notwithstanding the above, nothing herein shall supersede or modify 153 | the terms of any separate license agreement you may have executed 154 | with Licensor regarding such Contributions. 155 | 156 | 6. Trademarks. This License does not grant permission to use the trade 157 | names, trademarks, service marks, or product names of the Licensor, 158 | except as required for reasonable and customary use in describing the 159 | origin of the Work and reproducing the content of the NOTICE file. 160 | 161 | 7. Disclaimer of Warranty. Unless required by applicable law or 162 | agreed to in writing, Licensor provides the Work (and each 163 | Contributor provides its Contributions) on an "AS IS" BASIS, 164 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 165 | implied, including, without limitation, any warranties or conditions 166 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 167 | PARTICULAR PURPOSE. You are solely responsible for determining the 168 | appropriateness of using or redistributing the Work and assume any 169 | risks associated with Your exercise of permissions under this License. 170 | 171 | 8. Limitation of Liability. In no event and under no legal theory, 172 | whether in tort (including negligence), contract, or otherwise, 173 | unless required by applicable law (such as deliberate and grossly 174 | negligent acts) or agreed to in writing, shall any Contributor be 175 | liable to You for damages, including any direct, indirect, special, 176 | incidental, or consequential damages of any character arising as a 177 | result of this License or out of the use or inability to use the 178 | Work (including but not limited to damages for loss of goodwill, 179 | work stoppage, computer failure or malfunction, or any and all 180 | other commercial damages or losses), even if such Contributor 181 | has been advised of the possibility of such damages. 182 | 183 | 9. Accepting Warranty or Additional Liability. While redistributing 184 | the Work or Derivative Works thereof, You may choose to offer, 185 | and charge a fee for, acceptance of support, warranty, indemnity, 186 | or other liability obligations and/or rights consistent with this 187 | License. However, in accepting such obligations, You may act only 188 | on Your own behalf and on Your sole responsibility, not on behalf 189 | of any other Contributor, and only if You agree to indemnify, 190 | defend, and hold each Contributor harmless for any liability 191 | incurred by, or claims asserted against, such Contributor by reason 192 | of your accepting any such warranty or additional liability. 193 | 194 | END OF TERMS AND CONDITIONS --------------------------------------------------------------------------------