6199: Register protocol and file extensions on client launch on Windows r=def- a=Robyt3

When launching the client on Windows, associate the protocol `ddnet` and the file extensions `.map` and `.demo` with the client executable.

See #6072.

## Checklist

- [X] Tested the change ingame
- [ ] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test (especially base/) or added coverage to integration test
- [ ] Considered possible null pointers and out of bounds array indexing
- [ ] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: Robert Müller <robytemueller@gmail.com>
This commit is contained in:
bors[bot] 2023-01-02 12:58:15 +00:00 committed by GitHub
commit b252f2e344
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 414 additions and 2 deletions

View file

@ -58,7 +58,9 @@
#elif defined(CONF_FAMILY_WINDOWS) #elif defined(CONF_FAMILY_WINDOWS)
#define WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN
#undef _WIN32_WINNT #undef _WIN32_WINNT
#define _WIN32_WINNT 0x0501 /* required for mingw to get getaddrinfo to work */ // 0x0501 (Windows XP) is required for mingw to get getaddrinfo to work
// 0x0600 (Windows Vista) is required to use RegGetValueW and RegDeleteTreeW
#define _WIN32_WINNT 0x0600
#include <windows.h> #include <windows.h>
#include <winsock2.h> #include <winsock2.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
@ -70,6 +72,7 @@
#include <process.h> #include <process.h>
#include <share.h> #include <share.h>
#include <shellapi.h> #include <shellapi.h>
#include <shlobj.h> // SHChangeNotify
#include <shlwapi.h> #include <shlwapi.h>
#include <wincrypt.h> #include <wincrypt.h>
#else #else
@ -1436,7 +1439,7 @@ static int priv_net_close_all_sockets(NETSOCKET sock)
} }
#if defined(CONF_FAMILY_WINDOWS) #if defined(CONF_FAMILY_WINDOWS)
static char *windows_format_system_message(int error) static char *windows_format_system_message(unsigned long error)
{ {
WCHAR *wide_message; WCHAR *wide_message;
const DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_MAX_WIDTH_MASK; const DWORD flags = FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_MAX_WIDTH_MASK;
@ -4302,6 +4305,282 @@ CWindowsComLifecycle::~CWindowsComLifecycle()
{ {
CoUninitialize(); CoUninitialize();
} }
static void windows_print_error(const char *system, const char *prefix, HRESULT error)
{
char *message = windows_format_system_message(error);
dbg_msg(system, "%s: %s", prefix, message == nullptr ? "unknown error" : message);
free(message);
}
static std::wstring utf8_to_wstring(const char *str)
{
const int orig_length = str_length(str);
int size_needed = MultiByteToWideChar(CP_UTF8, 0, str, orig_length, NULL, 0);
std::wstring wide_string(size_needed, '\0');
dbg_assert(MultiByteToWideChar(CP_UTF8, 0, str, orig_length, &wide_string[0], size_needed) == size_needed, "MultiByteToWideChar failure");
return wide_string;
}
bool shell_register_protocol(const char *protocol_name, const char *executable, bool *updated)
{
const std::wstring protocol_name_wide = utf8_to_wstring(protocol_name);
const std::wstring executable_wide = utf8_to_wstring(executable);
// Open registry key for protocol associations of the current user
HKEY handle_subkey_classes;
const LRESULT result_subkey_classes = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes", 0, KEY_ALL_ACCESS, &handle_subkey_classes);
if(result_subkey_classes != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error opening registry key", result_subkey_classes);
return false;
}
// Create the protocol key
HKEY handle_subkey_protocol;
const LRESULT result_subkey_protocol = RegCreateKeyExW(handle_subkey_classes, protocol_name_wide.c_str(), 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_protocol, NULL);
RegCloseKey(handle_subkey_classes);
if(result_subkey_protocol != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error creating registry key", result_subkey_protocol);
return false;
}
// Set the default value for the key, which specifies the name of the display name of the protocol
const std::wstring value_protocol = L"URL:" + protocol_name_wide + L" Protocol";
const LRESULT result_value_protocol = RegSetValueExW(handle_subkey_protocol, L"", 0, REG_SZ, (BYTE *)value_protocol.c_str(), (value_protocol.length() + 1) * sizeof(wchar_t));
if(result_value_protocol != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error setting registry value", result_value_protocol);
RegCloseKey(handle_subkey_protocol);
return false;
}
// Set the "URL Protocol" value, to specify that this key describes a URL protocol
const LRESULT result_value_empty = RegSetValueEx(handle_subkey_protocol, L"URL Protocol", 0, REG_SZ, (BYTE *)L"", sizeof(wchar_t));
if(result_value_empty != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error setting registry value", result_value_empty);
RegCloseKey(handle_subkey_protocol);
return false;
}
// Create the "DefaultIcon" subkey
HKEY handle_subkey_icon;
const LRESULT result_subkey_icon = RegCreateKeyExW(handle_subkey_protocol, L"DefaultIcon", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_icon, NULL);
if(result_subkey_icon != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error creating registry key", result_subkey_icon);
RegCloseKey(handle_subkey_protocol);
return false;
}
// Set the default value for the key, which specifies the icon associated with the protocol
const std::wstring value_icon = L"\"" + executable_wide + L"\",0";
const LRESULT result_value_icon = RegSetValueExW(handle_subkey_icon, L"", 0, REG_SZ, (BYTE *)value_icon.c_str(), (value_icon.length() + 1) * sizeof(wchar_t));
RegCloseKey(handle_subkey_icon);
if(result_value_icon != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error setting registry value", result_value_icon);
RegCloseKey(handle_subkey_protocol);
return false;
}
// Create the "shell\open\command" subkeys
HKEY handle_subkey_shell_open_command;
const LRESULT result_subkey_shell_open_command = RegCreateKeyExW(handle_subkey_protocol, L"shell\\open\\command", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_shell_open_command, NULL);
RegCloseKey(handle_subkey_protocol);
if(result_subkey_shell_open_command != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error creating registry key", result_subkey_shell_open_command);
return false;
}
// Get the previous default value for the key, so we can determine if it changed
wchar_t old_value_executable[MAX_PATH + 16];
DWORD old_size_executable = sizeof(old_value_executable);
const LRESULT result_old_value_executable = RegGetValueW(handle_subkey_shell_open_command, NULL, L"", RRF_RT_REG_SZ, NULL, (BYTE *)old_value_executable, &old_size_executable);
const std::wstring value_executable = L"\"" + executable_wide + L"\" \"%1\"";
if(result_old_value_executable != ERROR_SUCCESS || wcscmp(old_value_executable, value_executable.c_str()) != 0)
{
// Set the default value for the key, which specifies the executable command associated with the protocol
const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t));
RegCloseKey(handle_subkey_shell_open_command);
if(result_value_executable != ERROR_SUCCESS)
{
windows_print_error("shell_register_protocol", "Error setting registry value", result_value_executable);
return false;
}
*updated = true;
}
return true;
}
bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable, bool *updated)
{
const std::wstring extension_wide = utf8_to_wstring(extension);
const std::wstring executable_name_wide = utf8_to_wstring(executable_name);
const std::wstring description_wide = executable_name_wide + L" " + utf8_to_wstring(description);
const std::wstring program_id_wide = executable_name_wide + extension_wide;
const std::wstring executable_wide = utf8_to_wstring(executable);
// Open registry key for file associations of the current user
HKEY handle_subkey_classes;
const LRESULT result_subkey_classes = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes", 0, KEY_ALL_ACCESS, &handle_subkey_classes);
if(result_subkey_classes != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error opening registry key", result_subkey_classes);
return false;
}
// Create the program ID key
HKEY handle_subkey_program_id;
const LRESULT result_subkey_program_id = RegCreateKeyExW(handle_subkey_classes, program_id_wide.c_str(), 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_program_id, NULL);
if(result_subkey_program_id != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error creating registry key", result_subkey_program_id);
RegCloseKey(handle_subkey_classes);
return false;
}
// Set the default value for the key, which specifies the file type description for legacy applications
const LRESULT result_description_default = RegSetValueExW(handle_subkey_program_id, L"", 0, REG_SZ, (BYTE *)description_wide.c_str(), (description_wide.length() + 1) * sizeof(wchar_t));
if(result_description_default != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error setting registry value", result_description_default);
RegCloseKey(handle_subkey_program_id);
RegCloseKey(handle_subkey_classes);
return false;
}
// Set the "FriendlyTypeName" value, which specifies the file type description for modern applications
const LRESULT result_description_friendly = RegSetValueExW(handle_subkey_program_id, L"FriendlyTypeName", 0, REG_SZ, (BYTE *)description_wide.c_str(), (description_wide.length() + 1) * sizeof(wchar_t));
if(result_description_friendly != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error setting registry value", result_description_friendly);
RegCloseKey(handle_subkey_program_id);
RegCloseKey(handle_subkey_classes);
return false;
}
// Create the "DefaultIcon" subkey
HKEY handle_subkey_icon;
const LRESULT result_subkey_icon = RegCreateKeyExW(handle_subkey_program_id, L"DefaultIcon", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_icon, NULL);
if(result_subkey_icon != ERROR_SUCCESS)
{
windows_print_error("register_protocol", "Error creating registry key", result_subkey_icon);
RegCloseKey(handle_subkey_program_id);
RegCloseKey(handle_subkey_classes);
return false;
}
// Set the default value for the key, which specifies the icon associated with the program ID
const std::wstring value_icon = L"\"" + executable_wide + L"\",0";
const LRESULT result_value_icon = RegSetValueExW(handle_subkey_icon, L"", 0, REG_SZ, (BYTE *)value_icon.c_str(), (value_icon.length() + 1) * sizeof(wchar_t));
RegCloseKey(handle_subkey_icon);
if(result_value_icon != ERROR_SUCCESS)
{
windows_print_error("register_protocol", "Error setting registry value", result_value_icon);
RegCloseKey(handle_subkey_program_id);
RegCloseKey(handle_subkey_classes);
return false;
}
// Create the "shell\open\command" subkeys
HKEY handle_subkey_shell_open_command;
const LRESULT result_subkey_shell_open_command = RegCreateKeyExW(handle_subkey_program_id, L"shell\\open\\command", 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_shell_open_command, NULL);
RegCloseKey(handle_subkey_program_id);
if(result_subkey_shell_open_command != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error creating registry key", result_subkey_shell_open_command);
RegCloseKey(handle_subkey_classes);
return false;
}
// Get the previous default value for the key, so we can determine if it changed
wchar_t old_value_executable[MAX_PATH + 16];
DWORD old_size_executable = sizeof(old_value_executable);
const LRESULT result_old_value_executable = RegGetValueW(handle_subkey_shell_open_command, NULL, L"", RRF_RT_REG_SZ, NULL, (BYTE *)old_value_executable, &old_size_executable);
const std::wstring value_executable = L"\"" + executable_wide + L"\" \"%1\"";
if(result_old_value_executable != ERROR_SUCCESS || wcscmp(old_value_executable, value_executable.c_str()) != 0)
{
// Set the default value for the key, which specifies the executable command associated with the application
const LRESULT result_value_executable = RegSetValueExW(handle_subkey_shell_open_command, L"", 0, REG_SZ, (BYTE *)value_executable.c_str(), (value_executable.length() + 1) * sizeof(wchar_t));
RegCloseKey(handle_subkey_shell_open_command);
if(result_value_executable != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error setting registry value", result_value_executable);
RegCloseKey(handle_subkey_classes);
return false;
}
*updated = true;
}
// Create the file extension key
HKEY handle_subkey_extension;
const LRESULT result_subkey_extension = RegCreateKeyExW(handle_subkey_classes, extension_wide.c_str(), 0, NULL, 0, KEY_ALL_ACCESS, NULL, &handle_subkey_extension, NULL);
RegCloseKey(handle_subkey_classes);
if(result_subkey_extension != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error creating registry key", result_subkey_extension);
return false;
}
// Get the previous default value for the key, so we can determine if it changed
wchar_t old_value_application[128];
DWORD old_size_application = sizeof(old_value_application);
const LRESULT result_old_value_application = RegGetValueW(handle_subkey_extension, NULL, L"", RRF_RT_REG_SZ, NULL, (BYTE *)old_value_application, &old_size_application);
if(result_old_value_application != ERROR_SUCCESS || wcscmp(old_value_application, program_id_wide.c_str()) != 0)
{
// Set the default value for the key, which associates the file extension with the program ID
const LRESULT result_value_application = RegSetValueExW(handle_subkey_extension, L"", 0, REG_SZ, (BYTE *)program_id_wide.c_str(), (program_id_wide.length() + 1) * sizeof(wchar_t));
RegCloseKey(handle_subkey_extension);
if(result_value_application != ERROR_SUCCESS)
{
windows_print_error("shell_register_extension", "Error setting registry value", result_value_application);
return false;
}
*updated = true;
}
return true;
}
bool shell_unregister(const char *shell_class, bool *updated)
{
const std::wstring class_wide = utf8_to_wstring(shell_class);
// Open registry key for protocol and file associations of the current user
HKEY handle_subkey_classes;
const LRESULT result_subkey_classes = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Classes", 0, KEY_ALL_ACCESS, &handle_subkey_classes);
if(result_subkey_classes != ERROR_SUCCESS)
{
windows_print_error("shell_unregister", "Error opening registry key", result_subkey_classes);
return false;
}
// Delete the registry keys for the shell class (protocol or program ID)
LRESULT result_delete = RegDeleteTreeW(handle_subkey_classes, class_wide.c_str());
RegCloseKey(handle_subkey_classes);
if(result_delete != ERROR_SUCCESS && result_delete != ERROR_FILE_NOT_FOUND)
{
windows_print_error("shell_unregister", "Error deleting registry key", result_delete);
if(result_delete == ERROR_SUCCESS)
*updated = true;
return false;
}
return true;
}
void shell_update()
{
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
}
#endif #endif
size_t std::hash<NETADDR>::operator()(const NETADDR &Addr) const noexcept size_t std::hash<NETADDR>::operator()(const NETADDR &Addr) const noexcept

