LightSwitchService.cpp
1 #include <windows.h> 2 #include <tchar.h> 3 #include "ThemeScheduler.h" 4 #include "ThemeHelper.h" 5 #include <common/SettingsAPI/settings_objects.h> 6 #include <common/SettingsAPI/settings_helpers.h> 7 #include <stdio.h> 8 #include <string> 9 #include <LightSwitchSettings.h> 10 #include <common/utils/gpo.h> 11 #include <logger/logger_settings.h> 12 #include <logger/logger.h> 13 #include <utils/logger_helper.h> 14 #include "LightSwitchStateManager.h" 15 #include <LightSwitchUtils.h> 16 #include <NightLightRegistryObserver.h> 17 #include <trace.h> 18 19 SERVICE_STATUS g_ServiceStatus = {}; 20 SERVICE_STATUS_HANDLE g_StatusHandle = nullptr; 21 HANDLE g_ServiceStopEvent = nullptr; 22 static LightSwitchStateManager* g_stateManagerPtr = nullptr; 23 24 VOID WINAPI ServiceMain(DWORD argc, LPTSTR* argv); 25 VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl); 26 DWORD WINAPI ServiceWorkerThread(LPVOID lpParam); 27 void ApplyTheme(bool shouldBeLight); 28 29 // Entry point for the executable 30 int _tmain(int argc, TCHAR* argv[]) 31 { 32 DWORD parentPid = 0; 33 bool debug = false; 34 for (int i = 1; i < argc; ++i) 35 { 36 if (_tcscmp(argv[i], _T("--debug")) == 0) 37 debug = true; 38 else if (_tcscmp(argv[i], _T("--pid")) == 0 && i + 1 < argc) 39 parentPid = _tstoi(argv[++i]); 40 } 41 42 // Try to connect to SCM 43 wchar_t serviceName[] = L"LightSwitchService"; 44 SERVICE_TABLE_ENTRYW table[] = { { serviceName, ServiceMain }, { nullptr, nullptr } }; 45 46 LoggerHelpers::init_logger(L"LightSwitch", L"Service", LogSettings::lightSwitchLoggerName); 47 48 if (!StartServiceCtrlDispatcherW(table)) 49 { 50 DWORD err = GetLastError(); 51 if (err == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT) // not launched by SCM 52 { 53 g_ServiceStopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); 54 HANDLE hThread = CreateThread( 55 nullptr, 0, ServiceWorkerThread, reinterpret_cast<void*>(static_cast<ULONG_PTR>(parentPid)), 0, nullptr); 56 57 // Wait so the process stays alive 58 WaitForSingleObject(hThread, INFINITE); 59 CloseHandle(hThread); 60 CloseHandle(g_ServiceStopEvent); 61 return 0; 62 } 63 return static_cast<int>(err); 64 } 65 66 return 0; 67 } 68 69 // Called when the service is launched by Windows 70 VOID WINAPI ServiceMain(DWORD, LPTSTR*) 71 { 72 g_StatusHandle = RegisterServiceCtrlHandler(_T("LightSwitchService"), ServiceCtrlHandler); 73 if (!g_StatusHandle) 74 return; 75 76 g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; 77 g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN; 78 g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; 79 SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 80 81 g_ServiceStopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); 82 if (!g_ServiceStopEvent) 83 { 84 g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; 85 g_ServiceStatus.dwWin32ExitCode = GetLastError(); 86 SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 87 return; 88 } 89 90 SECURITY_ATTRIBUTES sa{ sizeof(sa) }; 91 sa.bInheritHandle = FALSE; 92 sa.lpSecurityDescriptor = nullptr; 93 94 g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; 95 SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 96 97 HANDLE hThread = CreateThread(nullptr, 0, ServiceWorkerThread, nullptr, 0, nullptr); 98 WaitForSingleObject(hThread, INFINITE); 99 CloseHandle(hThread); 100 101 CloseHandle(g_ServiceStopEvent); 102 g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; 103 g_ServiceStatus.dwWin32ExitCode = 0; 104 SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 105 } 106 107 VOID WINAPI ServiceCtrlHandler(DWORD dwCtrl) 108 { 109 switch (dwCtrl) 110 { 111 case SERVICE_CONTROL_STOP: 112 if (g_ServiceStatus.dwCurrentState != SERVICE_RUNNING) 113 break; 114 115 g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING; 116 SetServiceStatus(g_StatusHandle, &g_ServiceStatus); 117 118 // Signal the service to stop 119 Logger::info(L"[LightSwitchService] Stop requested, signaling worker thread to exit."); 120 SetEvent(g_ServiceStopEvent); 121 break; 122 123 default: 124 break; 125 } 126 } 127 128 void ApplyTheme(bool shouldBeLight) 129 { 130 const auto& s = LightSwitchSettings::settings(); 131 132 if (s.changeSystem) 133 { 134 bool isSystemCurrentlyLight = GetCurrentSystemTheme(); 135 if (shouldBeLight != isSystemCurrentlyLight) 136 { 137 SetSystemTheme(shouldBeLight); 138 Logger::info(L"[LightSwitchService] Changed system theme to {}.", shouldBeLight ? L"light" : L"dark"); 139 } 140 } 141 142 if (s.changeApps) 143 { 144 bool isAppsCurrentlyLight = GetCurrentAppsTheme(); 145 if (shouldBeLight != isAppsCurrentlyLight) 146 { 147 SetAppsTheme(shouldBeLight); 148 Logger::info(L"[LightSwitchService] Changed apps theme to {}.", shouldBeLight ? L"light" : L"dark"); 149 } 150 } 151 } 152 153 static void DetectAndHandleExternalThemeChange(LightSwitchStateManager& stateManager) 154 { 155 const auto& s = LightSwitchSettings::settings(); 156 if (s.scheduleMode == ScheduleMode::Off) 157 return; 158 159 SYSTEMTIME st; 160 GetLocalTime(&st); 161 int nowMinutes = st.wHour * 60 + st.wMinute; 162 163 // Compute effective boundaries (with offsets if needed) 164 int effectiveLight = s.lightTime; 165 int effectiveDark = s.darkTime; 166 167 if (s.scheduleMode == ScheduleMode::SunsetToSunrise) 168 { 169 effectiveLight = (s.lightTime + s.sunrise_offset) % 1440; 170 effectiveDark = (s.darkTime + s.sunset_offset) % 1440; 171 } 172 173 // Use shared helper (handles wraparound logic) 174 bool shouldBeLight = false; 175 if (s.scheduleMode == ScheduleMode::FollowNightLight) 176 { 177 shouldBeLight = !IsNightLightEnabled(); 178 } 179 else 180 { 181 shouldBeLight = ShouldBeLight(nowMinutes, effectiveLight, effectiveDark); 182 } 183 184 // Compare current system/apps theme 185 bool currentSystemLight = GetCurrentSystemTheme(); 186 bool currentAppsLight = GetCurrentAppsTheme(); 187 188 bool systemMismatch = s.changeSystem && (currentSystemLight != shouldBeLight); 189 bool appsMismatch = s.changeApps && (currentAppsLight != shouldBeLight); 190 191 // Trigger manual override only if mismatch and not already active 192 if ((systemMismatch || appsMismatch) && !stateManager.GetState().isManualOverride) 193 { 194 Logger::info(L"[LightSwitchService] External theme change detected (Windows Settings). Entering manual override mode."); 195 stateManager.OnManualOverride(); 196 } 197 } 198 199 DWORD WINAPI ServiceWorkerThread(LPVOID lpParam) 200 { 201 DWORD parentPid = static_cast<DWORD>(reinterpret_cast<ULONG_PTR>(lpParam)); 202 HANDLE hParent = nullptr; 203 if (parentPid) 204 hParent = OpenProcess(SYNCHRONIZE, FALSE, parentPid); 205 206 Logger::info(L"[LightSwitchService] Worker thread starting..."); 207 Logger::info(L"[LightSwitchService] Parent PID: {}", parentPid); 208 209 // ──────────────────────────────────────────────────────────────── 210 // Initialization 211 // ──────────────────────────────────────────────────────────────── 212 static LightSwitchStateManager stateManager; 213 g_stateManagerPtr = &stateManager; 214 215 LightSwitchSettings::instance().InitFileWatcher(); 216 217 HANDLE hManualOverride = OpenEventW(SYNCHRONIZE | EVENT_MODIFY_STATE, FALSE, L"POWERTOYS_LIGHTSWITCH_MANUAL_OVERRIDE"); 218 HANDLE hSettingsChanged = LightSwitchSettings::instance().GetSettingsChangedEvent(); 219 220 static std::unique_ptr<NightLightRegistryObserver> g_nightLightWatcher; 221 222 LightSwitchSettings::instance().LoadSettings(); 223 const auto& settings = LightSwitchSettings::instance().settings(); 224 225 // after loading settings: 226 bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight); 227 228 if (nightLightNeeded && !g_nightLightWatcher) 229 { 230 Logger::info(L"[LightSwitchService] Starting Night Light registry watcher..."); 231 232 g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>( 233 HKEY_CURRENT_USER, 234 NIGHT_LIGHT_REGISTRY_PATH, 235 []() { 236 if (g_stateManagerPtr) 237 g_stateManagerPtr->OnNightLightChange(); 238 }); 239 } 240 else if (!nightLightNeeded && g_nightLightWatcher) 241 { 242 Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher..."); 243 g_nightLightWatcher->Stop(); 244 g_nightLightWatcher.reset(); 245 } 246 247 SYSTEMTIME st; 248 GetLocalTime(&st); 249 int nowMinutes = st.wHour * 60 + st.wMinute; 250 251 Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute); 252 stateManager.SyncInitialThemeState(); 253 254 // ──────────────────────────────────────────────────────────────── 255 // Worker Loop 256 // ──────────────────────────────────────────────────────────────── 257 for (;;) 258 { 259 HANDLE waits[4]; 260 DWORD count = 0; 261 waits[count++] = g_ServiceStopEvent; 262 if (hParent) 263 waits[count++] = hParent; 264 if (hManualOverride) 265 waits[count++] = hManualOverride; 266 waits[count++] = hSettingsChanged; 267 268 // Wait for one of these to trigger or for a new minute tick 269 SYSTEMTIME st; 270 GetLocalTime(&st); 271 int msToNextMinute = (60 - st.wSecond) * 1000 - st.wMilliseconds; 272 if (msToNextMinute < 50) 273 msToNextMinute = 50; 274 275 DWORD wait = WaitForMultipleObjects(count, waits, FALSE, msToNextMinute); 276 277 if (wait == WAIT_TIMEOUT) 278 { 279 // regular minute tick 280 GetLocalTime(&st); 281 nowMinutes = st.wHour * 60 + st.wMinute; 282 DetectAndHandleExternalThemeChange(stateManager); 283 stateManager.OnTick(); 284 continue; 285 } 286 287 if (wait == WAIT_OBJECT_0) 288 { 289 Logger::info(L"[LightSwitchService] Stop event triggered — exiting."); 290 break; 291 } 292 293 if (hParent && wait == WAIT_OBJECT_0 + 1) 294 { 295 Logger::info(L"[LightSwitchService] Parent process exited — stopping service."); 296 break; 297 } 298 299 if (hManualOverride && wait == WAIT_OBJECT_0 + (hParent ? 2 : 1)) 300 { 301 Logger::info(L"[LightSwitchService] Manual override event detected."); 302 stateManager.OnManualOverride(); 303 ResetEvent(hManualOverride); 304 continue; 305 } 306 307 if (wait == WAIT_OBJECT_0 + (hParent ? (hManualOverride ? 3 : 2) : 2)) 308 { 309 ResetEvent(hSettingsChanged); 310 LightSwitchSettings::instance().LoadSettings(); 311 stateManager.OnSettingsChanged(); 312 313 const auto& settings = LightSwitchSettings::instance().settings(); 314 bool nightLightNeeded = (settings.scheduleMode == ScheduleMode::FollowNightLight); 315 316 if (nightLightNeeded && !g_nightLightWatcher) 317 { 318 Logger::info(L"[LightSwitchService] Starting Night Light registry watcher..."); 319 320 g_nightLightWatcher = std::make_unique<NightLightRegistryObserver>( 321 HKEY_CURRENT_USER, 322 NIGHT_LIGHT_REGISTRY_PATH, 323 []() { 324 if (g_stateManagerPtr) 325 g_stateManagerPtr->OnNightLightChange(); 326 }); 327 328 stateManager.OnNightLightChange(); 329 } 330 else if (!nightLightNeeded && g_nightLightWatcher) 331 { 332 Logger::info(L"[LightSwitchService] Stopping Night Light registry watcher..."); 333 g_nightLightWatcher->Stop(); 334 g_nightLightWatcher.reset(); 335 } 336 337 continue; 338 } 339 } 340 341 // ──────────────────────────────────────────────────────────────── 342 // Cleanup 343 // ──────────────────────────────────────────────────────────────── 344 if (hManualOverride) 345 CloseHandle(hManualOverride); 346 if (hParent) 347 CloseHandle(hParent); 348 if (g_nightLightWatcher) 349 { 350 g_nightLightWatcher->Stop(); 351 g_nightLightWatcher.reset(); 352 } 353 354 Logger::info(L"[LightSwitchService] Worker thread exiting cleanly."); 355 return 0; 356 } 357 358 int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) 359 { 360 Trace::LightSwitch::RegisterProvider(); 361 362 if (powertoys_gpo::getConfiguredLightSwitchEnabledValue() == powertoys_gpo::gpo_rule_configured_disabled) 363 { 364 wchar_t msg[160]; 365 swprintf_s( 366 msg, 367 L"Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator."); 368 Logger::info(msg); 369 Trace::LightSwitch::UnregisterProvider(); 370 return 0; 371 } 372 int argc = 0; 373 LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc); 374 int rc = _tmain(argc, argv); // reuse your existing logic 375 LocalFree(argv); 376 377 Trace::LightSwitch::UnregisterProvider(); 378 return rc; 379 }