win32: add a helper to run `git.exe` without a foreground window

On Windows, there are two kinds of executables, console ones and non-console ones. Git's executables are all console ones. When launching the former e.g. in a scheduled task, a CMD window pops up. This is not what we want for the tasks installed via the `git maintenance` command. To work around this, let's introduce `headless-git.exe`, which is a non-console program that does _not_ pop up any window. All it does is to re-launch `git.exe`, suppressing that console window, passing through all command-line arguments as-are. Helped-by: Carlo Marcelo Arenas Belón <carenas@gmail.com> Helped-by: Yuyi Wang <Strawberry_Str@hotmail.com> Signed-off-by: Johannes Schindelin <johannes.schindelin@gmx.de> Signed-off-by: Derrick Stolee <dstolee@microsoft.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Aug 9, 2023 at 16:54 UTC 4b8a2717bb5ea6b787e0037174dcbf53ccf4985c
6 files changed +139 -2
Makefile
+9
@@ -2778,6 +2778,13 @@ compat/nedmalloc/nedmalloc.sp compat/nedmalloc/nedmalloc.o: EXTRA_CPPFLAGS = \
2778 compat/nedmalloc/nedmalloc.sp: SP_EXTRA_FLAGS += -Wno-non-pointer-null
2779 endif
2780
2781 +headless-git.o: compat/win32/headless.c GIT-CFLAGS
2782 + $(QUIET_CC)$(CC) $(ALL_CFLAGS) $(COMPAT_CFLAGS) \
2783 + -fno-stack-protector -o $@ -c -Wall -Wwrite-strings $<
2784 +
2785 +headless-git$X: headless-git.o git.res GIT-LDFLAGS
2786 + $(QUIET_LINK)$(CC) $(ALL_CFLAGS) $(ALL_LDFLAGS) -mwindows -o $@ $< git.res
2787 +
2788 git-%$X: %.o GIT-LDFLAGS $(GITLIBS)
2789 $(QUIET_LINK)$(CC) $(ALL_CFLAGS) -o $@ $(ALL_LDFLAGS) $(filter %.o,$^) $(LIBS)
2790
@@ -3651,6 +3658,7 @@ clean: profile-clean coverage-clean cocciclean
3658 $(RM) po/git.pot po/git-core.pot
3659 $(RM) git.res
3660 $(RM) $(OBJECTS)
3661 + $(RM) headless-git.o
3662 $(RM) $(LIB_FILE) $(XDIFF_LIB) $(REFTABLE_LIB) $(REFTABLE_TEST_LIB)
3663 $(RM) $(ALL_PROGRAMS) $(SCRIPT_LIB) $(BUILT_INS) $(OTHER_PROGRAMS)
3664 $(RM) $(TEST_PROGRAMS)
@@ -3679,6 +3687,7 @@ endif
3687 $(RM) GIT-SCRIPT-DEFINES GIT-PERL-DEFINES GIT-PERL-HEADER GIT-PYTHON-VARS
3688 ifdef MSVC
3689 $(RM) $(patsubst %.o,%.o.pdb,$(OBJECTS))
3690 + $(RM) headless-git.o.pdb
3691 $(RM) $(patsubst %.exe,%.pdb,$(OTHER_PROGRAMS))
3692 $(RM) $(patsubst %.exe,%.iobj,$(OTHER_PROGRAMS))
3693 $(RM) $(patsubst %.exe,%.ipdb,$(OTHER_PROGRAMS))
compat/win32/headless.c new
+115
@@ -0,0 +1,115 @@
1 +/*
2 + * headless Git - run Git without opening a console window on Windows
3 + */
4 +
5 +#define STRICT
6 +#define WIN32_LEAN_AND_MEAN
7 +#define UNICODE
8 +#define _UNICODE
9 +#include <windows.h>
10 +#include <stdio.h>
11 +#include <stdlib.h>
12 +#include <wchar.h>
13 +
14 +/*
15 + * If `dir` contains the path to a Git exec directory, extend `PATH` to
16 + * include the corresponding `bin/` directory (which is where all those
17 + * `.dll` files needed by `git.exe` are, on Windows).
18 + */
19 +static int extend_path(wchar_t *dir, size_t dir_len)
20 +{
21 + const wchar_t *suffix = L"\\libexec\\git-core";
22 + size_t suffix_len = wcslen(suffix);
23 + wchar_t *env;
24 + DWORD len;
25 +
26 + if (dir_len < suffix_len)
27 + return 0;
28 +
29 + dir_len -= suffix_len;
30 + if (memcmp(dir + dir_len, suffix, suffix_len * sizeof(wchar_t)))
31 + return 0;
32 +
33 + len = GetEnvironmentVariableW(L"PATH", NULL, 0);
34 + if (!len)
35 + return 0;
36 +
37 + env = _alloca((dir_len + 5 + len) * sizeof(wchar_t));
38 + wcsncpy(env, dir, dir_len);
39 + wcscpy(env + dir_len, L"\\bin;");
40 + if (!GetEnvironmentVariableW(L"PATH", env + dir_len + 5, len))
41 + return 0;
42 +
43 + SetEnvironmentVariableW(L"PATH", env);
44 + return 1;
45 +}
46 +
47 +int WINAPI wWinMain(_In_ HINSTANCE instance,
48 + _In_opt_ HINSTANCE previous_instance,
49 + _In_ LPWSTR command_line, _In_ int show)
50 +{
51 + wchar_t git_command_line[32768];
52 + size_t size = sizeof(git_command_line) / sizeof(wchar_t);
53 + const wchar_t *needs_quotes = L"";
54 + int slash = 0, i;
55 +
56 + STARTUPINFO startup_info = {
57 + .cb = sizeof(STARTUPINFO),
58 + .dwFlags = STARTF_USESHOWWINDOW,
59 + .wShowWindow = SW_HIDE,
60 + };
61 + PROCESS_INFORMATION process_info = { 0 };
62 + DWORD creation_flags = CREATE_UNICODE_ENVIRONMENT |
63 + CREATE_NEW_CONSOLE | CREATE_NO_WINDOW;
64 + DWORD exit_code;
65 +
66 + /* First, determine the full path of argv[0] */
67 + for (i = 0; _wpgmptr[i]; i++)
68 + if (_wpgmptr[i] == L' ')
69 + needs_quotes = L"\"";
70 + else if (_wpgmptr[i] == L'\\')
71 + slash = i;
72 +
73 + if (slash >= size - 11)
74 + return 127; /* Too long path */
75 +
76 + /* If it is in Git's exec path, add the bin/ directory to the PATH */
77 + extend_path(_wpgmptr, slash);
78 +
79 + /* Then, add the full path of `git.exe` as argv[0] */
80 + i = swprintf_s(git_command_line, size, L"%ls%.*ls\\git.exe%ls",
81 + needs_quotes, slash, _wpgmptr, needs_quotes);
82 + if (i < 0)
83 + return 127; /* Too long path */
84 +
85 + if (*command_line) {
86 + /* Now, append the command-line arguments */
87 + i = swprintf_s(git_command_line + i, size - i,
88 + L" %ls", command_line);
89 + if (i < 0)
90 + return 127;
91 + }
92 +
93 + startup_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
94 + startup_info.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
95 + startup_info.hStdError = GetStdHandle(STD_ERROR_HANDLE);
96 +
97 + if (!CreateProcess(NULL, /* infer argv[0] from the command line */
98 + git_command_line, /* modified command line */
99 + NULL, /* inherit process handles? */
100 + NULL, /* inherit thread handles? */
101 + FALSE, /* handles inheritable? */
102 + creation_flags,
103 + NULL, /* use this process' environment */
104 + NULL, /* use this process' working directory */
105 + &startup_info, &process_info))
106 + return 129; /* could not start */
107 + WaitForSingleObject(process_info.hProcess, INFINITE);
108 + if (!GetExitCodeProcess(process_info.hProcess, &exit_code))
109 + exit_code = 130; /* Could not determine exit code? */
110 +
111 + CloseHandle(process_info.hProcess);
112 + CloseHandle(process_info.hThread);
113 +
114 + return (int)exit_code;
115 +}
config.mak.uname
+3
@@ -526,6 +526,8 @@ else
526 endif
527 X = .exe
528
529 + EXTRA_PROGRAMS += headless-git$X
530 +
531 compat/msvc.o: compat/msvc.c compat/mingw.c GIT-CFLAGS
532 endif
533 ifeq ($(uname_S),Interix)
@@ -705,6 +707,7 @@ ifeq ($(uname_S),MINGW)
707 COMPAT_CFLAGS += -D__USE_MINGW_ANSI_STDIO=0 -DDETECT_MSYS_TTY \
708 -fstack-protector-strong
709 EXTLIBS += -lntdll
710 + EXTRA_PROGRAMS += headless-git$X
711 INSTALL = /bin/install
712 INTERNAL_QSORT = YesPlease
713 HAVE_LIBCHARSET_H = YesPlease
contrib/buildsystems/CMakeLists.txt
+9
@@ -738,6 +738,15 @@ if(WIN32)
738 else()
739 message(FATAL_ERROR "Unhandled compiler: ${CMAKE_C_COMPILER_ID}")
740 endif()
741 +
742 + add_executable(headless-git ${CMAKE_SOURCE_DIR}/compat/win32/headless.c)
743 + if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
744 + target_link_options(headless-git PUBLIC -municode -Wl,-subsystem,windows)
745 + elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
746 + target_link_options(headless-git PUBLIC /NOLOGO /ENTRY:wWinMainCRTStartup /SUBSYSTEM:WINDOWS)
747 + else()
748 + message(FATAL_ERROR "Unhandled compiler: ${CMAKE_C_COMPILER_ID}")
749 + endif()
750 elseif(UNIX)
751 target_link_libraries(common-main pthread rt)
752 endif()
contrib/buildsystems/Generators/Vcxproj.pm
+2 -2
@@ -76,7 +76,7 @@ sub createProject {
76
77 my $libs_release = "\n ";
78 my $libs_debug = "\n ";
79 - if (!$static_library) {
79 + if (!$static_library && $name ne 'headless-git') {
80 $libs_release = join(";", sort(grep /^(?!libgit\.lib|xdiff\/lib\.lib|vcs-svn\/lib\.lib|reftable\/libreftable\.lib)/, @{$$build_structure{"$prefix${name}_LIBS"}}));
81 $libs_debug = $libs_release;
82 $libs_debug =~ s/zlib\.lib/zlibd\.lib/g;
@@ -230,7 +230,7 @@ EOM
230 print F << "EOM";
231 </ItemGroup>
232 EOM
233 - if (!$static_library || $target =~ 'vcs-svn' || $target =~ 'xdiff') {
233 + if ((!$static_library || $target =~ 'vcs-svn' || $target =~ 'xdiff') && !($name =~ /headless-git/)) {
234 my $uuid_libgit = $$build_structure{"LIBS_libgit_GUID"};
235 my $uuid_libreftable = $$build_structure{"LIBS_reftable/libreftable_GUID"};
236 my $uuid_xdiff_lib = $$build_structure{"LIBS_xdiff/lib_GUID"};
contrib/buildsystems/engine.pl
+1
@@ -371,6 +371,7 @@ sub handleLinkLine
371 # exit(1);
372 foreach (@objfiles) {
373 my $sourcefile = $_;
374 + $sourcefile =~ s/^headless-git\.o$/compat\/win32\/headless.c/;
375 $sourcefile =~ s/\.o$/.c/;
376 push(@sources, $sourcefile);
377 push(@cflags, @{$compile_options{"${sourcefile}_CFLAGS"}});