From 0f2103077443faeebc2c879bed5faf0337704805 Mon Sep 17 00:00:00 2001 From: Selva Nair Date: Sat, 27 Feb 2016 13:10:49 -0500 Subject: [PATCH] Support sending commands to running instance - New option --command 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 --- main.c | 156 +++++++++++++++++++++++++++++++++++--- main.h | 15 ++++ manage.h | 2 - misc.c | 4 +- misc.h | 2 +- openvpn-gui-res.h | 1 + openvpn.c | 31 +++++--- options.c | 62 ++++++++++++++- options.h | 3 + res/openvpn-gui-res-en.rc | 2 + tray.h | 2 - 11 files changed, 251 insertions(+), 29 deletions(-) diff --git a/main.c b/main.c index ca4a6d7..b912934 100644 --- a/main.c +++ b/main.c @@ -55,6 +55,10 @@ #include #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]); diff --git a/main.h b/main.h index 1b2f61e..ba17e45 100644 --- a/main.h +++ b/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 diff --git a/manage.h b/manage.h index 1b89f41..94632bd 100644 --- a/manage.h +++ b/manage.h @@ -24,8 +24,6 @@ #include -#define WM_MANAGEMENT (WM_APP + 2) - typedef enum { ready, stop, diff --git a/misc.c b/misc.c index fb1e5d9..ec36fad 100644 --- a/misc.c +++ b/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); diff --git a/misc.h b/misc.h index f7f43cc..0480eeb 100644 --- a/misc.h +++ b/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); diff --git a/openvpn-gui-res.h b/openvpn-gui-res.h index 34c8647..cce2457 100644 --- a/openvpn-gui-res.h +++ b/openvpn-gui-res.h @@ -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 diff --git a/openvpn.c b/openvpn.c index eb0e0a2..e6155fe 100644 --- a/openvpn.c +++ b/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 diff --git a/options.c b/options.c index cf3cf15..9928c29 100644 --- a/options.c +++ b/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) diff --git a/options.h b/options.h index 5cae976..85468be 100644 --- a/options.h +++ b/options.h @@ -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); diff --git a/res/openvpn-gui-res-en.rc b/res/openvpn-gui-res-en.rc index 5c80fa3..9becd22 100644 --- a/res/openvpn-gui-res-en.rc +++ b/res/openvpn-gui-res-en.rc @@ -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 diff --git a/tray.h b/tray.h index c8dd3df..1ce5e07 100644 --- a/tray.h +++ b/tray.h @@ -25,8 +25,6 @@ #include "options.h" -#define WM_NOTIFYICONTRAY (WM_APP + 1) - #define IDM_SERVICE_START 100 #define IDM_SERVICE_STOP 101 #define IDM_SERVICE_RESTART 102