Macro to Restart Visual Studio Elevated

Kevin Dente ponders on Twitter:

Wonder if it would be possible to create a Visual Studio add-in or macro that re-launches VS elevated, with the current project, then exits

This is something I’ve been meaning to do for a while, as often I’ll open a Web Application in Visual Studio configured to run under IIS and be met with the following:

The Web Application Project is configured to use IIS. To access local IIS Web sites, you must run Visual Studio in the context of an administrator account..

Running anything elevated is easy, you just need to call ShellExecute (the .NET equivalent is System.Diagnostics.Process.Start) with the “RunAs” verb, so it’s simply a matter of starting Visual Studio’s devenv.exe passing the path to the current solution.

So here goes: copy and paste this into a Macro Module (ALT+F11) then wire up to a toolbar button as appropriate:

Sub ElevateVisualStudio()

    Dim slnPath As String = DTE.Solution.FullName
    DTE.Solution.Close(True)

    Dim startInfo As New System.Diagnostics.ProcessStartInfo(DTE.FullName, slnPath)
    startInfo.Verb = "RUNAS"
    System.Diagnostics.Process.Start(startInfo)

    DTE.Quit()

End Sub

Works On My Machine™, etc and only tested on Visual Studio 2008.

Fixing WebSVN “Unable to call svn command” error

WebSVN is a great PHP web-based interface to Subversion that we run on our IIS build server. But, after updating to a more recent build we started getting errors like the following when viewing diffs:

Unable to call svn command "svn --non-interactive --config-dir /tmp"

The source of the error is in the getFileContents function in svnlook.php, where it’s piping the output of svn cat to GNU Enscript (for syntax highlighting) and then parsing the output of that using sed. I guessed that maybe the regex that sed is using isn’t being escaped properly for the Windows command line and therefore failing.

But I noticed that all of this code was in an if($config->useEnscript) block. Just before that is an if($config->useGeshi) block that looks a lot simpler. Simpler because GeSHi is PHP-based syntax highlighter, rather than an external command whose output requires special parsing. So I went to our include/config.php and added the $config->useGeshi(); call (as detailed in the distconfig.php template config file), and it all started working happily again!

Unzipping/extracting MSI files

If, like me, you are constantly wanting to just extract the files from a Windows Installer MSI file quickly, then this is for you.

My ZIP utility of choice 7-Zip appears to support extracting MSI files but in fact extracts all the various weird and wonderful binary streams in the MSI rather than simply just the actual files. Thankfully I stumbled across a Windows Installer switch today after typing msiexec /? that does the job perfectly: the /a “administrative install” switch, e.g.:

msiexec /a foo.msi /qb TARGETDIR="C:\TEMP\Foo"

So, what I’ve done is packaged this up as a little registry tweak that conveniently lets you do this by simply right clicking a file like so:

image

Copy and paste the following into a *.reg file and double-click it:

Windows Registry Editor Version 5.00
[HKEY_CURRENT_USER\Software\Classes\Msi.Package\shell\Extract MSI\command]
@="msiexec.exe /a \"%1\" /qb TARGETDIR=\"%1 Extracted\""

Hope that helps!

Disabling direct PDF viewing/opening from the web

This worries me (more information). It would appear that you could craft PDFs that could hijack someone’s machine simply by their opening the PDF. This is especially pernicious because Adobe Reader is so intent on integrating PDF into Internet Explorer using various Browser Helper Objects and ActiveX Controls that a “drive-by” hijack would be simple to set up once you’ve crafted the malicious PDF file.

A quick fix (that Works On My Machine™) that prevents the automatic opening/viewing of PDF files from the web is to change the HKEY_CLASSES_ROOT\.pdf\Content Type registry key from application/pdf to something like application/octet-stream. This means that you have to save the files first before opening them.

Some possible scriptable ways of automating this:

Method 1: A .REG file

Save the following into a file with a “.reg” file extension and running using regedit.exe /s filename.reg.

REGEDIT4
[HKEY_CLASSES_ROOT\.pdf]
"Content Type"="application/octet-stream"

