04 Jul 2013
I really wanted the current git branch name in the prompt on Windows. It turns out that getting this was … somewhat non-trivial.
I thought at first that some wrappers around git commands might be sufficient, but in practice that doesn’t work because there’s many ways to change the current branch, and they’re not all going to go through the wrappers. And so it doesn’t work consistently, which makes it almost worse than not having it at all (because you can’t trust it).
Poking around in cmd.exe’s prompt /?
, I found the variable $M
. This
“displays the remote name associated with the current drive letter”.
Windbg’ing cmd.exe showed that functionality is implemented by calling
WNetGetConnection.
This returned string gets embedded into the prompt which is similar to
what I want, and since I didn’t even know about $M
, I probably
wouldn’t miss the original functionality…
Helpfully, there’s only one call to WNetGetConnectionW
too, so the
original functionality doesn’t need to be conditionally maintained. So,
I tried IAT patching WNetGetConnectionW
to replace it with a hardcoded
string as a test. Unfortunately the call to that function is guarded by
a call to GetDriveType
(to check that the drive is actually a remote
drive). GetDriveType
is a bit trickier; it’s used for a variety of
different reasons in many places, so it can’t be unconditionally
replaced without breaking other functionality.
The final solution looks like this (eliding a few complexities for differences between the Windows 7 and Windows 8 binaries):
CreateRemoteThread()
with a ThreadProc
of
LoadLibrary
to inject a DLL into the running cmd.exe.PROMPT
variable
into the environment that contains the $M
variable.kernel32!GetDriveType
with a
HLT
, and install a vectored exception handler to trap that invalid
instruction when it executes.GetDriveType
in the $M
handling code, which will fault. In the
fault handler, walk up the stack, and if the code at the callsite
matches the disassembly that we’re expecting (a comparison with
DRIVE_REMOTE
, a particular form of jmp), then patch the comparison
to compare to DRIVE_FIXED
instead. This allows us to get to the
WNetGetConnectionW
call.GetDriveType
, and remove the
vectored exception handler.WNetGetConnectionW
and have it
jump to our “get git branch information” function instead.Because we don’t have control over the working directory and don’t want
to affect the normal cmd.exe operation too much, I did a little extra
packaging of the various binaries and dlls into one big exe that
extracts to TEMP
and runs from there, and uses various path and DLL
functions to avoid DLL loading problems.
Also, I use Console2 to wrap cmd.exe. It’s x86-only and so the cmd.exe that I end up running is x86. Because of that and step #4 above, the patching code currently only works on C:\Windows\SysWOW64\cmd.exe (that is, the x86 version). It should be easy to extend to x64 though. And of course, step #4 is generally just crazy-hacky, so it almost surely doesn’t work on versions of Windows that I haven’t tried it on (fully patched Windows 7 and Windows 8).
Anyway, after all that morally questionable mucking around, the end result makes me happy. From an existing cmd.exe:
d:\src\cmdEx>out\cmdEx.exe
[master] d:\src\cmdEx>
Or, a bit more fancy during a conflicted rebase:
[refs/heads/wps_impl 1/3|REBASE] d:\src\cr3\src>
meaning rebasing in-progress on step 1 of 3 on the named branch. And there’s no janky delay in the prompt like there is if you do this with bash on Windows.
Code is at github.