rail: work on splitting X11 calls out to xcommon

master
Jay Sorg 12 years ago
parent cddcee4929
commit eafef9cd09

@ -28,6 +28,9 @@
#include "list.h" #include "list.h"
#include "file.h" #include "file.h"
#include "file_loc.h" #include "file_loc.h"
#include "log.h"
#include "rail.h"
#include "xcommon.h"
static struct trans* g_lis_trans = 0; static struct trans* g_lis_trans = 0;
static struct trans* g_con_trans = 0; static struct trans* g_con_trans = 0;
@ -36,6 +39,7 @@ static int g_num_chan_items = 0;
static int g_cliprdr_index = -1; static int g_cliprdr_index = -1;
static int g_rdpsnd_index = -1; static int g_rdpsnd_index = -1;
static int g_rdpdr_index = -1; static int g_rdpdr_index = -1;
static int g_rail_index = -1;
static tbus g_term_event = 0; static tbus g_term_event = 0;
static tbus g_thread_done_event = 0; static tbus g_thread_done_event = 0;
@ -46,17 +50,18 @@ int g_display_num = 0;
int g_cliprdr_chan_id = -1; /* cliprdr */ int g_cliprdr_chan_id = -1; /* cliprdr */
int g_rdpsnd_chan_id = -1; /* rdpsnd */ int g_rdpsnd_chan_id = -1; /* rdpsnd */
int g_rdpdr_chan_id = -1; /* rdpdr */ int g_rdpdr_chan_id = -1; /* rdpdr */
int g_rail_chan_id = -1; /* rail */
/*****************************************************************************/ /*****************************************************************************/
/* returns error */ /* returns error */
int APP_CC int APP_CC
send_channel_data(int chan_id, char* data, int size) send_channel_data(int chan_id, char* data, int size)
{ {
struct stream * s = (struct stream *)NULL; struct stream * s;
int chan_flags = 0; int chan_flags;
int total_size = 0; int total_size;
int sent = 0; int sent;
int rv = 0; int rv;
s = trans_get_out_s(g_con_trans, 8192); s = trans_get_out_s(g_con_trans, 8192);
if (s == 0) if (s == 0)
@ -176,18 +181,20 @@ process_message_init(struct stream* s)
static int APP_CC static int APP_CC
process_message_channel_setup(struct stream* s) process_message_channel_setup(struct stream* s)
{ {
int num_chans = 0; int num_chans;
int index = 0; int index;
int rv = 0; int rv;
struct chan_item* ci = (struct chan_item *)NULL; struct chan_item* ci;
g_num_chan_items = 0; g_num_chan_items = 0;
g_cliprdr_index = -1; g_cliprdr_index = -1;
g_rdpsnd_index = -1; g_rdpsnd_index = -1;
g_rdpdr_index = -1; g_rdpdr_index = -1;
g_rail_index = -1;
g_cliprdr_chan_id = -1; g_cliprdr_chan_id = -1;
g_rdpsnd_chan_id = -1; g_rdpsnd_chan_id = -1;
g_rdpdr_chan_id = -1; g_rdpdr_chan_id = -1;
g_rail_chan_id = -1;
LOGM((LOG_LEVEL_DEBUG, "process_message_channel_setup:")); LOGM((LOG_LEVEL_DEBUG, "process_message_channel_setup:"));
in_uint16_le(s, num_chans); in_uint16_le(s, num_chans);
LOGM((LOG_LEVEL_DEBUG, "process_message_channel_setup: num_chans %d", LOGM((LOG_LEVEL_DEBUG, "process_message_channel_setup: num_chans %d",
@ -216,6 +223,11 @@ process_message_channel_setup(struct stream* s)
g_rdpdr_index = g_num_chan_items; g_rdpdr_index = g_num_chan_items;
g_rdpdr_chan_id = ci->id; g_rdpdr_chan_id = ci->id;
} }
else if (g_strcasecmp(ci->name, "rail") == 0)
{
g_rail_index = g_num_chan_items;
g_rail_chan_id = ci->id;
}
g_num_chan_items++; g_num_chan_items++;
} }
rv = send_channel_setup_response_message(); rv = send_channel_setup_response_message();
@ -231,6 +243,10 @@ process_message_channel_setup(struct stream* s)
{ {
dev_redir_init(); dev_redir_init();
} }
if (g_rail_index >= 0)
{
rail_init();
}
return rv; return rv;
} }
@ -266,6 +282,10 @@ process_message_channel_data(struct stream* s)
{ {
rv = dev_redir_data_in(s, chan_id, chan_flags, length, total_length); rv = dev_redir_data_in(s, chan_id, chan_flags, length, total_length);
} }
else if (chan_id == g_rail_chan_id)
{
rv = rail_data_in(s, chan_id, chan_flags, length, total_length);
}
} }
return rv; return rv;
} }
@ -433,10 +453,10 @@ THREAD_RV THREAD_CC
channel_thread_loop(void* in_val) channel_thread_loop(void* in_val)
{ {
tbus objs[32]; tbus objs[32];
int num_objs = 0; int num_objs;
int timeout = 0; int timeout;
int error = 0; int error;
THREAD_RV rv = 0; THREAD_RV rv;
LOGM((LOG_LEVEL_INFO, "channel_thread_loop: thread start")); LOGM((LOG_LEVEL_INFO, "channel_thread_loop: thread start"));
rv = 0; rv = 0;
@ -456,6 +476,7 @@ channel_thread_loop(void* in_val)
clipboard_deinit(); clipboard_deinit();
sound_deinit(); sound_deinit();
dev_redir_deinit(); dev_redir_deinit();
rail_deinit();
break; break;
} }
if (g_lis_trans != 0) if (g_lis_trans != 0)
@ -475,6 +496,7 @@ channel_thread_loop(void* in_val)
clipboard_deinit(); clipboard_deinit();
sound_deinit(); sound_deinit();
dev_redir_deinit(); dev_redir_deinit();
rail_deinit();
/* delete g_con_trans */ /* delete g_con_trans */
trans_delete(g_con_trans); trans_delete(g_con_trans);
g_con_trans = 0; g_con_trans = 0;
@ -486,7 +508,7 @@ channel_thread_loop(void* in_val)
} }
} }
} }
clipboard_check_wait_objs(); xcommon_check_wait_objs();
sound_check_wait_objs(); sound_check_wait_objs();
dev_redir_check_wait_objs(); dev_redir_check_wait_objs();
timeout = -1; timeout = -1;
@ -495,7 +517,7 @@ channel_thread_loop(void* in_val)
num_objs++; num_objs++;
trans_get_wait_objs(g_lis_trans, objs, &num_objs); trans_get_wait_objs(g_lis_trans, objs, &num_objs);
trans_get_wait_objs(g_con_trans, objs, &num_objs); trans_get_wait_objs(g_con_trans, objs, &num_objs);
clipboard_get_wait_objs(objs, &num_objs, &timeout); xcommon_get_wait_objs(objs, &num_objs, &timeout);
sound_get_wait_objs(objs, &num_objs, &timeout); sound_get_wait_objs(objs, &num_objs, &timeout);
dev_redir_get_wait_objs(objs, &num_objs, &timeout); dev_redir_get_wait_objs(objs, &num_objs, &timeout);
} }
@ -595,15 +617,14 @@ main_cleanup(void)
static int APP_CC static int APP_CC
read_ini(void) read_ini(void)
{ {
char filename[256] = ""; char filename[256];
struct list* names = (struct list *)NULL; struct list* names;
struct list* values = (struct list *)NULL; struct list* values;
char* name = (char *)NULL; char* name;
char* value = (char *)NULL; char* value;
int index = 0; int index;
g_memset(filename,0,(sizeof(char)*256));
g_memset(filename,0,(sizeof(char) * 256));
names = list_create(); names = list_create();
names->auto_free = 1; names->auto_free = 1;
values = list_create(); values = list_create();

@ -30,6 +30,18 @@
#include "chansrv.h" #include "chansrv.h"
#include "clipboard.h" #include "clipboard.h"
extern int g_cliprdr_chan_id; /* in chansrv.c */
extern Display* g_display; /* in xcommon.c */
extern int g_x_socket; /* in xcommon.c */
extern tbus g_x_wait_obj; /* in xcommon.c */
extern Screen* g_screen; /* in xcommon.c */
extern int g_screen_num; /* in xcommon.c */
int g_clip_up = 0;
int g_waiting_for_data_response = 0;
int g_waiting_for_data_response_time = 0;
static Atom g_clipboard_atom = 0; static Atom g_clipboard_atom = 0;
static Atom g_clip_property_atom = 0; static Atom g_clip_property_atom = 0;
static Atom g_timestamp_atom = 0; static Atom g_timestamp_atom = 0;
@ -39,12 +51,7 @@ static Atom g_primary_atom = 0;
static Atom g_secondary_atom = 0; static Atom g_secondary_atom = 0;
static Atom g_get_time_atom = 0; static Atom g_get_time_atom = 0;
static Atom g_utf8_atom = 0; static Atom g_utf8_atom = 0;
static int g_x_socket = 0;
static tbus g_x_wait_obj = 0;
static int g_clip_up = 0;
static Window g_wnd = 0; static Window g_wnd = 0;
static Screen* g_screen = 0;
static int g_screen_num = 0;
static int g_xfixes_event_base = 0; static int g_xfixes_event_base = 0;
static int g_last_clip_size = 0; static int g_last_clip_size = 0;
@ -64,35 +71,6 @@ static int g_data_in_size = 0;
static int g_data_in_time = 0; static int g_data_in_time = 0;
static int g_data_in_up_to_date = 0; static int g_data_in_up_to_date = 0;
static int g_got_format_announce = 0; static int g_got_format_announce = 0;
static int g_waiting_for_data_response = 0;
static int g_waiting_for_data_response_time = 0;
static Display* g_display = 0;
extern int g_cliprdr_chan_id; /* in chansrv.c */
/*****************************************************************************/
int DEFAULT_CC
clipboard_error_handler(Display* dis, XErrorEvent* xer)
{
char text[256];
XGetErrorText(dis, xer->error_code, text, 255);
LOGM((LOG_LEVEL_ERROR,"error [%s]", text));
return 0;
}
/*****************************************************************************/
/* The X server had an internal error. This is the last function called.
Do any cleanup that needs to be done on exit, like removing temporary files.
Don't worry about memory leaks */
int DEFAULT_CC
clipboard_fatal_handler(Display* dis)
{
LOGM((LOG_LEVEL_ALWAYS, "fatal error, exiting"));
main_cleanup();
return 0;
}
/*****************************************************************************/ /*****************************************************************************/
/* this is one way to get the current time from the x server */ /* this is one way to get the current time from the x server */
@ -114,17 +92,6 @@ clipboard_get_server_time(void)
return xevent.xproperty.time; return xevent.xproperty.time;
} }
/*****************************************************************************/
/* returns time in miliseconds
this is like g_time2 in os_calls, but not miliseconds since machine was
up, something else
this is a time value similar to what the xserver uses */
static int APP_CC
clipboard_get_local_time(void)
{
return g_time3();
}
/*****************************************************************************/ /*****************************************************************************/
/* returns error */ /* returns error */
int APP_CC int APP_CC
@ -146,26 +113,6 @@ clipboard_init(void)
} }
clipboard_deinit(); clipboard_deinit();
rv = 0; rv = 0;
/* setting the error handlers can cause problem when shutting down
chansrv on some xlibs */
//XSetErrorHandler(clipboard_error_handler);
//XSetIOErrorHandler(clipboard_fatal_handler);
g_display = XOpenDisplay(0);
if (g_display == 0)
{
LOGM((LOG_LEVEL_ERROR, "clipboard_init: XOpenDisplay failed"));
rv = 1;
}
if (rv == 0)
{
g_x_socket = XConnectionNumber(g_display);
if (g_x_socket == 0)
{
LOGM((LOG_LEVEL_ERROR, "clipboard_init: XConnectionNumber failed"));
rv = 2;
}
g_x_wait_obj = g_create_wait_obj_from_socket(g_x_socket, 0);
}
if (rv == 0) if (rv == 0)
{ {
g_clipboard_atom = XInternAtom(g_display, "CLIPBOARD", False); g_clipboard_atom = XInternAtom(g_display, "CLIPBOARD", False);
@ -190,8 +137,6 @@ clipboard_init(void)
st = XFixesQueryVersion(g_display, &ver_maj, &ver_min); st = XFixesQueryVersion(g_display, &ver_maj, &ver_min);
LOGM((LOG_LEVEL_ERROR, "clipboard_init st %d, maj %d min %d", st, LOGM((LOG_LEVEL_ERROR, "clipboard_init st %d, maj %d min %d", st,
ver_maj, ver_min)); ver_maj, ver_min));
g_screen_num = DefaultScreen(g_display);
g_screen = ScreenOfDisplay(g_display, g_screen_num);
g_clip_property_atom = XInternAtom(g_display, "XRDP_CLIP_PROPERTY_ATOM", g_clip_property_atom = XInternAtom(g_display, "XRDP_CLIP_PROPERTY_ATOM",
False); False);
g_get_time_atom = XInternAtom(g_display, "XRDP_GET_TIME_ATOM", g_get_time_atom = XInternAtom(g_display, "XRDP_GET_TIME_ATOM",
@ -251,27 +196,16 @@ clipboard_init(void)
int APP_CC int APP_CC
clipboard_deinit(void) clipboard_deinit(void)
{ {
if (g_x_wait_obj != 0)
{
g_delete_wait_obj_from_socket(g_x_wait_obj);
g_x_wait_obj = 0;
}
if (g_wnd != 0) if (g_wnd != 0)
{ {
XDestroyWindow(g_display, g_wnd); XDestroyWindow(g_display, g_wnd);
g_wnd = 0; g_wnd = 0;
} }
g_x_socket = 0;
g_free(g_last_clip_data); g_free(g_last_clip_data);
g_last_clip_data = 0; g_last_clip_data = 0;
g_last_clip_size = 0; g_last_clip_size = 0;
free_stream(g_ins); free_stream(g_ins);
g_ins = 0; g_ins = 0;
if (g_display != 0)
{
XCloseDisplay(g_display);
g_display = 0;
}
g_clip_up = 0; g_clip_up = 0;
return 0; return 0;
} }
@ -584,7 +518,7 @@ clipboard_process_data_response(struct stream* s, int clip_msg_status,
} }
g_data_in_size = len; g_data_in_size = len;
g_wcstombs(g_data_in, wtext, len + 1); g_wcstombs(g_data_in, wtext, len + 1);
g_data_in_time = clipboard_get_local_time(); g_data_in_time = xcommon_get_local_time();
g_data_in_up_to_date = 1; g_data_in_up_to_date = 1;
} }
if (g_data_in != 0) if (g_data_in != 0)
@ -1032,7 +966,7 @@ clipboard_event_selection_request(XEvent* xevent)
{ {
clipboard_send_data_request(); clipboard_send_data_request();
g_waiting_for_data_response = 1; g_waiting_for_data_response = 1;
g_waiting_for_data_response_time = clipboard_get_local_time(); g_waiting_for_data_response_time = xcommon_get_local_time();
} }
g_selection_request_event_count++; g_selection_request_event_count++;
return 0; return 0;
@ -1088,85 +1022,48 @@ clipboard_event_property_notify(XEvent* xevent)
} }
/*****************************************************************************/ /*****************************************************************************/
/* returns error /* returns 0, event handled, 1 unhandled */
this is called to get any wait objects for the main loop
timeout can be nil */
int APP_CC
clipboard_get_wait_objs(tbus* objs, int* count, int* timeout)
{
int lcount;
if ((!g_clip_up) || (objs == 0) || (count == 0))
{
return 0;
}
lcount = *count;
objs[lcount] = g_x_wait_obj;
lcount++;
*count = lcount;
return 0;
}
/*****************************************************************************/
int APP_CC int APP_CC
clipboard_check_wait_objs(void) clipboard_xevent(void* xevent)
{ {
XEvent xevent; XEvent* lxevent;
int time_diff;
if (!g_clip_up) if (!g_clip_up)
{ {
return 0; return 1;
}
if (g_is_wait_obj_set(g_x_wait_obj))
{
if (XPending(g_display) < 1)
{
/* something is wrong, should not get here */
LOGM((LOG_LEVEL_ERROR, "clipboard_check_wait_objs: sck closed"));
return 0;
}
if (g_waiting_for_data_response)
{
time_diff = clipboard_get_local_time() -
g_waiting_for_data_response_time;
if (time_diff > 1000)
{
LOGM((LOG_LEVEL_ERROR, "clipboard_check_wait_objs: warning, "
"waiting for data response too long"));
}
} }
while (XPending(g_display) > 0) lxevent = (XEvent*)xevent;
{ switch (lxevent->type)
XNextEvent(g_display, &xevent);
switch (xevent.type)
{ {
case SelectionNotify: case SelectionNotify:
clipboard_event_selection_notify(&xevent); clipboard_event_selection_notify(lxevent);
break; break;
case SelectionRequest: case SelectionRequest:
clipboard_event_selection_request(&xevent); clipboard_event_selection_request(lxevent);
break; break;
case SelectionClear: case SelectionClear:
clipboard_event_selection_clear(&xevent); clipboard_event_selection_clear(lxevent);
break; break;
case MappingNotify: case MappingNotify:
break; break;
case PropertyNotify: case PropertyNotify:
clipboard_event_property_notify(&xevent); clipboard_event_property_notify(lxevent);
break;
case UnmapNotify:
LOG(0, ("chansrv::clipboard_xevent: got UnmapNotify"));
break;
case ClientMessage:
LOG(0, ("chansrv::clipboard_xevent: got ClientMessage"));
break; break;
default: default:
if (xevent.type == g_xfixes_event_base + if (lxevent->type == g_xfixes_event_base +
XFixesSetSelectionOwnerNotify) XFixesSetSelectionOwnerNotify)
{ {
clipboard_event_selection_owner_notify(&xevent); clipboard_event_selection_owner_notify(lxevent);
break; break;
} }
LOGM((LOG_LEVEL_ERROR, "clipboard_check_wait_objs unknown type %d", /* we didn't handle this message */
xevent.type)); return 1;
break;
}
}
} }
return 0; return 0;
} }

