C#: WPF / Console Hybrid Application

I am working on a WPF application which needs to also provide a command line interface running as a console application. This means that depending on how it is started, it should either work as a console application or start with a GUI.

In my case I already had a WPF application by the time this requirement came in. So I had to adapt my existing application to make it an hybrid application.

First I created a new class Program with a Main method:

using System;

namespace HybridApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
        }
    }
}

You might get an error saying that there are two Main methods, so you will have to select this new class as Startup object in the project properties.

Then you need to set the output type to “Console Application” in the project properties:

Output Type Console Application

This is required so that your application doesn’t automatically start with a GUI.

Now we’ll need either keep running as a Console application or start the WPF application depending on how the application was started.

First in order to be able to start the WPF application, you’ll need to add the STAThread attribute to your main method:

using System;

namespace HybridApp
{
    public class Program
    {
        [STAThread]
        public static void Main(string[] args)
        {
        }
    }
}

We’ll now assume that in order to start the UI, we’ll call the application with the -g argument. In order to start the GUI, all you need is to call the Main method of your WPF application:

using System;

namespace HybridApp
{
    public class Program
    {
        [STAThread]
        public static void Main(string[] args)
        {
			if (args.Length > 0 && args[0] == "-g") {
				// GUI mode
				App.Main();
			}
			else {
				// console mode
			}
        }
    }
}

Now, you’ll notice that when starting in GUI mode, you will still have a Console window displayed. This doesn’t look good. I did not find a way to completely get rid of it but at least managed to hide it before starting the UI so that you only see the console window for a very short time. In order to do this, you need to use GetConsoleWindow from kernel32.dll in order to get the handle of the console window and ShowWindow from user32.dll in order to hide it:

using System;
using System.Runtime.InteropServices;

namespace HybridApp
{
    public class Program
    {
        [DllImport("kernel32.dll")]
        private static extern IntPtr GetConsoleWindow();

        [DllImport("user32.dll")]
        private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

        [STAThread]
        public static void Main(string[] args)
        {
			if (args.Length > 0 && args[0] == "-g") {
				// GUI mode
				ShowWindow(GetConsoleWindow(), 0 /*SW_HIDE*/);
				App.Main();
			}
			else {
				// console mode
			}
        }
    }
}

Now all you need is to implement the logic for the console mode and you’re done !

AngularJS: binding HTML code with ng-bind-html and $sce.trustAsHtml

I am working on a project in which based on some user interactions I am calling a service on the server and receive some pieces of HTML code which I need to display in the UI.

Of course I could just inject them in the DOM but to keep it clean I wanted to just store the HTML in the scope and bind it to an HTML tag. Basically doing something like this:

<!DOCTYPE html>
<html>

<head>
  <script src="https://code.angularjs.org/1.4.0-beta.6/angular.js" data-semver="1.4.0-beta.6" data-require="angular.js@*"></script>
  <link href="style.css" rel="stylesheet" />
  <script src="script.js"></script>
</head>

<body ng-app="myApp" ng-controller="myCtrl">
  <div ng-bind-html="myHtmlVar"></div>
</body>

</html>

The JavaScript code being:

var app = angular.module('myApp', []);

app.controller('myCtrl', function($scope) {
    $scope.myHtmlVar= "<a href='http://benohead.com'>benohead.com</a>";
});

This will unfortunately not work and you will get the following error message in the JavaScript console:

Error: [$sce:unsafe] Attempting to use an unsafe value in a safe context.

This basically means that the value we provided to ng-bind-html is not inherently safe and is also not marked as safe. So we need to mark it as trustworthy. This can be done by using $sce.trustAsHtml. An easy fix to the problem is to use the following:

app.controller('myCtrl', function($scope, $sce) {
    $scope.myHtmlVar= $sce.trustAsHtml("<a href='http://benohead.com'>benohead.com</a>");
});

If you do not want to add the calls to trustAsHtml everywhere in your code, you can also introduce a function in your scope and reference it in your page:

app.controller('myCtrl', function($scope, $sce) {
    $scope.myHtmlVar= "<a href='http://benohead.com'>benohead.com</a>";
    
    $scope.trustAsHtml = function(html) {
      return $sce.trustAsHtml(html);
    }
});

This function can then be called in the ng-bind-html attribute:

<div ng-bind-html="trustAsHtml(myHtmlVar)"></div>

Another way to do it is to define a filter which works in exactly the same way as the trustAsHtml function above:

app.filter('trustAsHtml', function($sce) {
  return function(html) {
    return $sce.trustAsHtml(html);
  };
});

app.controller('myCtrl', function($scope, $sce) {
  $scope.myHtmlVar = "<a href='http://benohead.com'>benohead.com</a>";
});

And use it like this:

<div ng-bind-html="myHtmlVar | trustAsHtml"></div>

Also note that if you want to set the HTML to the scope variable and then perform some DOM manipulation on this HTML code, you’ll run into a problem, because as long as your code is running, the data binding won’t be reflected until you return. Here is such an example:

app.controller('myCtrl', function($scope, $sce, $http) {
	$scope.myHtmlVar = "<a href='http://benohead.com'>benohead.com</a>";

	$scope.getHtmlCode = function (serviceParameter) {
		var responsePromise = $http.post("my-service-url", JSON.stringify(serviceParameter));
		responsePromise.success(function (data) {
			$scope.myHtmlVar = data;
			$scope.manipulateDom();
		});
	};
	
	$scope.manipulateDom = function () {
		//do some DOM manipulation on the retrieved HTML code...
	});
});

In order to execute some additional code, you’ll have to wrap it using $timeout:

app.controller('myCtrl', function($scope, $sce, $http, $timeout) {
	$scope.myHtmlVar = "<a href='http://benohead.com'>benohead.com</a>";

	$scope.getHtmlCode = function (serviceParameter) {
		var responsePromise = $http.post("my-service-url", JSON.stringify(serviceParameter));
		responsePromise.success(function (data) {
			$scope.myHtmlVar = data;
			$timeout(function() {
				$scope.manipulateDom();
			});			
		});
	};
	
	$scope.manipulateDom = function () {
		//do some DOM manipulation on the retrieved HTML code...
	});
});

 

Getting a file extension with JavaScript

For a current project, I needed to get the extension of a file being given it’s full path. In C#, you’d do something like this:

var extension = Path.GetExtension(path);

