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;
}
}
}
}
}
}