using JetBrains.Annotations; using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Xml.Serialization; namespace Microsoft.Win32.TaskScheduler { /// Options for when to convert actions to PowerShell equivalents. [Flags] public enum PowerShellActionPlatformOption { /// /// Never convert any actions to PowerShell. This will force exceptions to be thrown when unsupported actions our action quantities /// are found. /// Never = 0, /// /// Convert actions under Version 1 of the library (Windows XP or Windows Server 2003 and earlier). This option supports multiple /// actions of all types. If not specified, only a single is supported. Developer must ensure that /// PowerShell v2 or higher is installed on the target computer. /// Version1 = 1, /// /// Convert all and references to their PowerShell equivalents on systems /// on or after Windows 8 / Server 2012. /// Version2 = 2, /// Convert all actions regardless of version or operating system. All = 3 } /// Collection that contains the actions that are performed by the task. [XmlRoot("Actions", Namespace = TaskDefinition.tns, IsNullable = false)] [PublicAPI] public sealed class ActionCollection : IList, IDisposable, IXmlSerializable, IList, INotifyCollectionChanged, INotifyPropertyChanged { internal const int MaxActions = 32; private const string IndexerName = "Item[]"; private static readonly string psV2IdRegex = $"(?:; )?{nameof(PowerShellConversion)}=(?0|1)"; private bool inV2set; private PowerShellActionPlatformOption psConvert = PowerShellActionPlatformOption.Version2; private readonly List v1Actions; private V1Interop.ITask v1Task; private readonly V2Interop.IActionCollection v2Coll; private V2Interop.ITaskDefinition v2Def; internal ActionCollection([NotNull] V1Interop.ITask task) { v1Task = task; v1Actions = GetV1Actions(); PowerShellConversion = Action.TryParse(v1Task.GetDataItem(nameof(PowerShellConversion)), psConvert | PowerShellActionPlatformOption.Version2); } internal ActionCollection([NotNull] V2Interop.ITaskDefinition iTaskDef) { v2Def = iTaskDef; v2Coll = iTaskDef.Actions; System.Text.RegularExpressions.Match match; if (iTaskDef.Data != null && (match = System.Text.RegularExpressions.Regex.Match(iTaskDef.Data, psV2IdRegex)).Success) { var on = false; try { on = bool.Parse(match.Groups["v"].Value); } catch { try { on = int.Parse(match.Groups["v"].Value) == 1; } catch { } } if (on) psConvert |= PowerShellActionPlatformOption.Version2; else psConvert &= ~PowerShellActionPlatformOption.Version2; } UnconvertUnsupportedActions(); } /// Occurs when a collection changes. public event NotifyCollectionChangedEventHandler CollectionChanged; /// Occurs when a property value changes. public event PropertyChangedEventHandler PropertyChanged; /// Gets or sets the identifier of the principal for the task. [XmlAttribute] [DefaultValue(null)] [CanBeNull] public string Context { get { if (v2Coll != null) return v2Coll.Context; return v1Task.GetDataItem("ActionCollectionContext"); } set { if (v2Coll != null) v2Coll.Context = value; else v1Task.SetDataItem("ActionCollectionContext", value); OnNotifyPropertyChanged(); } } /// /// Gets or sets the systems under which unsupported actions will be converted to PowerShell instances. /// /// The PowerShell platform options. /// /// This property will affect how many actions are physically stored in the system and is tied to the version of Task Scheduler. /// /// If set to , then no actions will ever be converted to PowerShell. This will /// force exceptions to be thrown when unsupported actions our action quantities are found. /// /// /// If set to , then actions will be converted only under Version 1 of the /// library (Windows XP or Windows Server 2003 and earlier). This option supports multiple actions of all types. If not specified, /// only a single is supported. Developer must ensure that PowerShell v2 or higher is installed on the /// target computer. /// /// /// If set to (which is the default value), then and references will be converted to their PowerShell equivalents on systems /// on or after Windows 8 / Server 2012. /// /// /// If set to , then any actions not supported by the Task Scheduler version will be /// converted to PowerShell. /// /// [DefaultValue(typeof(PowerShellActionPlatformOption), "Version2")] public PowerShellActionPlatformOption PowerShellConversion { get => psConvert; set { if (psConvert != value) { psConvert = value; if (v1Task != null) v1Task.SetDataItem(nameof(PowerShellConversion), value.ToString()); if (v2Def != null) { if (!string.IsNullOrEmpty(v2Def.Data)) v2Def.Data = System.Text.RegularExpressions.Regex.Replace(v2Def.Data, psV2IdRegex, ""); if (!SupportV2Conversion) v2Def.Data = string.Format("{0}; {1}=0", v2Def.Data, nameof(PowerShellConversion)); } OnNotifyPropertyChanged(); } } } /// Gets or sets an XML-formatted version of the collection. public string XmlText { get { if (v2Coll != null) return v2Coll.XmlText; return XmlSerializationHelper.WriteObjectToXmlText(this); } set { if (v2Coll != null) v2Coll.XmlText = value; else XmlSerializationHelper.ReadObjectFromXmlText(value, this); OnNotifyPropertyChanged(); } } /// Gets the number of actions in the collection. public int Count => (v2Coll != null) ? v2Coll.Count : v1Actions.Count; bool IList.IsFixedSize => false; bool ICollection.IsReadOnly => false; bool IList.IsReadOnly => false; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; private bool SupportV1Conversion => (PowerShellConversion & PowerShellActionPlatformOption.Version1) != 0; private bool SupportV2Conversion => (PowerShellConversion & PowerShellActionPlatformOption.Version2) != 0; /// Gets or sets a specified action from the collection. /// The . /// The id ( ) of the action to be retrieved. /// Specialized instance. /// /// /// /// Mismatching Id for action and lookup. [NotNull] public Action this[string actionId] { get { if (string.IsNullOrEmpty(actionId)) throw new ArgumentNullException(nameof(actionId)); var t = Find(a => string.Equals(a.Id, actionId)); if (t != null) return t; throw new ArgumentOutOfRangeException(nameof(actionId)); } set { if (value == null) throw new NullReferenceException(); if (string.IsNullOrEmpty(actionId)) throw new ArgumentNullException(nameof(actionId)); var index = IndexOf(actionId); value.Id = actionId; if (index >= 0) this[index] = value; else Add(value); } } /// Gets or sets a an action at the specified index. /// The zero-based index of the action to get or set. [NotNull] public Action this[int index] { get { if (v2Coll != null) return Action.CreateAction(v2Coll[++index]); if (v1Task != null) { if (SupportV1Conversion) return v1Actions[index]; else { if (index == 0) return v1Actions[0]; } } throw new ArgumentOutOfRangeException(); } set { if (index < 0 || Count <= index) throw new ArgumentOutOfRangeException(nameof(index), index, "Index is not a valid index in the ActionCollection"); var orig = this[index].Clone(); if (v2Coll != null) { inV2set = true; try { Insert(index, value); RemoveAt(index + 1); } finally { inV2set = false; } } else { v1Actions[index] = value; SaveV1Actions(); } OnNotifyPropertyChanged(IndexerName); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, value, orig, index)); } } object IList.this[int index] { get => this[index]; set => this[index] = (Action)value; } /// Adds an action to the task. /// A type derived from . /// A derived class. /// The bound that was added to the collection. [NotNull] public TAction Add([NotNull] TAction action) where TAction : Action { if (action == null) throw new ArgumentNullException(nameof(action)); if (v2Def != null) action.Bind(v2Def); else { if (!SupportV1Conversion && (v1Actions.Count >= 1 || !(action is ExecAction))) throw new NotV1SupportedException($"Only a single {nameof(ExecAction)} is supported unless the {nameof(PowerShellConversion)} property includes the {nameof(PowerShellActionPlatformOption.Version1)} value."); v1Actions.Add(action); SaveV1Actions(); } OnNotifyPropertyChanged(nameof(Count)); OnNotifyPropertyChanged(IndexerName); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, action)); return action; } /// Adds an to the task. /// Path to an executable file. /// Arguments associated with the command-line operation. This value can be null. /// /// Directory that contains either the executable file or the files that are used by the executable file. This value can be null. /// /// The bound that was added to the collection. [NotNull] public ExecAction Add([NotNull] string path, [CanBeNull] string arguments = null, [CanBeNull] string workingDirectory = null) => Add(new ExecAction(path, arguments, workingDirectory)); /// Adds a new instance to the task. /// Type of task to be created /// Specialized instance. [NotNull] public Action AddNew(TaskActionType actionType) { if (Count >= MaxActions) throw new ArgumentOutOfRangeException(nameof(actionType), "A maximum of 32 actions is allowed within a single task."); if (v1Task != null) { if (!SupportV1Conversion && (v1Actions.Count >= 1 || actionType != TaskActionType.Execute)) throw new NotV1SupportedException($"Only a single {nameof(ExecAction)} is supported unless the {nameof(PowerShellConversion)} property includes the {nameof(PowerShellActionPlatformOption.Version1)} value."); return Action.CreateAction(v1Task); } return Action.CreateAction(v2Coll.Create(actionType)); } /// Adds a collection of actions to the end of the . /// /// The actions to be added to the end of the . The collection itself cannot be null and cannot /// contain null elements. /// /// is null. public void AddRange([ItemNotNull, NotNull] IEnumerable actions) { if (actions == null) throw new ArgumentNullException(nameof(actions)); if (v1Task != null) { var list = new List(actions); var at = list.Count == 1 && list[0].ActionType == TaskActionType.Execute; if (!SupportV1Conversion && ((v1Actions.Count + list.Count) > 1 || !at)) throw new NotV1SupportedException($"Only a single {nameof(ExecAction)} is supported unless the {nameof(PowerShellConversion)} property includes the {nameof(PowerShellActionPlatformOption.Version1)} value."); v1Actions.AddRange(actions); SaveV1Actions(); } else { foreach (var item in actions) Add(item); } } /// Clears all actions from the task. public void Clear() { if (v2Coll != null) v2Coll.Clear(); else { v1Actions.Clear(); SaveV1Actions(); } OnNotifyPropertyChanged(nameof(Count)); OnNotifyPropertyChanged(IndexerName); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// Determines whether the contains a specific value. /// The object to locate in the . /// true if is found in the ; otherwise, false. public bool Contains([NotNull] Action item) => Find(a => a.Equals(item)) != null; /// Determines whether the specified action type is contained in this collection. /// Type of the action. /// true if the specified action type is contained in this collection; otherwise, false. public bool ContainsType(Type actionType) => Find(a => a.GetType() == actionType) != null; /// /// Copies the elements of the to an array of , starting at a particular index. /// /// /// The array that is the destination of the elements copied from . The array must have zero-based indexing. /// /// The zero-based index in array at which copying begins. public void CopyTo(Action[] array, int arrayIndex) => CopyTo(0, array, arrayIndex, Count); /// /// Copies the elements of the to an array, starting at a particular array index. /// /// The zero-based index in the source at which copying begins. /// /// The array that is the destination of the elements copied from . The array must have zero-based indexing. /// /// The zero-based index in array at which copying begins. /// The number of elements to copy. /// is null. /// is less than 0. /// /// The number of elements in the source is greater than the available space from to the end of the destination . /// public void CopyTo(int index, [NotNull] Action[] array, int arrayIndex, int count) { if (array == null) throw new ArgumentNullException(nameof(array)); if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index)); if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex)); if (count < 0 || count > (Count - index)) throw new ArgumentOutOfRangeException(nameof(count)); if ((Count - index) > (array.Length - arrayIndex)) throw new ArgumentOutOfRangeException(nameof(arrayIndex)); for (var i = 0; i < count; i++) array[arrayIndex + i] = (Action)this[index + i].Clone(); } /// Releases all resources used by this class. public void Dispose() { v1Task = null; v2Def = null; if (v2Coll != null) Marshal.ReleaseComObject(v2Coll); } /// /// Searches for an that matches the conditions defined by the specified predicate, and returns the first /// occurrence within the entire collection. /// /// /// The delegate that defines the conditions of the to search for. /// /// /// The first that matches the conditions defined by the specified predicate, if found; otherwise, null. /// public Action Find(Predicate match) { if (match == null) throw new ArgumentNullException(nameof(match)); foreach (var item in this) if (match(item)) return item; return null; } /// /// Searches for an that matches the conditions defined by the specified predicate, and returns the zero-based /// index of the first occurrence within the collection that starts at the specified index and contains the specified number of elements. /// /// The zero-based starting index of the search. /// The number of elements in the collection to search. /// The delegate that defines the conditions of the element to search for. /// /// The zero-based index of the first occurrence of an element that matches the conditions defined by match, if found; otherwise, –1. /// public int FindIndexOf(int startIndex, int count, [NotNull] Predicate match) { if (startIndex < 0 || startIndex >= Count) throw new ArgumentOutOfRangeException(nameof(startIndex)); if (startIndex + count > Count) throw new ArgumentOutOfRangeException(nameof(count)); if (match == null) throw new ArgumentNullException(nameof(match)); for (var i = startIndex; i < startIndex + count; i++) if (match(this[i])) return i; return -1; } /// /// Searches for an that matches the conditions defined by the specified predicate, and returns the zero-based /// index of the first occurrence within the collection. /// /// The delegate that defines the conditions of the element to search for. /// /// The zero-based index of the first occurrence of an element that matches the conditions defined by match, if found; otherwise, –1. /// public int FindIndexOf([NotNull] Predicate match) => FindIndexOf(0, Count, match); /// Retrieves an enumeration of each of the actions. /// /// Returns an object that implements the interface and that can iterate through the /// objects within the . /// public IEnumerator GetEnumerator() { if (v2Coll != null) return new ComEnumerator(() => v2Coll.Count, i => v2Coll[i], Action.CreateAction); return v1Actions.GetEnumerator(); } /// Determines the index of a specific item in the . /// The object to locate in the . /// The index of if found in the list; otherwise, -1. public int IndexOf(Action item) => FindIndexOf(a => a.Equals(item)); /// Determines the index of a specific item in the . /// The id ( ) of the action to be retrieved. /// The index of if found in the list; otherwise, -1. public int IndexOf(string actionId) { if (string.IsNullOrEmpty(actionId)) throw new ArgumentNullException(nameof(actionId)); return FindIndexOf(a => string.Equals(a.Id, actionId)); } /// Inserts an action at the specified index. /// The zero-based index at which action should be inserted. /// The action to insert into the list. public void Insert(int index, [NotNull] Action action) { if (action == null) throw new ArgumentNullException(nameof(action)); if (index < 0 || index > Count) throw new ArgumentOutOfRangeException(nameof(index)); if (v2Coll != null) { var pushItems = new Action[Count - index]; if (Count != index) { CopyTo(index, pushItems, 0, Count - index); for (var j = Count - 1; j >= index; j--) RemoveAt(j); } Add(action); if (Count != index) for (var k = 0; k < pushItems.Length; k++) Add(pushItems[k]); } else { if (!SupportV1Conversion && (index > 0 || !(action is ExecAction))) throw new NotV1SupportedException($"Only a single {nameof(ExecAction)} is supported unless the {nameof(PowerShellConversion)} property includes the {nameof(PowerShellActionPlatformOption.Version1)} value."); v1Actions.Insert(index, action); SaveV1Actions(); } if (!inV2set) { OnNotifyPropertyChanged(nameof(Count)); OnNotifyPropertyChanged(IndexerName); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, action, index)); } } /// Removes the first occurrence of a specific object from the . /// The object to remove from the . /// /// true if was successfully removed from the ; otherwise, false. This method /// also returns false if is not found in the original . /// public bool Remove([NotNull] Action item) { var idx = IndexOf(item); if (idx != -1) { try { RemoveAt(idx); return true; } catch { } } return false; } /// Removes the action at a specified index. /// Index of action to remove. /// Index out of range. public void RemoveAt(int index) { if (index < 0 || index >= Count) throw new ArgumentOutOfRangeException(nameof(index), index, "Failed to remove action. Index out of range."); var item = this[index].Clone(); if (v2Coll != null) v2Coll.Remove(++index); else { v1Actions.RemoveAt(index); SaveV1Actions(); } if (!inV2set) { OnNotifyPropertyChanged(nameof(Count)); OnNotifyPropertyChanged(IndexerName); CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); } } /// Copies the elements of the to a new array. /// An array containing copies of the elements of the . [NotNull, ItemNotNull] public Action[] ToArray() { var ret = new Action[Count]; CopyTo(ret, 0); return ret; } /// Returns a that represents the actions in this collection. /// A that represents the actions in this collection. public override string ToString() { if (Count == 1) return this[0].ToString(); if (Count > 1) return Properties.Resources.MultipleActions; return string.Empty; } void ICollection.Add(Action item) => Add(item); int IList.Add(object value) { Add((Action)value); return Count - 1; } bool IList.Contains(object value) => Contains((Action)value); void ICollection.CopyTo(Array array, int index) { if (array != null && array.Rank != 1) throw new RankException("Multi-dimensional arrays are not supported."); var src = new Action[Count]; CopyTo(src, 0); Array.Copy(src, 0, array, index, Count); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); System.Xml.Schema.XmlSchema IXmlSerializable.GetSchema() => null; int IList.IndexOf(object value) => IndexOf((Action)value); void IList.Insert(int index, object value) => Insert(index, (Action)value); void IXmlSerializable.ReadXml(System.Xml.XmlReader reader) { reader.ReadStartElement(XmlSerializationHelper.GetElementName(this), TaskDefinition.tns); Context = reader.GetAttribute("Context"); while (reader.MoveToContent() == System.Xml.XmlNodeType.Element) { var a = Action.CreateAction(Action.TryParse(reader.LocalName == "Exec" ? "Execute" : reader.LocalName, TaskActionType.Execute)); XmlSerializationHelper.ReadObject(reader, a); Add(a); } reader.ReadEndElement(); } void IList.Remove(object value) => Remove((Action)value); void IXmlSerializable.WriteXml(System.Xml.XmlWriter writer) { // TODO:FIX if (!string.IsNullOrEmpty(Context)) writer.WriteAttributeString("Context", Context); foreach (var item in this) XmlSerializationHelper.WriteObject(writer, item); } internal void ConvertUnsupportedActions() { if (TaskService.LibraryVersion.Minor > 3 && SupportV2Conversion) { for (var i = 0; i < Count; i++) { var action = this[i]; var bindable = action as IBindAsExecAction; if (bindable != null && !(action is ComHandlerAction)) this[i] = ExecAction.ConvertToPowerShellAction(action); } } } private List GetV1Actions() { var ret = new List(); if (v1Task != null && v1Task.GetDataItem("ActionType") != "EMPTY") { var exec = new ExecAction(v1Task); var items = exec.ParsePowerShellItems(); if (items != null) { if (items.Length == 2 && items[0] == "MULTIPLE") { PowerShellConversion |= PowerShellActionPlatformOption.Version1; var mc = System.Text.RegularExpressions.Regex.Matches(items[1], @"<# (?\w+):(?\w+) #>\s*(?[^<#]*)\s*"); foreach (System.Text.RegularExpressions.Match ms in mc) { var a = Action.ActionFromScript(ms.Groups["t"].Value, ms.Groups["c"].Value); if (a != null) { if (ms.Groups["id"].Value != "NO_ID") a.Id = ms.Groups["id"].Value; ret.Add(a); } } } else ret.Add(ExecAction.ConvertFromPowerShellAction(exec)); } else if (!string.IsNullOrEmpty(exec.Path)) { ret.Add(exec); } } return ret; } /// Called when a property has changed to notify any attached elements. /// Name of the property. private void OnNotifyPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); private void SaveV1Actions() { if (v1Task == null) throw new ArgumentNullException(nameof(v1Task)); if (v1Actions.Count == 0) { v1Task.SetApplicationName(string.Empty); v1Task.SetParameters(string.Empty); v1Task.SetWorkingDirectory(string.Empty); v1Task.SetDataItem("ActionId", null); v1Task.SetDataItem("ActionType", "EMPTY"); } else if (v1Actions.Count == 1) { if (!SupportV1Conversion && v1Actions[0].ActionType != TaskActionType.Execute) throw new NotV1SupportedException($"Only a single {nameof(ExecAction)} is supported unless the {nameof(PowerShellConversion)} property includes the {nameof(PowerShellActionPlatformOption.Version1)} value."); v1Task.SetDataItem("ActionType", null); v1Actions[0].Bind(v1Task); } else { if (!SupportV1Conversion) throw new NotV1SupportedException($"Only a single {nameof(ExecAction)} is supported unless the {nameof(PowerShellConversion)} property includes the {nameof(PowerShellActionPlatformOption.Version1)} value."); // Build list of internal PowerShell scripts var sb = new System.Text.StringBuilder(); foreach (var item in v1Actions) sb.Append($"<# {item.Id ?? "NO_ID"}:{item.ActionType} #> {item.GetPowerShellCommand()} "); // Build and save PS ExecAction var ea = ExecAction.CreatePowerShellAction("MULTIPLE", sb.ToString()); ea.Bind(v1Task); v1Task.SetDataItem("ActionId", null); v1Task.SetDataItem("ActionType", "MULTIPLE"); } } private void UnconvertUnsupportedActions() { if (TaskService.LibraryVersion.Minor > 3) { for (var i = 0; i < Count; i++) { var action = this[i] as ExecAction; if (action != null) { var newAction = Action.ConvertFromPowerShellAction(action); if (newAction != null) this[i] = newAction; } } } } } }