Implemented registry change notifications.
This commit is contained in:
parent
c124fd6f04
commit
11f4b44451
|
@ -1841,7 +1841,30 @@ LONG WINAPI RegNotifyChangeKeyValue( HKEY hkey, BOOL fWatchSubTree,
|
||||||
DWORD fdwNotifyFilter, HANDLE hEvent,
|
DWORD fdwNotifyFilter, HANDLE hEvent,
|
||||||
BOOL fAsync )
|
BOOL fAsync )
|
||||||
{
|
{
|
||||||
FIXME("(%p,%i,%ld,%p,%i): stub\n",hkey,fWatchSubTree,fdwNotifyFilter,
|
LONG ret;
|
||||||
|
|
||||||
|
TRACE("(%p,%i,%ld,%p,%i)\n",hkey,fWatchSubTree,fdwNotifyFilter,
|
||||||
hEvent,fAsync);
|
hEvent,fAsync);
|
||||||
return ERROR_SUCCESS;
|
|
||||||
|
if( !fAsync )
|
||||||
|
hEvent = CreateEventA(NULL, 0, 0, NULL);
|
||||||
|
|
||||||
|
SERVER_START_REQ( set_registry_notification )
|
||||||
|
{
|
||||||
|
req->hkey = hkey;
|
||||||
|
req->event = hEvent;
|
||||||
|
req->subtree = fWatchSubTree;
|
||||||
|
req->filter = fdwNotifyFilter;
|
||||||
|
ret = RtlNtStatusToDosError( wine_server_call(req) );
|
||||||
|
}
|
||||||
|
SERVER_END_REQ;
|
||||||
|
|
||||||
|
if( !fAsync )
|
||||||
|
{
|
||||||
|
if( ret == ERROR_SUCCESS )
|
||||||
|
WaitForSingleObject( hEvent, INFINITE );
|
||||||
|
CloseHandle( hEvent );
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1893,6 +1893,20 @@ struct set_registry_levels_reply
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
struct set_registry_notification_request
|
||||||
|
{
|
||||||
|
struct request_header __header;
|
||||||
|
obj_handle_t hkey;
|
||||||
|
obj_handle_t event;
|
||||||
|
int subtree;
|
||||||
|
unsigned int filter;
|
||||||
|
};
|
||||||
|
struct set_registry_notification_reply
|
||||||
|
{
|
||||||
|
struct reply_header __header;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
struct create_timer_request
|
struct create_timer_request
|
||||||
{
|
{
|
||||||
|
@ -3044,6 +3058,7 @@ enum request
|
||||||
REQ_save_registry,
|
REQ_save_registry,
|
||||||
REQ_save_registry_atexit,
|
REQ_save_registry_atexit,
|
||||||
REQ_set_registry_levels,
|
REQ_set_registry_levels,
|
||||||
|
REQ_set_registry_notification,
|
||||||
REQ_create_timer,
|
REQ_create_timer,
|
||||||
REQ_open_timer,
|
REQ_open_timer,
|
||||||
REQ_set_timer,
|
REQ_set_timer,
|
||||||
|
@ -3220,6 +3235,7 @@ union generic_request
|
||||||
struct save_registry_request save_registry_request;
|
struct save_registry_request save_registry_request;
|
||||||
struct save_registry_atexit_request save_registry_atexit_request;
|
struct save_registry_atexit_request save_registry_atexit_request;
|
||||||
struct set_registry_levels_request set_registry_levels_request;
|
struct set_registry_levels_request set_registry_levels_request;
|
||||||
|
struct set_registry_notification_request set_registry_notification_request;
|
||||||
struct create_timer_request create_timer_request;
|
struct create_timer_request create_timer_request;
|
||||||
struct open_timer_request open_timer_request;
|
struct open_timer_request open_timer_request;
|
||||||
struct set_timer_request set_timer_request;
|
struct set_timer_request set_timer_request;
|
||||||
|
@ -3394,6 +3410,7 @@ union generic_reply
|
||||||
struct save_registry_reply save_registry_reply;
|
struct save_registry_reply save_registry_reply;
|
||||||
struct save_registry_atexit_reply save_registry_atexit_reply;
|
struct save_registry_atexit_reply save_registry_atexit_reply;
|
||||||
struct set_registry_levels_reply set_registry_levels_reply;
|
struct set_registry_levels_reply set_registry_levels_reply;
|
||||||
|
struct set_registry_notification_reply set_registry_notification_reply;
|
||||||
struct create_timer_reply create_timer_reply;
|
struct create_timer_reply create_timer_reply;
|
||||||
struct open_timer_reply open_timer_reply;
|
struct open_timer_reply open_timer_reply;
|
||||||
struct set_timer_reply set_timer_reply;
|
struct set_timer_reply set_timer_reply;
|
||||||
|
@ -3462,6 +3479,6 @@ union generic_reply
|
||||||
struct get_next_hook_reply get_next_hook_reply;
|
struct get_next_hook_reply get_next_hook_reply;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define SERVER_PROTOCOL_VERSION 91
|
#define SERVER_PROTOCOL_VERSION 92
|
||||||
|
|
||||||
#endif /* __WINE_WINE_SERVER_PROTOCOL_H */
|
#endif /* __WINE_WINE_SERVER_PROTOCOL_H */
|
||||||
|
|
|
@ -3360,7 +3360,10 @@ typedef enum tagSID_NAME_USE {
|
||||||
#define REG_OPENED_EXISTING_KEY 0x00000002
|
#define REG_OPENED_EXISTING_KEY 0x00000002
|
||||||
|
|
||||||
/* For RegNotifyChangeKeyValue */
|
/* For RegNotifyChangeKeyValue */
|
||||||
#define REG_NOTIFY_CHANGE_NAME 0x1
|
#define REG_NOTIFY_CHANGE_NAME 0x01
|
||||||
|
#define REG_NOTIFY_CHANGE_ATTRIBUTES 0x02
|
||||||
|
#define REG_NOTIFY_CHANGE_LAST_SET 0x04
|
||||||
|
#define REG_NOTIFY_CHANGE_SECURITY 0x08
|
||||||
|
|
||||||
#define KEY_QUERY_VALUE 0x00000001
|
#define KEY_QUERY_VALUE 0x00000001
|
||||||
#define KEY_SET_VALUE 0x00000002
|
#define KEY_SET_VALUE 0x00000002
|
||||||
|
|
|
@ -332,6 +332,8 @@ int close_handle( struct process *process, obj_handle_t handle, int *fd )
|
||||||
table = handle_is_global(handle) ? global_table : process->handles;
|
table = handle_is_global(handle) ? global_table : process->handles;
|
||||||
if (entry < table->entries + table->free) table->free = entry - table->entries;
|
if (entry < table->entries + table->free) table->free = entry - table->entries;
|
||||||
if (entry == table->entries + table->last) shrink_handle_table( table );
|
if (entry == table->entries + table->last) shrink_handle_table( table );
|
||||||
|
/* hack: windows seems to treat registry handles differently */
|
||||||
|
registry_close_handle( obj, handle );
|
||||||
release_object( obj );
|
release_object( obj );
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,6 +195,7 @@ extern int get_page_size(void);
|
||||||
extern void init_registry(void);
|
extern void init_registry(void);
|
||||||
extern void flush_registry(void);
|
extern void flush_registry(void);
|
||||||
extern void close_registry(void);
|
extern void close_registry(void);
|
||||||
|
extern void registry_close_handle( struct object *obj, obj_handle_t hkey );
|
||||||
|
|
||||||
/* atom functions */
|
/* atom functions */
|
||||||
|
|
||||||
|
|
|
@ -1358,6 +1358,14 @@ enum char_info_mode
|
||||||
@END
|
@END
|
||||||
|
|
||||||
|
|
||||||
|
@REQ(set_registry_notification)
|
||||||
|
obj_handle_t hkey; /* key to watch for changes */
|
||||||
|
obj_handle_t event; /* event to set */
|
||||||
|
int subtree; /* should we watch the whole subtree? */
|
||||||
|
unsigned int filter; /* things to watch */
|
||||||
|
@END
|
||||||
|
|
||||||
|
|
||||||
/* Create a waitable timer */
|
/* Create a waitable timer */
|
||||||
@REQ(create_timer)
|
@REQ(create_timer)
|
||||||
int inherit; /* inherit flag */
|
int inherit; /* inherit flag */
|
||||||
|
|
|
@ -48,6 +48,16 @@
|
||||||
#include "winternl.h"
|
#include "winternl.h"
|
||||||
#include "wine/library.h"
|
#include "wine/library.h"
|
||||||
|
|
||||||
|
struct notify
|
||||||
|
{
|
||||||
|
struct event *event; /* event to set when changing this key */
|
||||||
|
int subtree; /* true if subtree notification */
|
||||||
|
unsigned int filter; /* which events to notify on */
|
||||||
|
obj_handle_t hkey; /* hkey associated with this notification */
|
||||||
|
struct notify *next; /* list of notifications */
|
||||||
|
struct notify *prev; /* list of notifications */
|
||||||
|
};
|
||||||
|
|
||||||
/* a registry key */
|
/* a registry key */
|
||||||
struct key
|
struct key
|
||||||
{
|
{
|
||||||
|
@ -64,6 +74,8 @@ struct key
|
||||||
short flags; /* flags */
|
short flags; /* flags */
|
||||||
short level; /* saving level */
|
short level; /* saving level */
|
||||||
time_t modif; /* last modification time */
|
time_t modif; /* last modification time */
|
||||||
|
struct notify *first_notify; /* list of notifications */
|
||||||
|
struct notify *last_notify; /* list of notifications */
|
||||||
};
|
};
|
||||||
|
|
||||||
/* key flags */
|
/* key flags */
|
||||||
|
@ -284,6 +296,53 @@ static void key_dump( struct object *obj, int verbose )
|
||||||
fprintf( stderr, "\n" );
|
fprintf( stderr, "\n" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* notify waiter and maybe delete the notification */
|
||||||
|
static void do_notification( struct key *key, struct notify *notify, int del )
|
||||||
|
{
|
||||||
|
if( notify->event )
|
||||||
|
{
|
||||||
|
set_event( notify->event );
|
||||||
|
release_object( notify->event );
|
||||||
|
notify->event = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !del )
|
||||||
|
return;
|
||||||
|
if( notify->next )
|
||||||
|
notify->next->prev = notify->prev;
|
||||||
|
else
|
||||||
|
key->last_notify = notify->prev;
|
||||||
|
if( notify->prev )
|
||||||
|
notify->prev->next = notify->next;
|
||||||
|
else
|
||||||
|
key->first_notify = notify->next;
|
||||||
|
free( notify );
|
||||||
|
}
|
||||||
|
|
||||||
|
static struct notify *find_notify( struct key *key, obj_handle_t hkey)
|
||||||
|
{
|
||||||
|
struct notify *n;
|
||||||
|
|
||||||
|
for( n=key->first_notify; n; n = n->next)
|
||||||
|
if( n->hkey == hkey )
|
||||||
|
break;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* close the notification associated with a handle */
|
||||||
|
void registry_close_handle( struct object *obj, obj_handle_t hkey )
|
||||||
|
{
|
||||||
|
struct key * key = (struct key *) obj;
|
||||||
|
struct notify *notify;
|
||||||
|
|
||||||
|
if( obj->ops != &key_ops )
|
||||||
|
return;
|
||||||
|
notify = find_notify( key, hkey );
|
||||||
|
if( !notify )
|
||||||
|
return;
|
||||||
|
do_notification( key, notify, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
static void key_destroy( struct object *obj )
|
static void key_destroy( struct object *obj )
|
||||||
{
|
{
|
||||||
int i;
|
int i;
|
||||||
|
@ -302,6 +361,9 @@ static void key_destroy( struct object *obj )
|
||||||
key->subkeys[i]->parent = NULL;
|
key->subkeys[i]->parent = NULL;
|
||||||
release_object( key->subkeys[i] );
|
release_object( key->subkeys[i] );
|
||||||
}
|
}
|
||||||
|
/* unconditionally notify everything waiting on this key */
|
||||||
|
while ( key->first_notify )
|
||||||
|
do_notification( key, key->first_notify, 1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/* duplicate a key path */
|
/* duplicate a key path */
|
||||||
|
@ -389,6 +451,8 @@ static struct key *alloc_key( const WCHAR *name, time_t modif )
|
||||||
key->level = current_level;
|
key->level = current_level;
|
||||||
key->modif = modif;
|
key->modif = modif;
|
||||||
key->parent = NULL;
|
key->parent = NULL;
|
||||||
|
key->first_notify = NULL;
|
||||||
|
key->last_notify = NULL;
|
||||||
if (!(key->name = strdupW( name )))
|
if (!(key->name = strdupW( name )))
|
||||||
{
|
{
|
||||||
release_object( key );
|
release_object( key );
|
||||||
|
@ -420,12 +484,32 @@ static void make_clean( struct key *key )
|
||||||
for (i = 0; i <= key->last_subkey; i++) make_clean( key->subkeys[i] );
|
for (i = 0; i <= key->last_subkey; i++) make_clean( key->subkeys[i] );
|
||||||
}
|
}
|
||||||
|
|
||||||
/* update key modification time */
|
/* go through all the notifications and send them if necessary */
|
||||||
static void touch_key( struct key *key )
|
void check_notify( struct key *key, unsigned int change, int not_subtree )
|
||||||
{
|
{
|
||||||
|
struct notify *n = key->first_notify;
|
||||||
|
while (n)
|
||||||
|
{
|
||||||
|
struct notify *next = n->next;
|
||||||
|
if ( ( not_subtree || n->subtree ) && ( change & n->filter ) )
|
||||||
|
do_notification( key, n, 0 );
|
||||||
|
n = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* update key modification time */
|
||||||
|
static void touch_key( struct key *key, unsigned int change )
|
||||||
|
{
|
||||||
|
struct key *k;
|
||||||
|
|
||||||
key->modif = time(NULL);
|
key->modif = time(NULL);
|
||||||
key->level = max( key->level, current_level );
|
key->level = max( key->level, current_level );
|
||||||
make_dirty( key );
|
make_dirty( key );
|
||||||
|
|
||||||
|
/* do notifications */
|
||||||
|
check_notify( key, change, 1 );
|
||||||
|
for ( k = key->parent; k; k = k->parent )
|
||||||
|
check_notify( k, change & ~REG_NOTIFY_CHANGE_LAST_SET, 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/* try to grow the array of subkeys; return 1 if OK, 0 on error */
|
/* try to grow the array of subkeys; return 1 if OK, 0 on error */
|
||||||
|
@ -583,6 +667,7 @@ static struct key *create_key( struct key *key, WCHAR *name, WCHAR *class,
|
||||||
|
|
||||||
if (!*path) goto done;
|
if (!*path) goto done;
|
||||||
*created = 1;
|
*created = 1;
|
||||||
|
touch_key( key, REG_NOTIFY_CHANGE_NAME ); /* FIXME: is this right? */
|
||||||
if (flags & KEY_DIRTY) make_dirty( key );
|
if (flags & KEY_DIRTY) make_dirty( key );
|
||||||
base = key;
|
base = key;
|
||||||
base_idx = index;
|
base_idx = index;
|
||||||
|
@ -718,7 +803,7 @@ static void delete_key( struct key *key )
|
||||||
}
|
}
|
||||||
if (debug_level > 1) dump_operation( key, NULL, "Delete" );
|
if (debug_level > 1) dump_operation( key, NULL, "Delete" );
|
||||||
free_subkey( parent, index );
|
free_subkey( parent, index );
|
||||||
touch_key( parent );
|
touch_key( parent, REG_NOTIFY_CHANGE_NAME );
|
||||||
}
|
}
|
||||||
|
|
||||||
/* try to grow the array of values; return 1 if OK, 0 on error */
|
/* try to grow the array of values; return 1 if OK, 0 on error */
|
||||||
|
@ -821,7 +906,7 @@ static void set_value( struct key *key, WCHAR *name, int type, const void *data,
|
||||||
value->type = type;
|
value->type = type;
|
||||||
value->len = len;
|
value->len = len;
|
||||||
value->data = ptr;
|
value->data = ptr;
|
||||||
touch_key( key );
|
touch_key( key, REG_NOTIFY_CHANGE_LAST_SET );
|
||||||
if (debug_level > 1) dump_operation( key, value, "Set" );
|
if (debug_level > 1) dump_operation( key, value, "Set" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -912,7 +997,7 @@ static void delete_value( struct key *key, const WCHAR *name )
|
||||||
if (value->data) free( value->data );
|
if (value->data) free( value->data );
|
||||||
for (i = index; i < key->last_value; i++) key->values[i] = key->values[i + 1];
|
for (i = index; i < key->last_value; i++) key->values[i] = key->values[i + 1];
|
||||||
key->last_value--;
|
key->last_value--;
|
||||||
touch_key( key );
|
touch_key( key, REG_NOTIFY_CHANGE_LAST_SET );
|
||||||
|
|
||||||
/* try to shrink the array */
|
/* try to shrink the array */
|
||||||
nb_values = key->nb_values;
|
nb_values = key->nb_values;
|
||||||
|
@ -1817,3 +1902,52 @@ DECL_HANDLER(save_registry_atexit)
|
||||||
release_object( key );
|
release_object( key );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* add a registry key change notification */
|
||||||
|
DECL_HANDLER(set_registry_notification)
|
||||||
|
{
|
||||||
|
struct key *key;
|
||||||
|
struct event *event;
|
||||||
|
struct notify *notify;
|
||||||
|
|
||||||
|
key = get_hkey_obj( req->hkey, KEY_NOTIFY );
|
||||||
|
if( key )
|
||||||
|
{
|
||||||
|
event = get_event_obj( current->process, req->event, SYNCHRONIZE );
|
||||||
|
if( event )
|
||||||
|
{
|
||||||
|
notify = find_notify( key, req->hkey );
|
||||||
|
if( notify )
|
||||||
|
{
|
||||||
|
release_object( notify->event );
|
||||||
|
grab_object( event );
|
||||||
|
notify->event = event;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
notify = (struct notify *) malloc (sizeof *notify);
|
||||||
|
if( notify )
|
||||||
|
{
|
||||||
|
grab_object( event );
|
||||||
|
notify->event = event;
|
||||||
|
notify->subtree = req->subtree;
|
||||||
|
notify->filter = req->filter;
|
||||||
|
notify->hkey = req->hkey;
|
||||||
|
|
||||||
|
/* add to linked list */
|
||||||
|
notify->prev = NULL;
|
||||||
|
notify->next = key->first_notify;
|
||||||
|
if ( notify->next )
|
||||||
|
notify->next->prev = notify;
|
||||||
|
else
|
||||||
|
key->last_notify = notify;
|
||||||
|
key->first_notify = notify;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
set_error( STATUS_NO_MEMORY );
|
||||||
|
}
|
||||||
|
release_object( event );
|
||||||
|
}
|
||||||
|
release_object( key );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -206,6 +206,7 @@ DECL_HANDLER(load_registry);
|
||||||
DECL_HANDLER(save_registry);
|
DECL_HANDLER(save_registry);
|
||||||
DECL_HANDLER(save_registry_atexit);
|
DECL_HANDLER(save_registry_atexit);
|
||||||
DECL_HANDLER(set_registry_levels);
|
DECL_HANDLER(set_registry_levels);
|
||||||
|
DECL_HANDLER(set_registry_notification);
|
||||||
DECL_HANDLER(create_timer);
|
DECL_HANDLER(create_timer);
|
||||||
DECL_HANDLER(open_timer);
|
DECL_HANDLER(open_timer);
|
||||||
DECL_HANDLER(set_timer);
|
DECL_HANDLER(set_timer);
|
||||||
|
@ -381,6 +382,7 @@ static const req_handler req_handlers[REQ_NB_REQUESTS] =
|
||||||
(req_handler)req_save_registry,
|
(req_handler)req_save_registry,
|
||||||
(req_handler)req_save_registry_atexit,
|
(req_handler)req_save_registry_atexit,
|
||||||
(req_handler)req_set_registry_levels,
|
(req_handler)req_set_registry_levels,
|
||||||
|
(req_handler)req_set_registry_notification,
|
||||||
(req_handler)req_create_timer,
|
(req_handler)req_create_timer,
|
||||||
(req_handler)req_open_timer,
|
(req_handler)req_open_timer,
|
||||||
(req_handler)req_set_timer,
|
(req_handler)req_set_timer,
|
||||||
|
|
|
@ -1569,6 +1569,14 @@ static void dump_set_registry_levels_request( const struct set_registry_levels_r
|
||||||
fprintf( stderr, " period=%d", req->period );
|
fprintf( stderr, " period=%d", req->period );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void dump_set_registry_notification_request( const struct set_registry_notification_request *req )
|
||||||
|
{
|
||||||
|
fprintf( stderr, " hkey=%p,", req->hkey );
|
||||||
|
fprintf( stderr, " event=%p,", req->event );
|
||||||
|
fprintf( stderr, " subtree=%d,", req->subtree );
|
||||||
|
fprintf( stderr, " filter=%08x", req->filter );
|
||||||
|
}
|
||||||
|
|
||||||
static void dump_create_timer_request( const struct create_timer_request *req )
|
static void dump_create_timer_request( const struct create_timer_request *req )
|
||||||
{
|
{
|
||||||
fprintf( stderr, " inherit=%d,", req->inherit );
|
fprintf( stderr, " inherit=%d,", req->inherit );
|
||||||
|
@ -2445,6 +2453,7 @@ static const dump_func req_dumpers[REQ_NB_REQUESTS] = {
|
||||||
(dump_func)dump_save_registry_request,
|
(dump_func)dump_save_registry_request,
|
||||||
(dump_func)dump_save_registry_atexit_request,
|
(dump_func)dump_save_registry_atexit_request,
|
||||||
(dump_func)dump_set_registry_levels_request,
|
(dump_func)dump_set_registry_levels_request,
|
||||||
|
(dump_func)dump_set_registry_notification_request,
|
||||||
(dump_func)dump_create_timer_request,
|
(dump_func)dump_create_timer_request,
|
||||||
(dump_func)dump_open_timer_request,
|
(dump_func)dump_open_timer_request,
|
||||||
(dump_func)dump_set_timer_request,
|
(dump_func)dump_set_timer_request,
|
||||||
|
@ -2617,6 +2626,7 @@ static const dump_func reply_dumpers[REQ_NB_REQUESTS] = {
|
||||||
(dump_func)0,
|
(dump_func)0,
|
||||||
(dump_func)0,
|
(dump_func)0,
|
||||||
(dump_func)0,
|
(dump_func)0,
|
||||||
|
(dump_func)0,
|
||||||
(dump_func)dump_create_timer_reply,
|
(dump_func)dump_create_timer_reply,
|
||||||
(dump_func)dump_open_timer_reply,
|
(dump_func)dump_open_timer_reply,
|
||||||
(dump_func)0,
|
(dump_func)0,
|
||||||
|
@ -2789,6 +2799,7 @@ static const char * const req_names[REQ_NB_REQUESTS] = {
|
||||||
"save_registry",
|
"save_registry",
|
||||||
"save_registry_atexit",
|
"save_registry_atexit",
|
||||||
"set_registry_levels",
|
"set_registry_levels",
|
||||||
|
"set_registry_notification",
|
||||||
"create_timer",
|
"create_timer",
|
||||||
"open_timer",
|
"open_timer",
|
||||||
"set_timer",
|
"set_timer",
|
||||||
|
|
Loading…
Reference in New Issue