// Copyright Keysight Technologies 2012-2019 // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, you can obtain one at http://mozilla.org/MPL/2.0/. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using System.Threading; using System.Collections.Concurrent; namespace OpenTap { /// /// Indicates how a IResource property should be handled when being opened or closed. /// public enum ResourceOpenBehavior { /// /// The resources pointed to by this property will be opened in sequence, so any referenced resources are open before Open() and until after Close(). /// /// This is the default behavior Before, /// /// Indicates that a resource property on a resource can be opened in parallel with the resource itself. /// InParallel, /// /// Do not try to open the resource referenced by this property. /// Ignore } /// /// Indicates how a IResource property should be handled when being opened or closed. /// By default the resources will be opened in sequence, so any referenced resources are open before Open() and until after Close(). /// [AttributeUsage(AttributeTargets.Property)] public class ResourceOpenAttribute : Attribute { /// /// Behavior of how the resource should be handled. /// public readonly ResourceOpenBehavior Behavior; /// /// Creates a new ResourceOpen attribute for a resource property. /// /// public ResourceOpenAttribute(ResourceOpenBehavior behavior) { this.Behavior = behavior; } } /// /// This indicates what stage a testplan execution is at. /// public enum TestPlanExecutionStage { /// /// Indicates that a testplan is being opened. /// Open, /// /// Indicates that a testplan is starting to execute. /// /// Implies that the testplan already is open. Execute, /// /// Indicates that a teststep is about to run it's PrePlanRun. /// PrePlanRun, /// /// Indicates that a teststep is about to be run. /// Run, /// /// Indicates that a teststep is about to run it's PostPlanRun. /// PostPlanRun, } /// /// A resource manager implements this interface to be able to control how resources are opened and closed during a testplan execution. /// public interface IResourceManager : ITapPlugin { /// /// This event is triggered when a resource is opened. The event may block in which case the resource will remain open for the entire call. /// event Action ResourceOpened; /// /// Get a snapshot of all currently opened resources. /// IEnumerable Resources { get; } /// /// Sets the resources that should always be opened when the testplan is. /// IEnumerable StaticResources { get; set; } /// /// This property should be set to all teststeps that are enabled to be run. /// List EnabledSteps { get; set; } /// /// Waits for all the resources that have been signalled to open to be opened. /// /// Used to cancel the wait early. void WaitUntilAllResourcesOpened(CancellationToken cancellationToken); /// /// Waits for the specific resources to be open. /// /// Used to cancel the wait early. /// void WaitUntilResourcesOpened(CancellationToken cancellationToken, params IResource[] targets); /// /// Signals that an action is beginning. /// /// The planrun for the currently executing testplan. /// The item affected by the current action. This can be either a testplan or a teststep. /// The stage that is beginning. /// Used to cancel the step early. void BeginStep(TestPlanRun planRun, ITestStepParent item, TestPlanExecutionStage stage, CancellationToken cancellationToken); /// /// Signals that an action has completed. /// /// The item affected by the current action. This can be either a testplan or a teststep. /// The stage that was just completed. void EndStep(ITestStepParent item, TestPlanExecutionStage stage); } /// /// Utility functions shared by and . /// internal static class ResourceManagerUtils { struct EnumerableKey { readonly int key; readonly object[] items; public EnumerableKey(object[] items) { this.items = items; key = 3275321; foreach (var item in items) key = ((item?.GetHashCode() ?? 0) + key) * 3275321; } public override int GetHashCode() => key; public override bool Equals(object obj) { if (obj is EnumerableKey other) return other.key == key && other.items.SequenceEqual(items); return false; } } static List GetResourceNodesNoCache(object[] source) { var resources = new ResourceDependencyAnalyzer().GetAllResources(source, out bool analysisError); if (analysisError) { throw new OperationCanceledException("Error while analyzing dependencies between resources."); } return resources; } class ResourceNodeCache : ICacheOptimizer { public static readonly ThreadField>> Cache = new ThreadField>>(); public void LoadCache() => Cache.Value = new Dictionary>(); public void UnloadCache() => Cache.Value = null; } /// /// Gets ResourceNodes for all resources. This includes the ones from and . /// public static List GetResourceNodes(IEnumerable _source) { var source = _source.ToArray(); var cache = ResourceNodeCache.Cache.Value; if (cache != null) { var key = new EnumerableKey(source); lock (cache) { if (cache.TryGetValue(key, out List result)) return result; result = GetResourceNodesNoCache(source); cache[key] = result; return result; } } return GetResourceNodesNoCache(source); } } /// /// Manages the asynchronous opening and closing of s in separate threads. /// Resources are opened and closed in order depending on dependencies between them. /// [Display("Default", Order: 0)] internal class ResourceTaskManager : IResourceManager { /// Prints a friendly name. public override string ToString() => "Default Resource Manager"; static readonly TraceSource log = Log.CreateSource("Resources"); readonly List openedResources = new(); readonly LockManager lockManager = new(); // open tasks is the main work of opening a resource. readonly ConcurrentDictionary openTasks = new(); // finally tasks are for notifying that resources has been opened. readonly ConcurrentDictionary finallyTasks = new(); /// /// This event is triggered when a resource is opened. The event may block in which case the resource will remain open for the entire call. /// public event Action ResourceOpened; internal static TraceSource GetLogSource(IResource res) { return Log.GetOwnedSource(res) ?? Log.CreateSource(res.Name ?? "Resource"); } void OpenResource(ResourceNode node, WaitHandle canStart) { canStart.WaitOne(); var taskArray = node.StrongDependencies.Select(dep => openTasks[dep]).ToArray(); Task.WaitAll(taskArray); var sw = Stopwatch.StartNew(); var resourceLog = GetLogSource(node.Resource); try { ResourcePreOpenEvent.Invoke(node.Resource); node.Resource.Open(); resourceLog.Info(sw, "Resource \"{0}\" opened.", node.Resource); // when there are no weak dependencies just invoke ResourceOpened directly. if (node.WeakDependencies.Count <= 0) { ResourceOpened?.Invoke(node.Resource); return; } // ...otherwise, we cannot wait for weak dependencies as part of the 'OpenResource' work. // Doing that will cause a dead-lock with circular resource references. // ResourceOpened should only be invoked after weak dependencies has been opened mostly for legacy reasons. // So we wait for weak deps and call ReosurceOpened in a new worker thread. var weakDeps = node.WeakDependencies.Select(dep => openTasks[dep]).ToArray(); finallyTasks[node.Resource] = TapThread.StartAwaitable(() => { try { Task.WaitAll(weakDeps); ResourceOpened?.Invoke(node.Resource); } catch (Exception ex) { string msg = $"Error while opening resource \"{node.Resource}\""; throw new ExceptionCustomStackTrace(msg, null, ex); } }); } catch (Exception ex) { string msg = $"Error while opening resource \"{node.Resource}\""; throw new ExceptionCustomStackTrace(msg, null, ex); } } /// Waits for a specific resource to be open. /// Used to cancel the wait early. /// The resources that we wait to be opened. public void WaitUntilResourcesOpened(CancellationToken cancellationToken, params IResource[] targets) { try { // successfully completed tasks are not worth waiting for. // waiting for unsuccessfully completed tasks gives us a useful error. var waitFor = targets.Select(target => openTasks[target]) .Where(task => !task.IsCompleted || task.IsFaulted).ToArray(); if (waitFor.Length > 0) { // WaitAll waits until all finish even if an exception occurs in one task. Task.WaitAll(waitFor, cancellationToken); } var waitForFinallyTasks = targets .Where(finallyTasks.ContainsKey) .Select(target => finallyTasks[target]) .Where(task => !task.IsCompleted || task.IsFaulted).ToArray(); if(waitForFinallyTasks.Length > 0) Task.WaitAll(waitForFinallyTasks, cancellationToken); } catch (OperationCanceledException) { } catch (Exception e) { e.RethrowInner(); } } /// /// Waits for all the resources to be opened. /// /// Used to cancel the wait early public void WaitUntilAllResourcesOpened(CancellationToken cancellationToken) { WaitUntilResourcesOpened(cancellationToken, openTasks.Keys.ToArray()); } /// /// Get a snapshot of all currently opened resources. /// public IEnumerable Resources => openTasks.Keys; /// /// Sets the resources that should always be opened when the test plan is. /// public IEnumerable StaticResources { get; set; } /// /// This property should be set to all test steps that are enabled to be run. /// public List EnabledSteps { get; set; } /// /// Blocks the thread while closing all resources in parallel. /// void CloseAllResources() { Dictionary> dependencies = openedResources.ToDictionary(r => r.Resource, r => new List()); // this dictionary will hold resources with dependencies (keys) and what resources depend on them (values) foreach (var n in openedResources) foreach (var dep in n.StrongDependencies) dependencies[dep].Add(n.Resource); Dictionary closeTasks = new Dictionary(); foreach (var r in openedResources) closeTasks[r.Resource] = new Task(o => { ResourceNode res = (ResourceNode) o; // Wait for the resource to open to open before closing it. // in rare cases, another instrument failing open will cause close to be called. try { openTasks[res.Resource].Wait(); } catch { } // wait for resources that depend on this resource (res) to close before closing this Task.WaitAll(dependencies[res.Resource].Select(x => closeTasks[x]).ToArray()); var resourceLog = GetLogSource(res.Resource); Stopwatch timer = Stopwatch.StartNew(); try { res.Resource.Close(); } catch (Exception e) { log.Error("Error while closing \"{0}\": {1}", res.Resource.Name, e.Message); log.Debug(e); } if (resourceLog != null) resourceLog.Info(timer, "Resource \"{0}\" closed.", res.Resource); }, r); var closeTaskArray = closeTasks.Values.ToArray(); closeTaskArray.ForEach(t => t.Start()); void complainAboutWait() { log.Debug("Waiting for resources to close:"); foreach (var res in openTasks.Keys) { if (res.IsConnected) log.Debug(" - {0}", res); } } using (TimeoutOperation.Create(complainAboutWait)) { Task.WaitAll(closeTaskArray); } } void BeginOpenResources(List resources, CancellationToken cancellationToken) { lockManager.BeforeOpen(resources, cancellationToken); { // check if any resources that has been deleted from InstrumentSettings or DutSettings // are still being referred to. var untouched = new IComponentSettingsList[]{InstrumentSettings.Current, DutSettings.Current, ResultSettings.Current} .SelectMany(x => x.GetRemovedAliveResources()) .ToHashSet(); foreach (var res in resources) { if (untouched.Contains(res.Resource)) { if(res.Depender != null) throw new Exception($"Deleted resource '{res.Resource}' is in use by {res.Depender.DeclaringType}."); throw new Exception($"Deleted resource '{res.Resource}' is in use."); } } } // Check null resources if (resources.Any(res => res.Resource == null)) { EnabledSteps.ForEach(step => step.CheckResources()); // Now check resources since we know one of them should have a null resource resources.ForEach(res => { if (res.StrongDependencies.Contains(null) || res.WeakDependencies.Contains(null)) throw new Exception(String.Format("Resource property not set on resource {0}. Please configure resource.", res.Resource)); }); } else { // Open all resources asynchronously var wait = new ManualResetEventSlim(false); foreach (ResourceNode r in resources) { if (openTasks.ContainsKey(r.Resource)) continue; openedResources.Add(r); // async used to avoid blocking the thread while waiting for tasks. openTasks[r.Resource] = TapThread.StartAwaitable(() => OpenResource(r, wait.WaitHandle)); } wait.Set(); } } /// /// Signals that an action is beginning. /// /// The planrun for the currently executing testplan. /// The item affected by the current action. This can be either a testplan or a teststep. /// The stage that is beginning. /// Used to cancel the step early. public void BeginStep(TestPlanRun planRun, ITestStepParent item, TestPlanExecutionStage stage, CancellationToken cancellationToken) { switch (stage) { case TestPlanExecutionStage.Execute: if (item is TestPlan testPlan) { var resources = ResourceManagerUtils.GetResourceNodes(StaticResources.Cast().Concat(EnabledSteps)); // Proceed to open resources in case they have been changed or closed since last opening/executing the testplan. // In case any are null, we need to do this before the resource prompt to allow a ILockManager implementation to // set the resource first. if (resources.Any(r => r.Resource == null)) BeginOpenResources(resources, cancellationToken); testPlan.StartResourcePromptAsync(planRun, resources.Select(res => res.Resource)); if (resources.Any(r => openTasks.ContainsKey(r.Resource) == false)) BeginOpenResources(resources, cancellationToken); } break; case TestPlanExecutionStage.Open: if (item is TestPlan) { var resources = ResourceManagerUtils.GetResourceNodes(StaticResources.Cast().Concat(EnabledSteps)); BeginOpenResources(resources, cancellationToken); } break; case TestPlanExecutionStage.Run: case TestPlanExecutionStage.PrePlanRun: { bool openCompletedWithSuccess = openTasks.Values.All(x => x.Status == TaskStatus.RanToCompletion); if (!openCompletedWithSuccess) { // open did not complete or threw an exception. using (TimeoutOperation.Create(() => TestPlan.PrintWaitingMessage(Resources))) WaitUntilAllResourcesOpened(cancellationToken); } break; } case TestPlanExecutionStage.PostPlanRun: break; } } /// /// Signals that an action has completed. /// /// The item affected by the current action. This can be either a testplan or a teststep. /// The stage that was just completed. public void EndStep(ITestStepParent item, TestPlanExecutionStage stage) { switch (stage) { case TestPlanExecutionStage.Open: if (item is TestPlan) { try { CloseAllResources(); } finally { lockManager.AfterClose(openedResources, CancellationToken.None); } } break; } } } /// /// Opens resources in a lazy way only before teststeps or global plan resources actually need them. /// [Display("Short Lived Connections", "Opens resources only right before they are needed (e.g. before an individual test step starts). And closes them again immediately after.", Order: 1)] internal class LazyResourceManager : IResourceManager { /// Prints a friendly name. /// public override string ToString() => "Short Lived Connections"; /// Manages the state for a single resource. For example an Instrument or Result Listener. class ResourceInfo { enum ResourceState { Reset, Opening, Open, Closing } ResourceState state { get; set; } = ResourceState.Reset; // counts up on open and down on close. When it reaches 0, it can finally be closed. int referenceCount; // lock used for managing local state. For example reference count. readonly object lockObj = new object(); Task openTask = Task.CompletedTask; Task closeTask = Task.CompletedTask; public ResourceNode ResourceNode { get; } public ResourceInfo(ResourceNode resourceNode) { ResourceNode = resourceNode; } public Task RequestSteady() { lock (lockObj) switch(state) { case ResourceState.Opening: return openTask; default: return Task.CompletedTask; } } public bool ShouldBeOpen() { lock (lockObj) return referenceCount > 0; } void OpenResource(LazyResourceManager requester, CancellationToken cancellationToken) { var node = ResourceNode; foreach (var dep in node.StrongDependencies) { if (dep == null) continue; requester.RequestResourceOpen(dep, cancellationToken).Wait(cancellationToken); } var sw = Stopwatch.StartNew(); var resourceLog = ResourceTaskManager.GetLogSource(node.Resource); try { try { ResourcePreOpenEvent.Invoke(node.Resource); node.Resource.Open(); resourceLog.Info(sw, "Resource \"{0}\" opened.", node.Resource); } finally { lock (lockObj) if (state == ResourceState.Opening) state = ResourceState.Open; } foreach (var dep in node.WeakDependencies) { if (dep == null) continue; requester.RequestResourceOpen(dep, cancellationToken).Wait(cancellationToken); } } catch (Exception ex) { string msg = $"Error while opening resource \"{node.Resource}\""; throw new ExceptionCustomStackTrace(msg, null, ex); } requester.ResourceOpenedCallback(node.Resource); } public Task RequestOpen(LazyResourceManager requester, CancellationToken cancellationToken) { lock (lockObj) { referenceCount++; switch (state) { case ResourceState.Reset: state = ResourceState.Opening; return openTask = TapThread.StartAwaitable(() => OpenResource(requester, cancellationToken)); case ResourceState.Opening: return openTask; case ResourceState.Open: return Task.CompletedTask; case ResourceState.Closing: return closeTask.ContinueWith(t => RequestOpen(requester, cancellationToken).Wait()); } return Task.CompletedTask; } } public Task RequestClose(LazyResourceManager requester) { lock (lockObj) { referenceCount--; if (referenceCount == 0) switch (state) { case ResourceState.Reset: case ResourceState.Closing: throw new Exception("Should never happen"); case ResourceState.Opening: case ResourceState.Open: { state = ResourceState.Closing; return closeTask = TapThread.StartAwaitable(() => { try { // wait for the resource to open before close. requester.resources[ResourceNode.Resource].openTask?.Wait(); } catch { } Task.WaitAll(ResourceNode.WeakDependencies.Select(requester.RequestResourceClose).ToArray()); var reslog = ResourceTaskManager.GetLogSource(ResourceNode.Resource); Stopwatch timer = Stopwatch.StartNew(); try { ResourceNode.Resource.Close(); } catch (Exception e) { reslog.Error("Error while closing \"{0}\": {1}", ResourceNode.Resource.Name, e.Message); reslog.Debug(e); } reslog.Info(timer, "Resource \"{0}\" closed.", ResourceNode.Resource); Task.WaitAll(ResourceNode.StrongDependencies.Select(requester.RequestResourceClose).ToArray()); state = ResourceState.Reset; }); } } return Task.CompletedTask; } } } readonly object resourceLock = new object(); readonly Dictionary resources = new Dictionary(); readonly Dictionary> resourceDependencies = new Dictionary>(); readonly List resourceWithBeforeOpenCalled = new List(); readonly LockManager lockManager = new LockManager(); void OpenResources(List toOpen, CancellationToken cancellationToken) { toOpen.RemoveAll(x => x.Resource == null); lock (resourceLock) foreach (var extra in toOpen) if (!resources.ContainsKey(extra.Resource)) resources[extra.Resource] = new ResourceInfo(extra); try { Task.WaitAll(toOpen.Select(res => RequestResourceOpen(res.Resource, cancellationToken)).ToArray(), cancellationToken); } catch(OperationCanceledException) { } } void CloseResources(IEnumerable toClose) { if (toClose.Any()) { Task.WaitAll(toClose.Select(RequestResourceClose).ToArray()); lock (resourceWithBeforeOpenCalled) { var nodes = resourceWithBeforeOpenCalled.Where(rn => toClose.Contains(rn.Resource)).ToArray(); lockManager.AfterClose(nodes, CancellationToken.None); foreach (var node in nodes) resourceWithBeforeOpenCalled.Remove(node); } } } /// /// This property should be set to all test steps that are enabled to be run. /// public List EnabledSteps { get; set; } /// /// This event is triggered when a resource is opened. The event may block in which case the resource will remain open for the entire call. /// public event Action ResourceOpened; /// /// Get a snapshot of all currently opened resources. /// public IEnumerable Resources { get { lock (resourceLock) return resources.Where(x => x.Value.ShouldBeOpen()).Select(x => x.Key).ToList(); } } /// /// Sets the resources that should always be opened when the testplan is. /// public IEnumerable StaticResources { get; set; } /// /// Signals that an action is beginning. /// /// The planrun for the currently executing testplan. /// The item affected by the current action. This can be either a testplan or a teststep. /// The stage that is beginning. /// Used to cancel the step early. public void BeginStep(TestPlanRun planRun, ITestStepParent item, TestPlanExecutionStage stage, CancellationToken cancellationToken) { switch (stage) { case TestPlanExecutionStage.Open: case TestPlanExecutionStage.Execute: { var resources = ResourceManagerUtils.GetResourceNodes(StaticResources); if (item is TestPlan plan && stage == TestPlanExecutionStage.Execute) { // Prompt for metadata for all resources, not only static ones. var testPlanResources = ResourceManagerUtils.GetResourceNodes(EnabledSteps); plan.StartResourcePromptAsync(planRun, resources.Concat(testPlanResources).Select(res => res.Resource)); } if (resources.All(r => r.Resource?.IsConnected ?? false)) return; // Call ILockManagers before checking for null try { lockManager.BeforeOpen(resources, cancellationToken); } finally { lock (resourceWithBeforeOpenCalled) { resourceWithBeforeOpenCalled.AddRange(resources); } } try { // Check null resources if (resources.Any(res => res.Resource == null)) { // Now check resources since we know one of them should have a null resource resources.ForEach(res => { if (res.StrongDependencies.Contains(null) || res.WeakDependencies.Contains(null)) throw new Exception(String.Format("Resource property not set on resource {0}. Please configure resource.", res.Resource)); }); } } finally { OpenResources(resources, cancellationToken); } break; } case TestPlanExecutionStage.Run: if (item is ITestStep step) { var resources = ResourceManagerUtils.GetResourceNodes(new List { step }); if (resources.Any()) { // Call ILockManagers before checking for null try { lockManager.BeforeOpen(resources, cancellationToken); } finally { lock (resourceWithBeforeOpenCalled) { resourceWithBeforeOpenCalled.AddRange(resources); } } try { // Check null resources if (resources.Any(res => res.Resource == null)) { step.CheckResources(); // Now check resources since we know one of them should have a null resource resources.ForEach(res => { if (res.StrongDependencies.Contains(null) || res.WeakDependencies.Contains(null)) throw new Exception(String.Format("Resource property not set on resource {0}. Please configure resource.", res.Resource)); }); } } finally { lock (resourceLock) { resourceDependencies[step] = resources.Select(x => x.Resource).ToList(); } OpenResources(resources, cancellationToken); } WaitHandle.WaitAny(new[] { planRun.PromptWaitHandle, planRun.MainThread.AbortToken.WaitHandle }); } } break; } } /// /// Signals that an action has completed. /// /// The item affected by the current action. This can be either a testplan or a teststep. /// The stage that was just completed. public void EndStep(ITestStepParent item, TestPlanExecutionStage stage) { switch (stage) { case TestPlanExecutionStage.Open: case TestPlanExecutionStage.Execute: lock (resourceLock) CloseResources(Resources); break; case TestPlanExecutionStage.Run: if (item is ITestStep step) { if (resourceDependencies.TryGetValue(step, out List usedResourcesRaw)) { var usedResources = usedResourcesRaw.Where(r => r is IResource).ToArray(); lock (resourceLock) { resourceDependencies.Remove(step); } CloseResources(usedResources); } } break; } } private void WaitFor(CancellationToken cancellationToken, IEnumerable targets) { try { Task[] steadyTasks; lock(resourceLock) steadyTasks = targets.Select(x => resources[x].RequestSteady()).ToArray(); Task.WaitAll(steadyTasks, cancellationToken); } catch (OperationCanceledException) { // Just exit since the abort exception will be handled elsewhere. } catch (KeyNotFoundException) { foreach (var target in targets) { if (resources.ContainsKey(target) == false) { throw new ArgumentException($"Resource '{target}' is not used in the test plan.", "targets"); } } throw; } } /// /// Waits for all the resources that have been signalled to open to be opened. /// /// Used to cancel the wait early. public void WaitUntilAllResourcesOpened(CancellationToken cancellationToken) { WaitFor(cancellationToken, Resources); } /// /// Waits for the specific resources to be open. /// /// Used to cancel the wait early. /// public void WaitUntilResourcesOpened(CancellationToken cancellationToken, params IResource[] targets) { WaitFor(cancellationToken, targets); } Task RequestResourceOpen(IResource resource, CancellationToken cancellationToken) { lock (resourceLock) return resources[resource].RequestOpen(this, cancellationToken); } Task RequestResourceClose(IResource resource) { lock (resourceLock) return resources[resource].RequestClose(this); } void ResourceOpenedCallback(IResource resource) { ResourceOpened?.Invoke(resource); } } }