ui: run selected app profile (systemd-run + appmark refresh)

This commit is contained in:
beckline
2026-02-15 22:08:03 +03:00
parent f14dd7bc89
commit 3bb0f11ec5

View File

@@ -267,6 +267,14 @@ RU: Восстанавливает маршруты/nft из последнег
self.btn_app_profile_load.clicked.connect(self.on_app_profile_load) self.btn_app_profile_load.clicked.connect(self.on_app_profile_load)
row_prof_btn.addWidget(self.btn_app_profile_load) row_prof_btn.addWidget(self.btn_app_profile_load)
self.btn_app_profile_run = QPushButton("Run profile")
self.btn_app_profile_run.setToolTip(
"EN: Launch selected profile via systemd-run --user and apply routing mark.\n"
"RU: Запустить выбранный профиль через systemd-run --user и применить метку маршрутизации."
)
self.btn_app_profile_run.clicked.connect(self.on_app_profile_run)
row_prof_btn.addWidget(self.btn_app_profile_run)
self.btn_app_profile_delete = QPushButton("Delete profile") self.btn_app_profile_delete = QPushButton("Delete profile")
self.btn_app_profile_delete.setToolTip( self.btn_app_profile_delete.setToolTip(
"EN: Delete selected saved profile.\n" "EN: Delete selected saved profile.\n"
@@ -1025,6 +1033,105 @@ RU: Применяет policy-rules и проверяет health. При оши
# Fallback: first token # Fallback: first token
return (cmd.split() or [""])[0].strip() return (cmd.split() or [""])[0].strip()
def _launch_and_mark(
self,
*,
cmdline: str,
target: str,
ttl_sec: int,
app_key: str = "",
) -> None:
cmdline = (cmdline or "").strip()
if not cmdline:
raise ValueError("empty command")
tgt = (target or "").strip().lower()
if tgt not in ("vpn", "direct"):
raise ValueError("invalid target")
ttl = int(ttl_sec or 0)
if ttl <= 0:
ttl = int(self.spn_app_ttl.value()) * 3600
key = (app_key or "").strip() or self._infer_app_key_from_cmdline(cmdline)
# EN: If we already have a running unit for the same app_key+target, refresh mark instead of spawning.
# RU: Если уже есть запущенный unit для того же app_key+target — обновляем метку, не плодим инстансы.
try:
items = list(self.ctrl.traffic_appmarks_items() or [])
except Exception:
items = []
for it in items:
if (getattr(it, "target", "") or "").strip().lower() != tgt:
continue
if (getattr(it, "app_key", "") or "").strip() != key:
continue
unit = (getattr(it, "unit", "") or "").strip()
if not unit:
continue
code, out = self._systemctl_user(["is-active", unit])
if code == 0 and (out or "").strip().lower() == "active":
cg = self._effective_cgroup_for_unit_retry(unit, timeout_sec=3.0)
self._append_app_log(
f"[profile] already running: app={key} target={tgt} unit={unit} (refreshing mark)"
)
res = self.ctrl.traffic_appmarks_apply(
op="add",
target=tgt,
cgroup=cg,
unit=unit,
command=cmdline,
app_key=key,
timeout_sec=ttl,
)
if not res.ok:
raise RuntimeError(res.message or "appmark refresh failed")
self._set_action_status(
f"App mark refreshed: target={tgt} cgroup_id={res.cgroup_id}",
ok=True,
)
self._set_last_scope(
unit=unit,
target=tgt,
app_key=key,
cmdline=cmdline,
cgroup_id=int(res.cgroup_id or 0),
)
self.refresh_appmarks_items(quiet=True)
self.refresh_appmarks_counts()
self.refresh_running_scopes(quiet=True)
return
unit = f"svpn-{tgt}-{int(time.time())}.service"
self._append_app_log(f"[profile] launching: app={key or '-'} target={tgt} ttl={ttl}s unit={unit}")
cg, out = self._run_systemd_unit(cmdline, unit=unit)
if out:
self._append_app_log(f"[profile] systemd-run:\n{out}")
self._append_app_log(f"[profile] ControlGroup: {cg}")
self._set_last_scope(unit=unit, target=tgt, app_key=key, cmdline=cmdline, cgroup_id=0)
res = self.ctrl.traffic_appmarks_apply(
op="add",
target=tgt,
cgroup=cg,
unit=unit,
command=cmdline,
app_key=key,
timeout_sec=ttl,
)
if not res.ok:
raise RuntimeError(res.message or "appmark apply failed")
self._append_app_log(f"[appmarks] OK: {res.message} cgroup_id={res.cgroup_id} timeout={res.timeout_sec}s")
self._set_action_status(f"App mark added: target={tgt} cgroup_id={res.cgroup_id}", ok=True)
self._set_last_scope(
unit=unit,
target=tgt,
app_key=key,
cmdline=cmdline,
cgroup_id=int(res.cgroup_id or 0),
)
self.refresh_appmarks_items(quiet=True)
self.refresh_appmarks_counts()
self.refresh_running_scopes(quiet=True)
def _selected_app_profile(self): def _selected_app_profile(self):
it = self.lst_app_profiles.currentItem() it = self.lst_app_profiles.currentItem()
if not it: if not it:
@@ -1142,6 +1249,25 @@ RU: Применяет policy-rules и проверяет health. При оши
self._safe(work, title="Load profile error") self._safe(work, title="Load profile error")
def on_app_profile_run(self) -> None:
prof = self._selected_app_profile()
if prof is None:
return
def work() -> None:
cmd = (getattr(prof, "command", "") or "").strip()
target = (getattr(prof, "target", "") or "").strip().lower()
ttl = int(getattr(prof, "ttl_sec", 0) or 0)
app_key = (getattr(prof, "app_key", "") or "").strip()
if not cmd:
raise RuntimeError("profile has empty command")
if target not in ("vpn", "direct"):
target = "vpn"
self._launch_and_mark(cmdline=cmd, target=target, ttl_sec=ttl, app_key=app_key)
self._safe(work, title="Run profile error")
def on_app_profile_delete(self) -> None: def on_app_profile_delete(self) -> None:
prof = self._selected_app_profile() prof = self._selected_app_profile()
if prof is None: if prof is None: