Add cancel
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,26 @@ namespace LeafWeb.Core.Entities
|
||||
public LeafInputStatusType CurrentStatus { get; set; }
|
||||
public virtual ICollection<LeafInputStatus> 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; }
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace LeafWeb.Core.Entities
|
||||
Running = 2,
|
||||
Finishing = 3,
|
||||
Complete = 4,
|
||||
Exception = 5
|
||||
Exception = 5,
|
||||
CancelPending = 6,
|
||||
Cancelling = 7,
|
||||
Cancelled = 8
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ namespace LeafWeb.Core.Remote
|
||||
void RunLeafInput(PiscalLeafInput leafInput);
|
||||
PiscalStatus GetLeafInputStatus(PiscalLeafInput leafInput);
|
||||
IEnumerable<PiscalLeafOutputFile> RetrieveLeafOutput(PiscalLeafInput leafInput);
|
||||
void KillLeafProcess(PiscalLeafInput leafInput);
|
||||
void CleanupLeafProcess(PiscalLeafInput leafInput);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -1 +1 @@
|
||||
C:\Users\poprhythm\AppData\Local\Temp\Temporary ASP.NET Files\vs\f80e29bb\faae20bf\App_Web_all.generated.cs.8f9494c4.rzzuwtuu.dll
|
||||
C:\Users\poprhythm\AppData\Local\Temp\Temporary ASP.NET Files\vs\f80e29bb\faae20bf\App_Web_all.generated.cs.8f9494c4._retrxtu.dll
|
||||
@@ -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; }
|
||||
|
||||
Vendored
+1
-1
@@ -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;}
|
||||
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;}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace LeafWeb.WebCms.Controllers
|
||||
{
|
||||
// Umbraco page IDs for LeafWeb application pages
|
||||
public static class LeafWebPageIds
|
||||
{
|
||||
public const int ManageQueue = 1107;
|
||||
|
||||
@@ -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<EmailNotificationService>(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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<LeafInputStatusViewModel> 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<LeafInputFile, string>().ConvertUsing(file => file?.Contents.GetString());
|
||||
|
||||
@@ -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<LeafInput, ResultItemViewModel>()
|
||||
Mapper.CreateMap<LeafInput, QueueItemViewModel>()
|
||||
.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);
|
||||
}
|
||||
@@ -6,6 +6,6 @@ namespace LeafWeb.WebCms.Models
|
||||
{
|
||||
public string ServerDescription { get; set; }
|
||||
public string ServerStatus { get; set; }
|
||||
public IEnumerable<ResultItemViewModel> Items { get; set; }
|
||||
public IEnumerable<QueueItemViewModel> Items { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
/// <summary>
|
||||
/// 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<EmailNotificationService>(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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
</a>
|
||||
|
||||
<a href="@Url.Action("DownloadOutputToUser", "Queue", new {id = Model.LeafInputId})"
|
||||
class="btn btn-default@{if (!Model.IsComplete)
|
||||
class="btn btn-default@{if (!Model.HasOutputFiles)
|
||||
{<text> disabled</text>}}" role="button">
|
||||
<span class="glyphicon glyphicon-download"></span> Download ToUser
|
||||
</a>
|
||||
@@ -22,7 +22,7 @@
|
||||
"SendUserDownloadLink", null, new { @class = "confirm", confirm_msg = "Confirm sending email to user" }))
|
||||
{
|
||||
<input type="hidden" name="id" value="@Model.LeafInputId" />
|
||||
<button type="submit" class="btn btn-default" @{if (!Model.IsComplete) { <text> disabled="disabled" </text> }}>
|
||||
<button type="submit" class="btn btn-default" @{if (!Model.HasOutputFiles) { <text> disabled="disabled" </text> }}>
|
||||
<span class="glyphicon glyphicon-send"></span> Email User Download link
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
<ul class="dropdown-menu">
|
||||
<li>@DetailsLink(item)</li>
|
||||
<li @DisableItem(!item.HasLeafChart)>@ChartLink(item)</li>
|
||||
@if (item.IsCancellable)
|
||||
{
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>@CancelLink(item)</li>
|
||||
}
|
||||
@*<li role="separator" class="divider"></li>
|
||||
<li class="dropdown-header">Priority</li>
|
||||
<li>@SetPriorityHigh(item, "Set High")</li>
|
||||
@@ -39,7 +44,7 @@
|
||||
<li role="separator" class="divider"></li>
|
||||
<li class="dropdown-header">Download</li>
|
||||
<li>@DownloadInput(item)</li>
|
||||
<li @DisableItem(!item.IsComplete)>@DownloadOutputToUser(item)</li>
|
||||
<li @DisableItem(!item.HasOutputFiles)>@DownloadOutputToUser(item)</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li @DisableItem(!item.IsDeletable)>@DeleteLink(item)</li>
|
||||
</ul>
|
||||
@@ -87,6 +92,11 @@
|
||||
@Html.Partial("DisplayTemplates/_DeleteForm", (Tuple<int, string, bool>)Tuple.Create(item.LeafInputId, item.LeafInputIdentifier, item.IsDeletable))
|
||||
}
|
||||
|
||||
@helper CancelLink(dynamic item)
|
||||
{
|
||||
@Html.Partial("DisplayTemplates/_CancelForm", (Tuple<int, string>)Tuple.Create(item.LeafInputId, item.LeafInputIdentifier))
|
||||
}
|
||||
|
||||
@helper DisableItem(bool disabled)
|
||||
{
|
||||
if (disabled) {<text>class="disabled"</text>}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@model IEnumerable<ResultItemViewModel>
|
||||
@model IEnumerable<QueueItemViewModel>
|
||||
|
||||
@{
|
||||
var grid = new WebGrid(Model, rowsPerPage: 45);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
@using LeafWeb.WebCms.Controllers
|
||||
@model Tuple<int, string>
|
||||
@{
|
||||
var leafInputId = Model.Item1;
|
||||
var identifier = Model.Item2;
|
||||
}
|
||||
@using (Html.BeginUmbracoForm<QueueController>("Cancel", null,
|
||||
new { @class = "confirm", confirm_msg = "Cancelling cannot be undone! Confirm cancelling '" + identifier + "'." }))
|
||||
{
|
||||
<input type="hidden" name="id" value="@leafInputId"/>
|
||||
<button type="submit" class="btn btn-link">
|
||||
<span class="glyphicon glyphicon-ban-circle"></span> Cancel
|
||||
</button>
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
@model string
|
||||
<span class="status status-@Model.ToLower()">@Model</span>
|
||||
@using LeafWeb.Core.Utility
|
||||
@model string
|
||||
<span class="status status-@Model.ToLower()">@Model.SplitCamelCase()</span>
|
||||
+12
-1
@@ -67,7 +67,7 @@
|
||||
<add key="SmtpUserName" value="" />
|
||||
<add key="SmtpPassword" value="" />
|
||||
<add key="LeafWebUrl" value="http://192.168.1.133:61755/" />
|
||||
<add key="PiscalNotifyCompleteUrlPath" value="LeafInput/NotifyComplete" />
|
||||
<add key="PiscalNotifyCompleteUrlPath" value="umbraco/surface/LeafInput/NotifyComplete" />
|
||||
<add key="ResultsDownloadPath" value="Results/Download?token={0}" />
|
||||
</appSettings>
|
||||
<connectionStrings>
|
||||
@@ -341,6 +341,17 @@
|
||||
<requestLimits maxAllowedContentLength="1000000000" />
|
||||
</requestFiltering>
|
||||
</security>
|
||||
<rewrite>
|
||||
<rules>
|
||||
<rule name="Redirect leafweb.ornl.gov to www.leafweb.org" stopProcessing="true">
|
||||
<match url=".*" />
|
||||
<conditions>
|
||||
<add input="{HTTP_HOST}" pattern="^leafweb.ornl.gov$" />
|
||||
</conditions>
|
||||
<action type="Redirect" url="http://www.leafweb.org/" redirectType="Permanent" />
|
||||
</rule>
|
||||
</rules>
|
||||
</rewrite>
|
||||
</system.webServer>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
|
||||
@@ -887,6 +887,7 @@
|
||||
<Content Include="Views\EmptyPage.cshtml" />
|
||||
<Content Include="Views\Shared\EditorTemplates\TermsOfService.cshtml" />
|
||||
<Content Include="Views\Shared\DisplayTemplates\_DeleteForm.cshtml" />
|
||||
<Content Include="Views\Shared\DisplayTemplates\_CancelForm.cshtml" />
|
||||
<None Include="Web.Debug.config">
|
||||
<DependentUpon>Web.config</DependentUpon>
|
||||
</None>
|
||||
@@ -923,7 +924,7 @@
|
||||
<Compile Include="Models\LeafInputDetails.cs" />
|
||||
<Compile Include="Models\LeafInputCreate.cs" />
|
||||
<Compile Include="Models\LeafInputStatusViewModel.cs" />
|
||||
<Compile Include="Models\ResultItemViewModel.cs" />
|
||||
<Compile Include="Models\QueueItemViewModel.cs" />
|
||||
<Compile Include="Models\QueueViewModel.cs" />
|
||||
<Compile Include="Models\SelectListViewModel.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
$("form#create").data("validator").settings.submitHandler =
|
||||
function (form) {
|
||||
var identifier = $(form).find("input[name='Identifier']").val();
|
||||
$("<div>" + "Confirm submitting " + identifier + "</div>")
|
||||
$("<div id='confirmCreate'>" + "Please confirm submitting '" + identifier + "'</div>")
|
||||
.dialog({
|
||||
buttons: {
|
||||
"Confirm": function () {
|
||||
$("#confirmCreate")
|
||||
.next(".ui-dialog-buttonpane button:contains('Confirm')")
|
||||
.attr("disabled", true);
|
||||
form.submit();
|
||||
},
|
||||
"Cancel": function () {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
text = confirmMsg;
|
||||
}
|
||||
$("<div>" + text + "</div>")
|
||||
.dialog({
|
||||
.dialog({
|
||||
buttons: {
|
||||
"Confirm": function () {
|
||||
form.submit();
|
||||
|
||||
Reference in New Issue
Block a user