mirror of https://github.com/OpenVPN/openvpn-gui
Support sending commands to running instance
- New option --command <action> <params> to send commands to a running instance of openvpn-gui.exe Supported actions are connect, disconnect, reconnect each of which takes the name of the config (with or without the extension .ovpn) as a parameter; disconnect_all, exit which take no parameter and silent_connection which takes an optional parameter = 0 or 1 (1 is the default) Examples: with the gui running, start a new instance as openvpn-gui.exe --command disconnect myvpn : ask running instance to disconnect myvpn if connected openvpn-gui.exe --command status myvpn : ask running instance to show the status window for myvpn if available openvpn-gui.exe --command disconnect_all : ask running instance to disconnect all active connections - The second instance exits after issuing a SendMessage to the already running instance. If no action is specified, the running instance is notified to show a balloon to alert the user - These messages may also be sent from scripts as COPYDATA messages with the wData element specifying the action to execute and lpData a pointer to the parameter. The dwData param must be one of WM_OVPN_xxx with xxx = START, STOP, RESTART, STOPALL, EXIT or SILENT. See main.h for their values. v2: Bug fixes based on test reports from larson0815 here: https://github.com/selvanair/openvpn-gui/issues/5 and cron410 here: https://github.com/OpenVPN/openvpn-gui/issues/104 Signed-off-by: Selva Nair <selva.nair@gmail.com>pull/188/head
parent
31896ce33b
commit
0f21030774
156
main.c
156
main.c
|
@ -55,6 +55,10 @@
|
|||
#include <openssl/err.h>
|
||||
#endif
|
||||
|
||||
#define OVPN_EXITCODE_ERROR 1
|
||||
#define OVPN_EXITCODE_TIMEOUT 2
|
||||
#define OVPN_EXITCODE_NOTREADY 3
|
||||
|
||||
/* Declare Windows procedure */
|
||||
LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM);
|
||||
static void ShowSettingsDialog();
|
||||
|
@ -100,6 +104,49 @@ VerifyAutoConnections()
|
|||
return TRUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* Send a copydata message corresponding to any --command action option specified
|
||||
* to the running instance and return success or error.
|
||||
*/
|
||||
static int
|
||||
NotifyRunningInstance()
|
||||
{
|
||||
/* Check if a previous instance has a window initialized
|
||||
* Even if we are not the first instance this may return null
|
||||
* if the previous instance has not fully started up
|
||||
*/
|
||||
HANDLE hwnd_master = FindWindow (szClassName, NULL);
|
||||
int exit_code = 0;
|
||||
if (hwnd_master)
|
||||
{
|
||||
/* GUI up and running -- send a message if any action is pecified,
|
||||
else show the balloon */
|
||||
COPYDATASTRUCT config_data = {0};
|
||||
int timeout = 30*1000; /* 30 seconds */
|
||||
if (!o.action)
|
||||
{
|
||||
o.action = WM_OVPN_NOTIFY;
|
||||
o.action_arg = LoadLocalizedString(IDS_NFO_CLICK_HERE_TO_START);
|
||||
}
|
||||
config_data.dwData = o.action;
|
||||
if (o.action_arg)
|
||||
{
|
||||
config_data.cbData = (wcslen(o.action_arg)+1)*sizeof(o.action_arg[0]);
|
||||
config_data.lpData = (void *) o.action_arg;
|
||||
}
|
||||
PrintDebug(L"Instance 2: called with action %d : %s", o.action, o.action_arg);
|
||||
if (!SendMessageTimeout (hwnd_master, WM_COPYDATA, 0,
|
||||
(LPARAM) &config_data, 0, timeout, NULL))
|
||||
exit_code = OVPN_EXITCODE_TIMEOUT;
|
||||
}
|
||||
else
|
||||
{
|
||||
PrintDebug(L"Instance 2: Previous instance not yet ready to accept comamnds");
|
||||
exit_code = OVPN_EXITCODE_NOTREADY;
|
||||
}
|
||||
PrintDebug(L"Instance 2: Returning exit code %d", exit_code);
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
int WINAPI _tWinMain (HINSTANCE hThisInstance,
|
||||
UNUSED HINSTANCE hPrevInstance,
|
||||
|
@ -109,6 +156,16 @@ int WINAPI _tWinMain (HINSTANCE hThisInstance,
|
|||
MSG messages; /* Here messages to the application are saved */
|
||||
WNDCLASSEX wincl; /* Data structure for the windowclass */
|
||||
DWORD shell32_version;
|
||||
BOOL first_instance = TRUE;
|
||||
/* a session local semaphore to detect second instance */
|
||||
HANDLE session_semaphore = InitSemaphore(L"Local\\"PACKAGE_NAME);
|
||||
|
||||
/* try to lock the semaphore, else we are not the first instance */
|
||||
if (session_semaphore &&
|
||||
WaitForSingleObject(session_semaphore, 0) != WAIT_OBJECT_0)
|
||||
{
|
||||
first_instance = FALSE;
|
||||
}
|
||||
|
||||
/* Initialize handlers for manangement interface notifications */
|
||||
mgmt_rtmsg_handler handler[] = {
|
||||
|
@ -166,16 +223,8 @@ int WINAPI _tWinMain (HINSTANCE hThisInstance,
|
|||
PrintDebug(_T("Shell32.dll version: 0x%lx"), shell32_version);
|
||||
#endif
|
||||
|
||||
|
||||
/* Check if a previous instance is already running. */
|
||||
if ((FindWindow (szClassName, NULL)) != NULL)
|
||||
{
|
||||
/* GUI already running */
|
||||
ShowLocalizedMsg(IDS_ERR_GUI_ALREADY_RUNNING);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
UpdateRegistry(); /* Checks version change and update keys/values */
|
||||
if (first_instance)
|
||||
UpdateRegistry(); /* Checks version change and update keys/values */
|
||||
|
||||
GetRegistryKeys();
|
||||
/* Parse command-line options */
|
||||
|
@ -183,6 +232,21 @@ int WINAPI _tWinMain (HINSTANCE hThisInstance,
|
|||
|
||||
EnsureDirExists(o.config_dir);
|
||||
|
||||
if (!first_instance)
|
||||
{
|
||||
int res = NotifyRunningInstance();
|
||||
exit(res);
|
||||
}
|
||||
else if (o.action == WM_OVPN_START)
|
||||
{
|
||||
PrintDebug(L"Instance 1: Called with --command connect xxx. Treating it as --connect xxx");
|
||||
}
|
||||
else if (o.action)
|
||||
{
|
||||
PrintDebug(L"Instance 1: Called with --command when no previous instance available");
|
||||
exit(OVPN_EXITCODE_ERROR);
|
||||
}
|
||||
|
||||
if (!CheckVersion()) {
|
||||
exit(1);
|
||||
}
|
||||
|
@ -200,6 +264,7 @@ int WINAPI _tWinMain (HINSTANCE hThisInstance,
|
|||
if (!VerifyAutoConnections()) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
GetProxyRegistrySettings();
|
||||
|
||||
#ifndef DISABLE_CHANGE_PASSWORD
|
||||
|
@ -350,6 +415,68 @@ dpi_initialize(void)
|
|||
dpi_setscale(dpix);
|
||||
}
|
||||
|
||||
static int
|
||||
HandleCopyDataMessage(const COPYDATASTRUCT *copy_data)
|
||||
{
|
||||
WCHAR *str = NULL;
|
||||
connection_t *c = NULL;
|
||||
PrintDebug (L"WM_COPYDATA message received. (dwData: %lu, cbData: %lu, lpData: %s)",
|
||||
copy_data->dwData, copy_data->cbData, copy_data->lpData);
|
||||
if (copy_data->cbData >= sizeof(WCHAR) && copy_data->lpData)
|
||||
{
|
||||
str = (WCHAR*) copy_data->lpData;
|
||||
str[copy_data->cbData/sizeof(WCHAR)-1] = L'\0'; /* in case not nul-terminated */
|
||||
c = GetConnByName(str);
|
||||
}
|
||||
if(copy_data->dwData == WM_OVPN_START && c)
|
||||
{
|
||||
if (!o.silent_connection)
|
||||
ForceForegroundWindow(o.hWnd);
|
||||
StartOpenVPN(c);
|
||||
}
|
||||
else if(copy_data->dwData == WM_OVPN_STOP && c)
|
||||
StopOpenVPN(c);
|
||||
else if(copy_data->dwData == WM_OVPN_RESTART && c)
|
||||
{
|
||||
if (!o.silent_connection)
|
||||
ForceForegroundWindow(o.hWnd);
|
||||
RestartOpenVPN(c);
|
||||
}
|
||||
else if(copy_data->dwData == WM_OVPN_SHOWSTATUS && c->hwndStatus && c)
|
||||
{
|
||||
ForceForegroundWindow(o.hWnd);
|
||||
ShowWindow(c->hwndStatus, SW_SHOW);
|
||||
}
|
||||
else if(copy_data->dwData == WM_OVPN_STOPALL)
|
||||
StopAllOpenVPN();
|
||||
else if(copy_data->dwData == WM_OVPN_SILENT && str)
|
||||
{
|
||||
if (_wtoi(str) == 0)
|
||||
o.silent_connection = 0;
|
||||
else
|
||||
o.silent_connection = 1;
|
||||
}
|
||||
else if(copy_data->dwData == WM_OVPN_EXIT)
|
||||
{
|
||||
CloseApplication(o.hWnd);
|
||||
}
|
||||
/* Not yet implemented
|
||||
else if(copy_data->dwData == WM_OVPN_IMPORT)
|
||||
{
|
||||
}
|
||||
*/
|
||||
else if (copy_data->dwData == WM_OVPN_NOTIFY)
|
||||
{
|
||||
ShowTrayBalloon(L"", copy_data->lpData);
|
||||
}
|
||||
else
|
||||
{
|
||||
PrintDebug (L"WM_COPYDATA message ignored. (dwData: %lu, cbData: %lu)",
|
||||
copy_data->dwData, copy_data->cbData);
|
||||
}
|
||||
return TRUE; /* indicate we handled the message */
|
||||
}
|
||||
|
||||
/* This function is called by the Windows function DispatchMessage() */
|
||||
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
|
@ -373,6 +500,11 @@ LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM
|
|||
SendMessage(hwnd, WM_SETICON, (WPARAM) (ICON_BIG), (LPARAM) (hIcon));
|
||||
}
|
||||
|
||||
/* Enable next line to accept WM_COPYDATA messages from lower level processes */
|
||||
#if 0
|
||||
ChangeWindowMessageFilterEx(hwnd, WM_COPYDATA, MSGFLT_ALLOW, NULL);
|
||||
#endif
|
||||
|
||||
CreatePopupMenus(); /* Create popup menus */
|
||||
ShowTrayIcon();
|
||||
if (o.service_only)
|
||||
|
@ -387,6 +519,10 @@ LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM
|
|||
OnNotifyTray(lParam); // Manages message from tray
|
||||
break;
|
||||
|
||||
case WM_COPYDATA: // custom messages with data from other processes
|
||||
HandleCopyDataMessage((COPYDATASTRUCT*) lParam);
|
||||
return TRUE; /* lets the sender free copy_data */
|
||||
|
||||
case WM_COMMAND:
|
||||
if ( (LOWORD(wParam) >= IDM_CONNECTMENU) && (LOWORD(wParam) < IDM_CONNECTMENU + MAX_CONFIGS) ) {
|
||||
StartOpenVPN(&o.conn[LOWORD(wParam) - IDM_CONNECTMENU]);
|
||||
|
|
15
main.h
15
main.h
|
@ -45,6 +45,21 @@
|
|||
/* Authorized group who can use any options and config locations */
|
||||
#define OVPN_ADMIN_GROUP TEXT("OpenVPN Administrators") /* May be reset in registry */
|
||||
|
||||
/* Application defined message IDs */
|
||||
#define WM_NOTIFYICONTRAY (WM_APP + 1)
|
||||
#define WM_MANAGEMENT (WM_APP + 2)
|
||||
|
||||
#define WM_OVPN_STOP (WM_APP + 10)
|
||||
#define WM_OVPN_SUSPEND (WM_APP + 11)
|
||||
#define WM_OVPN_RESTART (WM_APP + 12)
|
||||
#define WM_OVPN_START (WM_APP + 13)
|
||||
#define WM_OVPN_STOPALL (WM_APP + 14)
|
||||
#define WM_OVPN_SHOWSTATUS (WM_APP + 15)
|
||||
#define WM_OVPN_NOTIFY (WM_APP + 16)
|
||||
#define WM_OVPN_EXIT (WM_APP + 17)
|
||||
#define WM_OVPN_SILENT (WM_APP + 18)
|
||||
#define WM_OVPN_IMPORT (WM_APP + 20)
|
||||
|
||||
/* bool definitions */
|
||||
#define bool int
|
||||
#define true 1
|
||||
|
|
2
manage.h
2
manage.h
|
@ -24,8 +24,6 @@
|
|||
|
||||
#include <winsock2.h>
|
||||
|
||||
#define WM_MANAGEMENT (WM_APP + 2)
|
||||
|
||||
typedef enum {
|
||||
ready,
|
||||
stop,
|
||||
|
|
4
misc.c
4
misc.c
|
@ -396,10 +396,10 @@ BOOL IsUserAdmin(VOID)
|
|||
}
|
||||
|
||||
HANDLE
|
||||
InitSemaphore (void)
|
||||
InitSemaphore (WCHAR *name)
|
||||
{
|
||||
HANDLE semaphore = NULL;
|
||||
semaphore = CreateSemaphore (NULL, 1, 1, NULL);
|
||||
semaphore = CreateSemaphore (NULL, 1, 1, name);
|
||||
if (!semaphore)
|
||||
{
|
||||
MessageBoxW (NULL, L"Error creating semaphore", TEXT(PACKAGE_NAME), MB_OK);
|
||||
|
|
2
misc.h
2
misc.h
|
@ -33,7 +33,7 @@ BOOL wcsbegins(LPCWSTR, LPCWSTR);
|
|||
BOOL ForceForegroundWindow(HWND);
|
||||
|
||||
BOOL IsUserAdmin(VOID);
|
||||
HANDLE InitSemaphore (void);
|
||||
HANDLE InitSemaphore (WCHAR *);
|
||||
BOOL CheckFileAccess (const TCHAR *path, int access);
|
||||
|
||||
BOOL Base64Encode(const char *input, int input_len, char **output);
|
||||
|
|
|
@ -216,6 +216,7 @@
|
|||
#define IDS_NFO_ACTIVE_CONN_EXIT 1307
|
||||
#define IDS_NFO_SERVICE_ACTIVE_EXIT 1308
|
||||
#define IDS_ERR_CREATE_PATH 1309
|
||||
#define IDS_NFO_CLICK_HERE_TO_START 1310
|
||||
|
||||
/* Program Options Related */
|
||||
#define IDS_NFO_USAGE 1401
|
||||
|
|
31
openvpn.c
31
openvpn.c
|
@ -50,10 +50,6 @@
|
|||
#include "access.h"
|
||||
#include "save_pass.h"
|
||||
|
||||
#define WM_OVPN_STOP (WM_APP + 10)
|
||||
#define WM_OVPN_SUSPEND (WM_APP + 11)
|
||||
#define WM_OVPN_RESTART (WM_APP + 12)
|
||||
|
||||
extern options_t o;
|
||||
|
||||
static BOOL
|
||||
|
@ -1598,6 +1594,9 @@ StatusDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam)
|
|||
|
||||
case WM_OVPN_STOP:
|
||||
c = (connection_t *) GetProp(hwndDlg, cfgProp);
|
||||
/* external messages can trigger when we are not ready -- check the state */
|
||||
if (!IsWindowEnabled(GetDlgItem(c->hwndStatus, ID_DISCONNECT)))
|
||||
break;
|
||||
c->state = disconnecting;
|
||||
RunDisconnectScript(c, false);
|
||||
EnableWindow(GetDlgItem(c->hwndStatus, ID_DISCONNECT), FALSE);
|
||||
|
@ -1605,7 +1604,7 @@ StatusDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam)
|
|||
SetMenuStatus(c, disconnecting);
|
||||
SetDlgItemText(c->hwndStatus, ID_TXT_STATUS, LoadLocalizedString(IDS_NFO_STATE_WAIT_TERM));
|
||||
SetEvent(c->exit_event);
|
||||
SetTimer(hwndDlg, IDT_STOP_TIMER, 3000, NULL);
|
||||
SetTimer(hwndDlg, IDT_STOP_TIMER, 15000, NULL);
|
||||
break;
|
||||
|
||||
case WM_OVPN_SUSPEND:
|
||||
|
@ -1616,7 +1615,7 @@ StatusDialogFunc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam)
|
|||
SetMenuStatus(c, disconnecting);
|
||||
SetDlgItemText(c->hwndStatus, ID_TXT_STATUS, LoadLocalizedString(IDS_NFO_STATE_WAIT_TERM));
|
||||
SetEvent(c->exit_event);
|
||||
SetTimer(hwndDlg, IDT_STOP_TIMER, 3000, NULL);
|
||||
SetTimer(hwndDlg, IDT_STOP_TIMER, 15000, NULL);
|
||||
break;
|
||||
|
||||
case WM_TIMER:
|
||||
|
@ -1762,12 +1761,19 @@ StartOpenVPN(connection_t *c)
|
|||
|
||||
if (c->hwndStatus)
|
||||
{
|
||||
PrintDebug(L"Connection request when previous status window is still open -- ignored");
|
||||
WriteStatusLog(c, L"OpenVPN GUI> ",
|
||||
L"Complete the pending dialog before starting a new connection", false);
|
||||
SetForegroundWindow(c->hwndStatus);
|
||||
PrintDebug(L"Connection request when already started -- ignored");
|
||||
/* the tread can hang around after disconnect if user has not dismissed any popups */
|
||||
if (c->state == disconnected)
|
||||
WriteStatusLog(c, L"OpenVPN GUI> ",
|
||||
L"Complete any pending dialog before starting a new connection", false);
|
||||
if (!o.silent_connection)
|
||||
{
|
||||
SetForegroundWindow(c->hwndStatus);
|
||||
ShowWindow(c->hwndStatus, SW_SHOW);
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
PrintDebug(L"Starting openvpn on config %s", c->config_name);
|
||||
|
||||
RunPreconnectScript(c);
|
||||
|
||||
|
@ -1971,7 +1977,10 @@ SuspendOpenVPN(int config)
|
|||
void
|
||||
RestartOpenVPN(connection_t *c)
|
||||
{
|
||||
PostMessage(c->hwndStatus, WM_OVPN_RESTART, 0, 0);
|
||||
if (c->hwndStatus)
|
||||
PostMessage(c->hwndStatus, WM_OVPN_RESTART, 0, 0);
|
||||
else /* Not started: treat this as a request to connect */
|
||||
StartOpenVPN(c);
|
||||
}
|
||||
|
||||
void
|
||||
|
|
62
options.c
62
options.c
|
@ -203,6 +203,54 @@ add_option(options_t *options, int i, TCHAR **p)
|
|||
++i;
|
||||
options->preconnectscript_timeout = _ttoi(p[1]);
|
||||
}
|
||||
else if (streq(p[0], _T("command")) && p[1])
|
||||
{
|
||||
++i;
|
||||
/* command to be sent to a running instance */
|
||||
if (streq(p[1], _T("connect")) && p[2])
|
||||
{
|
||||
++i;
|
||||
options->action = WM_OVPN_START;
|
||||
options->action_arg = p[2];
|
||||
}
|
||||
else if (streq(p[1], _T("disconnect")) && p[2])
|
||||
{
|
||||
++i;
|
||||
options->action = WM_OVPN_STOP;
|
||||
options->action_arg = p[2];
|
||||
}
|
||||
else if (streq(p[1], _T("reconnect")) && p[2])
|
||||
{
|
||||
++i;
|
||||
options->action = WM_OVPN_RESTART;
|
||||
options->action_arg = p[2];
|
||||
}
|
||||
else if (streq(p[1], _T("status")) && p[2])
|
||||
{
|
||||
++i;
|
||||
options->action = WM_OVPN_SHOWSTATUS;
|
||||
options->action_arg = p[2];
|
||||
}
|
||||
else if (streq(p[1], _T("silent_connection")))
|
||||
{
|
||||
++i;
|
||||
options->action = WM_OVPN_SILENT;
|
||||
options->action_arg = p[2] ? p[2] : _T("1");
|
||||
}
|
||||
else if (streq(p[1], _T("disconnect_all")))
|
||||
{
|
||||
options->action = WM_OVPN_STOPALL;
|
||||
}
|
||||
else if (streq(p[1], _T("exit")))
|
||||
{
|
||||
options->action = WM_OVPN_EXIT;
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowLocalizedMsg(IDS_ERR_BAD_OPTION, p[0]);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Unrecognized option or missing parameter */
|
||||
|
@ -252,7 +300,7 @@ void
|
|||
InitOptions(options_t *opt)
|
||||
{
|
||||
CLEAR(*opt);
|
||||
opt->netcmd_semaphore = InitSemaphore ();
|
||||
opt->netcmd_semaphore = InitSemaphore (NULL);
|
||||
opt->version = MakeVersion (PACKAGE_VERSION_RESOURCE);
|
||||
opt->clr_warning = RGB(0xff, 0, 0);
|
||||
opt->clr_error = RGB(0xff, 0, 0);
|
||||
|
@ -354,6 +402,18 @@ GetConnByManagement(SOCKET sk)
|
|||
return NULL;
|
||||
}
|
||||
|
||||
connection_t*
|
||||
GetConnByName(const WCHAR *name)
|
||||
{
|
||||
for (int i = 0; i < o.num_configs; ++i)
|
||||
{
|
||||
if (wcsicmp (o.conn[i].config_file, name) == 0
|
||||
|| wcsicmp(o.conn[i].config_name, name) == 0)
|
||||
return &o.conn[i];
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* callback to set the initial value of folder browse selection */
|
||||
static int CALLBACK
|
||||
BrowseCallback (HWND h, UINT msg, UNUSED LPARAM l, LPARAM data)
|
||||
|
|
|
@ -180,12 +180,15 @@ typedef struct {
|
|||
unsigned int dpi_scale;
|
||||
COLORREF clr_warning;
|
||||
COLORREF clr_error;
|
||||
int action; /* action to send to a running instance */
|
||||
TCHAR *action_arg;
|
||||
} options_t;
|
||||
|
||||
void InitOptions(options_t *);
|
||||
void ProcessCommandLine(options_t *, TCHAR *);
|
||||
int CountConnState(conn_state_t);
|
||||
connection_t* GetConnByManagement(SOCKET);
|
||||
connection_t* GetConnByName(const WCHAR *config_name);
|
||||
INT_PTR CALLBACK ScriptSettingsDlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
INT_PTR CALLBACK ConnectionSettingsDlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
INT_PTR CALLBACK AdvancedSettingsDlgProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
|
|
|
@ -462,4 +462,6 @@ BEGIN
|
|||
IDS_ERR_INVALID_PASSWORD_INPUT "Invalid character in password"
|
||||
IDS_ERR_INVALID_USERNAME_INPUT "Invalid character in username"
|
||||
IDS_NFO_AUTO_CONNECT "Connecting automatically in %u seconds..."
|
||||
IDS_NFO_CLICK_HERE_TO_START "Right click here to start"
|
||||
|
||||
END
|
||||
|
|
Loading…
Reference in New Issue