Unfortunately, we have no such possibility in JavaScript. Fortunately, writing a function returning the extension in JavaScript is not that difficult.

First you need to get the file name from the full path (basically getting rid of all parent directories in the path). In order to do this you have to also consider that you could either have a slash or a backslash as separator in the path.

The file name is basically, the last part of the path when you split it with the file path separator. This means you just need to use a split and a pop:

var filename = path.split('/').pop(); // when using slash as a file path separator

var filename = path.split('\\').pop(); // when using backslash as a file path separator

In order to handle both, you just need to do it twice:

var filename = path.split('\\').pop().split('/').pop();

It doesn’t really matter in which order you do it. If you use slashes, the first split/pop combination will not do anything. If you use backslashes, the second one will not do anything.

Getting the file name without path information is important because we’ll be searching for the extension based on dots in the path. If one of the parent directories does contain a dot, it will return a wrong results, so it’s safer to first get the file name without path information and then get the file extension.

Now we have the file name (without full path information), we need to extract the extension.

Let’s first look at which results we expect in different cases:

  • if the input name is an empty string, the returned extension should be an empty string as well
  • if the input name is does not contain any dots, the returned extension should be an empty string
  • if the input name contains only one dot and it’s the first character of the file name (e.g. .htpasswd), the returned extension should be an empty string
  • if the input name starts with a dot but contains another dot (e.g. .htpasswd.sav), the returned extension should be the part after the last dot
  • in general if the name contains any dots which are not at the beginning of the file, the returned extension should be the part after the last dot

You can get the index of the last dot in the file name using:

filename.lastIndexOf(".")

This will return -1 if no dot is present in the name. In this case, there is no extension and we can return an empty string:

var lastIndex = filename.lastIndexOf(".");
if (lastIndex < 0) return "";

Actually, even if it returns 0, it means that the file name starts with a dot and contains no further dots. In this case we also want to return an empty string:

var lastIndex = filename.lastIndexOf(".");
if (lastIndex < 1) return "";

In other cases (i.e. when we have at least one dot which is not at the beginning of the name), we want to exclude everything until the dot (including it) and return the rest:

var lastIndex = filename.lastIndexOf(".");
if (lastIndex < 1) return "";
return filename.substr(lastIndex + 1);

So the complete function to get the file extension from an arbitrary file path is:

function getFilePathExtension(path) {
	var filename = path.split('\\').pop().split('/').pop();
	var lastIndex = filename.lastIndexOf(".");
	if (lastIndex < 1) return "";
	return filename.substr(lastIndex + 1);
}

Now let’s test it a little bit and check that we do get the expected results:

console.log(getFilePathExtension(""));
console.log(getFilePathExtension("name"));
console.log(getFilePathExtension("name.ext"));
console.log(getFilePathExtension(".name"));
console.log(getFilePathExtension(".name.ext"));
console.log(getFilePathExtension("name.with.a.few.dots.ext"));
console.log(getFilePathExtension("/path/with/.a/few/dots"));
console.log(getFilePathExtension("/path/with/.a/few/.dots"));
console.log(getFilePathExtension("/path/with/.a/few/.dots/name.with.a.few.dots.ext"));

As expected the 4 tests where the name ends with “.ext” will return ext. The others will return an empty string.

You can make the function above even a little bit compacter (but less readable) by using Infinity as index in case the index returned is 0 or less. This is done by using:

  1. the Math.max function so that we get 0 whether lastIndexOf returns -1 or 0
  2. the fact that 0 is evaluated as false
  3. the fact that substr will return an empty string if the provided index is higher than the index of the last character
  4. the fact that Infinity + 1 is still higher than the index of the last character

So the compacter code looks like this:

function getFilePathExtension(path) {
	var filename = path.split('\\').pop().split('/').pop();
	return filename.substr(( Math.max(0, filename.lastIndexOf(".")) || Infinity) + 1);
}

 

AngularJS: Using Plupload in a dialog

Plupload is a very configurable JavaScript library you can use to allow your users to upload images. I’ve already used it in a few projects. I now wanted to use it in my current AngularJS project. Integrating Plupload in an AngularJS project is not especially difficult. There are already a few articles showing how this can be done. But it gets more difficult, when you try to integrate it into a dialog.

When initialized, Plupload puts an overlay above the browse button. This unfortunately only works if this element is displayed. If it is not displayed, the Plupload initialization will fail.

When working with dialogs, this means you need to execute the initialization of Plupload once the dialog is already displayed.

Let’s assume you have the use the following method to open a dialog using ngDialog:

$scope.uploadFile = function(){
	ngDialog.open({
		template: '/template/fileUploadDialog.html',
		controller: 'FileUploadController'
	});
};

and your controller looks like this:

app.controller('FileUploadController', function($scope){
	$scope.uploader = new plupload.Uploader({
		browse_button: 'browse',
		url: 'upload.php'
	});

	$scope.uploader.init();
});

Or if you are using angular-dialog-service:

$scope.uploadFile = function () {
	dialogs.create('/template/fileUploadDialog.html', 'FileUploadController', {}, {
		'backdrop': false,
		'size': 'sm',
		'copy': false,
		'windowClass': 'dialog-type-primary'
	});
};

and the same controller.

You will see that although you do not get any kind of error, nothing will happen when you click on the browse button. This is because no overlay was added by Plupload. If you put a breakpoint in the controller code, you will see that the dialog is not yet displayed when the code runs.

This is because Plupload has issues rendering inside elements which hidden at initialization time You need to “refresh” Plupload after the dialog has been displayed:

$scope.uploader.refresh();

Now we know the root cause of the problem, all we need to do is either call the refresh method after displaying the dialog or just initialize Plupload after displaying the dialog. The problem is that I never found out how to provide a callback to get triggered after the dialog has been displayed.

After googling for it with no result, I decided to do it with a hack (won’t win a design price with this one but it works). Assuming the dialog will never take more than 300 milliseconds to display and the user will need at least 300 milliseconds to find the browse button, you can just solve it by executing some code asynchronously using setTimeout or $timeout (since we work with AngularJS):

app.controller('FileUploadController', function($scope, $timeout){
	$scope.uploader = new plupload.Uploader({
		browse_button: 'browse',
		url: 'upload.php'
	});

	$timeout(function () {
		$scope.uploader.init();
	}, 300);
});

