A bit about the stuff I've done


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.


    Friday, 17 August 2012

    Solving "Could not load file or assembly" problems on mono

    So once again I have been tearing my hair out for hours trying to solve this problem.
    Specifically in my case it was mod-mono-server2.exe that was producing this error. Now I *know* that that file exists - I checked, in fact that was the assembly I was executing.
    So it must be one of the dependencies - but which one?
    It turns out there IS a way to find out.
    By setting 2 environment variables before attempting to run the application you can get some debugging information which tells you exactly what files mono was trying to find when it errored.

    The 2 variables in question are:
    MONO_LOG_LEVEL=info MONO_LOG_MASK=asm
    So if you run
    MONO_LOG_LEVEL=info MONO_LOG_MASK=asm mono myapp.exe
    You will get a stack load of debugging info on the console, and just before the exception you should see some lines which looks something similar to this:
    Mono-INFO: Assembly Loader probing location: '/usr/lib64/mono/gac/mod-mono-server2/2.10.0.0__0738eb9f132ed756/mod-mono-server2.dll'. Mono-INFO: Assembly Loader probing location: '/usr/lib64/mod-mono-server2.dll'. Mono-INFO: Assembly Loader probing location: '/usr/lib64/mono/gac/mod-mono-server2/2.10.0.0__0738eb9f132ed756/mod-mono-server2.exe'. Mono-INFO: Assembly Loader probing location: '/usr/lib64/mod-mono-server2.exe'. Handling exception type FileNotFoundException
    Bingo! It's looking for an assembly called "mod-mono-server2"
    But wait a minute! That's the assembly I'm running!

    And no - I have no idea why it can't find it when I've actually given it the path to look in!
    But since I can see where it is looking a quick symbolic link solves my issue:

    ln -s "/usr/local/lib/mono/2.0/mod-mono-server2.exe" /usr/lib64/
    This is probably a bad idea, there must be a good reason why it can't find it in the correct location.

    Now I'm getting a different error about non-blocking sockets.

    *sigh*

    Thursday, 16 August 2012

    Installing MONO on CENTOS

    I've spent literally hours tearing my hair out trying to get this sorted so I thought I would share what I have found here:

    Most of the search results on google are out of date and/or contain links which no longer work.

    I also tried compiling everything from source but ran into trouble here too.

    Eventually after much headscratching I found a solution :)

    Take a look at this page: http://fedoraproject.org/wiki/EPEL

    Ignore that this is a fedora project as it works just fine on centos.

    Part way down the page under the "How can I use these extra packages?" heading are a couple of links. Choose the one that is appropriate to your version of centos. e.g. I am using centos 6 so I used the "newest version of 'epel-release' for EL6" link.

    Download the rpm file and install it.
    You should probably verify the rpm before installing, but I couldn't work out how and I was loosing patience by this point!

    rpm -i epel-release-*.noarch.rpm
    Now you should be able to search for and install the mono packages (and others) using yum.

    e.g.
    yum install mono-core mono-data mono-devel mono-extras mono-web mono-winforms monodoc mono-web-devel
    N.B. there is no monodevelop included in this lot.
    And I can't find anywhere to get it either :(

    Friday, 10 August 2012

    Fixing: Msg 15137, Level 16, State 1, Procedure sp_xp_cmdshell_proxy_account, Line 1 An error occurred during the execution of sp_xp_cmdshell_proxy_account. Possible reasons: the provided account was invalid or the '##xp_cmdshell_proxy_account##' credential could not be created. Error code: '0'.

    This one has been driving my crazy for ages. There are plenty of blog posts out there explaining how to fix this one by running SSMS as an administrator but in my case this did not help. The problem I was having was actually a really subtle one - getting the username right. In all other contexts I am able to use 'ip.add.re.ss\username' Not here so to fix the error in my case I had to use 'servername\username' Hopefully this post will save someone else a major headache!

    Wednesday, 23 May 2012

    Saving open files remotely (kate)

    So I neglected to save a file on my home pc before leaving. Then at work I wanted to continue editing the same file. My home desktop was not configured for VNC so what to do? I investiagted attaching a vnc server to a running desktop, without access to that desktop but quickly came to the conclusion that it would just be a massive security flaw and hence not allowed. I was just about to give up when I remembered dbus With an ssh session open I was able to tell the instance of kate running on the home pc to save the file without ever seeing the desktop. And it's actually really easy! First you need to get the right display - for 99% of cases that will be display 0 export DISPLAY=:0 now you need to find the running instance of kate qdbus | grep kate hopefully there is only one Now identify the correct document (N.B. replace the number below with the once found from the previous statement) qdbus org.kde.kate-28399 | grep Document If there is only one document open then its easy, otherwise you need to go through each document and see if its the right one. Unfortunatly there doesn't appear to be a way to get the filename, so you'll have to inspect the content qdbus org.kde.kate-28399 /Kate/Document/1 org.kde.KTextEditor.Document.text Repeat this for all the documents until you find the one you want then issue: qdbus org.kde.kate-28399 /Kate/Document/13 org.kde.KTextEditor.Document.save If you get the response true then the document is now saved! I did investigate whether this same method could be used to attach a vnc server using krfb but (thankfully) it can't This method almost certainly can be adapted to other applications and editors though - as long as they support dbus

    Thursday, 26 April 2012

    Why I hate Imports/using

    I'm sure other people must find this annoying, not just me.
    And even Microsoft do it on MSDN.

    What am I on about?

    Posting code that requires importing a namespace - without specifying what namespace(s) need to be imported.

    Imagine the following code:

    Stream stMyStream = new FileStream("SomeFile.txt", FileMode.Open);

    Now I know, and you know, that Stream exists within System.IO
    but without a using directive at the top of your file, the compiler doesn't know this and it gives you a whole bunch of red squiggly lines.

    In the above case it is obvious which namespace you need, but that isn't always so, and you can be left scratchign your head for ages trying to track down which namespace you are missing.

    So please people: when you post code example - either use a fully qualified type name, or specify the appropriate namespaces to be used.
    e.g.

    System.IO.Stream stMyStream = new System.IO.FileStream("SomeFile.txt", System.IO.FileMode.Open);

    Just a note to anyone who doesn't know:
    the C# directive
    using System.IO;
    is equivalant to the VB statement
    Imports System.IO


    To add further confusion both VB and C# have a using statement with is to do with scoping and disposing of variables.


    I use both C# and VB so I tend to interchange terms quite a lot!


    </Rant>