Add cancel

This commit is contained in:
2017-01-26 08:36:54 -05:00
parent 295b40bed8
commit 4338b4fee5
30 changed files with 333 additions and 62 deletions
+9 -2
View File
@@ -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);
}
}
}
+18 -6
View File
@@ -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; }
+4 -1
View File
@@ -7,6 +7,9 @@ namespace LeafWeb.Core.Entities
Running = 2,
Finishing = 3,
Complete = 4,
Exception = 5
Exception = 5,
CancelPending = 6,
Cancelling = 7,
Cancelled = 8
}
}
+1
View File
@@ -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);
}
}
+14
View File
@@ -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;
}
}
}
+19 -4
View File
@@ -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
View File
@@ -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
+10
View File
@@ -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; }
+1 -1
View File
@@ -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;}
+17
View File
@@ -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 {
+7 -6
View File
@@ -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
View File
@@ -1,5 +1,6 @@
namespace LeafWeb.WebCms.Controllers
{
// Umbraco page IDs for LeafWeb application pages
public static class LeafWebPageIds
{
public const int ManageQueue = 1107;
+39 -1
View File
@@ -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)
{
+1 -1
View File
@@ -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);
}
+6 -3
View File
@@ -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);
}
+1 -1
View File
@@ -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; }
}
}
+30 -4
View File
@@ -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())
+7 -1
View File
@@ -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);
}
}
}
+2 -2
View File
@@ -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>
}
+11 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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">
+2 -1
View File
@@ -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 -1
View File
@@ -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 () {
+1 -1
View File
@@ -13,7 +13,7 @@
text = confirmMsg;
}
$("<div>" + text + "</div>")
.dialog({
.dialog({
buttons: {
"Confirm": function () {
form.submit();