diff --git a/Core/Remote/IPiscalClient.cs b/Core/Remote/IPiscalClient.cs index 7053c04..c3bb407 100644 --- a/Core/Remote/IPiscalClient.cs +++ b/Core/Remote/IPiscalClient.cs @@ -8,5 +8,6 @@ namespace LeafWeb.Core.Remote PiscalStatus GetLeafInputFileStatus(PiscalLeafInputFile file); IEnumerable RetrieveLeafOutput(PiscalLeafInputFile file); void CleanupLeafProcess(PiscalLeafInputFile file); + string GetErrorMessage(PiscalLeafInputFile file); } } diff --git a/Core/Remote/PiscalSshClient.cs b/Core/Remote/PiscalSshClient.cs index c54db5c..5db655c 100644 --- a/Core/Remote/PiscalSshClient.cs +++ b/Core/Remote/PiscalSshClient.cs @@ -16,6 +16,7 @@ namespace LeafWeb.Core.Remote private const string StatusSuccess = "success"; private const string StatusRunning = "running"; + private const string StatusNotStarted = "not started"; private const string StatusError = "error"; public PiscalSshClient(string connectionString) @@ -62,7 +63,7 @@ namespace LeafWeb.Core.Remote ssh.Disconnect(); if (command.ExitStatus != 0) - throw new PiscalClientException(command.Error); + throw new PiscalClientException(command.Result); Console.Write(command.Result); } @@ -78,6 +79,8 @@ namespace LeafWeb.Core.Remote return PiscalStatus.Running; case StatusSuccess: return PiscalStatus.Success; + case StatusNotStarted: + return PiscalStatus.NotStarted; default: return PiscalStatus.Error; } @@ -94,7 +97,7 @@ namespace LeafWeb.Core.Remote ssh.Disconnect(); if (command.ExitStatus != 0) - throw new PiscalClientException(command.Error); + throw new PiscalClientException(command.Result); return command.Result .SplitNewLine() @@ -137,6 +140,16 @@ namespace LeafWeb.Core.Remote } } + public string GetErrorMessage(PiscalLeafInputFile file) + { + var status = GetLeafInputStatusRaw(file); + if (status[0] != StatusError) + return string.Empty; + + var errorLines = status.Skip(1).ToArray(); + return errorLines.Join(Environment.NewLine); + } + public void CleanupLeafProcess(PiscalLeafInputFile file) { var status = GetLeafInputStatusRaw(file); diff --git a/Core/Remote/PiscalStatus.cs b/Core/Remote/PiscalStatus.cs index c06582c..47f77a0 100644 --- a/Core/Remote/PiscalStatus.cs +++ b/Core/Remote/PiscalStatus.cs @@ -4,6 +4,7 @@ namespace LeafWeb.Core.Remote { Running, Success, + NotStarted, Error } } \ No newline at end of file diff --git a/Core/Utility/StringExtensions.cs b/Core/Utility/StringExtensions.cs index 9545f76..6027e4f 100644 --- a/Core/Utility/StringExtensions.cs +++ b/Core/Utility/StringExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.RegularExpressions; @@ -50,5 +51,10 @@ namespace LeafWeb.Core.Utility { return Regex.Replace(path, @".*/([^/]*$)", "$1"); } + + public static string Join(this IEnumerable enumerable, string separator) + { + return string.Join(separator, enumerable); + } } } diff --git a/Web.Tests/ViewModels/LeafOutput/LeafOutputViewModelTests.cs b/Web.Tests/ViewModels/LeafOutput/LeafOutputViewModelTests.cs index aed37b5..6c3ccde 100644 --- a/Web.Tests/ViewModels/LeafOutput/LeafOutputViewModelTests.cs +++ b/Web.Tests/ViewModels/LeafOutput/LeafOutputViewModelTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using LeafWeb.Core.Entities; using LeafWeb.Web.ViewModels.LeafOutput; using NUnit.Framework; @@ -8,34 +9,71 @@ namespace LeafWeb.Web.Tests.ViewModels.LeafOutput [TestFixture] public class LeafOutputViewModelTests { - private LeafInputFile file = new LeafInputFile + private LeafInputFile GetLeafInputFile() { - Filename = "MyFilename.ext", - Id = 3, - CurrentStatus = LeafInputStatusType.Running, - LeafInput = new LeafInput + return new LeafInputFile { - Added = DateTime.Today, - Email = "test@email.com", - Identifier = "Ident I Fier", - Name = "My Name", - PhotosynthesisType = new PhotosynthesisType { Id = "1", Name = "1", SortOrder = 1} - }, - LeafOutputFiles = new[] { new LeafOutputFile { Filename = "OutputFilename.txt" } } - }; + Filename = "MyFilename.ext", + Id = 3, + CurrentStatus = LeafInputStatusType.Complete, + LeafInput = new LeafInput + { + Added = DateTime.Today, + Email = "test@email.com", + Identifier = "Ident I Fier", + Name = "My Name", + PhotosynthesisType = new PhotosynthesisType {Id = "1", Name = "1", SortOrder = 1} + }, + LeafOutputFiles = new[] {new LeafOutputFile {Filename = "OutputFilename.txt"}} + }; + } [Test] public void CanConstructFromLeafInputFile() { + var file = GetLeafInputFile(); var viewModel = new LeafOutputViewModel(file); Assert.That(viewModel.CurrentStatus, Is.EqualTo(file.CurrentStatus.ToString())); Assert.That(viewModel.LeafInputFileId, Is.EqualTo(file.Id)); Assert.That(viewModel.LeafInputFilename, Is.EqualTo(file.Filename)); - Assert.That(viewModel.LeafOutputFilenames, Is.Empty); + Assert.That(viewModel.LeafOutputFilenames, Has.Length.EqualTo(1)); Assert.That(viewModel.LeafInputIdentifier, Is.EqualTo(file.LeafInput.Identifier)); Assert.That(viewModel.LeafInputSiteId, Is.EqualTo(file.LeafInput.SiteId)); Assert.That(viewModel.LeafInputPhotosynthesisType, Is.EqualTo(file.LeafInput.PhotosynthesisType.Name)); } + + [Test] + public void CanConstructFromLeafInputFile_Running() + { + var file = GetLeafInputFile(); + file.CurrentStatus = LeafInputStatusType.Running; + file.LeafOutputFiles = new LeafOutputFile[0]; + var viewModel = new LeafOutputViewModel(file); + + Assert.That(viewModel.CurrentStatus, Is.EqualTo(LeafInputStatusType.Running.ToString())); + Assert.That(viewModel.LeafOutputFilenames, Has.Length.EqualTo(0)); + } + + [Test] + public void CanConstructFromLeafInputFile_Error() + { + var file = GetLeafInputFile(); + file.CurrentStatus = LeafInputStatusType.Error; + file.StatusHistory = new [] + { + new LeafInputFileStatus + { + DateTime = DateTime.Today, + LeafInputFile = file, + Description = "My Error", + Status = LeafInputStatusType.Error + } + }; + var viewModel = new LeafOutputViewModel(file); + + Assert.That(viewModel.CurrentStatus, Is.EqualTo(LeafInputStatusType.Error.ToString())); + Assert.That(viewModel.ErrorMessages[0], Is.EqualTo(file.StatusHistory.First().Description)); + } } } diff --git a/Web/Controllers/LeafOutputController.cs b/Web/Controllers/LeafOutputController.cs index 1029c9f..13e323e 100644 --- a/Web/Controllers/LeafOutputController.cs +++ b/Web/Controllers/LeafOutputController.cs @@ -1,4 +1,6 @@ +using System.Linq; using System.Web.Mvc; +using LeafWeb.Web.ViewModels.LeafOutput; namespace LeafWeb.Web.Controllers { @@ -6,7 +8,11 @@ namespace LeafWeb.Web.Controllers { public ActionResult Index() { - var viewModel = DataService.GetLeafOutputFiles(); + var viewModel = + DataService.GetLeafInputFiles() + .OrderByDescending(f => f.Id) + .ToList() + .Select(f => new LeafOutputViewModel(f)); return View(viewModel); } } diff --git a/Web/NLog.config b/Web/NLog.config index dbc8f5e..e044f2e 100644 --- a/Web/NLog.config +++ b/Web/NLog.config @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/Web/Services/PiscalQueueManager.cs b/Web/Services/PiscalQueueManager.cs index eafe954..d736375 100644 --- a/Web/Services/PiscalQueueManager.cs +++ b/Web/Services/PiscalQueueManager.cs @@ -31,13 +31,18 @@ namespace LeafWeb.Web.Services { logger.Trace("ProcessQueue entered"); - ProcessRunning(logger); + try + { + ProcessRunning(logger); - ProcessQueue(logger); + ProcessQueue(logger); + } + finally + { + logger.Trace("ProcessQueue exit"); - logger.Trace("ProcessQueue completed"); - - Monitor.Exit(ProcessQueueLock); + Monitor.Exit(ProcessQueueLock); + } } else { @@ -54,14 +59,25 @@ namespace LeafWeb.Web.Services var queuedFile = _dataService .GetLeafInputFiles(LeafInputStatusType.Queued) - .OrderBy(l => l.Id) + .OrderBy(l => l.StatusHistory.Min(sh => sh.DateTime)) .FirstOrDefault(); if (queuedFile == null) return; logger.Info("LeafInputFile: {0}, Start", queuedFile.Id); - _piscalService.Run(queuedFile); + try + { + _piscalService.Run(queuedFile); + } + catch (PiscalClientException ex) + { + logger.Error("LeafInputFile: {0}, ProcessQueue Exception: {1}", queuedFile.Id, ex.Message); + _dataService.SetLeafInputFileStatus(queuedFile, LeafInputStatusType.Error, "Error occurred submitting LeafInput"); + + // TODO: re-queue + //_dataService.SetLeafInputFileStatus(queuedFile, LeafInputStatusType.Queued, "Re-queuing LeafInput"); + } _dataService.SetLeafInputFileStatus(queuedFile, LeafInputStatusType.Running); } @@ -71,33 +87,55 @@ namespace LeafWeb.Web.Services foreach (var file in runningLeafInputFiles) { var status = _piscalService.GetStatus(file); - switch (status) + try { - case PiscalStatus.Running: - logger.Trace("LeafInputFile: {0}, {1}", file.Id, status); - // continue running - break; - case PiscalStatus.Success: - logger.Info("LeafInputFile: {0}, {1}", file.Id, status); - // collect the leaf output - var leafOutputFiles = _piscalService.RetrieveOutputFiles(file).ToList(); - foreach (var outputFile in leafOutputFiles) - _dataService.AddLeafOutputFile(outputFile); + switch (status) + { + case PiscalStatus.Running: + logger.Trace("LeafInputFile: {0}, Running", file.Id); + // continue running + break; - logger.Info("LeafInputFile: {0}, output files: {1}", file.Id, - string.Join(", ", leafOutputFiles.Select(o => o.Filename))); + case PiscalStatus.Success: + logger.Info("LeafInputFile: {0}, Success", file.Id); + // collect the leaf output + var leafOutputFiles = _piscalService.RetrieveOutputFiles(file).ToList(); + foreach (var outputFile in leafOutputFiles) + _dataService.AddLeafOutputFile(outputFile); - // update db - _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Complete); + logger.Info("LeafInputFile: {0}, output files: {1}", file.Id, + string.Join(", ", leafOutputFiles.Select(o => o.Filename))); - // remove working data from the server - logger.Info("LeafInputFile: {0}, cleanup", file.Id); - _piscalService.Cleanup(file); - break; - case PiscalStatus.Error: - logger.Info("LeafInputFile: {0}, error", file.Id); - _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Error); - break; + // update db + _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Complete); + + // remove working data from the server + logger.Info("LeafInputFile: {0}, Cleanup", file.Id); + _piscalService.Cleanup(file); + break; + + case PiscalStatus.NotStarted: + logger.Warn("LeafInputFile: {0}, Not Started, re-queueing", file.Id); + // if it's not started, try to requeue the process - unusual state + _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Queued); + break; + + case PiscalStatus.Error: + logger.Info("LeafInputFile: {0}, Error", file.Id); + + var errorMessage = _piscalService.GetErrorMessage(file); + logger.Info("LeafInputFile: {0}, Error Message: {1}", file.Id, errorMessage); + + _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Error, errorMessage); + break; + } + } + catch (PiscalClientException ex) + { + logger.Error("LeafInputFile: {0}, ProcessRunning Exception: {1}", file.Id, ex.Message); + _dataService.SetLeafInputFileStatus(file, LeafInputStatusType.Error, "Error occurred processing LeafInput"); + + // TODO: re-queue } } } diff --git a/Web/Services/PiscalService.cs b/Web/Services/PiscalService.cs index 41ef30b..145262b 100644 --- a/Web/Services/PiscalService.cs +++ b/Web/Services/PiscalService.cs @@ -42,6 +42,12 @@ namespace LeafWeb.Web.Services } } + public string GetErrorMessage(LeafInputFile leafInputFile) + { + var inputFile = new PiscalLeafInputFile(leafInputFile); + return _piscalClient.GetErrorMessage(inputFile); + } + public void Cleanup(LeafInputFile leafInputFile) { var inputFile = new PiscalLeafInputFile(leafInputFile); diff --git a/Web/ViewModels/LeafOutput/LeafOutputViewModel.cs b/Web/ViewModels/LeafOutput/LeafOutputViewModel.cs index 0b5d04a..b4d19bc 100644 --- a/Web/ViewModels/LeafOutput/LeafOutputViewModel.cs +++ b/Web/ViewModels/LeafOutput/LeafOutputViewModel.cs @@ -1,5 +1,6 @@ using System.Linq; using AutoMapper; +using LeafWeb.Core.Entities; namespace LeafWeb.Web.ViewModels.LeafOutput { @@ -10,7 +11,9 @@ namespace LeafWeb.Web.ViewModels.LeafOutput public int LeafInputFileId { get; set; } public string LeafInputFilename { get; set; } public string CurrentStatus { get; set; } + public string[] ErrorMessages { get; set; } public string[] LeafOutputFilenames { get; set; } + public string LeafInputName { get; set; } public string LeafInputIdentifier { get; set; } public string LeafInputSiteId { get; set; } public string LeafInputPhotosynthesisType { get; set; } @@ -20,19 +23,33 @@ namespace LeafWeb.Web.ViewModels.LeafOutput var config = new MapperConfiguration(cfg => { - cfg.CreateMap() + cfg.CreateMap() .ForMember(dest => dest.LeafInputFileId, opt => opt.MapFrom(src => src.Id)) .ForMember(dest => dest.LeafInputFilename, opt => opt.MapFrom(src => src.Filename)) .ForMember(dest => dest.LeafOutputFilenames, - opt => opt.ResolveUsing(file => file.LeafOutputFiles?.Select(o => o.Filename).ToArray() ?? new string[] {})) + opt => opt.ResolveUsing( + file => + file.LeafOutputFiles? + .Select(o => o.Filename) + .ToArray() + ?? new string[] {})) + .ForMember(dest => dest.LeafInputName, opt => opt.MapFrom(src => src.LeafInput.Name)) .ForMember(dest => dest.LeafInputIdentifier, opt => opt.MapFrom(src => src.LeafInput.Identifier)) .ForMember(dest => dest.LeafInputSiteId, opt => opt.MapFrom(src => src.LeafInput.SiteId)) - .ForMember(dest => dest.LeafInputPhotosynthesisType, opt => opt.MapFrom(src => src.LeafInput.PhotosynthesisType.Name)); + .ForMember(dest => dest.LeafInputPhotosynthesisType, opt => opt.MapFrom(src => src.LeafInput.PhotosynthesisType.Name)) + .ForMember(dest => dest.ErrorMessages, + opt => opt.ResolveUsing( + src => + src.StatusHistory? + .Where(sh => sh.Status == LeafInputStatusType.Error) + .Select(sh => sh.Description) + .ToArray() + ?? new string[] {})); }); Mapper = config.CreateMapper(); } - public LeafOutputViewModel(Core.Entities.LeafInputFile leafInput) + public LeafOutputViewModel(LeafInputFile leafInput) { Mapper.Map(leafInput, this); } diff --git a/Web/Views/LeafOutput/Index.cshtml b/Web/Views/LeafOutput/Index.cshtml index b48cb3c..9890038 100644 --- a/Web/Views/LeafOutput/Index.cshtml +++ b/Web/Views/LeafOutput/Index.cshtml @@ -1,12 +1,19 @@ -@model LeafWeb.Web.ViewModels.LeafOutput.LeafOutputViewModel +@model IEnumerable @{ - ViewBag.Title = "Users"; + ViewBag.Title = "Results"; var grid = new WebGrid(Model, rowsPerPage: 45); } -

EDO Results

+

Results

-
- -
+@grid.GetHtml(columns: + grid.Columns( + grid.Column("LeafInputIdentifier", "Identifier"), + grid.Column("LeafInputSiteId", "Site Id"), + grid.Column("LeafInputFilename", "Filename"), + grid.Column("LeafInputName", "Submitted By"), + grid.Column("CurrentStatus", "Status") + ), + htmlAttributes: new { @class = "table table-striped table-bordered table-hover table-condensed" } + )