dllmain.cpp
1 #include "pch.h" 2 #include <interface/powertoy_module_interface.h> 3 #include <common/SettingsAPI/settings_objects.h> 4 #include <common/interop/shared_constants.h> 5 #include "trace.h" 6 #include "generateSecurityDescriptor.h" 7 8 #include <common/logger/logger.h> 9 #include <common/SettingsAPI/settings_helpers.h> 10 11 #include <common/utils/process_path.h> 12 #include <common/utils/resources.h> 13 #include <common/utils/winapi_error.h> 14 #include <common/utils/processApi.h> 15 #include <common/utils/elevation.h> 16 #include <common/utils/logger_helper.h> 17 18 HINSTANCE g_hInst_MouseWithoutBorders = 0; 19 20 BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) 21 { 22 switch (ul_reason_for_call) 23 { 24 case DLL_PROCESS_ATTACH: 25 g_hInst_MouseWithoutBorders = hModule; 26 Trace::MouseWithoutBorders::RegisterProvider(); 27 break; 28 case DLL_THREAD_ATTACH: 29 case DLL_THREAD_DETACH: 30 break; 31 case DLL_PROCESS_DETACH: 32 Trace::MouseWithoutBorders::UnregisterProvider(); 33 break; 34 } 35 return TRUE; 36 } 37 38 bool GetUserSid(const wchar_t* username, PSID& sid) 39 { 40 DWORD sidSize = 0; 41 DWORD domainNameSize = 0; 42 SID_NAME_USE sidNameUse; 43 44 LookupAccountName(nullptr, username, nullptr, &sidSize, nullptr, &domainNameSize, &sidNameUse); 45 if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) 46 { 47 Logger::error("Failed to get buffer sizes"); 48 return false; 49 } 50 51 sid = LocalAlloc(LPTR, sidSize); 52 LPWSTR domainName = static_cast<LPWSTR>(LocalAlloc(LPTR, domainNameSize * sizeof(wchar_t))); 53 54 if (!LookupAccountNameW(nullptr, username, sid, &sidSize, domainName, &domainNameSize, &sidNameUse)) 55 { 56 Logger::error("Failed to lookup account name"); 57 LocalFree(sid); 58 LocalFree(domainName); 59 return false; 60 } 61 62 LocalFree(domainName); 63 return true; 64 } 65 66 std::wstring GetCurrentUserSid() 67 { 68 wchar_t username[UNLEN + 1]; 69 DWORD usernameSize = UNLEN + 1; 70 71 std::wstring result; 72 if (!GetUserNameW(username, &usernameSize)) 73 { 74 Logger::error("Failed to get the current user name"); 75 return result; 76 } 77 78 PSID sid; 79 if (GetUserSid(username, sid)) 80 { 81 LPWSTR sidString; 82 if (ConvertSidToStringSid(sid, &sidString)) 83 { 84 result = sidString; 85 LocalFree(sidString); 86 } 87 LocalFree(sid); 88 } 89 else 90 { 91 Logger::error(L"Failed to get SID for user \""); 92 } 93 94 return result; 95 } 96 97 std::wstring escapeDoubleQuotes(const std::wstring& input) 98 { 99 std::wstring output; 100 output.reserve(input.size()); 101 102 for (const wchar_t& ch : input) 103 { 104 if (ch == L'"') 105 { 106 output += L'\\'; 107 } 108 output += ch; 109 } 110 111 return output; 112 } 113 114 const static wchar_t* MODULE_NAME = L"MouseWithoutBorders"; 115 const static wchar_t* MODULE_DESC = L"A module to move your mouse across computers."; 116 const static wchar_t* SERVICE_NAME = L"PowerToys.MWB.Service"; 117 const static std::wstring_view USE_SERVICE_PROPERTY_NAME = L"UseService"; 118 119 class MouseWithoutBorders : public PowertoyModuleIface 120 { 121 std::wstring app_name; 122 std::wstring app_key; 123 124 private: 125 bool m_enabled = false; 126 bool run_in_service_mode = false; 127 PROCESS_INFORMATION p_info = {}; 128 129 bool is_enabled_by_default() const override 130 { 131 return false; 132 } 133 134 bool is_process_running() 135 { 136 return WaitForSingleObject(p_info.hProcess, 0) == WAIT_TIMEOUT; 137 } 138 139 void launch_process() 140 { 141 Logger::trace(L"Launching PowerToys MouseWithoutBorders process"); 142 const std::wstring application_path = L"PowerToys.MouseWithoutBorders.exe"; 143 STARTUPINFO info = { sizeof(info) }; 144 std::wstring full_command_path = application_path; 145 if (run_in_service_mode) 146 { 147 full_command_path += L" "; 148 full_command_path += USE_SERVICE_PROPERTY_NAME; 149 } 150 151 if (!CreateProcessW(application_path.c_str(), full_command_path.data(), nullptr, nullptr, true, {}, nullptr, nullptr, &info, &p_info)) 152 { 153 DWORD error = GetLastError(); 154 std::wstring message = L"PowerToys MouseWithoutBorders failed to start with error: "; 155 message += std::to_wstring(error); 156 Logger::error(message); 157 } 158 159 Trace::MouseWithoutBorders::Activate(); 160 } 161 162 void unregister_service() 163 { 164 SC_HANDLE schSCManager = OpenSCManagerW(nullptr, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS); 165 166 SC_HANDLE hService = OpenServiceW(schSCManager, SERVICE_NAME, SERVICE_STOP | DELETE); 167 if (!hService) 168 { 169 Logger::error("Failed to open MWB service"); 170 return; 171 } 172 173 SERVICE_STATUS ss; 174 if (ControlService(hService, SERVICE_CONTROL_STOP, &ss)) 175 { 176 Sleep(1000); 177 for (int i = 0; i < 5; ++i) 178 { 179 while (QueryServiceStatus(hService, &ss)) 180 { 181 if (ss.dwCurrentState == SERVICE_STOP_PENDING) 182 { 183 Sleep(1000); 184 } 185 else 186 { 187 goto outer; 188 } 189 } 190 } 191 } 192 193 outer: 194 BOOL deleteResult = DeleteService(hService); 195 CloseServiceHandle(hService); 196 197 if (!deleteResult) 198 { 199 Logger::error("Failed to delete MWB service"); 200 return; 201 } 202 203 Trace::MouseWithoutBorders::ToggleServiceRegistration(false); 204 } 205 206 void 207 register_service() 208 { 209 SC_HANDLE schSCManager = OpenSCManagerW(nullptr, SERVICES_ACTIVE_DATABASE, SC_MANAGER_ALL_ACCESS); 210 if (schSCManager == nullptr) 211 { 212 Logger::error(L"Couldn't open sc manager"); 213 return; 214 } 215 216 const auto closeSCM = wil::scope_exit([&] { 217 CloseServiceHandle(schSCManager); 218 }); 219 220 SC_HANDLE schService = OpenServiceW(schSCManager, SERVICE_NAME, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG | SERVICE_CHANGE_CONFIG); 221 222 const auto closeService = wil::scope_exit([&] { 223 CloseServiceHandle(schService); 224 }); 225 226 const auto servicePath = get_module_folderpath(g_hInst_MouseWithoutBorders) + L"/PowerToys.MouseWithoutBordersService.exe"; 227 228 // Check that the service doesn't exist already and is not disabled 229 DWORD bytesNeeded; 230 LPQUERY_SERVICE_CONFIGW pServiceConfig = nullptr; 231 if (!QueryServiceConfigW(schService, nullptr, 0, &bytesNeeded)) 232 { 233 if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) 234 { 235 pServiceConfig = static_cast<LPQUERY_SERVICE_CONFIGW>(LocalAlloc(LMEM_FIXED, bytesNeeded)); 236 if (!QueryServiceConfigW(schService, pServiceConfig, bytesNeeded, &bytesNeeded)) 237 { 238 LocalFree(pServiceConfig); 239 pServiceConfig = nullptr; 240 CloseServiceHandle(schService); 241 } 242 } 243 } 244 245 // Pass local app data of the current user to the service 246 wil::unique_cotaskmem_string cLocalAppPath; 247 winrt::check_hresult(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0, nullptr, &cLocalAppPath)); 248 249 std::wstring localAppPath{ cLocalAppPath.get() }; 250 std::wstring binaryWithArgsPath = L"\""; 251 binaryWithArgsPath += servicePath; 252 binaryWithArgsPath += L"\" "; 253 binaryWithArgsPath += escapeDoubleQuotes(localAppPath); 254 255 bool alreadyRegistered = false; 256 bool isServicePathCorrect = true; 257 if (pServiceConfig) 258 { 259 std::wstring_view existingServicePath{ pServiceConfig->lpBinaryPathName }; 260 alreadyRegistered = true; 261 isServicePathCorrect = (existingServicePath == binaryWithArgsPath); 262 if (isServicePathCorrect) 263 { 264 Logger::warn(L"The service path is not correct. Current: {} Expected: {}", existingServicePath, binaryWithArgsPath); 265 } 266 267 if (alreadyRegistered && pServiceConfig->dwStartType == SERVICE_DISABLED) 268 { 269 if (!ChangeServiceConfigW(schService, 270 SERVICE_NO_CHANGE, 271 SERVICE_DEMAND_START, 272 SERVICE_NO_CHANGE, 273 nullptr, 274 nullptr, 275 nullptr, 276 nullptr, 277 nullptr, 278 nullptr, 279 nullptr)) 280 { 281 const bool markedForDelete = GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE; 282 // We cannot remove the mark for deletion from the service. 283 if (markedForDelete) 284 { 285 alreadyRegistered = false; 286 CloseServiceHandle(schService); 287 } 288 } 289 } 290 LocalFree(pServiceConfig); 291 } 292 293 if (alreadyRegistered) 294 { 295 if (!isServicePathCorrect) 296 { 297 if (!ChangeServiceConfigW(schService, 298 SERVICE_NO_CHANGE, 299 SERVICE_NO_CHANGE, 300 SERVICE_NO_CHANGE, 301 binaryWithArgsPath.c_str(), 302 nullptr, 303 nullptr, 304 nullptr, 305 nullptr, 306 nullptr, 307 nullptr)) 308 { 309 Logger::error(L"Failed to update the service's path. ERROR: {}", GetLastError()); 310 } 311 else 312 { 313 Logger::info(L"Updated the service's path."); 314 } 315 } 316 return; 317 } 318 319 schService = CreateServiceW( 320 schSCManager, 321 SERVICE_NAME, 322 SERVICE_NAME, 323 SERVICE_ALL_ACCESS, 324 SERVICE_WIN32_OWN_PROCESS, 325 SERVICE_DEMAND_START, 326 SERVICE_ERROR_NORMAL, 327 binaryWithArgsPath.c_str(), 328 nullptr, 329 nullptr, 330 nullptr, 331 nullptr, 332 nullptr); 333 334 if (schService == nullptr) 335 { 336 Logger::error(L"Failed to create service"); 337 return; 338 } 339 340 // Set up the security descriptor to allow non-elevated users to start the service 341 PSECURITY_DESCRIPTOR pSD = nullptr; 342 ULONG szSD = 0; 343 std::wstring securityDescriptor = generateSecurityDescriptor(GetCurrentUserSid()); 344 345 if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( 346 securityDescriptor.c_str(), 347 SDDL_REVISION_1, 348 &pSD, 349 &szSD)) 350 { 351 Logger::error(L"Failed to convert security descriptor string"); 352 CloseServiceHandle(schService); 353 return; 354 } 355 356 if (!SetServiceObjectSecurity(schService, DACL_SECURITY_INFORMATION, pSD)) 357 { 358 Logger::error("Failed to set service object security"); 359 } 360 361 LocalFree(pSD); 362 CloseServiceHandle(schService); 363 } 364 365 void update_state_from_settings(const PowerToysSettings::PowerToyValues& values) 366 { 367 bool new_run_in_service_mode = values.get_bool_value(USE_SERVICE_PROPERTY_NAME).value_or(false); 368 if (powertoys_gpo::getConfiguredMwbAllowServiceModeValue() == powertoys_gpo::gpo_rule_configured_disabled) 369 { 370 new_run_in_service_mode = false; 371 } 372 373 if (new_run_in_service_mode != run_in_service_mode) 374 { 375 run_in_service_mode = new_run_in_service_mode; 376 377 shutdown_processes(); 378 379 if (new_run_in_service_mode) 380 { 381 register_service(); 382 } 383 // Wait until Settings -> MWB IPC Shutdown() call is completed 384 else 385 { 386 const auto ps = getProcessHandlesByName(L"PowerToys.MouseWithoutBorders.exe", PROCESS_QUERY_LIMITED_INFORMATION); 387 for (const auto& p : ps) 388 { 389 DWORD status = STILL_ACTIVE; 390 do 391 { 392 GetExitCodeProcess(p.get(), &status); 393 } while (status == STILL_ACTIVE); 394 } 395 396 Sleep(1000); 397 } 398 399 if (m_enabled) 400 { 401 launch_process(); 402 } 403 404 Trace::MouseWithoutBorders::ToggleServiceRegistration(new_run_in_service_mode); 405 } 406 } 407 408 public: 409 MouseWithoutBorders() 410 { 411 app_name = L"MouseWithoutBorders"; 412 app_key = app_name; 413 414 LoggerHelpers::init_logger(app_key, L"ModuleInterface", LogSettings::mouseWithoutBordersLoggerName); 415 416 std::filesystem::path oldLogPath(PTSettingsHelper::get_module_save_folder_location(app_key)); 417 oldLogPath.append("LogsModuleInterface"); 418 LoggerHelpers::delete_old_log_folder(oldLogPath); 419 420 try 421 { 422 PowerToysSettings::PowerToyValues values = 423 PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME); 424 update_state_from_settings(values); 425 } 426 catch (std::exception&) 427 { 428 // Initial start/ 429 } 430 }; 431 432 // Return the configured status for the gpo policy for the module 433 virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override 434 { 435 return powertoys_gpo::getConfiguredMouseWithoutBordersEnabledValue(); 436 } 437 438 void shutdown_process(HANDLE handle) 439 { 440 auto cb = [](HWND hwnd, LPARAM lParam) -> BOOL { 441 DWORD processId; 442 GetWindowThreadProcessId(hwnd, &processId); 443 444 if (processId == lParam) 445 { 446 PostMessageW(hwnd, WM_CLOSE, 0, 0); 447 return FALSE; 448 } 449 450 return TRUE; 451 }; 452 453 DWORD processId = GetProcessId(handle); 454 EnumWindows(cb, processId); 455 456 DWORD waitResult = WaitForSingleObject(handle, 3000); 457 if (waitResult != WAIT_OBJECT_0) 458 { 459 TerminateProcess(handle, 0); 460 } 461 } 462 463 void shutdown_processes() 464 { 465 const auto services = getProcessHandlesByName(L"PowerToys.MouseWithoutBordersService.exe", PROCESS_TERMINATE); 466 for (const auto& svc : services) 467 TerminateProcess(svc.get(), 0); 468 wil::unique_process_handle s; 469 470 std::array<std::wstring_view, 2> processes_names = { L"PowerToys.MouseWithoutBorders.exe", 471 L"PowerToys.MouseWithoutBordersHelper.exe" }; 472 473 for (const auto process : processes_names) 474 { 475 const auto apps = getProcessHandlesByName(process, PROCESS_TERMINATE); 476 for (const auto& app : apps) 477 shutdown_process(app.get()); 478 } 479 } 480 481 virtual void destroy() override 482 { 483 shutdown_processes(); 484 TerminateProcess(p_info.hProcess, 1); 485 delete this; 486 } 487 488 virtual const wchar_t* get_name() override 489 { 490 return MODULE_NAME; 491 } 492 493 virtual bool get_config(wchar_t* buffer, int* buffer_size) override 494 { 495 HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase); 496 497 PowerToysSettings::Settings settings(hinstance, get_name()); 498 settings.set_description(MODULE_DESC); 499 500 return settings.serialize_to_buffer(buffer, buffer_size); 501 } 502 503 virtual const wchar_t* get_key() override 504 { 505 return app_key.c_str(); 506 } 507 508 virtual void set_config(const wchar_t* config) override 509 { 510 try 511 { 512 // Parse the input JSON string. 513 PowerToysSettings::PowerToyValues values = 514 PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); 515 516 update_state_from_settings(values); 517 // If you don't need to do any custom processing of the settings, proceed 518 // to persists the values. 519 values.save_to_settings_file(); 520 } 521 catch (std::exception&) 522 { 523 // Improper JSON. 524 } 525 } 526 527 virtual void enable() 528 { 529 Trace::MouseWithoutBorders::Enable(true); 530 531 launch_process(); 532 533 m_enabled = true; 534 }; 535 536 virtual void disable() 537 { 538 if (m_enabled) 539 { 540 Trace::MouseWithoutBorders::Enable(false); 541 Logger::trace(L"Disabling MouseWithoutBorders..."); 542 543 Logger::trace(L"Signaled exit event for PowerToys MouseWithoutBorders."); 544 TerminateProcess(p_info.hProcess, 1); 545 546 shutdown_processes(); 547 548 CloseHandle(p_info.hProcess); 549 } 550 551 m_enabled = false; 552 } 553 554 virtual bool is_enabled() override 555 { 556 return m_enabled; 557 } 558 559 virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override 560 { 561 constexpr size_t num_hotkeys = 4; // We have 4 hotkeys 562 563 if (hotkeys && buffer_size >= num_hotkeys) 564 { 565 PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME); 566 567 // Cache the raw JSON object to avoid multiple parsing 568 json::JsonObject root_json = values.get_raw_json(); 569 json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{}); 570 571 size_t hotkey_index = 0; 572 573 // Helper lambda to extract hotkey from JSON properties 574 auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey { 575 if (properties_json.HasKey(property_name)) 576 { 577 try 578 { 579 json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name); 580 581 // Extract hotkey properties directly from JSON 582 bool win = hotkey_json.GetNamedBoolean(L"win", false); 583 bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false); 584 bool alt = hotkey_json.GetNamedBoolean(L"alt", false); 585 bool shift = hotkey_json.GetNamedBoolean(L"shift", false); 586 unsigned char key = static_cast<unsigned char>( 587 hotkey_json.GetNamedNumber(L"code", 0)); 588 589 return { win, ctrl, shift, alt, key }; 590 } 591 catch (...) 592 { 593 // If parsing individual hotkey fails, use defaults 594 return { false, false, false, false, 0 }; 595 } 596 } 597 else 598 { 599 // Property doesn't exist, use defaults 600 return { false, false, false, false, 0 }; 601 } 602 }; 603 604 // Extract all hotkeys using the optimized helper 605 hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse 606 hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine 607 hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs 608 hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect 609 } 610 611 return num_hotkeys; 612 } 613 614 void launch_add_firewall_process() 615 { 616 Logger::trace(L"Starting Process to add firewall rule"); 617 618 std::wstring executable_path = get_module_folderpath(); 619 executable_path.append(L"\\PowerToys.MouseWithoutBorders.exe"); 620 621 std::wstring executable_args = L""; 622 executable_args.append(L"/S /c \""); 623 executable_args.append(L"echo \"Deleting existing inbound firewall rules for PowerToys.MouseWithoutBorders.exe\""); 624 executable_args.append(L" & netsh advfirewall firewall delete rule dir=in name=all program=\""); 625 executable_args.append(executable_path); 626 executable_args.append(L"\" & echo \"Adding an inbound firewall rule for PowerToys.MouseWithoutBorders.exe\""); 627 executable_args.append(L" & netsh advfirewall firewall add rule name=\"PowerToys.MouseWithoutBorders\" dir=in action=allow program=\""); 628 executable_args.append(executable_path); 629 executable_args.append(L"\" enable=yes remoteip=any profile=any protocol=tcp & pause\""); 630 631 SHELLEXECUTEINFOW sei{ sizeof(sei) }; 632 sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI }; 633 sei.lpFile = L"cmd.exe"; 634 sei.nShow = SW_SHOWNORMAL; 635 sei.lpParameters = executable_args.data(); 636 sei.lpVerb = L"runas"; 637 638 if (ShellExecuteExW(&sei)) 639 { 640 Logger::trace("Successfully started the firewall rule adding process"); 641 } 642 else 643 { 644 Logger::error(L"The firewall rule adding process failed to start. {}", get_last_error_or_default(GetLastError())); 645 } 646 } 647 648 virtual void call_custom_action(const wchar_t* action) override 649 { 650 try 651 { 652 PowerToysSettings::CustomActionObject action_object = 653 PowerToysSettings::CustomActionObject::from_json_string(action); 654 655 if (action_object.get_name() == L"add_firewall") 656 { 657 launch_add_firewall_process(); 658 Trace::MouseWithoutBorders::AddFirewallRule(); 659 } 660 else if (action_object.get_name() == L"uninstall_service") 661 { 662 unregister_service(); 663 } 664 } 665 catch (std::exception&) 666 { 667 Logger::error(L"Failed to parse action. {}", action); 668 } 669 } 670 }; 671 672 extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() 673 { 674 return new MouseWithoutBorders(); 675 }