Powered by Mono

A utility to help porting Windows .NET applications to Mono/Unix

by Marek Habersack 21. December 2009 13:43

Those of you who work or worked on porting Windows applications (any kind of them - ASP.NET, WinForms, console) might have come across a problem with case-sensitivity of file names. Windows file systems are either case-unaware and case-insensitive (the FAT family) or case-aware but case-insensitive (NTFS). In both cases applications performing I/O don't need to worry about using proper file name casing, as the underlying operating system will ignore the case and open the file without problems. This is not the case on Unix operating systems which have file systems that are both case-aware and case-sensitive.

Mono has had a solution for this problem for a long time - it's called IOMAP. It's a runtime mechanism which, if enabled by exporting the MONO_IOMAP variable, will translate not only file name case to the correct form used on disk, but will also strip DOS drive designators from the paths and translate DOS path separator character to the Unix one, thus making the application work seamlessly. The convenience comes at a price, of course, as translating the file names involves performing a lot of disk I/O to discover the real file name. So if your application is supposed to be ran on Unix most (if not all) of the time then the better to dive into the source and make sure that file names on disk are consistent with their use in the source code.

With small applications the operation is usually straight-forward and takes little time. With bigger codebases, however, it might pose a problem, especially if strings used to access files are constructed in different places of the application as opposed to being just plain literals.

For that reason I have recently created a Mono profiler module (it is somewhat of a misnomer, as the utility has little to do with code profiling) which aims at helping the developer/porter to find places in code which call .NET I/O routines passing them misformed file/directory paths as well as identify places where those strings are constructed. The former part is very simple and it merely prints to the console a stack trace leading to the I/O routine call site every time file/directory name mapping is performed. The latter part, however, is a bit more problematic as it has to deal with two separate moments in string's life - its creation and its actual use. The code uses the Mono profiler API to monitor string allocations storing all the strings created in hash tables as well as remembering the stack frame which leads to the string creation site. The code which does that is pretty fast, so it doesn't impact your application's performance too much even though it collects and stores a large amount of data (mostly pointers and some strings, though). When file name mapping is performed during file I/O, the profiler code looks up the string address and retrieves the stack trace to store it for later use. When the application exits a summary report is printed to the console which includes some statistics on the string, its original (requested) and target (mapped) form as well as a location (if it was possible to determine it) where the string was created. The location is determined using simple heuristics, so it might sometimes point to a location which is near the place where the string was created. The heuristics code walks the stack frames looking for first frame which is in application code - that is it ignores the well-known class library assemblies shipped with Mono (corlib, System*, some Mono* assemblies etc) and all the assemblies installed in the GAC. The first frame which doesn't belong in either of the above is considered to be user's code and is reported to be the string allocation location. If the stack trace doesn't contain any such frames, full trace is recorded and shown in the summary.

To take advantage of this utility you will need Mono from trunk (or 2.8 when it is released). It is advisable to compile your application with full debugging information, so that source files and line numbers can be reported (otherwise only Namespace.Class.Method will be printed). To enable the utility, make sure the MONO_IOMAP environment variable is present and set to 'all', 'case' or 'drive' and execute your application as follows:

   mono --debug --profile=iomap your_application.exe
If you're porting an ASP.NET application, use the following command line:
   MONO_OPTIONS="--debug --profile=iomap" xsp2
Below you can see output from a sample application showing the utility's behavior:

$ MONO_IOMAP=all mono --profile=iomap --debug iomap-report-sample.exe 
Running test: Mismatched file name case.
-=-=-=-=-=-=- MONO_IOMAP REPORT -=-=-=-=-=-=-
 - Requested file path: 'files/Test2.txt'
 -     Found file path: 'Files/test2.txt'