Method 2: The REG command

Alternatively the following, using the reg.exe command will achieve the same result:

reg add HKCR\.pdf /v "Content Type" /d "application/octet-stream" /f

You obviously still need to be careful about PDFs, you just have the ability now to actively decide whether you want to open them.

Cisco home page FAIL

UPDATE: it’s over 2 hours later now and they’ve finally fixed it :)

Cisco’s home page this morning: looks like they ran out of their allocation of lowercase letter ‘t’ (thanks to Chris Venus for pointing it out):

image

Or was it “Speak Like an Irishman” day and nobody told me?

Lowercase ‘t’s were being stripped from the source which explains why there were no styles, etc:

image

Chris reckons it’s a possibly a whitespace-trimming RegEx gone wrong, WDYT?

Solved: “Cannot read from the source file or disk”

I’ve finally solved a problem that’s been bugging me for years. One of our file shares ended up with several undelete-able files. Attempting to delete them results in “Error Deleting File or Folder – Cannot delete file: Cannot read from the source file or disk“.

Note: Windows 7′s version of this message is something like:

Could not find this item: This is no longer located in C:\Blah. Verify the item’s location and try again.

Even going to the file’s properties to check permissions presented a very blank properties dialog. And a CHKDSK didn’t sort thing out either.

It turns out the problem was: the filename ended with a dot, e.g. it was something like “C:\Temp\Stuff\Sales Agreement.“. As far as Windows is concerned this is an invalid file name: so although it gets reported in a directory listing, the standard Windows APIs for manipulating files subsequently deny its existence.

So how did this file get created in the first place? The answer: a Mac. The file was on a file share which had been accessed by a Mac user. Macs tend to write all sorts of metadata to extra “._DSStore” files and suchlike and had left this file behind.

So if Windows doesn’t appear to allow these file names, how did they get to be created? Well, it turns out that NTFS allows all sort of file name/path weirdness that Windows, or specifically the Win32 API, doesn’t allow. For example, NTFS actually allows file paths up to 32K but Windows restricts file paths to no more than 260 characters (MAX_PATH). I suppose this is all for DOS/Windows 9x backwards compatibility. As these files were being accessed over a file share I guess the usual Win32 checks are bypassed.

But thankfully you can get Win32 to ignore these checks by prefixing your file paths with \\?\, (ie. C:\Temp\SomeFile.txt becomes \\?\C:\Temp\SomeFile.txt) which I discovered after reading this blog post about long paths in .NET.

So at a command prompt (Start > All Programs > Accessories > Command Prompt) I was able to delete the file using:

del "\\?\C:\Temp\Stuff\Sales Agreement."

Note: On Windows 7 it seems you can just use wildcards without the \\?\ trick to delete the offending files: e.g.
del c:\temp\somefil*

If it’s a folder/directory you’re trying to delete use the rd or rmdir command, e.g.:

rd /s "\\?\C:\Documents and Settings\User\Desktop\Annoying Folder."

Tip: as you’re typing the file/directory name use the TAB key to auto-complete the name (press TAB repeatedly to cycle through possible names).

 


Of course the corollary of all of this is that you could really annoy somebody by doing this:

 

echo Hi > "\\?\%USERPROFILE%\Desktop\Annoying file you can't delete."

But you wouldn’t do that would you?

If this post helped you and you feel so inclined, feel free to buy me a beer :-)PayPal - The safer, easier way to pay online.

Moving on from SVN_ASP_DOT_NET_HACK

I noticed that I was running TortoiseSVN in SVN_ASP_DOT_NET_HACK mode (where Subversion clients use ‘_svn’ instead of ‘.svn’ directories) unnecessarily as I don’t have silly old Visual Studio 2003 installed anymore which caused this mess in the first place.

The _svn directories work just as well as .svn, but nevertheless (due to mild OCD?), I created a batch script that ripped through my project directory renaming all ‘_svn’ directories to ‘.svn’, so I could remove the SVN_ASP_DOT_NET_HACK mode. It uses the wonderfully flexible FOR command drive the whole process. We don’t need no stinkin’ Powershell round these parts…

