/* Copyright (c) Citrix Systems, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms,
* with or without modification, are permitted provided
* that the following conditions are met:
*
* * Redistributions of source code must retain the above
* copyright notice, this list of conditions and the
* following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the
* following disclaimer in the documentation and/or other
* materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
using System;
using System.Threading;
using XenAdmin.Network;
using XenAPI;
using System.Collections.Generic;
using System.Net;
using CookComputing.XmlRpc;
using System.Reflection;
namespace XenAdmin.Actions
{
public class CancellingAction : ActionBase
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
private Session _cancel_session = null;
private Session _session;
///
/// Whether, the last time we checked on the server, this task could be cancelled. This is a cached
/// value, so may be stale.
///
private volatile bool can_cancel = false;
///
/// Whether this operation is being cancelled. This takes precedence over can_cancel.
///
private volatile bool cancelling = false;
///
/// Whether this operation was cancelled or not.
///
private volatile bool cancelled = false;
protected CancellingAction(string title, string description, bool suppressHistory)
: base(title, description, suppressHistory)
{
}
protected CancellingAction(string title, string description, bool suppressHistory, bool completeImmediately)
: base(title, description, suppressHistory, completeImmediately)
{
}
private XenRef _relatedTask;
private readonly object connectionLock = new object();
public sealed override IXenConnection Connection
{
protected set
{
// lock any changes to the connection
// This only affect actions which run across multiple connections
lock (connectionLock)
{
base.Connection = value;
}
}
}
public Session Session
{
get { return _session; }
set { _session = value; }
}
private delegate void SetXenCenterUUIDDelegate(Session session, string _task, string uuid);
private delegate void SetAppliesToDelegate(Session session, string _task, List applies_to);
///
/// The XenAPI.Task object (if any) that corresponds to this action.
///
public XenRef RelatedTask
{
get { return _relatedTask; }
set
{
//Program.AssertOffEventThread();
_relatedTask = value;
if (_relatedTask != null && _session != null)
{
DoWithSessionRetry(ref _session, (SetXenCenterUUIDDelegate)Task.SetXenCenterUUID, _relatedTask.opaque_ref, XenAdminConfigManager.Provider.XenCenterUUID);
DoWithSessionRetry(ref _session, (SetAppliesToDelegate)Task.SetAppliesTo, _relatedTask.opaque_ref, AppliesTo);
RecomputeCanCancel();
}
}
}
public override sealed bool CanCancel
{
get { return !cancelling && can_cancel; }
protected set
{
if (can_cancel != value)
{
can_cancel = value;
OnChanged();
}
}
}
public bool Cancelling
{
get { return cancelling; }
protected set { cancelling = value; }
}
// You can't refer to property getters in order to treat them as a delegate, so this is
// a substitute.
public bool GetCancelling()
{
return cancelling;
}
public bool Cancelled
{
get { return cancelled; }
protected set { cancelled = value; }
}
///
/// Check again whether this task may be cancelled. Must be called from off the event thread,
/// whereas CanCancel is called on it.
///
public virtual void RecomputeCanCancel()
{
//Program.AssertOffEventThread();
try
{
XenRef task = _relatedTask;
if (task == null)
{
can_cancel = false;
return;
}
Session local_session = GetCancelSession();
if (local_session == null || string.IsNullOrEmpty(local_session.opaque_ref))
{
can_cancel = false;
return;
}
CanCancel = Task.get_allowed_operations(local_session, task.opaque_ref).Contains(task_allowed_operations.cancel);
}
catch (Exception exn)
{
log.Error(exn, exn);
LogoutCancelSession();
can_cancel = false;
}
}
///
/// Will return null if Connection is null.
///
///
protected Session GetCancelSession()
{
lock (connectionLock)
{
if (Connection == null || !Connection.IsConnected)
return null;
if (_cancel_session == null)
{
if (_session == null)
{
_cancel_session = Connection.DuplicateSession(XenAdminConfigManager.Provider.ConnectionTimeout);
}
else if (_session.Url == null) // DbProxy
{
return null;
}
else
{
_cancel_session = XenAdminConfigManager.Provider.CreateActionSession(_session, Connection);
}
}
return _cancel_session;
}
}
protected void LogoutCancelSession()
{
lock (connectionLock)
{
_cancel_session = null;
}
}
///
/// Cancels this action.
///
/// 1. Must be called on the event thread.
/// 2. Will return if Cancelling = true
/// 3. Runs RecomputeCanCancel() on a bg thread, then if CanCancel == true, sets Cancelling to true and runs Cancel_()
///
public override sealed void Cancel()
{
//Program.AssertOnEventThread();
log.Debug("Cancel() was called. Attempting to cancel action");
// We can always cancel before the action starts running
lock (_startedRunningLock)
{
if (!_startedRunning)
{
cancelled = true;
new Thread(delegate()
{
AuditLogCancelled();
MarkCompleted(new CancelledException());
Clean();
CleanOnError();
}).Start();
return;
}
}
lock (_cancellinglock)
{
if (Cancelling)
return;
Cancelling = true;
}
Thread t = new Thread(DoCancel);
t.Name = string.Format("Cancelling task {0}", Title);
t.IsBackground = true;
t.Priority = ThreadPriority.Lowest;
t.Start();
}
private void DoCancel()
{
RecomputeCanCancel();
if (can_cancel)
{
try
{
CancelRelatedTask();
}
catch (Exception e)
{
log.DebugFormat("Exception when cancelling action {0}", this.Description);
log.Debug(e, e);
LogoutCancelSession();
Cancelling = false;
}
}
}
///
/// Called by Cancel on a background thread, to do any heavy lifting.
/// Creates a new Session and cancels RelatedTask.
///
/// * Only called if RecomputeCanCancel sets CanCancel to true;
/// * Cancelling set to true before this call, but after call to RecomputeCanCancel and CanCancel.
/// * DO NOT CHECK CANCANCEL HERE, you will only be called with it as true, but it may change after.
///
///
protected virtual void CancelRelatedTask()
{
PerformSilentTaskOp(delegate()
{
// Create a new session since this.Session may be in-use by the thread pool thread, and, in particular, TaskPoller
Session local_session = GetCancelSession();
XenRef r = RelatedTask;
if (r != null)
{
XenAPI.Task.cancel(local_session, r);
}
});
}
protected void PerformSilentTaskOp(Action f)
{
if (_relatedTask == null)
return;
try
{
f();
}
catch (XenAPI.Failure exn)
{
if (exn.ErrorDescription.Count > 1 &&
exn.ErrorDescription[0] == XenAPI.Failure.HANDLE_INVALID &&
exn.ErrorDescription[1] == "task")
{
log.Debug(exn, exn);
// The task has disappeared.
_relatedTask = null;
}
else
{
log.Error(exn, exn);
// Ignore, and hope that this isn't a problem.
}
}
catch (Exception exn)
{
log.Error(exn, exn);
// Ignore, and hope that this isn't a problem.
}
}
private readonly object _cancellinglock = new object();
// _startedRunningLock controls the case that two threads try and Cancel and Run an action
// at the same time: one of them has to win and prevent the other.
private object _startedRunningLock = new object();
private bool _startedRunning = false;
public bool StartedRunning
{
get { return _startedRunning; }
protected set
{
lock(_startedRunningLock)
{
_startedRunning = value;
}
}
}
///
/// If there has been an exception this code will always execute after the action has finished, use for tidyup
///
protected virtual void CleanOnError() { }
///
/// This code will always execute after the action has finished, use for tidyup
///
protected virtual void Clean() { }
public virtual Session NewSession()
{
if (Connection == null)
return null;
return Connection.DuplicateSession();
}
///
/// Overload for use by actions, using elevated credentials on the retry, if implemented in NewSession().
/// Try and run the delegate.
/// If it fails with a web exception or invalid session, try again.
/// Only retry 60 times.
///
///
///
///
///
public object DoWithSessionRetry(ref Session session, Delegate f, params object[] p)
{
int retries = 60;
while (true)
{
try
{
object[] ps = new object[p.Length + 1];
ps[0] = session;
for (int i = 0; i < p.Length; i++)
{
ps[i + 1] = p[i];
}
try
{
return f.DynamicInvoke(ps);
}
catch (TargetInvocationException exn)
{
throw exn.InnerException;
}
}
catch (XmlRpcNullParameterException xmlExcept)
{
log.ErrorFormat("XmlRpcNullParameterException in DoWithSessionRetry, retry {0}", retries);
log.Error(xmlExcept, xmlExcept);
throw new Exception(Messages.INVALID_SESSION);
}
catch (XmlRpcIllFormedXmlException xmlRpcIllFormedXmlException)
{
log.ErrorFormat("XmlRpcIllFormedXmlException in DoWithSessionRetry, retry {0}", retries);
log.Error(xmlRpcIllFormedXmlException, xmlRpcIllFormedXmlException);
if (!Connection.ExpectDisruption || retries <= 0)
throw;
}
catch (WebException we)
{
log.ErrorFormat("WebException in DoWithSessionRetry, retry {0}", retries);
log.Error(we, we);
if (retries <= 0)
throw;
}
catch (Failure failure)
{
log.ErrorFormat("Failure in DoWithSessionRetry, retry {0}", retries);
log.Error(failure, failure);
if (retries <= 0)
throw;
if (failure.ErrorDescription.Count < 1 || failure.ErrorDescription[0] != XenAPI.Failure.SESSION_INVALID)
throw;
}
Session newSession;
try
{
// try to create a new TCP stream to use, as the other one has failed us
newSession = NewSession();
session = newSession;
}
catch (DisconnectionException e)
{
if (!Connection.ExpectDisruption)
{
//this was not expected, throw the d/c exception
throw e;
}
// We are expecting disruption on this connection. We need to wait for the hearbeat to recover.
// Though after 60 retries we will give up in the previous try catch block
}
catch
{
// do nothing
}
retries--;
Thread.Sleep(Connection.ExpectDisruption ? 500 : 100);
}
}
}
}