From 4218725a77889c074f51c3dc5db160275a7bac77 Mon Sep 17 00:00:00 2001 From: Suha Can Date: Wed, 16 Feb 2011 17:16:15 -0800 Subject: [PATCH] RecipeJournal implementation. - IStorageProvider based journal implementation for the recipe pipeline. - Was needed as the existing reporting capability was found insufficient. --HG-- branch : recipe --- .../Recipes/Services/RecipeManagerTests.cs | 30 ++++++- .../Services/RecipeJournalManager.cs | 87 +++++++++++++++++-- .../Orchard.Recipes/Services/RecipeManager.cs | 7 +- .../Services/RecipeStepExecutor.cs | 10 ++- src/Orchard/Recipes/Models/RecipeJournal.cs | 7 +- .../Recipes/Services/IRecipeJournal.cs | 3 +- 6 files changed, 127 insertions(+), 17 deletions(-) diff --git a/src/Orchard.Tests.Modules/Recipes/Services/RecipeManagerTests.cs b/src/Orchard.Tests.Modules/Recipes/Services/RecipeManagerTests.cs index a5549c2d5..3f21d0998 100644 --- a/src/Orchard.Tests.Modules/Recipes/Services/RecipeManagerTests.cs +++ b/src/Orchard.Tests.Modules/Recipes/Services/RecipeManagerTests.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; @@ -68,6 +67,7 @@ namespace Orchard.Tests.Modules.Recipes.Services { builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); @@ -145,6 +145,28 @@ namespace Orchard.Tests.Modules.Recipes.Services { } } + public class StubRecipeJournal : IRecipeJournal { + public void ExecutionStart(string executionId) { + } + + public void ExecutionComplete(string executionId) { + } + + public void ExecutionFailed(string executionId) { + } + + public void WriteJournalEntry(string executionId, string message) { + } + + public RecipeJournal GetRecipeJournal(string executionId) { + return new RecipeJournal(); + } + + public RecipeStatus GetRecipeStatus(string executionId) { + return RecipeStatus.Complete; + } + } + public class StubStepQueue : IRecipeStepQueue { readonly Queue _queue = new Queue(); @@ -171,8 +193,12 @@ namespace Orchard.Tests.Modules.Recipes.Services { public class CustomRecipeHandler : IRecipeHandler { public static string AttributeValue; + public string[] _handles = {"Module", "Theme", "Migration", "CleanUpInactive", "Custom1", "Custom2", "Command", "Metadata", "Feature", "Settings"}; public void ExecuteRecipeStep(RecipeContext recipeContext) { + if (_handles.Contains(recipeContext.RecipeStep.Name)) { + recipeContext.Executed = true; + } if (recipeContext.RecipeStep.Name == "Custom1") { foreach (var attribute in recipeContext.RecipeStep.Step.Attributes().Where(attribute => attribute.Name == "attr1")) { AttributeValue = attribute.Value; diff --git a/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeJournalManager.cs b/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeJournalManager.cs index b0b2513e6..2c282d170 100644 --- a/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeJournalManager.cs +++ b/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeJournalManager.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Xml.Linq; using Orchard.FileSystems.Media; using Orchard.Localization; using Orchard.Logging; @@ -7,6 +10,7 @@ using Orchard.Recipes.Models; namespace Orchard.Recipes.Services { public class RecipeJournalManager : IRecipeJournal { private readonly IStorageProvider _storageProvider; + private readonly string _recipeJournalFolder = "RecipeJournal" + Path.DirectorySeparatorChar; public RecipeJournalManager(IStorageProvider storageProvider) { _storageProvider = storageProvider; @@ -18,36 +22,103 @@ namespace Orchard.Recipes.Services { public Localizer T { get; set; } ILogger Logger { get; set; } - public void StartExecution(string executionId) { - throw new NotImplementedException(); + public void ExecutionStart(string executionId) { + var executionJournal = GetJournalFile(executionId); + var xElement = XElement.Parse(ReadJournal(executionJournal)); + xElement.Element("Status").Value = "Started"; + WriteJournal(executionJournal, xElement); } public void ExecutionComplete(string executionId) { - throw new NotImplementedException(); + var executionJournal = GetJournalFile(executionId); + var xElement = XElement.Parse(ReadJournal(executionJournal)); + xElement.Element("Status").Value = "Complete"; + WriteJournal(executionJournal, xElement); } public void ExecutionFailed(string executionId) { - throw new NotImplementedException(); + var executionJournal = GetJournalFile(executionId); + var xElement = XElement.Parse(ReadJournal(executionJournal)); + xElement.Element("Status").Value = "Failed"; + WriteJournal(executionJournal, xElement); } public void WriteJournalEntry(string executionId, string message) { - throw new NotImplementedException(); + var executionJournal = GetJournalFile(executionId); + var xElement = XElement.Parse(ReadJournal(executionJournal)); + var journalEntry = new XElement("Message", message); + xElement.Add(journalEntry); + WriteJournal(executionJournal, xElement); } public RecipeJournal GetRecipeJournal(string executionId) { - throw new NotImplementedException(); + var executionJournal = GetJournalFile(executionId); + var xElement = XElement.Parse(ReadJournal(executionJournal)); + + var journal = new RecipeJournal { ExecutionId = executionId }; + var messages = new List(); + + journal.Status = ReadStatusFromJournal(xElement); + foreach (var message in xElement.Elements("Message")) { + messages.Add(new JournalMessage {Message = message.Value}); + } + journal.Messages = messages; + + return journal; + } + + public RecipeStatus GetRecipeStatus(string executionId) { + var executionJournal = GetJournalFile(executionId); + var xElement = XElement.Parse(ReadJournal(executionJournal)); + + return ReadStatusFromJournal(xElement); } private IStorageFile GetJournalFile(string executionId) { IStorageFile journalFile; + var journalPath = Path.Combine(_recipeJournalFolder, executionId); try { - journalFile = _storageProvider.GetFile(executionId); + _storageProvider.TryCreateFolder(_recipeJournalFolder); + journalFile = _storageProvider.GetFile(journalPath); } catch (ArgumentException) { - journalFile = _storageProvider.CreateFile(executionId); + journalFile = _storageProvider.CreateFile(journalPath); + var recipeStepElement = new XElement("RecipeJournal"); + recipeStepElement.Add(new XElement("Status", "Unknown")); + WriteJournal(journalFile, recipeStepElement); } return journalFile; } + + private static string ReadJournal(IStorageFile executionJournal) { + using (var stream = executionJournal.OpenRead()) { + using (var streamReader = new StreamReader(stream)) { + return streamReader.ReadToEnd(); + } + } + } + + private static void WriteJournal(IStorageFile journalFile, XElement journal) { + string content = journal.ToString(); + using (var stream = journalFile.OpenWrite()) { + using (var tw = new StreamWriter(stream)) { + tw.Write(content); + } + } + } + + private static RecipeStatus ReadStatusFromJournal(XElement xElement) { + switch (xElement.Element("Status").Value) { + case "Started": + return RecipeStatus.Started; + case "Complete": + return RecipeStatus.Complete; + case "Failed": + return RecipeStatus.Failed; + default: + return RecipeStatus.Unknown; + } + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeManager.cs b/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeManager.cs index 811c3f924..f464ff40e 100644 --- a/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeManager.cs +++ b/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeManager.cs @@ -7,10 +7,12 @@ namespace Orchard.Recipes.Services { public class RecipeManager : IRecipeManager { private readonly IRecipeStepQueue _recipeStepQueue; private readonly IRecipeScheduler _recipeScheduler; + private readonly IRecipeJournal _recipeJournal; - public RecipeManager(IRecipeStepQueue recipeStepQueue, IRecipeScheduler recipeScheduler) { + public RecipeManager(IRecipeStepQueue recipeStepQueue, IRecipeScheduler recipeScheduler, IRecipeJournal recipeJournal) { _recipeStepQueue = recipeStepQueue; _recipeScheduler = recipeScheduler; + _recipeJournal = recipeJournal; Logger = NullLogger.Instance; T = NullLocalizer.Instance; @@ -24,7 +26,8 @@ namespace Orchard.Recipes.Services { return null; var executionId = Guid.NewGuid().ToString("n"); - // TODO: Run each step inside a transaction boundary. + _recipeJournal.ExecutionStart(executionId); + foreach (var recipeStep in recipe.RecipeSteps) { _recipeStepQueue.Enqueue(executionId, recipeStep); } diff --git a/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeStepExecutor.cs b/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeStepExecutor.cs index 0a17b5056..6f3529b9c 100644 --- a/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeStepExecutor.cs +++ b/src/Orchard.Web/Modules/Orchard.Recipes/Services/RecipeStepExecutor.cs @@ -7,10 +7,12 @@ using Orchard.Recipes.Models; namespace Orchard.Recipes.Services { public class RecipeStepExecutor : IRecipeStepExecutor { private readonly IRecipeStepQueue _recipeStepQueue; + private readonly IRecipeJournal _recipeJournal; private readonly IEnumerable _recipeHandlers; - public RecipeStepExecutor(IRecipeStepQueue recipeStepQueue, IEnumerable recipeHandlers) { + public RecipeStepExecutor(IRecipeStepQueue recipeStepQueue, IRecipeJournal recipeJournal, IEnumerable recipeHandlers) { _recipeStepQueue = recipeStepQueue; + _recipeJournal = recipeJournal; _recipeHandlers = recipeHandlers; Logger = NullLogger.Instance; @@ -23,8 +25,10 @@ namespace Orchard.Recipes.Services { public bool ExecuteNextStep(string executionId) { var nextRecipeStep= _recipeStepQueue.Dequeue(executionId); if (nextRecipeStep == null) { + _recipeJournal.ExecutionComplete(executionId); return false; } + _recipeJournal.WriteJournalEntry(executionId, string.Format("Executing step {0}.", nextRecipeStep.Name)); var recipeContext = new RecipeContext { RecipeStep = nextRecipeStep, Executed = false }; try { foreach (var recipeHandler in _recipeHandlers) { @@ -34,11 +38,15 @@ namespace Orchard.Recipes.Services { catch(Exception exception) { Logger.Error(exception, "Recipe execution {0} was cancelled because a step failed to execute", executionId); while (_recipeStepQueue.Dequeue(executionId) != null) ; + _recipeJournal.ExecutionFailed(executionId); return false; } if (!recipeContext.Executed) { Logger.Error("Could not execute recipe step '{0}' because the recipe handler was not found.", recipeContext.RecipeStep.Name); + while (_recipeStepQueue.Dequeue(executionId) != null) ; + _recipeJournal.ExecutionFailed(executionId); + return false; } return true; diff --git a/src/Orchard/Recipes/Models/RecipeJournal.cs b/src/Orchard/Recipes/Models/RecipeJournal.cs index 0ac162e2b..9852df6ff 100644 --- a/src/Orchard/Recipes/Models/RecipeJournal.cs +++ b/src/Orchard/Recipes/Models/RecipeJournal.cs @@ -3,7 +3,7 @@ namespace Orchard.Recipes.Models { public class RecipeJournal { public string ExecutionId { get; set; } - public RecipeJournalStatus Status { get; set; } + public RecipeStatus Status { get; set; } public IEnumerable Messages { get; set; } } @@ -11,8 +11,9 @@ namespace Orchard.Recipes.Models { public string Message { get; set; } } - public enum RecipeJournalStatus { - Running, + public enum RecipeStatus { + Unknown, + Started, Complete, Failed } diff --git a/src/Orchard/Recipes/Services/IRecipeJournal.cs b/src/Orchard/Recipes/Services/IRecipeJournal.cs index f3f1f5670..e40949267 100644 --- a/src/Orchard/Recipes/Services/IRecipeJournal.cs +++ b/src/Orchard/Recipes/Services/IRecipeJournal.cs @@ -2,10 +2,11 @@ namespace Orchard.Recipes.Services { public interface IRecipeJournal : IDependency { - void StartExecution(string executionId); + void ExecutionStart(string executionId); void ExecutionComplete(string executionId); void ExecutionFailed(string executionId); void WriteJournalEntry(string executionId, string message); RecipeJournal GetRecipeJournal(string executionId); + RecipeStatus GetRecipeStatus(string executionId); } }