Save this script as something like “SvnRenameDirs.cmd” in the root of your projects folder:

:: Make script directory current
pushd "%~dp0"

:: Unhide, rename and re-hide svn dirs
for /r /d %%D in (*) do @if exist "%%D\_svn" (
   attrib -H "%%D\_svn"
   ren "%%D\_svn" ".svn"
   attrib +H "%%D\.svn"
)
popd

rundll32.exe shell32.dll,Control_RunDLL sysdm.cpl,0,3

At the end click the Environment Variables button in the System Properties dialog and remove the SVN_ASP_DOT_NET_HACK environment variable and then log out and back in again (or restart explorer.exe and TSVNCache.exe).

System.Net.Mail: The specified string is not in the form required for a subject

Having your ASP.NET error handling routine, which sends you emails when an error occurs on your site, itself fail is annoying. Especially when you think you’ve made the code robust enough. Anyway the error handler for one site I work on was failing with “ArgumentException: The specified string is not in the form required for a subject“.

So what is exactly “the form required for a subject”? Googling for this error message returns a lot of junk and misinformed forum posts. It turns out that setting the Subject on a System.Net.Mail.Message internally calls MailBnfHelper.HasCROrLF (thank you Reflector) which does exactly what it says on the tin. Therefore one forum poster’s solution of subject.Replace("\r\n", " ") isn’t going to work when your have either a carriage return or line feed in there.

So, obviously, the solution is:

message.Subject = subject.Replace('\r', ' ').Replace('\n', ' ');

Personally, I think that the MailMessage should to this for you or at least Microsoft should document what actually constitutes a “form required for a subject” in MSDN or, even better, in the actual error message itself!

C# Extension Methods and IsNullOrEmpty

As Brad Wilson points out one of the nice features of C# 3.0′s extension methods is that they work on null instances. Indeed, why shouldn’t they? They’re just static methods that are invoked in a slightly different way. But this allows a subtle but very pleasing (to me this morning anyway) bit of syntactic sugar for the very commonly used String.IsNullOrEmpty() method.

So instead of:

if (string.IsNullOrEmpty(myString)) ...

you can have the more-readable:

if (myString.IsNullOrEmpty()) ...

by using:

public static class StringExtensions
{
   public static bool IsNullOrEmpty(this string str)
   {
      return string.IsNullOrEmpty(str);
   }
}

It’s a very small thing but I like it.

Draytek Vigor 2800 reboot script

After years of great service, our Draytek Vigor 2800 seems to be having problems after running for a few days of allowing HTTPS in to our network, which is only solved by rebooting the router. For the life of me I can’t upgrade the firmware using either the TFTP or FTP approach. So giving up, I’ve adopted the brute force approach of scheduling a reboot every night. To do this I considered scripting the appropriate Telnet commands to the router, but that would involve some custom or third party code. (Telnet, being a duplex protocol means you can’t simply pipe commands to telnet.exe from a batch file, you have to use something like expect to script a Telnet session.)

The simplest, lowest-tech approach (and they’re always the best, right?) I found was merely to mimic the reboot page on the router web management UI with the following Windows Scripting Host script:

////// RebootRouter.js ////////
// Change these for your environment
var ROUTER_IP = "192.168.0.1";
var ROUTER_USER = "admin";
var ROUTER_PASSWORD = "pA55w0rd";

var http = new ActiveXObject("Microsoft.XMLHTTP");
http.open("POST", "http://" + ROUTER_IP
    + "/cgi-bin/reboot.cgi", false,
    ROUTER_USER, ROUTER_PASSWORD);
http.setRequestHeader("Content-Type",
    "application/x-www-form-urlencoded");
http.send("sReboot=Current&submit=OK");

// For debugging/logging un-comment the following lines:
// WScript.Echo(http.status + " " + http.statusText);
// WScript.Echo(http.responseText);

Save this as RebootRouter.js, update the values accordingly and schedule for a appropriate time using Control Panel > Scheduled Tasks.