Exemple #1
0
        public async Task <TaskSetResult> Execute(List <TaskParameters> tasks, TaskEngineConfig engineConfig)
        {
            var tasksetresult = new TaskSetResult();

            #region Iterate through all tasks in collection
            foreach (var task in tasks)
            {
                try
                {
                    // Create TaskAdapter instance based on configuration object:
                    using (var t = LoadAdapter(task))
                    {
                        // Merge return values from previous step(s) into parameter set (overwriting configuration parameters, if required):
                        task.MergeParameters(tasksetresult.ReturnValues);

                        #region Execute TaskAdapter and handle result
                        var result = await t.ExecuteTask(task);

                        if (result.Success == false || result.Exceptions.Any())                         // Execution failed, or succeeded with errors
                        {
                            // Generate logging data for caller:
                            tasksetresult.LogMessage.AppendLine($"Task [{task.TaskName}] {(result.Success ? "succeeded with errors" : "execution failed")}");
                            foreach (var ex in result.Exceptions)
                            {
                                tasksetresult.LogMessage.AppendLine(ex.ToString());
                            }

                            // Update return value, unless it is already set to a higher (i.e. worse) value:
                            if (tasksetresult.ReturnValue < (result.Success ? 7 : 8))
                            {
                                tasksetresult.ReturnValue = (result.Success ? 7 : 8);
                            }
                        }
                        else                         // Execution succeeded
                        {
                            tasksetresult.LogMessage.AppendLine($"Task [{task.TaskName}] executed successfully");
                            if (tasksetresult.ReturnValue < 0)
                            {
                                tasksetresult.ReturnValue = 0;
                            }
                            // Merge any return values output by TaskAdapter into overall set result:
                            tasksetresult.MergeReturnValues(result.ReturnValues);
                        }
                        #endregion
                    }
                }
                catch (LoadAdapterException ex)
                {
                    // Exception loading adapter object; if value returned is higher (i.e. worse) than current return value, update:
                    if (tasksetresult.ReturnValue < ex.ErrorCode)
                    {
                        tasksetresult.ReturnValue = ex.ErrorCode;
                    }
                    // Add details to logging output:
                    tasksetresult.LogMessage.AppendLine($"Task [{task.TaskName}] failed adapter creation: {ex}");
                }
                catch (Exception ex)                 // General exception (likely during TaskAdapter execution):
                {
                    if (tasksetresult.ReturnValue < 9)
                    {
                        tasksetresult.ReturnValue = 9;
                    }
                    if (ex is AggregateException ae)                     // Exception caught from async task; simplify if possible
                    {
                        ex = TaskUtilities.General.SimplifyAggregateException(ae);
                    }
                    // Add details to logging output:
                    tasksetresult.LogMessage.AppendLine($"Task [{task.TaskName}] execution error: {ex}");
                }

                // If this step was not successful and caller requested halting batch when error encountered, stop now:
                if (tasksetresult.ReturnValue != 0 && engineConfig.HaltOnError)
                {
                    tasksetresult.LogMessage.AppendLine("Halting task list due to error executing previous step");
                    break;
                }
            }
            #endregion

            #region If return value debugging requested, log final set of return values
            if (engineConfig.DebugReturnValues)
            {
                foreach (var returnvalue in tasksetresult.ReturnValues)
                {
                    logger.LogDebug("RETURN VALUE {@returnvalue}", new { key = returnvalue.Key, value = returnvalue.Value });
                }
            }
            #endregion

            return(tasksetresult);
        }
