diff --git a/selective-vpn-gui/traffic_mode_dialog.py b/selective-vpn-gui/traffic_mode_dialog.py index 77de813..40b7f79 100644 --- a/selective-vpn-gui/traffic_mode_dialog.py +++ b/selective-vpn-gui/traffic_mode_dialog.py @@ -940,14 +940,77 @@ RU: Применяет policy-rules и проверяет health. При оши if p.returncode != 0: raise RuntimeError(f"systemd-run failed: {p.returncode}\n{out}".strip()) - # EN: Some apps (e.g. Chrome wrappers) can return quickly; the transient scope - # EN: may appear/disappear fast. Retry briefly to avoid race. - # RU: Некоторые приложения (например, chrome-wrapper) быстро завершаются; scope - # RU: может появиться/исчезнуть очень быстро. Делаем небольшой retry. - cg = self._control_group_for_unit_retry(unit, timeout_sec=3.0) + # EN: Some apps can be migrated into a different app scope by the desktop/session + # EN: integration. Using unit ControlGroup is then incorrect. Prefer reading the + # EN: effective cgroup from the unit MainPID (/proc//cgroup) and fall back + # EN: to systemctl ControlGroup only if needed. + # RU: Некоторые приложения могут мигрировать в другой app scope (интеграция + # RU: с desktop/session). Тогда ControlGroup юнита неверен. Предпочитаем читать + # RU: реальный cgroup по MainPID (/proc//cgroup) и только потом fallback + # RU: на systemctl ControlGroup. + cg = self._effective_cgroup_for_unit_retry(unit, timeout_sec=3.0) return cg, out + def _effective_cgroup_for_unit_retry(self, unit: str, *, timeout_sec: float = 2.0) -> str: + u = (unit or "").strip() + if not u: + raise ValueError("empty unit") + + deadline = time.time() + max(0.2, float(timeout_sec or 0)) + last_pid_out = "" + while time.time() < deadline: + code, out = self._systemctl_user(["show", "-p", "MainPID", "--value", u]) + last_pid_out = out or "" + if code == 0: + try: + pid = int((out or "").strip() or "0") + except Exception: + pid = 0 + if pid > 0: + cg = self._cgroup_path_from_pid(pid) + if cg: + return cg + low = (out or "").lower() + if "could not be found" in low or "not found" in low: + break + time.sleep(0.1) + + # Fallback: unit ControlGroup (may be wrong for migrated apps). + try: + cg = self._control_group_for_unit_retry(u, timeout_sec=1.0) + if cg: + return cg + except Exception: + pass + + raise RuntimeError( + ( + "failed to query effective cgroup\n" + + (last_pid_out.strip() or "(no output)") + + "\n\n" + + "EN: Could not resolve unit MainPID->/proc//cgroup.\n" + + "RU: Не удалось получить MainPID и прочитать /proc//cgroup." + ).strip() + ) + + def _cgroup_path_from_pid(self, pid: int) -> str: + p = int(pid or 0) + if p <= 0: + return "" + try: + with open(f"/proc/{p}/cgroup", "r", encoding="utf-8", errors="replace") as f: + for raw in f: + line = (raw or "").strip() + if not line: + continue + if line.startswith("0::"): + cg = line[len("0::") :].strip() + return cg + except Exception: + return "" + return "" + def _control_group_for_unit_retry(self, unit: str, *, timeout_sec: float = 2.0) -> str: u = (unit or "").strip() if not u: @@ -1239,7 +1302,20 @@ RU: Применяет policy-rules и проверяет health. При оши cg = "" cg_id = 0 try: - cg = self._control_group_for_unit(unit) + # Prefer effective cgroup via MainPID (/proc//cgroup) for services. + # Some GUI apps can migrate into a different app scope after launch. + if unit.endswith(".service"): + code, out = self._systemctl_user(["show", "-p", "MainPID", "--value", unit]) + if code == 0: + try: + pid = int((out or "").strip() or "0") + except Exception: + pid = 0 + if pid > 0: + cg = self._cgroup_path_from_pid(pid) + + if not cg: + cg = self._control_group_for_unit(unit) cg_id = self._cgroup_inode_id(cg) except Exception: pass