From 25d2c6596df6b22e4e05ded85753f80e531fd836 Mon Sep 17 00:00:00 2001 From: Shaun Walker Date: Fri, 15 Nov 2019 08:42:31 -0500 Subject: [PATCH] completed background job scheduler --- Oqtane.Client/Modules/Admin/Jobs/Add.razor | 137 ++++++++++++ Oqtane.Client/Modules/Admin/Jobs/Edit.razor | 160 ++++++++++++++ Oqtane.Client/Modules/Admin/Jobs/Index.razor | 137 ++++++++++++ Oqtane.Client/Modules/Admin/Jobs/Log.razor | 64 ++++++ Oqtane.Client/Modules/Admin/Roles/Add.razor | 17 +- Oqtane.Client/Modules/Admin/Roles/Edit.razor | 13 +- .../Modules/Controls/ActionDialog.razor | 19 +- .../Services/Interfaces/IJobLogService.cs | 19 ++ .../Services/Interfaces/IJobService.cs | 23 ++ .../Interfaces/IScheduleLogService.cs | 19 -- .../Services/Interfaces/IScheduleService.cs | 19 -- Oqtane.Client/Services/JobLogService.cs | 54 +++++ Oqtane.Client/Services/JobService.cs | 64 ++++++ Oqtane.Client/Services/ScheduleLogService.cs | 54 ----- Oqtane.Client/Services/ScheduleService.cs | 54 ----- Oqtane.Client/Startup.cs | 4 +- Oqtane.Server/Controllers/JobController.cs | 105 ++++++++++ ...eduleController.cs => JobLogController.cs} | 38 ++-- .../Controllers/ScheduleLogController.cs | 74 ------- .../Infrastructure/HostedServiceBase.cs | 92 -------- .../{ => Interfaces}/IInstallationManager.cs | 0 .../{ => Interfaces}/ILogManager.cs | 0 .../Infrastructure/Jobs/HostedServiceBase.cs | 197 ++++++++++++++++++ .../Infrastructure/Jobs/SampleJob.cs | 33 +++ Oqtane.Server/Infrastructure/TestJob.cs | 21 -- .../Repository/Context/DBContextBase.cs | 2 +- .../Repository/Context/MasterDBContext.cs | 6 +- .../Interfaces/IJobLogRepository.cs | 14 ++ .../Repository/Interfaces/IJobRepository.cs | 14 ++ .../Interfaces/IScheduleLogRepository.cs | 14 -- .../Interfaces/IScheduleRepository.cs | 14 -- Oqtane.Server/Repository/JobLogRepository.cs | 51 +++++ Oqtane.Server/Repository/JobRepository.cs | 49 +++++ .../Repository/ScheduleLogRepository.cs | 49 ----- .../Repository/ScheduleRepository.cs | 49 ----- Oqtane.Server/Repository/SiteRepository.cs | 3 + Oqtane.Server/Repository/TenantResolver.cs | 12 +- Oqtane.Server/Scripts/Master.sql | 78 ++++--- Oqtane.Server/Startup.cs | 12 +- Oqtane.Shared/Models/{Schedule.cs => Job.cs} | 10 +- .../Models/{ScheduleLog.cs => JobLog.cs} | 8 +- 41 files changed, 1248 insertions(+), 554 deletions(-) create mode 100644 Oqtane.Client/Modules/Admin/Jobs/Add.razor create mode 100644 Oqtane.Client/Modules/Admin/Jobs/Edit.razor create mode 100644 Oqtane.Client/Modules/Admin/Jobs/Index.razor create mode 100644 Oqtane.Client/Modules/Admin/Jobs/Log.razor create mode 100644 Oqtane.Client/Services/Interfaces/IJobLogService.cs create mode 100644 Oqtane.Client/Services/Interfaces/IJobService.cs delete mode 100644 Oqtane.Client/Services/Interfaces/IScheduleLogService.cs delete mode 100644 Oqtane.Client/Services/Interfaces/IScheduleService.cs create mode 100644 Oqtane.Client/Services/JobLogService.cs create mode 100644 Oqtane.Client/Services/JobService.cs delete mode 100644 Oqtane.Client/Services/ScheduleLogService.cs delete mode 100644 Oqtane.Client/Services/ScheduleService.cs create mode 100644 Oqtane.Server/Controllers/JobController.cs rename Oqtane.Server/Controllers/{ScheduleController.cs => JobLogController.cs} (58%) delete mode 100644 Oqtane.Server/Controllers/ScheduleLogController.cs delete mode 100644 Oqtane.Server/Infrastructure/HostedServiceBase.cs rename Oqtane.Server/Infrastructure/{ => Interfaces}/IInstallationManager.cs (100%) rename Oqtane.Server/Infrastructure/{ => Interfaces}/ILogManager.cs (100%) create mode 100644 Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs create mode 100644 Oqtane.Server/Infrastructure/Jobs/SampleJob.cs delete mode 100644 Oqtane.Server/Infrastructure/TestJob.cs create mode 100644 Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs create mode 100644 Oqtane.Server/Repository/Interfaces/IJobRepository.cs delete mode 100644 Oqtane.Server/Repository/Interfaces/IScheduleLogRepository.cs delete mode 100644 Oqtane.Server/Repository/Interfaces/IScheduleRepository.cs create mode 100644 Oqtane.Server/Repository/JobLogRepository.cs create mode 100644 Oqtane.Server/Repository/JobRepository.cs delete mode 100644 Oqtane.Server/Repository/ScheduleLogRepository.cs delete mode 100644 Oqtane.Server/Repository/ScheduleRepository.cs rename Oqtane.Shared/Models/{Schedule.cs => Job.cs} (69%) rename Oqtane.Shared/Models/{ScheduleLog.cs => JobLog.cs} (62%) diff --git a/Oqtane.Client/Modules/Admin/Jobs/Add.razor b/Oqtane.Client/Modules/Admin/Jobs/Add.razor new file mode 100644 index 00000000..29c77c6a --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Jobs/Add.razor @@ -0,0 +1,137 @@ +@namespace Oqtane.Modules.Admin.Jobs +@inherits ModuleBase +@inject NavigationManager NavigationManager +@inject IJobService JobService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + + +
+ + + +
+ + + +
+ + + +
+ +Cancel + +@code { + public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } } + + string name = ""; + string jobtype = ""; + string isenabled = "True"; + string interval = ""; + string frequency = ""; + string startdate = ""; + string enddate = ""; + string retentionhistory = "10"; + + private async Task SaveJob() + { + if (name != "" && !string.IsNullOrEmpty(jobtype) && frequency != "" && interval != "" && retentionhistory != "") + { + Job job = new Job(); + job.Name = name; + job.JobType = jobtype; + job.IsEnabled = Boolean.Parse(isenabled); + job.Frequency = frequency; + job.Interval = int.Parse(interval); + if (startdate == "") + { + job.StartDate = null; + } + else + { + job.StartDate = DateTime.Parse(startdate); + } + if (enddate == "") + { + job.EndDate = null; + } + else + { + job.EndDate = DateTime.Parse(enddate); + } + job.RetentionHistory = int.Parse(retentionhistory); + job.IsStarted = false; + job.IsExecuting = false; + job.NextExecution = null; + + try + { + job = await JobService.AddJobAsync(job); + await logger.LogInformation("Job Added {Job}", job); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Adding Job {Job} {Error}", job, ex.Message); + AddModuleMessage("Error Adding Job", MessageType.Error); + } + } + else + { + AddModuleMessage("You Must Provide The Job Name, Type, Frequency, and Retention", MessageType.Warning); + } + } + +} diff --git a/Oqtane.Client/Modules/Admin/Jobs/Edit.razor b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor new file mode 100644 index 00000000..a2651e87 --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Jobs/Edit.razor @@ -0,0 +1,160 @@ +@namespace Oqtane.Modules.Admin.Jobs +@inherits ModuleBase +@inject NavigationManager NavigationManager +@inject IJobService JobService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + + +
+ + + +
+ + + +
+ + + +
+ +Cancel + +@code { + public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } } + + int jobid; + string name = ""; + string jobtype = ""; + string isenabled = "True"; + string interval = ""; + string frequency = ""; + string startdate = ""; + string enddate = ""; + string retentionhistory = ""; + + protected override async Task OnInitializedAsync() + { + try + { + jobid = Int32.Parse(PageState.QueryString["id"]); + Job job = await JobService.GetJobAsync(jobid); + if (job != null) + { + name = job.Name; + jobtype = job.JobType; + isenabled = job.IsEnabled.ToString(); + interval = job.Interval.ToString(); + frequency = job.Frequency; + startdate = (job.StartDate != null) ? job.StartDate.ToString() : ""; + enddate = (job.EndDate != null) ? job.EndDate.ToString() : ""; + retentionhistory = job.RetentionHistory.ToString(); + } + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Loading Job {JobId} {Error}", jobid, ex.Message); + AddModuleMessage("Error Loading Job", MessageType.Error); + } + } + + private async Task SaveJob() + { + if (name != "" && !string.IsNullOrEmpty(jobtype) && frequency != "" && interval != "" && retentionhistory != "") + { + Job job = await JobService.GetJobAsync(jobid); + job.Name = name; + job.JobType = jobtype; + job.IsEnabled = Boolean.Parse(isenabled); + job.Frequency = frequency; + job.Interval = int.Parse(interval); + if (startdate == "") + { + job.StartDate = null; + } + else + { + job.StartDate = DateTime.Parse(startdate); + } + if (enddate == "") + { + job.EndDate = null; + } + else + { + job.EndDate = DateTime.Parse(enddate); + } + job.RetentionHistory = int.Parse(retentionhistory); + + try + { + job = await JobService.UpdateJobAsync(job); + await logger.LogInformation("Job Updated {Job}", job); + NavigationManager.NavigateTo(NavigateUrl()); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Udate Job {Job} {Error}", job, ex.Message); + AddModuleMessage("Error Updating Job", MessageType.Error); + } + } + else + { + AddModuleMessage("You Must Provide The Job Name, Type, Frequency, and Retention", MessageType.Warning); + } + } + +} diff --git a/Oqtane.Client/Modules/Admin/Jobs/Index.razor b/Oqtane.Client/Modules/Admin/Jobs/Index.razor new file mode 100644 index 00000000..e908225d --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Jobs/Index.razor @@ -0,0 +1,137 @@ +@namespace Oqtane.Modules.Admin.Jobs +@inherits ModuleBase +@inject IJobService JobService + +@if (Jobs == null) +{ +

Loading...

+} +else +{ + + + +

+ + +
+   +   +   + Name + Status + Frequency + Next Execution +   +
+ + + + + @context.Name + @DisplayStatus(context.IsEnabled, context.IsExecuting) + @DisplayFrequency(context.Interval, context.Frequency) + @context.NextExecution + + @if (context.IsStarted) + { + + } + else + { + + } + + +
+} + +@code { + public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } } + + List Jobs; + + protected override async Task OnParametersSetAsync() + { + Jobs = await JobService.GetJobsAsync(); + } + + private string DisplayStatus(bool IsEnabled, bool IsExecuting) + { + string status = ""; + if (!IsEnabled) + { + status = "Disabled"; + } + else + { + if (IsExecuting) + { + status = "Executing"; + } + else + { + status = "Idle"; + } + } + + return status; + } + + + private string DisplayFrequency(int Interval, string Frequency) + { + string frequency = "Every " + Interval.ToString() + " "; + switch (Frequency) + { + case "m": + frequency += "Minute"; + break; + case "H": + frequency += "Hour"; + break; + case "d": + frequency += "Day"; + break; + case "M": + frequency += "Month"; + break; + } + if (Interval > 1) + { + frequency += "s"; + } + return frequency; + } + + private async Task DeleteJob(Job Job) + { + try + { + await JobService.DeleteJobAsync(Job.JobId); + await logger.LogInformation("Job Deleted {Job}", Job); + StateHasChanged(); + } + catch (Exception ex) + { + await logger.LogError(ex, "Error Deleting Job {Job} {Error}", Job, ex.Message); + AddModuleMessage("Error Deleting Job", MessageType.Error); + } + } + + private async Task StartJob(int JobId) + { + await JobService.StartJobAsync(JobId); + } + + private async Task StopJob(int JobId) + { + await JobService.StopJobAsync(JobId); + } + + private async Task Refresh() + { + Jobs = await JobService.GetJobsAsync(); + StateHasChanged(); + } +} \ No newline at end of file diff --git a/Oqtane.Client/Modules/Admin/Jobs/Log.razor b/Oqtane.Client/Modules/Admin/Jobs/Log.razor new file mode 100644 index 00000000..bc48f07c --- /dev/null +++ b/Oqtane.Client/Modules/Admin/Jobs/Log.razor @@ -0,0 +1,64 @@ +@namespace Oqtane.Modules.Admin.Jobs +@inherits ModuleBase +@inject IJobLogService JobLogService + +@if (JobLogs == null) +{ +

Loading...

+} +else +{ + +
+ Name + Status + Started + Finished + Notes +
+ + @context.Job.Name + @DisplayStatus(context.Job.IsExecuting, context.Succeeded) + @context.StartDate + @context.FinishDate + + +
+} + +@code { + public override SecurityAccessLevel SecurityAccessLevel { get { return SecurityAccessLevel.Host; } } + + List JobLogs; + + protected override async Task OnParametersSetAsync() + { + JobLogs = await JobLogService.GetJobLogsAsync(); + if (PageState.QueryString.ContainsKey("id")) + { + JobLogs = JobLogs.Where(item => item.JobId == Int32.Parse(PageState.QueryString["id"])).ToList(); + } + JobLogs = JobLogs.OrderByDescending(item => item.JobLogId).ToList(); + } + + private string DisplayStatus(bool IsExecuting, bool? Succeeded) + { + string status = ""; + if (IsExecuting) + { + status = "Executing"; + } + else + { + if (Succeeded.Value) + { + status = "Succeeded"; + } + else + { + status = "Failed"; + } + } + return status; + } +} \ No newline at end of file diff --git a/Oqtane.Client/Modules/Admin/Roles/Add.razor b/Oqtane.Client/Modules/Admin/Roles/Add.razor index f4e26aa8..c2bff1da 100644 --- a/Oqtane.Client/Modules/Admin/Roles/Add.razor +++ b/Oqtane.Client/Modules/Admin/Roles/Add.razor @@ -56,14 +56,15 @@ private async Task SaveRole() { + Role role = new Role(); + role.SiteId = PageState.Page.SiteId; + role.Name = name; + role.Description = description; + role.IsAutoAssigned = (isautoassigned == null ? false : Boolean.Parse(isautoassigned)); + role.IsSystem = (issystem == null ? false : Boolean.Parse(issystem)); + try { - Role role = new Role(); - role.SiteId = PageState.Page.SiteId; - role.Name = name; - role.Description = description; - role.IsAutoAssigned = (isautoassigned == null ? false : Boolean.Parse(isautoassigned)); - role.IsSystem = (issystem == null ? false : Boolean.Parse(issystem)); role = await RoleService.AddRoleAsync(role); await logger.LogInformation("Role Added {Role}", role); @@ -71,8 +72,8 @@ } catch (Exception ex) { - await logger.LogError(ex, "Error Adding Role", ex.Message); - AddModuleMessage(ex.Message, MessageType.Error); + await logger.LogError(ex, "Error Adding Role {Role} {Error}", role, ex.Message); + AddModuleMessage("Error Adding Role", MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Admin/Roles/Edit.razor b/Oqtane.Client/Modules/Admin/Roles/Edit.razor index 3ab20d91..2557bd44 100644 --- a/Oqtane.Client/Modules/Admin/Roles/Edit.razor +++ b/Oqtane.Client/Modules/Admin/Roles/Edit.razor @@ -78,20 +78,21 @@ private async Task SaveRole() { + Role role = await RoleService.GetRoleAsync(roleid); + role.Name = name; + role.Description = description; + role.IsAutoAssigned = (isautoassigned == null ? false : Boolean.Parse(isautoassigned)); + role.IsSystem = (issystem == null ? false : Boolean.Parse(issystem)); + try { - Role role = await RoleService.GetRoleAsync(roleid); - role.Name = name; - role.Description = description; - role.IsAutoAssigned = (isautoassigned == null ? false : Boolean.Parse(isautoassigned)); - role.IsSystem = (issystem == null ? false : Boolean.Parse(issystem)); role = await RoleService.UpdateRoleAsync(role); await logger.LogInformation("Role Saved {Role}", role); NavigationManager.NavigateTo(NavigateUrl()); } catch (Exception ex) { - await logger.LogError(ex, "Error Saving Role {RoleId} {Error}", roleid, ex.Message); + await logger.LogError(ex, "Error Saving Role {Role} {Error}", role, ex.Message); AddModuleMessage("Error Saving Role", MessageType.Error); } } diff --git a/Oqtane.Client/Modules/Controls/ActionDialog.razor b/Oqtane.Client/Modules/Controls/ActionDialog.razor index 41dd4b21..96aac6c1 100644 --- a/Oqtane.Client/Modules/Controls/ActionDialog.razor +++ b/Oqtane.Client/Modules/Controls/ActionDialog.razor @@ -15,7 +15,10 @@

@Message

@@ -36,29 +39,25 @@ public string Message { get; set; } // required [Parameter] - public string Action { get; set; } // defaults to Ok if not specified + public string Text { get; set; } // optional - defaults to Action if not specified + + [Parameter] + public string Action { get; set; } // optional [Parameter] public SecurityAccessLevel? Security { get; set; } // optional - can be used to explicitly specify SecurityAccessLevel - [Parameter] - public string Text { get; set; } // optional - defaults to Action if not specified - [Parameter] public string Class { get; set; } // optional [Parameter] - public Action OnClick { get; set; } // required - executes a method in the calling component + public Action OnClick { get; set; } // required if an Action is specified - executes a method in the calling component bool visible = false; bool authorized = false; protected override void OnParametersSet() { - if (string.IsNullOrEmpty(Action)) - { - Action = "Ok"; - } if (string.IsNullOrEmpty(Text)) { Text = Action; diff --git a/Oqtane.Client/Services/Interfaces/IJobLogService.cs b/Oqtane.Client/Services/Interfaces/IJobLogService.cs new file mode 100644 index 00000000..2afc2851 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/IJobLogService.cs @@ -0,0 +1,19 @@ +using Oqtane.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + public interface IJobLogService + { + Task> GetJobLogsAsync(); + + Task GetJobLogAsync(int JobLogId); + + Task AddJobLogAsync(JobLog JobLog); + + Task UpdateJobLogAsync(JobLog JobLog); + + Task DeleteJobLogAsync(int JobLogId); + } +} diff --git a/Oqtane.Client/Services/Interfaces/IJobService.cs b/Oqtane.Client/Services/Interfaces/IJobService.cs new file mode 100644 index 00000000..15b508f9 --- /dev/null +++ b/Oqtane.Client/Services/Interfaces/IJobService.cs @@ -0,0 +1,23 @@ +using Oqtane.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Oqtane.Services +{ + public interface IJobService + { + Task> GetJobsAsync(); + + Task GetJobAsync(int JobId); + + Task AddJobAsync(Job Job); + + Task UpdateJobAsync(Job Job); + + Task DeleteJobAsync(int JobId); + + Task StartJobAsync(int JobId); + + Task StopJobAsync(int JobId); + } +} diff --git a/Oqtane.Client/Services/Interfaces/IScheduleLogService.cs b/Oqtane.Client/Services/Interfaces/IScheduleLogService.cs deleted file mode 100644 index d3f8af27..00000000 --- a/Oqtane.Client/Services/Interfaces/IScheduleLogService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Oqtane.Models; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Oqtane.Services -{ - public interface IScheduleLogService - { - Task> GetScheduleLogsAsync(); - - Task GetScheduleLogAsync(int ScheduleLogId); - - Task AddScheduleLogAsync(ScheduleLog ScheduleLog); - - Task UpdateScheduleLogAsync(ScheduleLog ScheduleLog); - - Task DeleteScheduleLogAsync(int ScheduleLogId); - } -} diff --git a/Oqtane.Client/Services/Interfaces/IScheduleService.cs b/Oqtane.Client/Services/Interfaces/IScheduleService.cs deleted file mode 100644 index f9d7a00a..00000000 --- a/Oqtane.Client/Services/Interfaces/IScheduleService.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Oqtane.Models; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Oqtane.Services -{ - public interface IScheduleService - { - Task> GetSchedulesAsync(); - - Task GetScheduleAsync(int ScheduleId); - - Task AddScheduleAsync(Schedule Schedule); - - Task UpdateScheduleAsync(Schedule Schedule); - - Task DeleteScheduleAsync(int ScheduleId); - } -} diff --git a/Oqtane.Client/Services/JobLogService.cs b/Oqtane.Client/Services/JobLogService.cs new file mode 100644 index 00000000..bd1d0c44 --- /dev/null +++ b/Oqtane.Client/Services/JobLogService.cs @@ -0,0 +1,54 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Linq; +using Microsoft.AspNetCore.Components; +using System.Collections.Generic; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + public class JobLogService : ServiceBase, IJobLogService + { + private readonly HttpClient http; + private readonly SiteState sitestate; + private readonly NavigationManager NavigationManager; + + public JobLogService(HttpClient http, SiteState sitestate, NavigationManager NavigationManager) + { + this.http = http; + this.sitestate = sitestate; + this.NavigationManager = NavigationManager; + } + + private string apiurl + { + get { return CreateApiUrl(sitestate.Alias, NavigationManager.Uri, "JobLog"); } + } + + public async Task> GetJobLogsAsync() + { + List Joblogs = await http.GetJsonAsync>(apiurl); + return Joblogs.OrderBy(item => item.StartDate).ToList(); + } + + public async Task GetJobLogAsync(int JobLogId) + { + return await http.GetJsonAsync(apiurl + "/" + JobLogId.ToString()); + } + + public async Task AddJobLogAsync(JobLog Joblog) + { + return await http.PostJsonAsync(apiurl, Joblog); + } + + public async Task UpdateJobLogAsync(JobLog Joblog) + { + return await http.PutJsonAsync(apiurl + "/" + Joblog.JobLogId.ToString(), Joblog); + } + public async Task DeleteJobLogAsync(int JobLogId) + { + await http.DeleteAsync(apiurl + "/" + JobLogId.ToString()); + } + } +} diff --git a/Oqtane.Client/Services/JobService.cs b/Oqtane.Client/Services/JobService.cs new file mode 100644 index 00000000..8481b0b1 --- /dev/null +++ b/Oqtane.Client/Services/JobService.cs @@ -0,0 +1,64 @@ +using Oqtane.Models; +using System.Threading.Tasks; +using System.Net.Http; +using System.Linq; +using Microsoft.AspNetCore.Components; +using System.Collections.Generic; +using Oqtane.Shared; + +namespace Oqtane.Services +{ + public class JobService : ServiceBase, IJobService + { + private readonly HttpClient http; + private readonly SiteState sitestate; + private readonly NavigationManager NavigationManager; + + public JobService(HttpClient http, SiteState sitestate, NavigationManager NavigationManager) + { + this.http = http; + this.sitestate = sitestate; + this.NavigationManager = NavigationManager; + } + + private string apiurl + { + get { return CreateApiUrl(sitestate.Alias, NavigationManager.Uri, "Job"); } + } + + public async Task> GetJobsAsync() + { + List Jobs = await http.GetJsonAsync>(apiurl); + return Jobs.OrderBy(item => item.Name).ToList(); + } + + public async Task GetJobAsync(int JobId) + { + return await http.GetJsonAsync(apiurl + "/" + JobId.ToString()); + } + + public async Task AddJobAsync(Job Job) + { + return await http.PostJsonAsync(apiurl, Job); + } + + public async Task UpdateJobAsync(Job Job) + { + return await http.PutJsonAsync(apiurl + "/" + Job.JobId.ToString(), Job); + } + public async Task DeleteJobAsync(int JobId) + { + await http.DeleteAsync(apiurl + "/" + JobId.ToString()); + } + + public async Task StartJobAsync(int JobId) + { + await http.GetAsync(apiurl + "/start/" + JobId.ToString()); + } + + public async Task StopJobAsync(int JobId) + { + await http.GetAsync(apiurl + "/stop/" + JobId.ToString()); + } + } +} diff --git a/Oqtane.Client/Services/ScheduleLogService.cs b/Oqtane.Client/Services/ScheduleLogService.cs deleted file mode 100644 index 68b6b154..00000000 --- a/Oqtane.Client/Services/ScheduleLogService.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Oqtane.Models; -using System.Threading.Tasks; -using System.Net.Http; -using System.Linq; -using Microsoft.AspNetCore.Components; -using System.Collections.Generic; -using Oqtane.Shared; - -namespace Oqtane.Services -{ - public class ScheduleLogService : ServiceBase, IScheduleLogService - { - private readonly HttpClient http; - private readonly SiteState sitestate; - private readonly NavigationManager NavigationManager; - - public ScheduleLogService(HttpClient http, SiteState sitestate, NavigationManager NavigationManager) - { - this.http = http; - this.sitestate = sitestate; - this.NavigationManager = NavigationManager; - } - - private string apiurl - { - get { return CreateApiUrl(sitestate.Alias, NavigationManager.Uri, "ScheduleLog"); } - } - - public async Task> GetScheduleLogsAsync() - { - List schedulelogs = await http.GetJsonAsync>(apiurl); - return schedulelogs.OrderBy(item => item.StartDate).ToList(); - } - - public async Task GetScheduleLogAsync(int ScheduleLogId) - { - return await http.GetJsonAsync(apiurl + "/" + ScheduleLogId.ToString()); - } - - public async Task AddScheduleLogAsync(ScheduleLog schedulelog) - { - return await http.PostJsonAsync(apiurl, schedulelog); - } - - public async Task UpdateScheduleLogAsync(ScheduleLog schedulelog) - { - return await http.PutJsonAsync(apiurl + "/" + schedulelog.ScheduleLogId.ToString(), schedulelog); - } - public async Task DeleteScheduleLogAsync(int ScheduleLogId) - { - await http.DeleteAsync(apiurl + "/" + ScheduleLogId.ToString()); - } - } -} diff --git a/Oqtane.Client/Services/ScheduleService.cs b/Oqtane.Client/Services/ScheduleService.cs deleted file mode 100644 index 6375ba9b..00000000 --- a/Oqtane.Client/Services/ScheduleService.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Oqtane.Models; -using System.Threading.Tasks; -using System.Net.Http; -using System.Linq; -using Microsoft.AspNetCore.Components; -using System.Collections.Generic; -using Oqtane.Shared; - -namespace Oqtane.Services -{ - public class ScheduleService : ServiceBase, IScheduleService - { - private readonly HttpClient http; - private readonly SiteState sitestate; - private readonly NavigationManager NavigationManager; - - public ScheduleService(HttpClient http, SiteState sitestate, NavigationManager NavigationManager) - { - this.http = http; - this.sitestate = sitestate; - this.NavigationManager = NavigationManager; - } - - private string apiurl - { - get { return CreateApiUrl(sitestate.Alias, NavigationManager.Uri, "Schedule"); } - } - - public async Task> GetSchedulesAsync() - { - List schedules = await http.GetJsonAsync>(apiurl); - return schedules.OrderBy(item => item.Name).ToList(); - } - - public async Task GetScheduleAsync(int ScheduleId) - { - return await http.GetJsonAsync(apiurl + "/" + ScheduleId.ToString()); - } - - public async Task AddScheduleAsync(Schedule schedule) - { - return await http.PostJsonAsync(apiurl, schedule); - } - - public async Task UpdateScheduleAsync(Schedule schedule) - { - return await http.PutJsonAsync(apiurl + "/" + schedule.ScheduleId.ToString(), schedule); - } - public async Task DeleteScheduleAsync(int ScheduleId) - { - await http.DeleteAsync(apiurl + "/" + ScheduleId.ToString()); - } - } -} diff --git a/Oqtane.Client/Startup.cs b/Oqtane.Client/Startup.cs index b201a879..e4afc001 100644 --- a/Oqtane.Client/Startup.cs +++ b/Oqtane.Client/Startup.cs @@ -54,8 +54,8 @@ namespace Oqtane.Client services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); // dynamically register module contexts and repository services Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); diff --git a/Oqtane.Server/Controllers/JobController.cs b/Oqtane.Server/Controllers/JobController.cs new file mode 100644 index 00000000..4dade1d9 --- /dev/null +++ b/Oqtane.Server/Controllers/JobController.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Authorization; +using Oqtane.Repository; +using Oqtane.Models; +using Oqtane.Shared; +using Oqtane.Infrastructure; +using System; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.DependencyInjection; + +namespace Oqtane.Controllers +{ + [Route("{site}/api/[controller]")] + public class JobController : Controller + { + private readonly IJobRepository Jobs; + private readonly ILogManager logger; + private readonly IServiceProvider ServiceProvider; + + public JobController(IJobRepository Jobs, ILogManager logger, IServiceProvider ServiceProvider) + { + this.Jobs = Jobs; + this.logger = logger; + this.ServiceProvider = ServiceProvider; + } + + // GET: api/ + [HttpGet] + public IEnumerable Get() + { + return Jobs.GetJobs(); + } + + // GET api//5 + [HttpGet("{id}")] + public Job Get(int id) + { + return Jobs.GetJob(id); + } + + // POST api/ + [HttpPost] + [Authorize(Roles = Constants.HostRole)] + public Job Post([FromBody] Job Job) + { + if (ModelState.IsValid) + { + Job = Jobs.AddJob(Job); + logger.Log(LogLevel.Information, this, LogFunction.Create, "Job Added {Job}", Job); + } + return Job; + } + + // PUT api//5 + [HttpPut("{id}")] + [Authorize(Roles = Constants.HostRole)] + public Job Put(int id, [FromBody] Job Job) + { + if (ModelState.IsValid) + { + Job = Jobs.UpdateJob(Job); + logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Updated {Job}", Job); + } + return Job; + } + + // DELETE api//5 + [HttpDelete("{id}")] + [Authorize(Roles = Constants.HostRole)] + public void Delete(int id) + { + Jobs.DeleteJob(id); + logger.Log(LogLevel.Information, this, LogFunction.Delete, "Job Deleted {JobId}", id); + } + + // GET api//start + [HttpGet("start/{id}")] + [Authorize(Roles = Constants.HostRole)] + public void Start(int id) + { + Job job = Jobs.GetJob(id); + Type jobtype = Type.GetType(job.JobType); + if (jobtype != null) + { + var jobobject = ActivatorUtilities.CreateInstance(ServiceProvider, jobtype); + ((IHostedService)jobobject).StartAsync(new System.Threading.CancellationToken()); + } + } + + // GET api//stop + [HttpGet("stop/{id}")] + [Authorize(Roles = Constants.HostRole)] + public void Stop(int id) + { + Job job = Jobs.GetJob(id); + Type jobtype = Type.GetType(job.JobType); + if (jobtype != null) + { + var jobobject = ActivatorUtilities.CreateInstance(ServiceProvider, jobtype); + ((IHostedService)jobobject).StopAsync(new System.Threading.CancellationToken()); + } + } + } +} diff --git a/Oqtane.Server/Controllers/ScheduleController.cs b/Oqtane.Server/Controllers/JobLogController.cs similarity index 58% rename from Oqtane.Server/Controllers/ScheduleController.cs rename to Oqtane.Server/Controllers/JobLogController.cs index 4975266b..f514b519 100644 --- a/Oqtane.Server/Controllers/ScheduleController.cs +++ b/Oqtane.Server/Controllers/JobLogController.cs @@ -9,55 +9,57 @@ using Oqtane.Infrastructure; namespace Oqtane.Controllers { [Route("{site}/api/[controller]")] - public class ScheduleController : Controller + public class JobLogController : Controller { - private readonly IScheduleRepository Schedules; + private readonly IJobLogRepository JobLogs; private readonly ILogManager logger; - public ScheduleController(IScheduleRepository Schedules, ILogManager logger) + public JobLogController(IJobLogRepository JobLogs, ILogManager logger) { - this.Schedules = Schedules; + this.JobLogs = JobLogs; this.logger = logger; } // GET: api/ [HttpGet] - public IEnumerable Get() + [Authorize(Roles = Constants.HostRole)] + public IEnumerable Get() { - return Schedules.GetSchedules(); + return JobLogs.GetJobLogs(); } // GET api//5 [HttpGet("{id}")] - public Schedule Get(int id) + [Authorize(Roles = Constants.HostRole)] + public JobLog Get(int id) { - return Schedules.GetSchedule(id); + return JobLogs.GetJobLog(id); } // POST api/ [HttpPost] [Authorize(Roles = Constants.HostRole)] - public Schedule Post([FromBody] Schedule Schedule) + public JobLog Post([FromBody] JobLog JobLog) { if (ModelState.IsValid) { - Schedule = Schedules.AddSchedule(Schedule); - logger.Log(LogLevel.Information, this, LogFunction.Create, "Schedule Added {Schedule}", Schedule); + JobLog = JobLogs.AddJobLog(JobLog); + logger.Log(LogLevel.Information, this, LogFunction.Create, "Job Log Added {JobLog}", JobLog); } - return Schedule; + return JobLog; } // PUT api//5 [HttpPut("{id}")] [Authorize(Roles = Constants.HostRole)] - public Schedule Put(int id, [FromBody] Schedule Schedule) + public JobLog Put(int id, [FromBody] JobLog JobLog) { if (ModelState.IsValid) { - Schedule = Schedules.UpdateSchedule(Schedule); - logger.Log(LogLevel.Information, this, LogFunction.Update, "Schedule Updated {Schedule}", Schedule); + JobLog = JobLogs.UpdateJobLog(JobLog); + logger.Log(LogLevel.Information, this, LogFunction.Update, "Job Log Updated {JobLog}", JobLog); } - return Schedule; + return JobLog; } // DELETE api//5 @@ -65,8 +67,8 @@ namespace Oqtane.Controllers [Authorize(Roles = Constants.HostRole)] public void Delete(int id) { - Schedules.DeleteSchedule(id); - logger.Log(LogLevel.Information, this, LogFunction.Delete, "Schedule Deleted {ScheduleId}", id); + JobLogs.DeleteJobLog(id); + logger.Log(LogLevel.Information, this, LogFunction.Delete, "Job Log Deleted {JobLogId}", id); } } } diff --git a/Oqtane.Server/Controllers/ScheduleLogController.cs b/Oqtane.Server/Controllers/ScheduleLogController.cs deleted file mode 100644 index cdbea209..00000000 --- a/Oqtane.Server/Controllers/ScheduleLogController.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Authorization; -using Oqtane.Repository; -using Oqtane.Models; -using Oqtane.Shared; -using Oqtane.Infrastructure; - -namespace Oqtane.Controllers -{ - [Route("{site}/api/[controller]")] - public class ScheduleLogController : Controller - { - private readonly IScheduleLogRepository ScheduleLogs; - private readonly ILogManager logger; - - public ScheduleLogController(IScheduleLogRepository ScheduleLogs, ILogManager logger) - { - this.ScheduleLogs = ScheduleLogs; - this.logger = logger; - } - - // GET: api/ - [HttpGet] - [Authorize(Roles = Constants.HostRole)] - public IEnumerable Get() - { - return ScheduleLogs.GetScheduleLogs(); - } - - // GET api//5 - [HttpGet("{id}")] - [Authorize(Roles = Constants.HostRole)] - public ScheduleLog Get(int id) - { - return ScheduleLogs.GetScheduleLog(id); - } - - // POST api/ - [HttpPost] - [Authorize(Roles = Constants.HostRole)] - public ScheduleLog Post([FromBody] ScheduleLog ScheduleLog) - { - if (ModelState.IsValid) - { - ScheduleLog = ScheduleLogs.AddScheduleLog(ScheduleLog); - logger.Log(LogLevel.Information, this, LogFunction.Create, "Schedule Log Added {ScheduleLog}", ScheduleLog); - } - return ScheduleLog; - } - - // PUT api//5 - [HttpPut("{id}")] - [Authorize(Roles = Constants.HostRole)] - public ScheduleLog Put(int id, [FromBody] ScheduleLog ScheduleLog) - { - if (ModelState.IsValid) - { - ScheduleLog = ScheduleLogs.UpdateScheduleLog(ScheduleLog); - logger.Log(LogLevel.Information, this, LogFunction.Update, "Schedule Log Updated {ScheduleLog}", ScheduleLog); - } - return ScheduleLog; - } - - // DELETE api//5 - [HttpDelete("{id}")] - [Authorize(Roles = Constants.HostRole)] - public void Delete(int id) - { - ScheduleLogs.DeleteScheduleLog(id); - logger.Log(LogLevel.Information, this, LogFunction.Delete, "Schedule Log Deleted {ScheduleLogId}", id); - } - } -} diff --git a/Oqtane.Server/Infrastructure/HostedServiceBase.cs b/Oqtane.Server/Infrastructure/HostedServiceBase.cs deleted file mode 100644 index 09b91572..00000000 --- a/Oqtane.Server/Infrastructure/HostedServiceBase.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Oqtane.Models; -using Oqtane.Repository; -using Oqtane.Shared; - -namespace Oqtane.Infrastructure -{ - public abstract class HostedServiceBase : IHostedService, IDisposable - { - private Task ExecutingTask; - private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); - private readonly IServiceScopeFactory ServiceScopeFactory; - - public HostedServiceBase(IServiceScopeFactory ServiceScopeFactory) - { - this.ServiceScopeFactory = ServiceScopeFactory; - } - - // abstract method must be overridden by job - public abstract void ExecuteJob(IServiceProvider provider); - - - protected async Task ExecuteAsync(CancellationToken stoppingToken) - { - try - { - while (!stoppingToken.IsCancellationRequested) - { - // allows consumption of scoped services - using (var scope = ServiceScopeFactory.CreateScope()) - { - string JobType = Utilities.GetFullTypeName(this.GetType().AssemblyQualifiedName); - IScheduleRepository ScheduleRepository = scope.ServiceProvider.GetRequiredService(); - List schedules = ScheduleRepository.GetSchedules().ToList(); - Schedule schedule = schedules.Where(item => item.JobType == JobType).FirstOrDefault(); - if (schedule != null && schedule.IsActive) - { - ExecuteJob(scope.ServiceProvider); - } - } - - await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); - } - } - catch - { - // can occur during the initial installation as there is no DBContext - } - - } - - public Task StartAsync(CancellationToken cancellationToken) - { - ExecutingTask = ExecuteAsync(CancellationTokenSource.Token); - - if (ExecutingTask.IsCompleted) - { - return ExecutingTask; - } - - return Task.CompletedTask; - } - - public async Task StopAsync(CancellationToken CancellationToken) - { - if (ExecutingTask == null) - { - return; - } - - try - { - CancellationTokenSource.Cancel(); - } - finally - { - await Task.WhenAny(ExecutingTask, Task.Delay(Timeout.Infinite, CancellationToken)); - } - } - - public void Dispose() - { - CancellationTokenSource.Cancel(); - } - } -} diff --git a/Oqtane.Server/Infrastructure/IInstallationManager.cs b/Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs similarity index 100% rename from Oqtane.Server/Infrastructure/IInstallationManager.cs rename to Oqtane.Server/Infrastructure/Interfaces/IInstallationManager.cs diff --git a/Oqtane.Server/Infrastructure/ILogManager.cs b/Oqtane.Server/Infrastructure/Interfaces/ILogManager.cs similarity index 100% rename from Oqtane.Server/Infrastructure/ILogManager.cs rename to Oqtane.Server/Infrastructure/Interfaces/ILogManager.cs diff --git a/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs new file mode 100644 index 00000000..7b81a102 --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/HostedServiceBase.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public abstract class HostedServiceBase : IHostedService, IDisposable + { + private Task ExecutingTask; + private readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource(); + private readonly IServiceScopeFactory ServiceScopeFactory; + + public HostedServiceBase(IServiceScopeFactory ServiceScopeFactory) + { + this.ServiceScopeFactory = ServiceScopeFactory; + } + + // abstract method must be overridden + public abstract string ExecuteJob(IServiceProvider provider); + + protected async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + while (!stoppingToken.IsCancellationRequested) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + // get name of job + string JobType = Utilities.GetFullTypeName(this.GetType().AssemblyQualifiedName); + + // load jobs and find current job + IJobRepository Jobs = scope.ServiceProvider.GetRequiredService(); + Job Job = Jobs.GetJobs().Where(item => item.JobType == JobType).FirstOrDefault(); + if (Job != null && Job.IsEnabled && !Job.IsExecuting) + { + // set next execution date + if (Job.NextExecution == null) + { + if (Job.StartDate != null) + { + Job.NextExecution = Job.StartDate; + } + else + { + Job.NextExecution = DateTime.Now; + } + } + + // determine if the job should be run + if (Job.NextExecution <= DateTime.Now && (Job.EndDate == null || Job.EndDate >= DateTime.Now)) + { + IJobLogRepository JobLogs = scope.ServiceProvider.GetRequiredService(); + + // create a job log entry + JobLog log = new JobLog(); + log.JobId = Job.JobId; + log.StartDate = DateTime.Now; + log.FinishDate = null; + log.Succeeded = false; + log.Notes = ""; + log = JobLogs.AddJobLog(log); + + // update the job to indicate it is running + Job.IsExecuting = true; + Jobs.UpdateJob(Job); + + // execute the job + try + { + log.Notes = ExecuteJob(scope.ServiceProvider); + log.Succeeded = true; + } + catch (Exception ex) + { + log.Notes = ex.Message; + log.Succeeded = false; + } + + // update the job log + log.FinishDate = DateTime.Now; + JobLogs.UpdateJobLog(log); + + // update the job + Job.NextExecution = CalculateNextExecution(Job.NextExecution.Value, Job.Frequency, Job.Interval); + Job.IsExecuting = false; + Jobs.UpdateJob(Job); + + // trim the job log + List logs = JobLogs.GetJobLogs().Where(item => item.JobId == Job.JobId) + .OrderByDescending(item => item.JobLogId).ToList(); + for (int i = logs.Count; i > Job.RetentionHistory; i--) + { + JobLogs.DeleteJobLog(logs[i - 1].JobLogId); + } + } + } + } + + // wait 1 minute + await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); + } + } + catch + { + // can occur during the initial installation as there is no DBContext + } + + } + + private DateTime CalculateNextExecution(DateTime NextExecution, string Frequency, int Interval) + { + switch (Frequency) + { + case "m": // minutes + NextExecution = NextExecution.AddMinutes(Interval); + break; + case "H": // hours + NextExecution = NextExecution.AddHours(Interval); + break; + case "d": // days + NextExecution = NextExecution.AddDays(Interval); + break; + case "M": // months + NextExecution = NextExecution.AddMonths(Interval); + break; + } + if (NextExecution < DateTime.Now) + { + NextExecution = DateTime.Now; + } + return NextExecution; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + try + { + // set IsExecuting to false in case this job was forcefully terminated previously + using (var scope = ServiceScopeFactory.CreateScope()) + { + string JobType = Utilities.GetFullTypeName(this.GetType().AssemblyQualifiedName); + IJobRepository Jobs = scope.ServiceProvider.GetRequiredService(); + Job Job = Jobs.GetJobs().Where(item => item.JobType == JobType).FirstOrDefault(); + if (Job != null) + { + Job.IsStarted = true; + Job.IsExecuting = false; + Jobs.UpdateJob(Job); + } + } + } + catch + { + // can occur during the initial installation as there is no DBContext + } + + ExecutingTask = ExecuteAsync(CancellationTokenSource.Token); + + if (ExecutingTask.IsCompleted) + { + return ExecutingTask; + } + + return Task.CompletedTask; + } + + public async Task StopAsync(CancellationToken CancellationToken) + { + if (ExecutingTask == null) + { + return; + } + + try + { + CancellationTokenSource.Cancel(); + } + finally + { + await Task.WhenAny(ExecutingTask, Task.Delay(Timeout.Infinite, CancellationToken)); + } + } + + public void Dispose() + { + CancellationTokenSource.Cancel(); + } + } +} diff --git a/Oqtane.Server/Infrastructure/Jobs/SampleJob.cs b/Oqtane.Server/Infrastructure/Jobs/SampleJob.cs new file mode 100644 index 00000000..816fbaef --- /dev/null +++ b/Oqtane.Server/Infrastructure/Jobs/SampleJob.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Oqtane.Models; +using Oqtane.Repository; +using Oqtane.Shared; + +namespace Oqtane.Infrastructure +{ + public class SampleJob : HostedServiceBase + { + // JobType = "Oqtane.Infrastructure.SampleJob, Oqtane.Server" + + public SampleJob(IServiceScopeFactory ServiceScopeFactory) : base(ServiceScopeFactory) {} + + public override string ExecuteJob(IServiceProvider provider) + { + // get the first alias for this installation + var Aliases = provider.GetRequiredService(); + Alias alias = Aliases.GetAliases().FirstOrDefault(); + + // use the SiteState to set the Alias explicitly so the tenant can be resolved + var sitestate = provider.GetRequiredService(); + sitestate.Alias = alias; + + // call a repository service which requires tenant resolution + var Sites = provider.GetRequiredService(); + Site site = Sites.GetSites().FirstOrDefault(); + + return "You Should Include Any Notes Related To The Execution Of The Schedule Job. This Job Simply Reports That The Default Site Is " + site.Name; + } + } +} diff --git a/Oqtane.Server/Infrastructure/TestJob.cs b/Oqtane.Server/Infrastructure/TestJob.cs deleted file mode 100644 index 1cabfc2d..00000000 --- a/Oqtane.Server/Infrastructure/TestJob.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Microsoft.Extensions.DependencyInjection; -using Oqtane.Models; -using Oqtane.Repository; - -namespace Oqtane.Infrastructure -{ - public class TestJob : HostedServiceBase - { - public TestJob(IServiceScopeFactory ServiceScopeFactory) : base(ServiceScopeFactory) {} - - public override void ExecuteJob(IServiceProvider provider) - { - var Tenants = provider.GetRequiredService(); - foreach(Tenant tenant in Tenants.GetTenants()) - { - // is it possible to set the HttpContext so that DbContextBase will resolve properly for tenants? - } - } - } -} diff --git a/Oqtane.Server/Repository/Context/DBContextBase.cs b/Oqtane.Server/Repository/Context/DBContextBase.cs index e6d0081b..bb1b2635 100644 --- a/Oqtane.Server/Repository/Context/DBContextBase.cs +++ b/Oqtane.Server/Repository/Context/DBContextBase.cs @@ -42,7 +42,7 @@ namespace Oqtane.Repository ChangeTracker.DetectChanges(); string username = ""; - if (accessor.HttpContext.User.Identity.Name != null) + if (accessor.HttpContext != null && accessor.HttpContext.User.Identity.Name != null) { username = accessor.HttpContext.User.Identity.Name; } diff --git a/Oqtane.Server/Repository/Context/MasterDBContext.cs b/Oqtane.Server/Repository/Context/MasterDBContext.cs index 3e0c2b8f..88663bfa 100644 --- a/Oqtane.Server/Repository/Context/MasterDBContext.cs +++ b/Oqtane.Server/Repository/Context/MasterDBContext.cs @@ -18,15 +18,15 @@ namespace Oqtane.Repository public virtual DbSet Alias { get; set; } public virtual DbSet Tenant { get; set; } public virtual DbSet ModuleDefinition { get; set; } - public virtual DbSet Schedule { get; set; } - public virtual DbSet ScheduleLog { get; set; } + public virtual DbSet Job { get; set; } + public virtual DbSet JobLog { get; set; } public override int SaveChanges() { ChangeTracker.DetectChanges(); string username = ""; - if (accessor.HttpContext.User.Identity.Name != null) + if (accessor.HttpContext != null && accessor.HttpContext.User.Identity.Name != null) { username = accessor.HttpContext.User.Identity.Name; } diff --git a/Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs b/Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs new file mode 100644 index 00000000..f99de0d2 --- /dev/null +++ b/Oqtane.Server/Repository/Interfaces/IJobLogRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface IJobLogRepository + { + IEnumerable GetJobLogs(); + JobLog AddJobLog(JobLog JobLog); + JobLog UpdateJobLog(JobLog JobLog); + JobLog GetJobLog(int JobLogId); + void DeleteJobLog(int JobLogId); + } +} diff --git a/Oqtane.Server/Repository/Interfaces/IJobRepository.cs b/Oqtane.Server/Repository/Interfaces/IJobRepository.cs new file mode 100644 index 00000000..8683009f --- /dev/null +++ b/Oqtane.Server/Repository/Interfaces/IJobRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Oqtane.Models; + +namespace Oqtane.Repository +{ + public interface IJobRepository + { + IEnumerable GetJobs(); + Job AddJob(Job Job); + Job UpdateJob(Job Job); + Job GetJob(int JobId); + void DeleteJob(int JobId); + } +} diff --git a/Oqtane.Server/Repository/Interfaces/IScheduleLogRepository.cs b/Oqtane.Server/Repository/Interfaces/IScheduleLogRepository.cs deleted file mode 100644 index 240f1fee..00000000 --- a/Oqtane.Server/Repository/Interfaces/IScheduleLogRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Oqtane.Models; - -namespace Oqtane.Repository -{ - public interface IScheduleLogRepository - { - IEnumerable GetScheduleLogs(); - ScheduleLog AddScheduleLog(ScheduleLog ScheduleLog); - ScheduleLog UpdateScheduleLog(ScheduleLog ScheduleLog); - ScheduleLog GetScheduleLog(int ScheduleLogId); - void DeleteScheduleLog(int ScheduleLogId); - } -} diff --git a/Oqtane.Server/Repository/Interfaces/IScheduleRepository.cs b/Oqtane.Server/Repository/Interfaces/IScheduleRepository.cs deleted file mode 100644 index 09c3b3ca..00000000 --- a/Oqtane.Server/Repository/Interfaces/IScheduleRepository.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; -using Oqtane.Models; - -namespace Oqtane.Repository -{ - public interface IScheduleRepository - { - IEnumerable GetSchedules(); - Schedule AddSchedule(Schedule Schedule); - Schedule UpdateSchedule(Schedule Schedule); - Schedule GetSchedule(int ScheduleId); - void DeleteSchedule(int ScheduleId); - } -} diff --git a/Oqtane.Server/Repository/JobLogRepository.cs b/Oqtane.Server/Repository/JobLogRepository.cs new file mode 100644 index 00000000..66ef6f2e --- /dev/null +++ b/Oqtane.Server/Repository/JobLogRepository.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.Linq; +using Oqtane.Models; +using Microsoft.EntityFrameworkCore; + +namespace Oqtane.Repository +{ + public class JobLogRepository : IJobLogRepository + { + private MasterDBContext db; + + public JobLogRepository(MasterDBContext context) + { + db = context; + } + + public IEnumerable GetJobLogs() + { + return db.JobLog + .Include(item => item.Job) // eager load jobs + .ToList(); + } + + public JobLog AddJobLog(JobLog JobLog) + { + db.JobLog.Add(JobLog); + db.SaveChanges(); + return JobLog; + } + + public JobLog UpdateJobLog(JobLog JobLog) + { + db.Entry(JobLog).State = EntityState.Modified; + db.SaveChanges(); + return JobLog; + } + + public JobLog GetJobLog(int JobLogId) + { + return db.JobLog.Include(item => item.Job) // eager load job + .SingleOrDefault(item => item.JobLogId == JobLogId); + } + + public void DeleteJobLog(int JobLogId) + { + JobLog Joblog = db.JobLog.Find(JobLogId); + db.JobLog.Remove(Joblog); + db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Repository/JobRepository.cs b/Oqtane.Server/Repository/JobRepository.cs new file mode 100644 index 00000000..46ffaba4 --- /dev/null +++ b/Oqtane.Server/Repository/JobRepository.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Linq; +using Oqtane.Models; +using Microsoft.EntityFrameworkCore; +using System; + +namespace Oqtane.Repository +{ + public class JobRepository : IJobRepository + { + private MasterDBContext db; + + public JobRepository(MasterDBContext context) + { + db = context; + } + + public IEnumerable GetJobs() + { + return db.Job.ToList(); + } + + public Job AddJob(Job Job) + { + db.Job.Add(Job); + db.SaveChanges(); + return Job; + } + + public Job UpdateJob(Job Job) + { + db.Entry(Job).State = EntityState.Modified; + db.SaveChanges(); + return Job; + } + + public Job GetJob(int JobId) + { + return db.Job.Find(JobId); + } + + public void DeleteJob(int JobId) + { + Job Job = db.Job.Find(JobId); + db.Job.Remove(Job); + db.SaveChanges(); + } + } +} diff --git a/Oqtane.Server/Repository/ScheduleLogRepository.cs b/Oqtane.Server/Repository/ScheduleLogRepository.cs deleted file mode 100644 index 03b67295..00000000 --- a/Oqtane.Server/Repository/ScheduleLogRepository.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Oqtane.Models; -using Microsoft.EntityFrameworkCore; -using System; - -namespace Oqtane.Repository -{ - public class ScheduleLogRepository : IScheduleLogRepository - { - private MasterDBContext db; - - public ScheduleLogRepository(MasterDBContext context) - { - db = context; - } - - public IEnumerable GetScheduleLogs() - { - return db.ScheduleLog.ToList(); - } - - public ScheduleLog AddScheduleLog(ScheduleLog ScheduleLog) - { - db.ScheduleLog.Add(ScheduleLog); - db.SaveChanges(); - return ScheduleLog; - } - - public ScheduleLog UpdateScheduleLog(ScheduleLog ScheduleLog) - { - db.Entry(ScheduleLog).State = EntityState.Modified; - db.SaveChanges(); - return ScheduleLog; - } - - public ScheduleLog GetScheduleLog(int ScheduleLogId) - { - return db.ScheduleLog.Find(ScheduleLogId); - } - - public void DeleteScheduleLog(int ScheduleLogId) - { - ScheduleLog schedulelog = db.ScheduleLog.Find(ScheduleLogId); - db.ScheduleLog.Remove(schedulelog); - db.SaveChanges(); - } - } -} diff --git a/Oqtane.Server/Repository/ScheduleRepository.cs b/Oqtane.Server/Repository/ScheduleRepository.cs deleted file mode 100644 index 052d2467..00000000 --- a/Oqtane.Server/Repository/ScheduleRepository.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Oqtane.Models; -using Microsoft.EntityFrameworkCore; -using System; - -namespace Oqtane.Repository -{ - public class ScheduleRepository : IScheduleRepository - { - private MasterDBContext db; - - public ScheduleRepository(MasterDBContext context) - { - db = context; - } - - public IEnumerable GetSchedules() - { - return db.Schedule.ToList(); - } - - public Schedule AddSchedule(Schedule Schedule) - { - db.Schedule.Add(Schedule); - db.SaveChanges(); - return Schedule; - } - - public Schedule UpdateSchedule(Schedule Schedule) - { - db.Entry(Schedule).State = EntityState.Modified; - db.SaveChanges(); - return Schedule; - } - - public Schedule GetSchedule(int ScheduleId) - { - return db.Schedule.Find(ScheduleId); - } - - public void DeleteSchedule(int ScheduleId) - { - Schedule schedule = db.Schedule.Find(ScheduleId); - db.Schedule.Remove(schedule); - db.SaveChanges(); - } - } -} diff --git a/Oqtane.Server/Repository/SiteRepository.cs b/Oqtane.Server/Repository/SiteRepository.cs index d21a7fe7..59a7233f 100644 --- a/Oqtane.Server/Repository/SiteRepository.cs +++ b/Oqtane.Server/Repository/SiteRepository.cs @@ -92,6 +92,9 @@ namespace Oqtane.Repository SiteTemplate.Add(new PageTemplate { Name = "Theme Management", Parent = "Admin", Path = "admin/themes", Icon = "brush", IsNavigation = false, IsPersonalizable = false, EditMode = true, PagePermissions = "[{\"PermissionName\":\"View\",\"Permissions\":\"Administrators\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"Administrators\"}]", PageTemplateModules = new List { new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.Themes, Oqtane.Client", Title = "Theme Management", Pane = "Content", ModulePermissions = "[{\"PermissionName\":\"View\",\"Permissions\":\"Administrators\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"Administrators\"}]", Content = "" } }}); + SiteTemplate.Add(new PageTemplate { Name = "Scheduled Jobs", Parent = "Admin", Path = "admin/jobs", Icon = "timer", IsNavigation = false, IsPersonalizable = false, EditMode = true, PagePermissions = "[{\"PermissionName\":\"View\",\"Permissions\":\"Administrators\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"Administrators\"}]", PageTemplateModules = new List { + new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.Jobs, Oqtane.Client", Title = "Scheduled Jobs", Pane = "Content", ModulePermissions = "[{\"PermissionName\":\"View\",\"Permissions\":\"Administrators\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"Administrators\"}]", Content = "" } + }}); SiteTemplate.Add(new PageTemplate { Name = "Upgrade Service", Parent = "Admin", Path = "admin/upgrade", Icon = "aperture", IsNavigation = false, IsPersonalizable = false, EditMode = true, PagePermissions = "[{\"PermissionName\":\"View\",\"Permissions\":\"Administrators\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"Administrators\"}]", PageTemplateModules = new List { new PageTemplateModule { ModuleDefinitionName = "Oqtane.Modules.Admin.Upgrade, Oqtane.Client", Title = "Upgrade Service", Pane = "Content", ModulePermissions = "[{\"PermissionName\":\"View\",\"Permissions\":\"Administrators\"},{\"PermissionName\":\"Edit\",\"Permissions\":\"Administrators\"}]", Content = "" } }}); diff --git a/Oqtane.Server/Repository/TenantResolver.cs b/Oqtane.Server/Repository/TenantResolver.cs index 30eb7c19..299a8a41 100644 --- a/Oqtane.Server/Repository/TenantResolver.cs +++ b/Oqtane.Server/Repository/TenantResolver.cs @@ -3,6 +3,7 @@ using System.Linq; using Oqtane.Models; using Microsoft.AspNetCore.Http; using System; +using Oqtane.Shared; namespace Oqtane.Repository { @@ -12,12 +13,14 @@ namespace Oqtane.Repository private readonly string aliasname; private readonly IAliasRepository Aliases; private readonly ITenantRepository Tenants; + private readonly SiteState sitestate; - public TenantResolver(MasterDBContext context, IHttpContextAccessor accessor, IAliasRepository Aliases, ITenantRepository Tenants) + public TenantResolver(MasterDBContext context, IHttpContextAccessor accessor, IAliasRepository Aliases, ITenantRepository Tenants, SiteState sitestate) { db = context; this.Aliases = Aliases; this.Tenants = Tenants; + this.sitestate = sitestate; aliasname = ""; // get alias based on request context @@ -35,6 +38,13 @@ namespace Oqtane.Repository aliasname = aliasname.Substring(0, aliasname.Length - 1); } } + else // background processes can pass in an alias using the SiteState service + { + if (sitestate != null) + { + aliasname = sitestate.Alias.Name; + } + } } public Alias GetAlias() diff --git a/Oqtane.Server/Scripts/Master.sql b/Oqtane.Server/Scripts/Master.sql index 2d68947a..b2f05b65 100644 --- a/Oqtane.Server/Scripts/Master.sql +++ b/Oqtane.Server/Scripts/Master.sql @@ -3,22 +3,6 @@ Create tables */ -CREATE TABLE [dbo].[Alias]( - [AliasId] [int] IDENTITY(1,1) NOT NULL, - [Name] [nvarchar](200) NOT NULL, - [TenantId] [int] NOT NULL, - [SiteId] [int] NOT NULL, - [CreatedBy] [nvarchar](256) NOT NULL, - [CreatedOn] [datetime] NOT NULL, - [ModifiedBy] [nvarchar](256) NOT NULL, - [ModifiedOn] [datetime] NOT NULL, - CONSTRAINT [PK_Alias] PRIMARY KEY CLUSTERED - ( - [AliasId] ASC - ) -) -GO - CREATE TABLE [dbo].[Tenant]( [TenantId] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](100) NOT NULL, @@ -36,6 +20,23 @@ CREATE TABLE [dbo].[Tenant]( ) GO +CREATE TABLE [dbo].[Alias]( + [AliasId] [int] IDENTITY(1,1) NOT NULL, + [Name] [nvarchar](200) NOT NULL, + [TenantId] [int] NOT NULL, + [SiteId] [int] NOT NULL, + [CreatedBy] [nvarchar](256) NOT NULL, + [CreatedOn] [datetime] NOT NULL, + [ModifiedBy] [nvarchar](256) NOT NULL, + [ModifiedOn] [datetime] NOT NULL, + CONSTRAINT [PK_Alias] PRIMARY KEY CLUSTERED + ( + [AliasId] ASC + ) +) +GO + + CREATE TABLE [dbo].[ModuleDefinition]( [ModuleDefinitionId] [int] IDENTITY(1,1) NOT NULL, [ModuleDefinitionName] [nvarchar](200) NOT NULL, @@ -50,38 +51,40 @@ CREATE TABLE [dbo].[ModuleDefinition]( ) GO -CREATE TABLE [dbo].[Schedule] ( - [ScheduleId] [int] IDENTITY(1,1) NOT NULL, +CREATE TABLE [dbo].[Job] ( + [JobId] [int] IDENTITY(1,1) NOT NULL, [Name] [nvarchar](200) NOT NULL, [JobType] [nvarchar](200) NOT NULL, - [Period] [int] NOT NULL, [Frequency] [char](1) NOT NULL, + [Interval] [int] NOT NULL, [StartDate] [datetime] NULL, - [IsActive] [bit] NOT NULL, + [EndDate] [datetime] NULL, + [IsEnabled] [bit] NOT NULL, + [IsStarted] [bit] NOT NULL, [IsExecuting] [bit] NOT NULL, [NextExecution] [datetime] NULL, [RetentionHistory] [int] NOT NULL, - [CreatedBy] [nvarchar](256) NULL, - [CreatedOn] [datetime] NULL, - [ModifiedBy] [nvarchar](256) NULL, - [ModifiedOn] [datetime] NULL, - CONSTRAINT [PK_Schedule] PRIMARY KEY CLUSTERED + [CreatedBy] [nvarchar](256) NOT NULL, + [CreatedOn] [datetime] NOT NULL, + [ModifiedBy] [nvarchar](256) NOT NULL, + [ModifiedOn] [datetime] NOT NULL, + CONSTRAINT [PK_Job] PRIMARY KEY CLUSTERED ( - [ScheduleId] ASC + [JobId] ASC ) ) GO -CREATE TABLE [dbo].[ScheduleLog] ( - [ScheduleLogId] [int] IDENTITY(1,1) NOT NULL, - [ScheduleId] [int] NOT NULL, +CREATE TABLE [dbo].[JobLog] ( + [JobLogId] [int] IDENTITY(1,1) NOT NULL, + [JobId] [int] NOT NULL, [StartDate] [datetime] NOT NULL, [FinishDate] [datetime] NULL, [Succeeded] [bit] NULL, [Notes] [nvarchar](max) NULL, - CONSTRAINT [PK_ScheduleLog] PRIMARY KEY CLUSTERED + CONSTRAINT [PK_JobLog] PRIMARY KEY CLUSTERED ( - [ScheduleLogId] ASC + [JobLogId] ASC ) ) GO @@ -107,8 +110,8 @@ REFERENCES [dbo].[Tenant] ([TenantId]) ON DELETE CASCADE GO -ALTER TABLE [dbo].[ScheduleLog] WITH NOCHECK ADD CONSTRAINT [FK_ScheduleLog_Schedule] FOREIGN KEY([ScheduleId]) -REFERENCES [dbo].[Schedule] ([ScheduleId]) +ALTER TABLE [dbo].[JobLog] WITH NOCHECK ADD CONSTRAINT [FK_JobLog_Job] FOREIGN KEY([JobId]) +REFERENCES [dbo].[Job] ([JobId]) ON DELETE CASCADE GO @@ -132,3 +135,12 @@ VALUES (1, N'{Alias}', 1, 1, '', getdate(), '', getdate()) GO SET IDENTITY_INSERT [dbo].[Alias] OFF GO + +SET IDENTITY_INSERT [dbo].[Job] ON +GO +INSERT [dbo].[Job] ([JobId], [Name], [JobType], [Frequency], [Interval], [StartDate], [EndDate], [IsEnabled], [IsStarted], [IsExecuting], [NextExecution], [RetentionHistory], [CreatedBy], [CreatedOn], [ModifiedBy], [ModifiedOn]) +VALUES (1, N'Sample Daily Job', N'Oqtane.Infrastructure.SampleJob, Oqtane.Server', N'd', 1, null, null, 1, 0, 0, null, 10, '', getdate(), '', getdate()) +GO +SET IDENTITY_INSERT [dbo].[Job] OFF +GO + diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 3d5a8497..a824dfbc 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -100,8 +100,8 @@ namespace Oqtane.Server services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddSingleton(); @@ -173,8 +173,8 @@ namespace Oqtane.Server services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // get list of loaded assemblies Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); @@ -382,8 +382,8 @@ namespace Oqtane.Server services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddTransient(); + services.AddTransient(); // get list of loaded assemblies Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); diff --git a/Oqtane.Shared/Models/Schedule.cs b/Oqtane.Shared/Models/Job.cs similarity index 69% rename from Oqtane.Shared/Models/Schedule.cs rename to Oqtane.Shared/Models/Job.cs index f82dfab3..2d1f40fc 100644 --- a/Oqtane.Shared/Models/Schedule.cs +++ b/Oqtane.Shared/Models/Job.cs @@ -2,15 +2,17 @@ namespace Oqtane.Models { - public class Schedule : IAuditable + public class Job : IAuditable { - public int ScheduleId { get; set; } + public int JobId { get; set; } public string Name { get; set; } public string JobType { get; set; } - public int Period { get; set; } public string Frequency { get; set; } + public int Interval { get; set; } public DateTime? StartDate { get; set; } - public bool IsActive { get; set; } + public DateTime? EndDate { get; set; } + public bool IsEnabled { get; set; } + public bool IsStarted { get; set; } public bool IsExecuting { get; set; } public DateTime? NextExecution { get; set; } public int RetentionHistory { get; set; } diff --git a/Oqtane.Shared/Models/ScheduleLog.cs b/Oqtane.Shared/Models/JobLog.cs similarity index 62% rename from Oqtane.Shared/Models/ScheduleLog.cs rename to Oqtane.Shared/Models/JobLog.cs index 77096d9b..161d5b54 100644 --- a/Oqtane.Shared/Models/ScheduleLog.cs +++ b/Oqtane.Shared/Models/JobLog.cs @@ -2,13 +2,15 @@ namespace Oqtane.Models { - public class ScheduleLog + public class JobLog { - public int ScheduleLogId { get; set; } - public int ScheduleId { get; set; } + public int JobLogId { get; set; } + public int JobId { get; set; } public DateTime StartDate { get; set; } public DateTime? FinishDate { get; set; } public bool? Succeeded { get; set; } public string Notes { get; set; } + + public Job Job { get; set; } } }