Exemple #2
0
        static void Main(string[] args)
        {
            var    engineConfig          = new TaskEngineConfig();                                            // Engine execution parameter collection
            string jobName               = null;                                                              // For logging purposes
            string taskFileName          = null;                                                              // For running a single file
            string taskFolderName        = null;                                                              // For running all files in a folder
            var    cmdlineTaskParameters = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); // Global overrides for task parameters

            #region Load application configuration
            var env           = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT");
            var configbuilder = new ConfigurationBuilder()
                                .SetBasePath(Directory.GetCurrentDirectory())
                                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
                                .AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: false)
                                .AddEnvironmentVariables();
            if (env == "Development")
            {
                configbuilder.AddUserSecrets <Program>();
            }
            if (args != null)
            {
                var configargs = new List <string>();                // Store unrecognized/generic parameters

                #region Parse command-line arguments
                // Name/value pairs with an "@" prefix are TaskParameter override values:
                var REGEX_TASKPARM = new Regex(@"^@(?<name>\S+)[=](?<value>.+)$");
                // Flag-only parameters (i.e. not a name/value pair):
                var REGEX_FLAGPARM = new Regex(@"^(--|-|/)?(?<value>[\S-[=]]+)$");
                // Other name/value pairs (optionally starting with any of "-", "--" or "/") are program-level:
                var REGEX_NAMEDPARM          = new Regex(@"^(--|-|/)?(?<name>\S+)[=](?<value>.+)$");
                var REGEX_NAMEDPARM_SHORTKEY = new Regex(@"^-(?![-])");

                // Iterate through all arguments, looking for recognized values (may short-circuit normal
                // configuration behavior to allow custom parameters for this application or any adapter)
                foreach (var arg in args)
                {
                    // Check for task parameter override value first
                    var match = REGEX_TASKPARM.Match(arg);
                    if (match.Success)
                    {
                        cmdlineTaskParameters[$"Parameters:{match.Groups["name"].Value}"] = match.Groups["value"].Value;
                    }
                    else
                    {
                        // Check for flag-only parameter:
                        match = REGEX_FLAGPARM.Match(arg);
                        if (match.Success)
                        {
                            // Check for reserved configuration flag values; if not one of the specified supported
                            // options, add to configuration collection (DI clients may recognize/use it):
                            switch (match.Groups["value"].Value.ToLowerInvariant())
                            {
                            case "console":                                     // Enable console logging
                                // Add console sink to Serilog config (added at node "99" to prevent overwriting
                                // any existing sinks; note that if a console sink is already configured, this
                                // will result in messages being output to console twice)
                                configargs.Add("Serilog:WriteTo:99:Name=Console");
                                break;

                            case "debug":                                     // Set minimum logging level to Debug
                                configargs.Add("Serilog:MinimumLevel=Debug");
                                break;

                            case "haltonerror":                                     // Halt batch of tasks if any individual step fails
                                engineConfig.HaltOnError = true;
                                break;

                            case "debugreturn":                                     // Dump return values at end of execution to debug log
                                engineConfig.DebugReturnValues = true;
                                break;

                            default:                                     // Unrecognized value - add to config collection
                                configargs.Add(arg);
                                break;
                            }
                            ;
                        }
                        else
                        {
                            // Check for named parameter:
                            match = REGEX_NAMEDPARM.Match(arg);
                            if (match.Success)
                            {
                                // Check for reserved configuration names; if not one of the specified supported
                                // options, add to configuration collection (DI clients may recognize/use it):
                                switch (match.Groups["name"].Value.ToLowerInvariant())
                                {
                                case "jobname":                                         // Name of job, for logging purposes
                                    jobName = match.Groups["value"].Value;
                                    break;

                                case "taskfile":                                         // Name of specific task file to execute
                                    taskFileName = match.Groups["value"].Value;
                                    break;

                                case "taskfolder":                                         // Name of folder of task files to execute
                                    taskFolderName = match.Groups["value"].Value;
                                    // Use folder name as default jobName, unless already provided:
                                    if (string.IsNullOrEmpty(jobName))
                                    {
                                        jobName = taskFolderName;
                                    }
                                    break;

                                default:                                         // Unrecognized parameter - add to config collection
                                    // (replace single leading "-" with "--", as MS config provider will
                                    // hysterically throw a formatting exception for unmapped "short keys")
                                    configargs.Add(REGEX_NAMEDPARM_SHORTKEY.Replace(arg, "--"));
                                    break;
                                }
                                ;
                            }
                            else
                            {
                                // Argument is in unrecognized format, just add to configuration collection:
                                configargs.Add(arg);
                            }
                        }
                    }
                }
                // If any arguments were added to configuration collection, add to ConfigBuilder now:
                if (configargs.Count > 0)
                {
                    configbuilder.AddCommandLine(configargs.ToArray());
                }
                #endregion
            }
            var config = configbuilder.Build();
            #endregion

            // Build logger configuration from application config file and create logger:
            Log.Logger = new LoggerConfiguration()
                         .ReadFrom.Configuration(config)
                         .Enrich.WithProperty("AppVer", typeof(Program).Assembly.GetName().Version)
                         .Enrich.WithProperty("LibVer", typeof(TaskEngine).Assembly.GetName().Version)
                         .CreateLogger();

            // Set up services and configurations for dependency injection:
            var serviceCollection = new ServiceCollection()
                                    .AddLogging(configure => configure.AddSerilog(dispose: true))
                                    .AddMemoryCache()
                                    .AddSingleton <IConfiguration>(config)
                                    .Configure <TaskUtilities.SmtpOptions>(config.GetSection("Smtp"))
                                    .AddTransient <TaskUtilities.Smtp>()
                                    .AddSingleton(x => ActivatorUtilities.CreateInstance <TaskEngine>(x, jobName))
            ;

            using (var serviceProvider = serviceCollection.BuildServiceProvider())
            {
                var adapterConfigs = new List <TaskParameters>();
                var logMessage     = new StringBuilder();

                #region Create collection of adapter configurations from incoming parameters
                try
                {
                    IOrderedEnumerable <string> configFiles = null;
                    if (!string.IsNullOrEmpty(taskFolderName))
                    {
                        // Running all adapter files in folder; retrieve list of JSON files:
                        configFiles = Directory.EnumerateFiles(taskFolderName, "*.json")
                                      // Ensure file extension is actually JSON (avoid false-positives with 8.3 filenames):
                                      .Where(filename => filename.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
                                      // Sort files in alphabetical order, to allow deliberate ordering of steps:
                                      .OrderBy(filename => filename);
                    }
                    else if (!string.IsNullOrEmpty(taskFileName))
                    {
                        // Running single task file only - create collection with single entry:
                        configFiles = new List <string>()
                        {
                            taskFileName
                        }.OrderBy(x => x);
                    }
                    // (else leave configFiles null, and fall through to error logic below)

                    #region Iterate through all files, constructing parameter objects
                    if (configFiles != null)
                    {
                        foreach (var configFile in configFiles)
                        {
                            try
                            {
                                // Build configuration object from Json file (combined with override parameters
                                // from command line), bind to a TaskParameterTemplate object:
                                var taskconfig = new ConfigurationBuilder()
                                                 .AddJsonFile(configFile, optional: false, reloadOnChange: false)
                                                 .AddInMemoryCollection(cmdlineTaskParameters)
                                                 .Build();
                                var taskparms = taskconfig.Get <TaskParametersTemplate>().EnsureValid();

                                // Create TaskParameters object from template object, and add to collection:
                                adapterConfigs.Add(new TaskParameters(taskparms.Parameters)
                                {
                                    TaskName         = string.IsNullOrEmpty(taskparms.TaskName) ? Path.GetFileName(configFile) : taskparms.TaskName,
                                    AdapterClassName = taskparms.AdapterClassName,
                                    AdapterDLLName   = taskparms.AdapterDLLName,
                                    AdapterDLLPath   = taskparms.AdapterDLLPath,
                                    Configuration    = taskconfig
                                });
                            }
                            catch (Exception ex)
                            {
                                // Error reading file or deserializing configuration object; if this is not part of a batch OR
                                // batch specifies halt on error, empty list and re-throw exception to stop all processing:
                                if (string.IsNullOrEmpty(taskFolderName) || engineConfig.HaltOnError)
                                {
                                    adapterConfigs.Clear();
                                    throw new Exception($"Error reading configuration file {configFile}", ex);
                                }
                                else                                 // Otherwise just log error and proceed with other configurations:
                                {
                                    Log.Logger.Warning(ex, $"Error reading configuration file {configFile}, skipped");
                                    logMessage.AppendLine($"Error reading configuration file {configFile}, skipped");
                                }
                            }
                        }
                    }
                    #endregion
                }
                catch (Exception ex)
                {
                    Log.Logger.Error(ex, "Error retrieving task configurations");
                }
                #endregion

                #region Execute TaskAdapter collection
                if (adapterConfigs.Count > 0)
                {
                    // Activate TaskEngine service, and pass in TaskAdapter configuration collection for (synchronous) execution:
                    using var te = serviceProvider.GetRequiredService <TaskEngine>();
                    var result = te.Execute(adapterConfigs, engineConfig).Result;

                    // Set executable exit code to execution return value, save log message for use below:
                    Environment.ExitCode = result.ReturnValue;
                    logMessage.Append(result.LogMessage);
                }
                else                 // No configurations available - exit with error message
                {
                    Environment.ExitCode = 1;
                    logMessage.Append("No task configurations available");
                }
                #endregion

                #region Notify users of execution results, if required
                if (logMessage.Length > 0)
                {
                    //Console.Error.WriteLine(logMessage); // TODO: UNCOMMENT CONSOLE OUTPUT
                }

                // If configuration specifies that results should be emailed (or it specifies that errors should be
                // emailed and application is NOT exiting with success), send alert now:
                var sendresultsto = (string.IsNullOrEmpty(config["sendresult"]) && Environment.ExitCode != 0) ?
                                    config["senderror"] : config["sendresult"];
                if (!string.IsNullOrEmpty(sendresultsto))
                {
                    using (var smtp = serviceProvider.GetService <TaskUtilities.Smtp>())
                    {
                        smtp.SendEmail($"Execution {(Environment.ExitCode == 0 ? "results" : "ERROR")} job [{jobName}] on {Environment.MachineName}",
                                       $"Return code {Environment.ExitCode} at {DateTime.Now:yyyy-MM-dd HH:mm:ss}<br/><br/><pre>{logMessage}</pre>",
                                       sendresultsto).Wait();
                    }
                }
                #endregion
            }
        }