Now your browse button should be working.

Plupload: previewing images and getting Data URI

I am working on a project where users can be created and assigned an image. The natural choice to upload images is to use Plupload. Plupload is a well-known JavaScript library to upload images using a variaty of uploader runtimes. It provides an HTML4 and an HTML5 runtime but also a Flash and a Silverlight runtime. You can configure a list of runtimes. Plupload will check for you which one is available on the particular client.

The usual way to use Plupload is to let the user select files with a native dialog or using drag&drop. You then upload the file to some server side business logic and display the progress. In my case, I wanted something different. First I want to show the selected image to the user and second I want to store the image not on the server but in a model on the client (which contains other data and will eventually be stored somewhere).

In this article, I will describe how to display a preview of the image in a canvas and how to convert the image to a Data URI.

First you need to deploy Plupload on your web server and reference it’s JavaScript file e.g.:

<script src="vendor/plupload/plupload.full.min.js"></script>

Then you need to have some HTML code to define a button which will be clicked by the user to open the file browser and a tag to display the image preview:

<a href="" type="button" id="selectfiles">Select</a>
<div id="preview"></div>

Now you’ll have to write some JavaScript code to initialize Plupload:

var uploader = new plupload.Uploader({
	runtimes: 'html5,flash,silverlight',
	browse_button: 'selectfiles',
	multi_selection: false,
	url: "about:blank",
	flash_swf_url: 'vendor/plupload/Moxie.swf',
	silverlight_xap_url: 'vendor/plupload/Moxie.xap',
	filters: [{title: "Image files", extensions: "jpg,gif,png"}]
});

uploader.init();

We’ve defined that Plupload should use the selectfiles element to let the user open the file browser. If you use the Flash and/or Silverlight runtimes, you have to point Plupload to the URL of those SWF and XAP files. And we define that a user can only choose one file and that it needs to be a GIF, JPEG or PNG file. This all pretty standard.

The only special trick is that we use “about:blank” as URL for the upload. Actually, you can specify whatever you want. It just needs to be a non-empty string. Otherwise Plupload will not initialize successfully. But since we won’t use Plupload to actually upload the file, we do not care about the value of this option, as long as it is defined.

Then we’ll want to be notified when the user as selected a file. So we need to register for an event:

uploader.bind("FilesAdded", filesAdded);

function filesAdded (uploader, files) {}

Now we’ll implement the filesAdded function. It will get the selected file and will need to embed that image in canvas in our preview element. Luckily, Plupload provides us with an Image class which provides us with all we need to do this:

function filesAdded (uploader, files) {
	$.each(files, function () {
		var img = new mOxie.Image();
		img.onload = function () {
			$('#preview').empty();
			this.embed($('#preview').get(0), {
				width: 100,
				height: 100
			});
		};
		img.onembedded = function () {
			this.destroy();
		};
		img.onerror = function () {
			this.destroy();
		};
		img.load(this.getSource());
	});
};

Now once the user select a file, it will be displayed as a preview. Now once the user decides to save, we can use this preview to save the image (note that this works in my case because I only want to store a 100×100 image and do not need the original image):

var dataURI = $("#preview canvas")[0].toDataURL();

Now you can store the data URI however you want.

Additionally, if you have a previously stored data URI and want to display it in the preview area, you can use the following code:

$('#preview').empty();
var $canvas = $("<canvas></canvas>");
$('#preview').append($canvas);
var context = $canvas.get(0).getContext('2d');
$canvas.attr('width', '100');
$canvas.attr('height', '100');
$('#preview').append($canvas);
var image = new Image();
image.onload = function () {
	context.drawImage(image, 0, 0, 100, 100);
};
image.src = $scope.data.user.image;

It just creates a canvas, loads the data URI in an Image object and has it drawn in the canvas’ 2D context.

Drag&Drop with AngularJS

I am working on a project where I have a board containing different lanes (a kind of grid) on which you can place cards and move them according to their completion status. In order to implement the drag&drop functionality, I am using the angular-dragdrop module.

It is a wrapper for jQueryUI draggable/droppable components which makes it easy to implement drag and drop functionality in AngularJS.

In order to install the module, just run the following in your terminal:

bower install angular-dragdrop

Then in order to load it, you need to reference its JavaScript file, just after the reference to angular.js (and also a reference to jQuery and jQuery UI which are used by this module):

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jqueryui/1/jquery-ui.min.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-dragdrop/src/angular-dragdrop.min.js"></script>

And add a dependency in your main module e.g.:

angular.module('myapp', [ 'ngDragDrop' ]);

Now, you’re ready to define draggable elements and droppable areas.

First, let’s make the card elements draggable. My elements looked like this before the change:

<div class="card">
	...
</div>

You need to add three custom attributes to this element:

<div class="card"
	 data-drag="true"
	 data-jqyoui-options="{revert: revertCard,
							helper: 'clone',
							appendTo: 'body',
							zIndex: 350}"
	 jqyoui-draggable="{index: 0,
						placeholder:true,
						animate:true,
						onStart: 'startCallback'}">
	...
</div>

jqyoui-draggable contain options passed to hte angular-dragdrop module. data-jqyoui-options contains the options passed to jQuery UI Draggable.

I’ve just copied the index, placeholder and animate options from some example, so you’ll have to use Google to find more info on them.

revertCard and startCallBack are functions on my controller which are used to control the way the draggable element react.

The zIndex is set so that the element is on top of all other elements while dragged. In my first try, I hadn’t set the appendTo and helper options. Since my draggable elements where contained in a container on which the overflow property was set, I could not move it out of its container. That’s because the element being dragged still had the same parent as the original element.

So you need to attach the element being dragged to the body of the page so that it can be moved anywhere. Unfortunately, attachTo with the “body” value only works if the helper option is set to “clone” i.e. it will not detach the element from its current container and attach it to the body but needs to work with a clone of the element which can be attached to the body.

The drawback is that the original element is still displayed. This is fine if you are writing an editor with some toolbox and want to use drag&drop to create new instances of this element. But if you actually want to move the element, it just doesn’t look good. So you need to additionally hide the original element while its clone is being dragged. That’s what is done in the startCallback of my controller:

$scope.startCallback = function (event, ui) {
	var $draggable = $(event.target);
	ui.helper.width($draggable.width());
	ui.helper.height($draggable.height());
	$draggable.css('opacity', '0');
};

