Tie windows and thread input structures to a specific desktop.
Support multiple desktop windows (one per desktop object). Use the window desktop to find the window station to use for property atoms.
This commit is contained in:
parent
30d06da60a
commit
5ad90c0f37
|
@ -94,6 +94,7 @@ struct timer
|
|||
struct thread_input
|
||||
{
|
||||
struct object obj; /* object header */
|
||||
struct desktop *desktop; /* desktop that this thread input belongs to */
|
||||
user_handle_t focus; /* focus window */
|
||||
user_handle_t capture; /* capture window */
|
||||
user_handle_t active; /* active window */
|
||||
|
@ -188,12 +189,17 @@ static void set_caret_window( struct thread_input *input, user_handle_t win )
|
|||
}
|
||||
|
||||
/* create a thread input object */
|
||||
static struct thread_input *create_thread_input(void)
|
||||
static struct thread_input *create_thread_input( struct thread *thread )
|
||||
{
|
||||
struct thread_input *input;
|
||||
|
||||
if ((input = alloc_object( &thread_input_ops )))
|
||||
{
|
||||
if (!(input->desktop = get_thread_desktop( thread, 0 /* FIXME: access rights */ )))
|
||||
{
|
||||
free( input );
|
||||
return NULL;
|
||||
}
|
||||
input->focus = 0;
|
||||
input->capture = 0;
|
||||
input->active = 0;
|
||||
|
@ -222,7 +228,7 @@ static struct msg_queue *create_msg_queue( struct thread *thread, struct thread_
|
|||
struct msg_queue *queue;
|
||||
int i;
|
||||
|
||||
if (!input && !(input = create_thread_input())) return NULL;
|
||||
if (!input && !(input = create_thread_input( thread ))) return NULL;
|
||||
if ((queue = alloc_object( &msg_queue_ops )))
|
||||
{
|
||||
queue->wake_bits = 0;
|
||||
|
@ -797,6 +803,7 @@ static void thread_input_destroy( struct object *obj )
|
|||
|
||||
if (foreground_input == input) foreground_input = NULL;
|
||||
empty_msg_list( &input->msg_list );
|
||||
release_object( input->desktop );
|
||||
}
|
||||
|
||||
/* fix the thread input data when a window is destroyed */
|
||||
|
@ -841,10 +848,20 @@ int init_thread_queue( struct thread *thread )
|
|||
/* attach two thread input data structures */
|
||||
int attach_thread_input( struct thread *thread_from, struct thread *thread_to )
|
||||
{
|
||||
struct desktop *desktop;
|
||||
struct thread_input *input;
|
||||
|
||||
if (!thread_to->queue && !(thread_to->queue = create_msg_queue( thread_to, NULL ))) return 0;
|
||||
if (!(desktop = get_thread_desktop( thread_from, 0 ))) return 0;
|
||||
input = (struct thread_input *)grab_object( thread_to->queue->input );
|
||||
if (input->desktop != desktop)
|
||||
{
|
||||
set_error( STATUS_ACCESS_DENIED );
|
||||
release_object( input );
|
||||
release_object( desktop );
|
||||
return 0;
|
||||
}
|
||||
release_object( desktop );
|
||||
|
||||
if (thread_from->queue)
|
||||
{
|
||||
|
@ -860,17 +877,11 @@ int attach_thread_input( struct thread *thread_from, struct thread *thread_to )
|
|||
}
|
||||
|
||||
/* detach two thread input data structures */
|
||||
static void detach_thread_input( struct thread *thread_from, struct thread *thread_to )
|
||||
void detach_thread_input( struct thread *thread_from )
|
||||
{
|
||||
struct thread_input *input;
|
||||
|
||||
if (!thread_from->queue || !thread_to->queue ||
|
||||
thread_from->queue->input != thread_to->queue->input)
|
||||
{
|
||||
set_error( STATUS_ACCESS_DENIED );
|
||||
return;
|
||||
}
|
||||
if ((input = create_thread_input()))
|
||||
if ((input = create_thread_input( thread_from )))
|
||||
{
|
||||
release_thread_input( thread_from );
|
||||
thread_from->queue->input = input;
|
||||
|
@ -1146,7 +1157,7 @@ static user_handle_t find_hardware_message_window( struct thread_input *input, s
|
|||
if (!input || !(win = input->capture))
|
||||
{
|
||||
if (!(win = msg->win) || !is_window_visible( win ))
|
||||
win = window_from_point( msg->x, msg->y );
|
||||
win = window_from_point( input->desktop, msg->x, msg->y );
|
||||
}
|
||||
}
|
||||
return win;
|
||||
|
@ -1808,7 +1819,14 @@ DECL_HANDLER(attach_thread_input)
|
|||
if (thread_from != thread_to)
|
||||
{
|
||||
if (req->attach) attach_thread_input( thread_from, thread_to );
|
||||
else detach_thread_input( thread_from, thread_to );
|
||||
else
|
||||
{
|
||||
if (thread_from->queue && thread_to->queue &&
|
||||
thread_from->queue->input == thread_to->queue->input)
|
||||
detach_thread_input( thread_from );
|
||||
else
|
||||
set_error( STATUS_ACCESS_DENIED );
|
||||
}
|
||||
}
|
||||
else set_error( STATUS_ACCESS_DENIED );
|
||||
release_object( thread_from );
|
||||
|
|
|
@ -56,6 +56,7 @@ struct desktop
|
|||
unsigned int flags; /* desktop flags */
|
||||
struct winstation *winstation; /* winstation this desktop belongs to */
|
||||
struct list entry; /* entry in winstation list of desktops */
|
||||
struct window *top_window; /* desktop window for this desktop */
|
||||
};
|
||||
|
||||
/* user handles functions */
|
||||
|
@ -86,6 +87,7 @@ extern void inc_queue_paint_count( struct thread *thread, int incr );
|
|||
extern void queue_cleanup_window( struct thread *thread, user_handle_t win );
|
||||
extern int init_thread_queue( struct thread *thread );
|
||||
extern int attach_thread_input( struct thread *thread_from, struct thread *thread_to );
|
||||
extern void detach_thread_input( struct thread *thread_from );
|
||||
extern void post_message( user_handle_t win, unsigned int message,
|
||||
unsigned int wparam, unsigned int lparam );
|
||||
extern void post_win_event( struct thread *thread, unsigned int event,
|
||||
|
@ -121,13 +123,14 @@ extern int rect_in_region( struct region *region, const rectangle_t *rect );
|
|||
|
||||
/* window functions */
|
||||
|
||||
extern void destroy_window( struct window *win );
|
||||
extern void destroy_thread_windows( struct thread *thread );
|
||||
extern int is_child_window( user_handle_t parent, user_handle_t child );
|
||||
extern int is_top_level_window( user_handle_t window );
|
||||
extern int is_window_visible( user_handle_t window );
|
||||
extern int make_window_active( user_handle_t window );
|
||||
extern struct thread *get_window_thread( user_handle_t handle );
|
||||
extern user_handle_t window_from_point( int x, int y );
|
||||
extern user_handle_t window_from_point( struct desktop *desktop, int x, int y );
|
||||
extern user_handle_t find_window_to_repaint( user_handle_t parent, struct thread *thread );
|
||||
extern struct window_class *get_window_class( user_handle_t window );
|
||||
|
||||
|
|
129
server/window.c
129
server/window.c
|
@ -61,6 +61,7 @@ struct window
|
|||
struct list entry; /* entry in parent's children list */
|
||||
user_handle_t handle; /* full handle for this window */
|
||||
struct thread *thread; /* thread owning the window */
|
||||
struct desktop *desktop; /* desktop that the window belongs to */
|
||||
struct window_class *class; /* window class */
|
||||
atom_t atom; /* class atom */
|
||||
user_handle_t last_active; /* last active popup */
|
||||
|
@ -200,8 +201,7 @@ static int add_handle_to_array( struct user_handle_array *array, user_handle_t h
|
|||
}
|
||||
|
||||
/* set a window property */
|
||||
static void set_property( struct winstation *winstation, struct window *win,
|
||||
atom_t atom, obj_handle_t handle, enum property_type type )
|
||||
static void set_property( struct window *win, atom_t atom, obj_handle_t handle, enum property_type type )
|
||||
{
|
||||
int i, free = -1;
|
||||
struct property *new_props;
|
||||
|
@ -223,7 +223,7 @@ static void set_property( struct winstation *winstation, struct window *win,
|
|||
}
|
||||
|
||||
/* need to add an entry */
|
||||
if (!grab_global_atom( winstation, atom )) return;
|
||||
if (!grab_global_atom( win->desktop->winstation, atom )) return;
|
||||
if (free == -1)
|
||||
{
|
||||
/* no free entry */
|
||||
|
@ -234,7 +234,7 @@ static void set_property( struct winstation *winstation, struct window *win,
|
|||
sizeof(*new_props) * (win->prop_alloc + 16) )))
|
||||
{
|
||||
set_error( STATUS_NO_MEMORY );
|
||||
release_global_atom( winstation, atom );
|
||||
release_global_atom( win->desktop->winstation, atom );
|
||||
return;
|
||||
}
|
||||
win->prop_alloc += 16;
|
||||
|
@ -248,7 +248,7 @@ static void set_property( struct winstation *winstation, struct window *win,
|
|||
}
|
||||
|
||||
/* remove a window property */
|
||||
static obj_handle_t remove_property( struct winstation *winstation, struct window *win, atom_t atom )
|
||||
static obj_handle_t remove_property( struct window *win, atom_t atom )
|
||||
{
|
||||
int i;
|
||||
|
||||
|
@ -257,7 +257,7 @@ static obj_handle_t remove_property( struct winstation *winstation, struct windo
|
|||
if (win->properties[i].type == PROP_TYPE_FREE) continue;
|
||||
if (win->properties[i].atom == atom)
|
||||
{
|
||||
release_global_atom( winstation, atom );
|
||||
release_global_atom( win->desktop->winstation, atom );
|
||||
win->properties[i].type = PROP_TYPE_FREE;
|
||||
return win->properties[i].handle;
|
||||
}
|
||||
|
@ -283,44 +283,40 @@ static obj_handle_t get_property( struct window *win, atom_t atom )
|
|||
/* destroy all properties of a window */
|
||||
inline static void destroy_properties( struct window *win )
|
||||
{
|
||||
struct winstation *winstation;
|
||||
int i;
|
||||
|
||||
if (!win->properties) return;
|
||||
/* FIXME: winstation pointer should be taken from window */
|
||||
if (!(winstation = get_process_winstation( win->thread->process, WINSTA_ACCESSGLOBALATOMS )))
|
||||
return;
|
||||
for (i = 0; i < win->prop_inuse; i++)
|
||||
{
|
||||
if (win->properties[i].type == PROP_TYPE_FREE) continue;
|
||||
release_global_atom( winstation, win->properties[i].atom );
|
||||
release_global_atom( win->desktop->winstation, win->properties[i].atom );
|
||||
}
|
||||
free( win->properties );
|
||||
release_object( winstation );
|
||||
}
|
||||
|
||||
/* destroy a window */
|
||||
static void destroy_window( struct window *win )
|
||||
void destroy_window( struct window *win )
|
||||
{
|
||||
struct thread *thread = win->thread;
|
||||
|
||||
/* destroy all children */
|
||||
while (!list_empty(&win->children))
|
||||
destroy_window( LIST_ENTRY( list_head(&win->children), struct window, entry ));
|
||||
while (!list_empty(&win->unlinked))
|
||||
destroy_window( LIST_ENTRY( list_head(&win->unlinked), struct window, entry ));
|
||||
|
||||
if (win->thread->queue)
|
||||
if (thread && thread->queue)
|
||||
{
|
||||
if (win->update_region) inc_queue_paint_count( win->thread, -1 );
|
||||
if (win->paint_flags & PAINT_INTERNAL) inc_queue_paint_count( win->thread, -1 );
|
||||
queue_cleanup_window( win->thread, win->handle );
|
||||
if (win->update_region) inc_queue_paint_count( thread, -1 );
|
||||
if (win->paint_flags & PAINT_INTERNAL) inc_queue_paint_count( thread, -1 );
|
||||
queue_cleanup_window( thread, win->handle );
|
||||
}
|
||||
|
||||
/* reset global window pointers, if the corresponding window is destroyed */
|
||||
if (win == shell_window) shell_window = NULL;
|
||||
if (win == shell_listview) shell_listview = NULL;
|
||||
if (win == progman_window) progman_window = NULL;
|
||||
if (win == taskman_window) taskman_window = NULL;
|
||||
assert( win->thread->desktop_users > 0 );
|
||||
win->thread->desktop_users--;
|
||||
free_user_handle( win->handle );
|
||||
destroy_properties( win );
|
||||
list_remove( &win->entry );
|
||||
|
@ -328,6 +324,12 @@ static void destroy_window( struct window *win )
|
|||
if (win->update_region) free_region( win->update_region );
|
||||
release_class( win->class );
|
||||
if (win->text) free( win->text );
|
||||
if (!is_desktop_window(win))
|
||||
{
|
||||
assert( thread->desktop_users > 0 );
|
||||
thread->desktop_users--;
|
||||
release_object( win->desktop );
|
||||
}
|
||||
memset( win, 0x55, sizeof(*win) + win->nb_extra_bytes - 1 );
|
||||
free( win );
|
||||
}
|
||||
|
@ -338,13 +340,21 @@ static struct window *create_window( struct window *parent, struct window *owner
|
|||
{
|
||||
int extra_bytes;
|
||||
struct window *win;
|
||||
struct window_class *class = grab_class( current->process, atom, instance, &extra_bytes );
|
||||
struct desktop *desktop;
|
||||
struct window_class *class;
|
||||
|
||||
if (!class) return NULL;
|
||||
if (!(desktop = get_thread_desktop( current, DESKTOP_CREATEWINDOW ))) return NULL;
|
||||
|
||||
if (!(class = grab_class( current->process, atom, instance, &extra_bytes )))
|
||||
{
|
||||
release_object( desktop );
|
||||
return NULL;
|
||||
}
|
||||
|
||||
win = mem_alloc( sizeof(*win) + extra_bytes - 1 );
|
||||
if (!win)
|
||||
{
|
||||
release_object( desktop );
|
||||
release_class( class );
|
||||
return NULL;
|
||||
}
|
||||
|
@ -353,6 +363,7 @@ static struct window *create_window( struct window *parent, struct window *owner
|
|||
win->parent = parent;
|
||||
win->owner = owner ? owner->handle : 0;
|
||||
win->thread = current;
|
||||
win->desktop = desktop;
|
||||
win->class = class;
|
||||
win->atom = atom;
|
||||
win->last_active = win->handle;
|
||||
|
@ -374,6 +385,13 @@ static struct window *create_window( struct window *parent, struct window *owner
|
|||
list_init( &win->children );
|
||||
list_init( &win->unlinked );
|
||||
|
||||
/* parent must be on the same desktop */
|
||||
if (parent && parent->desktop != desktop)
|
||||
{
|
||||
set_error( STATUS_ACCESS_DENIED );
|
||||
goto failed;
|
||||
}
|
||||
|
||||
/* if parent belongs to a different thread, attach the two threads */
|
||||
if (parent && parent->thread && parent->thread != current)
|
||||
{
|
||||
|
@ -386,12 +404,14 @@ static struct window *create_window( struct window *parent, struct window *owner
|
|||
|
||||
/* put it on parent unlinked list */
|
||||
if (parent) list_add_head( &parent->unlinked, &win->entry );
|
||||
else list_init( &win->entry );
|
||||
|
||||
current->desktop_users++;
|
||||
return win;
|
||||
|
||||
failed:
|
||||
if (win->handle) free_user_handle( win->handle );
|
||||
release_object( desktop );
|
||||
release_class( class );
|
||||
free( win );
|
||||
return NULL;
|
||||
|
@ -411,17 +431,27 @@ void destroy_thread_windows( struct thread *thread )
|
|||
}
|
||||
|
||||
/* get the desktop window */
|
||||
static struct window *get_desktop_window( int create )
|
||||
static struct window *get_desktop_window( struct thread *thread, int create )
|
||||
{
|
||||
static struct window *top_window; /* FIXME: should be part of the desktop object */
|
||||
struct window *top_window;
|
||||
struct desktop *desktop = get_thread_desktop( thread, 0 );
|
||||
|
||||
if (!top_window && create)
|
||||
if (!desktop) return NULL;
|
||||
|
||||
if (!(top_window = desktop->top_window) && create)
|
||||
{
|
||||
if (!(top_window = create_window( NULL, NULL, DESKTOP_ATOM, 0 ))) return NULL;
|
||||
current->desktop_users--;
|
||||
top_window->thread = NULL; /* no thread owns the desktop */
|
||||
top_window->style = WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;
|
||||
if ((top_window = create_window( NULL, NULL, DESKTOP_ATOM, 0 )))
|
||||
{
|
||||
current->desktop_users--;
|
||||
top_window->thread = NULL; /* no thread owns the desktop */
|
||||
top_window->style = WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;
|
||||
desktop->top_window = top_window;
|
||||
/* don't hold a reference to the desktop so that the desktop window can be */
|
||||
/* destroyed when the desktop ref count reaches zero */
|
||||
release_object( top_window->desktop );
|
||||
}
|
||||
}
|
||||
release_object( desktop );
|
||||
return top_window;
|
||||
}
|
||||
|
||||
|
@ -553,12 +583,12 @@ static int get_window_children_from_point( struct window *parent, int x, int y,
|
|||
}
|
||||
|
||||
/* find window containing point (in absolute coords) */
|
||||
user_handle_t window_from_point( int x, int y )
|
||||
user_handle_t window_from_point( struct desktop *desktop, int x, int y )
|
||||
{
|
||||
struct window *ret, *top_window;
|
||||
struct window *ret;
|
||||
|
||||
if (!(top_window = get_desktop_window(0))) return 0;
|
||||
ret = child_window_from_point( top_window, x, y );
|
||||
if (!desktop->top_window) return 0;
|
||||
ret = child_window_from_point( desktop->top_window, x, y );
|
||||
return ret->handle;
|
||||
}
|
||||
|
||||
|
@ -640,7 +670,7 @@ static struct window *find_child_to_repaint( struct window *parent, struct threa
|
|||
/* find a window that needs to receive a WM_PAINT; also clear its internal paint flag */
|
||||
user_handle_t find_window_to_repaint( user_handle_t parent, struct thread *thread )
|
||||
{
|
||||
struct window *ptr, *win, *top_window = get_desktop_window(0);
|
||||
struct window *ptr, *win, *top_window = get_desktop_window( thread, 0 );
|
||||
|
||||
if (!top_window) return 0;
|
||||
|
||||
|
@ -1382,7 +1412,7 @@ DECL_HANDLER(destroy_window)
|
|||
/* retrieve the desktop window for the current thread */
|
||||
DECL_HANDLER(get_desktop_window)
|
||||
{
|
||||
struct window *win = get_desktop_window(1);
|
||||
struct window *win = get_desktop_window( current, 1 );
|
||||
|
||||
if (win) reply->handle = win->handle;
|
||||
}
|
||||
|
@ -1883,42 +1913,35 @@ DECL_HANDLER(redraw_window)
|
|||
/* set a window property */
|
||||
DECL_HANDLER(set_window_property)
|
||||
{
|
||||
struct winstation *winstation;
|
||||
struct window *win = get_window( req->window );
|
||||
|
||||
if (!win) return;
|
||||
if (!(winstation = get_process_winstation( current->process, WINSTA_ACCESSGLOBALATOMS ))) return;
|
||||
|
||||
if (get_req_data_size())
|
||||
{
|
||||
atom_t atom = add_global_atom( winstation, get_req_data(), get_req_data_size() / sizeof(WCHAR) );
|
||||
atom_t atom = add_global_atom( win->desktop->winstation,
|
||||
get_req_data(), get_req_data_size() / sizeof(WCHAR) );
|
||||
if (atom)
|
||||
{
|
||||
set_property( winstation, win, atom, req->handle, PROP_TYPE_STRING );
|
||||
release_global_atom( winstation, atom );
|
||||
set_property( win, atom, req->handle, PROP_TYPE_STRING );
|
||||
release_global_atom( win->desktop->winstation, atom );
|
||||
}
|
||||
}
|
||||
else set_property( winstation, win, req->atom, req->handle, PROP_TYPE_ATOM );
|
||||
|
||||
release_object( winstation );
|
||||
else set_property( win, req->atom, req->handle, PROP_TYPE_ATOM );
|
||||
}
|
||||
|
||||
|
||||
/* remove a window property */
|
||||
DECL_HANDLER(remove_window_property)
|
||||
{
|
||||
struct winstation *winstation;
|
||||
struct window *win = get_window( req->window );
|
||||
|
||||
if (!win) return;
|
||||
|
||||
if ((winstation = get_process_winstation( current->process, WINSTA_ACCESSGLOBALATOMS )))
|
||||
if (win)
|
||||
{
|
||||
atom_t atom = req->atom;
|
||||
if (get_req_data_size()) atom = find_global_atom( winstation, get_req_data(),
|
||||
if (get_req_data_size()) atom = find_global_atom( win->desktop->winstation, get_req_data(),
|
||||
get_req_data_size() / sizeof(WCHAR) );
|
||||
if (atom) reply->handle = remove_property( winstation, win, atom );
|
||||
release_object( winstation );
|
||||
if (atom) reply->handle = remove_property( win, atom );
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1926,18 +1949,14 @@ DECL_HANDLER(remove_window_property)
|
|||
/* get a window property */
|
||||
DECL_HANDLER(get_window_property)
|
||||
{
|
||||
struct winstation *winstation;
|
||||
struct window *win = get_window( req->window );
|
||||
|
||||
if (!win) return;
|
||||
|
||||
if ((winstation = get_process_winstation( current->process, WINSTA_ACCESSGLOBALATOMS )))
|
||||
if (win)
|
||||
{
|
||||
atom_t atom = req->atom;
|
||||
if (get_req_data_size()) atom = find_global_atom( winstation, get_req_data(),
|
||||
if (get_req_data_size()) atom = find_global_atom( win->desktop->winstation, get_req_data(),
|
||||
get_req_data_size() / sizeof(WCHAR) );
|
||||
if (atom) reply->handle = get_property( win, atom );
|
||||
release_object( winstation );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -111,7 +111,8 @@ static void winstation_dump( struct object *obj, int verbose )
|
|||
{
|
||||
struct winstation *winstation = (struct winstation *)obj;
|
||||
|
||||
fprintf( stderr, "Winstation flags=%x ", winstation->flags );
|
||||
fprintf( stderr, "Winstation flags=%x clipboard=%p atoms=%p ",
|
||||
winstation->flags, winstation->clipboard, winstation->atom_table );
|
||||
dump_object_name( &winstation->obj );
|
||||
fputc( '\n', stderr );
|
||||
}
|
||||
|
@ -187,6 +188,7 @@ static struct desktop *create_desktop( const WCHAR *name, size_t len, unsigned i
|
|||
/* initialize it if it didn't already exist */
|
||||
desktop->flags = flags;
|
||||
desktop->winstation = (struct winstation *)grab_object( winstation );
|
||||
desktop->top_window = NULL;
|
||||
list_add_tail( &winstation->desktops, &desktop->entry );
|
||||
}
|
||||
}
|
||||
|
@ -198,7 +200,8 @@ static void desktop_dump( struct object *obj, int verbose )
|
|||
{
|
||||
struct desktop *desktop = (struct desktop *)obj;
|
||||
|
||||
fprintf( stderr, "Desktop flags=%x winstation=%p ", desktop->flags, desktop->winstation );
|
||||
fprintf( stderr, "Desktop flags=%x winstation=%p top_win=%p",
|
||||
desktop->flags, desktop->winstation, desktop->top_window );
|
||||
dump_object_name( &desktop->obj );
|
||||
fputc( '\n', stderr );
|
||||
}
|
||||
|
@ -218,6 +221,7 @@ static void desktop_destroy( struct object *obj )
|
|||
{
|
||||
struct desktop *desktop = (struct desktop *)obj;
|
||||
|
||||
if (desktop->top_window) destroy_window( desktop->top_window );
|
||||
list_remove( &desktop->entry );
|
||||
release_object( desktop->winstation );
|
||||
}
|
||||
|
@ -457,6 +461,8 @@ DECL_HANDLER(set_thread_desktop)
|
|||
else
|
||||
current->desktop = req->handle; /* FIXME: should we close the old one? */
|
||||
|
||||
if (old_desktop != new_desktop) detach_thread_input( current );
|
||||
|
||||
if (old_desktop) release_object( old_desktop );
|
||||
release_object( new_desktop );
|
||||
release_object( winstation );
|
||||
|
|
Loading…
Reference in New Issue