summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-02-18 11:15:55 +0100
committerOskar Nyberg <oskar@mullvad.net>2021-03-02 17:26:55 +0100
commit8f48eb4bc95a1439644936214c849573234fe7a7 (patch)
tree2ba685f3d459dd0bd84bfd1e468724df9d22935b /gui/src
parent5ebf5525627ce60ec531ba8070fadf0c7cb08449 (diff)
downloadmullvadvpn-8f48eb4bc95a1439644936214c849573234fe7a7.tar.xz
mullvadvpn-8f48eb4bc95a1439644936214c849573234fe7a7.zip
Add error handling when split tunnel app can't be launched
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/linux-split-tunneling.ts74
-rw-r--r--gui/src/renderer/app.tsx4
-rw-r--r--gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx33
-rw-r--r--gui/src/shared/ipc-schema.ts4
4 files changed, 91 insertions, 24 deletions
diff --git a/gui/src/main/linux-split-tunneling.ts b/gui/src/main/linux-split-tunneling.ts
index acaa674746..9c41740776 100644
--- a/gui/src/main/linux-split-tunneling.ts
+++ b/gui/src/main/linux-split-tunneling.ts
@@ -2,6 +2,9 @@ import argvSplit from 'argv-split';
import child_process from 'child_process';
import path from 'path';
import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
+import { messages } from '../shared/gettext';
+import { LaunchApplicationResult } from '../shared/ipc-schema';
+import { Scheduler } from '../shared/scheduler';
import {
getDesktopEntries,
readDesktopEntry,
@@ -25,25 +28,74 @@ const PROBLEMATIC_APPLICATIONS = {
launchingElsewhere: ['gnome-terminal'],
};
+// Launches an application. The application parameter could be a path the an executable or .desktop
+// file or an object representing an application
export async function launchApplication(
app: ILinuxSplitTunnelingApplication | string,
-): Promise<void> {
- let excludeArguments: string[] | undefined;
+): Promise<LaunchApplicationResult> {
+ let excludeArguments: string[];
+ try {
+ excludeArguments = await getLaunchCommand(app);
+ } catch (e) {
+ return { error: e.message };
+ }
+
+ return new Promise((resolve, _reject) => {
+ const scheduler = new Scheduler();
+ const proc = child_process.spawn('mullvad-exclude', excludeArguments, { detached: true });
+
+ // If the process exits within 200 milliseconds the user is notified that it failed to launch.
+ scheduler.schedule(() => {
+ proc.removeAllListeners();
+ resolve({ success: true });
+ }, 200);
+
+ proc.stderr.on('data', (data) => {
+ if (data.includes('Failed to launch the process') && data.includes('ENOENT')) {
+ scheduler.cancel();
+ proc.removeAllListeners();
+ resolve({
+ error:
+ // TRANSLATORS: This error message is shown if the user tries to launch an app that
+ // TRANSLATORS: doesn't exist.
+ messages.pgettext('split-tunneling-view', 'Please try again or contact support.'),
+ });
+ }
+ });
+ proc.once('exit', (code) => {
+ scheduler.cancel();
+ proc.removeAllListeners();
+
+ if (code === 1) {
+ resolve({
+ error:
+ // TRANSLATORS: This error message is shown if an application failes during startup.
+ messages.pgettext('split-tunneling-view', 'Please try again or contact support.'),
+ });
+ } else {
+ resolve({ success: true });
+ }
+ });
+ });
+}
+
+// Takes the same argument as launchApplication and returns the command to run
+async function getLaunchCommand(app: ILinuxSplitTunnelingApplication | string): Promise<string[]> {
if (typeof app === 'object') {
- excludeArguments = formatExec(app.exec);
+ return formatExec(app.exec);
} else if (path.extname(app) === '.desktop') {
const entry = await readDesktopEntry(app);
if (entry.exec !== undefined) {
- excludeArguments = formatExec(entry.exec);
+ return formatExec(entry.exec);
+ } else {
+ throw new Error(
+ // TRANSLATORS: This error message is shown if the user tries to launch a Linux desktop
+ // TRANSLATORS: entry file that doesn't contain the required 'Exec' value.
+ messages.pgettext('split-tunneling-view', 'Please contact support.'),
+ );
}
} else {
- excludeArguments = [app];
- }
-
- if (excludeArguments !== undefined && excludeArguments.length > 0) {
- child_process.spawn('mullvad-exclude', excludeArguments, { detached: true });
- } else {
- throw new Error('Invalid application');
+ return [app];
}
}
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 739ac48ffe..4c79cc2192 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -20,7 +20,7 @@ import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
import { messages, relayLocations } from '../shared/gettext';
import { IGuiSettingsState, SYSTEM_PREFERRED_LOCALE_KEY } from '../shared/gui-settings-state';
import log, { ConsoleOutput } from '../shared/logging';
-import { IRelayListPair } from '../shared/ipc-schema';
+import { IRelayListPair, LaunchApplicationResult } from '../shared/ipc-schema';
import consumePromise from '../shared/promise';
import History from './lib/history';
import { loadTranslations } from './lib/load-translations';
@@ -421,7 +421,7 @@ export default class AppRenderer {
public launchExcludedApplication(
application: ILinuxSplitTunnelingApplication | string,
- ): Promise<void> {
+ ): Promise<LaunchApplicationResult> {
return IpcRendererEventChannel.splitTunneling.launchApplication(application);
}
diff --git a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
index 6a87a73a5a..cb7c497011 100644
--- a/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
+++ b/gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx
@@ -107,10 +107,20 @@ export default function LinuxSplitTunnelingSettings() {
const [applications, setApplications] = useState<ILinuxSplitTunnelingApplication[]>();
const [applicationListHeight, setApplicationListHeight] = useState<number>();
const [browsing, setBrowsing] = useState(false);
- const [showBrowseFailureDialog, setShowBrowseFailureDialog] = useState(false);
+ const [browseError, setBrowseError] = useState<string>();
const applicationListRef = useRef() as React.RefObject<HTMLDivElement>;
+ const launchApplication = useCallback(
+ async (application: ILinuxSplitTunnelingApplication | string) => {
+ const result = await launchExcludedApplication(application);
+ if ('error' in result) {
+ setBrowseError(result.error);
+ }
+ },
+ [],
+ );
+
const launchWithFilePicker = useCallback(async () => {
setBrowsing(true);
const file = await showOpenDialog({
@@ -120,15 +130,11 @@ export default function LinuxSplitTunnelingSettings() {
setBrowsing(false);
if (file.filePaths[0]) {
- try {
- await launchExcludedApplication(file.filePaths[0]);
- } catch (e) {
- setShowBrowseFailureDialog(true);
- }
+ await launchApplication(file.filePaths[0]);
}
}, []);
- const hideBrowseFailureDialog = useCallback(() => setShowBrowseFailureDialog(false), []);
+ const hideBrowseFailureDialog = useCallback(() => setBrowseError(undefined), []);
useEffect(() => {
consumePromise(getSplitTunnelingApplications().then(setApplications));
@@ -188,7 +194,7 @@ export default function LinuxSplitTunnelingSettings() {
<ApplicationRow
key={application.absolutepath}
application={application}
- launchApplication={launchExcludedApplication}
+ launchApplication={launchApplication}
/>
))
)}
@@ -203,11 +209,18 @@ export default function LinuxSplitTunnelingSettings() {
</NavigationContainer>
</StyledContainer>
</Layout>
- {showBrowseFailureDialog && (
+ {browseError && (
<ModalAlert
type={ModalAlertType.warning}
iconColor={colors.red}
- message={messages.pgettext('split-tunneling-view', 'Failed to launch application')}
+ message={sprintf(
+ // TRANSLATORS: Error message showed in a dialog when an application failes to launch.
+ messages.pgettext(
+ 'split-tunneling-view',
+ 'Unable to launch selection. %(detailedErrorMessage)s',
+ ),
+ { detailedErrorMessage: browseError },
+ )}
buttons={[
<AppButton.BlueButton key="close" onClick={hideBrowseFailureDialog}>
{messages.gettext('Close')}
diff --git a/gui/src/shared/ipc-schema.ts b/gui/src/shared/ipc-schema.ts
index a31745c01f..62a3afc948 100644
--- a/gui/src/shared/ipc-schema.ts
+++ b/gui/src/shared/ipc-schema.ts
@@ -37,6 +37,8 @@ export interface IRelayListPair {
bridges: IRelayList;
}
+export type LaunchApplicationResult = { success: true } | { error: string };
+
export interface IAppStateSnapshot {
locale: string;
isConnected: boolean;
@@ -181,7 +183,7 @@ export const ipcSchema = {
},
splitTunneling: {
getApplications: invoke<void, ILinuxSplitTunnelingApplication[]>(),
- launchApplication: invoke<ILinuxSplitTunnelingApplication | string, void>(),
+ launchApplication: invoke<ILinuxSplitTunnelingApplication | string, LaunchApplicationResult>(),
},
problemReport: {
collectLogs: invoke<string[], string>(),