mirror of
https://github.com/xcp-ng/xenadmin.git
synced 2025-01-20 07:19:18 +01:00
New logic for connection to a xapi database and some connection tidy up:
- Removed event that had no subscribers. - The xml docs are more useful on the properties and not their supporting private fields. - Stop silencing cache errors as they may reveal other issues. - Adding server to history was called twice. - Connection null checks and refactoring of ConnectionExists exceptions. - Added some logging. Signed-off-by: Konstantina Chremmou <Konstantina.Chremmou@cloud.com>
This commit is contained in:
parent
8375754a5b
commit
a4df1eed61
@ -39,6 +39,7 @@ using XenAPI;
|
||||
using XenAdmin.Actions;
|
||||
using XenAdmin.Actions.Wlb;
|
||||
using XenAdmin.Commands;
|
||||
using XenAdmin.Network;
|
||||
|
||||
|
||||
namespace XenAdmin.Controls.Wlb
|
||||
@ -526,7 +527,7 @@ namespace XenAdmin.Controls.Wlb
|
||||
{
|
||||
Program.AssertOnEventThread();
|
||||
|
||||
if(_xenObject == null)
|
||||
if (_xenObject == null || _xenObject.Connection is XenConnection conn && conn.IsSimulatedConnection)
|
||||
return;
|
||||
|
||||
if (Helpers.WlbEnabled(_xenObject.Connection))
|
||||
|
@ -29,6 +29,7 @@
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using XenAdmin.Core;
|
||||
using XenAdmin.Network;
|
||||
@ -146,19 +147,12 @@ namespace XenAdmin.Dialogs
|
||||
conn = connection;
|
||||
|
||||
foreach (var server in servers)
|
||||
{
|
||||
StringUtility.ParseHostnamePort(server, out var hostname, out var port);
|
||||
|
||||
if (port == 0)
|
||||
port = ConnectionsManager.DEFAULT_XEN_PORT;
|
||||
|
||||
ConnectToServer(conn, hostname, port, username, password);
|
||||
}
|
||||
ConnectToServer(conn, server, username, password);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void ConnectToServer(IXenConnection conn, string hostname, int port, string username, string password)
|
||||
private void ConnectToServer(IXenConnection conn, string server, string username, string password)
|
||||
{
|
||||
if (conn == null)
|
||||
{
|
||||
@ -170,14 +164,27 @@ namespace XenAdmin.Dialogs
|
||||
conn.EndConnect(); // in case we're already connected
|
||||
}
|
||||
|
||||
conn.Hostname = hostname;
|
||||
conn.Port = port;
|
||||
conn.Username = username;
|
||||
conn.Password = password;
|
||||
conn.ExpectPasswordIsCorrect = false;
|
||||
|
||||
if (!_changedPass)
|
||||
if (File.Exists(server))
|
||||
{
|
||||
conn.Hostname = server;
|
||||
conn.Port = ConnectionsManager.DEFAULT_XEN_PORT;
|
||||
XenConnectionUI.ConnectToXapiDatabase(conn, this);
|
||||
}
|
||||
else if (!_changedPass)
|
||||
{
|
||||
StringUtility.ParseHostnamePort(server, out var hostname, out var port);
|
||||
|
||||
if (port == 0)
|
||||
port = ConnectionsManager.DEFAULT_XEN_PORT;
|
||||
|
||||
conn.Hostname = hostname;
|
||||
conn.Port = port;
|
||||
XenConnectionUI.BeginConnect(conn, true, Owner, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void conn_CachePopulated(IXenConnection conn)
|
||||
|
@ -62,7 +62,7 @@ namespace XenAdmin.Dialogs
|
||||
{
|
||||
InitializeComponent();
|
||||
UserDetails ud = session.CurrentUserDetails;
|
||||
labelCurrentUserValue.Text = ud.UserDisplayName ?? ud.UserName ?? Messages.UNKNOWN_AD_USER;
|
||||
labelCurrentUserValue.Text = ud?.UserDisplayName ?? ud?.UserName ?? Messages.UNKNOWN_AD_USER;
|
||||
labelCurrentRoleValue.Text = Role.FriendlyCSVRoleList(session.Roles);
|
||||
authorizedRoles.Sort((r1, r2) => r2.CompareTo(r1));
|
||||
labelRequiredRoleValue.Text = Role.FriendlyCSVRoleList(authorizedRoles);
|
||||
|
@ -44,6 +44,18 @@ namespace XenAdmin.Network
|
||||
{
|
||||
public static Dictionary<IXenConnection, ConnectingToServerDialog> connectionDialogs = new Dictionary<IXenConnection, ConnectingToServerDialog>();
|
||||
|
||||
public static void ConnectToXapiDatabase(IXenConnection conn, Form owner)
|
||||
{
|
||||
try
|
||||
{
|
||||
((XenConnection)conn).LoadFromDatabaseFile(conn.Hostname);
|
||||
}
|
||||
catch (ConnectionExists e)
|
||||
{
|
||||
ShowConnectingDialogError(owner, conn, e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start connecting to a server
|
||||
/// </summary>
|
||||
@ -254,7 +266,7 @@ namespace XenAdmin.Network
|
||||
|
||||
if (!Program.RunInAutomatedTestMode)
|
||||
{
|
||||
using (var dlg = new InformationDialog(c.GetDialogMessage(connection)))
|
||||
using (var dlg = new InformationDialog(c.GetDialogMessage()))
|
||||
dlg.ShowDialog(owner);
|
||||
}
|
||||
}
|
||||
|
181
XenModel/Db.cs
Normal file
181
XenModel/Db.cs
Normal file
@ -0,0 +1,181 @@
|
||||
/* Copyright (c) Cloud Software Group, Inc.
|
||||
*
|
||||
* 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.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Xml;
|
||||
using XenAdmin.Core;
|
||||
using XenAPI;
|
||||
|
||||
namespace XenAdmin
|
||||
{
|
||||
public class Db
|
||||
{
|
||||
private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
public Db(XmlDocument document)
|
||||
{
|
||||
XmlNode dbNode = document.ChildNodes.Cast<XmlNode>().FirstOrDefault(node => node.Name == "database") ??
|
||||
throw new ArgumentException("Couldn't find database tag in state db");
|
||||
|
||||
foreach (XmlNode n in dbNode.ChildNodes)
|
||||
{
|
||||
if (n.Name != "table")
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
Tables.Add(new DbTable(n));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<DbTable> Tables { get; } = new List<DbTable>();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Corresponds to an API class type. Its Rows are the class instances (API objects)
|
||||
/// </summary>
|
||||
public class DbTable
|
||||
{
|
||||
private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
|
||||
|
||||
private static readonly Type[] AssemblyTypes = Assembly.GetExecutingAssembly().GetTypes();
|
||||
|
||||
public DbTable(XmlNode tableNode)
|
||||
{
|
||||
var name = tableNode.Attributes?["name"].Value;
|
||||
|
||||
ApiClassType = AssemblyTypes
|
||||
.Where(t => t.Namespace == nameof(XenAPI))
|
||||
.FirstOrDefault(t => string.Equals(t.Name, name, StringComparison.CurrentCultureIgnoreCase));
|
||||
|
||||
foreach (XmlNode n in tableNode.ChildNodes)
|
||||
{
|
||||
if (n.Name != "row")
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
Rows.Add(new Row(ApiClassType, n));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_log.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Type ApiClassType { get; }
|
||||
|
||||
public List<Row> Rows { get; } = new List<Row>();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Corresponds to an API object
|
||||
/// </summary>
|
||||
public class Row
|
||||
{
|
||||
private readonly XmlNode _rowNode;
|
||||
|
||||
public Row(Type apiClassType, XmlNode rowNode)
|
||||
{
|
||||
_rowNode = rowNode;
|
||||
|
||||
HashTable = ConvertToHashtable(out var opaqueRef);
|
||||
XenObject = ConvertToXenObject(apiClassType);
|
||||
|
||||
if (XenObject != null)
|
||||
XenObject.opaque_ref = opaqueRef;
|
||||
|
||||
ObjectChange = new ObjectChange(apiClassType, opaqueRef, XenObject);
|
||||
}
|
||||
|
||||
public Hashtable HashTable { get; }
|
||||
|
||||
public IXenObject XenObject { get; }
|
||||
|
||||
public ObjectChange ObjectChange { get; }
|
||||
|
||||
private Hashtable ConvertToHashtable(out string opaqueRef)
|
||||
{
|
||||
opaqueRef = "OpaqueRef:NULL";
|
||||
|
||||
if (_rowNode.Attributes == null)
|
||||
return null;
|
||||
|
||||
var hashtable = new Hashtable();
|
||||
|
||||
foreach (XmlAttribute attr in _rowNode.Attributes)
|
||||
{
|
||||
if (attr.Name.StartsWith("__"))
|
||||
continue;
|
||||
|
||||
if (attr.Name == "_ref")
|
||||
{
|
||||
opaqueRef = attr.Value;
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = attr.Name.Replace("__", "_");
|
||||
|
||||
hashtable[key] = EscapeXapiChars(attr.Value);
|
||||
}
|
||||
|
||||
return hashtable;
|
||||
}
|
||||
|
||||
private IXenObject ConvertToXenObject(Type apiClassType)
|
||||
{
|
||||
return apiClassType?.GetConstructor(new[] { typeof(Hashtable) })?.Invoke(new object[] { HashTable }) as IXenObject;
|
||||
}
|
||||
|
||||
private string EscapeXapiChars(string input)
|
||||
{
|
||||
input = input.Replace("%.", " ");
|
||||
input = input.Replace("%_", " ");
|
||||
input = input.Replace("%n", "\n");
|
||||
input = input.Replace("%r", "\r");
|
||||
input = input.Replace("%t", "\t");
|
||||
input = input.Replace("%%", "%");
|
||||
return input;
|
||||
}
|
||||
}
|
||||
}
|
@ -62,9 +62,7 @@ namespace XenAdmin.Network
|
||||
Session DuplicateSession(int timeout);
|
||||
void EndConnect(bool resetState = true, bool exiting = false);
|
||||
void Interrupt();
|
||||
Session Connect(string user, string password);
|
||||
List<string> PoolMembers { get; set; }
|
||||
void LoadCache(Session session);
|
||||
bool SuppressErrors { get; set; }
|
||||
bool PreventResettingPasswordPrompt { get;set; }
|
||||
bool CoordinatorMayChange { get; set; }
|
||||
@ -83,7 +81,6 @@ namespace XenAdmin.Network
|
||||
void WaitFor(Func<bool> predicate, Func<bool> cancelling);
|
||||
TimeSpan ServerTimeOffset { get; set; }
|
||||
Session Session { get; }
|
||||
event EventHandler<EventArgs> TimeSkewUpdated;
|
||||
string UriScheme { get; }
|
||||
event EventHandler<EventArgs> XenObjectsUpdated;
|
||||
NetworkCredential NetworkCredential { get; set; }
|
||||
|
@ -39,7 +39,9 @@ using XenAdmin.Core;
|
||||
using XenAPI;
|
||||
using XenCenterLib;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
|
||||
@ -71,13 +73,20 @@ namespace XenAdmin.Network
|
||||
/// </summary>
|
||||
public bool fromDialog = false;
|
||||
|
||||
private volatile bool _expectedDisruption;
|
||||
private volatile bool _suppressErrors;
|
||||
private volatile bool _preventResettingPasswordPrompt;
|
||||
private volatile bool _coordinatorMayChange;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we're expecting network disruption, say because we're reconfiguring the network at this time. In that case, we ignore
|
||||
/// keepalive failures, and expect task polling to be disrupted.
|
||||
/// </summary>
|
||||
private volatile bool _expectedDisruption = false;
|
||||
|
||||
public bool ExpectDisruption { get { return _expectedDisruption; } set { _expectedDisruption = value; } }
|
||||
public bool ExpectDisruption
|
||||
{
|
||||
get => _expectedDisruption;
|
||||
set => _expectedDisruption = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we are 'expecting' this connection's Password property to contain the correct password
|
||||
@ -88,16 +97,12 @@ namespace XenAdmin.Network
|
||||
/// <summary>
|
||||
/// Used by the patch wizard, suppress any errors coming from reconnect attempts
|
||||
/// </summary>
|
||||
private volatile bool _suppressErrors;
|
||||
public bool SuppressErrors
|
||||
{
|
||||
get => _suppressErrors;
|
||||
set => _suppressErrors = value;
|
||||
}
|
||||
|
||||
|
||||
private volatile bool _preventResettingPasswordPrompt;
|
||||
|
||||
/// <summary>
|
||||
/// The password prompting function (<see cref="_promptForNewPassword"/>) is set to null when the connection is closed.
|
||||
/// Set this value to true in order to prevent that from happening.
|
||||
@ -112,9 +117,11 @@ namespace XenAdmin.Network
|
||||
/// <summary>
|
||||
/// Indicates whether we are expecting the pool coordinator to change soon (e.g. when explicitly designating a new coordinator).
|
||||
/// </summary>
|
||||
private volatile bool _coordinatorMayChange = false;
|
||||
|
||||
public bool CoordinatorMayChange { get { return _coordinatorMayChange; } set { _coordinatorMayChange = value; } }
|
||||
public bool CoordinatorMayChange
|
||||
{
|
||||
get => _coordinatorMayChange;
|
||||
set => _coordinatorMayChange = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set when we detect that Event.next() has become blocked and we need to reset the connection. See CA-33145.
|
||||
@ -132,11 +139,15 @@ namespace XenAdmin.Network
|
||||
private string CoordinatorIPAddress = "";
|
||||
|
||||
/// <summary>
|
||||
/// The lock that must be taken around connectTask and monitor.
|
||||
/// The lock that must be taken around connectTask and heartbeat.
|
||||
/// </summary>
|
||||
private readonly object connectTaskLock = new object();
|
||||
private ConnectTask connectTask = null;
|
||||
private Heartbeat heartbeat = null;
|
||||
|
||||
/// <summary>
|
||||
/// This is the metrics monitor. Has to be accessed within the connectTaskLock.
|
||||
/// </summary>
|
||||
private Heartbeat heartbeat;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we are trying to automatically connect to the new coordinator. Set in HandleConnectionLost.
|
||||
@ -166,7 +177,7 @@ namespace XenAdmin.Network
|
||||
|
||||
private DateTime m_startTime = DateTime.MinValue;
|
||||
private int m_lastDebug;
|
||||
private TimeSpan ServerTimeOffset_ = TimeSpan.Zero;
|
||||
private TimeSpan _serverTimeOffset = TimeSpan.Zero;
|
||||
private object ServerTimeOffsetLock = new object();
|
||||
/// <summary>
|
||||
/// The offset between the clock at the client and the clock at the server. server time + ServerTimeOffset = client time.
|
||||
@ -179,15 +190,14 @@ namespace XenAdmin.Network
|
||||
{
|
||||
lock (ServerTimeOffsetLock)
|
||||
{
|
||||
return ServerTimeOffset_;
|
||||
return _serverTimeOffset;
|
||||
}
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
lock (ServerTimeOffsetLock)
|
||||
{
|
||||
TimeSpan diff = ServerTimeOffset_ - value;
|
||||
TimeSpan diff = _serverTimeOffset - value;
|
||||
|
||||
if (diff.TotalSeconds < -1 || 1 < diff.TotalSeconds)
|
||||
{
|
||||
@ -212,11 +222,8 @@ namespace XenAdmin.Network
|
||||
}
|
||||
}
|
||||
|
||||
ServerTimeOffset_ = value;
|
||||
_serverTimeOffset = value;
|
||||
}
|
||||
|
||||
if (TimeSkewUpdated != null)
|
||||
TimeSkewUpdated(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
@ -250,8 +257,7 @@ namespace XenAdmin.Network
|
||||
/// <summary>
|
||||
/// The cache of XenAPI objects for this connection.
|
||||
/// </summary>
|
||||
private readonly ICache _cache = new Cache();
|
||||
public ICache Cache { get { return _cache; } }
|
||||
public ICache Cache { get; } = new Cache();
|
||||
|
||||
private readonly LockFreeQueue<ObjectChange> eventQueue = new LockFreeQueue<ObjectChange>();
|
||||
private readonly System.Threading.Timer cacheUpdateTimer;
|
||||
@ -259,12 +265,20 @@ namespace XenAdmin.Network
|
||||
/// <summary>
|
||||
/// Whether the cache for this connection has been populated.
|
||||
/// </summary>
|
||||
private bool cacheIsPopulated = false;
|
||||
public bool CacheIsPopulated
|
||||
{
|
||||
get { return cacheIsPopulated; }
|
||||
get => _cacheIsPopulated;
|
||||
private set
|
||||
{
|
||||
_cacheIsPopulated = value;
|
||||
|
||||
if (_cacheIsPopulated)
|
||||
CachePopulated?.Invoke(this);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _cacheIsPopulated;
|
||||
|
||||
private bool cacheUpdaterRunning = false;
|
||||
private bool updatesWaiting = false;
|
||||
|
||||
@ -280,56 +294,7 @@ namespace XenAdmin.Network
|
||||
cacheUpdateTimer = new Timer(cacheUpdater);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For use by unit tests only.
|
||||
/// </summary>
|
||||
/// <param name="user"></param>
|
||||
/// <param name="password"></param>
|
||||
/// <returns></returns>
|
||||
public Session Connect(string user, string password)
|
||||
{
|
||||
heartbeat = new Heartbeat(this, XenAdminConfigManager.Provider.ConnectionTimeout);
|
||||
Session session = new Session(Session.STANDARD_TIMEOUT, this, Hostname, Port);
|
||||
|
||||
try
|
||||
{
|
||||
session.login_with_password(user, password, Helper.APIVersionString(API_Version.LATEST), Session.UserAgent);
|
||||
|
||||
// this is required so connection.IsConnected returns true in the unit tests.
|
||||
connectTask = new ConnectTask("test", 0);
|
||||
connectTask.Connected = true;
|
||||
connectTask.Session = session;
|
||||
|
||||
return session;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by unit tests only.
|
||||
/// </summary>
|
||||
/// <param name="session">The session.</param>
|
||||
public void LoadCache(Session session)
|
||||
{
|
||||
this.Cache.Clear();
|
||||
|
||||
cacheIsPopulated = false;
|
||||
|
||||
string token = "";
|
||||
XenObjectDownloader.GetAllObjects(session, eventQueue, () => false, ref token);
|
||||
List<ObjectChange> events = new List<ObjectChange>();
|
||||
|
||||
while (eventQueue.NotEmpty)
|
||||
events.Add(eventQueue.Dequeue());
|
||||
|
||||
this.Cache.UpdateFrom(this, events);
|
||||
cacheIsPopulated = true;
|
||||
|
||||
}
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Fired just before the cache is cleared (i.e. the cache is still populated).
|
||||
@ -351,8 +316,48 @@ namespace XenAdmin.Network
|
||||
/// </summary>
|
||||
public event EventHandler<EventArgs> XenObjectsUpdated;
|
||||
|
||||
#endregion
|
||||
|
||||
public void LoadFromDatabaseFile(string databaseFile)
|
||||
{
|
||||
IsSimulatedConnection = true;
|
||||
|
||||
var foundConn = ConnectionsManager.XenConnections.Cast<XenConnection>().FirstOrDefault(c =>
|
||||
c.Hostname == databaseFile && c.CoordinatorIPAddress == CoordinatorIPAddress);
|
||||
if (foundConn != null && foundConn.IsConnected)
|
||||
throw new ConnectionExists(this);
|
||||
|
||||
var document = new XmlDocument();
|
||||
using (var sr = new StreamReader(databaseFile))
|
||||
document.LoadXml(sr.ReadToEnd());
|
||||
|
||||
var db = new Db(document);
|
||||
var events = db.Tables.SelectMany(t => t.Rows).Select(r => r.ObjectChange).ToList();
|
||||
|
||||
var session = new Session(Session.STANDARD_TIMEOUT, this, Path.GetFileName(Hostname), Port);
|
||||
connectTask = new ConnectTask(Hostname, Port) { Connected = true, Session = session };
|
||||
|
||||
OnBeforeMajorChange(false);
|
||||
Cache.Clear();
|
||||
CacheIsPopulated = false;
|
||||
OnAfterMajorChange(false);
|
||||
|
||||
OnBeforeMajorChange(false);
|
||||
Cache.UpdateFrom(this, events);
|
||||
OnAfterMajorChange(false);
|
||||
CacheIsPopulated = true;
|
||||
|
||||
var pool = Cache.Pools[0];
|
||||
CoordinatorIPAddress = Cache.Hosts.FirstOrDefault(h => h.opaque_ref == pool.master)?.address;
|
||||
|
||||
HandleSuccessfulConnection(connectTask, pool);
|
||||
MarkConnectActionComplete();
|
||||
OnXenObjectsUpdated();
|
||||
}
|
||||
|
||||
public NetworkCredential NetworkCredential { get; set; }
|
||||
public event EventHandler<EventArgs> TimeSkewUpdated;
|
||||
|
||||
public bool IsSimulatedConnection { get; private set; }
|
||||
|
||||
public bool IsConnected
|
||||
{
|
||||
@ -608,6 +613,8 @@ namespace XenAdmin.Network
|
||||
|
||||
lock (connectTaskLock)
|
||||
{
|
||||
//if connectTask != null a connection is already in progress
|
||||
|
||||
if (connectTask == null)
|
||||
{
|
||||
ClearEventQueue();
|
||||
@ -615,17 +622,16 @@ namespace XenAdmin.Network
|
||||
Cache.Clear();
|
||||
OnAfterMajorChange(false);
|
||||
connectTask = new ConnectTask(Hostname, Port);
|
||||
StopMonitor();
|
||||
|
||||
heartbeat?.Stop();
|
||||
heartbeat = null;
|
||||
heartbeat = new Heartbeat(this, XenAdminConfigManager.Provider.ConnectionTimeout);
|
||||
|
||||
Thread t = new Thread(ConnectWorkerThread);
|
||||
t.Name = "Connection to " + Hostname;
|
||||
t.IsBackground = true;
|
||||
t.Start(connectTask);
|
||||
}
|
||||
else
|
||||
{
|
||||
// a connection is already in progress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -745,7 +751,9 @@ namespace XenAdmin.Network
|
||||
|
||||
lock (connectTaskLock)
|
||||
{
|
||||
StopMonitor();
|
||||
heartbeat?.Stop();
|
||||
heartbeat = null;
|
||||
|
||||
if (task != null)
|
||||
{
|
||||
task.Cancelled = true;
|
||||
@ -759,6 +767,7 @@ namespace XenAdmin.Network
|
||||
}
|
||||
|
||||
MarkConnectActionComplete();
|
||||
log.Info($"Connection to {Hostname} is ended.");
|
||||
|
||||
// Save list of addresses of current hosts in pool
|
||||
List<string> members = new List<string>();
|
||||
@ -861,18 +870,6 @@ namespace XenAdmin.Network
|
||||
Cache.Clear();
|
||||
}
|
||||
|
||||
private void OnCachePopulated()
|
||||
{
|
||||
lock (connectTaskLock)
|
||||
{
|
||||
if (heartbeat != null)
|
||||
heartbeat.Start();
|
||||
}
|
||||
|
||||
CachePopulated?.Invoke(this);
|
||||
MarkConnectActionComplete();
|
||||
}
|
||||
|
||||
private string GetReason(Exception error)
|
||||
{
|
||||
if (error is ArgumentException)
|
||||
@ -930,8 +927,16 @@ namespace XenAdmin.Network
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleSuccessfulConnection(string taskHostname, int task_port)
|
||||
private void HandleSuccessfulConnection(ConnectTask task, Pool pool)
|
||||
{
|
||||
// Remove any other (disconnected) entries for this server from the tree
|
||||
|
||||
var existingConnections = ConnectionsManager.XenConnections.Where(c =>
|
||||
c.Hostname.Equals(task.Hostname) && !c.IsConnected).ToList();
|
||||
|
||||
foreach (var conn in existingConnections)
|
||||
ConnectionsManager.ClearCacheAndRemoveConnection(conn);
|
||||
|
||||
// add server name to history (if it's not already there)
|
||||
XenAdminConfigManager.Provider.UpdateServerHistory(HostnameWithPort);
|
||||
|
||||
@ -945,18 +950,20 @@ namespace XenAdmin.Network
|
||||
InvokeHelper.Invoke(XenAdminConfigManager.Provider.SaveSettingsIfRequired);
|
||||
}
|
||||
|
||||
log.InfoFormat("Connected to {0} ({1}:{2})", FriendlyName, taskHostname, task_port);
|
||||
string name = string.IsNullOrEmpty(FriendlyName) || FriendlyName == taskHostname
|
||||
? taskHostname
|
||||
: string.Format("{0} ({1})", FriendlyName, taskHostname);
|
||||
string name = string.IsNullOrEmpty(FriendlyName) || FriendlyName == task.Hostname
|
||||
? task.Hostname
|
||||
: string.Format("{0} ({1})", FriendlyName, task.Hostname);
|
||||
string title = string.Format(Messages.CONNECTING_NOTICE_TITLE, name);
|
||||
string msg = string.Format(Messages.CONNECTING_NOTICE_TEXT, name);
|
||||
log.Info($"Connecting to {name} in progress.");
|
||||
|
||||
ConnectAction = new DummyAction(title, msg);
|
||||
SetPoolAndHostInAction(ConnectAction, pool, PoolOpaqueRef);
|
||||
|
||||
ExpectPasswordIsCorrect = true;
|
||||
OnConnectionResult(true, null, null);
|
||||
|
||||
log.InfoFormat("Completed connection phase for pool {0} ({1}:{2}, {3}).",
|
||||
FriendlyName, task.Hostname, task.Port, PoolOpaqueRef);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -974,13 +981,6 @@ namespace XenAdmin.Network
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleConnectionTermination()
|
||||
{
|
||||
// clean up action so we dont stay open forever
|
||||
if (ConnectAction != null)
|
||||
ConnectAction.IsCompleted = true;
|
||||
}
|
||||
|
||||
private readonly object PromptLock = new object();
|
||||
/// <summary>
|
||||
/// Prompts the user for the changed password for a server.
|
||||
@ -1066,6 +1066,7 @@ namespace XenAdmin.Network
|
||||
// on the GUI thread to be included in this set of
|
||||
// updates, otherwise we might hose the gui thread
|
||||
// during an event storm (ie deleting 1000 vms)
|
||||
|
||||
List<ObjectChange> events = new List<ObjectChange>();
|
||||
|
||||
while (eventQueue.NotEmpty)
|
||||
@ -1073,43 +1074,24 @@ namespace XenAdmin.Network
|
||||
|
||||
if (events.Count > 0)
|
||||
{
|
||||
InvokeHelper.Invoke(delegate ()
|
||||
InvokeHelper.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
OnBeforeMajorChange(false);
|
||||
bool fire = Cache.UpdateFrom(this, events);
|
||||
OnAfterMajorChange(false);
|
||||
|
||||
if (fire)
|
||||
OnXenObjectsUpdated();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Error("Exception updating cache.", e);
|
||||
#if DEBUG
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
OnBeforeMajorChange(false);
|
||||
bool fire = Cache.UpdateFrom(this, events);
|
||||
OnAfterMajorChange(false);
|
||||
|
||||
if (fire)
|
||||
OnXenObjectsUpdated();
|
||||
});
|
||||
|
||||
if (!cacheIsPopulated)
|
||||
if (!CacheIsPopulated)
|
||||
{
|
||||
cacheIsPopulated = true;
|
||||
try
|
||||
{
|
||||
OnCachePopulated();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Error("Exception calling OnCachePopulated.", e);
|
||||
#if DEBUG
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
lock (connectTaskLock)
|
||||
heartbeat?.Start();
|
||||
|
||||
CacheIsPopulated = true;
|
||||
MarkConnectActionComplete();
|
||||
log.Info($"Connection to {Hostname} successful.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1123,7 +1105,6 @@ namespace XenAdmin.Network
|
||||
{
|
||||
string title = string.Format(Messages.CONNECTION_OK_NOTICE_TITLE, Hostname);
|
||||
string msg = string.Format(Messages.CONNECTION_OK_NOTICE_TEXT, Hostname);
|
||||
log.Info($"Connection to {Hostname} successful.");
|
||||
ConnectAction.Title = title;
|
||||
ConnectAction.Description = msg;
|
||||
|
||||
@ -1138,26 +1119,8 @@ namespace XenAdmin.Network
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops this connection's XenMetricsMonitor thread.
|
||||
/// Expects to be locked under connectTaskLock.
|
||||
/// </summary>
|
||||
private void StopMonitor()
|
||||
{
|
||||
if (heartbeat != null)
|
||||
{
|
||||
heartbeat.Stop();
|
||||
heartbeat = null;
|
||||
}
|
||||
}
|
||||
|
||||
private const int DEFAULT_MAX_SESSION_LOGIN_ATTEMPTS = 3;
|
||||
|
||||
public static bool IsSimulatorConnection(string url)
|
||||
{
|
||||
return url.EndsWith(".db") || url.EndsWith(".xml") || url.EndsWith(".tmp");
|
||||
}
|
||||
|
||||
private readonly string eventNextConnectionGroupName = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
@ -1186,7 +1149,7 @@ namespace XenAdmin.Network
|
||||
Session eventNextSession = DuplicateSession(EVENT_NEXT_TIMEOUT);
|
||||
eventNextSession.ConnectionGroupName = eventNextConnectionGroupName; // this will force the eventNextSession onto its own set of TCP streams (see CA-108676)
|
||||
|
||||
cacheIsPopulated = false;
|
||||
CacheIsPopulated = false;
|
||||
session.CacheWarming = true;
|
||||
|
||||
string token = "";
|
||||
@ -1207,14 +1170,18 @@ namespace XenAdmin.Network
|
||||
OnConnectionMessageChanged(string.Format(Messages.LABEL_SYNC, this.Hostname));
|
||||
}
|
||||
|
||||
log.Debug("Cache is warming. Starting XenObjectDownloader.GetAllObjects");
|
||||
XenObjectDownloader.GetAllObjects(session, eventQueue, task.GetCancelled, ref token);
|
||||
log.Debug("Cache is warming. XenObjectDownloader.GetAllObjects finished successfully");
|
||||
session.CacheWarming = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
log.Debug("Starting XenObjectDownloader.GetEvents");
|
||||
XenObjectDownloader.GetEvents(eventNextSession, eventQueue, task.GetCancelled, ref token);
|
||||
log.Debug("Starting XenObjectDownloader.GetEvents finished successfully");
|
||||
eventsExceptionLogged = false;
|
||||
}
|
||||
catch (Exception exn)
|
||||
@ -1225,8 +1192,8 @@ namespace XenAdmin.Network
|
||||
log.DebugFormat("Exception (disruption is expected) in XenObjectDownloader.GetEvents: {0}", exn.GetType().Name);
|
||||
|
||||
// ignoring some exceptions when disruption is expected
|
||||
if (exn is System.IO.IOException ||
|
||||
(exn is WebException && ((exn as WebException).Status == WebExceptionStatus.KeepAliveFailure || (exn as WebException).Status == WebExceptionStatus.ConnectFailure)))
|
||||
if (exn is IOException ||
|
||||
(exn is WebException webEx && (webEx.Status == WebExceptionStatus.KeepAliveFailure || webEx.Status == WebExceptionStatus.ConnectFailure)))
|
||||
{
|
||||
if (!eventsExceptionLogged)
|
||||
{
|
||||
@ -1269,16 +1236,10 @@ namespace XenAdmin.Network
|
||||
bool sameCoordinator = CoordinatorIPAddress == connection.CoordinatorIPAddress;
|
||||
|
||||
if (sameRef && sameCoordinator)
|
||||
{
|
||||
throw new ConnectionExists(connection);
|
||||
}
|
||||
else
|
||||
{
|
||||
// CA-15633: XenCenter does not allow connection to host
|
||||
// on which backup is restored.
|
||||
|
||||
throw new BadRestoreDetected(connection);
|
||||
}
|
||||
// CA-15633: XenCenter does not allow connection to host on which backup is restored.
|
||||
throw new BadRestoreDetected(connection);
|
||||
}
|
||||
|
||||
task.Connected = true;
|
||||
@ -1292,39 +1253,10 @@ namespace XenAdmin.Network
|
||||
: task.Hostname;
|
||||
} // ConnectionsLock
|
||||
|
||||
// Remove any other (disconnected) entries for this server from the tree
|
||||
List<IXenConnection> existingConnections = new List<IXenConnection>();
|
||||
foreach (IXenConnection connection in ConnectionsManager.XenConnections)
|
||||
{
|
||||
if (connection.Hostname.Equals(task.Hostname) && !connection.IsConnected)
|
||||
{
|
||||
existingConnections.Add(connection);
|
||||
}
|
||||
}
|
||||
foreach (IXenConnection connection in existingConnections)
|
||||
{
|
||||
ConnectionsManager.ClearCacheAndRemoveConnection(connection);
|
||||
}
|
||||
|
||||
|
||||
log.DebugFormat("Getting server time for pool {0} ({1})...", FriendlyName, PoolOpaqueRef);
|
||||
SetServerTimeOffset(session, pool.master.opaque_ref);
|
||||
|
||||
// add server name to history (if it's not already there)
|
||||
XenAdminConfigManager.Provider.UpdateServerHistory(HostnameWithPort);
|
||||
|
||||
HandleSuccessfulConnection(task.Hostname, task.Port);
|
||||
|
||||
try
|
||||
{
|
||||
SetPoolAndHostInAction(ConnectAction, pool, PoolOpaqueRef);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
log.Error(e, e);
|
||||
}
|
||||
|
||||
log.DebugFormat("Completed connection phase for pool {0} ({1}).", FriendlyName, PoolOpaqueRef);
|
||||
HandleSuccessfulConnection(task, pool);
|
||||
}
|
||||
|
||||
EventsPending();
|
||||
@ -1398,7 +1330,10 @@ namespace XenAdmin.Network
|
||||
ClearEventQueue();
|
||||
|
||||
connectTask = null;
|
||||
HandleConnectionTermination();
|
||||
|
||||
// clean up action so we don't stay open forever
|
||||
if (ConnectAction != null)
|
||||
ConnectAction.IsCompleted = true;
|
||||
|
||||
if (error is ServerNotSupported)
|
||||
{
|
||||
@ -1617,7 +1552,7 @@ namespace XenAdmin.Network
|
||||
{
|
||||
get
|
||||
{
|
||||
if (EventNextBlocked || IsSimulatorConnection(Hostname))
|
||||
if (EventNextBlocked || IsSimulatedConnection)
|
||||
return RECONNECT_SHORT_TIMEOUT_MS;
|
||||
|
||||
return RECONNECT_HOST_TIMEOUT_MS;
|
||||
@ -1962,6 +1897,7 @@ namespace XenAdmin.Network
|
||||
}
|
||||
|
||||
private bool disposed;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!disposed)
|
||||
@ -1980,7 +1916,7 @@ namespace XenAdmin.Network
|
||||
BeforeMajorChange = null;
|
||||
AfterMajorChange = null;
|
||||
XenObjectsUpdated = null;
|
||||
TimeSkewUpdated = null;
|
||||
|
||||
if (ReconnectionTimer != null)
|
||||
ReconnectionTimer.Dispose();
|
||||
if (cacheUpdateTimer != null)
|
||||
@ -2002,35 +1938,39 @@ namespace XenAdmin.Network
|
||||
|
||||
public class ConnectionExists : DisconnectionException
|
||||
{
|
||||
public IXenConnection connection;
|
||||
protected readonly IXenConnection Connection;
|
||||
|
||||
public ConnectionExists(IXenConnection connection)
|
||||
{
|
||||
this.connection = connection;
|
||||
Connection = connection;
|
||||
}
|
||||
|
||||
public override string Message
|
||||
{
|
||||
get
|
||||
{
|
||||
if (connection != null)
|
||||
return string.Format(Messages.CONNECTION_EXISTS, connection.Hostname);
|
||||
else
|
||||
if (Connection == null)
|
||||
return Messages.CONNECTION_EXISTS_NULL;
|
||||
|
||||
return string.Format(Messages.CONNECTION_EXISTS, Connection.Hostname);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public virtual string GetDialogMessage(IXenConnection _this)
|
||||
public virtual string GetDialogMessage()
|
||||
{
|
||||
Pool p = Helpers.GetPool(connection);
|
||||
if (p == null)
|
||||
return String.Format(Messages.ALREADY_CONNECTED, _this.Hostname);
|
||||
if (Connection == null)
|
||||
return Messages.CONNECTION_EXISTS_NULL;
|
||||
|
||||
return String.Format(Messages.SUPPORTER_ALREADY_CONNECTED, _this.Hostname, p.Name());
|
||||
Pool p = Helpers.GetPool(Connection);
|
||||
if (p == null)
|
||||
return string.Format(Messages.ALREADY_CONNECTED, Connection.Hostname);
|
||||
|
||||
return string.Format(Messages.SUPPORTER_ALREADY_CONNECTED, Connection.Hostname, p.Name());
|
||||
}
|
||||
}
|
||||
|
||||
class BadRestoreDetected : ConnectionExists
|
||||
internal class BadRestoreDetected : ConnectionExists
|
||||
{
|
||||
public BadRestoreDetected(IXenConnection xc)
|
||||
: base(xc)
|
||||
@ -2041,11 +1981,14 @@ namespace XenAdmin.Network
|
||||
{
|
||||
get
|
||||
{
|
||||
return String.Format(Messages.BAD_RESTORE_DETECTED, connection.Name);
|
||||
if (Connection == null)
|
||||
return Messages.CONNECTION_EXISTS_NULL;
|
||||
|
||||
return string.Format(Messages.BAD_RESTORE_DETECTED, Connection.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public override string GetDialogMessage(IXenConnection _this)
|
||||
public override string GetDialogMessage()
|
||||
{
|
||||
return Message;
|
||||
}
|
||||
|
@ -163,7 +163,7 @@ namespace XenAPI
|
||||
//Sort roles from highest to lowest
|
||||
roles.Sort((r1, r2) => r2.CompareTo(r1));
|
||||
//Take the highest role
|
||||
return roles[0].FriendlyName();
|
||||
return roles.Count > 0 ? roles[0].FriendlyName() : Messages.UNKNOWN_AD_USER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
using System.Linq;
|
||||
|
||||
|
||||
namespace XenAPI
|
||||
@ -45,7 +46,7 @@ namespace XenAPI
|
||||
/// <returns></returns>
|
||||
public static object convertStruct(Type t, Hashtable table)
|
||||
{
|
||||
return t.GetConstructor(new Type[] {typeof(Hashtable)}).Invoke(new object[] {table});
|
||||
return t.GetConstructor(new Type[] { typeof(Hashtable) }).Invoke(new object[] { table });
|
||||
}
|
||||
|
||||
public static Type GetXenAPIType(string name)
|
||||
@ -55,31 +56,30 @@ namespace XenAPI
|
||||
|
||||
public static bool ParseBool(Hashtable table, string key)
|
||||
{
|
||||
var val = table[key];
|
||||
return val == null ? false : (bool)table[key];
|
||||
bool.TryParse((string)table[key], out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static DateTime ParseDateTime(Hashtable table, string key)
|
||||
{
|
||||
var val = table[key];
|
||||
return val == null ? DateTime.MinValue : (DateTime)table[key];
|
||||
DateTime.TryParse((string)table[key], out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double ParseDouble(Hashtable table, string key)
|
||||
{
|
||||
var val = table[key];
|
||||
return val == null ? 0.0 : (double)table[key];
|
||||
double.TryParse((string)table[key], out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Hashtable ParseHashTable(Hashtable table, string key)
|
||||
{
|
||||
return (Hashtable)table[key];
|
||||
return ParseSxpDict((string)table[key]);
|
||||
}
|
||||
|
||||
public static long ParseLong(Hashtable table, string key)
|
||||
{
|
||||
long result;
|
||||
long.TryParse((string)table[key], out result);
|
||||
long.TryParse((string)table[key], out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -90,14 +90,12 @@ namespace XenAPI
|
||||
|
||||
public static string[] ParseStringArray(Hashtable table, string key)
|
||||
{
|
||||
var val = (object[])table[key];
|
||||
return val == null ? new string[0] : Array.ConvertAll(val, o => o.ToString());
|
||||
return ParseSxpList((string)table[key]).ToArray();
|
||||
}
|
||||
|
||||
public static long[] ParseLongArray(Hashtable table, string key)
|
||||
{
|
||||
var val = (object[])table[key];
|
||||
return val == null ? new long[0] : Array.ConvertAll(val, o => long.Parse(o.ToString()));
|
||||
return ParseSxpList((string)table[key]).Select(long.Parse).ToArray();
|
||||
}
|
||||
|
||||
public static XenRef<T> ParseRef<T>(Hashtable table, string key) where T : XenObject<T>
|
||||
@ -108,14 +106,85 @@ namespace XenAPI
|
||||
|
||||
public static List<XenRef<T>> ParseSetRef<T>(Hashtable table, string key) where T : XenObject<T>
|
||||
{
|
||||
var rs = (object[])table[key];
|
||||
return rs == null ? null : XenRef<T>.Create(rs);
|
||||
return ParseSxpList((string)table[key]).Select(XenRef<T>.Create).ToList();
|
||||
}
|
||||
|
||||
public static Dictionary<XenRef<T>, T> ParseMapRefRecord<T, U>(Hashtable table, string key) where T : XenObject<T>
|
||||
|
||||
private static Hashtable ParseSxpDict(string p)
|
||||
{
|
||||
Hashtable map = ParseHashTable(table, key);
|
||||
return map == null ? null : XenRef<T>.Create<U>(map);
|
||||
var result = new Hashtable();
|
||||
|
||||
using (var enumerator = Tokenize(p).GetEnumerator())
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
return result;
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
if (enumerator.Current == ")")
|
||||
break;
|
||||
|
||||
enumerator.MoveNext();
|
||||
var key = enumerator.Current;
|
||||
enumerator.MoveNext();
|
||||
var value = enumerator.Current;
|
||||
enumerator.MoveNext();
|
||||
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> ParseSxpList(string p)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
foreach (var token in Tokenize(p))
|
||||
{
|
||||
if (token == "(" || token == ")")
|
||||
continue;
|
||||
|
||||
result.Add(token);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> Tokenize(string str)
|
||||
{
|
||||
bool inStr = false;
|
||||
int j = 0;
|
||||
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
{
|
||||
switch (str[i])
|
||||
{
|
||||
case '(':
|
||||
if (!inStr)
|
||||
yield return "(";
|
||||
break;
|
||||
case ')':
|
||||
if (!inStr)
|
||||
yield return ")";
|
||||
break;
|
||||
|
||||
case '\'':
|
||||
case '"':
|
||||
if (!inStr)
|
||||
{
|
||||
inStr = true;
|
||||
j = i;
|
||||
}
|
||||
else if (str[i - 1] != '\\')
|
||||
{
|
||||
inStr = false;
|
||||
yield return str.Substring(j + 1, i - j - 1).Replace("\\\"", "\"").Replace("\\\'", "\'");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -532,6 +532,7 @@
|
||||
<Compile Include="XenAPI\XenObject.cs" />
|
||||
<Compile Include="XenObjectDownloader.cs" />
|
||||
<Compile Include="XenAPI\XenRef.cs" />
|
||||
<Compile Include="Db.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="..\Branding\Branding.ja.resx">
|
||||
|
Loading…
Reference in New Issue
Block a user