ui: allow appmark by PID (no launch)

This commit is contained in:
beckline
2026-02-15 22:56:00 +03:00
parent 8791f7f364
commit 2b32427e5b

View File

@@ -393,6 +393,27 @@ RU: Восстанавливает маршруты/nft из последнег
row_ttl.addStretch(1) row_ttl.addStretch(1)
run_layout.addLayout(row_ttl) run_layout.addLayout(row_ttl)
pid_group = QGroupBox("Mark existing PID (no launch)")
pid_layout = QHBoxLayout(pid_group)
pid_layout.addWidget(QLabel("PID"))
self.ed_app_pid = QLineEdit()
self.ed_app_pid.setPlaceholderText("e.g. 12345")
self.ed_app_pid.setToolTip(
"EN: Apply a runtime mark to an already running process.\n"
"EN: Reads /proc/<pid>/cgroup to get a cgroupv2 path.\n"
"RU: Применить runtime-метку к уже запущенному процессу.\n"
"RU: Читает /proc/<pid>/cgroup чтобы получить cgroupv2 path."
)
pid_layout.addWidget(self.ed_app_pid, stretch=1)
self.btn_app_mark_pid = QPushButton("Apply mark")
self.btn_app_mark_pid.setToolTip(
"EN: Apply routing mark to the PID (does not launch/stop the app).\n"
"RU: Применить метку маршрутизации к PID (не запускает/не останавливает приложение)."
)
self.btn_app_mark_pid.clicked.connect(self.on_app_mark_pid)
pid_layout.addWidget(self.btn_app_mark_pid)
run_layout.addWidget(pid_group)
row_btn = QHBoxLayout() row_btn = QHBoxLayout()
self.btn_app_run = QPushButton("Run + apply mark") self.btn_app_run = QPushButton("Run + apply mark")
self.btn_app_run.clicked.connect(self.on_app_run) self.btn_app_run.clicked.connect(self.on_app_run)
@@ -1650,7 +1671,7 @@ RU: Применяет policy-rules и проверяет health. При оши
self.lbl_app_last.setText("Last scope: —") self.lbl_app_last.setText("Last scope: —")
self.btn_app_stop_last.setEnabled(bool(unit)) self.btn_app_stop_last.setEnabled(bool(unit))
self.btn_app_unmark_last.setEnabled(bool(unit) and target in ("vpn", "direct") and cg_id > 0) self.btn_app_unmark_last.setEnabled(target in ("vpn", "direct") and cg_id > 0)
def _set_last_scope( def _set_last_scope(
self, self,
@@ -1793,6 +1814,28 @@ RU: Применяет policy-rules и проверяет health. При оши
return "" return ""
return "" return ""
def _cmdline_from_pid(self, pid: int) -> str:
p = int(pid or 0)
if p <= 0:
return ""
try:
with open(f"/proc/{p}/cmdline", "rb") as f:
raw = f.read() or b""
parts = [x for x in raw.split(b"\x00") if x]
out = " ".join([x.decode("utf-8", errors="replace") for x in parts])
return out.strip()
except Exception:
return ""
def _exe_from_pid(self, pid: int) -> str:
p = int(pid or 0)
if p <= 0:
return ""
try:
return os.readlink(f"/proc/{p}/exe").strip()
except Exception:
return ""
def _control_group_for_unit_retry(self, unit: str, *, timeout_sec: float = 2.0) -> str: def _control_group_for_unit_retry(self, unit: str, *, timeout_sec: float = 2.0) -> str:
u = (unit or "").strip() u = (unit or "").strip()
if not u: if not u:
@@ -1833,6 +1876,61 @@ RU: Применяет policy-rules и проверяет health. При оши
).strip() ).strip()
) )
def on_app_mark_pid(self) -> None:
def work() -> None:
raw = (self.ed_app_pid.text() or "").strip()
if not raw:
QMessageBox.warning(self, "Missing PID", "Please enter a PID first.")
return
try:
pid = int(raw)
except Exception:
QMessageBox.warning(self, "Invalid PID", f"PID must be an integer: {raw!r}")
return
if pid <= 0:
QMessageBox.warning(self, "Invalid PID", f"PID must be > 0: {pid}")
return
cg = self._cgroup_path_from_pid(pid)
if not cg:
raise RuntimeError(f"failed to read cgroup for pid={pid} (process may not exist)")
cmdline = self._cmdline_from_pid(pid) or f"pid={pid}"
app_key = self._exe_from_pid(pid) or self._infer_app_key_from_cmdline(cmdline) or f"pid:{pid}"
target = "vpn" if self.rad_app_vpn.isChecked() else "direct"
ttl_sec = int(self.spn_app_ttl.value()) * 3600
self._append_app_log(f"[pid] mark: pid={pid} target={target} ttl={ttl_sec}s")
self._append_app_log(f"[pid] cgroup: {cg}")
if cmdline:
self._append_app_log(f"[pid] cmdline: {cmdline}")
res = self.ctrl.traffic_appmarks_apply(
op="add",
target=target,
cgroup=cg,
unit="",
command=cmdline,
app_key=app_key,
timeout_sec=ttl_sec,
)
if not res.ok:
self._append_app_log(f"[pid] ERROR: {res.message}")
self._set_action_status(f"PID mark failed: {res.message}", ok=False)
QMessageBox.critical(self, "Mark PID error", res.message or "mark failed")
return
self._append_app_log(f"[pid] OK: {res.message} cgroup_id={res.cgroup_id} timeout={res.timeout_sec}s")
self._set_action_status(f"PID marked: target={target} cgroup_id={res.cgroup_id}", ok=True)
self._set_last_scope(unit="", target=target, app_key=app_key, cmdline=cmdline, cgroup_id=int(res.cgroup_id or 0))
self.refresh_appmarks_counts()
self.refresh_appmarks_items(quiet=True)
self.refresh_app_profiles(quiet=True)
self._safe(work, title="Mark PID error")
def on_app_run(self) -> None: def on_app_run(self) -> None:
def work() -> None: def work() -> None:
cmdline = (self.ed_app_cmd.text() or "").strip() cmdline = (self.ed_app_cmd.text() or "").strip()
@@ -2024,14 +2122,14 @@ RU: Применяет policy-rules и проверяет health. При оши
unit = (self._last_app_unit or "").strip() unit = (self._last_app_unit or "").strip()
target = (self._last_app_target or "").strip().lower() target = (self._last_app_target or "").strip().lower()
cg_id = int(self._last_app_cgroup_id or 0) cg_id = int(self._last_app_cgroup_id or 0)
if not unit or target not in ("vpn", "direct") or cg_id <= 0: if target not in ("vpn", "direct") or cg_id <= 0:
return return
def work() -> None: def work() -> None:
if QMessageBox.question( if QMessageBox.question(
self, self,
"Unmark last", "Unmark last",
f"Remove routing mark for last scope?\n\nunit={unit}\ntarget={target}\ncgroup_id={cg_id}", f"Remove routing mark for last item?\n\nunit={unit or '-'}\ntarget={target}\ncgroup_id={cg_id}",
) != QMessageBox.StandardButton.Yes: ) != QMessageBox.StandardButton.Yes:
return return