VirtualBox

source: vbox/trunk/src/VBox/Additions/WINNT/VBoxService/VBoxService.cpp@ 1959

Last change on this file since 1959 was 1, checked in by vboxsync, 55 years ago

import

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 22.9 KB
Line 
1/** @file
2 * VBoxService - Guest Additions Service
3 */
4
5/*
6 * Copyright (C) 2006 InnoTek Systemberatung GmbH
7 *
8 * This file is part of VirtualBox Open Source Edition (OSE), as
9 * available from http://www.virtualbox.org. This file is free software;
10 * you can redistribute it and/or modify it under the terms of the GNU
11 * General Public License as published by the Free Software Foundation,
12 * in version 2 as it comes in the "COPYING" file of the VirtualBox OSE
13 * distribution. VirtualBox OSE is distributed in the hope that it will
14 * be useful, but WITHOUT ANY WARRANTY of any kind.
15 *
16 * If you received this file as part of a commercial VirtualBox
17 * distribution, then only the terms of your commercial VirtualBox
18 * license agreement apply instead of the previous paragraph.
19 *
20 */
21
22#include "VBoxService.h"
23#include "resource.h"
24
25// #define DEBUG_DISPLAY_CHANGE
26
27/* global variables */
28HANDLE gVBoxDriver;
29HANDLE gStopSem;
30SERVICE_STATUS gVBoxServiceStatus;
31SERVICE_STATUS_HANDLE gVBoxServiceStatusHandle;
32HINSTANCE gInstance;
33HWND gToolWindow;
34
35
36/* prototypes */
37VOID DisplayChangeThread(void *dummy);
38LRESULT CALLBACK VBoxToolWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
39
40
41VOID SvcDebugOut2(LPSTR String, ...)
42{
43 va_list va;
44
45 va_start(va, String);
46
47 CHAR Buffer[1024];
48 if (strlen(String) < 1000)
49 {
50 vsprintf (Buffer, String, va);
51
52 OutputDebugStringA(Buffer);
53
54#ifdef DEBUG_DISPLAY_CHANGE
55 FILE *f = fopen ("vboxservice.log", "ab");
56 if (f)
57 {
58 fprintf (f, "%s", Buffer);
59 fclose (f);
60 }
61#endif /* DEBUG_DISPLAY_CHANGE */
62 }
63
64 va_end (va);
65}
66
67#ifdef DEBUG_DISPLAY_CHANGE
68#define DDCLOG(a) do { SvcDebugOut2 a; } while (0)
69#else
70#define DDCLOG(a) do {} while (0)
71#endif /* DEBUG_DISPLAY_CHANGE */
72
73
74/**
75 * Helper function to send a message to WinDbg
76 *
77 * @param String message string
78 * @param Status error code, will be inserted into String's placeholder
79 */
80VOID SvcDebugOut(LPSTR String, DWORD Status)
81{
82 SvcDebugOut2(String, Status);
83}
84
85/* The shared clipboard service prototypes. */
86int VBoxClipboardInit (const VBOXSERVICEENV *pEnv, void **ppInstance, bool *pfStartThread);
87unsigned __stdcall VBoxClipboardThread (void *pInstance);
88void VBoxClipboardDestroy (const VBOXSERVICEENV *pEnv, void *pInstance);
89
90/* The service table. */
91static VBOXSERVICEINFO vboxServiceTable[] =
92{
93 {
94 "Shared Clipboard",
95 VBoxClipboardInit,
96 VBoxClipboardThread,
97 VBoxClipboardDestroy
98 },
99 {
100 NULL
101 }
102};
103
104static int vboxStartServices (VBOXSERVICEENV *pEnv, VBOXSERVICEINFO *pTable)
105{
106 pEnv->hStopEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
107
108 if (!pEnv->hStopEvent)
109 {
110 /* Could not create event. */
111 return VERR_NOT_SUPPORTED;
112 }
113
114 while (pTable->pszName)
115 {
116 SvcDebugOut2 ("Starting %s...\n", pTable->pszName);
117
118 int rc = VINF_SUCCESS;
119
120 bool fStartThread = false;
121
122 pTable->hThread = (HANDLE)0;
123 pTable->pInstance = NULL;
124 pTable->fStarted = false;
125
126 if (pTable->pfnInit)
127 {
128 rc = pTable->pfnInit (pEnv, &pTable->pInstance, &fStartThread);
129 }
130
131 if (VBOX_FAILURE (rc))
132 {
133 SvcDebugOut2 ("Failed to initialize rc = %Vrc.\n", rc);
134 }
135 else
136 {
137 if (pTable->pfnThread && fStartThread)
138 {
139 unsigned threadid;
140
141 pTable->hThread = (HANDLE)_beginthreadex (NULL, /* security */
142 0, /* stacksize */
143 pTable->pfnThread,
144 pTable->pInstance,
145 0, /* initflag */
146 &threadid);
147
148 if (pTable->hThread == (HANDLE)(0))
149 {
150 rc = VERR_NOT_SUPPORTED;
151 }
152 }
153
154 if (VBOX_FAILURE (rc))
155 {
156 SvcDebugOut2 ("Failed to start the thread.\n");
157
158 if (pTable->pfnDestroy)
159 {
160 pTable->pfnDestroy (pEnv, pTable->pInstance);
161 }
162 }
163 else
164 {
165 pTable->fStarted = true;
166 }
167 }
168
169 /* Advance to next table element. */
170 pTable++;
171 }
172
173 return VINF_SUCCESS;
174}
175
176static void vboxStopServices (VBOXSERVICEENV *pEnv, VBOXSERVICEINFO *pTable)
177{
178 if (!pEnv->hStopEvent)
179 {
180 return;
181 }
182
183 /* Signal to all threads. */
184 SetEvent(pEnv->hStopEvent);
185
186 while (pTable->pszName)
187 {
188 if (pTable->fStarted)
189 {
190 if (pTable->pfnThread)
191 {
192 /* There is a thread, wait for termination. */
193 WaitForSingleObject(pTable->hThread, INFINITE);
194
195 CloseHandle (pTable->hThread);
196 pTable->hThread = 0;
197 }
198
199 if (pTable->pfnDestroy)
200 {
201 pTable->pfnDestroy (pEnv, pTable->pInstance);
202 }
203
204 pTable->fStarted = false;
205 }
206
207 /* Advance to next table element. */
208 pTable++;
209 }
210
211 CloseHandle (pEnv->hStopEvent);
212}
213
214
215void WINAPI VBoxServiceStart(void)
216{
217 SvcDebugOut2("VBoxService: Start\n");
218
219 VBOXSERVICEENV svcEnv;
220
221 DWORD status = NO_ERROR;
222
223 /* open VBox guest driver */
224 gVBoxDriver = CreateFile(VBOXGUEST_DEVICE_NAME,
225 GENERIC_READ | GENERIC_WRITE,
226 FILE_SHARE_READ | FILE_SHARE_WRITE,
227 NULL,
228 OPEN_EXISTING,
229 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
230 NULL);
231 if (gVBoxDriver == INVALID_HANDLE_VALUE)
232 {
233 SvcDebugOut("VBoxService: could not open VBox Guest Additions driver! rc = %d\n", GetLastError());
234 status = ERROR_GEN_FAILURE;
235 }
236
237 SvcDebugOut2("VBoxService: Driver h %p, st %p\n", gVBoxDriver, status);
238
239 if (status == NO_ERROR)
240 {
241 /* create a custom window class */
242 WNDCLASS windowClass = {0};
243 windowClass.style = CS_NOCLOSE;
244 windowClass.lpfnWndProc = (WNDPROC)VBoxToolWndProc;
245 windowClass.hInstance = gInstance;
246 windowClass.hCursor = LoadCursor(NULL, IDC_ARROW);
247 windowClass.lpszClassName = "VirtualBoxTool";
248 if (!RegisterClass(&windowClass))
249 status = GetLastError();
250 }
251
252 SvcDebugOut2("VBoxService: Class st %p\n", status);
253
254 if (status == NO_ERROR)
255 {
256 /* create our window */
257 gToolWindow = CreateWindowEx(WS_EX_TOOLWINDOW | WS_EX_TRANSPARENT | WS_EX_TOPMOST,
258 "VirtualBoxTool", "VirtualBoxTool",
259 WS_POPUPWINDOW,
260 -200, -200, 100, 100, NULL, NULL, gInstance, NULL);
261 if (!gToolWindow)
262 status = GetLastError();
263 else
264 {
265 /* move the window beneach the mouse pointer so that we get access to it */
266 POINT mousePos;
267 GetCursorPos(&mousePos);
268 SetWindowPos(gToolWindow, HWND_TOPMOST, mousePos.x - 10, mousePos.y - 10, 0, 0,
269 SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOCOPYBITS | SWP_NOREDRAW | SWP_NOSIZE);
270 /* change the mouse pointer so that we can go for a hardware shape */
271 SetCursor(LoadCursor(NULL, IDC_APPSTARTING));
272 SetCursor(LoadCursor(NULL, IDC_ARROW));
273 /* move back our tool window */
274 SetWindowPos(gToolWindow, HWND_TOPMOST, -200, -200, 0, 0,
275 SWP_NOACTIVATE | SWP_HIDEWINDOW | SWP_NOCOPYBITS | SWP_NOREDRAW | SWP_NOSIZE);
276 }
277 }
278
279 SvcDebugOut2("VBoxService: Window h %p, st %p\n", gToolWindow, status);
280
281 if (status == NO_ERROR)
282 {
283 gStopSem = CreateEvent(NULL, TRUE, FALSE, NULL);
284 if (gStopSem == NULL)
285 {
286 SvcDebugOut("VBoxService: CreateEvent failed: rc = %d\n", GetLastError());
287 return;
288 }
289 }
290
291 /*
292 * Start services listed in the vboxServiceTable.
293 */
294 svcEnv.hInstance = gInstance;
295 svcEnv.hDriver = gVBoxDriver;
296
297 if (status == NO_ERROR)
298 {
299 int rc = vboxStartServices (&svcEnv, vboxServiceTable);
300
301 if (VBOX_FAILURE (rc))
302 {
303 status = ERROR_GEN_FAILURE;
304 }
305 }
306
307 /* create display change thread */
308 HANDLE hDisplayChangeThread;
309 if (status == NO_ERROR)
310 {
311 hDisplayChangeThread = (HANDLE)_beginthread(DisplayChangeThread, 0, NULL);
312 if ((int)hDisplayChangeThread == -1L)
313 status = ERROR_GEN_FAILURE;
314 }
315
316 SvcDebugOut2("VBoxService: hDisplayChangeThread h %p, st %p\n", hDisplayChangeThread, status);
317
318 /* terminate service if something went wrong */
319 if (status != NO_ERROR)
320 {
321 vboxStopServices (&svcEnv, vboxServiceTable);
322 return;
323 }
324
325 BOOL fTrayIconCreated = false;
326
327 /* prepare the system tray icon */
328 NOTIFYICONDATA ndata;
329 memset (&ndata, 0, sizeof (ndata));
330 ndata.cbSize = NOTIFYICONDATA_V1_SIZE; // sizeof(NOTIFYICONDATA);
331 ndata.hWnd = gToolWindow;
332 ndata.uID = 2000;
333 ndata.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
334 ndata.uCallbackMessage = WM_USER;
335 ndata.hIcon = LoadIcon(gInstance, MAKEINTRESOURCE(IDI_VIRTUALBOX));
336 sprintf(ndata.szTip, "InnoTek VirtualBox Guest Additions %d.%d.%d", VBOX_VERSION_MAJOR, VBOX_VERSION_MINOR, VBOX_VERSION_BUILD);
337
338 SvcDebugOut2("VBoxService: ndata.hWnd %08X, ndata.hIcon = %p\n", ndata.hWnd, ndata.hIcon);
339
340 /*
341 * Main execution loop
342 * Wait for the stop semaphore to be posted or a window event to arrive
343 */
344 while(true)
345 {
346 DWORD waitResult = MsgWaitForMultipleObjectsEx(1, &gStopSem, 500, QS_ALLINPUT, 0);
347 if (waitResult == WAIT_OBJECT_0)
348 {
349 SvcDebugOut2("VBoxService: exit\n");
350 /* exit */
351 break;
352 }
353 else if (waitResult == WAIT_OBJECT_0 + 1)
354 {
355 /* a window message, handle it */
356 MSG msg;
357 while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
358 {
359 SvcDebugOut2("VBoxService: msg %p\n", msg.message);
360 if (msg.message == WM_QUIT)
361 {
362 SvcDebugOut2("VBoxService: WM_QUIT!\n");
363 SetEvent(gStopSem);
364 continue;
365 }
366 TranslateMessage(&msg);
367 DispatchMessage(&msg);
368 }
369 }
370 else /* timeout */
371 {
372 SvcDebugOut2("VBoxService: timed out\n");
373
374 /* we might have to repeat this operation because the shell might not be loaded yet */
375 if (!fTrayIconCreated)
376 {
377 fTrayIconCreated = Shell_NotifyIcon(NIM_ADD, &ndata);
378 SvcDebugOut2("VBoxService: fTrayIconCreated = %d, err %08X\n", fTrayIconCreated, GetLastError ());
379 }
380 }
381 }
382
383 SvcDebugOut("VBoxService: returned from main loop, exiting...\n", 0);
384
385 /* remove the system tray icon */
386 Shell_NotifyIcon(NIM_DELETE, &ndata);
387
388 SvcDebugOut("VBoxService: waiting for display change thread...\n", 0);
389
390 /* wait for the display change thread to terminate */
391 WaitForSingleObject(hDisplayChangeThread, INFINITE);
392
393 vboxStopServices (&svcEnv, vboxServiceTable);
394
395 SvcDebugOut("VBoxService: destroying tool window...\n", 0);
396
397 /* destroy the tool window */
398 DestroyWindow(gToolWindow);
399
400 UnregisterClass("VirtualBoxTool", gInstance);
401
402 CloseHandle(gVBoxDriver);
403 CloseHandle(gStopSem);
404
405 SvcDebugOut("VBoxService: leaving service main function\n", 0);
406
407 return;
408}
409
410
411/**
412 * Main function
413 */
414int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
415{
416 SvcDebugOut2("VBoxService: WinMain\n");
417 gInstance = hInstance;
418 VBoxServiceStart ();
419}
420
421static bool isVBoxDisplayDriverActive (void)
422{
423 bool result = false;
424
425 DISPLAY_DEVICE dispDevice;
426
427 FillMemory(&dispDevice, sizeof(DISPLAY_DEVICE), 0);
428
429 dispDevice.cb = sizeof(DISPLAY_DEVICE);
430
431 INT devNum = 0;
432
433 while (EnumDisplayDevices(NULL,
434 devNum,
435 &dispDevice,
436 0))
437 {
438 DDCLOG(("DevNum:%d\nName:%s\nString:%s\nID:%s\nKey:%s\nFlags=%08X\n\n",
439 devNum,
440 &dispDevice.DeviceName[0],
441 &dispDevice.DeviceString[0],
442 &dispDevice.DeviceID[0],
443 &dispDevice.DeviceKey[0],
444 dispDevice.StateFlags));
445
446 if (dispDevice.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE)
447 {
448 DDCLOG(("Primary device.\n"));
449
450 if (strcmp(&dispDevice.DeviceString[0], "VirtualBox Graphics Adapter") == 0)
451 {
452 DDCLOG(("VBox display driver is active.\n"));
453 result = true;
454 }
455
456 break;
457 }
458
459 FillMemory(&dispDevice, sizeof(DISPLAY_DEVICE), 0);
460
461 dispDevice.cb = sizeof(DISPLAY_DEVICE);
462
463 devNum++;
464 }
465
466 return result;
467}
468
469/**
470 * Thread function to wait for and process display change
471 * requests
472 */
473VOID DisplayChangeThread(void *dummy)
474{
475 bool fTerminate = false;
476 VBoxGuestFilterMaskInfo maskInfo;
477 DWORD cbReturned;
478
479 maskInfo.u32OrMask = VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST;
480 maskInfo.u32NotMask = 0;
481 if (DeviceIoControl (gVBoxDriver, IOCTL_VBOXGUEST_CTL_FILTER_MASK, &maskInfo, sizeof (maskInfo), NULL, 0, &cbReturned, NULL))
482 {
483 DDCLOG(("VBoxService: DeviceIOControl(CtlMask - or) succeeded\n"));
484 }
485 else
486 {
487 SvcDebugOut2("VBoxService: DeviceIOControl(CtlMask) failed, DisplayChangeThread exited\n");
488 return;
489 }
490
491 do
492 {
493 /* wait for a display change event */
494 VBoxGuestWaitEventInfo waitEvent;
495 waitEvent.u32TimeoutIn = 100;
496 waitEvent.u32EventMaskIn = VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST;
497 if (DeviceIoControl(gVBoxDriver, IOCTL_VBOXGUEST_WAITEVENT, &waitEvent, sizeof(waitEvent), &waitEvent, sizeof(waitEvent), &cbReturned, NULL))
498 {
499 DDCLOG(("VBoxService: DeviceIOControl succeded\n"));
500
501 /* are we supposed to stop? */
502 if (WaitForSingleObject(gStopSem, 0) == WAIT_OBJECT_0)
503 break;
504
505 DDCLOG(("VBoxService: checking event\n"));
506
507 /* did we get the right event? */
508 if (waitEvent.u32EventFlagsOut & VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST)
509 {
510 DDCLOG(("VBoxService: going to get display change information.\n"));
511
512 /* We got at least one event. Read the requested resolution
513 * and try to set it until success. New events will not be seen
514 * but a new resolution will be read in this poll loop.
515 */
516 for (;;)
517 {
518 /* get the display change request */
519 VMMDevDisplayChangeRequest displayChangeRequest = {0};
520 displayChangeRequest.header.size = sizeof(VMMDevDisplayChangeRequest);
521 displayChangeRequest.header.version = VMMDEV_REQUEST_HEADER_VERSION;
522 displayChangeRequest.header.requestType = VMMDevReq_GetDisplayChangeRequest;
523 displayChangeRequest.eventAck = VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST;
524 if (DeviceIoControl(gVBoxDriver, IOCTL_VBOXGUEST_VMMREQUEST, &displayChangeRequest, sizeof(displayChangeRequest),
525 &displayChangeRequest, sizeof(displayChangeRequest), &cbReturned, NULL))
526 {
527 DDCLOG(("VBoxService: VMMDevReq_GetDisplayChangeRequest: %dx%dx%d\n", displayChangeRequest.xres, displayChangeRequest.yres, displayChangeRequest.bpp));
528
529 /*
530 * Only try to change video mode if the active display driver is VBox additions.
531 */
532 if (isVBoxDisplayDriverActive ())
533 {
534 DEVMODE devMode;
535
536 /* get the current screen setup */
537 if (EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &devMode))
538 {
539 SvcDebugOut2("VBoxService: Current mode: %dx%dx%d\n", devMode.dmPelsWidth, devMode.dmPelsHeight, devMode.dmBitsPerPel);
540
541 /* Horizontal resolution must be a multiple of 8, round down. */
542 displayChangeRequest.xres &= 0xfff8;
543
544 /* Check whether a mode reset or a change is requested. */
545 if (displayChangeRequest.xres || displayChangeRequest.yres || displayChangeRequest.bpp)
546 {
547 /* A change is requested.
548 * Set values which are not to be changed to the current values.
549 */
550 if (!displayChangeRequest.xres)
551 displayChangeRequest.xres = devMode.dmPelsWidth;
552 if (!displayChangeRequest.yres)
553 displayChangeRequest.yres = devMode.dmPelsHeight;
554 if (!displayChangeRequest.bpp)
555 displayChangeRequest.bpp = devMode.dmBitsPerPel;
556 }
557 else
558 {
559 /* All zero values means a forced mode reset. Do nothing. */
560 }
561
562 /* Verify that the mode is indeed changed. */
563 if ( devMode.dmPelsWidth == displayChangeRequest.xres
564 && devMode.dmPelsHeight == displayChangeRequest.yres
565 && devMode.dmBitsPerPel == displayChangeRequest.bpp)
566 {
567 SvcDebugOut2("VBoxService: already at desired resolution.\n");
568 break;
569 }
570
571 // without this, Windows will not ask the miniport for its
572 // mode table but uses an internal cache instead
573 DEVMODE tempDevMode = {0};
574 tempDevMode.dmSize = sizeof(DEVMODE);
575 EnumDisplaySettings(NULL, 0xffffff, &tempDevMode);
576
577 /* adjust the values that are supposed to change */
578 if (displayChangeRequest.xres)
579 devMode.dmPelsWidth = displayChangeRequest.xres;
580 if (displayChangeRequest.yres)
581 devMode.dmPelsHeight = displayChangeRequest.yres;
582 if (displayChangeRequest.bpp)
583 devMode.dmBitsPerPel = displayChangeRequest.bpp;
584
585 DDCLOG(("VBoxService: setting the new mode %dx%dx%d\n", devMode.dmPelsWidth, devMode.dmPelsHeight, devMode.dmBitsPerPel));
586
587 /* set the new mode */
588 LONG status;
589 status = ChangeDisplaySettings(&devMode, CDS_UPDATEREGISTRY);
590 if (status != DISP_CHANGE_SUCCESSFUL)
591 {
592 SvcDebugOut("VBoxService: error from ChangeDisplaySettings: %d\n", status);
593 }
594 else
595 {
596 /* Successfully set new video mode. */
597 break;
598 }
599 }
600 else
601 {
602 SvcDebugOut("VBoxService: error from EnumDisplaySettings\n", 0);
603 }
604 }
605 else
606 {
607 SvcDebugOut2("VBoxService: vboxDisplayDriver is not active.\n", 0);
608 }
609
610 /* Retry the change a bit later. */
611 /* are we supposed to stop? */
612 if (WaitForSingleObject(gStopSem, 1000) == WAIT_OBJECT_0)
613 {
614 fTerminate = true;
615 break;
616 }
617 }
618 else
619 {
620 SvcDebugOut("VBoxService: error from DeviceIoControl IOCTL_VBOXGUEST_VMMREQUEST\n", 0);
621 /* sleep a bit to not eat too much CPU while retrying */
622 /* are we supposed to stop? */
623 if (WaitForSingleObject(gStopSem, 50) == WAIT_OBJECT_0)
624 {
625 fTerminate = true;
626 break;
627 }
628 }
629 }
630 }
631 } else
632 {
633 SvcDebugOut("VBoxService: error from DeviceIoControl IOCTL_VBOXGUEST_WAITEVENT\n", 0);
634 /* sleep a bit to not eat too much CPU in case the above call always fails */
635 if (WaitForSingleObject(gStopSem, 10) == WAIT_OBJECT_0)
636 {
637 fTerminate = true;
638 break;
639 }
640 }
641 } while (!fTerminate);
642
643 maskInfo.u32OrMask = 0;
644 maskInfo.u32NotMask = VMMDEV_EVENT_DISPLAY_CHANGE_REQUEST;
645 if (DeviceIoControl (gVBoxDriver, IOCTL_VBOXGUEST_CTL_FILTER_MASK, &maskInfo, sizeof (maskInfo), NULL, 0, &cbReturned, NULL))
646 {
647 DDCLOG(("VBoxService: DeviceIOControl(CtlMask - not) succeeded\n"));
648 }
649 else
650 {
651 SvcDebugOut2 ("VBoxService: DeviceIOControl(CtlMask) failed\n");
652 }
653
654 SvcDebugOut2("VBoxService: finished display change request thread\n");
655}
656
657/**
658 * Window procedure for our tool window
659 */
660LRESULT CALLBACK VBoxToolWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
661{
662 switch (msg)
663 {
664 case WM_CLOSE:
665 break;
666
667 case WM_DESTROY:
668 break;
669
670 default:
671 return DefWindowProc(hwnd, msg, wParam, lParam);
672 }
673 return 0;
674}
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette