1

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"
arnold_w
  • 59
  • 9
  • Why are you using `/b`? and what happens if you don't? – Compo May 12 '21 at 11:27
  • It makes no difference if I include /b or not. According to https://ss64.com/nt/start.html /B does the following, which I thought was unnecessary (Notepad has its own window and in my real application I don't need a window since it's a console application): "Start application without creating a new window. In this case Ctrl-C will be ignored - leaving Ctrl-Break as the only way to interrupt the application." – arnold_w May 12 '21 at 11:37
  • You're either wanting to open notepad.exe, or you're not, notepad, with no window, isn't really of any use is it? If you're not wanting to open notepad.exe, and you've provided us with only one problematic command, and that is to open notepad.exe, you're not really embracing the idea of providing sufficient information for us to reproduce and assist you with! – Compo May 12 '21 at 11:44
  • In my real life problem, I'm trying to execute a console-application I wrote myself, but to simplify the problem description, I changed it to Notepad. I'm getting the exact same hanging issue whether I try to execute my console-application or Notepad, but Notepad is more widely known in the programming world. Regardless of whether I include /b (or /B) or not, Notepad starts up fully visibly. – arnold_w May 12 '21 at 11:56
  • So you have a problem regarding a console application we've never heard of, seen, used, could decompile, or read the source code of, and you thought that pretending it was a GUI application called notepad.exe, was the obvious way of getting the assistance you needed! You may need to provide some more information, because, in a batch file, `start notepad.exe` will open notepad, and continue with the scripts execution, it will not wait for notepad.exe to close before it continues. – Compo May 12 '21 at 12:20
  • I thought simplifying the problem into the most simple and understandable form, where the problem at hand is still reproduceable, was the way to go. What you're saying about bat-files and `start notepad.exe`, is true, but it apparently doesn't work that way when triggered from a TortoiseGit hook. I also tried starting Notepad directly from the Linux hook-file using `notepad &` and I got the same result, the TortoiseGit Git Command Progress dialog hangs until I close Notepad. – arnold_w May 12 '21 at 12:55
  • Makes no sense to do this as the last line of your batch file: `cmd /c exit 0`. You are essentially spawning another environment and then just telling that environment to close. – Squashman May 12 '21 at 18:33

1 Answers1

0

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
arnold_w
  • 59
  • 9