1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
import argvSplit from 'argv-split';
import child_process from 'child_process';
import linuxAppList, { AppData } from 'linux-app-list';
import path from 'path';
import { pascalCaseToCamelCase } from './transform-object-keys';
import { ILinuxSplitTunnelingApplication } from '../shared/application-types';
import { findIconPath, getImageDataUrl, shouldShowApplication } from './linux-desktop-entry';
const PROBLEMATIC_APPLICATIONS = {
launchingInExistingProcess: [
'brave-browser-stable',
'chromium-browser',
'firefox',
'firefox-esr',
'google-chrome-stable',
'mate-terminal',
'opera',
'xfce4-terminal',
],
launchingElsewhere: ['gnome-terminal'],
};
export function launchApplication(app: ILinuxSplitTunnelingApplication | string) {
const excludeArguments = typeof app === 'string' ? [app] : formatExec(app.exec);
child_process.spawn('mullvad-exclude', excludeArguments, { detached: true });
}
// Removes placeholder arguments and separates command into list of strings
function formatExec(exec: string) {
return argvSplit(exec).filter((argument: string) => !/%[cdDfFikmnNuUv]/.test(argument));
}
// TODO: Switch to asyncronous reading of .desktop files
export function getApplications(locale: string): Promise<ILinuxSplitTunnelingApplication[]> {
const appList = linuxAppList();
const applications = appList
.list()
.map((filename) => {
const applications = localizeNameAndIcon(appList.data(filename), locale);
return pascalCaseToCamelCase<ILinuxSplitTunnelingApplication>(applications);
})
.filter(shouldShowApplication)
.map(addApplicationWarnings)
.sort((a, b) => a.name.localeCompare(b.name))
.map(replaceIconNameWithDataUrl);
return Promise.all(applications);
}
async function replaceIconNameWithDataUrl(
app: ILinuxSplitTunnelingApplication,
): Promise<ILinuxSplitTunnelingApplication> {
try {
// Either the app has no icon or it's already an absolute path.
if (app.icon === undefined || path.isAbsolute(app.icon)) {
return app;
}
const iconPath = await findIconPath(app.icon);
if (iconPath === undefined) {
return app;
}
return { ...app, icon: await getImageDataUrl(iconPath) };
} catch (e) {
return app;
}
}
function addApplicationWarnings(
application: ILinuxSplitTunnelingApplication,
): ILinuxSplitTunnelingApplication {
const binaryBasename = path.basename(application.exec!.split(' ')[0]);
if (PROBLEMATIC_APPLICATIONS.launchingInExistingProcess.includes(binaryBasename)) {
return {
...application,
warning: 'launches-in-existing-process',
};
} else if (PROBLEMATIC_APPLICATIONS.launchingElsewhere.includes(binaryBasename)) {
return {
...application,
warning: 'launches-elsewhere',
};
} else {
return application;
}
}
function localizeNameAndIcon(application: AppData, locale: string) {
if (application.lang) {
// linux-app-list prefixes and suffixes the locale keys with a space for some reason.
const lang = Object.fromEntries(
Object.entries(application.lang).map(([locale, value]) => [locale.trim(), value]),
)[locale];
application.Name = lang?.Name ?? application.Name;
application.Icon = lang?.Icon ?? application.Icon;
}
return application;
}
|