From 96d823bb564b606a4ff9d4ca5ad38a0d0886aac0 Mon Sep 17 00:00:00 2001 From: Kirill Date: Tue, 5 Feb 2008 23:20:06 -0500 Subject: [PATCH] Implement self-registration of the extension This is an implementation of TODO "Need to not hard-code the dll path" Registration is implemented with a minimal regedit engine: an array of registry values { path, name, value } is the data source. So, all registry entries are hard-coded in dll.c. The engine attempts to convert values into longs to create REG_DWORD values. msysGit (for PathToMsys) is searched in the following order: - $(TARGET)/.. - $(TARGET)/../.. - %PATH% - InstallLocation of uninstall info (registration-type dependant). Signed-off-by: Johannes Schindelin --- Makefile | 27 ++--- dll.c | 305 +++++++++++++++++++++++++++++++++++++++++++++++++++--- git_shell_ext.def | 1 + install.reg.in | 58 ----------- registry.c | 114 ++++++++++++++++++++ registry.h | 28 +++++ uninstall.reg | 27 ----- 7 files changed, 443 insertions(+), 117 deletions(-) delete mode 100644 install.reg.in create mode 100644 registry.c create mode 100644 registry.h delete mode 100644 uninstall.reg diff --git a/Makefile b/Makefile index 8322bfc..dd6c1fe 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -OBJECTS=ext.o debug.o dll.o factory.o menu.o systeminfo.o +OBJECTS=ext.o debug.o dll.o factory.o menu.o systeminfo.o registry.o CFLAGS=-O -g TARGET=git_shell_ext.dll @@ -17,29 +17,24 @@ $(TARGET): $(OBJECTS) git_shell_ext.def # gcc $(LDFLAGS) -o $@ $(OBJECTS) -lole32 -luuid -loleaut32 # dlltool -d git_shell_ext.def -l $@ $(OBJECTS) -dll.o: dll.h ext.h factory.h +dll.o: dll.h ext.h factory.h systeminfo.h registry.h ext.o: ext.h debug.h factory.o: factory.h ext.h menu.h menu.o: menu.h ext.h debug.h systeminfo.h systeminfo.o: systeminfo.h +registry.o: registry.h -install%: install%.reg all - regsvr32 -s $(DLL_PATH) - regedit -s $< +install: all + regsvr32 -s -n -i:machine $(DLL_PATH) -uninstall%: uninstall%.reg - regsvr32 -u -s $(DLL_PATH) - regedit -s $< +uninstall: all + regsvr32 -u -s -n -i:machine $(DLL_PATH) -install.reg: install.reg.in Makefile - sed < $< > $@ \ - -e 's|@@MSYSGIT_PATH@@|$(MSYSGIT_PATH)|' \ - -e 's|@@DLL_PATH@@|$(DLL_PATH)|' +install-user: all + regsvr32 -s $(DLL_PATH) -%-user.reg: %.reg - sed -e 's|HKEY_CLASSES_ROOT\\|HKEY_CURRENT_USER\\Software\\Classes\\|' \ - -e 's|HKEY_LOCAL_MACHINE\\|HKEY_CURRENT_USER\\|' \ - < $< > $@ +uninstall-user: all + regsvr32 -u -s $(DLL_PATH) clean: -rm -f $(OBJECTS) $(TARGET) diff --git a/dll.c b/dll.c index bab9d9a..b694e8b 100644 --- a/dll.c +++ b/dll.c @@ -1,12 +1,16 @@ #include +#include #include "dll.h" #include "ext.h" #include "factory.h" +#include "systeminfo.h" +#include "registry.h" /* * The following is just the necessary infrastructure for having a .dll * which can be registered as a COM object. */ +static HINSTANCE hInst; HRESULT PASCAL DllGetClassObject(REFCLSID obj_guid, REFIID factory_guid, void **factory_handle) @@ -25,33 +29,302 @@ HRESULT PASCAL DllCanUnloadNow(void) return (object_count || lock_count) ? S_FALSE : S_OK; } -HRESULT PASCAL DllRegisterServer(void) +BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) { - char module[MAX_PATH]; - wchar_t module_name[MAX_PATH]; - ITypeLib *typelib = NULL; + hInst = instance; - GetModuleFileName(NULL, module, MAX_PATH); - MultiByteToWideChar(CP_ACP, 0, module, -1, module_name, MAX_PATH); - if (LoadTypeLib(module_name, &typelib) == S_OK) { - HRESULT result = RegisterTypeLib(typelib, module_name, NULL); - typelib->lpVtbl->Release(typelib); - return result; + if (reason == DLL_PROCESS_ATTACH) { + object_count = lock_count = 0; + DisableThreadLibraryCalls(instance); } + return 1; } +/* replaces a substring pattern with a string replacement within a string + the replacement occurs in-place, hence string must be large enough to + hold the result + + the function does not handle recursive replacements, e.g. + strreplace ("foo", "bar", "another bar"); + + always returns *string +*/ +static char *strreplace(char *string, const size_t size, + const char *pattern, const char *replacement) +{ + size_t len = strlen(string); + const size_t pattern_len = strlen(pattern); + const size_t replace_len = strlen(replacement); + + char *found = strstr(string, pattern); + + while (found) { + /* if the new len is greater than size, bail out */ + if (len + replace_len - pattern_len >= size) + return string; + + if (pattern_len != replace_len) + memmove(found + replace_len, + found + pattern_len, + len - (found - string) - pattern_len + 1); + memcpy(found, replacement, replace_len); + len += replace_len - pattern_len; + + found = strstr(string, pattern); + } + + return string; +} + +/* + * The following is the data for our minimal regedit engine, + * required for registration/unregistration of the extension + */ +#define CLASS_CHEETAH CLASSES_ROOT "CLSID\\@@CLSID@@" +#define CONTEXTMENUHANDLER "shellex\\ContextMenuHandlers\\@@PROGRAM_NAME@@" + +static const char *get_module_filename() { + static char module_filename[MAX_PATH] = { '\0' }; + + if (!*module_filename) { + DWORD module_size; + + module_size = GetModuleFileName(hInst, + module_filename, MAX_PATH); + if (0 == module_size) + return NULL; + } + + return module_filename; +} + +/* as per "How to: Convert Between System::Guid and _GUID" */ +static const char *get_class_id() +{ + static char class_id[MAX_REGISTRY_PATH] = { '\0' }; + + if (!*class_id) { + GUID guid = CLSID_git_shell_ext; + sprintf(class_id, + "{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", + guid.Data1, guid.Data2, guid.Data3, + guid.Data4[0], guid.Data4[1], guid.Data4[2], + guid.Data4[3], guid.Data4[4], guid.Data4[5], + guid.Data4[6], guid.Data4[7]); + } + + return class_id; +} + +/* + * Tries to find msysGit in the following order: + * .. and ../.. (relative to the module) + * %PATH% + * as qgit (via InstallLocation of Git) + SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Git_is1\InstallLocation + */ +static const reg_value registry_info[] = { + { CURRENT_WINDOWS APPROVED_EXT, "@@CLSID@@", "@@PROGRAM_NAME@@" }, + { CURRENT_WINDOWS APPROVED_EXT "\\@@CLSID@@", + NULL, NULL }, + { CURRENT_WINDOWS APPROVED_EXT "\\@@CLSID@@", + NULL,"@@PROGRAM_NAME@@" }, + { CLASS_CHEETAH, NULL, NULL }, + { CLASS_CHEETAH, NULL, "@@PROGRAM_NAME@@" }, + { CLASS_CHEETAH "\\InProcServer32", NULL, NULL }, + { CLASS_CHEETAH "\\InProcServer32", NULL, "@@PROGRAM_PATH@@"}, + { CLASS_CHEETAH "\\InProcServer32", "ThreadingModel", "Apartment" }, + { CLASSES_ROOT "*\\" CONTEXTMENUHANDLER, NULL, NULL }, + { CLASSES_ROOT "*\\" CONTEXTMENUHANDLER, NULL, "@@CLSID@@" }, + { CLASSES_ROOT "Directory\\" CONTEXTMENUHANDLER, NULL, NULL }, + { CLASSES_ROOT "Directory\\" CONTEXTMENUHANDLER, NULL, "@@CLSID@@" }, + { CLASSES_ROOT "Directory\\Background\\" CONTEXTMENUHANDLER, + NULL, NULL }, + { CLASSES_ROOT "Directory\\Background\\" CONTEXTMENUHANDLER, + NULL, "@@CLSID@@" }, + { CLASSES_ROOT "Drive\\" CONTEXTMENUHANDLER, NULL, NULL }, + { CLASSES_ROOT "Drive\\" CONTEXTMENUHANDLER, NULL, "@@CLSID@@"}, + { CLASSES_ROOT "Folder\\" CONTEXTMENUHANDLER, NULL, NULL }, + { CLASSES_ROOT "Folder\\" CONTEXTMENUHANDLER, NULL, "@@CLSID@@" }, + { CLASSES_ROOT "InternetShortcut\\" CONTEXTMENUHANDLER, + NULL, NULL }, + { CLASSES_ROOT "InternetShortcut\\" CONTEXTMENUHANDLER, + NULL, "@@CLSID@@" }, + { GIT_CHEETAH_REG_PATH, NULL, NULL }, + { GIT_CHEETAH_REG_PATH, + GIT_CHEETAH_REG_PATHTOMSYS, "@@MSYSGIT_PATH@@" }, + { NULL, NULL, NULL } +}; + +static const reg_value debug_info[] = { + { CURRENT_WINDOWS "Explorer", "DesktopProcess", "1" }, + { CURRENT_WINDOWS "Explorer\\AlwaysUnloadDll", NULL, NULL }, + { CURRENT_WINDOWS "Explorer\\Advanced", "SeparateProcess", "1" }, + { NULL, NULL, NULL } +}; + +static char msysgit[MAX_PATH] = { '\0' }; + +static BOOL find_msysgit_in_path() +{ + char *file; /* file part of the path to git.exe */ + DWORD dwFound; /* length of path to git.exe */ + + dwFound = SearchPath(NULL, "git.exe", NULL, MAX_PATH, msysgit, &file); + /* if git is not in the PATH or its path is too long */ + if (0 == dwFound || + dwFound > MAX_PATH) + return FALSE; + + /* + * git.exe is in "\bin\" from what we really need + * the minimal case we can handle is c:\bin\git.exe + * otherwise declare failure + */ + if (file < msysgit + 7) + return FALSE; + if (strnicmp(file - 5, "\\bin\\", 5)) + return FALSE; + file[-5] = '\0'; + + return TRUE; +} + +static BOOL find_msysgit_relative(const char *path) +{ + char *c; + + strcpy(msysgit, get_module_filename()); + c = strrchr(msysgit, '\\'); + c[1] = '\0'; + strcat(msysgit, path); + strcat(msysgit, "\\bin\\git.exe"); + if (INVALID_FILE_ATTRIBUTES == GetFileAttributes(msysgit)) { + msysgit[0] = '\0'; /* restore the original result */ + return FALSE; + } + + c[1] = '\0'; + strcat(msysgit, path); + return TRUE; +} + +static BOOL find_msysgit_uninstall(HKEY root) +{ + HKEY key; + HRESULT result; + DWORD valuelen = MAX_PATH; + + result = RegOpenKeyEx(root, + CURRENT_WINDOWS "\\Uninstall\\Git_is1", + 0, KEY_READ, &key); + if (ERROR_SUCCESS != result) + return FALSE; + + result = RegQueryValue(key, "InstallLocation", + (LPBYTE)msysgit, &valuelen); + return ERROR_SUCCESS == result; +} + +static HKEY setup_root; + +static const char *find_msysgit() +{ + if ('\0' == msysgit[0]) { + if (find_msysgit_relative("..")) + return msysgit; + + if (find_msysgit_relative("..\\..")) + return msysgit; + + if (find_msysgit_in_path()) + return msysgit; + + if (setup_root) + find_msysgit_uninstall(setup_root); + } + + return msysgit; +} + +/* + * required by registry.c + * supports @@PROGRAM_NAME@@, @@PROGRAM_PATH@@, @@CLSID@@ patterns + */ +char *get_registry_path(const char *src, char dst[MAX_REGISTRY_PATH]) +{ + if (NULL == src) + return NULL; + + strcpy(dst, src); + strreplace(dst, MAX_REGISTRY_PATH, + "@@PROGRAM_NAME@@", program_name); + strreplace(dst, MAX_REGISTRY_PATH, + "@@PROGRAM_PATH@@", get_module_filename()); + strreplace(dst, MAX_REGISTRY_PATH, + "@@CLSID@@", get_class_id()); + strreplace(dst, MAX_REGISTRY_PATH, + "@@MSYSGIT_PATH@@", find_msysgit()); + + return dst; +} + +HRESULT PASCAL DllRegisterServer(void) +{ + setup_root = HKEY_CURRENT_USER; + return create_reg_entries (setup_root, registry_info); +} + HRESULT PASCAL DllUnregisterServer(void) { - return S_OK; + setup_root = HKEY_CURRENT_USER; + return delete_reg_entries(setup_root, registry_info); } -BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) +/* provide means to create/delete keys: + - described in Debugging with The Shell; + - machine-wide registration and debugging info + + possible combinations of regsvr32 command line options: + -n (absent) (present) + -i: + (absent) user reg (invalid) + debug user reg+debug user debug + machine user+machine reg machine reg + machinedebug user+machine reg+debug machine reg+debug + + Obviously missing option is "machine debug". To accomplish: + - execute "regsvr32 -n -i:machinedebug" and then + - regsvr32 -u -n -i:machine +*/ +HRESULT PASCAL DllInstall(BOOL bInstall, LPCWSTR pszCmdLine) { - if (reason == DLL_PROCESS_ATTACH) { - object_count = lock_count = 0; - DisableThreadLibraryCalls(instance); + BOOL bDebug = NULL != wcsstr(pszCmdLine, L"debug"); + HRESULT result = ERROR_SUCCESS; + + setup_root = wcsstr(pszCmdLine, L"machine") ? + HKEY_LOCAL_MACHINE : HKEY_CURRENT_USER; + + if (bInstall) { + if (bDebug) + result = create_reg_entries(setup_root, debug_info); + + /* for user-only registration, use DllRegister */ + if (ERROR_SUCCESS == result && + HKEY_LOCAL_MACHINE == setup_root) + result = create_reg_entries(setup_root, + registry_info); + } else { /* uninstall */ + if (bDebug) + result = delete_reg_entries(setup_root, debug_info); + + /* for user-only unregistration, use DllUnregister */ + if (ERROR_SUCCESS == result && + HKEY_LOCAL_MACHINE == setup_root) + result = delete_reg_entries(setup_root, + registry_info); } - return 1; + return result; } diff --git a/git_shell_ext.def b/git_shell_ext.def index 472c184..bea88da 100644 --- a/git_shell_ext.def +++ b/git_shell_ext.def @@ -5,3 +5,4 @@ DllRegisterServer PRIVATE DllUnregisterServer PRIVATE DllCanUnloadNow PRIVATE DllGetClassObject PRIVATE +DllInstall PRIVATE diff --git a/install.reg.in b/install.reg.in deleted file mode 100644 index 2eb8f13..0000000 --- a/install.reg.in +++ /dev/null @@ -1,58 +0,0 @@ -Windows Registry Editor Version 5.00 - -; This registry file creates neccessary entries for installation. -; **** If you change this file, keep uninstall.reg in sync! **** -; -; This file is slated for being generated and not hard-coded. - -[HKEY_LOCAL_MACHINE\SOFTWARE\Git-Cheetah] - -[HKEY_LOCAL_MACHINE\SOFTWARE\Git-Cheetah] -"PathToMsys"="@@MSYSGIT_PATH@@" - -[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved] -"{ca586c80-7c84-4b88-8537-726724df6929}"="Git-Cheetah" - -[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved\{ca586c80-7c84-4b88-8537-726724df6929}] -@="Git-Cheetah" - -[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}] - -[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}] -@="Git-Cheetah" - -[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}\InProcServer32] -@="@@DLL_PATH@@" - -[HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}\InProcServer32] -"ThreadingModel"="Apartment" - -[HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\Git-Cheetah] - -[HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\Git-Cheetah] -@="{ca586c80-7c84-4b88-8537-726724df6929}" - -[HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\Git-Cheetah] - -[HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\Git-Cheetah] -@="{ca586c80-7c84-4b88-8537-726724df6929}" - -[HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\Git-Cheetah] - -[HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\Git-Cheetah] -@="{ca586c80-7c84-4b88-8537-726724df6929}" - -[HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\Git-Cheetah] - -[HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\Git-Cheetah] -@="{ca586c80-7c84-4b88-8537-726724df6929}" - -[HKEY_CLASSES_ROOT\Folder\shellex\ContextMenuHandlers\Git-Cheetah] - -[HKEY_CLASSES_ROOT\Folder\shellex\ContextMenuHandlers\Git-Cheetah] -@="{ca586c80-7c84-4b88-8537-726724df6929}" - -[HKEY_CLASSES_ROOT\InternetShortcut\shellex\ContextMenuHandlers\Git-Cheetah] - -[HKEY_CLASSES_ROOT\InternetShortcut\shellex\ContextMenuHandlers\Git-Cheetah] -@="{ca586c80-7c84-4b88-8537-726724df6929}" diff --git a/registry.c b/registry.c new file mode 100644 index 0000000..dd63978 --- /dev/null +++ b/registry.c @@ -0,0 +1,114 @@ +#include +#include "registry.h" + +/* uses get_registry_path to replace patterns */ +HRESULT create_reg_entries(const HKEY root, reg_value const info[]) +{ + HRESULT result; + int i; + + for (i = 0; NULL != info[i].path; ++i) { + char path[MAX_REGISTRY_PATH]; + char name[MAX_REGISTRY_PATH], *regname = NULL; + char value [MAX_REGISTRY_PATH], *regvalue = NULL; + + HKEY key; + DWORD disp; + + get_registry_path(info[i].path, path); + result = RegCreateKeyEx(root, path, + 0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, + &key, &disp); + if (ERROR_SUCCESS != result) + return (result); + + regname = get_registry_path(info[i].name, name); + regvalue = get_registry_path(info[i].value, value); + + /* + * regname can legitimately be NULL, + * but if value is NULL, it's just a key + */ + if (NULL != regvalue) { + char *endptr; + DWORD dwValue = strtoul(regvalue, &endptr, 10); + if (endptr && !*endptr) + result = RegSetValueEx(key, regname, 0, + REG_DWORD, + (LPBYTE)&dwValue, + sizeof(dwValue)); + else + result = RegSetValueEx(key, regname, 0, + REG_SZ, + (LPBYTE)regvalue, + (DWORD)strlen(regvalue)); + } + + RegCloseKey(key); + if (ERROR_SUCCESS != result) + return (result); + } + + return ERROR_SUCCESS; +} + +static inline HRESULT mask_errors(HRESULT const result) +{ + switch (result) { + case ERROR_FILE_NOT_FOUND: return ERROR_SUCCESS; + } + + return result; +} + +HRESULT delete_reg_entries(HKEY const root, reg_value const info[]) +{ + HRESULT result; + int i = 0; + + /* count items in the array */ + while (NULL != info[i].path) + i++; + /* at this point, i is the __count__, so + make it an offset to the last element */ + i--; + + /* walk the array backwards (we're at the terminating triple-null) */ + do { + char path[MAX_REGISTRY_PATH]; + HKEY key; + + i--; + + get_registry_path(info[i].path, path); + + if (info[i].name || info[i].value) { + /* delete the value */ + + char name[MAX_REGISTRY_PATH], *regname = NULL; + + result = mask_errors(RegOpenKeyEx(root, path, + 0, KEY_WRITE, &key)); + if (ERROR_SUCCESS != result) + return result; + + /* + * some of our errors are masked (e.g. not found) + * don't work on this key if we could not open it + */ + if (NULL == key) + continue; + + regname = get_registry_path(info[i].name, name); + result = mask_errors(RegDeleteValue(key, regname)); + + RegCloseKey(key); + } else /* not the value, delete the key */ + result = mask_errors(RegDeleteKey(root, path)); + + if (ERROR_SUCCESS != result) + return (result); + } while (i); + + return ERROR_SUCCESS; +} diff --git a/registry.h b/registry.h new file mode 100644 index 0000000..44bae4e --- /dev/null +++ b/registry.h @@ -0,0 +1,28 @@ +/* + * This is basically a simplified regedit engine that supports + * custom patterns through get_registry_path() that is required + * to be provided by the clients. + * + * It attempts to convert values to LONG to create REG_DWORD values + */ + +#define MAX_REGISTRY_PATH MAX_PATH + +#define CURRENT_WINDOWS "Software\\Microsoft\\Windows\\CurrentVersion\\" +#define APPROVED_EXT "Shell Extensions\\Approved" +#define CLASSES_ROOT "Software\\Classes\\" + +typedef struct reg_value { + char *path; + char *name; + char *value; +} reg_value; + +/* + * Clients need to provide the implementation of this function. + * The simplest implementation includes just strcpy(dst, src); + */ +char *get_registry_path(const char *src, char dst[MAX_REGISTRY_PATH]); + +HRESULT create_reg_entries(const HKEY root, reg_value const info[]); +HRESULT delete_reg_entries(HKEY const root, reg_value const info[]); diff --git a/uninstall.reg b/uninstall.reg deleted file mode 100644 index bd33685..0000000 --- a/uninstall.reg +++ /dev/null @@ -1,27 +0,0 @@ -Windows Registry Editor Version 5.00 - -; This registry file creates neccessary entries for uninstallation. -; **** If you change this file, keep install.reg in sync! **** -; -; This file is slated for being generated and not hard-coded. - -[-HKEY_LOCAL_MACHINE\SOFTWARE\Git-Cheetah] - -[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved] -"{ca586c80-7c84-4b88-8537-726724df6929}"=- - -[-HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved\{ca586c80-7c84-4b88-8537-726724df6929}] - -[-HKEY_CLASSES_ROOT\CLSID\{ca586c80-7c84-4b88-8537-726724df6929}] - -[-HKEY_CLASSES_ROOT\*\shellex\ContextMenuHandlers\Git-Cheetah] - -[-HKEY_CLASSES_ROOT\Directory\shellex\ContextMenuHandlers\Git-Cheetah] - -[-HKEY_CLASSES_ROOT\Directory\Background\shellex\ContextMenuHandlers\Git-Cheetah] - -[-HKEY_CLASSES_ROOT\Drive\shellex\ContextMenuHandlers\Git-Cheetah] - -[-HKEY_CLASSES_ROOT\Folder\shellex\ContextMenuHandlers\Git-Cheetah] - -[-HKEY_CLASSES_ROOT\InternetShortcut\shellex\ContextMenuHandlers\Git-Cheetah] -- 2.11.4.GIT