using System; using System.ComponentModel; using System.ComponentModel.Design; using System.Diagnostics.Eventing.Reader; using System.IO; using JetBrains.Annotations; namespace Microsoft.Win32.TaskScheduler { /// /// Information about the task event. /// [PublicAPI] public class TaskEventArgs : EventArgs { private readonly TaskService taskService; internal TaskEventArgs([NotNull] TaskEvent evt, TaskService ts = null) { TaskEvent = evt; TaskPath = evt.TaskPath; taskService = ts; } /// /// If possible, gets the task associated with this event. /// /// /// The task or null if unable to retrieve. /// public Task Task { get { try { return taskService?.GetTask(TaskPath); } catch { return null; } } } /// /// Gets the . /// /// /// The TaskEvent. /// [NotNull] public TaskEvent TaskEvent { get; } /// /// Gets the task name. /// /// /// The task name. /// public string TaskName => Path.GetFileName(TaskPath); /// /// Gets the task path. /// /// /// The task path. /// public string TaskPath { get; } } /// /// Watches system events related to tasks and issues a event when the filtered conditions are met. /// Only available for Task Scheduler 2.0 on Windows Vista or Windows Server 2003 and later. /// /// Sometimes, a developer will need to know about events as they occur. In this case, they can use the TaskEventWatcher component that enables the developer to watch a task, a folder, or the entire system for filtered events. /// /// Below is information on how to watch a folder for all task events. For a complete example, look at this sample project: TestTaskWatcher.zip /// [DefaultEvent(nameof(EventRecorded)), DefaultProperty(nameof(Folder))] #if DESIGNER [Designer(typeof(Design.TaskEventWatcherDesigner))] #endif [ToolboxItem(true), Serializable] [PublicAPI] public class TaskEventWatcher : Component, ISupportInitialize { private const string root = "\\"; private const string star = "*"; private static readonly TimeSpan MaxV1EventLapse = TimeSpan.FromSeconds(1); private bool disposed; private bool enabled; private string folder = root; private bool includeSubfolders; private bool initializing; private StandardTaskEventId lastId = 0; private DateTime lastIdTime = DateTime.MinValue; private TaskService ts; private FileSystemWatcher v1Watcher; private EventLogWatcher watcher; private ISynchronizeInvoke synchronizingObject; /// /// Initializes a new instance of the class. If other /// properties are not set, this will watch for all events for all tasks on the local machine. /// public TaskEventWatcher() : this(TaskService.Instance) { } /// /// Initializes a new instance of the class watching only /// those events for the task with the provided path on the local machine. /// /// The full path (folders and name) of the task to watch. /// The task service. /// $Invalid task name: {taskPath} public TaskEventWatcher(string taskPath, TaskService taskService = null) : this(taskService ?? TaskService.Instance) { InitTask(taskPath); } /// /// Initializes a new instance of the class watching only /// those events for the specified task. /// /// The task to watch. /// Occurs if the is null. public TaskEventWatcher([NotNull] Task task) : this(task?.TaskService) { if (task == null) throw new ArgumentNullException(nameof(task)); InitTask(task); } /// /// Initializes a new instance of the class watching only those events for /// the tasks whose name matches the in the specified /// and optionally all subfolders. /// /// The task folder to watch. /// The filter for task names using standard file system wildcards. Use "*" to include all tasks. /// if set to true include events from tasks subfolders. /// Occurs if the is null. public TaskEventWatcher([NotNull] TaskFolder taskFolder, string taskFilter = "*", bool includeSubfolders = false) : this(taskFolder?.TaskService) { if (taskFolder == null) throw new ArgumentNullException(nameof(taskFolder)); InitTask(taskFolder, taskFilter, includeSubfolders); } /// /// Initializes a new instance of the class. /// /// The task folder to watch. /// The filter for task names using standard file system wildcards. Use "*" to include all tasks. /// if set to true include events from tasks subfolders. /// The task service. public TaskEventWatcher(string folder, string taskFilter, bool includeSubfolders, TaskService taskService = null) : this(taskService ?? TaskService.Instance) { InitTask(folder, taskFilter, includeSubfolders); } /// /// Initializes a new instance of the class on a remote machine. /// /// Name of the remote machine. /// The task path. /// The domain of the user account. /// The user name with permissions on the remote machine. /// The password for the user. public TaskEventWatcher(string machineName, string taskPath, string domain = null, string user = null, string password = null) : this(new TaskService(machineName, user, domain, password)) { InitTask(taskPath); } /// /// Initializes a new instance of the class on a remote machine. /// /// Name of the remote machine. /// The task folder to watch. /// The filter for task names using standard file system wildcards. Use "*" to include all tasks. /// if set to true include events from tasks subfolders. /// The domain of the user account. /// The user name with permissions on the remote machine. /// The password for the user. public TaskEventWatcher(string machineName, string folder, string taskFilter = "*", bool includeSubfolders = false, string domain = null, string user = null, string password = null) : this(new TaskService(machineName, user, domain, password)) { InitTask(folder, taskFilter, includeSubfolders); } private TaskEventWatcher(TaskService ts) { TaskService = ts; Filter = new EventFilter(this); } /// /// Occurs when a task or the task engine records an event. /// [Category("Action"), Description("Event recorded by a task or the task engine.")] public event EventHandler EventRecorded; /// /// Gets or sets a value indicating whether the component is enabled. /// /// /// true if enabled; otherwise, false. /// [DefaultValue(false), Category("Behavior"), Description("Indicates whether the component is enabled.")] public bool Enabled { get { return enabled; } set { if (enabled != value) { System.Diagnostics.Debug.WriteLine($"TaskEventWather: Set {nameof(Enabled)} = {value}"); enabled = value; if (!IsSuspended()) { if (enabled) StartRaisingEvents(); else StopRaisingEvents(); } } } } /// /// Gets the filter for this . /// /// /// The filter. /// [DesignerSerializationVisibility(DesignerSerializationVisibility.Content), Category("Behavior"), Description("Indicates the filter for the watcher.")] public EventFilter Filter { get; } /// /// Gets or sets the folder to watch. /// /// /// The folder path to watch. This value should include the leading "\" to indicate the root folder. /// /// Thrown if the folder specified does not exist or contains invalid characters. [DefaultValue(root), Category("Behavior"), Description("Indicates the folder to watch.")] public string Folder { get { return folder; } set { if (string.IsNullOrEmpty(value)) value = root; if (!value.EndsWith("\\")) value += "\\"; if (string.Compare(folder, value, StringComparison.OrdinalIgnoreCase) == 0) return; if ((DesignMode && (value.IndexOfAny(new[] { '*', '?' }) != -1 || value.IndexOfAny(Path.GetInvalidPathChars()) != -1)) || (TaskService.GetFolder(value == root ? value : value.TrimEnd('\\')) == null)) throw new ArgumentException($"Invalid folder name: {value}"); folder = value; } } /// /// Gets or sets a value indicating whether to include events from subfolders when the /// property is set. If the property is set, /// this property is ignored. /// /// true if include events from subfolders; otherwise, false. [DefaultValue(false), Category("Behavior"), Description("Indicates whether to include events from subfolders.")] public bool IncludeSubfolders { get { return includeSubfolders; } set { if (includeSubfolders == value) return; includeSubfolders = value; Restart(); } } /// /// Gets or sets the synchronizing object. /// /// /// The synchronizing object. /// [Browsable(false), DefaultValue(null)] public ISynchronizeInvoke SynchronizingObject { get { if (synchronizingObject == null && DesignMode) { var so = ((IDesignerHost)GetService(typeof(IDesignerHost)))?.RootComponent as ISynchronizeInvoke; if (so != null) synchronizingObject = so; } return synchronizingObject; } set { synchronizingObject = value; } } /// /// Gets or sets the name of the computer that is running the Task Scheduler service that the user is connected to. /// [Category("Connection"), Description("The name of the computer to connect to."), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public string TargetServer { get { return TaskService.TargetServer; } set { if (value == null || value.Trim() == string.Empty) value = null; if (string.Compare(value, TaskService.TargetServer, StringComparison.OrdinalIgnoreCase) == 0) return; TaskService.TargetServer = value; Restart(); } } /// /// Gets or sets the instance associated with this event watcher. Setting this value /// will override any values set for , , /// , and and set them to those values in the supplied /// instance. /// /// The TaskService. [Category("Data"), Description("The TaskService for this event watcher.")] public TaskService TaskService { get { return ts; } set { ts = value; Restart(); } } /// /// Gets or sets the user account domain to be used when connecting to the . /// /// The user account domain. [Category("Connection"), Description("The user account domain to be used when connecting."), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public string UserAccountDomain { get { return TaskService.UserAccountDomain; } set { if (value == null || value.Trim() == string.Empty) value = null; if (string.Compare(value, TaskService.UserAccountDomain, StringComparison.OrdinalIgnoreCase) == 0) return; TaskService.UserAccountDomain = value; Restart(); } } /// /// Gets or sets the user name to be used when connecting to the . /// /// The user name. [Category("Connection"), Description("The user name to be used when connecting."), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public string UserName { get { return TaskService.UserName; } set { if (value == null || value.Trim() == string.Empty) value = null; if (string.Compare(value, TaskService.UserName, StringComparison.OrdinalIgnoreCase) == 0) return; TaskService.UserName = value; Restart(); } } /// /// Gets or sets the user password to be used when connecting to the . /// /// The user password. [Category("Connection"), Description("The user password to be used when connecting."), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public string UserPassword { get { return TaskService.UserPassword; } set { if (value == null || value.Trim() == string.Empty) value = null; if (string.Compare(value, TaskService.UserPassword, StringComparison.OrdinalIgnoreCase) == 0) return; TaskService.UserPassword = value; Restart(); } } /// /// Gets a value indicating if watching is available. /// [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] private bool IsHandleInvalid => IsV1 ? v1Watcher == null : watcher == null; private static bool IsV1 => Environment.OSVersion.Version.Major < 6; /// /// Signals the object that initialization is starting. /// public void BeginInit() { System.Diagnostics.Debug.WriteLine($"TaskEventWather: {nameof(BeginInit)}"); initializing = true; var localEnabled = enabled; StopRaisingEvents(); enabled = localEnabled; TaskService.BeginInit(); } /// /// Signals the object that initialization is complete. /// public void EndInit() { System.Diagnostics.Debug.WriteLine($"TaskEventWather: {nameof(EndInit)}"); initializing = false; TaskService.EndInit(); if (enabled) StartRaisingEvents(); } /// /// Releases the unmanaged resources used by the FileSystemWatcher and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { try { if (disposing) { StopRaisingEvents(); TaskService = null; } else { StopListening(); } } finally { disposed = true; base.Dispose(disposing); } } /// /// Fires the event. /// /// The sender. /// The instance containing the event data. protected virtual void OnEventRecorded(object sender, TaskEventArgs e) { var h = EventRecorded; if (h == null) return; if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) SynchronizingObject.BeginInvoke(h, new object[] { this, e }); else h(sender, e); } private void InitTask([NotNull] Task task) { Filter.TaskName = task.Name; Folder = task.Folder.Path; } private void InitTask(TaskFolder taskFolder, string taskFilter, bool includeSubfolders) { this.includeSubfolders = includeSubfolders; Filter.TaskName = taskFilter; Folder = taskFolder?.Path; } private void InitTask(string taskFolder, string taskFilter, bool includeSubfolders) { this.includeSubfolders = includeSubfolders; Filter.TaskName = taskFilter; Folder = taskFolder; } private void InitTask(string taskPath) { Filter.TaskName = Path.GetFileNameWithoutExtension(taskPath); Folder = Path.GetDirectoryName(taskPath); } private bool IsSuspended() => initializing || DesignMode; private void ReleaseWatcher() { if (IsV1) { if (v1Watcher == null) return; v1Watcher.EnableRaisingEvents = false; v1Watcher.Changed -= Watcher_DirectoryChanged; v1Watcher.Created -= Watcher_DirectoryChanged; v1Watcher.Deleted -= Watcher_DirectoryChanged; v1Watcher.Renamed -= Watcher_DirectoryChanged; v1Watcher = null; } else { if (watcher == null) return; watcher.Enabled = false; watcher.EventRecordWritten -= Watcher_EventRecordWritten; watcher = null; } } private void ResetTaskService() { ts = TaskService.Instance; } private void Restart() { if (IsSuspended() || !enabled) return; System.Diagnostics.Debug.WriteLine($"TaskEventWather: {nameof(Restart)}"); StopRaisingEvents(); StartRaisingEvents(); } private void SetupWatcher() { ReleaseWatcher(); string taskPath = null; if (Filter.Wildcard == null) taskPath = Path.Combine(folder, Filter.TaskName); if (IsV1) { var di = new DirectoryInfo(Environment.GetFolderPath(Environment.SpecialFolder.System)); string dir = di.Parent != null ? Path.Combine(di.Parent.FullName, "Tasks") : "Tasks"; v1Watcher = new FileSystemWatcher(dir, "*.job") { NotifyFilter = NotifyFilters.Size | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Attributes }; v1Watcher.Changed += Watcher_DirectoryChanged; v1Watcher.Created += Watcher_DirectoryChanged; v1Watcher.Deleted += Watcher_DirectoryChanged; v1Watcher.Renamed += Watcher_DirectoryChanged; } else { var log = new TaskEventLog(taskPath, Filter.EventIds, Filter.EventLevels, DateTime.UtcNow, TargetServer, UserAccountDomain, UserName, UserPassword); log.Query.ReverseDirection = false; watcher = new EventLogWatcher(log.Query); watcher.EventRecordWritten += Watcher_EventRecordWritten; } } private bool ShouldSerializeFilter() => Filter.ShouldSerialize(); private bool ShouldSerializeTaskService() => !Equals(TaskService, TaskService.Instance); private void StartRaisingEvents() { if (disposed) throw new ObjectDisposedException(GetType().Name); if (IsSuspended()) return; System.Diagnostics.Debug.WriteLine($"TaskEventWather: {nameof(StartRaisingEvents)}"); enabled = true; SetupWatcher(); if (IsV1) try { v1Watcher.EnableRaisingEvents = true; } catch { } else try { watcher.Enabled = true; } catch { } } private void StopListening() { enabled = false; ReleaseWatcher(); } private void StopRaisingEvents() { System.Diagnostics.Debug.WriteLine($"TaskEventWather: {nameof(StopRaisingEvents)}"); if (IsSuspended()) enabled = false; else if (!IsHandleInvalid) StopListening(); } private void Watcher_DirectoryChanged(object sender, FileSystemEventArgs e) { StandardTaskEventId id = StandardTaskEventId.TaskUpdated; if (e.ChangeType == WatcherChangeTypes.Deleted) id = StandardTaskEventId.TaskDeleted; else if (e.ChangeType == WatcherChangeTypes.Created) id = StandardTaskEventId.JobRegistered; if (lastId == id && DateTime.Now.Subtract(lastIdTime) <= MaxV1EventLapse) return; OnEventRecorded(this, new TaskEventArgs(new TaskEvent(Path.Combine("\\", e.Name.Replace(".job", "")), id, DateTime.Now), TaskService)); lastId = id; lastIdTime = DateTime.Now; } private void Watcher_EventRecordWritten(object sender, EventRecordWrittenEventArgs e) { try { var taskEvent = new TaskEvent(e.EventRecord); System.Diagnostics.Debug.WriteLine("Task event: " + taskEvent.ToString()); // Get the task name and folder if (string.IsNullOrEmpty(taskEvent.TaskPath)) return; int cpos = taskEvent.TaskPath.LastIndexOf('\\'); string name = taskEvent.TaskPath.Substring(cpos + 1); string fld = taskEvent.TaskPath.Substring(0, cpos + 1); // Check folder and name filters if (!string.IsNullOrEmpty(Filter.TaskName) && string.Compare(Filter.TaskName, taskEvent.TaskPath, StringComparison.OrdinalIgnoreCase) != 0) { if (Filter.Wildcard != null && !Filter.Wildcard.IsMatch(name)) return; if (IncludeSubfolders && !fld.StartsWith(folder, StringComparison.OrdinalIgnoreCase)) return; if (!IncludeSubfolders && string.Compare(folder, fld, StringComparison.OrdinalIgnoreCase) != 0) return; } OnEventRecorded(this, new TaskEventArgs(taskEvent, TaskService)); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"{nameof(Watcher_EventRecordWritten)} has failed. Error: {ex.ToString()}"); } } /// /// Holds filter information for a . /// [TypeConverter(typeof(ExpandableObjectConverter)), Serializable] [PublicAPI] public class EventFilter { private string filter = star; private int[] ids; private int[] levels; private readonly TaskEventWatcher parent; internal EventFilter([NotNull] TaskEventWatcher parent) { this.parent = parent; } /// /// Gets or sets an optional array of event identifiers to use when filtering those events that will fire a event. /// /// /// The array of event identifier filters. All know task event identifiers are declared in the enumeration. /// [DefaultValue(null), Category("Filter"), Description("An array of event identifiers to use when filtering.")] public int[] EventIds { get { return ids; } set { if (ids != value) { ids = value; parent.Restart(); } } } /// /// Gets or sets an optional array of event levels to use when filtering those events that will fire a event. /// /// /// The array of event levels. While event providers can define custom levels, most will use integers defined in the System.Diagnostics.Eventing.Reader.StandardEventLevel enumeration. /// [DefaultValue(null), Category("Filter"), Description("An array of event levels to use when filtering.")] public int[] EventLevels { get { return levels; } set { if (levels != value) { levels = value; parent.Restart(); } } } /// /// Gets or sets the task name, which can utilize wildcards, to look for when watching a folder. /// /// A task name or wildcard. [DefaultValue(star), Category("Filter"), Description("A task name, which can utilize wildcards, for filtering.")] public string TaskName { get { return filter; } set { if (string.IsNullOrEmpty(value)) value = star; if (string.Compare(filter, value, StringComparison.OrdinalIgnoreCase) != 0) { filter = value; Wildcard = (value.IndexOfAny(new[] { '?', '*' }) == -1) ? null : new Wildcard(value); parent.Restart(); } } } internal Wildcard Wildcard { get; private set; } = new Wildcard(star); /// /// Returns a that represents this instance. /// /// /// A that represents this instance. /// public override string ToString() => filter + (levels == null ? "" : " +levels") + (ids == null ? "" : " +id's"); internal bool ShouldSerialize() => ids != null || levels != null || filter != star; } } #if DESIGNER namespace Design { internal class TaskEventWatcherDesigner : ComponentDesigner { public override void InitializeNewComponent(IDictionary defaultValues) { base.InitializeNewComponent(defaultValues); var refs = GetService(); var tsColl = refs?.GetReferences(typeof(TaskService)); System.Diagnostics.Debug.Assert(refs != null && tsColl != null && tsColl.Length > 0, "Designer couldn't find host, reference service, or existing TaskService."); if (tsColl != null && tsColl.Length > 0) { TaskEventWatcher tsComp = Component as TaskEventWatcher; TaskService ts = tsColl[0] as TaskService; if (tsComp != null) tsComp.TaskService = ts; } } protected virtual T GetService() => (T)Component?.Site?.GetService(typeof(T)); } } #endif }