/ src / modules / LightSwitch / LightSwitchService / LightSwitchService.cpp
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  }