@ -30,8 +30,6 @@ int APP_CC
clipboard_data_in(struct stream* s, int chan_id, int chan_flags, int length, clipboard_data_in(struct stream* s, int chan_id, int chan_flags, int length,
int total_length); int total_length);
int APP_CC int APP_CC
clipboard_get_wait_objs(tbus* objs, int* count, int* timeout); clipboard_xevent(void* xevent);
int APP_CC
clipboard_check_wait_objs(void);
#endif #endif

@ -25,3 +25,36 @@
#include "rail.h" #include "rail.h"
#include "xcommon.h" #include "xcommon.h"
#include "log.h" #include "log.h"
int g_rail_up = 0;
/*****************************************************************************/
int APP_CC
rail_init(void)
{
return 0;
}
/*****************************************************************************/
int APP_CC
rail_deinit(void)
{
return 0;
}
/*****************************************************************************/
/* data in from client ( client -> xrdp -> chansrv ) */
int APP_CC
rail_data_in(struct stream* s, int chan_id, int chan_flags, int length,
int total_length)
{
return 0;
}
/*****************************************************************************/
/* returns 0, event handled, 1 unhandled */
int APP_CC
rail_xevent(void* xevent)
{
return 1;
}

@ -19,4 +19,17 @@
#ifndef _RAIL_H_ #ifndef _RAIL_H_
#define _RAIL_H_ #define _RAIL_H_
#include "arch.h"
#include "parse.h"
int APP_CC
rail_init(void);
int APP_CC
rail_deinit(void);
int APP_CC
rail_data_in(struct stream* s, int chan_id, int chan_flags,
int length, int total_length);
int APP_CC
rail_xevent(void* xevent);
#endif #endif

