From cf2df815e3b60598b1b9b338c06f7803fd612eae Mon Sep 17 00:00:00 2001 From: berendi Date: Sat, 22 Mar 2025 18:26:28 +0100 Subject: [PATCH] Support starting OpenRGB as a Windows service Commits squashed, code style cleaned up, and manually rebased by Adam Honse --- OpenRGB.pro | 6 +- main.cpp | 345 ----------------- scripts/build-msi.sh | 8 +- startup/main_Linux_MacOS.cpp | 105 ++++++ startup/main_Windows.cpp | 699 +++++++++++++++++++++++++++++++++++ startup/startup.cpp | 134 +++++++ startup/startup.h | 12 + 7 files changed, 957 insertions(+), 352 deletions(-) delete mode 100644 main.cpp create mode 100644 startup/main_Linux_MacOS.cpp create mode 100644 startup/main_Windows.cpp create mode 100644 startup/startup.cpp create mode 100644 startup/startup.h diff --git a/OpenRGB.pro b/OpenRGB.pro index e361c6eb..5974d232 100644 --- a/OpenRGB.pro +++ b/OpenRGB.pro @@ -206,6 +206,7 @@ HEADERS += RGBController/RGBController_Dummy.h \ RGBController/RGBControllerKeyNames.h \ RGBController/RGBController_Network.h \ + startup/startup.h \ SOURCES += \ $$GUI_CPP \ @@ -240,7 +241,7 @@ SOURCES += dependencies/hueplusplus-1.2.0/src/UPnP.cpp \ dependencies/hueplusplus-1.2.0/src/Utils.cpp \ dependencies/hueplusplus-1.2.0/src/ZLLSensors.cpp \ - main.cpp \ + startup/startup.cpp \ cli.cpp \ dmiinfo/dmiinfo.cpp \ LogManager.cpp \ @@ -349,6 +350,7 @@ win32:SOURCES += SuspendResume/SuspendResume_Windows.cpp \ wmi/wmi.cpp \ AutoStart/AutoStart-Windows.cpp \ + startup/main_Windows.cpp \ win32:HEADERS += $$CONTROLLER_H_WINDOWS @@ -520,6 +522,7 @@ contains(QMAKE_PLATFORM, linux) { SPDAccessor/EE1004Accessor_Linux.cpp \ SPDAccessor/SPD5118Accessor_Linux.cpp \ SuspendResume/SuspendResume_Linux_FreeBSD.cpp \ + startup/main_Linux_MacOS.cpp \ #-------------------------------------------------------------------------------------------# # Set up install paths # @@ -710,6 +713,7 @@ macx { AutoStart/AutoStart-MacOS.cpp \ qt/macutils.mm \ SuspendResume/SuspendResume_MacOS.cpp \ + startup/main_Linux_MacOS.cpp \ SOURCES += $$CONTROLLER_CPP_MACOS diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 592609b5..00000000 --- a/main.cpp +++ /dev/null @@ -1,345 +0,0 @@ -/*---------------------------------------------------------*\ -| main.cpp | -| | -| Entry point for the OpenRGB application | -| | -| This file is part of the OpenRGB project | -| SPDX-License-Identifier: GPL-2.0-only | -\*---------------------------------------------------------*/ - -#include "ResourceManager.h" -#include "NetworkServer.h" -#include "LogManager.h" -#include "cli.h" - -#include -#include -#include -#include - -#ifdef _MACOSX_X86_X64 -#include "macUSPCIOAccess.h" -io_connect_t macUSPCIO_driver_connection; -#endif - -#include "OpenRGBDialog.h" - -#ifdef __APPLE__ -#include "macutils.h" -#endif - -#ifdef __linux__ -#include -#endif - -using namespace std::chrono_literals; - -/******************************************************************************************\ -* * -* InitializeTimerResolutionThreadFunction (Win32) * -* * -* On Windows, the default timer resolution is 15.6ms. For higher accuracy delays, * -* the timer resolution should be set to a shorter interval. The shortest interval * -* that can be set is 0.5ms. * -* * -\******************************************************************************************/ -#ifdef _WIN32 -void InitializeTimerResolutionThreadFunction() -{ - /*-----------------------------------------------------*\ - | NtSetTimerResolution function pointer type | - \*-----------------------------------------------------*/ - typedef unsigned int NTSTATUS; - typedef NTSTATUS (*NTSETTIMERRESOLUTION)(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution); - - /*-----------------------------------------------------*\ - | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 4, | - | isn't defined in Win10 headers | - \*-----------------------------------------------------*/ - PROCESS_POWER_THROTTLING_STATE PowerThrottlingState { PROCESS_POWER_THROTTLING_CURRENT_VERSION, 4, 0 }; - - ULONG CurrentResolution; - HMODULE NtDllHandle; - NTSETTIMERRESOLUTION NtSetTimerResolution; - - /*-----------------------------------------------------*\ - | Load ntdll.dll and get pointer to NtSetTimerResolution| - \*-----------------------------------------------------*/ - NtDllHandle = LoadLibrary("ntdll.dll"); - NtSetTimerResolution = (NTSETTIMERRESOLUTION)GetProcAddress(NtDllHandle, "NtSetTimerResolution"); - - /*-----------------------------------------------------*\ - | Windows 11 requires | - | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION | - \*-----------------------------------------------------*/ - SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &PowerThrottlingState, sizeof(PowerThrottlingState)); - - /*-----------------------------------------------------*\ - | Call NtSetTimerResolution to set timer resolution to | - | 0.5ms every 500ms | - \*-----------------------------------------------------*/ - while(1) - { - NtSetTimerResolution(5000, TRUE, &CurrentResolution); - - std::this_thread::sleep_for(500ms); - } -} -#endif - -void WaitWhileServerOnline(NetworkServer* srv) -{ - while (srv->GetOnline()) - { - std::this_thread::sleep_for(1s); - }; -} - -/******************************************************************************************\ -* * -* Install SMBus Driver WinRing0, If not already installed (Win32) * -* * -\******************************************************************************************/ -#ifdef _WIN32 -void InstallWinRing0() -{ - /*-----------------------------------------------------*\ - | Driver final location usually | - | C:\windows\system32\drivers\WinRing0x64.sys | - \*-----------------------------------------------------*/ - TCHAR winring0_install_location[MAX_PATH]; - uint system_path_length = GetSystemDirectory(winring0_install_location, MAX_PATH); - std::string winring0_filename = "WinRing0.sys"; - BOOL bIsWow64 = false; -#if _WIN64 - winring0_filename = "WinRing0x64.sys"; -#else - BOOL (*fnIsWow64Process)(HANDLE, PBOOL) = (BOOL (__cdecl *)(HANDLE, PBOOL))GetProcAddress(GetModuleHandle(TEXT("kernel32")),"IsWow64Process"); - if (fnIsWow64Process) - { - fnIsWow64Process(GetCurrentProcess(),&bIsWow64); - } - if(bIsWow64) - { - winring0_filename = "WinRing0x64.sys"; - } -#endif - std::strncat(winring0_install_location, "\\drivers\\", MAX_PATH - system_path_length - 1); - std::strncat(winring0_install_location, winring0_filename.c_str(), MAX_PATH - system_path_length - 10); - - std::string driver_name = winring0_filename.substr(0, winring0_filename.size() - 4); // driver name: WinRing0 or WinRing0x64 - SC_HANDLE manager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); - if (manager) - { - PVOID wow64_fsredirection_OldValue = NULL; - if(bIsWow64) - { - Wow64DisableWow64FsRedirection(&wow64_fsredirection_OldValue); - } - if(INVALID_FILE_ATTRIBUTES == GetFileAttributes(winring0_install_location) && GetLastError()==ERROR_FILE_NOT_FOUND) - { - char module_path_buffer[MAX_PATH]; - GetModuleFileNameA(NULL, module_path_buffer, MAX_PATH); - std::string::size_type exe_loc = std::string(module_path_buffer).find_last_of("\\/"); - std::string driver_source_path = std::string(module_path_buffer).substr(0, exe_loc + 1) + winring0_filename; - CopyFile(driver_source_path.c_str(), winring0_install_location, true); - } - if(bIsWow64) - { - Wow64RevertWow64FsRedirection(wow64_fsredirection_OldValue); - } - - SC_HANDLE service = OpenService(manager, driver_name.c_str(), SERVICE_ALL_ACCESS); - if(!service) - { - std::string service_sys_path = "System32\\Drivers\\" + winring0_filename; - service = CreateService(manager, - driver_name.c_str(), - driver_name.c_str(), - SERVICE_ALL_ACCESS, - SERVICE_KERNEL_DRIVER, - SERVICE_AUTO_START, - SERVICE_ERROR_NORMAL, - service_sys_path.c_str(), - NULL, - NULL, - NULL, - NULL, - NULL); - } - CloseServiceHandle(service); - CloseServiceHandle(manager); - } -} -#endif - -/******************************************************************************************\ -* * -* Linux signal handler * -* * -\******************************************************************************************/ -#ifdef __linux__ -void sigHandler(int s) -{ - std::signal(s, SIG_DFL); - qApp->quit(); -} -#endif - -/******************************************************************************************\ -* * -* main * -* * -* Main function. Detects busses and Aura controllers, then opens the main window * -* * -\******************************************************************************************/ - -int main(int argc, char* argv[]) -{ - int exitval = EXIT_SUCCESS; -#ifdef _WIN32 - /*-----------------------------------------------------*\ - | Windows only - Attach console output | - \*-----------------------------------------------------*/ - if (AttachConsole(ATTACH_PARENT_PROCESS)) - { - /*-------------------------------------------------*\ - | We are running under some terminal context; | - | otherwise leave the GUI and CRT alone | - \*-------------------------------------------------*/ - freopen("CONIN$", "r", stdin); - freopen("CONOUT$", "w", stdout); - freopen("CONOUT$", "w", stderr); - } - - /*-----------------------------------------------------*\ - | Windows only - Start timer resolution correction | - | thread | - \*-----------------------------------------------------*/ - std::thread * InitializeTimerResolutionThread; - InitializeTimerResolutionThread = new std::thread(InitializeTimerResolutionThreadFunction); - InitializeTimerResolutionThread->detach(); - - /*-----------------------------------------------------*\ - | Windows only - Install SMBus Driver WinRing0 | - \*-----------------------------------------------------*/ - InstallWinRing0(); -#endif - - /*-----------------------------------------------------*\ - | Mac x86/x64 only - Install SMBus Driver macUSPCIO | - \*-----------------------------------------------------*/ -#ifdef _MACOSX_X86_X64 - InitMacUSPCIODriver(); -#endif - - /*-----------------------------------------------------*\ - | Process command line arguments before detection | - \*-----------------------------------------------------*/ - unsigned int ret_flags = cli_pre_detection(argc, argv); - - ResourceManager::get()->Initialize( - !(ret_flags & RET_FLAG_NO_AUTO_CONNECT), - !(ret_flags & RET_FLAG_NO_DETECT), - ret_flags & RET_FLAG_START_SERVER, - ret_flags & RET_FLAG_CLI_POST_DETECTION); - - /*-----------------------------------------------------*\ - | If the command line parser indicates that the GUI | - | should run, or if there were no command line | - | arguments, start the GUI. | - \*-----------------------------------------------------*/ - if(ret_flags & RET_FLAG_START_GUI) - { - LOG_TRACE("[main] initializing GUI"); - - /*-------------------------------------------------*\ - | Enable high DPI scaling support | - \*-------------------------------------------------*/ - #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); - QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); - #endif - - /*-------------------------------------------------*\ - | Enable high DPI fractional scaling support on | - | Windows | - \*-------------------------------------------------*/ - #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && defined(Q_OS_WIN) - QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); - #endif - - QApplication a(argc, argv); - QGuiApplication::setDesktopFileName("org.openrgb.OpenRGB"); - LOG_TRACE("[main] QApplication created"); - - /*-------------------------------------------------*\ - | Main UI widget | - \*-------------------------------------------------*/ - OpenRGBDialog dlg; - LOG_TRACE("[main] Dialog created"); - - if(ret_flags & RET_FLAG_I2C_TOOLS) - { - dlg.AddI2CToolsPage(); - } - - if(ret_flags & RET_FLAG_START_MINIMIZED) - { -#ifdef _WIN32 - /*---------------------------------------------*\ - | Show the window always, even if it will | - | immediately be hidden. On Windows, events | - | are not delivered to nativeEventFilter (for | - | SuspendResume) until the window has been | - | shown once. | - \*---------------------------------------------*/ - dlg.showMinimized(); -#endif -#ifdef __APPLE__ - MacUtils::ToggleApplicationDocklessState(false); -#endif - dlg.hide(); - } - else - { - dlg.show(); - } - - LOG_TRACE("[main] Ready to exec() the dialog"); -#ifdef __linux__ - std::signal(SIGINT, sigHandler); - std::signal(SIGTERM, sigHandler); -#endif - exitval = a.exec(); - } - else - { - /*-------------------------------------------------*\ - | If no GUI is needed, we let the background | - | threads run as long as they need, but we need to | - | AT LEAST wait for initialization to finish | - \*-------------------------------------------------*/ - ResourceManager::get()->WaitForInitialization(); - - if(ret_flags & RET_FLAG_START_SERVER) - { - NetworkServer* server = ResourceManager::get()->GetServer(); - - if(!server->GetOnline()) - { - exitval = EXIT_FAILURE; - } - else - { - WaitWhileServerOnline(server); - } - } - } - ResourceManager::get()->Cleanup(); -#ifdef _MACOSX_X86_X64 - CloseMacUSPCIODriver(); -#endif - LOG_TRACE("OpenRGB finishing with exit code %d", exitval); - return exitval; -} diff --git a/scripts/build-msi.sh b/scripts/build-msi.sh index 4ad6798d..df535dd8 100755 --- a/scripts/build-msi.sh +++ b/scripts/build-msi.sh @@ -102,15 +102,9 @@ XML_FILE+=" InstallerVersion='200' Languages='1033' Compressed='yes' XML_FILE+=" \r\n" XML_FILE+=" VersionNT64\r\n" XML_FILE+=" \r\n" -XML_FILE+=" \r\n" -XML_FILE+=" \r\n" XML_FILE+=" \r\n" XML_FILE+=" \r\n" XML_FILE+=" \r\n" -XML_FILE+=" \r\n" -XML_FILE+=" NOT Installed\r\n" -XML_FILE+=" NOT Installed\r\n" -XML_FILE+=" \r\n" XML_FILE+=" \r\n" XML_FILE+=" \r\n" XML_FILE+=" \r\n" @@ -123,6 +117,8 @@ XML_FILE+=" \r\n" XML_FILE+=" \r\n" XML_FILE+=" \r\n" XML_FILE+="${FILES}\r\n" +XML_FILE+=" \r\n" +XML_FILE+=" \r\n" XML_FILE+=" \r\n" XML_FILE+="${DIRECTORIES}\r\n" XML_FILE+=" \r\n" diff --git a/startup/main_Linux_MacOS.cpp b/startup/main_Linux_MacOS.cpp new file mode 100644 index 00000000..36421679 --- /dev/null +++ b/startup/main_Linux_MacOS.cpp @@ -0,0 +1,105 @@ +/*---------------------------------------------------------*\ +| main_Linux_MacOS.cpp | +| | +| Entry point for the OpenRGB application | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include "cli.h" +#include "ResourceManager.h" +#include "NetworkServer.h" +#include "LogManager.h" +#include "startup.h" + +#ifdef _MACOSX_X86_X64 +#include "macUSPCIOAccess.h" +io_connect_t macUSPCIO_driver_connection; +#endif + +#ifdef __APPLE__ +#include "macutils.h" +#endif + +using namespace std::chrono_literals; + +/*---------------------------------------------------------*\ +| WaitWhileServerOnline | +| | +| Wait while NetworkServer is online and return only when | +| it has shut down | +\*---------------------------------------------------------*/ +void WaitWhileServerOnline(NetworkServer* srv) +{ + while(srv->GetOnline()) + { + std::this_thread::sleep_for(1s); + }; +} + +/*---------------------------------------------------------*\ +| main | +| | +| Entry point, calls the startup processing | +\*---------------------------------------------------------*/ +int main(int argc, char* argv[]) +{ + /*-----------------------------------------------------*\ + | Mac x86/x64 only - Install SMBus Driver macUSPCIO | + \*-----------------------------------------------------*/ +#ifdef _MACOSX_X86_X64 + InitMacUSPCIODriver(); +#endif + + /*-----------------------------------------------------*\ + | Perform CLI pre-detection processing to get return | + | flags | + \*-----------------------------------------------------*/ + unsigned int ret_flags = cli_pre_detection(argc, argv); + + /*-----------------------------------------------------*\ + | Initialize ResourceManager | + \*-----------------------------------------------------*/ + ResourceManager::get()->Initialize( + !(ret_flags & RET_FLAG_NO_AUTO_CONNECT), + !(ret_flags & RET_FLAG_NO_DETECT), + ret_flags & RET_FLAG_START_SERVER, + ret_flags & RET_FLAG_CLI_POST_DETECTION); + + /*-----------------------------------------------------*\ + | Perform application startup and run the application. | + | This call returns only when the GUI application is | + | closing or if not running the GUI. | + \*-----------------------------------------------------*/ + int exitval = startup(argc, argv, ret_flags); + + /*-----------------------------------------------------*\ + | If started in headless server mode, wait until server | + | shuts down before closing application. | + \*-----------------------------------------------------*/ + if((ret_flags & RET_FLAG_START_SERVER) && !(ret_flags & RET_FLAG_START_GUI)) + { + NetworkServer* server = ResourceManager::get()->GetServer(); + if(server) + { + WaitWhileServerOnline(server); + } + } + + /*-----------------------------------------------------*\ + | Perform ResourceManager cleanup before exiting | + \*-----------------------------------------------------*/ + ResourceManager::get()->Cleanup(); + + LOG_TRACE("OpenRGB finishing with exit code %d", exitval); + + /*-----------------------------------------------------*\ + | Mac x86/x64 only - Uninstall SMBus Driver macUSPCIO | + \*-----------------------------------------------------*/ +#ifdef _MACOSX_X86_X64 + CloseMacUSPCIODriver(); +#endif + + return exitval; +} diff --git a/startup/main_Windows.cpp b/startup/main_Windows.cpp new file mode 100644 index 00000000..a812a679 --- /dev/null +++ b/startup/main_Windows.cpp @@ -0,0 +1,699 @@ +/*---------------------------------------------------------*\ +| main_Windows.cpp | +| | +| Entry point for the OpenRGB application on Windows | +| Differentiate between service and application startup | +| Housekeeping for Windows service management | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include +#include +#include +#include +#include + +#include + +#include "cli.h" +#include "startup.h" +#include "LogManager.h" +#include "NetworkServer.h" +#include "ResourceManager.h" + +using namespace std::chrono_literals; + +static int common_main(int argc, char* argv[]); + +static void WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv); +static void ReportServiceStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint); + +static char service_name[] = "OpenRGB"; +static SERVICE_TABLE_ENTRY service_dispatch_table[] = { { service_name, ServiceMain }, { NULL, NULL } }; +static DWORD service_checkpoint = 1; +static SERVICE_STATUS_HANDLE service_status_handle; +static SERVICE_STATUS service_status; + +static bool started_as_service; +static volatile bool service_stop_requested; +static bool have_console; + +static std::mutex service_status_mutex; + +/*---------------------------------------------------------*\ +| Detection progress goes from 0 to 100 twice, passing an | +| estimate based on that confuses the service manager | +\*---------------------------------------------------------*/ +static int detection_pass; +static unsigned int lastpercent = 101; + +/*---------------------------------------------------------*\ +| ServiceStartupProgress | +| | +| Report detection progress when running as a service | +\*---------------------------------------------------------*/ +static void ServiceStartupProgress(void*) +{ + unsigned int percent = ResourceManager::get()->GetDetectionPercent(); + unsigned int estimate; + + percent = std::clamp(percent, 0u, 100u); + + if(lastpercent > percent) + { + detection_pass += 1; + } + + lastpercent = percent; + + switch(detection_pass) + { + case 0: + percent = 0; + break; + case 1: + percent = percent * 4 / 5; + break; + case 2: + percent = percent / 5 + 80; + break; + default: + percent = 100; + break; + } + + estimate = (100 - percent) / 5 + 10; + + ReportServiceStatus(SERVICE_START_PENDING, NO_ERROR, estimate * 1000); +} + +/*---------------------------------------------------------*\ +| ReportServiceStatus | +| | +| As of writing this (24H2) there are 7 possible service | +| states: | +| SERVICE_START_PENDING, SERVICE_RUNNING, | +| SERVICE_STOP_PENDING, SERVICE_STOPPED, | +| SERVICE_PAUSE_PENDING, SERVICE_PAUSED, | +| SERVICE_CONTINUE_PENDING | +| | +| We don't accept pause requests, so only start_pending | +| through stopped are possible. | +| | +| Control requests can arrive asynchronously so we take | +| some precautions here not to report an out of order | +| status. | +\*---------------------------------------------------------*/ +static void ReportServiceStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, DWORD dwWaitHint) +{ + static DWORD last_state = SERVICE_START_PENDING; + + LOG_TRACE("ReportServiceStatus(%lu, %lu, %lu), PID %lu Thread %lu", dwCurrentState, dwWin32ExitCode, dwWaitHint, GetCurrentProcessId(), GetCurrentThreadId()); + + service_status_mutex.lock(); + + switch(last_state) + { + case SERVICE_RUNNING: + /*---------------------------------------------*\ + | Don't go back to SERVICE_START_PENDING | + \*---------------------------------------------*/ + if(dwCurrentState == SERVICE_START_PENDING) + { + service_status_mutex.unlock(); + return; + } + break; + + case SERVICE_STOP_PENDING: + /*---------------------------------------------*\ + | Don't go back to starting or running | + \*---------------------------------------------*/ + if((dwCurrentState != SERVICE_STOP_PENDING) && (dwCurrentState != SERVICE_STOPPED)) + { + service_status_mutex.unlock(); + return; + } + break; + + case SERVICE_STOPPED: + /*---------------------------------------------*\ + | Don't call SetServiceStatus anymore after | + | SERVICE_STOPPED has been reported | + \*---------------------------------------------*/ + service_status_mutex.unlock(); + return; + } + + switch(dwCurrentState) + { + case SERVICE_START_PENDING: + case SERVICE_STOP_PENDING: + service_status.dwControlsAccepted = 0; + service_status.dwCheckPoint = service_checkpoint; + service_checkpoint += 1; + break; + + case SERVICE_RUNNING: + case SERVICE_STOPPED: + service_status.dwCheckPoint = 0; + service_status.dwControlsAccepted = + // SERVICE_ACCEPT_POWEREVENT | + SERVICE_ACCEPT_STOP | + SERVICE_ACCEPT_PRESHUTDOWN; + break; + + default: + /*---------------------------------------------*\ + | Shouldn't happen | + \*---------------------------------------------*/ + service_status_mutex.unlock(); + return; + } + + last_state = dwCurrentState; + service_status.dwCurrentState = dwCurrentState; + service_status.dwWin32ExitCode = dwWin32ExitCode; + service_status.dwWaitHint = dwWaitHint; + + SetServiceStatus(service_status_handle, &service_status); + + service_status_mutex.unlock(); + + return; +} + +/*---------------------------------------------------------*\ +| ServiceControlHandler | +| | +| Handler function to register with | +| RegisterServiceControlHandlerEx, to handle service | +| control operations (stop/shutdown). | +\*---------------------------------------------------------*/ +static DWORD WINAPI ServiceControlHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) +{ + DWORD retval = NO_ERROR; + + LOG_TRACE("ServiceControlHandler(%lu, %lu, %p, %p) called, PID %lu Thread %lu", dwControl, dwEventType, lpEventData, lpContext, GetCurrentProcessId(), GetCurrentThreadId()); + + switch(dwControl) + { + /*-------------------------------------------------*\ + | TODO either this or | + | RegisterSuspendResumeNotification somewhere | + \*-------------------------------------------------*/ + // case SERVICE_CONTROL_POWEREVENT: + case SERVICE_CONTROL_INTERROGATE: + /*---------------------------------------------*\ + | Should do nothing but return NO_ERROR | + \*---------------------------------------------*/ + break; + + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_PRESHUTDOWN: + ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 10000); + service_stop_requested = true; + break; + + default: + retval = ERROR_CALL_NOT_IMPLEMENTED; + break; + } + + return retval; +} + +/*---------------------------------------------------------*\ +| LogEvent | +| | +| Log an event to the service log when running as a | +| service. Only used for startup and shutdown events. | +\*---------------------------------------------------------*/ +static void LogEvent(const char *fmt, ...) +{ + va_list args1; + va_list args2; + + /*-----------------------------------------------------*\ + | Handle variable arguments | + \*-----------------------------------------------------*/ + va_start(args1, fmt); + va_copy(args2, args1); + + /*-----------------------------------------------------*\ + | Determine string length and allocate buffer | + \*-----------------------------------------------------*/ + int len = vsnprintf(NULL, 0, fmt, args1); + char * buf = (char *)malloc(len + 1); + + /*-----------------------------------------------------*\ + | Fill the string buffer | + \*-----------------------------------------------------*/ + va_end(args1); + vsnprintf(buf, len + 1, fmt, args2); + va_end(args2); + + /*-----------------------------------------------------*\ + | If we somehow ended up here with a working console | + | then just print it | + \*-----------------------------------------------------*/ + if(have_console) + { + printf("%s\n", buf); + free(buf); + return; + } + + /*-----------------------------------------------------*\ + | Otherwise, register event source with our service | + | name and report event to the event log | + \*-----------------------------------------------------*/ + HANDLE event_source = RegisterEventSource(NULL, service_name); + + if(event_source) + { + LPCSTR strings[2] = { service_name, buf }; + ReportEvent( + event_source, + EVENTLOG_ERROR_TYPE, + 0, + 1, // error # + NULL, + 2, + 0, + strings, + NULL); + DeregisterEventSource(event_source); + } + else + { + /*-------------------------------------------------*\ + | No console, no logmanager, no eventlog, no more | + | ideas. Sometimes it's OK to give up. | + \*-------------------------------------------------*/ + } + + /*-----------------------------------------------------*\ + | Free the buffer | + \*-----------------------------------------------------*/ + free(buf); +} + +/*---------------------------------------------------------*\ +| LogError | +| | +| Log an error event when running as a service | +\*---------------------------------------------------------*/ +static void LogError(const char *fn, DWORD lasterror) +{ + std::string message = std::system_category().message(lasterror); + LogEvent("%s failed with error code %lu \"%s\"", fn, lasterror, message.c_str()); +} + +/*---------------------------------------------------------*\ +| ServiceMain | +| | +| Figures out whether we are started as a service, calls | +| common_main one way or another. | +\*---------------------------------------------------------*/ +static void WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) +{ + /*-----------------------------------------------------*\ + | Initialize service status | + \*-----------------------------------------------------*/ + service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + service_status.dwServiceSpecificExitCode = 0; + + started_as_service = true; + service_status_handle = RegisterServiceCtrlHandlerEx(service_name, ServiceControlHandler, NULL); + + /*-----------------------------------------------------*\ + | Exit if service status handle is invalid | + \*-----------------------------------------------------*/ + if(service_status_handle == 0) + { + LogError("RegisterServiceCtrlHandlerEx", GetLastError()); + return; + } + + /*-----------------------------------------------------*\ + | Report service status start pending | + \*-----------------------------------------------------*/ + ReportServiceStatus(SERVICE_START_PENDING, NO_ERROR, 30000); + + /*-----------------------------------------------------*\ + | Perform common main processing | + \*-----------------------------------------------------*/ + int exitval = common_main(dwArgc, lpszArgv); + + /*-----------------------------------------------------*\ + | Log if exit was unsuccessful | + \*-----------------------------------------------------*/ + if(exitval != EXIT_SUCCESS) + { + LogEvent("OpenRGB finishing with exit code %d", exitval); + } + + /*-----------------------------------------------------*\ + | Report service status stopped | + \*-----------------------------------------------------*/ + ReportServiceStatus(SERVICE_STOPPED, NO_ERROR, 0); +} + +/*---------------------------------------------------------*\ +| main | +| | +| Entry point, checks if started as a service and then | +| calls the common main processing | +\*---------------------------------------------------------*/ +int main(int argc, char* argv[]) +{ + started_as_service = false; + + /*-----------------------------------------------------*\ + | This will call ServiceMain() if we are started as a | + | service | + \*-----------------------------------------------------*/ + bool sscd = StartServiceCtrlDispatcher(service_dispatch_table); + + /*-----------------------------------------------------*\ + | ... returns a specific error code otherwise | + \*-----------------------------------------------------*/ + DWORD lasterror = GetLastError(); + + if(sscd) + { + if(started_as_service) + { + /*---------------------------------------------*\ + | StartServiceCtrlDispatcher reported success, | + | ServiceMain was reached. Don't log anything, | + | it's the normal exit path when a service is | + | stopped. | + \*---------------------------------------------*/ + return EXIT_SUCCESS; + } + else + { + /*---------------------------------------------*\ + | Should not happen | + \*---------------------------------------------*/ + LogEvent("StartServiceCtrlDispatcher returned true, but ServiceMain has not been reached."); + return EXIT_FAILURE; + } + } + + /*-----------------------------------------------------*\ + | StartServiceCtrlDispatcher reported failure. | + | First, check that the error isn't the one returned | + | when it's called from an app | + \*-----------------------------------------------------*/ + if(lasterror != ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) + { + LogError("StartServiceCtrlDispatcher", lasterror); + return EXIT_FAILURE; + } + + /*-----------------------------------------------------*\ + | Now we are quite sure we are not started as a service | + \*-----------------------------------------------------*/ + if(started_as_service) + { + /*-------------------------------------------------*\ + | ... so this should not happen | + \*-------------------------------------------------*/ + LogEvent("StartServiceCtrlDispatcher returned false, but ServiceMain has been reached."); + return EXIT_FAILURE; + } + + /*-----------------------------------------------------*\ + | Code that needs to be run at startup when we are | + | started as an app and not as a service goes here. | + \*-----------------------------------------------------*/ + + /*-----------------------------------------------------*\ + | Attach console output | + \*-----------------------------------------------------*/ + if(AttachConsole(ATTACH_PARENT_PROCESS)) + { + /*-------------------------------------------------*\ + | We are running under some terminal context; | + | otherwise leave the GUI and CRT alone | + \*-------------------------------------------------*/ + freopen("CONIN$", "r", stdin); + freopen("CONOUT$", "w", stdout); + freopen("CONOUT$", "w", stderr); + have_console = true; + } + + return common_main(argc, argv); +} + +/*---------------------------------------------------------*\ +| InitializeTimerResolutionThreadFunction (Win32) | +| | +| On Windows, the default timer resolution is 15.6ms. | +| For higher accuracy delays, the timer resolution should | +| be set to a shorter interval. The shortest interval | +| that can be set is 0.5ms. | +\*---------------------------------------------------------*/ +void InitializeTimerResolutionThreadFunction() +{ + /*-----------------------------------------------------*\ + | NtSetTimerResolution function pointer type | + \*-----------------------------------------------------*/ + typedef unsigned int NTSTATUS; + typedef NTSTATUS (*NTSETTIMERRESOLUTION)(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution); + + /*-----------------------------------------------------*\ + | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION = 4, | + | isn't defined in Win10 headers | + \*-----------------------------------------------------*/ + PROCESS_POWER_THROTTLING_STATE PowerThrottlingState { PROCESS_POWER_THROTTLING_CURRENT_VERSION, 4, 0 }; + + ULONG CurrentResolution; + HMODULE NtDllHandle; + NTSETTIMERRESOLUTION NtSetTimerResolution; + + /*-----------------------------------------------------*\ + | Load ntdll.dll and get pointer to NtSetTimerResolution| + \*-----------------------------------------------------*/ + NtDllHandle = LoadLibrary("ntdll.dll"); + NtSetTimerResolution = (NTSETTIMERRESOLUTION)GetProcAddress(NtDllHandle, "NtSetTimerResolution"); + + /*-----------------------------------------------------*\ + | Windows 11 requires | + | PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION | + \*-----------------------------------------------------*/ + SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &PowerThrottlingState, sizeof(PowerThrottlingState)); + + /*-----------------------------------------------------*\ + | Call NtSetTimerResolution to set timer resolution to | + | 0.5ms every 500ms | + \*-----------------------------------------------------*/ + while(1) + { + NtSetTimerResolution(5000, TRUE, &CurrentResolution); + + std::this_thread::sleep_for(500ms); + } +} + +/*---------------------------------------------------------*\ +| WaitWhileServerOnline | +| | +| Wait while NetworkServer is online and return only when | +| it has shut down | +\*---------------------------------------------------------*/ +static void WaitWhileServerOnline(NetworkServer* srv) +{ + while(srv->GetOnline()) + { + std::this_thread::sleep_for(1s); + if(service_stop_requested) + { + srv->StopServer(); + } + }; +} + +/*---------------------------------------------------------*\ +| InstallWinRing0 | +| | +| Install SMBus Driver WinRing0, If not already installed | +| (Win32) | +\*---------------------------------------------------------*/ +void InstallWinRing0() +{ + /*-----------------------------------------------------*\ + | Driver final location usually | + | C:\windows\system32\drivers\WinRing0x64.sys | + \*-----------------------------------------------------*/ + TCHAR winring0_install_location[MAX_PATH]; + uint system_path_length = GetSystemDirectory(winring0_install_location, MAX_PATH); + std::string winring0_filename = "WinRing0.sys"; + BOOL bIsWow64 = false; +#if _WIN64 + winring0_filename = "WinRing0x64.sys"; +#else + BOOL (*fnIsWow64Process)(HANDLE, PBOOL) = (BOOL (__cdecl *)(HANDLE, PBOOL))GetProcAddress(GetModuleHandle(TEXT("kernel32")),"IsWow64Process"); + if (fnIsWow64Process) + { + fnIsWow64Process(GetCurrentProcess(),&bIsWow64); + } + if(bIsWow64) + { + winring0_filename = "WinRing0x64.sys"; + } +#endif + std::strncat(winring0_install_location, "\\drivers\\", MAX_PATH - system_path_length - 1); + std::strncat(winring0_install_location, winring0_filename.c_str(), MAX_PATH - system_path_length - 10); + + std::string driver_name = winring0_filename.substr(0, winring0_filename.size() - 4); // driver name: WinRing0 or WinRing0x64 + SC_HANDLE manager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); + if (manager) + { + PVOID wow64_fsredirection_OldValue = NULL; + if(bIsWow64) + { + Wow64DisableWow64FsRedirection(&wow64_fsredirection_OldValue); + } + if(INVALID_FILE_ATTRIBUTES == GetFileAttributes(winring0_install_location) && GetLastError()==ERROR_FILE_NOT_FOUND) + { + char module_path_buffer[MAX_PATH]; + GetModuleFileNameA(NULL, module_path_buffer, MAX_PATH); + std::string::size_type exe_loc = std::string(module_path_buffer).find_last_of("\\/"); + std::string driver_source_path = std::string(module_path_buffer).substr(0, exe_loc + 1) + winring0_filename; + CopyFile(driver_source_path.c_str(), winring0_install_location, true); + } + if(bIsWow64) + { + Wow64RevertWow64FsRedirection(wow64_fsredirection_OldValue); + } + + SC_HANDLE service = OpenService(manager, driver_name.c_str(), SERVICE_ALL_ACCESS); + if(!service) + { + std::string service_sys_path = "System32\\Drivers\\" + winring0_filename; + service = CreateService(manager, + driver_name.c_str(), + driver_name.c_str(), + SERVICE_ALL_ACCESS, + SERVICE_KERNEL_DRIVER, + SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, + service_sys_path.c_str(), + NULL, + NULL, + NULL, + NULL, + NULL); + } + CloseServiceHandle(service); + CloseServiceHandle(manager); + } +} + +/*---------------------------------------------------------*\ +| common_main | +| | +| Common entry functionality after determining whether we | +| were started as a service or not. | +\*---------------------------------------------------------*/ +static int common_main(int argc, char* argv[]) +{ + unsigned int ret_flags; + + if(started_as_service) + { + /*-------------------------------------------------*\ + | Passing command line arguments to a service is | + | difficult, can cause all kinds of trouble and | + | doesn't have a way to warn about them | + \*-------------------------------------------------*/ + ret_flags = RET_FLAG_START_SERVER | RET_FLAG_NO_AUTO_CONNECT; + } + else + { + /*-------------------------------------------------*\ + | Perform CLI pre-detection processing to get | + | return flags | + \*-------------------------------------------------*/ + ret_flags = cli_pre_detection(argc, argv); + } + + /*-----------------------------------------------------*\ + | Start timer resolution correction thread | + \*-----------------------------------------------------*/ + std::thread * InitializeTimerResolutionThread; + InitializeTimerResolutionThread = new std::thread(InitializeTimerResolutionThreadFunction); + InitializeTimerResolutionThread->detach(); + + /*-----------------------------------------------------*\ + | Install SMBus Driver WinRing0 | + \*-----------------------------------------------------*/ + InstallWinRing0(); + + /*-----------------------------------------------------*\ + | Initialize ResourceManager | + \*-----------------------------------------------------*/ + ResourceManager::get()->Initialize( + !(ret_flags & RET_FLAG_NO_AUTO_CONNECT), + !(ret_flags & RET_FLAG_NO_DETECT), + ret_flags & RET_FLAG_START_SERVER, + ret_flags & RET_FLAG_CLI_POST_DETECTION); + + /*-----------------------------------------------------*\ + | If running as a service, register the service startup | + | progress callback with ResourceManager to report | + | startup progress based on detection progress. | + \*-----------------------------------------------------*/ + if(started_as_service) + { + ResourceManager::get()->RegisterDetectionProgressCallback(ServiceStartupProgress, NULL); + } + + /*-----------------------------------------------------*\ + | Perform application startup and run the application. | + | This call returns only when the GUI application is | + | closing or if not running the GUI. | + \*-----------------------------------------------------*/ + int exitval = startup(argc, argv, ret_flags); + + /*-----------------------------------------------------*\ + | If running as a service, unregister the service | + | startup progress callback when shutting down. | + \*-----------------------------------------------------*/ + if(started_as_service) + { + ResourceManager::get()->UnregisterDetectionProgressCallback(ServiceStartupProgress, NULL); + } + + /*-----------------------------------------------------*\ + | If started in headless server mode, wait until server | + | shuts down before closing application. | + \*-----------------------------------------------------*/ + if((ret_flags & RET_FLAG_START_SERVER) && !(ret_flags & RET_FLAG_START_GUI)) + { + NetworkServer* server = ResourceManager::get()->GetServer(); + if(server && server->GetOnline()) + { + if(started_as_service) + { + ReportServiceStatus(SERVICE_RUNNING, NO_ERROR, 0); + } + WaitWhileServerOnline(server); + } + } + + /*-----------------------------------------------------*\ + | Perform ResourceManager cleanup before exiting | + \*-----------------------------------------------------*/ + ResourceManager::get()->Cleanup(); + + LOG_TRACE("OpenRGB finishing with exit code %d", exitval); + + return exitval; +} diff --git a/startup/startup.cpp b/startup/startup.cpp new file mode 100644 index 00000000..1002a3cd --- /dev/null +++ b/startup/startup.cpp @@ -0,0 +1,134 @@ +/*---------------------------------------------------------*\ +| startup.cpp | +| | +| Startup for the OpenRGB application | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#include "cli.h" +#include "ResourceManager.h" +#include "NetworkServer.h" +#include "startup.h" + +#include + +#include "OpenRGBDialog.h" + +#ifdef __APPLE__ +#include "macutils.h" +#endif + +/******************************************************************************************\ +* * +* startup * +* * +* Opens the main windows or starts the server * +* * +\******************************************************************************************/ +int startup(int argc, char* argv[], unsigned int ret_flags) +{ + /*-----------------------------------------------------*\ + | Initialize exit value, which will be returned on exit | + | in main() | + \*-----------------------------------------------------*/ + int exitval = EXIT_SUCCESS; + + /*-----------------------------------------------------*\ + | If the command line parser indicates that the GUI | + | should run, or if there were no command line | + | arguments, start the GUI. | + \*-----------------------------------------------------*/ + if(ret_flags & RET_FLAG_START_GUI) + { + LOG_TRACE("[main] initializing GUI"); + + /*-------------------------------------------------*\ + | Enable high DPI scaling support | + \*-------------------------------------------------*/ + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true); + + /*-------------------------------------------------*\ + | Enable high DPI fractional scaling support on | + | Windows | + \*-------------------------------------------------*/ + #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) && defined(Q_OS_WIN) + QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + #endif + + /*-------------------------------------------------*\ + | Create Qt application | + \*-------------------------------------------------*/ + QApplication a(argc, argv); + QGuiApplication::setDesktopFileName("org.openrgb.OpenRGB"); + LOG_TRACE("[startup] QApplication created"); + + /*-------------------------------------------------*\ + | Main UI widget | + \*-------------------------------------------------*/ + OpenRGBDialog dlg; + LOG_TRACE("[startup] Dialog created"); + + if(ret_flags & RET_FLAG_I2C_TOOLS) + { + dlg.AddI2CToolsPage(); + } + + dlg.AddClientTab(); + + if(ret_flags & RET_FLAG_START_MINIMIZED) + { +#ifdef _WIN32 + /*---------------------------------------------*\ + | Show the window always, even if it will | + | immediately be hidden. On Windows, events | + | are not delivered to nativeEventFilter (for | + | SuspendResume) until the window has been | + | shown once. | + | | + | TODO Try using | + | RegisterSuspendResumeNotification instead, | + | that should work in headless mode too | + \*---------------------------------------------*/ + dlg.showMinimized(); +#endif +#ifdef __APPLE__ + MacUtils::ToggleApplicationDocklessState(false); +#endif + dlg.hide(); + } + else + { + dlg.show(); + } + + LOG_TRACE("[main] Ready to exec() the dialog"); + exitval = a.exec(); + } + else + { + /*-------------------------------------------------*\ + | If no GUI is needed, we let the background | + | threads run as long as they need, but we need to | + | AT LEAST wait for initialization to finish | + \*-------------------------------------------------*/ + ResourceManager::get()->WaitForInitialization(); + + if(ret_flags & RET_FLAG_START_SERVER) + { + NetworkServer* server = ResourceManager::get()->GetServer(); + if(server) + { + exitval = !server->GetOnline(); + } + else + { + exitval = EXIT_FAILURE; + } + } + } + + return(exitval); +} diff --git a/startup/startup.h b/startup/startup.h new file mode 100644 index 00000000..5b941b40 --- /dev/null +++ b/startup/startup.h @@ -0,0 +1,12 @@ +/*---------------------------------------------------------*\ +| startup.h | +| | +| Startup for the OpenRGB application | +| | +| This file is part of the OpenRGB project | +| SPDX-License-Identifier: GPL-2.0-only | +\*---------------------------------------------------------*/ + +#pragma once + +int startup(int argc, char* argv[], unsigned int ret_flags);