Executing Cygwin Bash scripts on Windows

I was reading Jeremy Rothman-Shore’s post regarding kicking off a Cygwin script from a Windows batch file which addresses two things:

  1. Invoking Cygwin’s Bash, passing it a shell script to execute
  2. Resolving the script’s directory in bash so that it can access other resources its directory.

Here I want to improve a bit on the first point and come up with:

  • A general purpose “shim” that will execute a shell script on Windows in Cygwin
  • Will pass through any command-line arguments
  • Doesn’t trip up on any spaces in the path

The idea is that if I have a shell script called foo.sh, I create a Windows equivalent called foo.cmd alongside it that can be called directly on Windows without the caller worrying about Cygwin etc (apart from having it installed):

image

(Or foo.bat – I prefer the *.cmd extension because this isn’t MS-DOS we’re running here).

The CMD script has to:

  1. Find its fully-qualified current location and locate its *.sh counterpart
  2. Translate this Windows-style path into a Unix style path
  3. Pass this to Cygwin’s bash along with any other arguments

Firstly, finding a batch file’s location is similar to how it’s done in Unix: it’s the first argument to the script. So in our case we use %0, and we can extract parts of the path like so: %~dp0 will extract the drive and path from argument 0 (i.e. the directory). See the for command for more information on this funky %~ syntax.

Secondly, the translation from a c:windowsstylepath to a /unix/style/path is done by Cygwin’s cygpath command. We do this in a slightly roundabout way via the ever versatile for command.

Thirdly, arguments passed to a batch file can either be accessed individually using $1, %2, $3 etc, or all in one go using %*, which is what we use here.

In addition, so that we don’t litter the Cygwin environment with temporary variables that we’ve created, we use a little setlocal/endlocal trick.

Here it is:

@echo off
setlocal

if not exist "%~dpn0.sh" echo Script "%~dpn0.sh" not found & exit 2

set _CYGBIN=C:cygwinbin
if not exist "%_CYGBIN%" echo Couldn't find Cygwin at "%_CYGBIN%" & exit 3

:: Resolve ___.sh to /cygdrive based *nix path and store in %_CYGSCRIPT%
for /f "delims=" %%A in ('%_CYGBIN%cygpath.exe "%~dpn0.sh"') do set _CYGSCRIPT=%%A

:: Throw away temporary env vars and invoke script, passing any args that were passed to us
endlocal & %_CYGBIN%bash --login "%_CYGSCRIPT%" %*

Note that you just name this the same as your shell script with a .cmd or .bat file extension. and then just execute it as normal.

For example for the following shell script called foo.sh:

#!/bin/sh
echo
echo "Hello from bash script $0"
echo "Working dir is $(pwd)"
echo
echo "arg 1 = $1"
echo "arg 2 = $2"
echo

Here’s me calling it from Windows:

image

Hope that helps!

SOLVED: Windows Identity Foundation – “The system cannot find the file specified”

I’ve been working on a proof of concept for using claims-based authorisation with Windows Identity Foundation (WIF) against an Active Directory Federation Services (ADFS) 2.0 security token service (STS).

I seemed to have everything in place but came up against the following error in a yellow screen of death:

System.Security.Cryptography.CryptographicException: The system cannot find the file specified.

image

Looking at the stack trace it seems that Data Protection API (DPAPI which in .NET is exposed as System.Security.Cryptography.ProtectedData) is being used to encrypt data. A common use of DPAPI is to do encryption without you having to worry about key management: you leave it to Windows to worry about where the keys are stored. Those keys are typically buried in your user profile/registry somewhere – so it seemed odd DPAPI was being used here at all – the DPAPI keys would need to be part of the App Pool user account profile/registry.

Anyway, to cut a long story short I wasn’t Reading The Fine error Message fully. The interesting/useful bit was scrolled horizontally off-screen:

[CryptographicException: The system cannot find the file specified.]
System.Security.Cryptography.ProtectedData.Protect(Byte[] userData, Byte[] optionalEntropy, DataProtectionScope scope) +681

Microsoft.IdentityModel.Web.ProtectedDataCookieTransform.Encode(Byte[] value) +121
[InvalidOperationException: ID1074: A CryptographicException occurred when attempting to encrypt the cookie using the ProtectedData API (see inner exception for details). If you are using IIS 7.5, this could be due to the loadUserProfile setting on the Application Pool being set to false. ]
Microsoft.IdentityModel.Web.ProtectedDataCookieTransform.Encode(Byte[] value) +1280740
Microsoft.IdentityModel.Tokens.SessionSecurityTokenHandler.ApplyTransforms(Byte[] cookie, Boolean outbound) +74

Sure enough WIF was using DPAPI to encrypt a token, but DPAPI was complaining it couldn’t get to the keys because there was no user profile for the App Pool identity, which in this case Environment.UserDomainName/UserName told me was “IIS APPPOOLDefaultAppPool” – and there was no such user profile directory under C:Users.

So sure enough in IIS, in the advanced settings for the App Pool, Load User Profile was false, and setting it to true creates and loads user profile (a “DefaultAppPool” profile directory appeared in C:Users), and the application worked:

image

Typically WIF tutorials use the ASP.NET Web Sites, which use Cassini for testing, which runs under the current user identity and therefore a user profile with its DPAPI keys will be loaded – which is why if you use the standard run throughs/demos you don’t come up against this issue. But if you run as a Web Application under “real” IIS, this is when you may hit this problem.

It’s also debatable whether the use of DPAPI here is at all sensible. In a web farm environment the DPAPI keys for the App Pool identities across servers will be different, so if you don’t have sticky sessions enabled on your load balancer you run the risk of such federated logins not working 100% of the time. This issue is mentioned by Matias Woloski in the Geneva forums.

Another case of bad defaults all round.

Chrome Extension: Amazon UK Search and Switch

I created a very simple Google Chrome extension that:

  • Allows you to search Amazon UK with a right-click
  • Switch back and forth between the UK and US versions of a page

Install it from the Chrome Extensions site.

The latter feature I find myself using all the time. If you follow, say, a book recommendation link to the .com site, but you want to buy  it on the .co.uk site. Alternatively, you may be reading a handful of reviews on the UK site, but over on the US site, there are lots more. When you’re on the Amazon.co.uk/.com site a “page action” icon will appear which, when clicked, will simply replace the amazon.co.uk in the URL with amazon.com or vice-versa.

It was incredibly simple to create. (It took longer to create the icons than write the code!) Extensions are essentially web pages that run in the background and have access to an API for interacting with Chrome: e.g. chrome.contextMenus, chrome.tabs, etc

For example: the search menu was created like so:

chrome.contextMenus.create({
    title: "Search Amazon.co.uk for '%s'",
    contexts:["selection"],
    onclick: function (info, tab) {
        var query = info.selectionText;
        chrome.tabs.create({
            selected: true,
            url: 'http://www.amazon.co.uk/s/ref=nb_sb_noss?url=search-alias%3Daps&field-keywords=' + encodeURIComponent(query)
        });
    }
});

To look at the rest of the source go to the install page, right click the Install button, save the CRX file and unzip it.

In future, I may add an options page so it’s not hard-coded for the UK/US sites and to selectively enable/disable the features.

Hope you find it as useful as I do.