FreeRDP
Loading...
Searching...
No Matches
shadow_service.c
1
19/* freerdp-shadow-service: runs freerdp-shadow-cli on the console session, as
20 * SYSTEM, so a remote user sees and uses whatever that session shows: the
21 * user's desktop, and the secure desktop too (the logon and lock screens, UAC
22 * prompts), which only SYSTEM may capture and drive.
23 *
24 * A service runs in session 0, which has no screen. This one copies its own
25 * SYSTEM token, moves the copy to the console session and starts the shadow
26 * server with it there (on WinSta0\Default; the server then follows the input
27 * desktop). When the console session changes (a logoff, a user switch) or the
28 * server ends, it starts it again on the session that is now the console's.
29 * It also sends the secure attention sequence (Ctrl+Alt+Del) when the server
30 * asks for it (win_sas.h), as only a service may.
31 *
32 * freerdp-shadow-service install [--log <dir>] [--allow-sas] -- <freerdp-shadow-cli options>
33 * freerdp-shadow-service uninstall
34 * freerdp-shadow-service run [--log <dir>] -- <options> what the service manager starts
35 *
36 * freerdp-shadow-cli.exe is taken from the folder of this program. With
37 * --log, this service writes service.log there and each start of the shadow
38 * server its own shadow-<session>-<time>.log (WLog's file appender).
39 * --allow-sas sets the policy that lets services send Ctrl+Alt+Del
40 * (SoftwareSASGeneration = 1); without it Windows ignores the request.
41 * Uninstall puts the policy back as it was before. */
42
43#include <windows.h>
44#include <wtsapi32.h>
45#include <shellapi.h>
46#include <stdarg.h>
47#include <stdio.h>
48#include <stdlib.h>
49#include <wchar.h>
50
51#include <winpr/assert.h>
52
53#include "win_sas.h"
54
55#define SERVICE_NAME L"FreeRDPShadow"
56#define SERVICE_DISPLAY L"FreeRDP shadow server"
57#define SERVICE_ABOUT \
58 L"Shares the console session's screen, logon and lock screens included, over RDP " \
59 L"(freerdp-shadow-cli)"
60#define SHADOW_CLI L"freerdp-shadow-cli.exe"
61#define NO_SESSION 0xFFFFFFFF
62/* the longest path Windows knows, in characters (\\?\ paths) */
63#define LONGEST_PATH 32767
64
65static SERVICE_STATUS_HANDLE g_statusHandle = nullptr;
66static SERVICE_STATUS g_status = WINPR_C_ARRAY_INIT;
67static HANDLE g_stopEvent = nullptr;
68static HANDLE g_sessionEvent = nullptr;
69static WCHAR* g_logDir = nullptr;
70static WCHAR* g_options = nullptr;
71
72/* ---- strings of the size they need (freed with free()) */
73
74/* winpr_vasprintf for WCHAR */
75WINPR_ATTR_NODISCARD
76static WCHAR* wcs_vasprintf(const WCHAR* format, va_list args)
77{
78 va_list copy = WINPR_C_ARRAY_INIT;
79
80 WINPR_ASSERT(format);
81
82 va_copy(copy, args);
83 const int length = _vscwprintf(format, copy);
84 va_end(copy);
85 if (length < 0)
86 return nullptr;
87
88 WCHAR* str = calloc((size_t)length + 1, sizeof(WCHAR));
89 if (!str)
90 return nullptr;
91
92 va_copy(copy, args);
93 const int written = _vsnwprintf_s(str, (size_t)length + 1, _TRUNCATE, format, copy);
94 va_end(copy);
95 if (written != length)
96 {
97 free(str);
98 return nullptr;
99 }
100 return str;
101}
102
103/* winpr_asprintf for WCHAR */
104WINPR_ATTR_NODISCARD
105static WCHAR* wcs_asprintf(const WCHAR* format, ...)
106{
107 va_list args = WINPR_C_ARRAY_INIT;
108
109 va_start(args, format);
110 WCHAR* str = wcs_vasprintf(format, args);
111 va_end(args);
112 return str;
113}
114
115/* ---- the log: service.log in --log <dir>, else the debugger's output */
116
117static void say(const WCHAR* format, ...)
118{
119 SYSTEMTIME now = WINPR_C_ARRAY_INIT;
120 va_list args = WINPR_C_ARRAY_INIT;
121
122 WINPR_ASSERT(format);
123
124 GetLocalTime(&now);
125 va_start(args, format);
126 WCHAR* message = wcs_vasprintf(format, args);
127 va_end(args);
128 /* without memory for the message, at least its format */
129 WCHAR* text =
130 wcs_asprintf(L"%04u-%02u-%02u %02u:%02u:%02u %ls", now.wYear, now.wMonth, now.wDay,
131 now.wHour, now.wMinute, now.wSecond, message ? message : format);
132 free(message);
133 if (!text)
134 return;
135 OutputDebugStringW(text);
136 OutputDebugStringW(L"\n");
137
138 WCHAR* path = g_logDir ? wcs_asprintf(L"%ls\\service.log", g_logDir) : nullptr;
139 FILE* fp = nullptr;
140 if (path && (_wfopen_s(&fp, path, L"a, ccs=UTF-8") == 0) && fp)
141 {
142 (void)fwprintf(fp, L"%ls\n", text);
143 (void)fclose(fp);
144 }
145 free(path);
146 free(text);
147}
148
149/* ---- command lines */
150
151/* appends one argument to a command line, quoted the way
152 * CommandLineToArgvW reads it back */
153WINPR_ATTR_NODISCARD
154static BOOL append_quoted(WCHAR* line, size_t size, const WCHAR* arg)
155{
156 WINPR_ASSERT(line);
157 WINPR_ASSERT(arg);
158
159 size_t len = wcslen(line);
160 const BOOL quote = (arg[0] == L'\0') || (wcspbrk(arg, L" \t\"") != nullptr);
161
162#define PUT(c) \
163 do \
164 { \
165 if (len + 1 >= size) \
166 return FALSE; \
167 line[len++] = (c); \
168 } while (0)
169
170 if (len > 0)
171 PUT(L' ');
172 if (!quote)
173 {
174 for (const WCHAR* p = arg; *p; p++)
175 PUT(*p);
176 line[len] = L'\0';
177 return TRUE;
178 }
179
180 PUT(L'"');
181 for (const WCHAR* p = arg;; p++)
182 {
183 size_t backslashes = 0;
184 while (*p == L'\\')
185 {
186 backslashes++;
187 p++;
188 }
189 if (*p == L'\0')
190 {
191 /* double the backslashes before the closing quote */
192 for (size_t i = 0; i < backslashes * 2; i++)
193 PUT(L'\\');
194 break;
195 }
196 if (*p == L'"')
197 {
198 for (size_t i = 0; i < backslashes * 2 + 1; i++)
199 PUT(L'\\');
200 PUT(L'"');
201 }
202 else
203 {
204 for (size_t i = 0; i < backslashes; i++)
205 PUT(L'\\');
206 PUT(*p);
207 }
208 }
209 PUT(L'"');
210 line[len] = L'\0';
211 return TRUE;
212#undef PUT
213}
214
215/* the path of this program, however long: GetModuleFileNameW truncates to
216 * the buffer it gets, so the buffer grows until the path fits */
217WINPR_ATTR_NODISCARD
218static WCHAR* program_path(void)
219{
220 for (DWORD size = 256; size <= LONGEST_PATH + 1; size *= 2)
221 {
222 WCHAR* path = calloc(size, sizeof(WCHAR));
223 if (!path)
224 return nullptr;
225 const DWORD n = GetModuleFileNameW(nullptr, path, size);
226 if ((n > 0) && (n < size))
227 return path;
228 free(path);
229 if (n == 0)
230 return nullptr;
231 }
232 return nullptr;
233}
234
235/* the folder of this program, with a trailing backslash */
236WINPR_ATTR_NODISCARD
237static WCHAR* program_folder(void)
238{
239 WCHAR* folder = program_path();
240 if (!folder)
241 return nullptr;
242 WCHAR* slash = wcsrchr(folder, L'\\');
243 if (!slash)
244 {
245 free(folder);
246 return nullptr;
247 }
248 slash[1] = L'\0';
249 return folder;
250}
251
252/* the full path of a file or a folder, however long */
253WINPR_ATTR_NODISCARD
254static WCHAR* full_path(const WCHAR* name)
255{
256 WINPR_ASSERT(name);
257
258 const DWORD size = GetFullPathNameW(name, 0, nullptr, nullptr);
259 if (size == 0)
260 return nullptr;
261 WCHAR* path = calloc(size, sizeof(WCHAR));
262 if (!path)
263 return nullptr;
264 const DWORD n = GetFullPathNameW(name, size, path, nullptr);
265 if ((n == 0) || (n >= size))
266 {
267 free(path);
268 return nullptr;
269 }
270 return path;
271}
272
273/* ---- the shadow server on the console session */
274
275WINPR_ATTR_NODISCARD
276static HANDLE start_shadow(DWORD session)
277{
278 HANDLE self = nullptr;
279 HANDLE token = nullptr;
280 HANDLE process = nullptr;
281 WCHAR* folder = nullptr;
282 WCHAR* exe = nullptr;
283 static WCHAR line[32768];
284 STARTUPINFOW si = WINPR_C_ARRAY_INIT;
285 PROCESS_INFORMATION pi = WINPR_C_ARRAY_INIT;
286
287 folder = program_folder();
288 if (!folder)
289 {
290 say(L"the folder of this program: 0x%08lX", GetLastError());
291 goto out;
292 }
293 exe = wcs_asprintf(L"%ls%ls", folder, SHADOW_CLI);
294 if (!exe)
295 goto out;
296 line[0] = L'\0';
297 if (!append_quoted(line, ARRAYSIZE(line), exe))
298 goto out;
299 if (g_options && g_options[0])
300 {
301 if (wcslen(line) + 1 + wcslen(g_options) + 1 > ARRAYSIZE(line))
302 goto out;
303 wcscat_s(line, ARRAYSIZE(line), L" ");
304 wcscat_s(line, ARRAYSIZE(line), g_options);
305 }
306
307 /* our own SYSTEM token, moved to the console session (SE_TCB_NAME,
308 * which SYSTEM holds, allows the move) */
309 if (!OpenProcessToken(GetCurrentProcess(),
310 TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY |
311 TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID,
312 &self))
313 {
314 say(L"OpenProcessToken failed: 0x%08lX", GetLastError());
315 goto out;
316 }
317 if (!DuplicateTokenEx(self, MAXIMUM_ALLOWED, nullptr, SecurityImpersonation, TokenPrimary,
318 &token))
319 {
320 say(L"DuplicateTokenEx failed: 0x%08lX", GetLastError());
321 goto out;
322 }
323 if (!SetTokenInformation(token, TokenSessionId, &session, sizeof(session)))
324 {
325 say(L"SetTokenInformation(TokenSessionId %lu) failed: 0x%08lX", session, GetLastError());
326 goto out;
327 }
328
329 /* WLog to a file of its own, when there is a log folder */
330 if (g_logDir)
331 {
332 SYSTEMTIME now = WINPR_C_ARRAY_INIT;
333 GetLocalTime(&now);
334 WCHAR* name = wcs_asprintf(L"shadow-%lu-%04u%02u%02u-%02u%02u%02u.log", session, now.wYear,
335 now.wMonth, now.wDay, now.wHour, now.wMinute, now.wSecond);
336 if (!name || !SetEnvironmentVariableW(L"WLOG_APPENDER", L"FILE") ||
337 !SetEnvironmentVariableW(L"WLOG_FILEAPPENDER_OUTPUT_FILE_PATH", g_logDir) ||
338 !SetEnvironmentVariableW(L"WLOG_FILEAPPENDER_OUTPUT_FILE_NAME", name))
339 say(L"the shadow server's log file is not set: 0x%08lX", GetLastError());
340 free(name);
341 }
342
343 si.cb = sizeof(si);
344 si.lpDesktop = (LPWSTR)L"winsta0\\default";
345 if (!CreateProcessAsUserW(token, exe, line, nullptr, nullptr, FALSE,
346 CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, nullptr, folder, &si,
347 &pi))
348 {
349 say(L"CreateProcessAsUserW(%ls) failed: 0x%08lX", exe, GetLastError());
350 goto out;
351 }
352 (void)CloseHandle(pi.hThread);
353 process = pi.hProcess;
354 say(L"started %ls on session %lu, process %lu", SHADOW_CLI, session, pi.dwProcessId);
355
356out:
357 if (token)
358 (void)CloseHandle(token);
359 if (self)
360 (void)CloseHandle(self);
361 free(exe);
362 free(folder);
363 return process;
364}
365
366static void stop_shadow(HANDLE* process)
367{
368 WINPR_ASSERT(process);
369
370 if (!*process)
371 return;
372 if (WaitForSingleObject(*process, 0) == WAIT_TIMEOUT)
373 {
374 if (!TerminateProcess(*process, 0))
375 say(L"TerminateProcess(%ls) failed: 0x%08lX", SHADOW_CLI, GetLastError());
376 if (WaitForSingleObject(*process, 5000) != WAIT_OBJECT_0)
377 say(L"%ls did not end within 5 s", SHADOW_CLI);
378 }
379 (void)CloseHandle(*process);
380 *process = nullptr;
381}
382
383/* SendSAS lives in sas.dll; FALSE: as a service, not as an application */
384static void send_sas(void)
385{
386 typedef VOID(WINAPI * fnSendSAS)(BOOL AsUser);
387 HMODULE sas = LoadLibraryW(L"sas.dll");
388 fnSendSAS sendSAS = sas ? (fnSendSAS)(void*)GetProcAddress(sas, "SendSAS") : nullptr;
389
390 if (!sendSAS)
391 {
392 say(L"Ctrl+Alt+Del: no SendSAS in sas.dll");
393 if (sas)
394 (void)FreeLibrary(sas);
395 return;
396 }
397 sendSAS(FALSE);
398 say(L"Ctrl+Alt+Del: secure attention sequence sent (Windows ignores it unless "
399 L"SoftwareSASGeneration allows services: install --allow-sas)");
400 (void)FreeLibrary(sas);
401}
402
403/* until the stop event: the shadow server on whichever session is the
404 * console's */
405static void serve(void)
406{
407 HANDLE process = nullptr;
408 DWORD session = NO_SESSION;
409 DWORD restarts = 0;
410 HANDLE sasEvent = CreateEventW(nullptr, FALSE, FALSE, WIN_SHADOW_SAS_EVENT);
411
412 if (!sasEvent)
413 say(L"CreateEvent(%ls) failed: 0x%08lX; Ctrl+Alt+Del will not work", WIN_SHADOW_SAS_EVENT,
414 GetLastError());
415
416 for (;;)
417 {
418 const DWORD console = WTSGetActiveConsoleSessionId();
419
420 if (process && (console != session))
421 {
422 say(L"the console moved from session %lu to %lu", session, console);
423 stop_shadow(&process);
424 }
425 if (!process && (console != NO_SESSION))
426 {
427 process = start_shadow(console);
428 session = console;
429 if (process)
430 restarts++;
431 }
432
433 HANDLE events[4] = { g_stopEvent, g_sessionEvent };
434 DWORD count = 2;
435 if (sasEvent)
436 events[count++] = sasEvent;
437 const DWORD processIndex = count;
438 if (process)
439 events[count++] = process;
440
441 /* every 2 s at least: the console session may change unannounced */
442 const DWORD rc = WaitForMultipleObjects(count, events, FALSE, 2000);
443 if (rc == WAIT_OBJECT_0)
444 break;
445 if (sasEvent && (rc == WAIT_OBJECT_0 + 2))
446 send_sas();
447 if (process && (rc == WAIT_OBJECT_0 + processIndex))
448 {
449 DWORD code = 0;
450 if (GetExitCodeProcess(process, &code))
451 say(L"%ls on session %lu ended (exit code %lu)", SHADOW_CLI, session, code);
452 else
453 say(L"%ls on session %lu ended (exit code unknown: 0x%08lX)", SHADOW_CLI, session,
454 GetLastError());
455 (void)CloseHandle(process);
456 process = nullptr;
457 /* not in a tight loop when it cannot start */
458 if (WaitForSingleObject(g_stopEvent, restarts > 3 ? 5000 : 1000) == WAIT_OBJECT_0)
459 break;
460 }
461 }
462
463 stop_shadow(&process);
464 if (sasEvent)
465 (void)CloseHandle(sasEvent);
466}
467
468/* ---- the service */
469
470static void set_state(DWORD state)
471{
472 g_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
473 g_status.dwCurrentState = state;
474 g_status.dwControlsAccepted =
475 (state == SERVICE_RUNNING)
476 ? (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_SESSIONCHANGE)
477 : 0;
478 g_status.dwWin32ExitCode = NO_ERROR;
479 g_status.dwWaitHint = (state == SERVICE_RUNNING || state == SERVICE_STOPPED) ? 0 : 10000;
480 if (!SetServiceStatus(g_statusHandle, &g_status))
481 say(L"SetServiceStatus(%lu) failed: 0x%08lX", state, GetLastError());
482}
483
484static DWORD WINAPI handler(DWORD control, DWORD eventType, LPVOID eventData, LPVOID context)
485{
486 (void)eventData;
487 (void)context;
488 switch (control)
489 {
490 case SERVICE_CONTROL_STOP:
491 case SERVICE_CONTROL_SHUTDOWN:
492 set_state(SERVICE_STOP_PENDING);
493 if (!SetEvent(g_stopEvent))
494 say(L"SetEvent(stop) failed: 0x%08lX", GetLastError());
495 return NO_ERROR;
496 case SERVICE_CONTROL_SESSIONCHANGE:
497 /* WTS_CONSOLE_CONNECT 1, WTS_CONSOLE_DISCONNECT 2, WTS_SESSION_LOGON 5,
498 * WTS_SESSION_LOGOFF 6, WTS_SESSION_LOCK 7, WTS_SESSION_UNLOCK 8 */
499 say(L"session change %lu", eventType);
500 if (!SetEvent(g_sessionEvent))
501 say(L"SetEvent(session change) failed: 0x%08lX", GetLastError());
502 return NO_ERROR;
503 case SERVICE_CONTROL_INTERROGATE:
504 return NO_ERROR;
505 default:
506 return ERROR_CALL_NOT_IMPLEMENTED;
507 }
508}
509
510static VOID WINAPI service_main(DWORD argc, LPWSTR* argv)
511{
512 (void)argc;
513 (void)argv;
514 g_statusHandle = RegisterServiceCtrlHandlerExW(SERVICE_NAME, handler, nullptr);
515 if (!g_statusHandle)
516 return;
517 set_state(SERVICE_START_PENDING);
518 say(L"service started; options: %ls", g_options ? g_options : L"");
519 set_state(SERVICE_RUNNING);
520 serve();
521 say(L"service stopped");
522 set_state(SERVICE_STOPPED);
523}
524
525/* ---- install, uninstall */
526
527#define SAS_POLICY_KEY L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"
528#define SAS_POLICY L"SoftwareSASGeneration"
529/* what the policy was before install changed it, kept in the service's own key (removed with
530 * the service): its old value, or SAS_WAS_UNSET */
531#define SERVICE_KEY L"SYSTEM\\CurrentControlSet\\Services\\" SERVICE_NAME
532#define SAS_BEFORE L"SoftwareSASGenerationBefore"
533#define SAS_WAS_UNSET 0xFFFFFFFFu
534
535/* Lets services send Ctrl+Alt+Del: SoftwareSASGeneration 1 (services) or 3 (services and Ease of
536 * Access applications) allows it. When it is set already, nothing changes and nothing is kept;
537 * otherwise the old state is kept for uninstall, unless an earlier install kept it already. */
538WINPR_ATTR_NODISCARD
539static BOOL allow_sas(void)
540{
541 DWORD before = SAS_WAS_UNSET;
542 DWORD size = sizeof(before);
543 const LSTATUS read = RegGetValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY,
544 RRF_RT_REG_DWORD, nullptr, &before, &size);
545 if (read != ERROR_SUCCESS)
546 before = SAS_WAS_UNSET;
547 if ((before != SAS_WAS_UNSET) && ((before & 1) != 0))
548 {
549 wprintf(L"SoftwareSASGeneration = %lu already lets services send Ctrl+Alt+Del\n",
550 (unsigned long)before);
551 return TRUE;
552 }
553
554 DWORD kept = 0;
555 size = sizeof(kept);
556 if (RegGetValueW(HKEY_LOCAL_MACHINE, SERVICE_KEY, SAS_BEFORE, RRF_RT_REG_DWORD, nullptr, &kept,
557 &size) != ERROR_SUCCESS)
558 {
559 const LSTATUS rc = RegSetKeyValueW(HKEY_LOCAL_MACHINE, SERVICE_KEY, SAS_BEFORE, REG_DWORD,
560 &before, sizeof(before));
561 if (rc != ERROR_SUCCESS)
562 {
563 fwprintf(stderr, L"%ls: 0x%08lX\n", SAS_BEFORE, (unsigned long)rc);
564 return FALSE;
565 }
566 }
567
568 /* services, and Ease of Access applications too where they were allowed (2) */
569 const DWORD services = (before == SAS_WAS_UNSET) ? 1 : (before | 1);
570 const LSTATUS rc = RegSetKeyValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY, REG_DWORD,
571 &services, sizeof(services));
572 if (rc != ERROR_SUCCESS)
573 {
574 fwprintf(stderr, L"SoftwareSASGeneration: 0x%08lX\n", (unsigned long)rc);
575 return FALSE;
576 }
577 wprintf(
578 L"SoftwareSASGeneration = %lu: services may send Ctrl+Alt+Del (uninstall restores it)\n",
579 (unsigned long)services);
580 return TRUE;
581}
582
583/* Puts SoftwareSASGeneration back as it was before install changed it */
584static void restore_sas(void)
585{
586 DWORD before = 0;
587 DWORD size = sizeof(before);
588 if (RegGetValueW(HKEY_LOCAL_MACHINE, SERVICE_KEY, SAS_BEFORE, RRF_RT_REG_DWORD, nullptr,
589 &before, &size) != ERROR_SUCCESS)
590 return; /* install did not change it */
591
592 LSTATUS rc = ERROR_SUCCESS;
593 if (before == SAS_WAS_UNSET)
594 {
595 rc = RegDeleteKeyValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY);
596 if (rc == ERROR_FILE_NOT_FOUND)
597 rc = ERROR_SUCCESS;
598 }
599 else
600 rc = RegSetKeyValueW(HKEY_LOCAL_MACHINE, SAS_POLICY_KEY, SAS_POLICY, REG_DWORD, &before,
601 sizeof(before));
602 if (rc != ERROR_SUCCESS)
603 fwprintf(stderr, L"SoftwareSASGeneration: 0x%08lX, not restored\n", (unsigned long)rc);
604 else if (before == SAS_WAS_UNSET)
605 wprintf(L"SoftwareSASGeneration removed again (it was not set before install)\n");
606 else
607 wprintf(L"SoftwareSASGeneration = %lu again\n", (unsigned long)before);
608}
609
610WINPR_ATTR_NODISCARD
611static int install(BOOL sas)
612{
613 static WCHAR image[32768];
614 SC_HANDLE scm = nullptr;
615 SC_HANDLE service = nullptr;
616 int rc = 1;
617
618 WCHAR* self = program_path();
619 if (!self)
620 {
621 fwprintf(stderr, L"GetModuleFileName: 0x%08lX\n", GetLastError());
622 return 1;
623 }
624 image[0] = L'\0';
625 if (!append_quoted(image, ARRAYSIZE(image), self) ||
626 !append_quoted(image, ARRAYSIZE(image), L"run"))
627 goto out;
628 if (g_logDir && (!append_quoted(image, ARRAYSIZE(image), L"--log") ||
629 !append_quoted(image, ARRAYSIZE(image), g_logDir)))
630 goto out;
631 if (!append_quoted(image, ARRAYSIZE(image), L"--"))
632 goto out;
633 if (g_options && g_options[0])
634 {
635 if (wcslen(image) + 1 + wcslen(g_options) + 1 > ARRAYSIZE(image))
636 goto out;
637 wcscat_s(image, ARRAYSIZE(image), L" ");
638 wcscat_s(image, ARRAYSIZE(image), g_options);
639 }
640
641 scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CREATE_SERVICE | SC_MANAGER_CONNECT);
642 if (!scm)
643 {
644 fwprintf(stderr, L"OpenSCManager: 0x%08lX (an administrator's prompt is needed)\n",
645 GetLastError());
646 goto out;
647 }
648 service = CreateServiceW(scm, SERVICE_NAME, SERVICE_DISPLAY, SERVICE_ALL_ACCESS,
649 SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL,
650 image, nullptr, nullptr, nullptr, nullptr, nullptr);
651 if (!service && (GetLastError() == ERROR_SERVICE_EXISTS))
652 {
653 service = OpenServiceW(scm, SERVICE_NAME, SERVICE_ALL_ACCESS);
654 if (service &&
655 !ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_AUTO_START, SERVICE_NO_CHANGE,
656 image, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr))
657 {
658 fwprintf(stderr, L"ChangeServiceConfig: 0x%08lX\n", GetLastError());
659 goto out;
660 }
661 }
662 if (!service)
663 {
664 fwprintf(stderr, L"CreateService: 0x%08lX\n", GetLastError());
665 goto out;
666 }
667 /* after the service exists: its key keeps what the policy was */
668 if (sas && !allow_sas())
669 goto out;
670
671 SERVICE_DESCRIPTIONW about = { (LPWSTR)SERVICE_ABOUT };
672 if (!ChangeServiceConfig2W(service, SERVICE_CONFIG_DESCRIPTION, &about))
673 fwprintf(stderr, L"warning: ChangeServiceConfig2(description): 0x%08lX\n", GetLastError());
674 /* started again 5 s after a crash */
675 SC_ACTION again[3] = { { SC_ACTION_RESTART, 5000 },
676 { SC_ACTION_RESTART, 5000 },
677 { SC_ACTION_RESTART, 5000 } };
678 SERVICE_FAILURE_ACTIONSW failure = { 86400, nullptr, nullptr, ARRAYSIZE(again), again };
679 if (!ChangeServiceConfig2W(service, SERVICE_CONFIG_FAILURE_ACTIONS, &failure))
680 fwprintf(stderr, L"warning: ChangeServiceConfig2(failure actions): 0x%08lX\n",
681 GetLastError());
682
683 if (!StartServiceW(service, 0, nullptr) && (GetLastError() != ERROR_SERVICE_ALREADY_RUNNING))
684 {
685 fwprintf(stderr, L"StartService: 0x%08lX\n", GetLastError());
686 goto out;
687 }
688 wprintf(L"%ls installed and started: %ls\n", SERVICE_NAME, image);
689 rc = 0;
690
691out:
692 if (service)
693 (void)CloseServiceHandle(service);
694 if (scm)
695 (void)CloseServiceHandle(scm);
696 free(self);
697 return rc;
698}
699
700WINPR_ATTR_NODISCARD
701static int uninstall(void)
702{
703 SERVICE_STATUS status = WINPR_C_ARRAY_INIT;
704 SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
705 if (!scm)
706 {
707 fwprintf(stderr, L"OpenSCManager: 0x%08lX\n", GetLastError());
708 return 1;
709 }
710 SC_HANDLE service =
711 OpenServiceW(scm, SERVICE_NAME, SERVICE_STOP | DELETE | SERVICE_QUERY_STATUS);
712 if (!service)
713 {
714 fwprintf(stderr, L"OpenService: 0x%08lX\n", GetLastError());
715 (void)CloseServiceHandle(scm);
716 return 1;
717 }
718 if (!ControlService(service, SERVICE_CONTROL_STOP, &status))
719 {
720 const DWORD error = GetLastError();
721 if (error != ERROR_SERVICE_NOT_ACTIVE)
722 fwprintf(stderr, L"warning: ControlService(stop): 0x%08lX\n", error);
723 }
724 for (int i = 0;
725 i < 50 && QueryServiceStatus(service, &status) && status.dwCurrentState != SERVICE_STOPPED;
726 i++)
727 Sleep(200);
728 /* before the service's key goes, with what it kept */
729 restore_sas();
730 const BOOL deleted = DeleteService(service);
731 if (!deleted)
732 fwprintf(stderr, L"DeleteService: 0x%08lX\n", GetLastError());
733 else
734 wprintf(L"%ls stopped and removed\n", SERVICE_NAME);
735 (void)CloseServiceHandle(service);
736 (void)CloseServiceHandle(scm);
737 return deleted ? 0 : 1;
738}
739
740/* ---- the command line */
741
742WINPR_ATTR_NODISCARD
743static int usage(void)
744{
745 fwprintf(stderr, L"usage: freerdp-shadow-service install [--log <dir>] [--allow-sas] -- "
746 L"<freerdp-shadow-cli options>\n"
747 L" freerdp-shadow-service uninstall\n"
748 L" freerdp-shadow-service run [--log <dir>] -- <options>\n");
749 return 2;
750}
751
752int main(void)
753{
754 int rc = 1;
755 int argc = 0;
756 BOOL sas = FALSE;
757 LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
758 static WCHAR options[32768];
759
760 if (!argv || argc < 2)
761 {
762 rc = usage();
763 goto out;
764 }
765
766 const WCHAR* command = argv[1];
767 int i = 2;
768 for (; i < argc; i++)
769 {
770 if (wcscmp(argv[i], L"--") == 0)
771 {
772 i++;
773 break;
774 }
775 if ((wcscmp(argv[i], L"--log") == 0) && (i + 1 < argc))
776 {
777 free(g_logDir);
778 g_logDir = full_path(argv[++i]);
779 if (!g_logDir)
780 {
781 rc = usage();
782 goto out;
783 }
784 if (!CreateDirectoryW(g_logDir, nullptr))
785 {
786 const DWORD error = GetLastError();
787 if (error != ERROR_ALREADY_EXISTS)
788 fwprintf(stderr, L"warning: CreateDirectory(%ls): 0x%08lX\n", g_logDir, error);
789 }
790 }
791 else if (wcscmp(argv[i], L"--allow-sas") == 0)
792 sas = TRUE;
793 else
794 {
795 rc = usage();
796 goto out;
797 }
798 }
799 options[0] = L'\0';
800 for (; i < argc; i++)
801 {
802 if (!append_quoted(options, ARRAYSIZE(options), argv[i]))
803 {
804 rc = usage();
805 goto out;
806 }
807 }
808 g_options = options;
809
810 if (wcscmp(command, L"install") == 0)
811 rc = install(sas);
812 else if (wcscmp(command, L"uninstall") == 0)
813 rc = uninstall();
814 else if (wcscmp(command, L"run") != 0)
815 rc = usage();
816 else
817 {
818 g_stopEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
819 g_sessionEvent = CreateEventW(nullptr, FALSE, FALSE, nullptr);
820 if (!g_stopEvent || !g_sessionEvent)
821 goto out;
822 SERVICE_TABLE_ENTRYW table[] = { { (LPWSTR)SERVICE_NAME, service_main },
823 { nullptr, nullptr } };
824 if (!StartServiceCtrlDispatcherW(table))
825 {
826 fwprintf(stderr, L"run is for the service manager (install starts it): 0x%08lX\n",
827 GetLastError());
828 goto out;
829 }
830 rc = 0;
831 }
832
833out:
834 free(g_logDir);
835 g_logDir = nullptr;
836 if (argv)
837 (void)LocalFree((HLOCAL)argv);
838 return rc;
839}