From 4338b4fee537a5afb85eef5ab838437999d9f813 Mon Sep 17 00:00:00 2001 From: James Kolpack Date: Thu, 26 Jan 2017 08:36:54 -0500 Subject: [PATCH] Add cancel --- Core.Tests/Remote/PiscalSshClientTests.cs | 11 +- Core/Entities/LeafInput.cs | 24 +++-- Core/Entities/LeafInputStatusType.cs | 5 +- Core/Remote/IPiscalClient.cs | 1 + Core/Remote/PiscalClientException.cs | 14 +++ Core/Remote/PiscalSshClient.cs | 23 +++- .../Models/ResultStatusViewModelTests.cs | 6 +- WebCms/App_Data/Models/all.dll.path | 2 +- WebCms/Content/style.css | 10 ++ WebCms/Content/style.min.css | 2 +- WebCms/Content/style.scss | 17 +++ WebCms/Controllers/BaseController.cs | 13 +-- WebCms/Controllers/LeafWebPageIds.cs | 1 + WebCms/Controllers/QueueController.cs | 40 ++++++- WebCms/Controllers/ResultsController.cs | 2 +- WebCms/Models/LeafInputDetails.cs | 9 +- ...ItemViewModel.cs => QueueItemViewModel.cs} | 10 +- WebCms/Models/QueueViewModel.cs | 2 +- WebCms/Services/EmailNotificationService.cs | 34 +++++- .../PiscalQueue/PiscalQueueManager.cs | 101 +++++++++++++++--- WebCms/Services/PiscalQueue/PiscalService.cs | 8 +- WebCms/Views/Queue/Details.cshtml | 4 +- WebCms/Views/Queue/Index.cshtml | 12 ++- WebCms/Views/Results/Index.cshtml | 2 +- .../DisplayTemplates/_CancelForm.cshtml | 14 +++ .../DisplayTemplates/_LeafInputStatus.cshtml | 5 +- WebCms/Web.config | 13 ++- WebCms/WebCms.csproj | 3 +- WebCms/scripts/LeafInputCreate.js | 5 +- WebCms/scripts/site.js | 2 +- 30 files changed, 333 insertions(+), 62 deletions(-) rename WebCms/Models/{ResultItemViewModel.cs => QueueItemViewModel.cs} (88%) create mode 100644 WebCms/Views/Shared/DisplayTemplates/_CancelForm.cshtml diff --git a/Core.Tests/Remote/PiscalSshClientTests.cs b/Core.Tests/Remote/PiscalSshClientTests.cs index 67e324d..21fcf4a 100644 --- a/Core.Tests/Remote/PiscalSshClientTests.cs +++ b/Core.Tests/Remote/PiscalSshClientTests.cs @@ -17,8 +17,8 @@ namespace LeafWeb.Core.Tests.Remote new PiscalLeafInput { LeafInputId = 1, - PiscalDirectoryName = "TestDirectory3", - PhotosyntheticType = "C4_photosynthesis_leafweb", + PiscalDirectoryName = "TestDirectory", + PhotosyntheticType = "C3_photosynthesis_leafweb", InputFiles = new[] { new PiscalLeafInputFile @@ -72,5 +72,12 @@ namespace LeafWeb.Core.Tests.Remote var client = GetTestClient(); client.CleanupLeafProcess(_testInput); } + + [Test] + public void KillLeafProcess() + { + var client = GetTestClient(); + client.KillLeafProcess(_testInput); + } } } diff --git a/Core/Entities/LeafInput.cs b/Core/Entities/LeafInput.cs index e3cb2a6..38beabd 100644 --- a/Core/Entities/LeafInput.cs +++ b/Core/Entities/LeafInput.cs @@ -21,14 +21,26 @@ namespace LeafWeb.Core.Entities public LeafInputStatusType CurrentStatus { get; set; } public virtual ICollection StatusHistory { get; set; } - public bool IsInProgress => CurrentStatus == LeafInputStatusType.Starting - || CurrentStatus == LeafInputStatusType.Running - || CurrentStatus == LeafInputStatusType.Finishing; - - public bool IsComplete => CurrentStatus == LeafInputStatusType.Complete; - public bool IsDeletable => !IsInProgress; public bool IsPending => CurrentStatus == LeafInputStatusType.Pending; + public bool IsStarting => CurrentStatus == LeafInputStatusType.Starting; public bool IsRunning => CurrentStatus == LeafInputStatusType.Running; + public bool IsFinishing => CurrentStatus == LeafInputStatusType.Finishing; + public bool IsComplete => CurrentStatus == LeafInputStatusType.Complete; + public bool IsException => CurrentStatus == LeafInputStatusType.Exception; + public bool IsCancelPending => CurrentStatus == LeafInputStatusType.CancelPending; + public bool IsCancelling => CurrentStatus == LeafInputStatusType.Cancelling; + public bool IsCancelled => CurrentStatus == LeafInputStatusType.Cancelled; + + public bool IsInProgress => IsStarting + || IsRunning + || IsFinishing + || IsCancelPending + || IsCancelling; + public bool IsDeletable => !IsInProgress; + public bool HasOutputFiles => OutputFiles.Any(); + public bool IsCancellable => + IsRunning + || IsPending; [Required(ErrorMessage = "Name required")] public string Name { get; set; } diff --git a/Core/Entities/LeafInputStatusType.cs b/Core/Entities/LeafInputStatusType.cs index 12492bb..74e1be3 100644 --- a/Core/Entities/LeafInputStatusType.cs +++ b/Core/Entities/LeafInputStatusType.cs @@ -7,6 +7,9 @@ namespace LeafWeb.Core.Entities Running = 2, Finishing = 3, Complete = 4, - Exception = 5 + Exception = 5, + CancelPending = 6, + Cancelling = 7, + Cancelled = 8 } } \ No newline at end of file diff --git a/Core/Remote/IPiscalClient.cs b/Core/Remote/IPiscalClient.cs index a962057..0187d9e 100644 --- a/Core/Remote/IPiscalClient.cs +++ b/Core/Remote/IPiscalClient.cs @@ -8,6 +8,7 @@ namespace LeafWeb.Core.Remote void RunLeafInput(PiscalLeafInput leafInput); PiscalStatus GetLeafInputStatus(PiscalLeafInput leafInput); IEnumerable RetrieveLeafOutput(PiscalLeafInput leafInput); + void KillLeafProcess(PiscalLeafInput leafInput); void CleanupLeafProcess(PiscalLeafInput leafInput); } } diff --git a/Core/Remote/PiscalClientException.cs b/Core/Remote/PiscalClientException.cs index 9ba1cef..40190f0 100644 --- a/Core/Remote/PiscalClientException.cs +++ b/Core/Remote/PiscalClientException.cs @@ -1,13 +1,27 @@ using System; +using LeafWeb.Core.Utility; +using Renci.SshNet; namespace LeafWeb.Core.Remote { public class PiscalClientException : Exception { public int LeafInputId { get; private set; } + public string CommandText { get; private set; } + public PiscalClientException(int leafInputId, string error) : base(error) { LeafInputId = leafInputId; } + + public PiscalClientException(int leafInputId, SshCommand command) : base( + !string.IsNullOrEmpty(command.Error) + ? command.Error.TrimEndNewLine() + : command.Result.TrimEndNewLine() + ) + { + LeafInputId = leafInputId; + CommandText = command.CommandText; + } } } \ No newline at end of file diff --git a/Core/Remote/PiscalSshClient.cs b/Core/Remote/PiscalSshClient.cs index 48950dc..d3e3131 100644 --- a/Core/Remote/PiscalSshClient.cs +++ b/Core/Remote/PiscalSshClient.cs @@ -115,7 +115,7 @@ namespace LeafWeb.Core.Remote Disconnect(leafInput, ssh); if (command.ExitStatus != 0) - throw new PiscalClientException(leafInput.LeafInputId, command.Error.TrimEndNewLine()); + throw new PiscalClientException(leafInput.LeafInputId, command); var result = command.Result.TrimEndNewLine(); if (result == "started") @@ -154,7 +154,7 @@ namespace LeafWeb.Core.Remote Disconnect(leafInput, ssh); if (command.ExitStatus != 0) - throw new PiscalClientException(leafInput.LeafInputId, command.Error.TrimEndNewLine()); + throw new PiscalClientException(leafInput.LeafInputId, command); return command.Result .SplitNewLine() @@ -203,7 +203,22 @@ namespace LeafWeb.Core.Remote Disconnect(leafInput, scp); } } - + + public void KillLeafProcess(PiscalLeafInput leafInput) + { + using (var ssh = GetSshClient()) + { + Connect(leafInput, ssh); + var commandText = $"{RemoteScriptPath} -d {leafInput.PiscalDirectoryName} -k"; + var command = ssh.CreateCommand(commandText); + command.Execute(); + Disconnect(leafInput, ssh); + + if (command.ExitStatus != 0) + throw new PiscalClientException(leafInput.LeafInputId, command); + } + } + public void CleanupLeafProcess(PiscalLeafInput leafInput) { using (var ssh = GetSshClient()) @@ -215,7 +230,7 @@ namespace LeafWeb.Core.Remote Disconnect(leafInput, ssh); if (command.ExitStatus != 0) - throw new PiscalClientException(leafInput.LeafInputId, command.Error.TrimEndNewLine()); + throw new PiscalClientException(leafInput.LeafInputId, command); } } } diff --git a/WebCms.Tests/Models/ResultStatusViewModelTests.cs b/WebCms.Tests/Models/ResultStatusViewModelTests.cs index 86fdad2..82e68d3 100644 --- a/WebCms.Tests/Models/ResultStatusViewModelTests.cs +++ b/WebCms.Tests/Models/ResultStatusViewModelTests.cs @@ -34,7 +34,7 @@ namespace LeafWeb.WebCms.Tests.Models public void CanConstructFromLeafInputFile() { var leafInput = GetLeafInput(); - var viewModel = new ResultItemViewModel(leafInput); + var viewModel = new QueueItemViewModel(leafInput); Assert.That(viewModel.CurrentStatus, Is.EqualTo(leafInput.CurrentStatus.ToString())); Assert.That(viewModel.LeafInputId, Is.EqualTo(leafInput.Id)); @@ -50,7 +50,7 @@ namespace LeafWeb.WebCms.Tests.Models var leafInput = GetLeafInput(); leafInput.CurrentStatus = LeafInputStatusType.Running; leafInput.OutputFiles = new LeafOutputFile[0]; - var viewModel = new ResultItemViewModel(leafInput); + var viewModel = new QueueItemViewModel(leafInput); Assert.That(viewModel.CurrentStatus, Is.EqualTo(LeafInputStatusType.Running.ToString())); //Assert.That(viewModel.LeafOutputFilenames, Has.Length.EqualTo(0)); @@ -71,7 +71,7 @@ namespace LeafWeb.WebCms.Tests.Models Status = LeafInputStatusType.Exception } }; - var viewModel = new ResultItemViewModel(leafInput); + var viewModel = new QueueItemViewModel(leafInput); Assert.That(viewModel.CurrentStatus, Is.EqualTo(LeafInputStatusType.Exception.ToString())); //Assert.That(viewModel.ErrorMessages[0], Is.EqualTo(leafInput.StatusHistory.First().Description)); diff --git a/WebCms/App_Data/Models/all.dll.path b/WebCms/App_Data/Models/all.dll.path index 6e2e3d6..f027e12 100644 --- a/WebCms/App_Data/Models/all.dll.path +++ b/WebCms/App_Data/Models/all.dll.path @@ -1 +1 @@ -C:\Users\poprhythm\AppData\Local\Temp\Temporary ASP.NET Files\vs\f80e29bb\faae20bf\App_Web_all.generated.cs.8f9494c4.rzzuwtuu.dll \ No newline at end of file +C:\Users\poprhythm\AppData\Local\Temp\Temporary ASP.NET Files\vs\f80e29bb\faae20bf\App_Web_all.generated.cs.8f9494c4._retrxtu.dll \ No newline at end of file diff --git a/WebCms/Content/style.css b/WebCms/Content/style.css index f835cc9..54a5479 100644 --- a/WebCms/Content/style.css +++ b/WebCms/Content/style.css @@ -90,6 +90,16 @@ a.banner-link:hover { content: "\e026"; } .status.status-finishing:after { content: "\e027"; } + .status.status-cancelpending, .status.status-cancelling { + color: #f0ad4e; } + .status.status-cancelled { + color: #ec971f; } + .status.status-cancelpending:after { + content: "\e090"; } + .status.status-cancelling:after { + content: "\e090"; } + .status.status-cancelled:after { + content: "\e090"; } #chart { padding-top: 20px; } diff --git a/WebCms/Content/style.min.css b/WebCms/Content/style.min.css index 8e9d871..011b68e 100644 --- a/WebCms/Content/style.min.css +++ b/WebCms/Content/style.min.css @@ -1 +1 @@ -h1{padding:24px 0 12px 0;}p{padding:12px 0;}footer{margin-top:24px !important;}.row-no-padding [class*="col-"]{padding-left:0 !important;padding-right:0 !important;}.home .dark .row:first-child .column:first-child h1{padding-top:0;}.home .blogarchive{padding-top:20px;}.top-buffer{margin-top:20px;}.detail-actions>a,.detail-actions>form>button{margin-top:20px;float:left;clear:left;}.banner-link{white-space:normal;padding:20px;background:#000;background:rgba(0,0,0,.5);-moz-border-radius:10px;border-radius:10px;}.banner-link .glyphicon{color:#8cc641;}a.banner-link:hover{text-decoration:none;background:rgba(0,0,0,.6);}a.banner-link:hover .glyphicon{color:#a8ed4e;}.headline-icon h1:after{color:rgba(172,214,118,.8);font-family:"Glyphicons Halflings";font-size:.8em;padding-left:10px;}.headline-icon.headline-icon-file h1:after{content:"";}.headline-icon.headline-icon-leaf h1:after{content:"";}.headline-icon.headline-icon-question h1:after{content:"";}.headline-icon.headline-icon-stats h1:after{content:"";}.headline-icon.headline-icon-user h1:after{content:"";}.headline-icon.headline-icon-list h1:after{content:"";}.status{white-space:nowrap;}.status:after{font-family:"Glyphicons Halflings";font-size:.8em;padding-left:5px;}.status.status-pending{color:#f0ad4e;}.status.status-pending:after{content:"";}.status.status-complete{color:#337ab7;}.status.status-complete:after{content:"";}.status.status-exception{color:#a94442;}.status.status-exception:after{content:"";}.status.status-running,.status.status-starting,.status.status-finishing{color:#3c763d;}.status.status-running:after{content:"";}.status.status-starting:after{content:"";}.status.status-finishing:after{content:"";}#chart{padding-top:20px;}.btn-file{position:relative;overflow:hidden;}.btn-file input[type=file]{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;filter:alpha(opacity=0);opacity:0;outline:none;background:#fff;cursor:inherit;display:block;}form .validation-summary-errors ul{list-style-type:none;}.autocomplete-suggestions{border:1px solid #999;background:#fff;overflow:auto;}.autocomplete-suggestion{padding:2px 5px;white-space:nowrap;overflow:hidden;}.autocomplete-selected{background:#f0f0f0;}.autocomplete-suggestions strong{font-weight:normal;color:#39f;}.autocomplete-group{padding:2px 5px;}.autocomplete-group strong{display:block;border-bottom:1px solid #000;}.toggle{width:15px;}.dropdown-menu li form .btn-link{display:block;color:#333;clear:both;float:left;font-size:1rem;font-weight:normal;line-height:1.42857;min-width:160px;padding:3px 20px;text-align:left;white-space:nowrap;}.dropdown-menu li form .btn-link:focus,.dropdown-menu li form .btn-link:hover{text-decoration:none;color:#262626;background-color:#f5f5f5;}.divider-right{border-right:1px dashed #333;} \ No newline at end of file +h1{padding:24px 0 12px 0;}p{padding:12px 0;}footer{margin-top:24px !important;}.row-no-padding [class*="col-"]{padding-left:0 !important;padding-right:0 !important;}.home .dark .row:first-child .column:first-child h1{padding-top:0;}.home .blogarchive{padding-top:20px;}.top-buffer{margin-top:20px;}.detail-actions>a,.detail-actions>form>button{margin-top:20px;float:left;clear:left;}.banner-link{white-space:normal;padding:20px;background:#000;background:rgba(0,0,0,.5);-moz-border-radius:10px;border-radius:10px;}.banner-link .glyphicon{color:#8cc641;}a.banner-link:hover{text-decoration:none;background:rgba(0,0,0,.6);}a.banner-link:hover .glyphicon{color:#a8ed4e;}.headline-icon h1:after{color:rgba(172,214,118,.8);font-family:"Glyphicons Halflings";font-size:.8em;padding-left:10px;}.headline-icon.headline-icon-file h1:after{content:"";}.headline-icon.headline-icon-leaf h1:after{content:"";}.headline-icon.headline-icon-question h1:after{content:"";}.headline-icon.headline-icon-stats h1:after{content:"";}.headline-icon.headline-icon-user h1:after{content:"";}.headline-icon.headline-icon-list h1:after{content:"";}.status{white-space:nowrap;}.status:after{font-family:"Glyphicons Halflings";font-size:.8em;padding-left:5px;}.status.status-pending{color:#f0ad4e;}.status.status-pending:after{content:"";}.status.status-complete{color:#337ab7;}.status.status-complete:after{content:"";}.status.status-exception{color:#a94442;}.status.status-exception:after{content:"";}.status.status-running,.status.status-starting,.status.status-finishing{color:#3c763d;}.status.status-running:after{content:"";}.status.status-starting:after{content:"";}.status.status-finishing:after{content:"";}.status.status-cancelpending,.status.status-cancelling{color:#f0ad4e;}.status.status-cancelled{color:#ec971f;}.status.status-cancelpending:after{content:"";}.status.status-cancelling:after{content:"";}.status.status-cancelled:after{content:"";}#chart{padding-top:20px;}.btn-file{position:relative;overflow:hidden;}.btn-file input[type=file]{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;filter:alpha(opacity=0);opacity:0;outline:none;background:#fff;cursor:inherit;display:block;}form .validation-summary-errors ul{list-style-type:none;}.autocomplete-suggestions{border:1px solid #999;background:#fff;overflow:auto;}.autocomplete-suggestion{padding:2px 5px;white-space:nowrap;overflow:hidden;}.autocomplete-selected{background:#f0f0f0;}.autocomplete-suggestions strong{font-weight:normal;color:#39f;}.autocomplete-group{padding:2px 5px;}.autocomplete-group strong{display:block;border-bottom:1px solid #000;}.toggle{width:15px;}.dropdown-menu li form .btn-link{display:block;color:#333;clear:both;float:left;font-size:1rem;font-weight:normal;line-height:1.42857;min-width:160px;padding:3px 20px;text-align:left;white-space:nowrap;}.dropdown-menu li form .btn-link:focus,.dropdown-menu li form .btn-link:hover{text-decoration:none;color:#262626;background-color:#f5f5f5;}.divider-right{border-right:1px dashed #333;} \ No newline at end of file diff --git a/WebCms/Content/style.scss b/WebCms/Content/style.scss index 5b1ac3d..37a7d4e 100644 --- a/WebCms/Content/style.scss +++ b/WebCms/Content/style.scss @@ -112,6 +112,7 @@ a.banner-link:hover { content:"\e023"; } } + &.status-complete { color: #337ab7; &:after { @@ -137,6 +138,22 @@ a.banner-link:hover { &.status-finishing:after { content:"\e027"; } + + &.status-cancelpending,&.status-cancelling { + color: #f0ad4e; + } + &.status-cancelled { + color: #ec971f; + } + &.status-cancelpending:after { + content:"\e090"; + } + &.status-cancelling:after { + content:"\e090"; + } + &.status-cancelled:after { + content:"\e090"; + } } #chart { diff --git a/WebCms/Controllers/BaseController.cs b/WebCms/Controllers/BaseController.cs index ac89058..8bf0362 100644 --- a/WebCms/Controllers/BaseController.cs +++ b/WebCms/Controllers/BaseController.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Web.Mvc; +using System.Web.Routing; using log4net; using LeafWeb.Core.DAL; using Umbraco.Web.Mvc; @@ -21,9 +22,7 @@ namespace LeafWeb.WebCms.Controllers { if (filterContext?.Exception != null) { - var controller = filterContext.RouteData.Values["controller"].ToString(); - var action = filterContext.RouteData.Values["action"].ToString(); - var loggerName = $"LeafWeb.WebCms.Controllers.{controller}Controller.{action}"; + var loggerName = LoggerName(filterContext.RouteData); LogManager.GetLogger(loggerName).Error(filterContext.Exception); } @@ -31,12 +30,14 @@ namespace LeafWeb.WebCms.Controllers base.OnException(filterContext); } - protected bool IsHttpParamActionMatch() + protected string LoggerName(RouteData routeData) { - return ControllerContext.RouteData.Values["action"].ToString() - .Equals("Action", StringComparison.InvariantCultureIgnoreCase); + var controller = RouteData.Values["controller"].ToString(); + var action = RouteData.Values["action"].ToString(); + return $"LeafWeb.WebCms.Controllers.{controller}Controller.{action}"; } + // Status messages to pages protected enum StatusType { Info, diff --git a/WebCms/Controllers/LeafWebPageIds.cs b/WebCms/Controllers/LeafWebPageIds.cs index 1908c23..9545f9e 100644 --- a/WebCms/Controllers/LeafWebPageIds.cs +++ b/WebCms/Controllers/LeafWebPageIds.cs @@ -1,5 +1,6 @@ namespace LeafWeb.WebCms.Controllers { + // Umbraco page IDs for LeafWeb application pages public static class LeafWebPageIds { public const int ManageQueue = 1107; diff --git a/WebCms/Controllers/QueueController.cs b/WebCms/Controllers/QueueController.cs index 5e77f0a..a6afe9e 100644 --- a/WebCms/Controllers/QueueController.cs +++ b/WebCms/Controllers/QueueController.cs @@ -2,8 +2,10 @@ using System.Linq; using System.Web.Mvc; using Hangfire; +using log4net; using LeafWeb.Core.Entities; using LeafWeb.Core.Utility; +using LeafWeb.WebCms.App_Start; using LeafWeb.WebCms.Models; using LeafWeb.WebCms.Services; using LeafWeb.WebCms.Services.PiscalQueue; @@ -18,7 +20,7 @@ namespace LeafWeb.WebCms.Controllers DataService.GetLeafInputs() .OrderByDescending(f => f.Id) .ToList() - .Select(leafInput => new ResultItemViewModel(leafInput)); + .Select(leafInput => new QueueItemViewModel(leafInput)); string serviceDescription; try @@ -121,6 +123,42 @@ namespace LeafWeb.WebCms.Controllers return RedirectToUmbracoPage(LeafWebPageIds.ManageQueue); } + [ActionLog] + public ActionResult Cancel(int id) + { + var leafInput = DataService.GetLeafInput(id); + + if (leafInput == null) + { + SetStatusMessage($"LeafInput '${id}' not found, may have been deleted?"); + return RedirectToUmbracoPage(LeafWebPageIds.ManageQueue); + } + + if (leafInput.IsPending) + { + LogManager.GetLogger(LoggerName(RouteData)).DebugFormat("LeafInput: {0}, Set Cancelled from Pending", leafInput.Id); + DataService.SetLeafInputStatus(leafInput, LeafInputStatusType.Cancelled, + "Emailing cancellation notification to user", + $"Email: \'{leafInput.Email}\'"); + + // send notification immediately + BackgroundJob.Enqueue(email => email.SendLeafWebCancelled(leafInput.Id)); + SetStatusMessage($"Cancelling LeafInput '{leafInput.Identifier}'", StatusType.Success); + } + else if (leafInput.IsRunning) + { + DataService.SetLeafInputStatus(leafInput, LeafInputStatusType.CancelPending); + SetStatusMessage($"Cancelling LeafInput '{leafInput.Identifier}'", StatusType.Success); + HangfireStartup.TriggerPiscalProcessQueue(); + } + else + { + // don't allow to be cancelled if it isn't currently running + SetStatusMessage($"LeafInput '{leafInput.Identifier}' is not currently running!", StatusType.Error); + } + return RedirectToCurrentUmbracoUrl(); + } + [ActionLog] public ActionResult SendUserDownloadLink(int id) { diff --git a/WebCms/Controllers/ResultsController.cs b/WebCms/Controllers/ResultsController.cs index 8439bad..dcd3314 100644 --- a/WebCms/Controllers/ResultsController.cs +++ b/WebCms/Controllers/ResultsController.cs @@ -19,7 +19,7 @@ namespace LeafWeb.WebCms.Controllers orderby li.Id descending select li ).ToList() - .Select(leafInput => new ResultItemViewModel(leafInput)); + .Select(leafInput => new QueueItemViewModel(leafInput)); return View(viewModel); } diff --git a/WebCms/Models/LeafInputDetails.cs b/WebCms/Models/LeafInputDetails.cs index 2c32130..87efdc7 100644 --- a/WebCms/Models/LeafInputDetails.cs +++ b/WebCms/Models/LeafInputDetails.cs @@ -47,15 +47,15 @@ namespace LeafWeb.WebCms.Models [Display(Name = "Piscal Warning")] public string OutputWarningMessage { get; set; } - //[UIHint("Status")] - //public string CurrentStatus { get; set; } - [UIHint("LeafInputStatusViewModels")] public List StatusHistory { get; set; } [HiddenInput(DisplayValue = false)] public bool HasLeafChart { get; set; } + [HiddenInput(DisplayValue = false)] + public bool HasOutputFiles { get; set; } + [HiddenInput(DisplayValue = false)] public bool IsRunning { get; set; } @@ -65,6 +65,9 @@ namespace LeafWeb.WebCms.Models [HiddenInput(DisplayValue = false)] public bool IsDeletable { get; set; } + [HiddenInput(DisplayValue = false)] + public bool IsCancellable { get; set; } + static LeafInputDetails() { Mapper.CreateMap().ConvertUsing(file => file?.Contents.GetString()); diff --git a/WebCms/Models/ResultItemViewModel.cs b/WebCms/Models/QueueItemViewModel.cs similarity index 88% rename from WebCms/Models/ResultItemViewModel.cs rename to WebCms/Models/QueueItemViewModel.cs index 1883c42..6c29f50 100644 --- a/WebCms/Models/ResultItemViewModel.cs +++ b/WebCms/Models/QueueItemViewModel.cs @@ -4,7 +4,7 @@ using LeafWeb.Core.Entities; namespace LeafWeb.WebCms.Models { - public class ResultItemViewModel + public class QueueItemViewModel { public int LeafInputId { get; set; } public string LeafInputName { get; set; } @@ -15,16 +15,18 @@ namespace LeafWeb.WebCms.Models public bool IsRunning { get; set; } public bool IsComplete { get; set; } public bool IsDeletable { get; set; } + public bool IsCancellable { get; set; } public bool IsPending { get; set; } + public bool HasOutputFiles { get; set; } public string CurrentStatus { get; set; } //public string[] ErrorMessages { get; set; } //public string[] LeafOutputFilenames { get; set; } //public bool HasLeafChartOutputFile { get; set; } - static ResultItemViewModel() + static QueueItemViewModel() { - Mapper.CreateMap() + Mapper.CreateMap() .ForMember(dest => dest.LeafInputId, opt => opt.MapFrom(src => src.Id)) .ForMember(dest => dest.HasLeafChart, opt => opt.ResolveUsing(src => src.OutputFiles.Any(o => o.IsLeafChartFile))) //.ForMember(dest => dest.LeafOutputFilenames, @@ -52,7 +54,7 @@ namespace LeafWeb.WebCms.Models ; } - public ResultItemViewModel(LeafInput leafInput) + public QueueItemViewModel(LeafInput leafInput) { Mapper.Map(leafInput, this); } diff --git a/WebCms/Models/QueueViewModel.cs b/WebCms/Models/QueueViewModel.cs index e9cf045..5a2586a 100644 --- a/WebCms/Models/QueueViewModel.cs +++ b/WebCms/Models/QueueViewModel.cs @@ -6,6 +6,6 @@ namespace LeafWeb.WebCms.Models { public string ServerDescription { get; set; } public string ServerStatus { get; set; } - public IEnumerable Items { get; set; } + public IEnumerable Items { get; set; } } } \ No newline at end of file diff --git a/WebCms/Services/EmailNotificationService.cs b/WebCms/Services/EmailNotificationService.cs index b5b02e3..a753a69 100644 --- a/WebCms/Services/EmailNotificationService.cs +++ b/WebCms/Services/EmailNotificationService.cs @@ -20,6 +20,7 @@ namespace LeafWeb.WebCms.Services private const string EmailSuccessSubject = "LeafWeb results"; private const string EmailErrorSubject = "LeafWeb processing error"; private const string EmailSystemErrorSubject = "LeafWeb system error"; + private const string EmailCancelledSubject = "LeafWeb cancelled"; /// /// Comma separated values @@ -32,6 +33,11 @@ namespace LeafWeb.WebCms.Services private readonly DownloadUrlService _downloadUrlService; + private string FormatSubject(string subject, LeafInput leafInput) + { + return subject + $" - '{leafInput.Identifier}'"; + } + public EmailNotificationService(DataService dataService) { _dataService = dataService; @@ -60,8 +66,10 @@ namespace LeafWeb.WebCms.Services var leafInput = _dataService.GetLeafInput(leafInputId); if (leafInput.CurrentStatus != LeafInputStatusType.Complete) { - Logger.Error($"Attempting to SendLeafWebComplete when status is not complete for leafInput: {leafInput}, current status: {leafInput.CurrentStatus}"); - throw new ArgumentException($"Attempting to SendLeafWebComplete when status is not complete for leafInput: {leafInput}, current status: {leafInput.CurrentStatus}"); + var notComplete = "Attempting to SendLeafWebComplete when status is not complete" + + $" for leafInput: {leafInput}, current status: {leafInput.CurrentStatus}"; + Logger.Error(notComplete); + throw new ArgumentException(notComplete); } var outputErrorMessage = leafInput.OutputErrorMessage; @@ -71,6 +79,24 @@ namespace LeafWeb.WebCms.Services SendLeafWebSuccess(leafInput); } + public void SendLeafWebCancelled(int leafInputId) + { + var leafInput = _dataService.GetLeafInput(leafInputId); + if (leafInput.CurrentStatus != LeafInputStatusType.Cancelled) + { + var notComplete = "Attempting to SendLeafWebCancelled when status is not complete " + + $"for leafInput: {leafInput}, current status: {leafInput.CurrentStatus}"; + Logger.Error(notComplete); + throw new ArgumentException(notComplete); + } + + var body = $"Your leaf analysis job, {leafInput.Identifier}, has been cancelled. " + + "Contact the administrator with any questions."; + + var message = new MailMessage(_emailFromAddress, leafInput.Email, FormatSubject(EmailCancelledSubject, leafInput), body); + SendMessage(message); + } + public void SendAdministratorMessage(string subject, string body) { var message = new MailMessage(_emailFromAddress, _adminEmailAddresses, subject, body); @@ -94,7 +120,7 @@ namespace LeafWeb.WebCms.Services + Environment.NewLine + Environment.NewLine + downloadUrl; - var message = new MailMessage(_emailFromAddress, leafInput.Email, EmailSuccessSubject, body); + var message = new MailMessage(_emailFromAddress, leafInput.Email, FormatSubject(EmailSuccessSubject, leafInput), body); SendMessage(message); } else @@ -138,7 +164,7 @@ namespace LeafWeb.WebCms.Services body += FormatWarningMessage(leafInput); - var message = new MailMessage(_emailFromAddress, leafInput.Email, EmailErrorSubject, body); + var message = new MailMessage(_emailFromAddress, leafInput.Email, FormatSubject(EmailErrorSubject, leafInput), body); SendMessage(message); } diff --git a/WebCms/Services/PiscalQueue/PiscalQueueManager.cs b/WebCms/Services/PiscalQueue/PiscalQueueManager.cs index a51f48c..22626ae 100644 --- a/WebCms/Services/PiscalQueue/PiscalQueueManager.cs +++ b/WebCms/Services/PiscalQueue/PiscalQueueManager.cs @@ -19,11 +19,13 @@ namespace LeafWeb.WebCms.Services.PiscalQueue try { + UpdateCancelling(); + + StartCancelPending(); + UpdateRunning(); StartNextPending(); - - // TODO: handle starting and finishing } finally { @@ -38,27 +40,100 @@ namespace LeafWeb.WebCms.Services.PiscalQueue } } - // TODO: clear any stalled processes - private void ClearStalled() + private void StartCancelPending() { - var leafInputs = + var cancelPendingLeafInputs = DataService.GetLeafInputs( - LeafInputStatusType.Starting, - LeafInputStatusType.Finishing - ) - .Where(li => - li.StatusHistory.OrderBy(sh => sh.DateTime).First().DateTime - > DateTime.Now.Subtract(TimeSpan.FromHours(1))) - .ToList(); + LeafInputStatusType.CancelPending + ).ToList(); + + foreach (var leafInput in cancelPendingLeafInputs) + { + try + { + var status = PiscalService.GetStatus(leafInput); + switch (status) + { + case PiscalStatus.Running: + Logger.DebugFormat("LeafInput: {0}, Set Cancelling", leafInput.Id); + DataService.SetLeafInputStatus(leafInput, LeafInputStatusType.Cancelling); + + Logger.InfoFormat("LeafInput: {0}, Kill", leafInput.Id); + PiscalService.Kill(leafInput); + + break; + case PiscalStatus.Complete: + Logger.DebugFormat("LeafInput: {0}, Piscal Complete after cancelled - " + + "setting to Running to copy output and notify user", leafInput.Id); + DataService.SetLeafInputStatus(leafInput, LeafInputStatusType.Running, + "Piscal Complete after cancelled - setting to Running to copy output and notify user"); + break; + } + } + catch (PiscalClientException ex) + { + PiscalExceptionHandler(ex, leafInput); + } + catch (Exception ex) + { + var errorMessage = FormatException(ex); + Logger.Error(errorMessage); + } + } } + private void UpdateCancelling() + { + var cancellingLeafInputs = + DataService.GetLeafInputs( + LeafInputStatusType.Cancelling + ).ToList(); + + foreach (var leafInput in cancellingLeafInputs) + { + try + { + var status = PiscalService.GetStatus(leafInput); + switch (status) + { + case PiscalStatus.Running: + Logger.InfoFormat("LeafInput: {0}, Piscal Running - still cancelling", leafInput.Id); + // continue running + break; + + case PiscalStatus.Complete: + Logger.DebugFormat("LeafInput: {0}, Set Cancelled", leafInput.Id); + DataService.SetLeafInputStatus(leafInput, LeafInputStatusType.Cancelled, + "Emailing cancellation notification to user and cleaning up files on Piscal", + $"Email: \'{leafInput.Email}\'"); + + BackgroundJobEnqueueRetry(email => email.SendLeafWebCancelled(leafInput.Id)); + + Logger.InfoFormat("LeafInput: {0}, Cleanup", leafInput.Id); + PiscalService.Cleanup(leafInput); + + break; + } + } + catch (PiscalClientException ex) + { + PiscalExceptionHandler(ex, leafInput); + } + catch (Exception ex) + { + var errorMessage = FormatException(ex); + Logger.Error(errorMessage); + } + } + } + private void StartNextPending() { var runningLeafInputs = DataService.GetLeafInputs( LeafInputStatusType.Starting, LeafInputStatusType.Running, - LeafInputStatusType.Finishing + LeafInputStatusType.Finishing ).ToList(); if (runningLeafInputs.Any()) diff --git a/WebCms/Services/PiscalQueue/PiscalService.cs b/WebCms/Services/PiscalQueue/PiscalService.cs index dc978ae..8bcde99 100644 --- a/WebCms/Services/PiscalQueue/PiscalService.cs +++ b/WebCms/Services/PiscalQueue/PiscalService.cs @@ -35,7 +35,7 @@ namespace LeafWeb.WebCms.Services.PiscalQueue if (!string.IsNullOrEmpty(_notifyCompleteUrl)) inputFile.NotifyCompleteUrl = _notifyCompleteUrl; - // TODO: remove this, just for testing + // NOTE: Assume that from this address we don't want to store it if (string.Equals(leafInput.Email, "james.kolpack@gmail.com", StringComparison.InvariantCultureIgnoreCase)) inputFile.SuppressStorageCopy = true; _piscalClient.RunLeafInput(inputFile); @@ -64,5 +64,11 @@ namespace LeafWeb.WebCms.Services.PiscalQueue var input = new PiscalLeafInput(leafInput); _piscalClient.CleanupLeafProcess(input); } + + public void Kill(LeafInput leafInput) + { + var input = new PiscalLeafInput(leafInput); + _piscalClient.KillLeafProcess(input); + } } } \ No newline at end of file diff --git a/WebCms/Views/Queue/Details.cshtml b/WebCms/Views/Queue/Details.cshtml index a27180e..101c8d5 100644 --- a/WebCms/Views/Queue/Details.cshtml +++ b/WebCms/Views/Queue/Details.cshtml @@ -13,7 +13,7 @@ disabled}}" role="button"> Download ToUser @@ -22,7 +22,7 @@ "SendUserDownloadLink", null, new { @class = "confirm", confirm_msg = "Confirm sending email to user" })) { - } diff --git a/WebCms/Views/Queue/Index.cshtml b/WebCms/Views/Queue/Index.cshtml index 4ee9ac5..c1a5144 100644 --- a/WebCms/Views/Queue/Index.cshtml +++ b/WebCms/Views/Queue/Index.cshtml @@ -32,6 +32,11 @@ @@ -87,6 +92,11 @@ @Html.Partial("DisplayTemplates/_DeleteForm", (Tuple)Tuple.Create(item.LeafInputId, item.LeafInputIdentifier, item.IsDeletable)) } +@helper CancelLink(dynamic item) +{ +@Html.Partial("DisplayTemplates/_CancelForm", (Tuple)Tuple.Create(item.LeafInputId, item.LeafInputIdentifier)) +} + @helper DisableItem(bool disabled) { if (disabled) {class="disabled"} diff --git a/WebCms/Views/Results/Index.cshtml b/WebCms/Views/Results/Index.cshtml index 6e218d0..bec63d0 100644 --- a/WebCms/Views/Results/Index.cshtml +++ b/WebCms/Views/Results/Index.cshtml @@ -1,4 +1,4 @@ -@model IEnumerable +@model IEnumerable @{ var grid = new WebGrid(Model, rowsPerPage: 45); diff --git a/WebCms/Views/Shared/DisplayTemplates/_CancelForm.cshtml b/WebCms/Views/Shared/DisplayTemplates/_CancelForm.cshtml new file mode 100644 index 0000000..dcafe63 --- /dev/null +++ b/WebCms/Views/Shared/DisplayTemplates/_CancelForm.cshtml @@ -0,0 +1,14 @@ +@using LeafWeb.WebCms.Controllers +@model Tuple +@{ + var leafInputId = Model.Item1; + var identifier = Model.Item2; +} +@using (Html.BeginUmbracoForm("Cancel", null, + new { @class = "confirm", confirm_msg = "Cancelling cannot be undone! Confirm cancelling '" + identifier + "'." })) +{ + + +} \ No newline at end of file diff --git a/WebCms/Views/Shared/DisplayTemplates/_LeafInputStatus.cshtml b/WebCms/Views/Shared/DisplayTemplates/_LeafInputStatus.cshtml index eb3a5b3..40e1198 100644 --- a/WebCms/Views/Shared/DisplayTemplates/_LeafInputStatus.cshtml +++ b/WebCms/Views/Shared/DisplayTemplates/_LeafInputStatus.cshtml @@ -1,2 +1,3 @@ -@model string -@Model \ No newline at end of file +@using LeafWeb.Core.Utility +@model string +@Model.SplitCamelCase() \ No newline at end of file diff --git a/WebCms/Web.config b/WebCms/Web.config index 55be5f8..3d9ffe5 100644 --- a/WebCms/Web.config +++ b/WebCms/Web.config @@ -67,7 +67,7 @@ - + @@ -341,6 +341,17 @@ + + + + + + + + + + + diff --git a/WebCms/WebCms.csproj b/WebCms/WebCms.csproj index adbad86..dc9b6e3 100644 --- a/WebCms/WebCms.csproj +++ b/WebCms/WebCms.csproj @@ -887,6 +887,7 @@ + Web.config @@ -923,7 +924,7 @@ - + diff --git a/WebCms/scripts/LeafInputCreate.js b/WebCms/scripts/LeafInputCreate.js index 9e80709..0dcab21 100644 --- a/WebCms/scripts/LeafInputCreate.js +++ b/WebCms/scripts/LeafInputCreate.js @@ -4,10 +4,13 @@ $("form#create").data("validator").settings.submitHandler = function (form) { var identifier = $(form).find("input[name='Identifier']").val(); - $("
" + "Confirm submitting " + identifier + "
") + $("
" + "Please confirm submitting '" + identifier + "'
") .dialog({ buttons: { "Confirm": function () { + $("#confirmCreate") + .next(".ui-dialog-buttonpane button:contains('Confirm')") + .attr("disabled", true); form.submit(); }, "Cancel": function () { diff --git a/WebCms/scripts/site.js b/WebCms/scripts/site.js index 5728231..231f265 100644 --- a/WebCms/scripts/site.js +++ b/WebCms/scripts/site.js @@ -13,7 +13,7 @@ text = confirmMsg; } $("
" + text + "
") - .dialog({ + .dialog({ buttons: { "Confirm": function () { form.submit();