/// <summary> /// Reads an qualified object name from its defining element. Outputs an error if the name is missing. /// </summary> /// <param name="Element">Element to read the name for</param> /// <param name="QualifiedName">Output variable to receive the name of the object</param> /// <returns>True if the object had a valid name (assigned to the Name variable), false if the name was invalid or missing.</returns> bool TryReadQualifiedObjectName(ScriptElement Element, out string[] QualifiedName) { // Check the name attribute is present if (!Element.HasAttribute("Name")) { LogError(Element, "Missing 'Name' attribute"); QualifiedName = null; return(false); } // Get the value of it, strip any leading or trailing whitespace, and make sure it's not empty string[] Values = ReadAttribute(Element, "Name").Split('.'); foreach (string Value in Values) { if (!ValidateName(Element, Value)) { QualifiedName = null; return(false); } } // Return it QualifiedName = Values; return(true); }
/// <summary> /// Checks that the given name is valid syntax /// </summary> /// <param name="Element">The element that contains the name</param> /// <param name="Name">The name to check</param> /// <returns>True if the name is valid</returns> bool ValidateName(ScriptElement Element, string Name) { // Check it's not empty if (Name.Length == 0) { LogError(Element, "Name is empty"); return(false); } // Check there are no invalid characters for (int Idx = 0; Idx < Name.Length; Idx++) { if (Idx > 0 && Name[Idx] == ' ' && Name[Idx - 1] == ' ') { LogError(Element, "Consecutive spaces in object name"); return(false); } if (!Char.IsLetterOrDigit(Name[Idx]) && Name[Idx] != '_' && Name[Idx] != ' ') { LogError(Element, "Invalid character in object name - '{0}'", Name[Idx]); return(false); } } return(true); }
/// <summary> /// Checks that the given name does not already used to refer to a node, and print an error if it is. /// </summary> /// <param name="Element">Xml element to read from</param> /// <param name="Name">Name of the alias</param> /// <param name="Nodes">Array of nodes that this name should resolve to</param> /// <returns>True if the name was registered correctly, false otherwise.</returns> bool CheckNameIsUnique(ScriptElement Element, string Name) { // Get the nodes that it maps to if (Graph.ContainsName(Name)) { LogError(Element, "'{0}' is already defined; cannot add a second time", Name); return(false); } return(true); }
/// <summary> /// Reads the definition for an aggregate, and adds it to the given agent group /// </summary> /// <param name="Element">Xml element to read the definition from</param> void ReadAggregate(ScriptElement Element) { string Name; if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name) && CheckNameIsUnique(Element, Name)) { string[] RequiredNames = ReadListAttribute(Element, "Requires"); Graph.AggregateNameToNodes.Add(Name, ResolveReferences(Element, RequiredNames).ToArray()); } }
/// <summary> /// Reads a property assignment. /// </summary> /// <param name="Element">Xml element to read the definition from</param> void ReadProperty(ScriptElement Element) { if (EvaluateCondition(Element)) { string Name = ReadAttribute(Element, "Name"); if (ValidateName(Element, Name)) { GlobalProperties[Name] = ReadAttribute(Element, "Value"); } } }
/// <summary> /// Reads a local property assignment. /// </summary> /// <param name="Element">Xml element to read the definition from</param> void ReadLocalProperty(ScriptElement Element) { if (EvaluateCondition(Element)) { string Name = ReadAttribute(Element, "Name"); if (ValidateName(Element, Name)) { ScopedProperties[ScopedProperties.Count - 1][Name] = ReadAttribute(Element, "Value"); } } }
/// <summary> /// Read an include directive, and the contents of the target file /// </summary> /// <param name="Element">Xml element to read the definition from</param> /// <param name="BaseDir">Base directory to resolve relative include paths from </param> void ReadInclude(ScriptElement Element, DirectoryReference BaseDir) { if (EvaluateCondition(Element)) { FileReference Script = FileReference.Combine(BaseDir, Element.GetAttribute("Script")); if (Script.Exists()) { TryRead(Script); } else { LogError(Element, "Cannot find included script '{0}'", Script.FullName); } } }
/// <summary> /// Reads a warning from the given element, evaluates the condition on it, and writes it to the log if the condition passes. /// </summary> /// <param name="Element">Xml element to read the definition from</param> /// <param name="EventType">The diagnostic event type</param> /// <param name="EnclosingObject">The enclosing object instance</param> void ReadDiagnostic(ScriptElement Element, LogEventType EventType, Node EnclosingNode, AgentGroup EnclosingGroup, ManualTrigger EnclosingTrigger) { if (EvaluateCondition(Element)) { string Message = ReadAttribute(Element, "Message"); GraphDiagnostic Diagnostic = new GraphDiagnostic(); Diagnostic.EventType = EventType; Diagnostic.Message = String.Format("{0}({1}): {2}", Element.File.FullName, Element.LineNumber, Message); Diagnostic.EnclosingNode = EnclosingNode; Diagnostic.EnclosingGroup = EnclosingGroup; Diagnostic.EnclosingTrigger = EnclosingTrigger; Graph.Diagnostics.Add(Diagnostic); } }
/// <summary> /// Reads an attribute from the given XML element, expands any properties in it, and parses it as an enum of the given type. /// </summary> /// <typeparam name="T">The enum type to parse the attribute as</typeparam> /// <param name="Element">Element to read the attribute from</param> /// <param name="Name">Name of the attribute</param> /// <param name="DefaultValue">Default value for the enum, if the attribute is missing</param> /// <returns>The value of the attribute field</returns> T ReadEnumAttribute <T>(ScriptElement Element, string Name, T DefaultValue) where T : struct { T Result = DefaultValue; if (Element.HasAttribute(Name)) { string Value = ReadAttribute(Element, Name).Trim(); T EnumValue; if (Enum.TryParse(Value, true, out EnumValue)) { Result = EnumValue; } else { LogError(Element, "Invalid value '{0}' - expected {1}", Value, String.Join("/", Enum.GetNames(typeof(T)))); } } return(Result); }
/// <summary> /// Evaluates the (optional) conditional expression on a given XML element via the If="..." attribute, and returns true if the element is enabled. /// </summary> /// <param name="Element">The element to check</param> /// <returns>True if the element's condition evaluates to true (or doesn't have a conditional expression), false otherwise</returns> bool EvaluateCondition(ScriptElement Element) { // Check if the element has a conditional attribute const string AttributeName = "If"; if (!Element.HasAttribute(AttributeName)) { return(true); } // If it does, try to evaluate it. try { string Text = ExpandProperties(Element.GetAttribute("If")); return(Condition.Evaluate(Text)); } catch (ConditionException Ex) { LogError(Element, "Error in condition: {0}", Ex.Message); return(false); } }
/// <summary> /// Reads an attribute from the given XML element, expands any properties in it, and parses it as a boolean. /// </summary> /// <param name="Element">Element to read the attribute from</param> /// <param name="Name">Name of the attribute</param> /// <param name="DefaultValue">Default value if the attribute is missing</param> /// <returns>The value of the attribute field</returns> bool ReadBooleanAttribute(ScriptElement Element, string Name, bool bDefaultValue) { bool bResult = bDefaultValue; if (Element.HasAttribute(Name)) { string Value = ReadAttribute(Element, Name).Trim(); if (Value.Equals("true", StringComparison.InvariantCultureIgnoreCase)) { bResult = true; } else if (Value.Equals("false", StringComparison.InvariantCultureIgnoreCase)) { bResult = false; } else { LogError(Element, "Invalid boolean value '{0}' - expected 'true' or 'false'", Value); } } return(bResult); }
/// <summary> /// Resolve a list of references to a set of nodes /// </summary> /// <param name="Element">Element used to locate any errors</param> /// <param name="ReferenceNames">Sequence of names to look up</param> /// <returns>Set of all the nodes included by the given names</returns> HashSet <NodeOutput> ResolveInputReferences(ScriptElement Element, IEnumerable <string> ReferenceNames) { HashSet <NodeOutput> Inputs = new HashSet <NodeOutput>(); foreach (string ReferenceName in ReferenceNames) { NodeOutput[] ReferenceInputs; if (Graph.TryResolveInputReference(ReferenceName, out ReferenceInputs)) { Inputs.UnionWith(ReferenceInputs); } else if (!ReferenceName.StartsWith("#") && Graph.NameToNodeOutput.ContainsKey(ReferenceName)) { LogError(Element, "Reference to '{0}' cannot be resolved; did you mean '#{0}'?", ReferenceName); } else { LogError(Element, "Reference to '{0}' cannot be resolved; check it has been defined.", ReferenceName); } } return(Inputs); }
/// <summary> /// Reads an object name from its defining element. Outputs an error if the name is missing. /// </summary> /// <param name="Element">Element to read the name for</param> /// <param name="Name">Output variable to receive the name of the object</param> /// <returns>True if the object had a valid name (assigned to the Name variable), false if the name was invalid or missing.</returns> bool TryReadObjectName(ScriptElement Element, out string Name) { // Check the name attribute is present if (!Element.HasAttribute("Name")) { LogError(Element, "Missing 'Name' attribute"); Name = null; return(false); } // Get the value of it, strip any leading or trailing whitespace, and make sure it's not empty string Value = ReadAttribute(Element, "Name"); if (!ValidateName(Element, Value)) { Name = null; return(false); } // Return it Name = Value; return(true); }
/// <summary> /// Expands any properties and reads an attribute. /// </summary> /// <param name="Element">Element to read the attribute from</param> /// <param name="Name">Name of the attribute</param> /// <returns>Array of names, with all leading and trailing whitespace removed</returns> string ReadAttribute(ScriptElement Element, string Name) { return(ExpandProperties(Element.GetAttribute(Name))); }
/// <summary> /// Outputs an error message to the log and increments the number of errors, referencing the file and line number of the element that caused it. /// </summary> /// <param name="Element">The script element causing the error</param> /// <param name="Format">Standard String.Format()-style format string</param> /// <param name="Args">Optional arguments</param> void LogError(ScriptElement Element, string Format, params object[] Args) { CommandUtils.LogError("{0}({1}): {2}", Element.File.FullName, Element.LineNumber, String.Format(Format, Args)); NumErrors++; }
/// <summary> /// Reads the definition for an agent group. /// </summary> /// <param name="Element">Xml element to read the definition from</param> void ReadTrigger(ScriptElement Element) { string[] QualifiedName; if (EvaluateCondition(Element) && TryReadQualifiedObjectName(Element, out QualifiedName)) { // Validate all the parent triggers ManualTrigger ParentTrigger = null; for (int Idx = 0; Idx < QualifiedName.Length - 1; Idx++) { ManualTrigger NextTrigger; if (!Graph.NameToTrigger.TryGetValue(QualifiedName[Idx], out NextTrigger)) { LogError(Element, "Unknown trigger '{0}'", QualifiedName[Idx]); return; } if (NextTrigger.Parent != ParentTrigger) { LogError(Element, "Qualified name of trigger '{0}' is '{1}'", NextTrigger.Name, NextTrigger.QualifiedName); return; } ParentTrigger = NextTrigger; } // Get the name of the new trigger string Name = QualifiedName[QualifiedName.Length - 1]; // Create the new trigger ManualTrigger Trigger; if (!Graph.NameToTrigger.TryGetValue(Name, out Trigger)) { Trigger = new ManualTrigger(ParentTrigger, Name); Graph.NameToTrigger.Add(Name, Trigger); } else if (Trigger.Parent != ParentTrigger) { LogError(Element, "Conflicting parent for '{0}' - previously declared as '{1}', now '{2}'", Name, Trigger.QualifiedName, new ManualTrigger(ParentTrigger, Name).QualifiedName); return; } // Read the root BuildGraph element EnterScope(); foreach (ScriptElement ChildElement in Element.ChildNodes.OfType <ScriptElement>()) { switch (ChildElement.Name) { case "Property": ReadProperty(ChildElement); break; case "Local": ReadLocalProperty(ChildElement); break; case "Agent": ReadAgent(ChildElement, Trigger); break; case "Aggregate": ReadAggregate(ChildElement); break; case "Notifier": ReadNotifier(ChildElement); break; case "Warning": ReadDiagnostic(ChildElement, LogEventType.Warning, null, null, Trigger); break; case "Error": ReadDiagnostic(ChildElement, LogEventType.Error, null, null, Trigger); break; default: LogError(ChildElement, "Invalid element '{0}'", ChildElement.Name); break; } } LeaveScope(); } }
/// <summary> /// Reads the definition for an email notifier /// </summary> /// <param name="Element">Xml element to read the definition from</param> void ReadNotifier(ScriptElement Element) { if (EvaluateCondition(Element)) { string[] TargetNames = ReadListAttribute(Element, "Targets"); string[] ExceptNames = ReadListAttribute(Element, "Except"); string[] IndividualNodeNames = ReadListAttribute(Element, "Nodes"); string[] TriggerNames = ReadListAttribute(Element, "Triggers"); string[] Users = ReadListAttribute(Element, "Users"); string[] Submitters = ReadListAttribute(Element, "Submitters"); bool? bWarnings = Element.HasAttribute("Warnings")? (bool?)ReadBooleanAttribute(Element, "Warnings", true) : null; // Find the list of targets which are included, and recurse through all their dependencies HashSet <Node> Nodes = new HashSet <Node>(); if (TargetNames != null) { HashSet <Node> TargetNodes = ResolveReferences(Element, TargetNames); foreach (Node Node in TargetNodes) { Nodes.Add(Node); Nodes.UnionWith(Node.InputDependencies); } } // Add all the individually referenced nodes if (IndividualNodeNames != null) { HashSet <Node> IndividualNodes = ResolveReferences(Element, IndividualNodeNames); Nodes.UnionWith(IndividualNodes); } // Exclude all the exceptions if (ExceptNames != null) { HashSet <Node> ExceptNodes = ResolveReferences(Element, ExceptNames); Nodes.ExceptWith(ExceptNodes); } // Update all the referenced nodes with the settings foreach (Node Node in Nodes) { if (Users != null) { Node.NotifyUsers.UnionWith(Users); } if (Submitters != null) { Node.NotifySubmitters.UnionWith(Submitters); } if (bWarnings.HasValue) { Node.bNotifyOnWarnings = bWarnings.Value; } } // Add the users to the list of triggers if (TriggerNames != null) { foreach (string TriggerName in TriggerNames) { ManualTrigger Trigger; if (Graph.NameToTrigger.TryGetValue(TriggerName, out Trigger)) { Trigger.NotifyUsers.UnionWith(Users); } else { LogError(Element, "Trigger '{0}' has not been defined", TriggerName); } } } } }
/// <summary> /// Reads a task definition from the given element, and add it to the given list /// </summary> /// <param name="Element">Xml element to read the definition from</param> /// <param name="Tasks">List of tasks to add to</param> void ReadTask(ScriptElement Element, List <CustomTask> Tasks) { if (EvaluateCondition(Element)) { // Get the reflection info for this element ScriptTask Task; if (!Schema.TryGetTask(Element.Name, out Task)) { LogError(Element, "Unknown task '{0}'", Element.Name); return; } // Check all the required parameters are present bool bHasRequiredAttributes = true; foreach (ScriptTaskParameter Parameter in Task.NameToParameter.Values) { if (!Parameter.bOptional && !Element.HasAttribute(Parameter.Name)) { LogError(Element, "Missing required attribute - {0}", Parameter.Name); bHasRequiredAttributes = false; } } // Read all the attributes into a parameters object for this task object ParametersObject = Activator.CreateInstance(Task.ParametersClass); foreach (XmlAttribute Attribute in Element.Attributes) { if (String.Compare(Attribute.Name, "If", StringComparison.InvariantCultureIgnoreCase) != 0) { // Get the field that this attribute should be written to in the parameters object ScriptTaskParameter Parameter; if (!Task.NameToParameter.TryGetValue(Attribute.Name, out Parameter)) { LogError(Element, "Unknown attribute '{0}'", Attribute.Name); continue; } // Expand variables in the value string ExpandedValue = ExpandProperties(Attribute.Value); // Parse it and assign it to the parameters object object Value; if (Parameter.FieldInfo.FieldType.IsEnum) { Value = Enum.Parse(Parameter.FieldInfo.FieldType, ExpandedValue); } else if (Parameter.FieldInfo.FieldType == typeof(Boolean)) { Value = Condition.Evaluate(ExpandedValue); } else { Value = Convert.ChangeType(ExpandedValue, Parameter.FieldInfo.FieldType); } Parameter.FieldInfo.SetValue(ParametersObject, Value); } } // Construct the task if (bHasRequiredAttributes) { Tasks.Add((CustomTask)Activator.CreateInstance(Task.TaskClass, ParametersObject)); } } }
/// <summary> /// Reads the definition for a node, and adds it to the given agent group /// </summary> /// <param name="Element">Xml element to read the definition from</param> /// <param name="Group">Group for the node to be added to</param> /// <param name="ControllingTrigger">The controlling trigger for this node</param> void ReadNode(ScriptElement Element, AgentGroup Group, ManualTrigger ControllingTrigger) { string Name; if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name)) { string[] RequiresNames = ReadListAttribute(Element, "Requires"); string[] ProducesNames = ReadListAttribute(Element, "Produces"); string[] AfterNames = ReadListAttribute(Element, "After"); // Resolve all the inputs we depend on HashSet <NodeOutput> Inputs = ResolveInputReferences(Element, RequiresNames); // Gather up all the input dependencies, and check they're all upstream of the current node HashSet <Node> InputDependencies = new HashSet <Node>(); foreach (Node InputDependency in Inputs.Select(x => x.ProducingNode).Distinct()) { if (InputDependency.ControllingTrigger != null && InputDependency.ControllingTrigger != ControllingTrigger && !InputDependency.ControllingTrigger.IsUpstreamFrom(ControllingTrigger)) { LogError(Element, "'{0}' is dependent on '{1}', which is behind a different controlling trigger ({2})", Name, InputDependency.Name, InputDependency.ControllingTrigger.QualifiedName); } else { InputDependencies.Add(InputDependency); } } // Recursively include all their dependencies too foreach (Node InputDependency in InputDependencies.ToArray()) { InputDependencies.UnionWith(InputDependency.InputDependencies); } // Add the name of the node itself to the list of outputs. List <string> OutputNames = new List <string>(); foreach (string ProducesName in ProducesNames) { if (ProducesName.StartsWith("#")) { OutputNames.Add(ProducesName.Substring(1)); } else { LogError(Element, "Output tag names must begin with a '#' character ('{0}')", ProducesName); } } OutputNames.Add(Name); // Gather up all the order dependencies HashSet <Node> OrderDependencies = new HashSet <Node>(InputDependencies); OrderDependencies.UnionWith(ResolveReferences(Element, AfterNames)); // Recursively include all their order dependencies too foreach (Node OrderDependency in OrderDependencies.ToArray()) { OrderDependencies.UnionWith(OrderDependency.OrderDependencies); } // Check that we're not dependent on anything completing that is declared after the initial declaration of this group. int GroupIdx = Graph.Groups.IndexOf(Group); for (int Idx = GroupIdx + 1; Idx < Graph.Groups.Count; Idx++) { foreach (Node Node in Graph.Groups[Idx].Nodes.Where(x => OrderDependencies.Contains(x))) { LogError(Element, "Node '{0}' has a dependency on '{1}', which was declared after the initial definition of '{2}'.", Name, Node.Name, Group.Name); } } // Construct and register the node if (CheckNameIsUnique(Element, Name)) { // Add it to the node lookup Node NewNode = new Node(Name, Inputs.ToArray(), OutputNames.ToArray(), InputDependencies.ToArray(), OrderDependencies.ToArray(), ControllingTrigger); Graph.NameToNode.Add(Name, NewNode); // Register each of the outputs as a reference to this node foreach (NodeOutput Output in NewNode.Outputs) { if (Output.Name == Name || CheckNameIsUnique(Element, Output.Name)) { Graph.NameToNodeOutput.Add(Output.Name, Output); } } // Add all the tasks EnterScope(); foreach (ScriptElement ChildElement in Element.ChildNodes.OfType <ScriptElement>()) { switch (ChildElement.Name) { case "Property": ReadProperty(ChildElement); break; case "Local": ReadLocalProperty(ChildElement); break; case "Warning": ReadDiagnostic(ChildElement, LogEventType.Warning, NewNode, Group, ControllingTrigger); break; case "Error": ReadDiagnostic(ChildElement, LogEventType.Error, NewNode, Group, ControllingTrigger); break; default: ReadTask(ChildElement, NewNode.Tasks); break; } } LeaveScope(); // Add it to the current agent group Group.Nodes.Add(NewNode); } } }
/// <summary> /// Reads the definition for an agent group. /// </summary> /// <param name="Element">Xml element to read the definition from</param> /// <param name="Trigger">The controlling trigger for nodes in this group</param> void ReadAgent(ScriptElement Element, ManualTrigger Trigger) { string Name; if (EvaluateCondition(Element) && TryReadObjectName(Element, out Name)) { // Read the valid agent types. This may be omitted if we're continuing an existing group. string[] Types = ReadListAttribute(Element, "Type"); // Create the group object, or continue an existing one AgentGroup Group; if (NameToGroup.TryGetValue(Name, out Group)) { if (Types.Length > 0 && Group.PossibleTypes.Length > 0) { string[] NewTypes = Group.PossibleTypes.Intersect(Types, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (NewTypes.Length == 0) { LogError(Element, "No common agent types with previous agent definition"); } Group.PossibleTypes = NewTypes; } } else { if (Types.Length == 0) { LogError(Element, "Missing agent type for group '{0}'", Name); } Group = new AgentGroup(Name, Types); NameToGroup.Add(Name, Group); Graph.Groups.Add(Group); } // Process all the child elements. EnterScope(); foreach (ScriptElement ChildElement in Element.ChildNodes.OfType <ScriptElement>()) { switch (ChildElement.Name) { case "Property": ReadProperty(ChildElement); break; case "Local": ReadLocalProperty(ChildElement); break; case "Node": ReadNode(ChildElement, Group, Trigger); break; case "Aggregate": ReadAggregate(ChildElement); break; case "Warning": ReadDiagnostic(ChildElement, LogEventType.Warning, null, Group, Trigger); break; case "Error": ReadDiagnostic(ChildElement, LogEventType.Error, null, Group, Trigger); break; default: LogError(ChildElement, "Unexpected element type '{0}'", ChildElement.Name); break; } } LeaveScope(); } }
/// <summary> /// Expands any properties and reads a list of strings from an attribute, separated by semi-colon characters /// </summary> /// <param name="Element"></param> /// <param name="Name"></param> /// <returns>Array of names, with all leading and trailing whitespace removed</returns> string[] ReadListAttribute(ScriptElement Element, string Name) { string Value = ReadAttribute(Element, Name); return(Value.Split(new char[] { ';' }).Select(x => x.Trim()).Where(x => x.Length > 0).ToArray()); }