Search code examples
batch-filetortoisegitpost-checkout-hook

Why does TortoiseGit Git Command Progress dialog hang after post-checkout hook starts Notepad?


In my everyday work I use TortoiseGit and I'm trying to write a post-checkout hook. I prefer to work in Windows-environment so the only thing the hook-file does is call a standard windows .bat-file:

#!/bin/sh
echo "The Git post-checkout Linux Shell-file has now started to execute"
cmd.exe "/c post-checkout.bat"
echo "The Git post-checkout Linux Shell-file has now finished executing"

Inside my standard Windows .bat-file, I do the following:

@echo off
echo --------------------------------------------------------------------------
echo post-checkout.bat in repository root has now started to execute

START /b  notepad.exe

echo post-checkout.bat in repository root has now finished executing
echo --------------------------------------------------------------------------
cmd /c exit 0

When I choose Switch/Checkout in TortoiseGit, my hook-file is successfully executed and Notepad starts. However, the strange thing is that the TortoiseGit Git Command Progress dialog hangs until I close Notepad. Please note that I can see "The Git post-checkout Linux Shell-file has now finished executing" in the TortoiseGit Git Command Progress dialog before I close Notepad. If I checkout using the C:\Program Files\Git\bin\bash.exe command window, then I get no hanging issue. Does anybody know how to solve this?

Edit: Putting the following directly in the Linux hook file (i.e. forgetting completely about the Window bat-file) produces the exact same result, the TortoiseGit Git Command Progress dialog hangs until I close Notepad:

#!/bin/sh
echo "The Git post-checkout Linux Shell-file has now started to execute"
notepad &
echo "The Git post-checkout Linux Shell-file has now finished executing"