-= Stack Trace =-
   at System.Environment.get_StackTrace() in /usr/src/tmp/mono/mcs/class/corlib/System/Environment.cs:line 183
   at System.IO.MonoIO.GetFileAttributes(System.String , MonoIOError ByRef )
   at System.IO.MonoIO.ExistsDirectory(System.String path, MonoIOError ByRef error) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/MonoIO.cs:line 252
   at System.IO.Directory.Exists(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/Directory.cs:line 201
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/FileStream.cs:line 246
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.OpenRead(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 331
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding, Boolean detectEncodingFromByteOrderMarks, Int32 bufferSize) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/StreamReader.cs:line 171
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding)
   at System.IO.File.ReadAllText(System.String path, System.Text.Encoding encoding) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 542
   at System.IO.File.ReadAllText(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 537
   at app.MismatchedFileNames() in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 45
   at app.RunTest(System.String banner, System.Action test) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 24
   at app.Main(System.String[] args) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 39

-=-=-=-=-=-=- MONO_IOMAP REPORT -=-=-=-=-=-=-
 - Requested file path: '/home/grendel/Projects/work/iomap-report-sample/files'
 -     Found file path: '/home/grendel/Projects/work/iomap-report-sample/Files'

-= Stack Trace =-
   at System.Environment.get_StackTrace() in /usr/src/tmp/mono/mcs/class/corlib/System/Environment.cs:line 183
   at System.IO.MonoIO.GetFileAttributes(System.String , MonoIOError ByRef )
   at System.IO.MonoIO.ExistsDirectory(System.String path, MonoIOError ByRef error) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/MonoIO.cs:line 252
   at System.IO.Directory.Exists(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/Directory.cs:line 201
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/FileStream.cs:line 270
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.OpenRead(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 331
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding, Boolean detectEncodingFromByteOrderMarks, Int32 bufferSize) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/StreamReader.cs:line 171
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding)
   at System.IO.File.ReadAllText(System.String path, System.Text.Encoding encoding) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 542
   at System.IO.File.ReadAllText(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 537
   at app.MismatchedFileNames() in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 45
   at app.RunTest(System.String banner, System.Action test) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 24
   at app.Main(System.String[] args) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 39

File 1: Hello, world - test2.txt here


-=-=-=-=-=-=- MONO_IOMAP REPORT -=-=-=-=-=-=-
 - Requested file path: 'files/test.txt'
 -     Found file path: 'Files/TesT.txt'

-= Stack Trace =-
   at System.Environment.get_StackTrace() in /usr/src/tmp/mono/mcs/class/corlib/System/Environment.cs:line 183
   at System.IO.MonoIO.GetFileAttributes(System.String , MonoIOError ByRef )
   at System.IO.MonoIO.ExistsDirectory(System.String path, MonoIOError ByRef error) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/MonoIO.cs:line 252
   at System.IO.Directory.Exists(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/Directory.cs:line 201
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/FileStream.cs:line 246
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.OpenRead(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 331
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding, Boolean detectEncodingFromByteOrderMarks, Int32 bufferSize) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/StreamReader.cs:line 171
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding)
   at System.IO.File.ReadAllText(System.String path, System.Text.Encoding encoding) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 542
   at System.IO.File.ReadAllText(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 537
   at app.MismatchedFileNames() in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 46
   at app.RunTest(System.String banner, System.Action test) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 24
   at app.Main(System.String[] args) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 39

File 2: Hello, world - TesT here


Running test: Access from an assembly.
Reading a file.
-=-=-=-=-=-=- MONO_IOMAP REPORT -=-=-=-=-=-=-
 - Requested file path: 'fileS/tESt.txt'
 -     Found file path: 'Files/TesT.txt'

-= Stack Trace =-
   at System.Environment.get_StackTrace() in /usr/src/tmp/mono/mcs/class/corlib/System/Environment.cs:line 183
   at System.IO.MonoIO.GetFileAttributes(System.String , MonoIOError ByRef )
   at System.IO.MonoIO.ExistsDirectory(System.String path, MonoIOError ByRef error) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/MonoIO.cs:line 252
   at System.IO.Directory.Exists(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/Directory.cs:line 201
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/FileStream.cs:line 246
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.OpenRead(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 331
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding, Boolean detectEncodingFromByteOrderMarks, Int32 bufferSize) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/StreamReader.cs:line 171
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding)
   at System.IO.File.ReadAllText(System.String path, System.Text.Encoding encoding) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 542
   at System.IO.File.ReadAllText(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 537
   at LibClass.ReadFile(System.String dir, System.String fileName) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-lib.cs:line 12
   at app.AccessFromAssembly() in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 51
   at app.RunTest(System.String banner, System.Action test) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 24
   at app.Main(System.String[] args) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 40

-=-=-=-=-=-=- MONO_IOMAP REPORT -=-=-=-=-=-=-
 - Requested file path: '/home/grendel/Projects/work/iomap-report-sample/fileS'
 -     Found file path: '/home/grendel/Projects/work/iomap-report-sample/Files'

-= Stack Trace =-
   at System.Environment.get_StackTrace() in /usr/src/tmp/mono/mcs/class/corlib/System/Environment.cs:line 183
   at System.IO.MonoIO.GetFileAttributes(System.String , MonoIOError ByRef )
   at System.IO.MonoIO.ExistsDirectory(System.String path, MonoIOError ByRef error) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/MonoIO.cs:line 252
   at System.IO.Directory.Exists(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/Directory.cs:line 201
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, Boolean anonymous, FileOptions options) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/FileStream.cs:line 270
   at System.IO.FileStream..ctor(System.String path, FileMode mode, FileAccess access, FileShare share)
   at System.IO.File.OpenRead(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 331
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding, Boolean detectEncodingFromByteOrderMarks, Int32 bufferSize) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/StreamReader.cs:line 171
   at System.IO.StreamReader..ctor(System.String path, System.Text.Encoding encoding)
   at System.IO.File.ReadAllText(System.String path, System.Text.Encoding encoding) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 542
   at System.IO.File.ReadAllText(System.String path) in /usr/src/tmp/mono/mcs/class/corlib/System.IO/File.cs:line 537
   at LibClass.ReadFile(System.String dir, System.String fileName) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-lib.cs:line 12
   at app.AccessFromAssembly() in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 51
   at app.RunTest(System.String banner, System.Action test) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 24
   at app.Main(System.String[] args) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:line 40

Reading file: Hello, world - TesT here



-=-=-=-=-=-=-= MONO_IOMAP Stats -=-=-=-=-=-=-=
    Count: 2
Requested: /home/grendel/Projects/work/iomap-report-sample/fileS
   Actual: /home/grendel/Projects/work/iomap-report-sample/Files
Locations:
        LibClass:ReadFile (string,string) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-lib.cs:12

    Count: 7
Requested: fileS/tESt.txt
   Actual: Files/TesT.txt
Locations:
        LibClass:ReadFile (string,string) in /home/grendel/Projects/work/iomap-report-sample/iomap-report-lib.cs:12

    Count: 4
Requested: /home/grendel/Projects/work/iomap-report-sample/files
   Actual: /home/grendel/Projects/work/iomap-report-sample/Files
Locations:
        app:MismatchedFileNames () in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:45
        app:MismatchedFileNames () in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:45

    Count: 7
Requested: files/Test2.txt
   Actual: Files/test2.txt
Locations:
        app:MismatchedFileNames () in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:45

    Count: 7
Requested: files/test.txt
   Actual: Files/TesT.txt
Locations:
        app:MismatchedFileNames () in /home/grendel/Projects/work/iomap-report-sample/iomap-report-sample.cs:46

Tags:

Mono | ASP.NET | Tip

The importance of micro optimizations

by Marek Habersack 7. October 2009 12:54

Many years ago when coding in C/C++ or assembler I learned one important rule - DO micro-optimize your code. Unlike macro scale optimization, which should be done after your code is generally working and feature complete, micro optimization should be applied as you go. Why? Because sticking to a few simple rules (which don't affect the overall way in which the code works) as you code is easier than having to fish for the small inefficiencies in your source later on. Some of the optimizations can be performed by the compiler (like taking a constant expression out of loop) but some still depend on the coder doing the Right Thing™.

It seems that, a month ago, I proved that I had forgotten the rules I learned and applied in the past by committing code which affected Mono ASP.NET's performance in a really major way. I'm posting this blog entry as a memento to myself and to others who, like me, might have forgotten that such rules still apply in the 21st century despite all our smart compilers, sophisticated virtual machines and runtimes.

The committed code did (eventually) something like this:

void Method (string s)
{
string s2 = GetSomeString ()

CallMethod (s + '@' + s2);
}

The CallMethod line caused (as we discovered yesterday) a drammatic decrease in MojoPortal's performance - its home page would open on my computer, using local connection in 2.56s (yes, seconds) on average. Gonzalo went hunting for the issue and this morning I read a mail from him pin-pointing (after lots and lots of time wasted on it) the performance loss to that very line. I fixed the issue in our code and suddenly the load time for the page went down to ~107ms (yes, milliseconds)! One might ask whether it was because concatenating two strings and a char is so inefficient? Yes, it is inefficient (as the operation has to convert a char to a string, then allocate and concatenate two more strings) but not enough to justify such performance loss. The key here is the frequency at which the code is being called - GetSection gets called hundreds (if not thousands) of times during one request, and the operation's summed up time contributes to the peformance loss.

This is exactly what I described at the beginning of this post - a micro optimization, a right thing to do in the right place. It can be compared to adding two integer or float variables whose values don't change in a loop which takes thousands of iterations and is invoked by some code very often. The addition is not very expensive if your code performs it once in a while, but in the scenario when it repeatedly calls the code, you will see major loss of performance. The solution is to take the operation outside of the loop. This is precisely what happened here.

So, even in this day and age - ladies and gentlemen, do micro-optimize and pay attention to where your "slightly inefficient" code lives :)

Update:

As Jon suggested below, I should have suggested possible solutions to the issue:

  • CallMethod could take 3 arguments instead of one (depends on what the method does, of course) - that was the solution chosen in this instance to fix the issue as the string concatenation wasn't at all necessary.
  • Using "@" instead of '@' would avoid costly (culture-sensitive) conversion of a char to string

Update:

I'm officially an idiot. The real cause of the slowness was that we've been bypassing the cache because of different key being generated to query cache and another to insert into it. My only excuse - lack of sleep :P (I know, a poor one :D). The post's main point still stands, though - just ignore the rambling about string concatenation. Now, where's my brown paper bag...

Tags:

General | Mono | ASP.NET

Umbraco on Mono

by Marek Habersack 23. July 2009 17:34

This week I've spent some time trying to fix bugs preventing Umbraco from running unmodified on Mono. Unfortunately, some modifications to Umbraco source code (diff here) were necessary due to the inherent Windows-centric coding in parts of the CMS. The diff above takes care of just one part of the cross-platform issues in Umbraco, namely it replaces hard-coded use of the \ directory separator character with the, correct, Path.DirectorySeparatorChar. For some reason MONO_IOMAP mechanism didn't take care of the problem, hence the workaround (I'm going to fix the IOMAP issue after I'm back from vacation mid-August). The other cross-platform issue is (very common in Windows .NET applications) inconsistent use of file naming case which MONO_IOMAP is dealing with nicely.

Making Umbraco run on Mono required some fixes to our ASP.NET parser (revisions 138474, 138520, 138521 and 138592 in trunk, for those interested. Backported to both 2.4 and 2.4.2 branches) as well as making AppDomain not reload when Umbraco writes to the Web.config file. All of the fixes willbe part of the upcoming 2.4.2.3 release. There are however some issues not fixed yet, but with workarounds:

Web.config modifications during setup
Generally it works fine - application isn't restarted, data is written to the config file, but for some reason Umbraco installer caches data from before database access credentials change. So if, for instance, you were presented with MS-SQL setup box and switched to MySQL, the connection and/or database creation will fail trying to use MS-SQL code. This is a bug in Mono which I'm going to investigate in August. Workaround for this issue is to stop the app after selecting the new database provider and credentials and just restart the installation process - everything will work fine from that moment on.
VistaDB not working
VistaDB comes without source, so I wasn't able to investigate what the issue was. The symptom is that when trying to connect to or populate VistaDB database you will get either a ThreadAbortException, a TypeInitializationException or a JIT error about invalid IL in the VistaDB assembly. This is likely a runtime issue - to be investigated. You can use either SQL Server or MySQL providers instead (I used MySQL for testing).
Intermittent exceptions
Sometimes visiting certain pages in the admin interface causes an exception to be thrown (usually NullReferenceException) but I never succeeded in reproducing the exception(s) by repeating the actions. To be investigated.
TinyDLL.dll is a native library
Since it is native, our compiler cannot figure out what to do with it and throws an exception. Simply remove the dll from there - it shouldn't be there anyway.
So, this is the current status of Umbraco on Mono - please play with it and report any bugs you might find, giving exact directions how to reproduce the issue. I will take a look at them once I'm back from vacations.

Since not everybody might want to or know how to compile and set up Umbraco from sources, I have precompiled the package with the diff applied and repackaged the 4.0.2.1 distribution with the bin/ assemblies replaced by their "Mono versions" (note that the ONLY difference in the assemblies is that the patched ones are cross-platform). Here are the download locations:

Tags:

Mono | ASP.NET

Mono ASP.NET MVC saga update

by Marek Habersack 17. July 2009 01:18

As many of you are probably well aware, we failed to ship the recently open-sourced Microsoft ASP.NET MVC with Mono 2.4.2. The release was soon followed by an update, 2.4.2.1, which included the source of MVC but, unfortunately, the story doesn't end here.

Sometime during MVC integration process, we discovered our C# compiler had a bug which made certain parts of MVC behave incorrectly. The bug was quickly fixed in trunk, but it was determined that backporting it to the 2.4 branches was not an option and since the issue was easily worked around, we decided to just put the modified MVC code in our repositories. Unfortunately, due to my mistake, the workaround didn't actually make it in neither 2.4.2.1 or 2.4.2.2 releases. I have just committed the 2-line fix (revisions r138087 and r138088) to the 2.4 branches and we should expect to see 2.4.2.3 released soon. My apologies for this omission to everybody who expected to see the bug fixed in 2.4.2.2.

If your MVC application throws an exception similar to this one:

System.ArgumentNullException: Argument cannot be null.
Parameter name: type
  at System.ComponentModel.TypeDescriptor.GetConverter (System.Type type) [0x000ab] in /usr/src/tmp/mono-branch/mcs/class/System/System.ComponentModel/TypeDescriptor.cs:437 
  at System.Web.Mvc.DefaultModelBinder.BindModel (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ModelBindingContext bindingContext) [0x000ba] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/DefaultModelBinder.cs:186 
  at System.Web.Mvc.ControllerActionInvoker.GetParameterValue (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ParameterDescriptor parameterDescriptor) [0x00090] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/ControllerActionInvoker.cs:119 
  at System.Web.Mvc.ControllerActionInvoker.GetParameterValues (System.Web.Mvc.ControllerContext controllerContext, System.Web.Mvc.ActionDescriptor actionDescriptor) [0x00021] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/ControllerActionInvoker.cs:128 
  at System.Web.Mvc.ControllerActionInvoker.InvokeAction (System.Web.Mvc.ControllerContext controllerContext, System.String actionName) [0x00099] in /usr/src/tmp/mono-branch/mcs/class/System.Web.Mvc/System.Web.Mvc/ControllerActionInvoker.cs:162 
then you are hit by the bug. At this point you have three choices:
  • Download the fixed binary from here, unpack it and replace the copy found in your 2.4 GAC either manually or by issuing the following command as user who has GAC folder write rights:
    gacutil -i System.Web.Mvc.dll
  • Take System.Web.Mvc sources from the Mono 2.4 (or 2.4.2) branch, compile it (with 2.4+ compiler)
  • Use System.Web.Mvc.dll from Microsoft .NET distribution - it will work just fine with Mono

ASP.NET MVC sources in the Mono 2.4 and 2.4.2 branches are identical to the upstream source except for the workaround. Trunk version of ASP.NET MVC is identical to upstream.

Tags:

Mono | ASP.NET

Mono with CGI on shared hosts

by Marek Habersack 1. June 2009 23:32

Some of you might want to run Mono on a shared (virtual) Linux host but your ISP/hosting company doesn't let you install mod_mono or Apache FastCGI modules. Kornél Pál has just contributed a piece of documentation which explains how to host Mono applications on CGI-only boxes without incurring the high CGI cost.

Tags:

Mono | ASP.NET | mod_mono

System.Web.Routing URL matching and generation

by Marek Habersack 25. May 2009 19:50

Recently .NET got a new feature - System.Web.Routing - which is a general purpose routing engine used at this point (within the realms of .NET class library) by System.Web.Mvc and System.Web.DynamicData namespaces. Most of System.Web.Routing is pretty straight-forward and easy to understand, but there's one pretty poorly documented feature (albeit a very important one, not to say crucial) - the route URLs.

During the past week was fixing several bugs in the Mono implementation of System.Web.Routing engine, all of which were related to route URL parsing and virtual path generation. At the beginning of the week I thought the task would be quick and easy, but it turned out that documentation of some aspects of the subject is few and far between and, in effect, correct implementation of the support took me the whole week. Since the feature is important for both the implementor of the namespace as well as for the end user, this blog entry attempts to collect and explain all the details of the subject.

URLs are used for two purposes in System.Web.Routing - for checking if a request matches a route (in this instance the route URL is considered to be specified using a simple pattern DSL) and for generating virtual paths (in this instance the route URL is considered to be a replacement pattern for the virtual path).

Note: all of the information below has been acquired through tests and whatever little documentation is available on the subject, I saw no source code for .NET's version (we never do that at Mono, as you probably know) so there might be errors and mistakes in the description. If you find one, please let me know (or better yet - create a simple test case which demonstrates the failure and file a bug for Mono's Sys.Web component. If you're interested in the tests we have, feel free to take a look

From the two, route matching is better documented, but let me outline the rules which the matching URLs follow:

  • URL must not start with either ~ or / characters
  • URL must not contain the ? character.
  • Each URL is built from zero or more segments separated with the / character
  • Each segment may contain any number of sections enclosed in the { and } characters.
  • Within one segment, no two sections can be adjacent, they must be separated with literal string of characters.
  • No section name must be empty. Empty in this case is understood as "not containing any characters" - you can have a section name consisting only of whitespace (including tabs and newlines).
  • Sections are parsed using hungry matching - that is, they will consume all the text from their starting point all the way to the start of the last ocurrence of the following literal or to the end of input, whichever comes first.
  • A segment may contain a catch-all section which is marked by putting * as the first character of the section name. Catch-all sections must come alone within the segment and they are valid only in the last segment of the URL

This is a pretty simple set of rules and is easy to implement (see PatternParser.cs in Mono repository)

 

Let's pass to the other part of the subject, virtual path generation. The theory seems simple here - take the original route URL and replace it with provided values. The simple recipe has embedded traps, however. Once you discover the rules, they're easy and the algorithm is clear, but discovering them is, as it turns out, not so easy today. Thanks to Atushi Enomoto's work on implementing our System.Web.Routing there already was basic support for the parsing/matching/generating operations which covered the most common cases. It didn't work correctly for the more obscure and less frequently encountered scenarios. There was very little meaningful information on the web I could find on those and, by far, the most valuable piece of information came from the "ASP.NET MVC 1.0" book (Chapter 4). All gathered together, however, it still didn't cover everything. What you can see below is a conglomerate of the above information with knowledge learned from tests.

There are several terms you need to be familiar with in order to understand what's going on:

Required values
Values which are found in the route URL (i.e. sections) which don't have defaults provided
Default values
Values provided by the programmer in the route's Default dictionary.
Ambient values
Values gathered by the routing engine from the current request. They are accessible via RequestContext.RouteData.Values
Constraints
Programmer can provide the routing engine with a set of per-route constraints. Constraints may be presented in two flavors - either strings, which are treated as regular expressions, or instances of the IRouteConstraint interface. Those values are consulted when generating the virtual path.
Overflow values
Those are the values provided explicitly in the call to Route.GetVirtualPath that are not in the route URL and also are not specified in the Defaults and Constraints dictionaries attached to the Route class. Such values are appended as query parameters to the generated virtual path.

Given the information above, the rules for virtual path generation are outlined below. This is where it becomes a bit entangled.

  • If there are any required parameters in the current route, a check is made whether all of them are present in the dictionary passed by user to the GetVirtualPath method.
    • If any are missing, there are several rules before we decide it's a mismatch:
      • If there are no defaults
      • or the defaults dictionary contains at least one value used in the route URL
      • and the user values do not contain any value used in the route URL,
      • then the code is allowed to check for the required value in the ambient values. If the value is not found there, we have a mismatch.
      • If the above four conditions are not met and the value is not found in the user values, we have a mismatch
    • If all of them are present, checks are performed to see whether
      • there are any default values which are not used in the route URL.
      • If any such entries are found, another check is made to make sure that any corresponding values found in the user-passed dictionary have exactly the same value as specified in the defaults. If the check fails, we have a mismatch.
      • Further on, the code looks whether there are any constraints defined for the route and, if yes, iterates over the dictionary to make sure that all of the constraints are met.
      • If there are no errors, we have a match and we can start generating the virtual path.

This completes the preliminary checks whether the route qualifies for virtual path generation in the context of the current request.

 

The actual virtual path code generation can begin now. Code generating the virtual path can trim the result if by skipping continuous block of values at the end of the URL if all of the values are defaults. After this process is done, code looks for the overflow values and appends them to the resulting virtual path as query parameters. Both names and values in the query are encoded using Uri.EscapeDataString.

This concludes the virtual path generation process. Hope somebody finds this description useful :)

Tags:

Mono | ASP.NET

Book meme, per instructions :)

by Marek Habersack 18. May 2009 19:50

Following a fellow hackerette's instructions, here's my meme:

"Computer science has a subdiscipline called Information Retrieval 
(IR for short) that focuses almost entirely on this problem."

And the original Andreia's instructions copied here:

  • Grab the nearest book.
  • Open it to page 56.
  • Find the fifth sentence.
  • Post the text of the sentence in your journal along with these instructions.
  • Don't dig for your favorite book, the cool book, or the intellectual one: pick the CLOSEST.

So, Andreia, what's the page number for next week? :)

Tags:

General | fun

Tip: Mono ASP.NET application burning CPU in idle state - FileSystemWatcher

by Marek Habersack 13. May 2009 15:40

Update: added sample code to detect the watcher in use, courtesy of Robert Jordan - thanks!

mod_mono is an Apache module for hosting ASP.NET applications. The module itself doesn't run any .NET code, instead it spawns a backend server (mod-mono-server.exe for ASP.NET 1.1 and mod-mono-server2.exe for ASP.NET 2.0) which is handed all the requests coming in from the client browser and sends back response generated by the application.

If you run Mono on a VPS server (e.g. Xen, OpenVZ) then you don't usually have any control over what Linux kernel version and with what capabilities you run. It may happen that the kernel lacks capabilities used by parts of the Mono runtime and Mono will have to fall back to other methods of doing the same task. One such part is the FileSystemWatcher class which is used to monitor changes to files/directories on disk so that the application can take any steps it deems necessary in reaction to file creation/deletion/modification events.

Mono's FileSystemWatcher does its best to perform its assigned task in various environments, under various operating systems. Part of the effort is selecting the actual filesystem monitoring backend best for the runtime environment. Under Unix the supported backends are as follow:

  • FAM
  • kevent (BSD*/MacOSX only)
  • gamin
  • inotify (Linux only)
  • Managed watcher
Out of those, assuming you run Linux, inotify is the preferred backend mechanism as it requires no polling effort on userland application part, instead the Linux kernel will notify the application (in our case the Mono runtime) whenever interesting events happen. However, it requires the Linux kernel to support the mechanism and, what's more important, for your VPS operator to actually include the support in the kernel your VPS runs on.

If your kernel doesn't support inotify, Mono will attempt to use FAM and Gamin which are userland daemons doing active filesystem polling but outside of the consumer application. The consumer application will use provided FAM/Gamin libraries to receive events and react to them. Performance of this setup is worse than inotify but not tragic.

Should Mono fail to detect inotify, FAM or Gamin support, it will fall back to the last resort option - the managed watcher. This watcher is implemented in managed code and uses a separate thread for filesystem monitoring, polling for changes on selected files/directories. As the application may (and in the case of ASP.NET sometimes does) watch directories recursively, it might be a very expensive situation requiring checking changes to a big set of files. Each change detection run requires checking whether a file/directory exists (in case of the Managed watcher those are two stat (2) calls) and then checking the file metadata for changes (size, modification times etc) and, possibly, generating an event. This happens approximately every 750ms and can generate substantial load on the server's CPU.

If you notice (using top or htop applications) that your copy of mod-mono-server burns several per-cent of CPU but is otherwise in the S (Sleeping) process state, chances are your application is using the managed watcher. You can confirm that by using htop which allows you to watch individual process threads - you will see two threads consuming nearly the same amount of CPU time and one of them waking up every ~750ms.

The cure for the itch is easy, if you can live without filesystem monitoring (that means your application will not auto-restart when you modify Web.config, files won't be recompiled if you modify a code-behind .cs or an .aspx, .ascx etc. files). Mono supports a MONO_MANAGED_WATCHER environment variable which can be set to value disable with the effect of definitely disabling filesystem monitoring (it will use a "dumb" implementation of the watcher backend which does nothing) and relieve your application of the filesystem polling chores described above.

You can set the environment variable for your Apache VirtualHost by using the following mod_mono directive:

MonoSetEnv [server_alias] MONO_MANAGED_WATCHER=disable

Sample program to detect which watcher backend is used:

using System;
using System.Reflection;
using System.IO;

class Program {

    public static void Main()
    {
        object watcher = new FileSystemWatcher()
            .GetType ()
            .GetField ("watcher", BindingFlags.NonPublic | BindingFlags.Static)
            .GetValue (null);
        
        Console.WriteLine (watcher != null
                   ? watcher.GetType ().FullName
                   : "unknown");
    }
}

Tags:

Mono | mod_mono | Tip

Mono + Linux + BlogEngine.NET

by Marek Habersack 13. May 2009 02:32

As you can see at the bottom of the page, this site is powered by the BlogEngine.NET open-source blogging software but, yes, it is running on Linux with Mono and Apache.

There have been just two issues with case-insensitivity in BlogEngine.NET source code, but otherwise the deployment went without any issues what-so-ever! This is a live proof for both Mono and Mono's ASP.NET maturity as well as the .NET's realized promise, thanks to Mono again, of enabling one to write and deliver cross-platform software.

Tags:

DotNet | Mono | ASP.NET

Back to blogging

by Marek Habersack 13. May 2009 02:17

So, it has been over a year since I last blogged. A lot has happened in the 15 months in Mono - we have released version 2.0 and continued new releases all the way to the latest 2.4 which came with lots of performance and stability improvements. ASP.NET in Mono now has support for almost all .NET 3.5 controls (except for the LinqDataSource which is not fully implemented, but it's on its way), we have System.Web.Routing, beginnings of System.Web.DynamicData (more on that in some later post) and integrated System.Web.Mvc after Microsoft released it under an opensource license. But you are all well aware of those events, so there's no point in continuing the list here.

As I feared, I wasn't the most active blogger, for various reasons. This time, though, I do hope to keep you updated on what's going on in ASP.NET in Mono and, perhaps, about other things I find interesting. So, check back from time to time and maybe I'll manage to put together a few words which make sense :)

Tags:

Mono | General

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen | Modified by Mooglegiant