2011-08-31 13:59:03 +04:00
|
|
|
/**
|
2012-10-09 07:02:04 +04:00
|
|
|
* FreeRDP: A Remote Desktop Protocol Implementation
|
2011-08-31 13:59:03 +04:00
|
|
|
* Windows Client
|
|
|
|
*
|
|
|
|
* Copyright 2009-2011 Jay Sorg
|
|
|
|
* Copyright 2010-2011 Vic Lee
|
|
|
|
* Copyright 2010-2011 Marc-Andre Moreau <marcandre.moreau@gmail.com>
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
2012-08-15 01:20:53 +04:00
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include "config.h"
|
|
|
|
#endif
|
|
|
|
|
2012-05-05 03:36:35 +04:00
|
|
|
#include <winpr/windows.h>
|
2012-03-06 06:04:42 +04:00
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
#include <errno.h>
|
|
|
|
#include <stdio.h>
|
|
|
|
#include <stdlib.h>
|
|
|
|
#include <string.h>
|
2012-02-29 02:10:06 +04:00
|
|
|
#include <tchar.h>
|
2011-08-31 13:59:03 +04:00
|
|
|
#include <sys/types.h>
|
|
|
|
|
2011-12-21 04:32:04 +04:00
|
|
|
#ifdef _MSC_VER
|
|
|
|
#include <intrin.h>
|
|
|
|
#endif
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
#include <freerdp/freerdp.h>
|
2011-12-16 23:43:14 +04:00
|
|
|
#include <freerdp/constants.h>
|
2011-08-31 13:59:03 +04:00
|
|
|
#include <freerdp/utils/args.h>
|
|
|
|
#include <freerdp/utils/event.h>
|
2011-10-22 00:45:35 +04:00
|
|
|
#include <freerdp/utils/memory.h>
|
2012-10-02 10:58:54 +04:00
|
|
|
#include <freerdp/utils/load_plugin.h>
|
|
|
|
#include <freerdp/utils/svc_plugin.h>
|
2012-10-28 20:12:36 +04:00
|
|
|
|
|
|
|
#include <freerdp/client/file.h>
|
2012-10-09 20:23:35 +04:00
|
|
|
#include <freerdp/client/channels.h>
|
2011-10-17 07:45:58 +04:00
|
|
|
#include <freerdp/channels/channels.h>
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2011-09-26 00:30:20 +04:00
|
|
|
#include "wf_gdi.h"
|
2011-10-22 00:45:35 +04:00
|
|
|
#include "wf_graphics.h"
|
2012-07-21 01:05:06 +04:00
|
|
|
#include "wf_cliprdr.h"
|
2011-09-26 00:30:20 +04:00
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
#include "wfreerdp.h"
|
|
|
|
|
|
|
|
struct _thread_data
|
|
|
|
{
|
|
|
|
freerdp* instance;
|
|
|
|
};
|
|
|
|
typedef struct _thread_data thread_data;
|
|
|
|
|
|
|
|
HANDLE g_done_event;
|
|
|
|
HINSTANCE g_hInstance;
|
|
|
|
HCURSOR g_default_cursor;
|
|
|
|
volatile int g_thread_count = 0;
|
|
|
|
LPCTSTR g_wnd_class_name = L"wfreerdp";
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
void wf_context_new(freerdp* instance, rdpContext* context)
|
2011-10-17 07:45:58 +04:00
|
|
|
{
|
|
|
|
context->channels = freerdp_channels_new();
|
|
|
|
}
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
void wf_context_free(freerdp* instance, rdpContext* context)
|
2011-10-17 07:45:58 +04:00
|
|
|
{
|
2011-10-22 00:45:35 +04:00
|
|
|
|
2011-10-17 07:45:58 +04:00
|
|
|
}
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
int wf_create_console(void)
|
|
|
|
{
|
|
|
|
if (!AllocConsole())
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
freopen("CONOUT$", "w", stdout);
|
|
|
|
printf("Debug console created.\n");
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2011-12-04 02:24:18 +04:00
|
|
|
void wf_sw_begin_paint(rdpContext* context)
|
2011-10-05 00:05:09 +04:00
|
|
|
{
|
2011-12-04 02:24:18 +04:00
|
|
|
rdpGdi* gdi = context->gdi;
|
2011-10-05 00:05:09 +04:00
|
|
|
gdi->primary->hdc->hwnd->invalid->null = 1;
|
|
|
|
gdi->primary->hdc->hwnd->ninvalid = 0;
|
|
|
|
}
|
|
|
|
|
2011-12-04 02:24:18 +04:00
|
|
|
void wf_sw_end_paint(rdpContext* context)
|
2011-10-05 00:05:09 +04:00
|
|
|
{
|
|
|
|
int i;
|
2011-10-16 08:50:10 +04:00
|
|
|
rdpGdi* gdi;
|
2011-10-05 00:05:09 +04:00
|
|
|
wfInfo* wfi;
|
2012-10-09 11:26:39 +04:00
|
|
|
INT32 x, y;
|
|
|
|
UINT32 w, h;
|
2011-10-05 00:05:09 +04:00
|
|
|
int ninvalid;
|
|
|
|
RECT update_rect;
|
|
|
|
HGDI_RGN cinvalid;
|
|
|
|
|
2011-12-04 02:24:18 +04:00
|
|
|
gdi = context->gdi;
|
|
|
|
wfi = ((wfContext*) context)->wfi;
|
2011-10-05 00:05:09 +04:00
|
|
|
|
|
|
|
if (gdi->primary->hdc->hwnd->ninvalid < 1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
ninvalid = gdi->primary->hdc->hwnd->ninvalid;
|
|
|
|
cinvalid = gdi->primary->hdc->hwnd->cinvalid;
|
|
|
|
|
|
|
|
for (i = 0; i < ninvalid; i++)
|
|
|
|
{
|
|
|
|
x = cinvalid[i].x;
|
|
|
|
y = cinvalid[i].y;
|
|
|
|
w = cinvalid[i].w;
|
|
|
|
h = cinvalid[i].h;
|
|
|
|
|
|
|
|
update_rect.left = x;
|
|
|
|
update_rect.top = y;
|
|
|
|
update_rect.right = x + w - 1;
|
|
|
|
update_rect.bottom = y + h - 1;
|
|
|
|
|
|
|
|
InvalidateRect(wfi->hwnd, &update_rect, FALSE);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2011-12-04 02:24:18 +04:00
|
|
|
void wf_hw_begin_paint(rdpContext* context)
|
2011-10-05 00:05:09 +04:00
|
|
|
{
|
2011-12-04 02:24:18 +04:00
|
|
|
wfInfo* wfi = ((wfContext*) context)->wfi;
|
2011-10-05 00:05:09 +04:00
|
|
|
wfi->hdc->hwnd->invalid->null = 1;
|
|
|
|
wfi->hdc->hwnd->ninvalid = 0;
|
|
|
|
}
|
|
|
|
|
2011-12-04 02:24:18 +04:00
|
|
|
void wf_hw_end_paint(rdpContext* context)
|
2011-10-05 00:05:09 +04:00
|
|
|
{
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2012-10-09 10:38:39 +04:00
|
|
|
BOOL wf_pre_connect(freerdp* instance)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-09-26 00:30:20 +04:00
|
|
|
int i1;
|
2011-08-31 13:59:03 +04:00
|
|
|
wfInfo* wfi;
|
2012-10-28 20:12:36 +04:00
|
|
|
rdpFile* file;
|
2011-10-17 07:45:58 +04:00
|
|
|
wfContext* context;
|
2011-08-31 13:59:03 +04:00
|
|
|
rdpSettings* settings;
|
|
|
|
|
|
|
|
wfi = (wfInfo*) xzalloc(sizeof(wfInfo));
|
2011-10-17 07:45:58 +04:00
|
|
|
context = (wfContext*) instance->context;
|
2011-09-26 01:47:40 +04:00
|
|
|
wfi->instance = instance;
|
2011-10-17 07:45:58 +04:00
|
|
|
context->wfi = wfi;
|
2011-08-31 13:59:03 +04:00
|
|
|
|
|
|
|
settings = instance->settings;
|
|
|
|
|
2012-10-28 20:12:36 +04:00
|
|
|
settings = instance->settings;
|
|
|
|
|
|
|
|
if (settings->connection_file)
|
|
|
|
{
|
|
|
|
file = freerdp_client_rdp_file_new();
|
|
|
|
|
|
|
|
printf("Using connection file: %s\n", settings->connection_file);
|
|
|
|
|
|
|
|
freerdp_client_parse_rdp_file(file, settings->connection_file);
|
|
|
|
freerdp_client_populate_settings_from_rdp_file(file, settings);
|
|
|
|
}
|
|
|
|
|
2011-12-24 11:48:11 +04:00
|
|
|
settings->os_major_type = OSMAJORTYPE_WINDOWS;
|
|
|
|
settings->os_minor_type = OSMINORTYPE_WINDOWS_NT;
|
2012-08-30 01:50:13 +04:00
|
|
|
settings->order_support[NEG_DSTBLT_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_PATBLT_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_SCRBLT_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_OPAQUE_RECT_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_DRAWNINEGRID_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_MULTIDSTBLT_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_MULTIPATBLT_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_MULTISCRBLT_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_MULTIOPAQUERECT_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_MULTI_DRAWNINEGRID_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_LINETO_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_POLYLINE_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_MEMBLT_INDEX] = TRUE;
|
|
|
|
settings->order_support[NEG_MEM3BLT_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_SAVEBITMAP_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_GLYPH_INDEX_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_FAST_INDEX_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_FAST_GLYPH_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_POLYGON_SC_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_POLYGON_CB_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_ELLIPSE_SC_INDEX] = FALSE;
|
|
|
|
settings->order_support[NEG_ELLIPSE_CB_INDEX] = FALSE;
|
|
|
|
|
2012-10-12 23:47:51 +04:00
|
|
|
settings->glyphSupportLevel = GLYPH_SUPPORT_NONE;
|
2012-01-16 20:40:18 +04:00
|
|
|
|
2011-09-26 00:30:20 +04:00
|
|
|
wfi->cursor = g_default_cursor;
|
|
|
|
|
|
|
|
wfi->fullscreen = settings->fullscreen;
|
|
|
|
wfi->fs_toggle = wfi->fullscreen;
|
2011-10-05 00:05:09 +04:00
|
|
|
wfi->sw_gdi = settings->sw_gdi;
|
|
|
|
|
|
|
|
wfi->clrconv = (HCLRCONV) xzalloc(sizeof(CLRCONV));
|
|
|
|
wfi->clrconv->palette = NULL;
|
2012-08-30 01:50:13 +04:00
|
|
|
wfi->clrconv->alpha = FALSE;
|
2011-10-05 00:05:09 +04:00
|
|
|
|
2011-10-17 07:45:58 +04:00
|
|
|
instance->context->cache = cache_new(settings);
|
2011-09-26 00:30:20 +04:00
|
|
|
|
|
|
|
if (wfi->percentscreen > 0)
|
|
|
|
{
|
|
|
|
i1 = (GetSystemMetrics(SM_CXSCREEN) * wfi->percentscreen) / 100;
|
|
|
|
settings->width = i1;
|
|
|
|
|
|
|
|
i1 = (GetSystemMetrics(SM_CYSCREEN) * wfi->percentscreen) / 100;
|
|
|
|
settings->height = i1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (wfi->fs_toggle)
|
|
|
|
{
|
|
|
|
settings->width = GetSystemMetrics(SM_CXSCREEN);
|
|
|
|
settings->height = GetSystemMetrics(SM_CYSCREEN);
|
|
|
|
}
|
|
|
|
|
|
|
|
i1 = settings->width;
|
|
|
|
i1 = (i1 + 3) & (~3);
|
|
|
|
settings->width = i1;
|
|
|
|
|
|
|
|
if ((settings->width < 64) || (settings->height < 64) ||
|
|
|
|
(settings->width > 4096) || (settings->height > 4096))
|
|
|
|
{
|
|
|
|
printf("wf_pre_connect: invalid dimensions %d %d\n", settings->width, settings->height);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
settings->kbd_layout = (int) GetKeyboardLayout(0) & 0x0000FFFF;
|
2011-10-22 00:45:35 +04:00
|
|
|
freerdp_channels_pre_connect(instance->context->channels, instance);
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
return TRUE;
|
2011-08-31 13:59:03 +04:00
|
|
|
}
|
|
|
|
|
2011-12-16 23:43:14 +04:00
|
|
|
void cpuid(unsigned info, unsigned *eax, unsigned *ebx, unsigned *ecx, unsigned *edx)
|
|
|
|
{
|
2011-12-21 04:32:04 +04:00
|
|
|
#if defined(__GNUC__)
|
2011-12-16 23:43:14 +04:00
|
|
|
#if defined(__i386__) || defined(__x86_64__)
|
|
|
|
*eax = info;
|
|
|
|
__asm volatile
|
|
|
|
("mov %%ebx, %%edi;" /* 32bit PIC: don't clobber ebx */
|
|
|
|
"cpuid;"
|
|
|
|
"mov %%ebx, %%esi;"
|
|
|
|
"mov %%edi, %%ebx;"
|
|
|
|
:"+a" (*eax), "=S" (*ebx), "=c" (*ecx), "=d" (*edx)
|
|
|
|
: :"edi");
|
|
|
|
#endif
|
2011-12-21 04:32:04 +04:00
|
|
|
#elif defined(_MSC_VER)
|
|
|
|
int a[4];
|
|
|
|
__cpuid(a, info);
|
|
|
|
*eax = a[0];
|
|
|
|
*ebx = a[1];
|
|
|
|
*ecx = a[2];
|
|
|
|
*edx = a[3];
|
2011-12-16 23:43:14 +04:00
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2012-10-09 11:26:39 +04:00
|
|
|
UINT32 wfi_detect_cpu()
|
2011-12-16 23:43:14 +04:00
|
|
|
{
|
2012-10-09 11:26:39 +04:00
|
|
|
UINT32 cpu_opt = 0;
|
2011-12-16 23:43:14 +04:00
|
|
|
unsigned int eax, ebx, ecx, edx = 0;
|
|
|
|
|
|
|
|
cpuid(1, &eax, &ebx, &ecx, &edx);
|
|
|
|
|
|
|
|
if (edx & (1<<26))
|
|
|
|
{
|
|
|
|
cpu_opt |= CPU_SSE2;
|
|
|
|
}
|
|
|
|
|
|
|
|
return cpu_opt;
|
|
|
|
}
|
|
|
|
|
2012-10-09 10:38:39 +04:00
|
|
|
BOOL wf_post_connect(freerdp* instance)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-10-16 08:50:10 +04:00
|
|
|
rdpGdi* gdi;
|
2011-08-31 13:59:03 +04:00
|
|
|
wfInfo* wfi;
|
2011-10-17 07:45:58 +04:00
|
|
|
rdpCache* cache;
|
|
|
|
wfContext* context;
|
2011-09-26 00:30:20 +04:00
|
|
|
int width, height;
|
|
|
|
wchar_t win_title[64];
|
|
|
|
rdpSettings* settings;
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2011-09-26 00:30:20 +04:00
|
|
|
settings = instance->settings;
|
2011-10-17 07:45:58 +04:00
|
|
|
context = (wfContext*) instance->context;
|
|
|
|
cache = instance->context->cache;
|
|
|
|
wfi = context->wfi;
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2011-10-05 00:05:09 +04:00
|
|
|
wfi->dstBpp = 32;
|
2011-09-26 00:30:20 +04:00
|
|
|
width = settings->width;
|
|
|
|
height = settings->height;
|
|
|
|
|
2011-10-05 00:05:09 +04:00
|
|
|
if (wfi->sw_gdi)
|
|
|
|
{
|
2011-12-13 20:36:05 +04:00
|
|
|
gdi_init(instance, CLRCONV_ALPHA | CLRCONV_INVERT | CLRBUF_32BPP, NULL);
|
2011-10-17 07:45:58 +04:00
|
|
|
gdi = instance->context->gdi;
|
2011-10-05 00:05:09 +04:00
|
|
|
wfi->hdc = gdi->primary->hdc;
|
2011-10-07 22:55:07 +04:00
|
|
|
wfi->primary = wf_image_new(wfi, width, height, wfi->dstBpp, gdi->primary_buffer);
|
2011-12-21 04:32:04 +04:00
|
|
|
|
|
|
|
rfx_context_set_cpu_opt(gdi->rfx_context, wfi_detect_cpu());
|
2011-10-05 00:05:09 +04:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
wf_gdi_register_update_callbacks(instance->update);
|
2011-10-08 17:42:50 +04:00
|
|
|
wfi->srcBpp = instance->settings->color_depth;
|
2011-10-05 00:05:09 +04:00
|
|
|
wfi->primary = wf_image_new(wfi, width, height, wfi->dstBpp, NULL);
|
|
|
|
|
|
|
|
wfi->hdc = gdi_GetDC();
|
|
|
|
wfi->hdc->bitsPerPixel = wfi->dstBpp;
|
|
|
|
wfi->hdc->bytesPerPixel = wfi->dstBpp / 8;
|
|
|
|
|
|
|
|
wfi->hdc->alpha = wfi->clrconv->alpha;
|
|
|
|
wfi->hdc->invert = wfi->clrconv->invert;
|
|
|
|
|
2012-10-09 07:21:26 +04:00
|
|
|
wfi->hdc->hwnd = (HGDI_WND) malloc(sizeof(GDI_WND));
|
2011-10-05 00:05:09 +04:00
|
|
|
wfi->hdc->hwnd->invalid = gdi_CreateRectRgn(0, 0, 0, 0);
|
|
|
|
wfi->hdc->hwnd->invalid->null = 1;
|
|
|
|
|
|
|
|
wfi->hdc->hwnd->count = 32;
|
2012-10-09 07:21:26 +04:00
|
|
|
wfi->hdc->hwnd->cinvalid = (HGDI_RGN) malloc(sizeof(GDI_RGN) * wfi->hdc->hwnd->count);
|
2011-10-05 00:05:09 +04:00
|
|
|
wfi->hdc->hwnd->ninvalid = 0;
|
2011-12-16 23:43:14 +04:00
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
wfi->image = wf_image_new(wfi, 64, 64, 32, NULL);
|
2011-12-21 04:32:04 +04:00
|
|
|
wfi->image->_bitmap.data = NULL;
|
|
|
|
|
2011-12-16 23:43:14 +04:00
|
|
|
if (settings->rfx_codec)
|
|
|
|
{
|
2012-08-30 01:50:13 +04:00
|
|
|
wfi->tile = wf_image_new(wfi, 64, 64, 32, NULL);
|
2011-12-16 23:43:14 +04:00
|
|
|
wfi->rfx_context = rfx_context_new();
|
|
|
|
rfx_context_set_cpu_opt(wfi->rfx_context, wfi_detect_cpu());
|
|
|
|
}
|
|
|
|
|
|
|
|
if (settings->ns_codec)
|
|
|
|
wfi->nsc_context = nsc_context_new();
|
2011-10-05 00:05:09 +04:00
|
|
|
}
|
|
|
|
|
2011-12-09 22:53:19 +04:00
|
|
|
if (settings->window_title != NULL)
|
2012-08-29 02:01:42 +04:00
|
|
|
_snwprintf(win_title, ARRAY_SIZE(win_title), L"%S", settings->window_title);
|
2011-09-26 00:30:20 +04:00
|
|
|
else if (settings->port == 3389)
|
2012-02-28 21:11:11 +04:00
|
|
|
_snwprintf(win_title, ARRAY_SIZE(win_title), L"FreeRDP: %S", settings->hostname);
|
2011-09-26 00:30:20 +04:00
|
|
|
else
|
2012-02-28 21:11:11 +04:00
|
|
|
_snwprintf(win_title, ARRAY_SIZE(win_title), L"FreeRDP: %S:%d", settings->hostname, settings->port);
|
2011-09-26 00:30:20 +04:00
|
|
|
|
|
|
|
if (wfi->hwnd == 0)
|
|
|
|
{
|
|
|
|
wfi->hwnd = CreateWindowEx((DWORD) NULL, g_wnd_class_name, win_title,
|
|
|
|
0, 0, 0, 0, 0, NULL, NULL, g_hInstance, NULL);
|
|
|
|
|
|
|
|
SetWindowLongPtr(wfi->hwnd, GWLP_USERDATA, (LONG_PTR) wfi);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (wfi->fullscreen)
|
|
|
|
{
|
|
|
|
SetWindowLongPtr(wfi->hwnd, GWL_STYLE, WS_POPUP);
|
|
|
|
SetWindowPos(wfi->hwnd, HWND_TOP, 0, 0, width, height, SWP_FRAMECHANGED);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
POINT diff;
|
|
|
|
RECT rc_client, rc_wnd;
|
|
|
|
|
|
|
|
SetWindowLongPtr(wfi->hwnd, GWL_STYLE, WS_CAPTION | WS_OVERLAPPED | WS_SYSMENU | WS_MINIMIZEBOX);
|
|
|
|
/* Now resize to get full canvas size and room for caption and borders */
|
|
|
|
SetWindowPos(wfi->hwnd, HWND_TOP, 10, 10, width, height, SWP_FRAMECHANGED);
|
|
|
|
GetClientRect(wfi->hwnd, &rc_client);
|
|
|
|
GetWindowRect(wfi->hwnd, &rc_wnd);
|
|
|
|
diff.x = (rc_wnd.right - rc_wnd.left) - rc_client.right;
|
|
|
|
diff.y = (rc_wnd.bottom - rc_wnd.top) - rc_client.bottom;
|
|
|
|
SetWindowPos(wfi->hwnd, HWND_TOP, -1, -1, width + diff.x, height + diff.y, SWP_NOMOVE | SWP_FRAMECHANGED);
|
|
|
|
}
|
|
|
|
|
2011-10-05 00:05:09 +04:00
|
|
|
BitBlt(wfi->primary->hdc, 0, 0, width, height, NULL, 0, 0, BLACKNESS);
|
2011-09-26 00:30:20 +04:00
|
|
|
wfi->drawing = wfi->primary;
|
|
|
|
|
|
|
|
ShowWindow(wfi->hwnd, SW_SHOWNORMAL);
|
|
|
|
UpdateWindow(wfi->hwnd);
|
|
|
|
|
2011-10-05 00:05:09 +04:00
|
|
|
if (wfi->sw_gdi)
|
|
|
|
{
|
|
|
|
instance->update->BeginPaint = wf_sw_begin_paint;
|
|
|
|
instance->update->EndPaint = wf_sw_end_paint;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
instance->update->BeginPaint = wf_hw_begin_paint;
|
|
|
|
instance->update->EndPaint = wf_hw_end_paint;
|
|
|
|
}
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
pointer_cache_register_callbacks(instance->update);
|
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
if (wfi->sw_gdi != TRUE)
|
2011-10-14 04:10:59 +04:00
|
|
|
{
|
2011-10-22 00:45:35 +04:00
|
|
|
brush_cache_register_callbacks(instance->update);
|
2011-10-14 04:10:59 +04:00
|
|
|
bitmap_cache_register_callbacks(instance->update);
|
|
|
|
offscreen_cache_register_callbacks(instance->update);
|
|
|
|
}
|
|
|
|
|
2011-10-26 23:38:50 +04:00
|
|
|
wf_register_graphics(instance->context->graphics);
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
freerdp_channels_post_connect(instance->context->channels, instance);
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2012-07-21 01:05:06 +04:00
|
|
|
wf_cliprdr_init(wfi, instance->context->channels);
|
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
return TRUE;
|
2011-08-31 13:59:03 +04:00
|
|
|
}
|
|
|
|
|
2012-10-09 10:38:39 +04:00
|
|
|
BOOL wf_verify_certificate(freerdp* instance, char* subject, char* issuer, char* fingerprint)
|
2011-10-18 19:02:05 +04:00
|
|
|
{
|
2012-02-29 02:10:06 +04:00
|
|
|
#if 0
|
|
|
|
DWORD mode;
|
|
|
|
int read_size;
|
|
|
|
DWORD read_count;
|
|
|
|
TCHAR answer[2];
|
|
|
|
TCHAR* read_buffer;
|
|
|
|
HANDLE input_handle;
|
|
|
|
#endif
|
|
|
|
|
|
|
|
printf("Certificate details:\n");
|
|
|
|
printf("\tSubject: %s\n", subject);
|
|
|
|
printf("\tIssuer: %s\n", issuer);
|
|
|
|
printf("\tThumbprint: %s\n", fingerprint);
|
|
|
|
printf("The above X.509 certificate could not be verified, possibly because you do not have "
|
|
|
|
"the CA certificate in your certificate store, or the certificate has expired. "
|
|
|
|
"Please look at the documentation on how to create local certificate store for a private CA.\n");
|
|
|
|
|
|
|
|
/* TODO: ask for user validation */
|
|
|
|
#if 0
|
|
|
|
input_handle = GetStdHandle(STD_INPUT_HANDLE);
|
|
|
|
|
|
|
|
GetConsoleMode(input_handle, &mode);
|
|
|
|
mode |= ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT;
|
|
|
|
SetConsoleMode(input_handle, mode);
|
|
|
|
#endif
|
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
return TRUE;
|
2011-10-18 19:02:05 +04:00
|
|
|
}
|
|
|
|
|
2012-10-09 11:01:37 +04:00
|
|
|
int wf_receive_channel_data(freerdp* instance, int channelId, BYTE* data, int size, int flags, int total_size)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-10-16 22:57:15 +04:00
|
|
|
return freerdp_channels_data(instance, channelId, data, size, flags, total_size);
|
2011-08-31 13:59:03 +04:00
|
|
|
}
|
|
|
|
|
2011-10-17 07:45:58 +04:00
|
|
|
void wf_process_channel_event(rdpChannels* channels, freerdp* instance)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
|
|
|
RDP_EVENT* event;
|
|
|
|
|
2011-10-17 07:45:58 +04:00
|
|
|
event = freerdp_channels_pop_event(channels);
|
2011-08-31 13:59:03 +04:00
|
|
|
|
|
|
|
if (event)
|
|
|
|
freerdp_event_free(event);
|
|
|
|
}
|
|
|
|
|
2012-10-09 10:38:39 +04:00
|
|
|
BOOL wf_get_fds(freerdp* instance, void** rfds, int* rcount, void** wfds, int* wcount)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2012-08-30 01:50:13 +04:00
|
|
|
return TRUE;
|
2011-08-31 13:59:03 +04:00
|
|
|
}
|
|
|
|
|
2012-10-09 10:38:39 +04:00
|
|
|
BOOL wf_check_fds(freerdp* instance)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2012-08-30 01:50:13 +04:00
|
|
|
return TRUE;
|
2011-08-31 13:59:03 +04:00
|
|
|
}
|
|
|
|
|
2011-09-25 11:12:29 +04:00
|
|
|
int wf_process_plugin_args(rdpSettings* settings, const char* name, RDP_PLUGIN_DATA* plugin_data, void* user_data)
|
|
|
|
{
|
2012-10-09 20:23:35 +04:00
|
|
|
void* entry = NULL;
|
2011-10-17 07:45:58 +04:00
|
|
|
rdpChannels* channels = (rdpChannels*) user_data;
|
2011-09-25 11:12:29 +04:00
|
|
|
|
2012-10-16 20:41:16 +04:00
|
|
|
entry = freerdp_channels_client_find_static_entry("VirtualChannelEntry", name);
|
2012-10-09 20:23:35 +04:00
|
|
|
|
|
|
|
if (entry)
|
|
|
|
{
|
|
|
|
if (freerdp_channels_client_load(channels, settings, entry, plugin_data) == 0)
|
|
|
|
{
|
|
|
|
printf("loading channel %s (static)\n", name);
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
printf("loading channel %s (plugin)\n", name);
|
2011-10-17 07:45:58 +04:00
|
|
|
freerdp_channels_load_plugin(channels, settings, name, plugin_data);
|
2011-09-25 11:12:29 +04:00
|
|
|
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
int wf_process_client_args(rdpSettings* settings, const char* opt, const char* val, void* user_data)
|
2011-09-25 11:12:29 +04:00
|
|
|
{
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
int wfreerdp_run(freerdp* instance)
|
|
|
|
{
|
|
|
|
MSG msg;
|
|
|
|
int index;
|
|
|
|
int rcount;
|
|
|
|
int wcount;
|
2011-11-12 10:41:28 +04:00
|
|
|
BOOL msg_ret;
|
|
|
|
int quit_msg;
|
2011-08-31 13:59:03 +04:00
|
|
|
void* rfds[32];
|
|
|
|
void* wfds[32];
|
|
|
|
int fds_count;
|
|
|
|
HANDLE fds[64];
|
2011-10-17 07:45:58 +04:00
|
|
|
rdpChannels* channels;
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2011-09-25 11:12:29 +04:00
|
|
|
memset(rfds, 0, sizeof(rfds));
|
|
|
|
memset(wfds, 0, sizeof(wfds));
|
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
if (freerdp_connect(instance) != TRUE)
|
2011-09-25 11:12:29 +04:00
|
|
|
return 0;
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
channels = instance->context->channels;
|
2011-08-31 13:59:03 +04:00
|
|
|
|
|
|
|
while (1)
|
|
|
|
{
|
|
|
|
rcount = 0;
|
|
|
|
wcount = 0;
|
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
if (freerdp_get_fds(instance, rfds, &rcount, wfds, &wcount) != TRUE)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
|
|
|
printf("Failed to get FreeRDP file descriptor\n");
|
|
|
|
break;
|
|
|
|
}
|
2012-08-30 01:50:13 +04:00
|
|
|
if (wf_get_fds(instance, rfds, &rcount, wfds, &wcount) != TRUE)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-09-25 11:12:29 +04:00
|
|
|
printf("Failed to get wfreerdp file descriptor\n");
|
2011-08-31 13:59:03 +04:00
|
|
|
break;
|
|
|
|
}
|
2012-08-30 01:50:13 +04:00
|
|
|
if (freerdp_channels_get_fds(channels, instance, rfds, &rcount, wfds, &wcount) != TRUE)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-09-25 11:12:29 +04:00
|
|
|
printf("Failed to get channel manager file descriptor\n");
|
2011-08-31 13:59:03 +04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
fds_count = 0;
|
|
|
|
/* setup read fds */
|
|
|
|
for (index = 0; index < rcount; index++)
|
|
|
|
{
|
|
|
|
fds[fds_count++] = rfds[index];
|
|
|
|
}
|
|
|
|
/* setup write fds */
|
|
|
|
for (index = 0; index < wcount; index++)
|
|
|
|
{
|
|
|
|
fds[fds_count++] = wfds[index];
|
|
|
|
}
|
|
|
|
/* exit if nothing to do */
|
|
|
|
if (fds_count == 0)
|
|
|
|
{
|
|
|
|
printf("wfreerdp_run: fds_count is zero\n");
|
|
|
|
break;
|
|
|
|
}
|
2011-11-12 10:41:28 +04:00
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
/* do the wait */
|
2012-10-01 23:09:28 +04:00
|
|
|
if (MsgWaitForMultipleObjects(fds_count, fds, FALSE, 1000, QS_ALLINPUT) == WAIT_FAILED)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-09-25 11:12:29 +04:00
|
|
|
printf("wfreerdp_run: WaitForMultipleObjects failed: 0x%04X\n", GetLastError());
|
2011-08-31 13:59:03 +04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
if (freerdp_check_fds(instance) != TRUE)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
|
|
|
printf("Failed to check FreeRDP file descriptor\n");
|
|
|
|
break;
|
|
|
|
}
|
2012-10-02 18:28:04 +04:00
|
|
|
if (freerdp_shall_disconnect(instance))
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
2012-08-30 01:50:13 +04:00
|
|
|
if (wf_check_fds(instance) != TRUE)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
|
|
|
printf("Failed to check wfreerdp file descriptor\n");
|
|
|
|
break;
|
|
|
|
}
|
2012-08-30 01:50:13 +04:00
|
|
|
if (freerdp_channels_check_fds(channels, instance) != TRUE)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
|
|
|
printf("Failed to check channel manager file descriptor\n");
|
|
|
|
break;
|
|
|
|
}
|
2011-10-17 07:45:58 +04:00
|
|
|
wf_process_channel_event(channels, instance);
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2011-11-12 10:41:28 +04:00
|
|
|
quit_msg = FALSE;
|
|
|
|
while (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-11-12 10:41:28 +04:00
|
|
|
msg_ret = GetMessage(&msg, NULL, 0, 0);
|
|
|
|
|
|
|
|
if (msg_ret == 0 || msg_ret == -1)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-11-12 10:41:28 +04:00
|
|
|
quit_msg = TRUE;
|
2011-08-31 13:59:03 +04:00
|
|
|
break;
|
|
|
|
}
|
2011-11-12 10:41:28 +04:00
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
TranslateMessage(&msg);
|
|
|
|
DispatchMessage(&msg);
|
|
|
|
}
|
2011-11-12 10:41:28 +04:00
|
|
|
|
|
|
|
if (quit_msg)
|
2011-08-31 13:59:03 +04:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* cleanup */
|
2012-08-30 01:50:13 +04:00
|
|
|
|
|
|
|
freerdp_channels_close(channels, instance);
|
2011-10-17 07:45:58 +04:00
|
|
|
freerdp_channels_free(channels);
|
2012-08-30 01:50:13 +04:00
|
|
|
freerdp_disconnect(instance);
|
2011-08-31 13:59:03 +04:00
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
static DWORD WINAPI thread_func(LPVOID lpParam)
|
|
|
|
{
|
|
|
|
wfInfo* wfi;
|
|
|
|
freerdp* instance;
|
|
|
|
thread_data* data;
|
|
|
|
|
|
|
|
data = (thread_data*) lpParam;
|
|
|
|
instance = data->instance;
|
|
|
|
|
|
|
|
wfi = (wfInfo*) xzalloc(sizeof(wfInfo));
|
2011-10-17 07:45:58 +04:00
|
|
|
((wfContext*) instance->context)->wfi = wfi;
|
2011-09-26 00:30:20 +04:00
|
|
|
wfi->instance = instance;
|
2011-08-31 13:59:03 +04:00
|
|
|
|
|
|
|
wfreerdp_run(instance);
|
2011-09-25 11:12:29 +04:00
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
g_thread_count--;
|
|
|
|
|
|
|
|
if (g_thread_count < 1)
|
|
|
|
SetEvent(g_done_event);
|
|
|
|
|
|
|
|
return (DWORD) NULL;
|
|
|
|
}
|
|
|
|
|
|
|
|
static DWORD WINAPI kbd_thread_func(LPVOID lpParam)
|
|
|
|
{
|
|
|
|
MSG msg;
|
2011-09-26 00:30:20 +04:00
|
|
|
BOOL status;
|
2011-08-31 13:59:03 +04:00
|
|
|
HHOOK hook_handle;
|
|
|
|
|
|
|
|
hook_handle = SetWindowsHookEx(WH_KEYBOARD_LL, wf_ll_kbd_proc, g_hInstance, 0);
|
|
|
|
|
|
|
|
if (hook_handle)
|
|
|
|
{
|
2012-01-16 20:40:18 +04:00
|
|
|
while ((status = GetMessage( &msg, NULL, 0, 0 )) != 0)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2011-09-26 00:30:20 +04:00
|
|
|
if (status == -1)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
|
|
|
printf("keyboard thread error getting message\n");
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
TranslateMessage(&msg);
|
|
|
|
DispatchMessage(&msg);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
UnhookWindowsHookEx(hook_handle);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
printf("failed to install keyboard hook\n");
|
|
|
|
|
|
|
|
return (DWORD) NULL;
|
|
|
|
}
|
|
|
|
|
2012-10-02 10:58:54 +04:00
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
|
|
|
{
|
|
|
|
freerdp* instance;
|
|
|
|
thread_data* data;
|
|
|
|
WSADATA wsa_data;
|
|
|
|
WNDCLASSEX wnd_cls;
|
|
|
|
|
2012-01-10 01:19:28 +04:00
|
|
|
if (NULL == getenv("HOME"))
|
|
|
|
{
|
|
|
|
char home[MAX_PATH * 2] = "HOME=";
|
|
|
|
strcat(home, getenv("HOMEDRIVE"));
|
|
|
|
strcat(home, getenv("HOMEPATH"));
|
|
|
|
_putenv(home);
|
|
|
|
}
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
if (WSAStartup(0x101, &wsa_data) != 0)
|
|
|
|
return 1;
|
|
|
|
|
|
|
|
g_done_event = CreateEvent(0, 1, 0, 0);
|
|
|
|
|
|
|
|
#if defined(WITH_DEBUG) || defined(_DEBUG)
|
|
|
|
wf_create_console();
|
|
|
|
#endif
|
|
|
|
|
|
|
|
g_default_cursor = LoadCursor(NULL, IDC_ARROW);
|
|
|
|
|
|
|
|
wnd_cls.cbSize = sizeof(WNDCLASSEX);
|
|
|
|
wnd_cls.style = CS_HREDRAW | CS_VREDRAW;
|
2011-09-26 00:30:20 +04:00
|
|
|
wnd_cls.lpfnWndProc = wf_event_proc;
|
2011-08-31 13:59:03 +04:00
|
|
|
wnd_cls.cbClsExtra = 0;
|
|
|
|
wnd_cls.cbWndExtra = 0;
|
|
|
|
wnd_cls.hIcon = LoadIcon(NULL, IDI_APPLICATION);
|
|
|
|
wnd_cls.hCursor = g_default_cursor;
|
2011-09-25 22:54:07 +04:00
|
|
|
wnd_cls.hbrBackground = (HBRUSH) GetStockObject(WHITE_BRUSH);
|
2011-08-31 13:59:03 +04:00
|
|
|
wnd_cls.lpszMenuName = NULL;
|
|
|
|
wnd_cls.lpszClassName = g_wnd_class_name;
|
|
|
|
wnd_cls.hInstance = hInstance;
|
|
|
|
wnd_cls.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
|
|
|
|
RegisterClassEx(&wnd_cls);
|
|
|
|
|
|
|
|
g_hInstance = hInstance;
|
2011-10-16 22:57:15 +04:00
|
|
|
freerdp_channels_global_init();
|
2011-08-31 13:59:03 +04:00
|
|
|
|
|
|
|
instance = freerdp_new();
|
|
|
|
instance->PreConnect = wf_pre_connect;
|
|
|
|
instance->PostConnect = wf_post_connect;
|
2011-10-18 19:02:05 +04:00
|
|
|
instance->VerifyCertificate = wf_verify_certificate;
|
2011-08-31 13:59:03 +04:00
|
|
|
instance->ReceiveChannelData = wf_receive_channel_data;
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
instance->context_size = sizeof(wfContext);
|
|
|
|
instance->ContextNew = wf_context_new;
|
|
|
|
instance->ContextFree = wf_context_free;
|
2011-10-17 07:45:58 +04:00
|
|
|
freerdp_context_new(instance);
|
|
|
|
|
2011-10-22 00:45:35 +04:00
|
|
|
instance->context->argc = __argc;
|
|
|
|
instance->context->argv = __argv;
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2012-10-02 10:58:54 +04:00
|
|
|
if (!CreateThread(NULL, 0, kbd_thread_func, NULL, 0, NULL))
|
2011-08-31 13:59:03 +04:00
|
|
|
printf("error creating keyboard handler thread");
|
|
|
|
|
2011-09-26 00:30:20 +04:00
|
|
|
//while (1)
|
2011-08-31 13:59:03 +04:00
|
|
|
{
|
2012-03-31 01:27:21 +04:00
|
|
|
int arg_parse_result;
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
data = (thread_data*) xzalloc(sizeof(thread_data));
|
|
|
|
data->instance = instance;
|
|
|
|
|
2012-03-31 01:27:21 +04:00
|
|
|
arg_parse_result = freerdp_parse_args(instance->settings, __argc, __argv,
|
2011-10-22 00:45:35 +04:00
|
|
|
wf_process_plugin_args, instance->context->channels, wf_process_client_args, NULL);
|
2011-08-31 13:59:03 +04:00
|
|
|
|
2012-03-31 01:27:21 +04:00
|
|
|
if (arg_parse_result < 0)
|
|
|
|
{
|
|
|
|
if (arg_parse_result == FREERDP_ARGS_PARSE_FAILURE)
|
|
|
|
printf("failed to parse arguments.\n");
|
|
|
|
|
|
|
|
#ifdef _DEBUG
|
|
|
|
system("pause");
|
|
|
|
#endif
|
|
|
|
exit(-1);
|
|
|
|
}
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
if (CreateThread(NULL, 0, thread_func, data, 0, NULL) != 0)
|
|
|
|
g_thread_count++;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (g_thread_count > 0)
|
|
|
|
WaitForSingleObject(g_done_event, INFINITE);
|
|
|
|
else
|
|
|
|
MessageBox(GetConsoleWindow(),
|
|
|
|
L"Failed to start wfreerdp.\n\nPlease check the debug output.",
|
|
|
|
L"FreeRDP Error", MB_ICONSTOP);
|
|
|
|
|
2012-08-30 01:50:13 +04:00
|
|
|
freerdp_context_free(instance);
|
|
|
|
freerdp_free(instance);
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
WSACleanup();
|
|
|
|
|
2011-09-26 00:30:20 +04:00
|
|
|
#ifdef _DEBUG
|
|
|
|
system("pause");
|
|
|
|
#endif
|
|
|
|
|
2011-08-31 13:59:03 +04:00
|
|
|
return 0;
|
|
|
|
}
|