Solution

  • In the source code https://gitlab.com/tortoisegit/tortoisegit/-/blob/master/src/Utils/Hooks.cpp it can be seen that a hook-file is executed in a new process that is created using CreateProcess and then it waits for the process and all its children to finish by calling WaitForSingleObject (see e.g. this explanation How to start a process and make it 'independent'). So the behavior is very much expected. One solution can be found here https://stackoverflow.com/a/1569941/2075678 which refers to this webpage https://svn.haxx.se/users/archive-2008-11/0301.shtml. I rewrote it slightly:

    #include "stdafx.h"
    #include <windows.h>
    
    int getFirstIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor);
    int getLastIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor);
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        WCHAR* pOriginalCmd = ::GetCommandLine();
        // Test code (modify paths to RunDetached.exe and MyFile.txt appropriately)
    //    pOriginalCmd = _T("\"D:\\My Visual Studio Projects\\RunDetached\\debug\\RunDetached.exe\" \"C:\\Windows\\System32\\notepad.exe\" \"D:\\1.txt\"");
    
        int CmdLen = (int)wcslen(pOriginalCmd);
    
        // Determine where certain characters are located (excl means the particular index is not included, e.g. 
        // if indexExcl is 5 then index 4 is the last included index).
        int beginningOf1stArg   = getFirstIndexOfChar(pOriginalCmd, 0,                     L'\"');
        int endOf1stArgExcl     = getFirstIndexOfChar(pOriginalCmd, beginningOf1stArg + 1, L'\"') + 1;
        int beginningOf2ndArg   = getFirstIndexOfChar(pOriginalCmd, endOf1stArgExcl   + 1, L'\"');
        int endOf2ndArgExcl     = getFirstIndexOfChar(pOriginalCmd, beginningOf2ndArg + 1, L'\"') + 1;
        int beginningOf3rdArg   = getFirstIndexOfChar(pOriginalCmd, endOf2ndArgExcl   + 1, L'\"');
        int endOfLastArgExcl    = getLastIndexOfChar (pOriginalCmd, CmdLen            - 1, L'\"') + 1;
        int beginningOfFileName = getLastIndexOfChar (pOriginalCmd, endOf2ndArgExcl   - 2, L'\\') + 1;
        int endOfFileNameExcl   = endOf2ndArgExcl - 1;
        if ((beginningOf1stArg < 0) || (endOf1stArgExcl     < 0) || (beginningOf2ndArg < 0) || (endOf2ndArgExcl < 0) ||
            (endOfLastArgExcl  < 0) || (beginningOfFileName < 0) || (endOfFileNameExcl < 0))
        {
            return -1;
        }
    
        // Determine the application to execute including full path. E.g. for notepad this should be:
        // C:\Windows\System32\notepad.exe (without any double-quotes)
        int lengthOfApplicationNameAndPathInChars = (endOf2ndArgExcl -1) - (beginningOf2ndArg + 1);  // Skip double-quotes
        WCHAR* lpApplicationNameAndPath = (WCHAR*)malloc(sizeof(WCHAR) * (lengthOfApplicationNameAndPathInChars + 1));
        memcpy(lpApplicationNameAndPath, &pOriginalCmd[beginningOf2ndArg + 1], sizeof(WCHAR) * (lengthOfApplicationNameAndPathInChars));
        lpApplicationNameAndPath[lengthOfApplicationNameAndPathInChars] = (WCHAR)0;  // Null terminate
    
        // Determine the command argument. Must be in modifyable memory and should start with the
        // application name without the path. E.g. for notepad with command argument D:\MyFile.txt:
        // "notepad.exe" "D:\MyFile.txt" (with the double-quotes).
        WCHAR* modifiedCmd = NULL;
        if (0 < beginningOf3rdArg)
        {
            int lengthOfApplicationNameInChars = endOfFileNameExcl - beginningOfFileName;  // Application name without path
            int lengthOfRestOfCmdInChars = CmdLen - beginningOf3rdArg;
            int neededCmdLengthInChars = 1 + lengthOfApplicationNameInChars + 2 + lengthOfRestOfCmdInChars; // Two double-quotes and one space extra
    
            modifiedCmd = (WCHAR*)malloc(sizeof(WCHAR) * (neededCmdLengthInChars + 1));  // Extra char is null-terminator
            modifiedCmd[0] = L'\"';                                                             // Start with double-quoute
            memcpy(&modifiedCmd[1], &pOriginalCmd[beginningOfFileName], sizeof(WCHAR) * (lengthOfApplicationNameInChars));
            modifiedCmd[1 + (lengthOfApplicationNameInChars)] = L'\"';
            modifiedCmd[1 + (lengthOfApplicationNameInChars) + 1] = L' ';
            memcpy(&modifiedCmd[1 + (lengthOfApplicationNameInChars) + 2], &pOriginalCmd[beginningOf3rdArg], sizeof(WCHAR) * lengthOfRestOfCmdInChars);
            modifiedCmd[neededCmdLengthInChars] = (WCHAR)0;
        }
    
        STARTUPINFO si;
        ZeroMemory( &si, sizeof(si) );
        si.cb = sizeof(si);
        PROCESS_INFORMATION pi;
        ZeroMemory( &pi, sizeof(pi) );
    
        BOOL result = CreateProcess    // Start the detached process.
        (
            lpApplicationNameAndPath, // Module name and full path
            modifiedCmd,              // Command line
            NULL,                     // Process handle not inheritable
            NULL,                     // Thread handle not inheritable
            FALSE,                    // Set bInheritHandles to FALSE
            DETACHED_PROCESS,         // Detach process
            NULL,                     // Use parent's environment block
            NULL,                     // Use parent's starting directory
            &si,                      // Pointer to STARTUPINFO structure
            &pi                       // Pointer to PROCESS_INFORMATION structure (returned)
        );
        free(lpApplicationNameAndPath);
        if (modifiedCmd != NULL)
        {
            free(modifiedCmd);
        }
        if (result) return 0;
        wchar_t msg[2048];
        FormatMessage
        (
            FORMAT_MESSAGE_FROM_SYSTEM,
            NULL,
            ::GetLastError(),
            MAKELANGID(LANG_NEUTRAL, SUBLANG_SYS_DEFAULT),
            msg, sizeof(msg),
            NULL
        );
        fputws(msg, stderr);
        _flushall();
        return -1;
    }
    
    int getFirstIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor)
    {
        int stringLen = (int)wcslen(stringToInvestigate);
        if (5000 < stringLen)   // Sanity check
        {
            return -1;
        }
        for (int i = startIndex; i < stringLen; i++)
        {
            if (stringToInvestigate[i] == charToLookFor)
            {
                return i;
            }
        }
        return -1;
    }
    
    int getLastIndexOfChar(WCHAR* stringToInvestigate, int startIndex, WCHAR charToLookFor)
    {
        int stringLen = (int)wcslen(stringToInvestigate);
        if (5000 < stringLen)   // Sanity check
        {
            return -1;
        }
        for (int i = min(stringLen - 1, startIndex); 0 <= i; i--)
        {
            if (stringToInvestigate[i] == charToLookFor)
            {
                return i;
            }
        }
        return -1;
    }
    
    

    After I had compiled the above, I modified my .bat-file and now it works great:

    @echo off
    echo --------------------------------------------------------------------------
    echo post-checkout.bat in repository root has now started to execute
    
    RunDetached  notepad
    
    echo post-checkout.bat in repository root has now finished executing
    echo --------------------------------------------------------------------------
    exit 0