A bit about the stuff I've done


Wednesday, 24 July 2013

Private members in javascript classes

Whoever told you this couldn't be done was lying.

By utilising closures we can create a class that holds its own private members which cannot be accessed outside of that class - even by adding a new function onto the class

Consider the following javascript function

function Outer() { var PrivateVar='PrivateValue'; function Inner() { PrivateVar='Some Value'; } }

This is known as a closure.
You can read more about them here: http://stackoverflow.com/questions/111102/how-do-javascript-closures-work

Put simply the inner function has access to the variables in the outer function.
These variables are "live" - that is they are not fixed when the inner function was created.

Now consider that the Outer and Inner functions are, in fact, constructors

function MyClassWithPrivateVars(params) { var PrivateVar; function MyClass(params) { PrivateVar='Initial Value'; } return new MyClass(params); } var Instance = new MyClassWithPrivateVars('whatever');

When you call the outer constructor you are actually calling the inner constructor.
The inner constructor has access to the vars within the outer constructor as before.

Let's add a method to the inner constructor.
This new method of course has access to the variables in the outer constructor.

In this instance the method is a "property".

function MyClassWithPrivateVars(params) { var PrivateVar; function MyClass(params) { PrivateVar='Initial Value'; this.PublicProperty = function(value) { if (typeof(value)!=="undefined") { PrivateVar=value; } return PrivateVar; } } return new MyClass(); }

Now if we create an instance of MyClassWithPrivateVars and invoke the property we'll see that it gets the value as expected.

var MyInstance=new MyClassWithPrivateVars(); console.log(MyInstance.PublicProperty());

Outputs "Initial Value" because the private variable was set to that value in the constructor.

MyInstance.PublicProperty(10); console.log(MyInstance.PublicProperty());

This time we get 10

So is the variable really private?

Well - let's try and access it directly:

console.log(MyInstance.PrivateVar);

This should output undefined as PrivateVar is not a member of the inner class.

Likewise

console.log(MyClassWithPrivateVars.PrivateVar);

is also undefined because PrivateVar is a member of the instance of the outer class - not of the constructor itself.

Friday, 31 May 2013

Type 'System.Collections.Generic.IDictionary`2[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089],[System.Object, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' is not supported for deserialization of an array.

So I was getting this error when calling a page method from jquery. Google turned up quite a few variations but not the exact string, and the suggestions associated with those variations were not relevant. It turned out that in my case the data packet I was sending in the jquery ajax call was wrong. I had code like this:

MyParam = []; ... $.ajax({ type: 'POST', url: document.location.pathname + '/DoSomething', data: JSON.stringify(MyParam), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (a, b, c, d) { console.log(a, b, c, d); } });

and my page method looked like this:

[WebMethod()] public static void DoSomething(List<s> B) { B.ToString(); }

fairly simple but there are 2 things wrong with this. The first problem was the data - I hadn't named the paramater

so the fixed javascript code looks like this:

MyParam = []; ... $.ajax({ type: 'POST', url: document.location.pathname + '/DoSomething', data: JSON.stringify({B:MyParam}), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (a, b, c, d) { console.log(a, b, c, d); } });

The second problem was it turns out the .NET javascript seraliser doesn't like List<t> so a quick change to the C# fixed that.

[WebMethod()] public static void DoSomething(S[] B) { B.ToString(); }

So in summary - make sure you pass an object with named parameters as the data argument - not simple the content of the only parameter!

Friday, 4 January 2013

Staging long loops in Javascript

So I've encounted this issue a number of times where you need to do something, probably in a loop, which is going to take some time and the browser will "hang" will the loop takes place.
Sometimes you may even get a message pop up asking if you want to terminate a stuck script.

Not only does this make for a bad user experience but it can cause scripts to terminate without warning.

 The solution?
Plenty of posts on forum sites such as StackOverflow will tell you to use setTimeout.
It can get kind of messy though - so I thought I'd write a wrapper function to take away the pain, and have something that *looks* mostly like the familiar simple loop.
Now obviously there is a performance hit by adding all these extra "unnecessary" delays. (approx. 30% for a tight loop, less for a complex one) BUT if the browser pops up a box and waits for the user to respond, this will add much more delay to the total execution time!
so - heres the code:
function StagedLoop(IterationsBetweenPauses, Delay, AtLeastOneDelay, Test, Run, Complete) { /// <summary>Stages a loop, inserting timer pauses every n Iterations ///Warning - this method is asyncronus and will exit before looping is completed! /// put code to be run only after looping has finished into the Complete inline function parameter ///</summary> ///<param name="Test">A method to run which will perform the "test" part of the loop to see if looping should continue. This test will be run at the start of the loop (i.e. the loop may run 0 or more times)</param> ///<param name="Run">A method which will be run on every iteration of the loop - don't forget to increment any counters!</param> ///<param name="IterationsBetweenPauses">How many loops to run before using a timer pause - suggestion: use a number between 10 and 10000, but if iteration counters will appear anywhere on screen don't use a round number (i.e. 1000)</param> ///<param name="Delay">Milliseconds to set the timer pause for between stages. Recommended value: 1 - this gives the browser ample time to do "stuff" but avoids unnecassary delay. N.B. the actual delay could be substantially longer than the one specified here due to the way browser events are handled</param> ///<param name="Complete">Function to run when the loop has exited</param> if (IterationsBetweenPauses.IterationsBetweenPauses) { Run = IterationsBetweenPauses.Run; Delay = IterationsBetweenPauses.Delay; Complete = IterationsBetweenPauses.Complete; AtLeastOneDelay = IterationsBetweenPauses.AtLeastOneDelay; Test = IterationsBetweenPauses.Test; IterationsBetweenPauses = IterationsBetweenPauses.IterationsBetweenPauses; } if (AtLeastOneDelay) { setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, Complete: Complete, AtLeastOneDelay: false }); return; } var RemainingIterations = IterationsBetweenPauses; while (RemainingIterations > 0) { if (Test && !Test()) { Complete(); return; } Run(); RemainingIterations--; } setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, AtLeastOneDelay: AtLeastOneDelay, Complete: Complete }); } And here is an example usage: var I = 0; StagedLoop(4567, 1, false,function () { return I < 10000; }, function () { DoSomethingWith(I); I++; //don't forget to increment! },function(){ //run the next bit of code here }); //N.B. code places here will be run *before* the loop exists (possibly before it runs even a single iteration) //Don't put any code here! This would replace a simple for loop such as this one: for (var I=0;I<10000;I++) { DoSomethingWith(I); } //run the next bit of code here
ok - so it's not quite as clean, but it gets the job done with minimal fuss! Complete test harness below showing the difference in times taken <div id="here">Please Wait... - there is a good chance the browser will "hang" at this point</div> <script type="text/javascript"> setTimeout(RunTest, 100); function RunTest() { var Iterations = 100000; var I; var Start = new Date().getTime(); for (I = 0; I < Iterations; I++) { document.getElementById('here').innerHTML = I; } var End = new Date().getTime(); var Normal = (End - Start); I = 0; Start = new Date().getTime(); StagedLoop(4567, 1, false,function () { return I < Iterations; }, function () { document.getElementById('here').innerHTML = I; I++; }, function () { document.getElementById('here').innerHTML = 'Please wait...'; End = new Date().getTime(); var Staged = (End - Start); console.log('Duration for staged loop: ' + Staged); console.log('Duration for "normal" loop: ' + Normal); document.getElementById('here').innerHTML = 'Duration for "normal" loop: ' + Normal + '<br/>' + 'Duration for staged loop: ' + Staged + '<br/>' + '% performance loss: ' + Math.round(((100.0 * (Staged - Normal)) / Normal)) + '%'; console.log(Staged - Normal); console.log(100.0 * (Staged - Normal)); console.log(((100.0 * (Staged - Normal)) / Normal)); //end inline function }); //N.B. code places here will be run *before* the loop exits (possibly before it runs even a single iteration) //Don't put any code here! } function StagedLoop(IterationsBetweenPauses, Delay, AtLeastOneDelay, Test, Run, Complete) {/// <summary>Stages a loop, inserting timer pauses every n Iterations ///Warning - this method is asyncronus and will exit before looping is completed! /// put code to be run only after looping has finished into the Complete inline function parameter ///</summary> ///<param name="Test">A method to run which will perform the "test" part of the loop to see if looping should continue. This test will be run at the start of the loop (i.e. the loop may run 0 or more times)</param> ///<param name="Run">A method which will be run on every iteration of the loop - don't forget to increment any counters!</param> ///<param name="IterationsBetweenPauses">How many loops to run before using a timer pause - suggestion: use a number between 10 and 10000, but if iteration counters will appear anywhere on screen don't use a round number (i.e. 1000)</param> ///<param name="Delay">Milliseconds to set the timer pause for between stages. Recommended value: 1 - this gives the browser ample time to do "stuff" but avoids unnecassary delay. N.B. the actual delay could be substantially longer than the one specified here due to the way browser events are handled</param> ///<param name="Complete">Function to run when the loop has exited</param> if (IterationsBetweenPauses.IterationsBetweenPauses) { Run = IterationsBetweenPauses.Run; Delay = IterationsBetweenPauses.Delay; Complete = IterationsBetweenPauses.Complete; AtLeastOneDelay = IterationsBetweenPauses.AtLeastOneDelay; Test = IterationsBetweenPauses.Test; IterationsBetweenPauses = IterationsBetweenPauses.IterationsBetweenPauses; } if (AtLeastOneDelay) { setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, Complete: Complete, AtLeastOneDelay: false }); return; } var RemainingIterations = IterationsBetweenPauses; while (RemainingIterations > 0) { if (Test && !Test()) { Complete(); return; } Run(); RemainingIterations--; } setTimeout(StagedLoop, Delay, { Test: Test, Run: Run, IterationsBetweenPauses: IterationsBetweenPauses, Delay: Delay, AtLeastOneDelay: AtLeastOneDelay, Complete: Complete }); } </script>

Monday, 22 October 2012

Solving long JSON return from a webservice

I was being plagued by this error intermittently:
Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property

Some Googling led me to realise that there is indeed a limit, and this limit is quite low.
Most of the posts explained how to increase the limit but also explained why this was a bad idea and that you should instead reduce your JSON size.

I agree with this, better to return what you can to the client and tell them to "try again" for whatever is left.
But this poses a problem - how do you know how much data you can return at once?
Google drew a blank on this one.

This little function here will do the trick:

using System.Configuration; using System.Web.Configuration; public int GetMaxJSONLength() { Configuration cConfig = WebConfigurationManager.OpenWebConfiguration(null); return ((ScriptingWebServicesSectionGroup) cConfig. SectionGroups["system.web.extensions"]. SectionGroups["scripting"]. SectionGroups["webServices"] ).JsonSerialization.MaxJsonLength; }
This can be executed from anywhere, it doesn't have to be part of the web project itself.

Something else to be aware of - any strings in your return value will be encoded.
That means characters like < and > will become \u003c and \u003e - 6 times longer than they were previously.
So if you have a string containing html this can make a substantial difference to the length of the string.

To account for this make sure you use the length of the encoded string, not the original.

Tuesday, 9 October 2012

The woes of inline functions

OK; so I like inline fucntions really - especially in javascript. But now I am realising their evil intent after I spent a not insubstantial amount of time tracing a bug.
here is the scenario:
A simple method with tbh not a whole lot in it.
In fact the entire method is taken by a single if/else block. Regardless of the outcome of both if blocks it is necassary to call a method afterwards.
So obviously that call goes outside the if.
Normally that would be fine.
But in my case the else block contained an inline function used as a callback.
A callback that will occur some undetermined (but very small) amount of time after the if block has exited.
Of course I needed to wait until this callback had been executed before calling the final method.
function DoSomething() { if (condition) { DoOneThing(); } else { AsyncronuslyDoAnother(function (result){ DoSomethingWithTheResultWhenTheAsyncCallCompletes(); }); } DoFinalThing(); } Now the simple solution in this instance was to move the call to DoFinalThing() to be inside the if block, and also inside the anonymous callback function.
function DoSomething2() { if (condition) { DoOneThing(); DoFinalThing(); } else { AsyncronuslyDoAnother(function (result){ DoSomethingWithTheResultWhenTheAsyncCallCompletes(); DoFinalThing(); }); } } However in my case the problem only arose in the first place because I simply didn't notice the inline function when I added the call to DoFinalThing() and assumed that it was all just part of the else logic.
So additionally I removed the inline function and henceforth declare all such things to be evil!
(ok well not quite, but at least I am more wary now!)
function AsyncCallback(result) { DoSomethingWithTheResultWhenTheAsyncCallCompletes(); DoFinalThing(); } function DoSomething3() { if (condition) { DoOneThing(); DoFinalThing(); } else { AsyncronuslyDoAnother(AsyncCallback); //N.B. no parens on AsyncCallback - we are passing a reference to the method not invoking it } }

Friday, 14 September 2012

Ajaxable Web Control in a Class Library

So I recently went on a course to get my AJAX knowledge up to scratch.
The course was fine and taught me quite a bit that I didn't know but the most important thing for me was missing - how do you do all this in a class library?

Basically I want the ability to have some control which has both server and client side components, for these 2 parts to talk to eachother with ajax, and I want to be able to wrap the whole thing up into a DLL file that can be dropped into any web application and with no user modifications required.

The instructor gave me this link which was helpful but didn't go the whole way.
I knew there had to be a better, more complete way, and so I set to work.

The result is what you see below.

Add the C# code in the block below to a class library and inherit from it for your server control.
You'll need a WebService and you'll have to provide that as the type parameter to the base class.
And that's about it!
The class does require some tweaking to the web.config file - but it will do that for you the first time you run it.

Here you can find the compiled DLL: http://dev.dj-djl.com/AjaxableServerControlWithWebService.zip
And a Template project for Visual Studio 2012: dev.dj-djl.com/AjaxableServerControlWithWebServiceTemplate.zip
The template control contains a single web-exposed method which is triggered when the button is clicked and the result is given to a simple alert box.

The code below contains some code taken from this Code Project post, which was modified by me.
The rest of the code is written by me. It is all licecned under an LGPL3.0 licence.   /* LGPL3.0 Classes enabling embedding of a fully AJAXable ASPX Server Control into a DLL for reuse or redistribution. Most of the code below is LGPL3.0 - Copyright (C) 2012 Lee Smith http://dev.dj-djl.com http://blog.dj-djl.com Contains some LGPL3.0 code As indicated - (C) 2007 James Ashley http://www.codeproject.com/Articles/22384/ASP-NET-AJAX-Controls-and-Extenders#http_handler LGPL3.0: This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not please see <http://www.gnu.org/licenses/>. */ using System; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Web; using System.Web.Services; using System.Web.SessionState; using System.Web.UI; namespace DJL { internal static class Utils { public static string ToDelimitedstring<T>(this List<T> lst, string Delimiter = ",") { System.Text.StringBuilder sbReturn = new StringBuilder(); Boolean flgFirst = true; foreach (T Item in lst) { if (!flgFirst) { sbReturn.Append(Delimiter); } sbReturn.Append(Item.ToString()); flgFirst = false; } return sbReturn.ToString(); } public static string CompressAndBase64(string Value) { System.IO.MemoryStream mst = new System.IO.MemoryStream(); System.IO.Compression.GZipStream gz = new System.IO.Compression.GZipStream(mst, System.IO.Compression.CompressionLevel.Optimal, true); byte[] bytValue = System.Text.Encoding.UTF8.GetBytes(Value); gz.Write(bytValue,0 , bytValue.Length); gz.Flush(); gz.Close(); return Convert.ToBase64String(mst.ToArray()).Replace("+", "_").Replace("/", "-"); } public static string DeBase64AndDecompress(string Base64Value) { //return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(Base64Value)); byte[] bytValue = Convert.FromBase64String(Base64Value.Replace("-", "/").Replace("_", "+")); System.IO.MemoryStream mst = new System.IO.MemoryStream(); System.IO.Compression.GZipStream gz =null; try { mst.Write(bytValue, 0, bytValue.Length); mst.Seek(0, System.IO.SeekOrigin.Begin); gz = new System.IO.Compression.GZipStream(mst, System.IO.Compression.CompressionMode.Decompress, false); byte[] bytDecompressedValue = new byte[102400]; //100k - should be enough! return System.Text.Encoding.UTF8.GetString(bytDecompressedValue, 0, gz.Read(bytDecompressedValue, 0, 102400)); } finally { gz.Close(); mst.Close(); } } public static string GetURLWithoutAsmx(Type type) { //System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create(); //byte[] bytTypeName = System.Text.Encoding.UTF8.GetBytes(type.Assembly.FullName + "/" + type.FullName); //return typeof(AjaxableScriptContolWithWebserviceHandlerFactory).Name + "/" + Convert.ToBase64String(md5.ComputeHash(bytTypeName)); return typeof(AjaxableScriptContolWithWebserviceHandlerFactory).Name + "/" + CompressAndBase64(type.Assembly.FullName + "/" + type.FullName); } public static string GetURLWithoutAsmx<T>() { return GetURLWithoutAsmx(typeof(T)); } } /// <summary> /// A serverside control which can be included into a DLL, with full ajax. /// It uses a webservice embedded into the same DLL to perform ajax operations. /// </summary> /// <typeparam name="TService">The type of the Webservice</typeparam> public abstract class AjaxableScriptContolWithWebservice<TService> : ScriptControl where TService : System.Web.Services.WebService { private bool flgConfigChanged = false; /// <summary> /// When set indicates that the web.config file was altered /// </summary> protected bool ConfigChanged { get { return flgConfigChanged; } } public AjaxableScriptContolWithWebservice() { CheckConfig(); this.Load += AjaxableScriptContolWithWebservice_Load; } private void CheckConfig() { //System.Configuration blah = System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~"); System.Configuration.Configuration WebConfig = System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~"); System.Xml.XmlDocument xmdWebConfig = new System.Xml.XmlDocument(); xmdWebConfig.Load(WebConfig.FilePath); if (System.Web.HttpRuntime.UsingIntegratedPipeline) { SetIntegratedConfig(xmdWebConfig, WebConfig.FilePath); } else { SetClassicConfig(xmdWebConfig, WebConfig.FilePath); } } private void SetIntegratedConfig(System.Xml.XmlDocument WebConfig, string WebConfigPath) { /* <system.webServer> <handlers> <add name="AjaxServerControl1.ServerControl1" verb="*" path="AjaxServerControl1.ServerControl1.asmx" type="AjaxServerControl1.ServerControl1" /> </handlers> </system.webServer> */ System.Xml.XmlElement xmeSystemWeb = (System.Xml.XmlElement)WebConfig.SelectSingleNode("/configuration/system.webServer"); if (xmeSystemWeb == null) { xmeSystemWeb = WebConfig.CreateElement("system.webServer"); WebConfig.DocumentElement.AppendChild(xmeSystemWeb); } System.Xml.XmlElement xmeHandlers = (System.Xml.XmlElement)xmeSystemWeb.SelectSingleNode("handlers"); if (xmeHandlers == null) { xmeHandlers = WebConfig.CreateElement("handlers"); xmeSystemWeb.AppendChild(xmeHandlers); } string strURL = Utils.GetURLWithoutAsmx<TService>(); System.Xml.XmlElement xmeAdd = (System.Xml.XmlElement)xmeHandlers.SelectSingleNode("add[@name='" + strURL + "']"); if (xmeAdd == null) { xmeAdd = WebConfig.CreateElement("add"); xmeAdd.SetAttribute("verb", "*"); xmeAdd.SetAttribute("name", strURL); xmeAdd.SetAttribute("path", strURL + ".asmx"); xmeAdd.SetAttribute("type", typeof(AjaxableScriptContolWithWebserviceHandlerFactory).FullName); xmeHandlers.AppendChild(xmeAdd); WebConfig.Save(WebConfigPath); flgConfigChanged = true; } } private void SetClassicConfig(System.Xml.XmlDocument WebConfig, string WebConfigPath) { throw new Exception(); /* <system.web> <httpHandlers> <add verb="*" path="AjaxServerControl1.ServerControl1.asmx" type="AjaxServerControl1.ServerControl1" validate="true" /> </httpHandlers> </system.web> * */ System.Xml.XmlElement xmeSystemWeb = (System.Xml.XmlElement)WebConfig.SelectSingleNode("/configuration/system.web"); if (xmeSystemWeb == null) { xmeSystemWeb = WebConfig.CreateElement("system.web"); WebConfig.DocumentElement.AppendChild(xmeSystemWeb); } System.Xml.XmlElement xmeHTTPHandlers = (System.Xml.XmlElement)xmeSystemWeb.SelectSingleNode("httpHandlers"); if (xmeHTTPHandlers == null) { xmeHTTPHandlers = WebConfig.CreateElement("httpHandlers"); xmeSystemWeb.AppendChild(xmeHTTPHandlers); } string strURL = Utils.GetURLWithoutAsmx<TService>(); System.Xml.XmlElement xmeAdd = (System.Xml.XmlElement)xmeHTTPHandlers.SelectSingleNode("add[@path='" + strURL + ".asmx']"); if (xmeAdd == null) { xmeAdd = WebConfig.CreateElement("add"); xmeAdd.SetAttribute("verb", "*"); xmeAdd.SetAttribute("path", strURL + ".asmx"); xmeAdd.SetAttribute("type", typeof(AjaxableScriptContolWithWebserviceHandlerFactory).FullName); xmeAdd.SetAttribute("validate", "true"); xmeHTTPHandlers.AppendChild(xmeAdd); WebConfig.Save(WebConfigPath); flgConfigChanged = true; } } protected bool AutoRefreshOnConfigChange { get; set; } protected override sealed void Render(HtmlTextWriter writer) { base.Render(writer); if (ConfigChanged && AutoRefreshOnConfigChange) { writer.WriteLine("<div style='position: fixed; height: 100%; width: 100%; top: 0; left: 0; background-color: rgba(128,128,128,0.85);'>\n" + " <div style='width: 40em; height: 5em; background-color: white; color: black; box-shadow: 2px 2px 0px black; text-align: center;" + "position: absolute; left: 50%; top: 50%; margin-left: -20em; margin-top: -2.5em;'><br/>" + "The web.config was updated. The page will now be reloaded.<br/>\n" + " N.B. You may have to check out the web.config file if it is under source control</div>\n" + "</div>"); writer.WriteLine("<meta http-equiv='refresh' content='1;" + this.Page.Request.Url.ToString() + "'/>"); } else { RenderContent(writer); } } protected virtual void RenderContent(HtmlTextWriter writer) { } void AjaxableScriptContolWithWebservice_Load(object sender, EventArgs e) { string strURL = Utils.GetURLWithoutAsmx<TService>(); ScriptManager.GetCurrent(this.Page).Services.Add(new ServiceReference(strURL + ".asmx")); } } // This class is largely written by James Ashley with some modifications by Lee Smith as indicated // see http://www.codeproject.com/Articles/22384/ASP-NET-AJAX-Controls-and-Extenders#http_handler for the original source code public class AjaxableScriptContolWithWebserviceHandlerFactory : IHttpHandlerFactory { #region IHttpHandlerFactory Members IHttpHandlerFactory factory = null; public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated) { Assembly ajaxAssembly = typeof(System.Web.Script.Services.GenerateScriptTypeAttribute).Assembly; factory = (IHttpHandlerFactory)System.Activator.CreateInstance(ajaxAssembly.GetType("System.Web.Script.Services.RestHandlerFactory")); IHttpHandler restHandler = (IHttpHandler)System.Activator.CreateInstance(ajaxAssembly.GetType("System.Web.Script.Services.RestHandler")); ConstructorInfo WebServiceDataConstructor = ajaxAssembly.GetType("System.Web.Script.Services.WebServiceData").GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(Type), typeof(bool) }, null); MethodInfo CreateHandlerMethod = restHandler.GetType().GetMethod("CreateHandler", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { ajaxAssembly.GetType("System.Web.Script.Services.WebServiceData"), typeof(string) }, null); IHttpHandler originalHandler = null; //LS Change the behvaiour for some URLs, original code contained some hard-coded assumptions and also did not provide the js and jsdebug methods. string strAssemblyName = context.Request.Url.Segments[2].Trim('/').Trim(); if (strAssemblyName.ToLower().EndsWith(".asmx")) { strAssemblyName = strAssemblyName.Substring(0, strAssemblyName.Length - 5); } strAssemblyName = Utils.DeBase64AndDecompress(strAssemblyName); string strTypeName = strAssemblyName.Split('/')[1].Trim(); strAssemblyName = strAssemblyName.Split('.')[0].Trim(); string strMethodName = context.Request.Url.Segments[3]; Assembly assAssembly = Assembly.Load(strAssemblyName); Type typWebServiceType = assAssembly.GetType(strTypeName); if (strMethodName == "js" || strMethodName == "jsdebug") { originalHandler = new WebserviceJSGenerator(typWebServiceType); } else { //LS End modifications originalHandler = (IHttpHandler)CreateHandlerMethod.Invoke(restHandler, new Object[]{ WebServiceDataConstructor.Invoke(new object[] { typWebServiceType, false }), strMethodName }); } //<-- LS Type t = ajaxAssembly.GetType("System.Web.Script.Services.ScriptHandlerFactory"); Type wrapperType = null; if (originalHandler is IRequiresSessionState) { wrapperType = t.GetNestedType("HandlerWrapperWithSession", BindingFlags.NonPublic | BindingFlags.Instance); } else { wrapperType = t.GetNestedType("HandlerWrapper", BindingFlags.NonPublic | BindingFlags.Instance); } return (IHttpHandler)System.Activator.CreateInstance(wrapperType, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { originalHandler, factory }, null); } public void ReleaseHandler(IHttpHandler handler) { factory.ReleaseHandler(handler); } #endregion } public class WebserviceJSGenerator : IHttpHandler { public bool IsReusable { get { return false; } } public void ProcessRequest(HttpContext context) { if (context == null) { throw new ArgumentNullException("context"); } string clientProxyScript = GetClientProxyScript(type); if (clientProxyScript != null) { context.Response.ContentType = "application/x-javascript"; context.Response.Write(clientProxyScript); } } private string GetClientProxyScript(Type Type) { StringBuilder strScript = new StringBuilder(); strScript.AppendLine("// Generated by " + this.GetType().FullName); strScript.AppendLine("// http://dev.dj-djl.com"); strScript.AppendLine("// http://blog.dj-djl.com"); strScript.AppendLine("// loosely based on the code generated by Microsoft's System.Web.Script.Services.ClientProxyGenerator class"); strScript.AppendLine("// requires other Microsoft javascript libraries to be loaded and assumes that to be the case."); strScript.AppendLine("Type.registerNamespace('" + Type.Namespace + "');"); strScript.AppendLine(Type.FullName.Replace("+", "_") + "=function(){"); strScript.AppendLine(Type.FullName.Replace("+", "_") + ".initializeBase(this);"); strScript.AppendLine(" this._timeout=0,"); strScript.AppendLine(" this._userContext=null,"); strScript.AppendLine(" this._succeeded=null,"); strScript.AppendLine(" this._failed=null}"); strScript.AppendLine(Type.FullName.Replace("+", "_") + ".prototype={"); strScript.AppendLine(" _get_path:function(){"); strScript.AppendLine(" var p = this.get_path();"); strScript.AppendLine(" if (p) return p;"); strScript.AppendLine(" else return " + Type.FullName.Replace("+", "_") + "._staticInstance.get_path();"); strScript.AppendLine(" }"); foreach (System.Reflection.MethodInfo miMethod in Type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (miMethod.GetCustomAttributes(typeof(WebMethodAttribute), true).Length > 0) { strScript.Append(" ," + miMethod.Name + ":function("); List<string> lstParametersObject = new List<string>(); List<string> lstParametersXmlComments = new List<string>(); List<string> lstParametersJSFunction = new List<string>(); foreach (System.Reflection.ParameterInfo piParam in miMethod.GetParameters()) { if (!piParam.IsRetval) { lstParametersObject.Add(piParam.Name + ":" + piParam.Name); lstParametersXmlComments.Add(" /// <param name=\"" + piParam.Name + "\" type=\"" + piParam.ParameterType.Name + "\" optional=\"" + (piParam.IsOptional ? "true" : "false") + "\" mayBeNull=\"" + (piParam.ParameterType.IsValueType ? "true" : "false") + "\"></param>"); lstParametersJSFunction.Add(piParam.Name); } } if (lstParametersJSFunction.Count > 0) { lstParametersJSFunction.Add(""); }; //add a blank parameter so that we do infact get a trailing comma, but only if there are items strScript.AppendLine(lstParametersJSFunction.ToDelimitedstring(", ") + "succeededCallback, failedCallback, userContext) {"); strScript.AppendLine(lstParametersXmlComments.ToDelimitedstring("\n")); strScript.AppendLine(" /// <param name=\"succeededCallback\" type=\"Function\" optional=\"true\" mayBeNull=\"true\"></param>"); strScript.AppendLine(" /// <param name=\"failedCallback\" type=\"Function\" optional=\"true\" mayBeNull=\"true\"></param>"); strScript.AppendLine(" /// <param name=\"userContext\" optional=\"true\" mayBeNull=\"true\"></param>"); strScript.Append(" return this._invoke(this.get_path(), '" + miMethod.Name + "', false, {"); strScript.Append(lstParametersObject.ToDelimitedstring(", ")); strScript.AppendLine("}, succeededCallback,failedCallback,userContext);} "); } } strScript.AppendLine(" }"); string strStatic = @" <<TypeName>>.registerClass('<<TypeName>>',Sys.Net.WebServiceProxy); <<TypeName>>._staticInstance = new <<TypeName>>(); <<TypeName>>.set_path = function(value) { <<TypeName>>._staticInstance.set_path(value); } <<TypeName>>.get_path = function() { /// <value type=""String"" mayBeNull=""true"">The service url.</value> return <<TypeName>>._staticInstance.get_path();} <<TypeName>>.set_timeout = function(value) { <<TypeName>>._staticInstance.set_timeout(value); } <<TypeName>>.get_timeout = function() { /// <value type=""Number"">The service timeout.</value> return <<TypeName>>._staticInstance.get_timeout(); } <<TypeName>>.set_defaultUserContext = function(value) { <<TypeName>>._staticInstance.set_defaultUserContext(value); } <<TypeName>>.get_defaultUserContext = function() { /// <value mayBeNull=""true"">The service default user context.</value> return <<TypeName>>._staticInstance.get_defaultUserContext(); } <<TypeName>>.set_defaultSucceededCallback = function(value) { <<TypeName>>._staticInstance.set_defaultSucceededCallback(value); } <<TypeName>>.get_defaultSucceededCallback = function() { /// <value type=""Function"" mayBeNull=""true"">The service default succeeded callback.</value> return <<TypeName>>._staticInstance.get_defaultSucceededCallback(); } <<TypeName>>.set_defaultFailedCallback = function(value) { <<TypeName>>._staticInstance.set_defaultFailedCallback(value); } <<TypeName>>.get_defaultFailedCallback = function() { /// <value type=""Function"" mayBeNull=""true"">The service default failed callback.</value> return <<TypeName>>._staticInstance.get_defaultFailedCallback(); } <<TypeName>>.set_enableJsonp = function(value) { <<TypeName>>._staticInstance.set_enableJsonp(value); } <<TypeName>>.get_enableJsonp = function() { /// <value type=""Boolean"">Specifies whether the service supports JSONP for cross domain calling.</value> return <<TypeName>>._staticInstance.get_enableJsonp(); } <<TypeName>>.set_jsonpCallbackParameter = function(value) { <<TypeName>>._staticInstance.set_jsonpCallbackParameter(value); } <<TypeName>>.get_jsonpCallbackParameter = function() { /// <value type=""String"">Specifies the parameter name that contains the callback function name for a JSONP request.</value> return <<TypeName>>._staticInstance.get_jsonpCallbackParameter(); } <<TypeName>>.set_path(""/<<URL>>""); "; string strURL = Utils.GetURLWithoutAsmx(Type); strScript.AppendLine(strStatic.Replace("<<NameSpace>>", Type.Namespace).Replace("<<TypeName>>", Type.FullName.Replace("+", "_")).Replace("<<URL>>", strURL + ".asmx")); foreach (System.Reflection.MethodInfo miMethod in Type.GetMethods(BindingFlags.Public | BindingFlags.Instance)) { if (miMethod.GetCustomAttributes(typeof(WebMethodAttribute), true).Length > 0) { List<string> lstParametersJSFunction = new List<string>(); List<string> lstParametersXmlComments = new List<string>(); foreach (System.Reflection.ParameterInfo piParam in miMethod.GetParameters()) { if (!piParam.IsRetval) { lstParametersJSFunction.Add(piParam.Name); lstParametersXmlComments.Add(" /// <param name=\"" + piParam.Name + "\" type=\"" + piParam.ParameterType.Name + "\" optional=\"" + (piParam.IsOptional ? "true" : "false") + "\" mayBeNull=\"" + (piParam.ParameterType.IsValueType ? "true" : "false") + "\"></param>"); } } if (lstParametersJSFunction.Count > 0) { lstParametersJSFunction.Add(""); } string strMethodStatic = @" <<TypeName>>.<<MethodName>>= function(" + lstParametersJSFunction.ToDelimitedstring(", ") + @"onSuccess,onFailed,userContext) { " + lstParametersXmlComments.ToDelimitedstring("\n") + @" /// <param name=""succeededCallback"" type=""Function"" optional=""true"" mayBeNull=""true""></param> /// <param name=""failedCallback"" type=""Function"" optional=""true"" mayBeNull=""true""></param> /// <param name=""userContext"" optional=""true"" mayBeNull=""true""></param> <<TypeName>>._staticInstance.<<MethodName>>(" + lstParametersJSFunction.ToDelimitedstring(", ") + @" onSuccess,onFailed,userContext); }"; strScript.AppendLine(strMethodStatic.Replace("<<NameSpace>>", Type.Namespace).Replace("<<TypeName>>", Type.FullName.Replace("+", "_")).Replace("<<MethodName>>", miMethod.Name)); } } return strScript.ToString(); } Type type; public WebserviceJSGenerator(Type type) { this.type = type; } } }  

Friday, 24 August 2012

Changing the OS on a remote server with no physical access

So this is a rather odd thing to want to do but actually with VPS providers having a limited range of pre-installed OSes available - which are not always the ones we are familar with, perhaps its not that odd after all.


So I have a number of VPS servers with CentOS installed.
Now I'm sure Centos is fine for a lot of people but personally I just can't get on with it.

I am a .NET developer, which means I need mono.
The version of mono which ships with CentOS 5 is just too far out of date for my needs now.
And CentOS 6 doesn't seem to have mono at all?!?

So after much battling with trying to compile applications from source, trying to add redhat repositories and install mono from there, trying a number of different solutions I finally gave up and decided to attempt to switch the OS to openSUSE, which I am familar with and I know has an up-to-date mono available with all the bells and whistles that I require.

So now the big question is - how to switch the OS on a remote (other side of world) server with no physical access?

Again I have tried a number of methods most of which ended in a dead-end but the solution I settled on seems to be quite functional, while not ideal.

IMPORTANT: before doing any of the things mentioned in this article please, please, (I cannot stress this enough) PLEASE! do a backup!

Fortunately my VPS provider has a handy "re-install" OS option in case things went spectacularly bad, but I hope never to have to use it.

Also I was working on a new VPS with nothing actually running on it yet, so if I had to delete the VPS and make a new one - it wouldn't be the end of the world.

I strongly recommend that you do NOT attempt any of the following on anything that even remotely resembles a live production server.


If you choose to ignore this warning - don't come running to me!


OK with that (somewhat verbose) disclaimer out of the way - let's get on with it!

N.B. For all the following I wil assume that you are familiar with YAST

So SUSE has this handy feature in YAST named "install to directory".

Sadly I couldn't find any way to run yast from within CentOS.

So you need to have an existing installation of SUSE somewhere, on some other server or computer.

So the first few steps are run from an existing installation of SUSE, somewhere.
Optionally you can skip to Step 11.
  1. Open yast and install the package "yast2-dirinstall"
  2. Close yast
  3. Open yast again (this is necassary to load the new module)
  4. Under software you should now have a new entry "Installation into directory" - open this.
  5. Set the target directory
  6. Turn OFF the option to run YAST and SUSEconfig.
  7. Create Image: No (actually I had this on, but it didn't seem to do anything for me?)
  8. Set the software.
  9.     At this point I recommend having a very minimal install to reduce the size of the files to be copied onto the server to be changed.
        With a minimal software selection the installed size is almost 900MB (SUSE 12.1)
        Also I think it is unlikely that you will be able to get a working X install (although I have not tried) with this method.
        You will however need at least the following packages:
    • bash
    • grep
    • sed
    • yast2
    • vim (or your favourite command line editor)
    • zypper
    • iputils
         that is a nice short list, but of course they pull in a thousand dependencies...
  10. Let that run for a while then quit YAST.
  11. Package the installed to directory into an archive and copy the archive to the server to be changed.
  12. You can optionally skip steps 1-10. By downloading the image I have created here: http://dev.dj-djl.com/TinySuse12.1.tgz




The following steps should be performed on the server to be changed.

  1. Unpack the archive somewhere sensible on your system.
    Do NOT unpack it to the root directory!
  2. Use your favorite editor to create a short script which will open the suse environment in a chroot session:
  3. user@centos# vim runsusechrooted.sh
    #!/bin/bash echo "mounting dev"
    sudo mount -o bind /dev tinysuse/dev
    echo "mounting proc"
    sudo mount -o bind /proc tinysuse/proc
    echo "mounting sys"
    sudo mount -o bind /sys tinysuse/sys

    echo "chrooting"
    sudo chroot tinysuse
    echo "chroot environment closed."
    echo "umounting dev"
    sudo umount tinysuse/dev
    echo "unmounting proc"
    sudo umount tinysuse/proc
    echo "unmounting sys"
    sudo umount tinysuse/sys

  4. set the script as executable
    chmod +x runsusechrooted.sh 
  5. run the script (your system may require you to run this as root):
    user@centos# ./runsusechrooted.sh
    mounting dev
    mounting proc
    mounting sys
    chrooting
    suse:/ #
  6. If you get this far then you now have a successful SUSE environment running inside the parent OS.
    Now the fun part begins!
    At this point in the process I, at first, attempted to get the VPS to boot from the suse system, with not much luck.
    I tried using yast within the suse system to overwrite the bootloader on the VPS.
    I tried editing the grub menu from the host centos system.
    Nothing that I tried would result in the VPS booting the suse system, it always booted CentOS. Unfortunately I can't even see the grub screen to see what errors may or may not be there. My suspicion is that the VPS provider is somehow skipping the bootloader and going straight into a predefined kernel.
    If anyone knows a way to rig it so that the VPS can boot from the new system it would save on all the faff that follows!
    The first thing is we need to get a working internet connection. so:
  7. Exit out of the chroot environment,copy the resolv.conf from the host, then go back into the chrooted environment:
    suse:/ # exit
    user@centos# cp /etc/resolv.conf tinysuse/etc/resolv.conf
    cp: overwrite `tinysuse/etc/resolv.conf'? y
    user@centos#  ./runsusechrooted.sh
    suse:/ # ping -c 1 www.google.com
    PING www.l.google.com (74.125.227.51) 56(84) bytes of data.
    64 bytes from dfw06s06-in-f19.1e100.net (74.125.227.51): icmp_seq=1 ttl=56 time=1.83 ms

    --- www.l.google.com ping statistics ---
    1 packets transmitted, 1 received, 0% packet loss, time 0ms
    rtt min/avg/max/mdev = 1.834/1.834/1.834/0.000 ms
  8. N.B. If you think your resolv.conf might change, you might want to try hard-linking it. I've not tested this so I have no idea how well it may work? 
  9.  Now we need to install any additional software that may be required:
  10. suse:/ # yast
  11. First open the Software->Software Repositories section and add at least the standard SUSE OSS repository by using the community repositories option. you may want to add other repos at this point too.
  12. Once you have done this you can install software using either yast or zipper.
So now we have a working environment, but what about services?
Thankfully I've devised a cunning hack to get services running in suse too.
  1. First make sure you don't have a conflicting service running in the host environment, I chose to completely uninstall  apache from the host to ensure there are no conflicts.
  2. user@centos# yum remove httpd
  3. Next configure the service in suse as normal (I used yast for the initial config, but will manually edit the config files later to suit my setup)
  4. finally we need a script to start the services.
    suse:/ # vim chrootboot.sh
    #!/bin/bash
    NEW=$1
    OLD=$( cat /runlevel )
     [ "a${OLD}a" == "aa" ] && OLD=0
    IGNORE="network halt reboot"
    echo Changing runlevel from $OLD to $NEW

            START=$(diff <(ls /etc/init.d/rc${OLD}.d -1 | grep "^S" | sed "s/^S[0-9]*//" |sort ) <(ls /etc/init.d/rc${NEW}.d -1 | grep "^S" | sed "s/^S[0-9]*//" | sort ) | grep "^>" | sed "s/^> //")

            STOP=$( diff <(ls /etc/init.d/rc${OLD}.d -1 | grep "^K" | sed "s/^K[0-9]*//" |sort ) <(ls /etc/init.d/rc${NEW}.d -1 | grep "^S" | sed "s/^S[0-9]*//" | sort ) | grep "^<" | sed "s/^< //")

    echo Stopping services in runlevel $OLD

            echo ${STOP} | grep -v "^ *$" | sed "s/ /\n/g" |  grep -v "$(echo $IGNORE | sed "s/ /\\\|/g")" |  sed -e "s#^.*\$#ls -1 /etc/rc.d/rc${OLD}.d/K*\0#" | bash | sort | sed -e "s#^/etc/rc.d/rc${OLD}.d/K[0-9]*\([^0-9].*\)\$#/sbin/service \1 stop#"  | bash

    echo Starting Services in runlevel $1

            echo ${START} | grep -v "^ *$" | sed "s/ /\n/g" |  grep -v "$(echo $IGNORE | sed "s/ /\\\|/g")" | sed -e "s#^.*\$#ls -1 /etc/rc.d/rc${NEW}.d/S*\0#" | bash | sort | sed -e "s#^/etc/rc.d/rc${NEW}.d/S[0-9]*\([^0-9].*\)\$#/sbin/service \1 start#"  | bash

    echo $1 > /runlevel

    N.B. You may want to disable some services to avoid them erroring when they try to start.
  5. Now if you exit from the chroot environment at this point you will notice that the unmounting fails - this is because the services remain running (this is good!)
  6. But - if you reboot the host, you will also notice that the services are not started again.
  7. So now we need a way to start the chrooted services on system boot, and close them cleanly when we reboot.
  8. For this we will create a new host service.
    user@centos# cd /etc/rc.d/init.d
    user@centos# vim susechroot
    #!/bin/bash . /etc/init.d/functions #echo -e "\n\n $(date +%Y-%m-%dT%H%M%S ) $0 $*" >> /susechroot.log case $1 in start)  mount  -o bind /dev /tinysuse/dev         mount -o bind /proc /tinysuse/proc         mount -o bind /sys /tinysuse/sys         mount none -t  devpts /tinysuse/dev/pts         chroot /tinysuse /chrootboot.sh 5         touch /var/lock/subsys/susechroot ;; stop)   chroot /tinysuse /chrootboot.sh 0         sleep 1         umount /tinysuse/dev         umount /tinysuse/proc         umount /tinysuse/sys         umount /tinysuse/dev/pts         rm -f /var/lock/subsys/susechroot ;; status) chroot /tinysuse service --status-all ;; esac
    cd ../rc3.d
    ln -s ../init.d/susechroot S99susechroot
    cd ../rc5.d
    ln -s ../init.d/susechroot S99susechroot
    cd ../rc6.d
    ln -s ../init.d/susechroot K0susechroot
    cd ../rc0.d
    ln -s ../init.d/susechroot K0susechroot
    /sbin/service susechroot start
eh Voila!

Now whenever we reboot the server it executes the scripts for runlevel 5 in the suse environment on startup.
So if we have, e.g. Apache installed in the suse environment and configured for runlevel 5. We can reboot the server and apache will be started with no manual intervention.

Now if anyone can work out how to use Suse's init system, rather than my custom script - please let me know!

Also just a note that it appears SUSE's and CentOS's init systems are actually quite different.
In SUSE the kill scripts live in the same runlevel directory as the Start scripts, but are only executed when there is no matching Start script in the target runlevel.

In CentOS the kill scripts live in all runlevel directories which do not have a start script.

This confusion delayed me in coming up with the above solution.