rhino-licensing
rhino-licensing copied to clipboard
SntpClient failure notification
The SntpClient class is notifying failures multiple times. This can be reproduced by pulling the network cable from machine. I added the following check to avoid this:
public void BeginGetDate(Action<DateTime> getTime, Action failure)
{
_index += 1;
if (_hosts.Length <= _index)
{
if (_hosts.Length == _index)
{
failure();
}
return;
}
I suspect that there may be a deeper problem here but the fix isn't obvious to me.
The actual reason for the failure isn't that interesting. It can be a network cable not connected, bad WIFI connection,etc.
We want to handle all errors in the same manner
I think that the problem is that MaybeOperationTimeout is not checking to see if a socket was created:
private void MaybeOperationTimeout(object state, bool timedOut)
{
if (timedOut == false)
return;
var theState = (State)state;
try
{
theState.Socket?.Close();
}
This results in an unnecessary retry because Dns.BeginGetHostAddresses is failing (it takes longer than 1/2 second) and there is no socket.
The problem is that it is notifying the failure multiple times. I think that it should either call the success or failure function once.
I think that the root cause of the problem that MaybeOperationTimeout is called from RegisterWaitForTimeout which is called from BeginGetDate and there is a timeout but no socket. Dns.BeginGetHostAddresses throws an exception (it takes a while) which causes a recursive BeginGetDate. The null socket also causes a BeginGetDate as well.
What I did was:
- Only notify 1 time
- Test if the socket is null in MaybeOperationTimeout
Thanks for contributing the code.
On Thu, Aug 27, 2015 at 12:18 AM, Ayende Rahien [email protected] wrote:
The actual reason for the failure isn't that interesting. It can be a network cable not connected, bad WIFI connection,etc.
We want to handle all errors in the same manner
— Reply to this email directly or view it on GitHub https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135295215 .
Can you show your changes? I'm not sure that I can follow from the description.
See attached code.
On Fri, Aug 28, 2015 at 5:53 AM, Ayende Rahien [email protected] wrote:
Can you show your changes? I'm not sure that I can follow from the description.
— Reply to this email directly or view it on GitHub https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135738405 .
using System; using System.CodeDom.Compiler; using System.Net; using System.Net.Sockets; using System.Threading;
namespace LicensingShared
{
///
/// Provides support for asynchronously acquiring the current time
/// via well known sources for time.
/// </summary>
/// <remarks>
/// This code is taken from <a href="https://github.com/ayende/rhino-licensing">rhino-licensing</a>.
/// See <a href="https://github.com/ayende/rhino-licensing/blob/master/license.txt">copyright notice</a>.
/// Minor corrections where made to the code as a result of testing.
/// </remarks>
[GeneratedCode(@"rhino-licensing", @"08/25/2015")]
internal class SntpClient
{
private const byte SntpDataLength = 48;
private readonly string[] _hosts;
private int _index = -1;
public SntpClient(string[] hosts)
{
_hosts = hosts;
}
private static bool GetIsServerMode(byte[] sntpData)
{
return (sntpData[0] & 0x7) == 4 /* server mode */;
}
private static DateTime GetTransmitTimestamp(byte[] sntpData)
{
var milliSeconds = GetMilliSeconds(sntpData, 40);
return ComputeDate(milliSeconds);
}
private static DateTime ComputeDate(ulong milliseconds)
{
return new DateTime(1900, 1, 1).Add(TimeSpan.FromMilliseconds(milliseconds));
}
private static ulong GetMilliSeconds(byte[] sntpData, byte offset)
{
ulong intpart = 0, fractpart = 0;
for (var i = 0; i <= 3; i++)
{
intpart = 256 * intpart + sntpData[offset + i];
}
for (var i = 4; i <= 7; i++)
{
fractpart = 256 * fractpart + sntpData[offset + i];
}
var milliseconds = intpart * 1000 + (fractpart * 1000) / 0x100000000L;
return milliseconds;
}
public void BeginGetDate(Action<DateTime> getTime, Action failure)
{
_index += 1;
if (_hosts.Length <= _index)
{
if (_hosts.Length == _index)
{
failure();
}
return;
}
try
{
var host = _hosts[_index];
var state = new State(null, null, getTime, failure);
var result = Dns.BeginGetHostAddresses(host, EndGetHostAddress, state);
RegisterWaitForTimeout(state, result);
}
catch (Exception)
{
// retry, recursion stops at the end of the hosts
BeginGetDate(getTime, failure);
}
}
private void EndGetHostAddress(IAsyncResult asyncResult)
{
var state = (State)asyncResult.AsyncState;
try
{
var addresses = Dns.EndGetHostAddresses(asyncResult);
var endPoint = new IPEndPoint(addresses[0], 123);
var socket = new UdpClient();
socket.Connect(endPoint);
socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 500);
socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 500);
var sntpData = new byte[SntpDataLength];
sntpData[0] = 0x1B; // version = 4 & mode = 3 (client)
var newState = new State(socket, endPoint, state.GetTime, state.Failure);
var result = socket.BeginSend(sntpData, sntpData.Length, EndSend, newState);
RegisterWaitForTimeout(newState, result);
}
catch (Exception)
{
// retry, recursion stops at the end of the hosts
BeginGetDate(state.GetTime, state.Failure);
}
}
private void RegisterWaitForTimeout(State newState, IAsyncResult result)
{
if (result != null)
{
ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, MaybeOperationTimeout, newState, 500,
true);
}
}
private void MaybeOperationTimeout(object state, bool timedOut)
{
if (timedOut == false)
return;
var theState = (State)state;
try
{
theState.Socket?.Close();
}
catch (Exception)
{
// retry, recursion stops at the end of the hosts
BeginGetDate(theState.GetTime, theState.Failure);
}
}
private void EndSend(IAsyncResult ar)
{
var state = (State)ar.AsyncState;
try
{
state.Socket.EndSend(ar);
var result = state.Socket.BeginReceive(EndReceive, state);
RegisterWaitForTimeout(state, result);
}
catch
{
state.Socket.Close();
BeginGetDate(state.GetTime, state.Failure);
}
}
private void EndReceive(IAsyncResult ar)
{
var state = (State)ar.AsyncState;
try
{
var endPoint = state.EndPoint;
var sntpData = state.Socket.EndReceive(ar, ref endPoint);
if (IsResponseValid(sntpData) == false)
{
state.Failure();
return;
}
var transmitTimestamp = GetTransmitTimestamp(sntpData);
state.GetTime(transmitTimestamp);
}
catch
{
BeginGetDate(state.GetTime, state.Failure);
}
finally
{
state.Socket.Close();
}
}
private bool IsResponseValid(byte[] sntpData)
{
return sntpData.Length >= SntpDataLength && GetIsServerMode(sntpData);
}
#region Nested type: State
private class State
{
public State(UdpClient socket, IPEndPoint endPoint, Action<DateTime> getTime, Action failure)
{
Socket = socket;
EndPoint = endPoint;
GetTime = getTime;
Failure = failure;
}
public UdpClient Socket { get; private set; }
public Action<DateTime> GetTime { get; private set; }
public Action Failure { get; private set; }
public IPEndPoint EndPoint { get; private set; }
}
#endregion
}
}
I can't really read this. Can you send this as a PR, that way it would be much easier to see the changes.
*Hibernating Rhinos Ltd *
Oren Eini* l CEO l *Mobile: + 972-52-548-6969
Office: +972-4-622-7811 *l *Fax: +972-153-4-622-7811
On Fri, Aug 28, 2015 at 4:54 PM, trailway [email protected] wrote:
See attached code.
On Fri, Aug 28, 2015 at 5:53 AM, Ayende Rahien [email protected] wrote:
Can you show your changes? I'm not sure that I can follow from the description.
— Reply to this email directly or view it on GitHub < https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135738405
.
using System; using System.CodeDom.Compiler; using System.Net; using System.Net.Sockets; using System.Threading;
namespace LicensingShared { ///
/// Provides support for asynchronously acquiring the current time /// via well known sources for time. ///
///
/// This code is taken from rhino-licensing. /// See copyright notice. /// Minor corrections where made to the code as a result of testing. /// [GeneratedCode(@"rhino-licensing", @"08/25/2015")] internal class SntpClient { private const byte SntpDataLength = 48; private readonly string[] _hosts; private int _index = -1;public SntpClient(string[] hosts) { _hosts = hosts; }
private static bool GetIsServerMode(byte[] sntpData) { return (sntpData[0] & 0x7) == 4 /* server mode */; }
private static DateTime GetTransmitTimestamp(byte[] sntpData) { var milliSeconds = GetMilliSeconds(sntpData, 40); return ComputeDate(milliSeconds); }
private static DateTime ComputeDate(ulong milliseconds) { return new DateTime(1900, 1, 1).Add(TimeSpan.FromMilliseconds(milliseconds)); }
private static ulong GetMilliSeconds(byte[] sntpData, byte offset) { ulong intpart = 0, fractpart = 0;
for (var i = 0; i <= 3; i++) { intpart = 256 * intpart + sntpData[offset + i]; } for (var i = 4; i <= 7; i++) { fractpart = 256 * fractpart + sntpData[offset + i]; } var milliseconds = intpart * 1000 + (fractpart * 1000) / 0x100000000L; return milliseconds; }
public void BeginGetDate(Action<DateTime> getTime, Action failure) { _index += 1; if (_hosts.Length <= _index) { if (_hosts.Length == _index) { failure(); } return; } try { var host = _hosts[_index]; var state = new State(null, null, getTime, failure); var result = Dns.BeginGetHostAddresses(host, EndGetHostAddress, state); RegisterWaitForTimeout(state, result); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(getTime, failure); } }
private void EndGetHostAddress(IAsyncResult asyncResult) { var state = (State)asyncResult.AsyncState; try { var addresses = Dns.EndGetHostAddresses(asyncResult); var endPoint = new IPEndPoint(addresses[0], 123);
var socket = new UdpClient(); socket.Connect(endPoint); socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 500); socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 500); var sntpData = new byte[SntpDataLength]; sntpData[0] = 0x1B; // version = 4 & mode = 3 (client)
var newState = new State(socket, endPoint, state.GetTime, state.Failure); var result = socket.BeginSend(sntpData, sntpData.Length, EndSend, newState); RegisterWaitForTimeout(newState, result); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(state.GetTime, state.Failure); } }
private void RegisterWaitForTimeout(State newState, IAsyncResult result) { if (result != null) { ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, MaybeOperationTimeout, newState, 500, true); } }
private void MaybeOperationTimeout(object state, bool timedOut) { if (timedOut == false) return;
var theState = (State)state; try { theState.Socket?.Close(); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(theState.GetTime, theState.Failure); } }
private void EndSend(IAsyncResult ar) { var state = (State)ar.AsyncState; try { state.Socket.EndSend(ar); var result = state.Socket.BeginReceive(EndReceive, state); RegisterWaitForTimeout(state, result); } catch { state.Socket.Close(); BeginGetDate(state.GetTime, state.Failure); } }
private void EndReceive(IAsyncResult ar) { var state = (State)ar.AsyncState; try { var endPoint = state.EndPoint; var sntpData = state.Socket.EndReceive(ar, ref endPoint); if (IsResponseValid(sntpData) == false) { state.Failure(); return; } var transmitTimestamp = GetTransmitTimestamp(sntpData); state.GetTime(transmitTimestamp); } catch { BeginGetDate(state.GetTime, state.Failure); } finally { state.Socket.Close(); } }
private bool IsResponseValid(byte[] sntpData) { return sntpData.Length >= SntpDataLength && GetIsServerMode(sntpData); }
#region Nested type: State
private class State { public State(UdpClient socket, IPEndPoint endPoint, Action<DateTime> getTime, Action failure) { Socket = socket; EndPoint = endPoint; GetTime = getTime; Failure = failure; }
public UdpClient Socket { get; private set; }
public Action<DateTime> GetTime { get; private set; }
public Action Failure { get; private set; }
public IPEndPoint EndPoint { get; private set; } }
#endregion } }
— Reply to this email directly or view it on GitHub https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135780507 .
Done.
My first pull request.
On Fri, Aug 28, 2015 at 11:07 AM, Ayende Rahien [email protected] wrote:
I can't really read this. Can you send this as a PR, that way it would be much easier to see the changes.
*Hibernating Rhinos Ltd *
Oren Eini* l CEO l *Mobile: + 972-52-548-6969
Office: +972-4-622-7811 *l *Fax: +972-153-4-622-7811
On Fri, Aug 28, 2015 at 4:54 PM, trailway [email protected] wrote:
See attached code.
On Fri, Aug 28, 2015 at 5:53 AM, Ayende Rahien <[email protected]
wrote:
Can you show your changes? I'm not sure that I can follow from the description.
— Reply to this email directly or view it on GitHub <
https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135738405
.
using System; using System.CodeDom.Compiler; using System.Net; using System.Net.Sockets; using System.Threading;
namespace LicensingShared { ///
/// Provides support for asynchronously acquiring the current time /// via well known sources for time. ///
///
/// This code is taken from rhino-licensing. /// See copyright notice. /// Minor corrections where made to the code as a result of testing. /// [GeneratedCode(@"rhino-licensing", @"08/25/2015")] internal class SntpClient { private const byte SntpDataLength = 48; private readonly string[] _hosts; private int _index = -1;public SntpClient(string[] hosts) { _hosts = hosts; }
private static bool GetIsServerMode(byte[] sntpData) { return (sntpData[0] & 0x7) == 4 /* server mode */; }
private static DateTime GetTransmitTimestamp(byte[] sntpData) { var milliSeconds = GetMilliSeconds(sntpData, 40); return ComputeDate(milliSeconds); }
private static DateTime ComputeDate(ulong milliseconds) { return new DateTime(1900, 1, 1).Add(TimeSpan.FromMilliseconds(milliseconds)); }
private static ulong GetMilliSeconds(byte[] sntpData, byte offset) { ulong intpart = 0, fractpart = 0;
for (var i = 0; i <= 3; i++) { intpart = 256 * intpart + sntpData[offset + i]; } for (var i = 4; i <= 7; i++) { fractpart = 256 * fractpart + sntpData[offset + i]; } var milliseconds = intpart * 1000 + (fractpart * 1000) / 0x100000000L; return milliseconds; }
public void BeginGetDate(Action<DateTime> getTime, Action failure) { _index += 1; if (_hosts.Length <= _index) { if (_hosts.Length == _index) { failure(); } return; } try { var host = _hosts[_index]; var state = new State(null, null, getTime, failure); var result = Dns.BeginGetHostAddresses(host, EndGetHostAddress, state); RegisterWaitForTimeout(state, result); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(getTime, failure); } }
private void EndGetHostAddress(IAsyncResult asyncResult) { var state = (State)asyncResult.AsyncState; try { var addresses = Dns.EndGetHostAddresses(asyncResult); var endPoint = new IPEndPoint(addresses[0], 123);
var socket = new UdpClient(); socket.Connect(endPoint); socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 500); socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 500); var sntpData = new byte[SntpDataLength]; sntpData[0] = 0x1B; // version = 4 & mode = 3 (client)
var newState = new State(socket, endPoint, state.GetTime, state.Failure); var result = socket.BeginSend(sntpData, sntpData.Length, EndSend, newState); RegisterWaitForTimeout(newState, result); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(state.GetTime, state.Failure); } }
private void RegisterWaitForTimeout(State newState, IAsyncResult result) { if (result != null) { ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, MaybeOperationTimeout, newState, 500, true); } }
private void MaybeOperationTimeout(object state, bool timedOut) { if (timedOut == false) return;
var theState = (State)state; try { theState.Socket?.Close(); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(theState.GetTime, theState.Failure); } }
private void EndSend(IAsyncResult ar) { var state = (State)ar.AsyncState; try { state.Socket.EndSend(ar); var result = state.Socket.BeginReceive(EndReceive, state); RegisterWaitForTimeout(state, result); } catch { state.Socket.Close(); BeginGetDate(state.GetTime, state.Failure); } }
private void EndReceive(IAsyncResult ar) { var state = (State)ar.AsyncState; try { var endPoint = state.EndPoint; var sntpData = state.Socket.EndReceive(ar, ref endPoint); if (IsResponseValid(sntpData) == false) { state.Failure(); return; } var transmitTimestamp = GetTransmitTimestamp(sntpData); state.GetTime(transmitTimestamp); } catch { BeginGetDate(state.GetTime, state.Failure); } finally { state.Socket.Close(); } }
private bool IsResponseValid(byte[] sntpData) { return sntpData.Length >= SntpDataLength && GetIsServerMode(sntpData); }
#region Nested type: State
private class State { public State(UdpClient socket, IPEndPoint endPoint, Action<DateTime> getTime, Action failure) { Socket = socket; EndPoint = endPoint; GetTime = getTime; Failure = failure; }
public UdpClient Socket { get; private set; }
public Action<DateTime> GetTime { get; private set; }
public Action Failure { get; private set; }
public IPEndPoint EndPoint { get; private set; } }
#endregion } }
— Reply to this email directly or view it on GitHub < https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135780507
.
— Reply to this email directly or view it on GitHub https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135817838 .
Thanks, I mreged it.
*Hibernating Rhinos Ltd *
Oren Eini* l CEO l *Mobile: + 972-52-548-6969
Office: +972-4-622-7811 *l *Fax: +972-153-4-622-7811
On Fri, Aug 28, 2015 at 8:46 PM, trailway [email protected] wrote:
Done.
My first pull request.
On Fri, Aug 28, 2015 at 11:07 AM, Ayende Rahien [email protected] wrote:
I can't really read this. Can you send this as a PR, that way it would be much easier to see the changes.
*Hibernating Rhinos Ltd *
Oren Eini* l CEO l *Mobile: + 972-52-548-6969
Office: +972-4-622-7811 *l *Fax: +972-153-4-622-7811
On Fri, Aug 28, 2015 at 4:54 PM, trailway [email protected] wrote:
See attached code.
On Fri, Aug 28, 2015 at 5:53 AM, Ayende Rahien < [email protected]
wrote:
Can you show your changes? I'm not sure that I can follow from the description.
— Reply to this email directly or view it on GitHub <
https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135738405
.
using System; using System.CodeDom.Compiler; using System.Net; using System.Net.Sockets; using System.Threading;
namespace LicensingShared { ///
/// Provides support for asynchronously acquiring the current time /// via well known sources for time. ///
///
/// This code is taken from rhino-licensing. /// See copyright notice. /// Minor corrections where made to the code as a result of testing. /// [GeneratedCode(@"rhino-licensing", @"08/25/2015")] internal class SntpClient { private const byte SntpDataLength = 48; private readonly string[] _hosts; private int _index = -1;public SntpClient(string[] hosts) { _hosts = hosts; }
private static bool GetIsServerMode(byte[] sntpData) { return (sntpData[0] & 0x7) == 4 /* server mode */; }
private static DateTime GetTransmitTimestamp(byte[] sntpData) { var milliSeconds = GetMilliSeconds(sntpData, 40); return ComputeDate(milliSeconds); }
private static DateTime ComputeDate(ulong milliseconds) { return new DateTime(1900, 1, 1).Add(TimeSpan.FromMilliseconds(milliseconds)); }
private static ulong GetMilliSeconds(byte[] sntpData, byte offset) { ulong intpart = 0, fractpart = 0;
for (var i = 0; i <= 3; i++) { intpart = 256 * intpart + sntpData[offset + i]; } for (var i = 4; i <= 7; i++) { fractpart = 256 * fractpart + sntpData[offset + i]; } var milliseconds = intpart * 1000 + (fractpart * 1000) / 0x100000000L; return milliseconds; }
public void BeginGetDate(Action<DateTime> getTime, Action failure) { _index += 1; if (_hosts.Length <= _index) { if (_hosts.Length == _index) { failure(); } return; } try { var host = _hosts[_index]; var state = new State(null, null, getTime, failure); var result = Dns.BeginGetHostAddresses(host, EndGetHostAddress, state); RegisterWaitForTimeout(state, result); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(getTime, failure); } }
private void EndGetHostAddress(IAsyncResult asyncResult) { var state = (State)asyncResult.AsyncState; try { var addresses = Dns.EndGetHostAddresses(asyncResult); var endPoint = new IPEndPoint(addresses[0], 123);
var socket = new UdpClient(); socket.Connect(endPoint); socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, 500); socket.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.SendTimeout, 500); var sntpData = new byte[SntpDataLength]; sntpData[0] = 0x1B; // version = 4 & mode = 3 (client)
var newState = new State(socket, endPoint, state.GetTime, state.Failure); var result = socket.BeginSend(sntpData, sntpData.Length, EndSend, newState); RegisterWaitForTimeout(newState, result); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(state.GetTime, state.Failure); } }
private void RegisterWaitForTimeout(State newState, IAsyncResult result) { if (result != null) { ThreadPool.RegisterWaitForSingleObject(result.AsyncWaitHandle, MaybeOperationTimeout, newState, 500, true); } }
private void MaybeOperationTimeout(object state, bool timedOut) { if (timedOut == false) return;
var theState = (State)state; try { theState.Socket?.Close(); } catch (Exception) { // retry, recursion stops at the end of the hosts BeginGetDate(theState.GetTime, theState.Failure); } }
private void EndSend(IAsyncResult ar) { var state = (State)ar.AsyncState; try { state.Socket.EndSend(ar); var result = state.Socket.BeginReceive(EndReceive, state); RegisterWaitForTimeout(state, result); } catch { state.Socket.Close(); BeginGetDate(state.GetTime, state.Failure); } }
private void EndReceive(IAsyncResult ar) { var state = (State)ar.AsyncState; try { var endPoint = state.EndPoint; var sntpData = state.Socket.EndReceive(ar, ref endPoint); if (IsResponseValid(sntpData) == false) { state.Failure(); return; } var transmitTimestamp = GetTransmitTimestamp(sntpData); state.GetTime(transmitTimestamp); } catch { BeginGetDate(state.GetTime, state.Failure); } finally { state.Socket.Close(); } }
private bool IsResponseValid(byte[] sntpData) { return sntpData.Length >= SntpDataLength && GetIsServerMode(sntpData); }
#region Nested type: State
private class State { public State(UdpClient socket, IPEndPoint endPoint, Action<DateTime> getTime, Action failure) { Socket = socket; EndPoint = endPoint; GetTime = getTime; Failure = failure; }
public UdpClient Socket { get; private set; }
public Action<DateTime> GetTime { get; private set; }
public Action Failure { get; private set; }
public IPEndPoint EndPoint { get; private set; } }
#endregion } }
— Reply to this email directly or view it on GitHub <
https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135780507
.
— Reply to this email directly or view it on GitHub < https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135817838
.
— Reply to this email directly or view it on GitHub https://github.com/ayende/rhino-licensing/issues/12#issuecomment-135842901 .