@ -25,3 +25,161 @@
#include "clipboard.h" #include "clipboard.h"
#include "rail.h" #include "rail.h"
extern int g_clip_up; /* in clipboard.c */
extern int g_waiting_for_data_response; /* in clipboard.c */
extern int g_waiting_for_data_response_time; /* in clipboard.c */
extern int g_rail_up; /* in rail.c */
Display* g_display = 0;
int g_x_socket = 0;
tbus g_x_wait_obj = 0;
Screen* g_screen = 0;
int g_screen_num = 0;
Window g_root_window = 0;
Atom g_wm_delete_window_atom = 0;
Atom g_wm_protocols_atom = 0;
/*****************************************************************************/
static int DEFAULT_CC
xcommon_error_handler(Display* dis, XErrorEvent* xer)
{
char text[256];
XGetErrorText(dis, xer->error_code, text, 255);
log_message(LOG_LEVEL_ERROR, "X error [%s](%d) opcodes %d/%d\n "
"resource 0x%lx", text, xer->error_code,
xer->request_code, xer->minor_code, xer->resourceid);
return 0;
}
/*****************************************************************************/
/* The X server had an internal error. This is the last function called.
Do any cleanup that needs to be done on exit, like removing temporary files.
Don't worry about memory leaks */
static int DEFAULT_CC
xcommon_fatal_handler(Display* dis)
{
return 0;
}
/*****************************************************************************/
/* returns time in miliseconds
this is like g_time2 in os_calls, but not miliseconds since machine was
up, something else
this is a time value similar to what the xserver uses */
int APP_CC
xcommon_get_local_time(void)
{
return g_time3();
}
/******************************************************************************/
/* this should be called first */
int APP_CC
xcommon_init(void)
{
if (g_display != 0)
{
g_writeln("xcommon_init: xcommon_init already called");
return 0;
}
g_display = XOpenDisplay(0);
if (g_display == 0)
{
log_message(LOG_LEVEL_ERROR, "xcommon_init: error, XOpenDisplay failed");
return 1;
}
/* setting the error handlers can cause problem when shutting down
chansrv on some xlibs */
XSetErrorHandler(xcommon_error_handler);
//XSetIOErrorHandler(xcommon_fatal_handler);
g_x_socket = XConnectionNumber(g_display);
if (g_x_socket == 0)
{
log_message(LOG_LEVEL_ERROR, "xcommon_init: XConnectionNumber failed");
return 1;
}
g_x_wait_obj = g_create_wait_obj_from_socket(g_x_socket, 0);
g_screen_num = DefaultScreen(g_display);
g_screen = ScreenOfDisplay(g_display, g_screen_num);
g_root_window = RootWindowOfScreen(g_screen);
g_wm_delete_window_atom = XInternAtom(g_display, "WM_DELETE_WINDOW", 0);
g_wm_protocols_atom = XInternAtom(g_display, "WM_PROTOCOLS", 0);
return 0;
}
/*****************************************************************************/
/* returns error
this is called to get any wait objects for the main loop
timeout can be nil */
int APP_CC
xcommon_get_wait_objs(tbus* objs, int* count, int* timeout)
{
int lcount;
if (((!g_clip_up) && (!g_rail_up)) || (objs == 0) || (count == 0))
{
g_writeln("xcommon_get_wait_objs: nothing to do");
return 0;
}
lcount = *count;
objs[lcount] = g_x_wait_obj;
lcount++;
*count = lcount;
return 0;
}
/*****************************************************************************/
int APP_CC
xcommon_check_wait_objs(void)
{
XEvent xevent;
int time_diff;
int clip_rv;
int rail_rv;
if ((!g_clip_up) && (!g_rail_up))
{
g_writeln("xcommon_check_wait_objs: nothing to do");
return 0;
}
if (g_is_wait_obj_set(g_x_wait_obj))
{
if (XPending(g_display) < 1)
{
/* something is wrong, should not get here */
log_message(LOG_LEVEL_ERROR, "xcommon_check_wait_objs: sck closed");
return 0;
}
if (g_waiting_for_data_response)
{
time_diff = xcommon_get_local_time() -
g_waiting_for_data_response_time;
if (time_diff > 1000)
{
log_message(LOG_LEVEL_ERROR, "xcommon_check_wait_objs: warning, "
"waiting for data response too long");
}
}
while (XPending(g_display) > 0)
{
g_memset(&xevent, 0, sizeof(xevent));
XNextEvent(g_display, &xevent);
clip_rv = clipboard_xevent(&xevent);
rail_rv = rail_xevent(&xevent);
if ((clip_rv == 1) && (rail_rv == 1))
{
LOG(0, ("xcommon_check_wait_objs unknown xevent type %d", xevent.type));
}
}
}
return 0;
}

@ -22,11 +22,11 @@
#include "arch.h" #include "arch.h"
#include "parse.h" #include "parse.h"
int APP_CC
xcommon_init(void);
int APP_CC int APP_CC
xcommon_get_local_time(void); xcommon_get_local_time(void);
int APP_CC int APP_CC
xcommon_init(void);
int APP_CC
xcommon_get_wait_objs(tbus* objs, int* count, int* timeout); xcommon_get_wait_objs(tbus* objs, int* count, int* timeout);
int APP_CC int APP_CC
xcommon_check_wait_objs(void); xcommon_check_wait_objs(void);

Loading…
Cancel
Save