diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-02-18 11:15:55 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-03-02 17:26:55 +0100 |
| commit | 8f48eb4bc95a1439644936214c849573234fe7a7 (patch) | |
| tree | 2ba685f3d459dd0bd84bfd1e468724df9d22935b /gui/src | |
| parent | 5ebf5525627ce60ec531ba8070fadf0c7cb08449 (diff) | |
| download | mullvadvpn-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.ts | 74 | ||||
| -rw-r--r-- | gui/src/renderer/app.tsx | 4 | ||||
| -rw-r--r-- | gui/src/renderer/components/LinuxSplitTunnelingSettings.tsx | 33 | ||||
| -rw-r--r-- | gui/src/shared/ipc-schema.ts | 4 |
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>(), |
