diff --git a/selective-vpn-gui/traffic_mode_dialog.py b/selective-vpn-gui/traffic_mode_dialog.py index 5eb0a6b..77de813 100644 --- a/selective-vpn-gui/traffic_mode_dialog.py +++ b/selective-vpn-gui/traffic_mode_dialog.py @@ -195,7 +195,7 @@ RU: Восстанавливает маршруты/nft из последнег self.tabs.addTab(tab_basic, "Traffic basics") # ----------------------------------------------------------------- - # Apps (runtime): systemd --user scope + backend appmarks + # Apps (runtime): systemd --user unit + backend appmarks # ----------------------------------------------------------------- tab_apps = QWidget() @@ -203,15 +203,15 @@ RU: Восстанавливает маршруты/nft из последнег apps_hint = QLabel( "Runtime per-app routing (Wayland-friendly):\n" - "- Launch uses systemd-run --user --scope.\n" - "- Backend adds the scope cgroup into nftset -> fwmark rules.\n" + "- Launch uses systemd-run --user (transient unit).\n" + "- Backend adds the unit cgroup into nftset -> fwmark rules.\n" "- Marks are temporary (TTL). Use Policy overrides for persistent policy." ) apps_hint.setWordWrap(True) apps_hint.setStyleSheet("color: gray;") tab_apps_layout.addWidget(apps_hint) - run_group = QGroupBox("Run app in scope + apply mark") + run_group = QGroupBox("Run app (systemd unit) + apply mark") run_layout = QVBoxLayout(run_group) row_cmd = QHBoxLayout() @@ -221,8 +221,8 @@ RU: Восстанавливает маршруты/nft из последнег "e.g. firefox --private-window https://example.com" ) self.ed_app_cmd.setToolTip( - "EN: Command line to run. This runs as current user in a systemd --user scope.\n" - "RU: Команда запуска. Запускается от текущего пользователя в systemd --user scope." + "EN: Command line to run. This runs as current user via systemd --user.\n" + "RU: Команда запуска. Запускается от текущего пользователя через systemd --user." ) row_cmd.addWidget(self.ed_app_cmd, stretch=1) self.btn_app_pick = QPushButton("Pick app...") @@ -314,30 +314,30 @@ RU: Восстанавливает маршруты/nft из последнег tab_apps_layout.addWidget(run_group) - scopes_group = QGroupBox("Active svpn scopes (systemd --user)") + scopes_group = QGroupBox("Active svpn units (systemd --user)") scopes_layout = QVBoxLayout(scopes_group) scopes_row = QHBoxLayout() - self.btn_scopes_refresh = QPushButton("Refresh scopes") + self.btn_scopes_refresh = QPushButton("Refresh units") self.btn_scopes_refresh.setToolTip( - "EN: Refresh list of running svpn-* scopes.\n" - "RU: Обновить список запущенных svpn-* scope." + "EN: Refresh list of running svpn-* units (.service/.scope).\n" + "RU: Обновить список запущенных svpn-* unit (.service/.scope)." ) self.btn_scopes_refresh.clicked.connect(self.refresh_running_scopes) scopes_row.addWidget(self.btn_scopes_refresh) self.btn_scopes_stop_selected = QPushButton("Stop selected") self.btn_scopes_stop_selected.setToolTip( - "EN: Unmarks + stops selected scopes.\n" - "RU: Удаляет метки + останавливает выбранные scope." + "EN: Unmarks + stops selected units.\n" + "RU: Удаляет метки + останавливает выбранные unit." ) self.btn_scopes_stop_selected.clicked.connect(self.on_scopes_stop_selected) scopes_row.addWidget(self.btn_scopes_stop_selected) - self.btn_scopes_cleanup = QPushButton("Cleanup all svpn scopes") + self.btn_scopes_cleanup = QPushButton("Cleanup all svpn units") self.btn_scopes_cleanup.setToolTip( - "EN: Unmarks + stops ALL running svpn-* scopes.\n" - "RU: Удаляет метки + останавливает ВСЕ запущенные svpn-* scope." + "EN: Unmarks + stops ALL running svpn-* units.\n" + "RU: Удаляет метки + останавливает ВСЕ запущенные svpn-* unit." ) self.btn_scopes_cleanup.clicked.connect(self.on_scopes_cleanup_all) scopes_row.addWidget(self.btn_scopes_cleanup) @@ -348,14 +348,14 @@ RU: Восстанавливает маршруты/nft из последнег self.lst_scopes = QListWidget() self.lst_scopes.setSelectionMode(QAbstractItemView.ExtendedSelection) self.lst_scopes.setToolTip( - "EN: Running svpn scopes. Double click to copy unit name.\n" - "RU: Запущенные svpn scope. Двойной клик копирует имя unit." + "EN: Running svpn units. Double click to copy unit name.\n" + "RU: Запущенные svpn unit. Двойной клик копирует имя unit." ) self.lst_scopes.itemDoubleClicked.connect(lambda it: self._copy_scope_unit(it)) self.lst_scopes.setFixedHeight(140) scopes_layout.addWidget(self.lst_scopes) - self.lbl_scopes = QLabel("Running scopes: —") + self.lbl_scopes = QLabel("Running units: —") self.lbl_scopes.setStyleSheet("color: gray;") scopes_layout.addWidget(self.lbl_scopes) @@ -908,7 +908,7 @@ RU: Применяет policy-rules и проверяет health. При оши self._safe(work, title="App picker error") - def _run_systemd_scope(self, cmdline: str, *, unit: str) -> tuple[str, str]: + def _run_systemd_unit(self, cmdline: str, *, unit: str) -> tuple[str, str]: args = shlex.split(cmdline or "") if not args: raise ValueError("empty command") @@ -916,8 +916,6 @@ RU: Применяет policy-rules и проверяет health. При оши run_cmd = [ "systemd-run", "--user", - "--scope", - "--no-block", "--unit", unit, "--collect", @@ -946,7 +944,7 @@ RU: Применяет policy-rules и проверяет health. При оши # 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=2.0) + cg = self._control_group_for_unit_retry(unit, timeout_sec=3.0) return cg, out @@ -999,13 +997,13 @@ RU: Применяет policy-rules и проверяет health. При оши target = "vpn" if self.rad_app_vpn.isChecked() else "direct" ttl_sec = int(self.spn_app_ttl.value()) * 3600 - unit = f"svpn-{target}-{int(time.time())}.scope" + unit = f"svpn-{target}-{int(time.time())}.service" self._append_app_log( f"[app] launching: target={target} ttl={ttl_sec}s unit={unit}" ) - cg, out = self._run_systemd_scope(cmdline, unit=unit) + cg, out = self._run_systemd_unit(cmdline, unit=unit) if out: self._append_app_log(f"[app] systemd-run:\n{out}") self._append_app_log(f"[app] ControlGroup: {cg}") @@ -1182,11 +1180,12 @@ RU: Применяет policy-rules и проверяет health. При оши except subprocess.TimeoutExpired: return 124, f"timeout running: {' '.join(cmd)}" - def _list_running_svpn_scopes(self) -> list[str]: + def _list_running_svpn_units(self) -> list[str]: code, out = self._systemctl_user( [ "list-units", "--type=scope", + "--type=service", "--state=running", "--no-legend", "--no-pager", @@ -1205,7 +1204,7 @@ RU: Применяет policy-rules и проверяет health. При оши if not fields: continue unit = fields[0].strip() - if unit.startswith("svpn-") and unit.endswith(".scope"): + if unit.startswith("svpn-") and (unit.endswith(".scope") or unit.endswith(".service")): units.append(unit) units.sort() return units @@ -1232,7 +1231,7 @@ RU: Применяет policy-rules и проверяет health. При оши def refresh_running_scopes(self) -> None: def work() -> None: - units = self._list_running_svpn_scopes() + units = self._list_running_svpn_units() self.lst_scopes.clear() for unit in units: @@ -1262,17 +1261,17 @@ RU: Применяет policy-rules и проверяет health. При оши f"Target: {target or '-'}\n" f"ControlGroup: {cg or '-'}\n" f"cgroup_id: {cg_id or '-'}\n\n" - "EN: Stop selected will unmark by cgroup_id (if known) and stop the scope.\n" - "RU: Stop selected удалит метку по cgroup_id (если известен) и остановит scope." + "EN: Stop selected will unmark by cgroup_id (if known) and stop the unit.\n" + "RU: Stop selected удалит метку по cgroup_id (если известен) и остановит unit." ) self.lst_scopes.addItem(it) - self.lbl_scopes.setText(f"Running scopes: {len(units)}") + self.lbl_scopes.setText(f"Running units: {len(units)}") has_any = self.lst_scopes.count() > 0 self.btn_scopes_stop_selected.setEnabled(has_any) self.btn_scopes_cleanup.setEnabled(has_any) - self._safe(work, title="Scopes refresh error") + self._safe(work, title="Units refresh error") def _copy_scope_unit(self, it: QListWidgetItem) -> None: unit = "" @@ -1287,7 +1286,7 @@ RU: Применяет policy-rules и проверяет health. При оши QtGui.QGuiApplication.clipboard().setText(unit) except Exception: pass - self._append_app_log(f"[scope] copied unit: {unit}") + self._append_app_log(f"[unit] copied unit: {unit}") self._set_action_status(f"Copied unit: {unit}", ok=True) def _stop_scope_unit(self, unit: str) -> None: @@ -1328,47 +1327,47 @@ RU: Применяет policy-rules и проверяет health. При оши def work() -> None: infos = self._selected_scope_infos() if not infos: - QMessageBox.information(self, "No selection", "Select one or more scopes first.") + QMessageBox.information(self, "No selection", "Select one or more units first.") return if QMessageBox.question( self, "Stop selected", - f"Unmark + stop {len(infos)} selected scope(s)?", + f"Unmark + stop {len(infos)} selected unit(s)?", ) != QMessageBox.StandardButton.Yes: return for info in infos: self._append_app_log( - f"[scope] stop: unit={info.unit} target={info.target} cgroup_id={info.cgroup_id}" + f"[unit] stop: unit={info.unit} target={info.target} cgroup_id={info.cgroup_id}" ) try: self._unmark_scope(info) - self._append_app_log("[scope] unmark OK") + self._append_app_log("[unit] unmark OK") except Exception as e: - self._append_app_log(f"[scope] unmark WARN: {e}") + self._append_app_log(f"[unit] unmark WARN: {e}") self._stop_scope_unit(info.unit) - self._append_app_log("[scope] stop OK") + self._append_app_log("[unit] stop OK") self.refresh_running_scopes() self.refresh_appmarks_counts() - self._set_action_status(f"Stopped scopes: {len(infos)}", ok=True) + self._set_action_status(f"Stopped units: {len(infos)}", ok=True) - self._safe(work, title="Stop selected scopes error") + self._safe(work, title="Stop selected units error") def on_scopes_cleanup_all(self) -> None: def work() -> None: - units = self._list_running_svpn_scopes() + units = self._list_running_svpn_units() if not units: - self._set_action_status("No running svpn scopes", ok=True) + self._set_action_status("No running svpn units", ok=True) self.refresh_running_scopes() return if QMessageBox.question( self, "Cleanup all", - f"Unmark + stop ALL running svpn scopes ({len(units)})?", + f"Unmark + stop ALL running svpn units ({len(units)})?", ) != QMessageBox.StandardButton.Yes: return @@ -1389,24 +1388,24 @@ RU: Применяет policy-rules и проверяет health. При оши cgroup_id=int(cg_id or 0), ) self._append_app_log( - f"[scope] cleanup: unit={info.unit} target={info.target} cgroup_id={info.cgroup_id}" + f"[unit] cleanup: unit={info.unit} target={info.target} cgroup_id={info.cgroup_id}" ) try: self._unmark_scope(info) - self._append_app_log("[scope] unmark OK") + self._append_app_log("[unit] unmark OK") except Exception as e: - self._append_app_log(f"[scope] unmark WARN: {e}") + self._append_app_log(f"[unit] unmark WARN: {e}") try: self._stop_scope_unit(info.unit) stopped += 1 except Exception as e: - self._append_app_log(f"[scope] stop ERROR: {e}") + self._append_app_log(f"[unit] stop ERROR: {e}") self.refresh_running_scopes() self.refresh_appmarks_counts() self._set_action_status(f"Cleanup done: stopped={stopped}/{len(units)}", ok=True) - self._safe(work, title="Cleanup scopes error") + self._safe(work, title="Cleanup units error") def on_rollback(self) -> None: def work() -> None: