Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions python_rpc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
# This can be streamed down from Node
downloading_game_id = -1
current_download_limit = None
current_network_interface = None

torrent_session = lt.session(
{"listen_interfaces": "0.0.0.0:{port}".format(port=torrent_port)}
Expand Down Expand Up @@ -247,6 +248,39 @@
if callable(set_download_limit):
set_download_limit(current_download_limit)


def normalize_network_interface(value):
if not isinstance(value, str):
return None

trimmed = value.strip()
return trimmed or None


def apply_network_interface(interface):
if interface:
listen_interfaces = "{iface}:{port}".format(
iface=interface, port=torrent_port
)
outgoing_interfaces = interface
else:
listen_interfaces = "0.0.0.0:{port}".format(port=torrent_port)
outgoing_interfaces = ""

try:
torrent_session.apply_settings(
{
"listen_interfaces": listen_interfaces,
"outgoing_interfaces": outgoing_interfaces,
}
)
logger.info(
"Bound torrent client to network interface: %s",
interface or "default (all adapters)",
)
except Exception as error:
logger.error("Failed to bind torrent client to interface: %s", error)

Check failure on line 282 in python_rpc/main.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "logging.exception()" instead.

See more on https://sonarcloud.io/project/issues?id=hydralauncher_hydra&issues=AZ76zklD4L5Jot4fMcnU&open=AZ76zklD4L5Jot4fMcnU&pullRequest=2405

def validate_rpc_password_value(password: Optional[str]):
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
if rpc_password == "":
return True
Expand Down Expand Up @@ -429,6 +463,7 @@
def action(data: Optional[dict] = None):
global downloading_game_id
global current_download_limit
global current_network_interface
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

data = data or {}
action_name = data.get("action")
Expand Down Expand Up @@ -511,6 +546,11 @@

for downloader in active_downloaders:
apply_download_limit(downloader)
elif action_name == "set_network_interface":
current_network_interface = normalize_network_interface(
data.get("interface")
)
apply_network_interface(current_network_interface)
else:
raise RpcError("invalid_action")
except RpcError:
Expand Down
4 changes: 4 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,10 @@
"app_basics": "App basics",
"startup_behavior": "Startup behavior",
"download_behavior": "Download behavior",
"network_interface": "Network adapter for torrents",
"network_interface_default": "Default (all adapters)",
"network_interface_hint": "Bind the torrent client to a specific adapter, such as a VPN. While bound, torrents only connect through that adapter, and downloads stop if it goes down (no traffic leaks through your regular connection).",
"network_interface_unavailable": "unavailable",
"library_notifications": "Library notifications",
"achievement_notifications": "Achievement notifications",
"content_gameplay": "Content & gameplay",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,10 @@
"app_basics": "Básicos de la aplicación",
"startup_behavior": "Comportamiento al iniciar",
"download_behavior": "Comportamiento de descarga",
"network_interface": "Adaptador de red para torrents",
"network_interface_default": "Predeterminado (todos los adaptadores)",
"network_interface_hint": "Vincula el cliente de torrents a un adaptador específico, como una VPN. Mientras está vinculado, los torrents solo se conectan a través de ese adaptador y las descargas se detienen si deja de funcionar (sin fugas de tráfico por tu conexión habitual).",
"network_interface_unavailable": "no disponible",
"library_notifications": "Notificaciones de la librería",
"achievement_notifications": "Notificaciones de logros",
"content_preferences": "Preferencias de contenido",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/pt-BR/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,10 @@
"app_basics": "Noções básicas do aplicativo",
"startup_behavior": "Comportamento na inicialização",
"download_behavior": "Comportamento de download",
"network_interface": "Adaptador de rede para torrents",
"network_interface_default": "Padrão (todos os adaptadores)",
"network_interface_hint": "Vincula o cliente de torrent a um adaptador específico, como uma VPN. Enquanto vinculado, os torrents só se conectam por esse adaptador e os downloads param se ele cair (sem vazamento de tráfego pela sua conexão normal).",
"network_interface_unavailable": "indisponível",
"library_notifications": "Notificações da biblioteca",
"achievement_notifications": "Notificações de conquistas",
"content_preferences": "Preferências de conteúdo",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/pt-PT/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,10 @@
"app_basics": "Noções básicas da aplicação",
"startup_behavior": "Comportamento no arranque",
"download_behavior": "Comportamento de transferência",
"network_interface": "Adaptador de rede para torrents",
"network_interface_default": "Predefinido (todos os adaptadores)",
"network_interface_hint": "Vincula o cliente de torrent a um adaptador específico, como uma VPN. Enquanto vinculado, os torrents só se ligam através desse adaptador e as transferências param se ele ficar indisponível (sem fugas de tráfego pela sua ligação normal).",
"network_interface_unavailable": "indisponível",
"library_notifications": "Notificações da biblioteca",
"achievement_notifications": "Notificações de conquistas",
"content_preferences": "Preferências de conteúdo",
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ru/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,10 @@
"app_basics": "Основные настройки приложения",
"startup_behavior": "Поведение при запуске",
"download_behavior": "Поведение загрузок",
"network_interface": "Сетевой адаптер для торрентов",
"network_interface_default": "По умолчанию (все адаптеры)",
"network_interface_hint": "Привязывает торрент-клиент к выбранному адаптеру, например к VPN. Пока привязка активна, торренты подключаются только через этот адаптер, а загрузки останавливаются, если он отключится (без утечки трафика через обычное соединение).",
"network_interface_unavailable": "недоступен",
"library_notifications": "Уведомления библиотеки",
"achievement_notifications": "Уведомления о достижениях",
"content_gameplay": "Контент и игровой процесс",
Expand Down
33 changes: 33 additions & 0 deletions src/main/events/hardware/get-network-interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import os from "node:os";

import type { NetworkInterface } from "@types";
import { registerEvent } from "../register-event";

const getNetworkInterfaces = async (): Promise<NetworkInterface[]> => {
const interfaces = os.networkInterfaces();

return Object.entries(interfaces).reduce<NetworkInterface[]>(
(acc, [name, addresses]) => {
const external = (addresses ?? []).filter((address) => !address.internal);

if (external.length === 0) {
return acc;
}

const sorted = external.sort((a, b) => {
if (a.family === b.family) return 0;
return a.family === "IPv4" ? -1 : 1;
});

acc.push({
name,
addresses: sorted.map((address) => address.address),
});

return acc;
},
[]
);
};

registerEvent("getNetworkInterfaces", getNetworkInterfaces);
1 change: 1 addition & 0 deletions src/main/events/hardware/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
import "./check-folder-write-permission";
import "./get-disk-free-space";
import "./get-network-interfaces";
6 changes: 6 additions & 0 deletions src/main/events/user-preferences/update-user-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ const updateUserPreferences = async (
preferences.maxDownloadSpeedBytesPerSecond ?? null
);
}

if (Object.hasOwn(preferences, "torrentNetworkInterface")) {
await DownloadManager.applyNetworkInterface(
preferences.torrentNetworkInterface ?? null
);
}
};

registerEvent("updateUserPreferences", updateUserPreferences);
30 changes: 30 additions & 0 deletions src/main/services/download/download-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,42 @@
});
}

private static async getPersistedNetworkInterface() {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{ valueEncoding: "json" }
);

return userPreferences?.torrentNetworkInterface ?? null;
}

public static async applyNetworkInterface(
value?: string | null
): Promise<void> {
const networkInterface =
value === undefined ? await this.getPersistedNetworkInterface() : value;

Check warning on line 282 in src/main/services/download/download-manager.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer using nullish coalescing operator (`??`) instead of a ternary expression, as it is simpler to read.

See more on https://sonarcloud.io/project/issues?id=hydralauncher_hydra&issues=AZ76zkkA4L5Jot4fMcnT&open=AZ76zkkA4L5Jot4fMcnT&pullRequest=2405

await PythonRPC.rpc
.call("action", {
action: "set_network_interface",
interface: networkInterface ?? "",
})
.catch((error) => {
logger.error(
"[DownloadManager] Failed to update RPC network interface:",
error
);
});
}

public static async startRPC(
download?: Download,
downloadsToSeed?: Download[]
) {
await PythonRPC.spawn();

await this.applyNetworkInterface();

if (downloadsToSeed?.length) {
for (const seedDownload of downloadsToSeed) {
await this.resumeSeeding(seedDownload).catch((error) => {
Expand Down
1 change: 1 addition & 0 deletions src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getDiskFreeSpace", path),
checkFolderWritePermission: (path: string) =>
ipcRenderer.invoke("checkFolderWritePermission", path),
getNetworkInterfaces: () => ipcRenderer.invoke("getNetworkInterfaces"),

/* Cloud save */
uploadSaveGame: (
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/declaration.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
AchievementNotificationInfo,
Game,
DiskUsage,
NetworkInterface,
DownloadSource,
LocalNotification,
ProtonVersion,
Expand Down Expand Up @@ -605,6 +606,7 @@ declare global {
/* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskUsage>;
checkFolderWritePermission: (path: string) => Promise<boolean>;
getNetworkInterfaces: () => Promise<NetworkInterface[]>;

/* Cloud save */
uploadSaveGame: (
Expand Down
62 changes: 60 additions & 2 deletions src/renderer/src/pages/settings/settings-context-downloads.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";

import { CheckboxField, TextField } from "@renderer/components";
import { CheckboxField, SelectField, TextField } from "@renderer/components";
import { settingsContext } from "@renderer/context";
import { useAppSelector } from "@renderer/hooks";
import type { NetworkInterface } from "@types";
import { SettingsDownloadSources } from "./settings-download-sources";

import "./settings-general.scss";
Expand Down Expand Up @@ -50,8 +51,20 @@
createStartMenuShortcut: true,
maxDownloadSpeedMegabytes: "",
deleteArchiveFilesAfterExtractionByDefault: false,
torrentNetworkInterface: "",
});

const [networkInterfaces, setNetworkInterfaces] = useState<
NetworkInterface[]
>([]);

useEffect(() => {
window.electron

Check warning on line 62 in src/renderer/src/pages/settings/settings-context-downloads.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=hydralauncher_hydra&issues=AZ76zkfD4L5Jot4fMcnS&open=AZ76zkfD4L5Jot4fMcnS&pullRequest=2405
.getNetworkInterfaces()
.then(setNetworkInterfaces)
.catch(() => setNetworkInterfaces([]));
}, []);

useEffect(() => {
if (!userPreferences) return;

Expand All @@ -72,9 +85,40 @@
: "",
deleteArchiveFilesAfterExtractionByDefault:
userPreferences.deleteArchiveFilesAfterExtractionByDefault ?? false,
torrentNetworkInterface: userPreferences.torrentNetworkInterface ?? "",
});
}, [userPreferences]);

const networkInterfaceOptions = useMemo(() => {
const options = [
{ key: "default", value: "", label: t("network_interface_default") },
...networkInterfaces.map((networkInterface) => {
const ipv4 = networkInterface.addresses.find(
(address) => !address.includes(":")
);

return {
key: networkInterface.name,
value: networkInterface.name,
label: ipv4
? `${networkInterface.name} (${ipv4})`
: networkInterface.name,
};
}),
];

const selected = form.torrentNetworkInterface;
if (selected && !options.some((option) => option.value === selected)) {
options.push({
key: selected,
value: selected,
label: `${selected} (${t("network_interface_unavailable")})`,
});
}

return options;
}, [networkInterfaces, form.torrentNetworkInterface, t]);

const handleChange = (values: Partial<typeof form>) => {
setForm((prev) => ({ ...prev, ...values }));
updateUserPreferences(values);
Expand Down Expand Up @@ -159,6 +203,20 @@
placeholder={t("max_download_speed_unlimited")}
/>

<div className="settings-general__network-interface">
<SelectField
label={t("network_interface")}
value={form.torrentNetworkInterface}
onChange={(event) =>
handleChange({ torrentNetworkInterface: event.target.value })
}
options={networkInterfaceOptions}
/>
<small className="settings-general__network-interface-hint">
{t("network_interface_hint")}
</small>
</div>

<CheckboxField
label={t("seed_after_download_complete")}
checked={form.seedAfterDownloadComplete}
Expand Down
11 changes: 11 additions & 0 deletions src/renderer/src/pages/settings/settings-general.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@
font-style: italic;
}

&__network-interface {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
}

&__network-interface-hint {
font-size: 13px;
color: globals.$muted-color;
}
Comment thread
Moyasee marked this conversation as resolved.

&__volume-control {
display: flex;
flex-direction: column;
Expand Down
6 changes: 6 additions & 0 deletions src/types/level.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,19 @@ export interface UserPreferences {
bigPictureDiagnosticsEnabled?: boolean;
bigPictureDiagnosticsPosition?: BigPictureDiagnosticsPosition;
maxDownloadSpeedBytesPerSecond?: number | null;
torrentNetworkInterface?: string | null;
defaultProtonPath?: string | null;
autoRunMangohud?: boolean;
autoRunGamemode?: boolean;
hideClassicsBookmark?: boolean;
classicsUseHeroLayout?: boolean;
}

export interface NetworkInterface {
name: string;
addresses: string[];
}

export interface ScreenState {
x?: number;
y?: number;
Expand Down
Loading