ui: run selected app profile (systemd-run + appmark refresh)
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user