private async Task RunScriptAsync(TheScript script, TheThing variables, int stepNumber = 1, bool replay = false) { TheThing variablesSnapshot; try { for (; stepNumber <= script.Steps.Length; stepNumber++) { //Clone thing before step occurs variablesSnapshot = new TheThing(); variables.CloneThingAndPropertyMetaData(variablesSnapshot, true); var step = script.Steps[stepNumber - 1]; var existingSnapshot = MyScriptTableStorage.MyMirrorCache.GetEntryByFunc(snapshot => snapshot.ScriptName == script.Name && snapshot.ScriptStep == stepNumber); if (existingSnapshot?.Disabled == true) { TheBaseAssets.MySYSLOG.WriteToLog(175002, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Finished script step: skipped step because it was disabled", eMsgLevel.l3_ImportantMessage, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Step", stepNumber }, { "Message", step.Message.MessageName }, { "Target", step.Message.Target }, }))); UpdateStorageList(script.Name, "Disabled", stepNumber, script, variablesSnapshot, replay); continue; } if (step.Condition != null) { var condition = TheCommonUtils.GenerateFinalStr(step.Condition, variables); if ( (condition == "" || condition.ToLowerInvariant() == "false" || condition.Trim() == "0") || (condition.StartsWith("!") && condition.Length >= 1 && (condition.Substring(1).ToLowerInvariant() == "true") || condition.Substring(1).Trim() == "1")) { TheBaseAssets.MySYSLOG.WriteToLog(175002, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Finished script step: skipped step due to condition not met", eMsgLevel.l3_ImportantMessage, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Step", stepNumber }, { "Message", step.Message.MessageName }, { "Target", step.Message.Target }, { "Condition", step.Condition }, { "ConditionEvaluated", condition }, }))); UpdateStorageList(script.Name, "Condition Not Met", stepNumber, script, variablesSnapshot, replay); continue; } } var messageType = TheCommonUtils.GenerateFinalStr(step.Message.MessageName, variables); var txtPayload = TheCommonUtils.GenerateFinalStr(step.Message.Parameters?.ToString(), variables); { var txtPayload2 = txtPayload?.Replace("\"\\\"", ""); var txtPayload3 = txtPayload2?.Replace("\\\"\"", ""); txtPayload = txtPayload3; } // TODO Need a simpler and more flexible way to specify thing address in the script JSON var target = step.Message.Target; if (target == null) { if (txtPayload.Contains("EngineName")) { var payloadDict = TheCommonUtils.DeserializeJSONStringToObject <Dictionary <string, object> >(txtPayload); object engineNameInferred = null; if (payloadDict?.TryGetValue("EngineName", out engineNameInferred) == true && !string.IsNullOrEmpty(engineNameInferred?.ToString())) { target = new TheMessageAddress { EngineName = engineNameInferred.ToString() }; } } } if (target.EngineName.StartsWith("%") || target.EngineName.StartsWith("{")) { target.EngineName = TheCommonUtils.GenerateFinalStr(target.EngineName, variables); // TODO Clean this up: support a serialized TheMessageAddress in the engine name, so that an output variable can be fed into a method invocation try { var newTarget = TheCommonUtils.DeserializeJSONStringToObject <TheMessageAddress>(target.EngineName); if (newTarget != null) { target = newTarget; } } catch { // parsing error: ignore, will result in other errors downstream } } await TheThingRegistry.WaitForInitializeAsync(target); bool bDoRetry; int remainingRetryCount = step.RetryCount ?? 0; do { existingSnapshot = MyScriptTableStorage.MyMirrorCache.GetEntryByFunc(snapshot => snapshot.ScriptName == script.Name && snapshot.ScriptStep == stepNumber); if (existingSnapshot?.Disabled == true) { TheBaseAssets.MySYSLOG.WriteToLog(175002, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Finished script step: skipped step because it was disabled", eMsgLevel.l3_ImportantMessage, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Step", stepNumber }, { "Message", step.Message.MessageName }, { "Target", step.Message.Target }, }))); UpdateStorageList(script.Name, "Disabled", stepNumber, script, variablesSnapshot, replay); break; } bDoRetry = false; var response = await TheCommRequestResponse.PublishRequestAsync(MyBaseThing, target, messageType, new TimeSpan(0, 0, 0, 0, step.Message.timeout), null, txtPayload, null); if (!string.IsNullOrEmpty(response?.PLS)) { var outputs = TheCommonUtils.DeserializeJSONStringToObject <Dictionary <string, object> >(response.PLS); if (outputs != null) { if (step.Message.outputs != null) { foreach (var output in step.Message.outputs) { if (output.Key == "*") { variables.SetProperty(output.Value, response.PLS); } else if (outputs.TryGetValue(output.Key, out var outputValue)) { variables.SetProperty(output.Value, outputValue); if (output.Value.Contains("Error") && !string.IsNullOrEmpty(TheCommonUtils.CStr(outputValue))) { TheBaseAssets.MySYSLOG.WriteToLog(175004, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Error in script step: output reported error", eMsgLevel.l1_Error, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Step", stepNumber }, { "Message", messageType }, { "Target", target }, { "PLS", txtPayload }, { "Response", response }, { "ResponsePLS", response?.PLS }, }))); UpdateStorageList(script.Name, $"Error {outputValue} in output", stepNumber, script, variablesSnapshot, replay); if (remainingRetryCount < 0 || remainingRetryCount > 0) { remainingRetryCount--; bDoRetry = true; } string retriesRemaining = bDoRetry ? (remainingRetryCount >= 0 ? $"{remainingRetryCount + 1}" : "infinite") : "none"; MyBaseThing.SetStatus(3, $"Error in script '{script?.Name}', step {stepNumber}: output '{output.Value}' reported error {outputValue}. Retries remaining: {retriesRemaining}"); } } else { // TODO provide access to sub-elements in the JSON //var outputParts = output.Key.Split('/'); //dynamic currentNode = outputs; //foreach (var outputPart in outputParts) //{ // if (currentNode.TryGetValue(outputPart, out var nextNode)) // { // currentNode = nextNode; // } //} } } } TheBaseAssets.MySYSLOG.WriteToLog(175003, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Finished script step", eMsgLevel.l3_ImportantMessage, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Step", stepNumber }, { "Message", messageType }, { "Target", target }, { "PLS", txtPayload }, { "ResponsePLS", response.PLS }, }))); UpdateStorageList(script.Name, "Finished", stepNumber, script, variablesSnapshot, replay); } else { TheBaseAssets.MySYSLOG.WriteToLog(175004, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Error in script step: no outputs found in response", eMsgLevel.l1_Error, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Step", stepNumber }, { "Message", messageType }, { "Target", target }, { "PLS", txtPayload }, { "Response", response }, { "ResponsePLS", response?.PLS }, }))); UpdateStorageList(script.Name, "Error: No Output", stepNumber, script, variablesSnapshot, replay); if (step.DontRetryOnEmptyResponse != true && (remainingRetryCount < 0 || remainingRetryCount > 0)) { remainingRetryCount--; bDoRetry = true; } string retriesRemaining = bDoRetry ? (remainingRetryCount >= 0 ? $"{remainingRetryCount + 1}" : "infinite") : "none"; MyBaseThing.SetStatus(3, $"Error in script '{script?.Name}', step {stepNumber}: no outputs found in response. Retries remaining: {retriesRemaining}"); } } else { TheBaseAssets.MySYSLOG.WriteToLog(175005, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Error Script step: timeout", eMsgLevel.l1_Error, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Step", stepNumber }, { "Message", messageType }, { "Target", target }, { "PLS", txtPayload }, { "Response", response }, }))); UpdateStorageList(script.Name, "Error: Timeout", stepNumber, script, variablesSnapshot, replay); //Retries infinitely unless count is specified if (remainingRetryCount < 0 || remainingRetryCount > 0) { remainingRetryCount--; bDoRetry = true; } string retriesRemaining = bDoRetry ? (remainingRetryCount >= 0 ? $"{remainingRetryCount + 1}" : "infinite") : "none"; MyBaseThing.SetStatus(3, $"Error in script '{script?.Name}', step {stepNumber}: timeout. Retries remaining: {retriesRemaining}"); } if (bDoRetry) { await TheCommonUtils.TaskDelayOneEye(30000, 100).ConfigureAwait(false); } } while (bDoRetry && TheBaseAssets.MasterSwitch); } } catch (Exception e) { TheBaseAssets.MySYSLOG.WriteToLog(175006, TSM.L(eDEBUG_LEVELS.OFF) ? null : new TSM(MyBaseThing.EngineName, "Error in script step", eMsgLevel.l1_Error, TheCommonUtils.SerializeObjectToJSONString(new Dictionary <String, object> { { "Script", script.Name }, { "Exception", e.Message }, }))); MyBaseThing.SetStatus(3, $"Error in script '{script?.Name}': {e.Message}"); //Save variables instead of snapshot in case of error UpdateStorageList(script.Name, $"Error: {e.Message}", stepNumber, script, variables, replay); } }