This callback does two things:

  1. It makes sure that the clone element doesn’t grow to take up the whole screen (because its size is not limited by its parent anymore).
  2. It hides it by setting its opacity to 0.

In one of my early tries, I had hidden the original element by using display:none. The problem is that all other elements in this container would then move because this element was not displayed anymore. But setting its opacity to 0, it works as an empty placeholder.

Later on, once the dragging is stopped, I’ll make it visible again if required (only if it is not dropped at a valid location).

Usually, you will find that most example will set the revert option to “invalid”. This means that if you drop the component outside of a valid drop area, the helper will just come back to the original location and disappear. The problem is that since we’ve hidden the original element, you will end up with no element displayed at all. Thankfully, revert can also take a function as value. This is our second callback:

$scope.revertCard = function (valid) {
	if (!valid) {
		var that = this;
		setTimeout(function () {
			$(that).css('opacity', 'inherit');
		}, 500);
	}
	return !valid;
};

Returning false will cause the helper to return to the original location. The default duration of the animation is 500 milliseconds. Before returning true of false, we check whether the drop location was valid and if yes, we make the original element visible again. If we set the opacity to “inherit” immediately, the original element would be visible while the helper is moving back to the original location. That’s why we make the CSS change with a timeout of 500 milliseconds so that the original element is made visible as soon as the revert animation is finished.

Now we have a working draggable element and just need to define droppable areas where we can drop it:

<div class="lane"
	 data-drop="true"
	 jqyoui-droppable="{multiple: true,
						onDrop: 'dropCallback'}"
	 data-jqyoui-options="{hoverClass: 'ui-state-active',
							tolerance: 'pointer'}">
	...
</div>

The hoverClass is just used so that we can display the droppable area below the mouse pointer differently.

The tolerance option is used so that the element is dropped on the area below the mouse pointer and not on the area below the corner of the dragged element. This is useful when the dragged element is larger than the droppable areas.

We also define an additional callback in the controller:

$scope.dropCallback = function (event, ui) {
	var $lane = $(event.target);
	var $card = ui.draggable;
	if ($card.scope().card.lane != $lane.scope().lane.id) {
		$card.scope().card.lane = $lane.scope().lane.id;
	}
	else {
		$card.css('opacity', 'inherit');
		return false;
	}
};

This callback does two things:

  1. It updates the model so that the card is displayed on the lane it was dropped on.
  2. It prevents dropping the card on the lane it was on before dragging.

The first part is pretty straightforward. The second one just involves checking the original lane ID and the lane ID on which the drop operation is being performed. If they do not match we allow the drop. Otherwise we return false which is similar to a revert. Unfortunately, returning false doesn’t cause the revert callback to be called. So I have to change the opacity of the original element before returning false.

You’ll also notice that I do not set a timeout of 500 milliseconds. This is because returning false in the drop callback doesn’t cause a revert animation to be executed. I haven’t yet found out how to do this. So the helper just immediately disappears and I have to make the original element visible immediately.

That’s it ! It was actually pretty easy. Since you can set any draggable and droppable options without limitation, having this angular module between you and jQuery UI doesn’t seem to introduce any additional limitation. And since all references to event handler all point to callback in your controller, everything stays clean.

Update: I’ve also come accross an issue with a button on the card. I’m using the angular dialog service to display a dialog when a button on the card is clicked (using ng-click to trigger a function in my controller). At first, nothing happened when clicking on the button. This is because the mouse click didn’t go to the button. The solution was to increase the z-index of the button to 500 i.e. higher than the 350 configured for the draggable card.

Update: I’ve created a Plunker to show a working example.

WP Prism Syntax Highlighter not working on archive pages

Prism is a is a lightweight and extensible syntax highlighter written in JavaScript and CSS by Lea Verou. It’s fast and the rendered code looks great.

WP Prism Syntax Highlighter is one of the WordPress plugins integrating the Prism syntax highlighter. It is relatively new and I didn’t see much updating done but it works well.

Until now, I’ve only found one real problem with this plugin: althought it does work fine on individual posts, it just doesn’t work on archive pages and on the home page. So basically it fails to work on all pages showing lists of posts. It’s not an issue if you only display excerpts on those pages. But if you display full posts on them, all you see is the standard appearance of <pre> tags. This is because Prism will not be loaded on such pages.

The problem is that the plugin tries to limit the pages on which it loads Prism to those pages which actually do display some code. It does it with the following code:

private function decide_load_prism() {
	if ( strstr( get_post()->post_content, '<code class="language-' ) !== false ) {
		$this->load_pjsh = true;
	}
}

It basically gets the content of the current post and checks whether a Prism code tag is there. Only then will it load prism. This ensures that you do not load Prism on pages where you don’t need it in the first place e.g. posts without code, disclaimers, about me pages…

The problem is that this piece of code actually only works on single posts, not on archive pages or on the home page. Anyway for such pages, you’d need to check whether the whole post is displayed, if yes go through the posts being displayed and check whether code is present there. That’s kind of complex and probably the reason why it was implemented this way.

So to solve this you could delete the “if” line and the corresponding closing bracket. But then you’d load Prism on all pages including the ones where you definitely do not need it. After loading on all pages displaying code no matter whether a single post or a list of posts, the second best solution would be to make sure that single post pages only load Prism if required and on archives / on the home page, Prism is always loaded.