View file

@ -2581,6 +2581,63 @@ public:
CWindowsComLifecycle(bool HasWindow); CWindowsComLifecycle(bool HasWindow);
~CWindowsComLifecycle(); ~CWindowsComLifecycle();
}; };
/**
* Registers a protocol handler.
*
* @ingroup Shell
*
* @param protocol_name The name of the protocol.
* @param executable The absolute path of the executable that will be associated with the protocol.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_register_protocol(const char *protocol_name, const char *executable, bool *updated);
/**
* Registers a file extension.
*
* @ingroup Shell
*
* @param extension The file extension, including the leading dot.
* @param description A readable description for the file extension.
* @param executable_name A unique name that will used to describe the application.
* @param executable The absolute path of the executable that will be associated with the file extension.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_register_extension(const char *extension, const char *description, const char *executable_name, const char *executable, bool *updated);
/**
* Unregisters a protocol or file extension handler.
*
* @ingroup Shell
*
* @param shell_class The shell class to delete.
* For protocols this is the name of the protocol.
* For file extensions this is the program ID associated with the file extension.
* @param updated Pointer to a variable that will be set to true, iff the shell needs to be updated.
*
* @return true on success, false on failure.
*
* @remark The caller must later call shell_update, iff the shell needs to be updated.
*/
bool shell_unregister(const char *shell_class, bool *updated);
/**
* Notifies the system that a protocol or file extension has been changed and the shell needs to be updated.
*
* @ingroup Shell
*
* @remark This is a potentially expensive operation, so it should only be called when necessary.
*/
void shell_update();
#endif #endif
/** /**

View file

@ -280,6 +280,11 @@ public:
virtual CChecksumData *ChecksumData() = 0; virtual CChecksumData *ChecksumData() = 0;
virtual bool InfoTaskRunning() = 0; virtual bool InfoTaskRunning() = 0;
virtual int UdpConnectivity(int NetType) = 0; virtual int UdpConnectivity(int NetType) = 0;
#if defined(CONF_FAMILY_WINDOWS)
virtual void ShellRegister() = 0;
virtual void ShellUnregister() = 0;
#endif
}; };
class IGameClient : public IInterface class IGameClient : public IInterface

View file

@ -4704,6 +4704,11 @@ int main(int argc, const char **argv)
} }
} }
// Register protocol and file extensions
#if defined(CONF_FAMILY_WINDOWS)
pClient->ShellRegister();
#endif
// init SDL // init SDL
if(SDL_Init(0) < 0) if(SDL_Init(0) < 0)
{ {
@ -4897,3 +4902,53 @@ int CClient::UdpConnectivity(int NetType)
} }
return Connectivity; return Connectivity;
} }
#if defined(CONF_FAMILY_WINDOWS)
void CClient::ShellRegister()
{
char aBinaryPath[IO_MAX_PATH_LENGTH];
Storage()->GetBinaryPath(PLAT_CLIENT_EXEC, aBinaryPath, sizeof(aBinaryPath));
char aFullPath[IO_MAX_PATH_LENGTH];
if(fs_is_relative_path(aBinaryPath))
{
if(fs_getcwd(aFullPath, sizeof(aFullPath)))
{
str_append(aFullPath, "/", sizeof(aFullPath));
str_append(aFullPath, aBinaryPath, sizeof(aFullPath));
}
else
aFullPath[0] = '\0';
}
else
str_copy(aFullPath, aBinaryPath);
if(!aFullPath[0])
{
dbg_msg("client", "Failed to register protocol and file extensions: could not determine absolute path");
return;
}
bool Updated = false;
if(!shell_register_protocol("ddnet", aFullPath, &Updated))
dbg_msg("client", "Failed to register ddnet protocol");
if(!shell_register_extension(".map", "Map File", GAME_NAME, aFullPath, &Updated))
dbg_msg("client", "Failed to register .map file extension");
if(!shell_register_extension(".demo", "Demo File", GAME_NAME, aFullPath, &Updated))
dbg_msg("client", "Failed to register .demo file extension");
if(Updated)
shell_update();
}
void CClient::ShellUnregister()
{
bool Updated = false;
if(!shell_unregister("ddnet", &Updated))
dbg_msg("client", "Failed to unregister ddnet protocol");
if(!shell_unregister(GAME_NAME ".map", &Updated))
dbg_msg("client", "Failed to unregister .map file extension");
if(!shell_unregister(GAME_NAME ".demo", &Updated))
dbg_msg("client", "Failed to unregister .demo file extension");
if(Updated)
shell_update();
}
#endif

View file

@ -546,6 +546,11 @@ public:
CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; } CChecksumData *ChecksumData() override { return &m_Checksum.m_Data; }
bool InfoTaskRunning() override { return m_pDDNetInfoTask != nullptr; } bool InfoTaskRunning() override { return m_pDDNetInfoTask != nullptr; }
int UdpConnectivity(int NetType) override; int UdpConnectivity(int NetType) override;
#if defined(CONF_FAMILY_WINDOWS)
void ShellRegister() override;
void ShellUnregister() override;
#endif
}; };
#endif #endif

View file

@ -3433,6 +3433,17 @@ void CMenus::RenderSettingsDDNet(CUIRect MainView)
SUIExEditBoxProperties EditProps; SUIExEditBoxProperties EditProps;
EditProps.m_pEmptyText = Localize("Chat command (e.g. showall 1)"); EditProps.m_pEmptyText = Localize("Chat command (e.g. showall 1)");
UI()->DoEditBox(g_Config.m_ClRunOnJoin, &Button, g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin), 14.0f, &s_RunOnJoin, false, IGraphics::CORNER_ALL, EditProps); UI()->DoEditBox(g_Config.m_ClRunOnJoin, &Button, g_Config.m_ClRunOnJoin, sizeof(g_Config.m_ClRunOnJoin), 14.0f, &s_RunOnJoin, false, IGraphics::CORNER_ALL, EditProps);
#if defined(CONF_FAMILY_WINDOWS)
static CButtonContainer s_ButtonUnregisterShell;
Right.HSplitTop(10.0f, nullptr, &Right);
Right.HSplitTop(20.0f, &Button, &Right);
if(DoButton_Menu(&s_ButtonUnregisterShell, Localize("Unregister protocol and file extensions"), 0, &Button))
{
Client()->ShellUnregister();
}
#endif
// Updater // Updater
#if defined(CONF_AUTOUPDATE) #if defined(CONF_AUTOUPDATE)
{ {