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