To implement it, open wp-prism-syntax-highlighter.php (http://YOURSITE/wp-admin/plugin-editor.php?file=wp-prism-syntax-highlighter/wp-prism-syntax-highlighter.php&a=te), search for “decide_load_prism” and replace this function by this one:

private function decide_load_prism() {
	if ( !is_single() || strstr( get_post()->post_content, '<code class="language-' ) !== false ) {
		$this->load_pjsh = true;
	}
}

This is almost the same as the previous one. The only difference is that it also loads Prism if is_single() is false i.e. we’re displaying a list of posts.

Now, the code displayed in my category archives and on my homepage also looks great !! Of course, if you don’t have a problem in the first place e.g. because you only display excerpts on these pages, then there is no point in implementing this since it will unnecessarily load Prism on these pages.

Please note that when the changes you make in a plugin will be overwritten after every update of the plugin. So until this issue is fixed in the plugin, you will need to reapply it after every update.

Update: There are no support requests on the plugin page on wordpress.org but I’ve just seen that there is a bug report on github.

WordPress: Menu items combining multiple categories

On one of the websites I am administrating, posts are assigned two kinds of categories:

  • the age of children
  • for boys or for girls

In the navigation menu, you need to be able to select menu items corresponding to the following types of criteria:

  • Display all posts for boys or all posts for girls
  • Display all posts for children in a specific age category
  • Display all posts for boys in a specific age category or for girls in a specific age category

The first two requirements are easy. You just need to add the corresponding category archives to the menu. For the last one, there is no way to select a combination of categories in the WordPress menu builder.

So you need to add links to the menu and point to the results of a query for a combination of categories. Fortunately, you do not need to write any code to have such a list of post displayed. This can all be done by providing the appropriate parameters in the URL e.g.:

http://mysite.com/?cat=for-boys,6-12-months

for-boys and 6-12-months are the slugs of the corresponding categories.

If you have pretty URLs, it’d look like this:

http://mysite.com/category/for-boys,6-12-months

This will show you posts belonging to either category. What we actually want are posts belonging to both categories. In order to get this result, you need to replace the comma by a plus sign:

http://mysite.com/?cat=for-boys+6-12-months

If you have pretty URLs, it’d look like this:

http://mysite.com/category/for-boys+6-12-months

Instead of “category” you can use “tag” for post tags or “author” for post authors.

WordPress Spam Fighting

Until September 2014, I was mostly relying on Akismet to prevent comment spam on my blog. But some time last summer, I noticed that there were actually quite a few spam comments which weren’t identified as such. The reason for this was that the total number of spam comments reached about 1000 a day. So of course having a few of them slip through every day wasn’t actually much. But it was still a pain to go through them as I was only checking comments once in a while.

So I decided to find out what for methods I could use to prevent comment spam and not only rely on Akismet. This led me to write a plugin called WP Spam Fighter.

In this plugin, I’ve implemented a few methods designed to address different vectors used to create comment spam.

Spam Bots vs. Normal Users

First, most comment spam is not generated by humans but by bots just creating comments on thousands of sites. It makes it easier to trick them, since they are not designed to workaround all possible tricks used to identify them but to create as many comments as possible on as many sites as possible.

So how do you identify Spam bots ?

Well, first you have to understand how to identify a normal user:

  1. A normal user sees a rendered web page and not only the HTML code.
  2. A normal user actually reads your posts.
  3. A normal user also understands the fields contained in a comment form.

You should also note that the second characteristic of normal users also differentiate them from human spammers. Human spammers do see a rendered site and do understand the comment form fields but do not actually read your post.

Ok, so now we know how to identify a normal user, how do we use this knowledge to stop comment spam ?

A normal user sees a rendered web page

This basically means that a normal users will only fill in form fields which are actually visible on the rendered page. Spam Bots will fetch the HTML code and will not apply any CSS styles. So if you add a field in the form and make it invisible, normal users will not fill it but Spam bots might.

This leads us to a spam fighting method usually called a honeypot-based mechanism. Spam bots will usually go through all fields in the form and try to put in some value (since they do not know which fields are mandatory fields and which ones are optional).

The more the additional fields looks like a normal form fields the better the chances that even a half-intelligent Spam bot will not identify it as a honeypot.

A normal user actually reads your posts

Spam bots as well as human spammer actually do not care about the contents of your post. There is almost no targetted Spam which makes sure that the spam comments are added to a post which is actually really related to their comment. So even human spammers will just scroll down to the bottom of the post and enter some text in the form fields. Their goal is to produce as much spam as possible in the shortest period of time. So the chances are that they will try to spend as little time as possible on your page.

So a way to fight both human spammers and spam bots is to make sure that a comment can only be posted after the user has spent a certain amount of time on your page. How long this is basically depends on the type of content you have on your page. If you just post short jokes, it could be that a legitimate user posts a smiley as comment after only 20 seconds on your page. If you have long and complex articles on your site, the chances are that nobody will comment on your posts without spending at least a minute on the page.

This spam protection mechanism can be implemented by adding some JavaScript code which will return an error message when you try to submit a comment to early. Of course anything which resides purely on the client side is out of your control, so it can be circumvented by a spammer. That’s why you need to also have a check in the backend making sure that the JavaScript check has run (i.e. by adding data to the form before posting it).

A normal user also understands the fields contained in a comment form

Of course a human spammer also does. So developping a spam protection mechanism based on this will only help against spam bots not specifically targetting your site.

This just involves adding a form field and expecting a given value. It’s simplest form is having a checkbox labelled “I am NOT a spammer”. The most complex form is having some kind of captcha. Since I hate captchas, I’ve only implemented the simple checkbox option in my plugin. It’s not much additional work for a legitimate user and will still block stupid spam bots.

Additionally to this (or as an alternative), you can also implement a second similar mechanism involving automatically adding some kind of token to the form field using JavaScript (when the form is submitted). In the backend you then check the presence of this token. Spam bots ususally do not run JavaScript code on your page, so they will submit the form without this token.

Additional ways to identify legitimate users

If you want to make 100% sure that you do not get comment spam, there are a few additional methods you can use to prevent spam:

  1. Check whether a gravatar is associated with the provided email address
  2. Only allow registered users to comment
  3. Completely disable commenting

Of course, for a blog, I wouldn’t recommend disabling commenting as comments are often a valuable input both for you and for your visitors. Also many spam fighting mechanisms also make life more difficult for legitimate visitors who want to comment on a page. I personally hate Captchas and every time I get a captcha wrong on first entry or get a text I need to enter which I can hardly read, I just move away.

So forcing users to have a gravatar or registering in order to comment, could reduce the number of visitors who will actually take the time to post a comment.

Conclusion

Many of the mechanisms presented in this article are fairly simple and not at all bullet-proof. A human spammer would be able to workaround them all and it would also be possible to create a spam bot being able to work around them. What you need to keep in mind is:

  • Spam is usually not targetted. The spam comments you get are posted on millions of sites.
  • The goal of spammers is to get through with least effort.

This basically means that even though a spammer might implement a bot to workaround these mechanisms, why should he waste his time ? There are millions of sites out there and you are only one of them. Even spending an hour implementing a way to workaround you spam protection would be a waste of time.

So, of course, a human spammer could wait for 30 seconds before posting a comment on your site. But instead of spending 5 minutes posting 10 spam comments on your site, it’d make more sense to post 30 spam comments on another sites which doesn’t have this kind of protection.

On this site, I’ve configure the WP Spam Fighter plugin to enable the following mechanisms:

  • Time based protection
  • Honeypot protection
  • “Not a spammer” checkbox
  • JavaScript human check

And the results are not bad. After switching to this plugin from Akismet, the number of spam comments which went through reduced from 1 or 2 a day to 1 or 2 spam comments a month. Since I still do get a spam comment every 5 minutes (which is automatically marked as Spam by the plugin) and I only check the spam comment folder once in a while, it’s difficult to say whether there are any false positives. But there should actually not be any, since none of these mechanisms interpret comment data to identify spam. Moreover the number of comments I get on the site didn’t change after introducing this plugin.

And the great advantage versus Akismet is that this plugin doesn’t need to store or transmit any data related to the visitors of this site, which can be a problem in some countries.

Linux: ls hangs after NFS connection issues

We noticed today that ls was hanging when run on a particular directory. It didn’t seem to matter which options were used. This directory was not any directory but a directory where NFS shares were mounted.

Checking the output of dmesg, I could also see that there were some issues communicating with the other host:

host2:~ # dmesg
...

[1392590.834500] nfs: server host1 not responding, still trying

...

Then I tried running different commands to try and find out what was working and what was hanging:

host2:~ # cd /home/system/mnt/
host2:/home/system/mnt # ls

was hanging.

host2:~ # cd /home/system/mnt/
host2:/home/system/mnt # df -h .
Filesystem            Size  Used Avail Use% Mounted on
host1:/home         18G  9.3G  7.6G  56% /home

was not hanging. We can also see that the problem is not related to a huge or full share.

host2:~ # cd /home/system/mnt/
host2:/home/system/mnt # cat

pressing tab twice to get the completion was hanging

Also using CTRL-C or CTRL-Z was not working but it was possible to kill the session from another shell:

host2:~ # ps -Af | grep ls
root     19980     1  0 11:27 ?        00:00:00 ls -l /home/system/mnt/
root     22230 22196  0 11:47 pts/8    00:00:00 grep ls
host2:~ # kill -9 19980
host2:~ # ps -Af | grep ls
root     22300 22196  0 11:48 pts/8    00:00:00 grep ls

At some point I figured out that a simple was actually working. It was just hanging because ls was an alias and additional arguments were used. After removing the alias, a normal ls worked but an ls -l was hanging:

host2:~ # alias ls
alias ls='ls $LS_OPTIONS'
host2:~ # cd /home/system/mnt/
host2:/home/system/mnt # unalias ls
host2:/home/system/mnt # alias ls
-bash: alias: ls: not found
host2:/home/system/mnt # ls .
db_bck_add  ext_bck  twiddle.log  upload

Calling ls with strace didn’t show any issue when run without any option:

host2:~ # strace ls /home/system/mnt/
execve("/bin/ls", ["ls", "/home/system/mnt/"], [/* 174 vars */]) = 0
brk(0)                                  = 0x618000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ffa000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=63299, ...}) = 0
mmap(NULL, 63299, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7fea000
close(3)                                = 0
open("/lib64/librt.so.1", O_RDONLY)     = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\"\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=47206, ...}) = 0
mmap(NULL, 2132976, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7bd5000
fadvise64(3, 0, 2132976, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff7bdd000, 2093056, PROT_NONE) = 0
mmap(0x7ffff7ddc000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7000) = 0x7ffff7ddc000
close(3)                                = 0
open("/lib64/libselinux.so.1", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@X\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=113904, ...}) = 0
mmap(NULL, 2213528, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff79b8000
fadvise64(3, 0, 2213528, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff79d3000, 2093056, PROT_NONE) = 0
mmap(0x7ffff7bd2000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a000) = 0x7ffff7bd2000
mmap(0x7ffff7bd4000, 1688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff7bd4000
close(3)                                = 0
open("/lib64/libacl.so.1", O_RDONLY)    = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\37\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=31464, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe9000
mmap(NULL, 2126448, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff77b0000
fadvise64(3, 0, 2126448, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff77b7000, 2093056, PROT_NONE) = 0
mmap(0x7ffff79b6000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7ffff79b6000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY)      = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\354\1\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1661454, ...}) = 0
mmap(NULL, 3528776, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7452000
fadvise64(3, 0, 3528776, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff75a6000, 2097152, PROT_NONE) = 0
mmap(0x7ffff77a6000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x154000) = 0x7ffff77a6000
mmap(0x7ffff77ab000, 18504, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff77ab000
close(3)                                = 0
open("/lib64/libpthread.so.0", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@Z\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=135646, ...}) = 0
mmap(NULL, 2212736, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7235000
fadvise64(3, 0, 2212736, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff724c000, 2097152, PROT_NONE) = 0
mmap(0x7ffff744c000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x17000) = 0x7ffff744c000
mmap(0x7ffff744e000, 13184, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff744e000
close(3)                                = 0
open("/lib64/libdl.so.2", O_RDONLY)     = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\r\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=19114, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe8000
mmap(NULL, 2109696, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7031000
fadvise64(3, 0, 2109696, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff7033000, 2097152, PROT_NONE) = 0
mmap(0x7ffff7233000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7ffff7233000
close(3)                                = 0
open("/lib64/libattr.so.1", O_RDONLY)   = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\24\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=18936, ...}) = 0
mmap(NULL, 2113888, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff6e2c000
fadvise64(3, 0, 2113888, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff6e30000, 2093056, PROT_NONE) = 0
mmap(0x7ffff702f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7ffff702f000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe7000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe5000
arch_prctl(ARCH_SET_FS, 0x7ffff7fe5780) = 0
mprotect(0x7ffff702f000, 4096, PROT_READ) = 0
mprotect(0x7ffff7233000, 4096, PROT_READ) = 0
mprotect(0x7ffff744c000, 4096, PROT_READ) = 0
mprotect(0x7ffff77a6000, 16384, PROT_READ) = 0
mprotect(0x7ffff79b6000, 4096, PROT_READ) = 0
mprotect(0x7ffff7bd2000, 4096, PROT_READ) = 0
mprotect(0x7ffff7ddc000, 4096, PROT_READ) = 0
mprotect(0x616000, 4096, PROT_READ)     = 0
mprotect(0x7ffff7ffc000, 4096, PROT_READ) = 0
munmap(0x7ffff7fea000, 63299)           = 0
set_tid_address(0x7ffff7fe5a50)         = 22807
set_robust_list(0x7ffff7fe5a60, 0x18)   = 0
futex(0x7fffffffd01c, FUTEX_WAKE_PRIVATE, 1) = 0
futex(0x7fffffffd01c, 0x189 /* FUTEX_??? */, 1, NULL, 7ffff7fe5780) = -1 EAGAIN (Resource temporarily unavailable)
rt_sigaction(SIGRTMIN, {0x7ffff723a8b0, [], SA_RESTORER|SA_SIGINFO, 0x7ffff72445d0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {0x7ffff723a940, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x7ffff72445d0}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM_INFINITY}) = 0
brk(0)                                  = 0x618000
brk(0x639000)                           = 0x639000
open("/etc/selinux/config", O_RDONLY)   = -1 ENOENT (No such file or directory)
statfs("/selinux", {f_type="EXT2_SUPER_MAGIC", f_bsize=4096, f_blocks=3352304, f_bfree=2448097, f_bavail=2277808, f_files=851968, f_ffree=714434, f_fsid={151929686, 1001588022}, f_namelen=255, f_frsize=4096}) = 0
open("/proc/mounts", O_RDONLY)          = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff9000
read(3, "rootfs / rootfs rw 0 0\nudev /dev"..., 1024) = 1024
read(3, "600,retrans=2,sec=sys,mountaddr="..., 1024) = 1024
read(3, "e 0 0\n/dev/sdg1 /db/dump1 ext3 r"..., 1024) = 222
read(3, "", 1024)                       = 0
close(3)                                = 0
munmap(0x7ffff7ff9000, 4096)            = 0
open("/usr/lib/locale/locale-archive", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/locale.alias", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=2512, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff9000
read(3, "# Locale name alias data base.\n#"..., 4096) = 2512
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0x7ffff7ff9000, 4096)            = 0
open("/usr/lib/locale/en_US.UTF-8/LC_CTYPE", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_US.utf8/LC_CTYPE", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=256324, ...}) = 0
mmap(NULL, 256324, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7fa6000
close(3)                                = 0
open("/usr/lib64/gconv/gconv-modules.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=26050, ...}) = 0
mmap(NULL, 26050, PROT_READ, MAP_SHARED, 3, 0) = 0x7ffff7ff3000
close(3)                                = 0
futex(0x7ffff77aaf60, FUTEX_WAKE_PRIVATE, 2147483647) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=50, ws_col=120, ws_xpixel=0, ws_ypixel=0}) = 0
stat("/home/system/mnt/", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
open("/home/system/mnt/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
fcntl(3, F_GETFD)                       = 0x1 (flags FD_CLOEXEC)
getdents64(3, /* 6 entries */, 32768)   = 176
getdents64(3, /* 0 entries */, 32768)   = 0
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 8), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff2000
write(1, "db_bck_add  ext_bck  twiddle.log"..., 41db_bck_add  ext_bck  twiddle.log  upload
) = 41
close(1)                                = 0
munmap(0x7ffff7ff2000, 4096)            = 0
close(2)                                = 0
exit_group(0)                           = ?
s

But run with the -l option, it was hanging and I could see which entry was causing this:

host2:~ # strace ls -l /home/system/mnt/
execve("/bin/ls", ["ls", "-l", "/home/system/mnt/"], [/* 174 vars */]) = 0
brk(0)                                  = 0x618000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ffa000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=63299, ...}) = 0
mmap(NULL, 63299, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7fea000
close(3)                                = 0
open("/lib64/librt.so.1", O_RDONLY)     = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\"\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=47206, ...}) = 0
mmap(NULL, 2132976, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7bd5000
fadvise64(3, 0, 2132976, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff7bdd000, 2093056, PROT_NONE) = 0
mmap(0x7ffff7ddc000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7000) = 0x7ffff7ddc000
close(3)                                = 0
open("/lib64/libselinux.so.1", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@X\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=113904, ...}) = 0
mmap(NULL, 2213528, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff79b8000
fadvise64(3, 0, 2213528, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff79d3000, 2093056, PROT_NONE) = 0
mmap(0x7ffff7bd2000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a000) = 0x7ffff7bd2000
mmap(0x7ffff7bd4000, 1688, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff7bd4000
close(3)                                = 0
open("/lib64/libacl.so.1", O_RDONLY)    = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\37\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=31464, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe9000
mmap(NULL, 2126448, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff77b0000
fadvise64(3, 0, 2126448, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff77b7000, 2093056, PROT_NONE) = 0
mmap(0x7ffff79b6000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7ffff79b6000
close(3)                                = 0
open("/lib64/libc.so.6", O_RDONLY)      = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\354\1\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1661454, ...}) = 0
mmap(NULL, 3528776, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7452000
fadvise64(3, 0, 3528776, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff75a6000, 2097152, PROT_NONE) = 0
mmap(0x7ffff77a6000, 20480, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x154000) = 0x7ffff77a6000
mmap(0x7ffff77ab000, 18504, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff77ab000
close(3)                                = 0
open("/lib64/libpthread.so.0", O_RDONLY) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@Z\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=135646, ...}) = 0
mmap(NULL, 2212736, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7235000
fadvise64(3, 0, 2212736, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff724c000, 2097152, PROT_NONE) = 0
mmap(0x7ffff744c000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x17000) = 0x7ffff744c000
mmap(0x7ffff744e000, 13184, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff744e000
close(3)                                = 0
open("/lib64/libdl.so.2", O_RDONLY)     = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360\r\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=19114, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe8000
mmap(NULL, 2109696, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff7031000
fadvise64(3, 0, 2109696, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff7033000, 2097152, PROT_NONE) = 0
mmap(0x7ffff7233000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7ffff7233000
close(3)                                = 0
open("/lib64/libattr.so.1", O_RDONLY)   = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\300\24\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=18936, ...}) = 0
mmap(NULL, 2113888, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff6e2c000
fadvise64(3, 0, 2113888, POSIX_FADV_WILLNEED) = 0
mprotect(0x7ffff6e30000, 2093056, PROT_NONE) = 0
mmap(0x7ffff702f000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7ffff702f000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe7000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fe5000
arch_prctl(ARCH_SET_FS, 0x7ffff7fe5780) = 0
mprotect(0x7ffff702f000, 4096, PROT_READ) = 0
mprotect(0x7ffff7233000, 4096, PROT_READ) = 0
mprotect(0x7ffff744c000, 4096, PROT_READ) = 0
mprotect(0x7ffff77a6000, 16384, PROT_READ) = 0
mprotect(0x7ffff79b6000, 4096, PROT_READ) = 0
mprotect(0x7ffff7bd2000, 4096, PROT_READ) = 0
mprotect(0x7ffff7ddc000, 4096, PROT_READ) = 0
mprotect(0x616000, 4096, PROT_READ)     = 0
mprotect(0x7ffff7ffc000, 4096, PROT_READ) = 0
munmap(0x7ffff7fea000, 63299)           = 0
set_tid_address(0x7ffff7fe5a50)         = 22977
set_robust_list(0x7ffff7fe5a60, 0x18)   = 0
futex(0x7fffffffd00c, FUTEX_WAKE_PRIVATE, 1) = 0
futex(0x7fffffffd00c, 0x189 /* FUTEX_??? */, 1, NULL, 7ffff7fe5780) = -1 EAGAIN (Resource temporarily unavailable)
rt_sigaction(SIGRTMIN, {0x7ffff723a8b0, [], SA_RESTORER|SA_SIGINFO, 0x7ffff72445d0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {0x7ffff723a940, [], SA_RESTORER|SA_RESTART|SA_SIGINFO, 0x7ffff72445d0}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM_INFINITY}) = 0
brk(0)                                  = 0x618000
brk(0x639000)                           = 0x639000
open("/etc/selinux/config", O_RDONLY)   = -1 ENOENT (No such file or directory)
statfs("/selinux", {f_type="EXT2_SUPER_MAGIC", f_bsize=4096, f_blocks=3352304, f_bfree=2448097, f_bavail=2277808, f_file                                                                                        s=851968, f_ffree=714434, f_fsid={151929686, 1001588022}, f_namelen=255, f_frsize=4096}) = 0
open("/proc/mounts", O_RDONLY)          = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff9000
read(3, "rootfs / rootfs rw 0 0\nudev /dev"..., 1024) = 1024
read(3, "600,retrans=2,sec=sys,mountaddr="..., 1024) = 1024
read(3, "e 0 0\n/dev/sdg1 /db/dump1 ext3 r"..., 1024) = 222
read(3, "", 1024)                       = 0
close(3)                                = 0
munmap(0x7ffff7ff9000, 4096)            = 0
open("/usr/lib/locale/locale-archive", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/locale.alias", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=2512, ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7ff9000
read(3, "# Locale name alias data base.\n#"..., 4096) = 2512
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0x7ffff7ff9000, 4096)            = 0
open("/usr/lib/locale/en_US.UTF-8/LC_CTYPE", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/locale/en_US.utf8/LC_CTYPE", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=256324, ...}) = 0
mmap(NULL, 256324, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7fa6000
close(3)                                = 0
open("/usr/lib64/gconv/gconv-modules.cache", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=26050, ...}) = 0
mmap(NULL, 26050, PROT_READ, MAP_SHARED, 3, 0) = 0x7ffff7ff3000
close(3)                                = 0
futex(0x7ffff77aaf60, FUTEX_WAKE_PRIVATE, 2147483647) = 0
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=50, ws_col=120, ws_xpixel=0, ws_ypixel=0}) = 0
lstat("/home/system/mnt/", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
lgetxattr("/home/system/mnt/", "security.selinux", 0x61e400, 255) = -1 EOPNOTSUPP (Operation not supported)
getxattr("/home/system/mnt/", "system.posix_acl_access", 0x0, 0) = -1 ENODATA (No data available)
getxattr("/home/system/mnt/", "system.posix_acl_default", 0x0, 0) = -1 ENODATA (No data available)
socket(PF_FILE, 0x80801 /* SOCK_??? */, 0) = 3
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = 0
sendto(3, "\2\0\0\0\v\0\0\0\7\0\0\0passwd\0", 19, MSG_NOSIGNAL, NULL, 0) = 19
poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])
recvmsg(3, {msg_name(0)=NULL, msg_iov(2)=[{"passwd\0", 7}, {"\270O\3\0\0\0\0\0", 8}], msg_controllen=24, {cmsg_len=20, c                                                                                        msg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, {4}}, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 15
mmap(NULL, 217016, PROT_READ, MAP_SHARED, 4, 0) = 0x7ffff7f71000
close(4)                                = 0
close(3)                                = 0
socket(PF_FILE, 0x80801 /* SOCK_??? */, 0) = 3
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = 0
sendto(3, "\2\0\0\0\f\0\0\0\6\0\0\0group\0", 18, MSG_NOSIGNAL, NULL, 0) = 18
poll([{fd=3, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])
recvmsg(3, {msg_name(0)=NULL, msg_iov(2)=[{"group\0", 6}, {"\270O\3\0\0\0\0\0", 8}], msg_controllen=24, {cmsg_len=20, cm                                                                                        sg_level=SOL_SOCKET, cmsg_type=SCM_RIGHTS, {4}}, msg_flags=MSG_CMSG_CLOEXEC}, MSG_CMSG_CLOEXEC) = 14
mmap(NULL, 217016, PROT_READ, MAP_SHARED, 4, 0) = 0x7ffff7f3c000
close(4)                                = 0
close(3)                                = 0
open("/home/system/mnt/", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
fcntl(3, F_GETFD)                       = 0x1 (flags FD_CLOEXEC)
getdents64(3, /* 6 entries */, 32768)   = 176
lstat("/home/system/mnt/db_bck_add",

The directory on which it was hanging is itself an NFS share mounted on the other host. This share had some problem and had to be unmounted and remounted. It looks like this caused the NFS connection to hang for some reason. NFS seems to be an extremely fragile filesystem and if either your network or  the server itself are not 100% stable, you have a problem.

So the summary is that a reboot solves this issue and I at least figured out how to identify which of the directory contents was causing the issue. Unfortunately, not much more… If this happens again and I find out how to further investigate this or whether a restart of the NFS service is enough to solve the issue (without reboot), I’ll update this post.