ui+api: dedup per-app marks by app_key; auto-refresh runtime
This commit is contained in:
@@ -48,6 +48,9 @@ type appMarkItem struct {
|
|||||||
Cgroup string `json:"cgroup"` // absolute path ("/user.slice/..."), informational
|
Cgroup string `json:"cgroup"` // absolute path ("/user.slice/..."), informational
|
||||||
CgroupRel string `json:"cgroup_rel"`
|
CgroupRel string `json:"cgroup_rel"`
|
||||||
Level int `json:"level"`
|
Level int `json:"level"`
|
||||||
|
Unit string `json:"unit,omitempty"`
|
||||||
|
Command string `json:"command,omitempty"`
|
||||||
|
AppKey string `json:"app_key,omitempty"`
|
||||||
AddedAt string `json:"added_at"`
|
AddedAt string `json:"added_at"`
|
||||||
ExpiresAt string `json:"expires_at"`
|
ExpiresAt string `json:"expires_at"`
|
||||||
}
|
}
|
||||||
@@ -74,6 +77,9 @@ func handleTrafficAppMarks(w http.ResponseWriter, r *http.Request) {
|
|||||||
op := TrafficAppMarksOp(strings.ToLower(strings.TrimSpace(string(body.Op))))
|
op := TrafficAppMarksOp(strings.ToLower(strings.TrimSpace(string(body.Op))))
|
||||||
target := strings.ToLower(strings.TrimSpace(body.Target))
|
target := strings.ToLower(strings.TrimSpace(body.Target))
|
||||||
cgroup := strings.TrimSpace(body.Cgroup)
|
cgroup := strings.TrimSpace(body.Cgroup)
|
||||||
|
unit := strings.TrimSpace(body.Unit)
|
||||||
|
command := strings.TrimSpace(body.Command)
|
||||||
|
appKey := strings.TrimSpace(body.AppKey)
|
||||||
timeoutSec := body.TimeoutSec
|
timeoutSec := body.TimeoutSec
|
||||||
|
|
||||||
if op == "" {
|
if op == "" {
|
||||||
@@ -165,7 +171,7 @@ func handleTrafficAppMarks(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := appMarksAdd(target, inodeID, cgAbs, rel, level, ttl); err != nil {
|
if err := appMarksAdd(target, inodeID, cgAbs, rel, level, unit, command, appKey, ttl); err != nil {
|
||||||
writeJSON(w, http.StatusOK, TrafficAppMarksResponse{
|
writeJSON(w, http.StatusOK, TrafficAppMarksResponse{
|
||||||
OK: false,
|
OK: false,
|
||||||
Op: string(op),
|
Op: string(op),
|
||||||
@@ -250,7 +256,7 @@ func appMarksGetStatus() (vpnCount int, directCount int) {
|
|||||||
return vpnCount, directCount
|
return vpnCount, directCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int, ttlSec int) error {
|
func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int, unit string, command string, appKey string, ttlSec int) error {
|
||||||
target = strings.ToLower(strings.TrimSpace(target))
|
target = strings.ToLower(strings.TrimSpace(target))
|
||||||
if target != "vpn" && target != "direct" {
|
if target != "vpn" && target != "direct" {
|
||||||
return fmt.Errorf("invalid target")
|
return fmt.Errorf("invalid target")
|
||||||
@@ -271,6 +277,27 @@ func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int,
|
|||||||
st := loadAppMarksState()
|
st := loadAppMarksState()
|
||||||
changed := pruneExpiredAppMarksLocked(&st, time.Now().UTC())
|
changed := pruneExpiredAppMarksLocked(&st, time.Now().UTC())
|
||||||
|
|
||||||
|
unit = strings.TrimSpace(unit)
|
||||||
|
command = strings.TrimSpace(command)
|
||||||
|
appKey = normalizeAppKey(appKey, command)
|
||||||
|
|
||||||
|
// EN: Avoid unbounded growth of marks for the same app.
|
||||||
|
// RU: Не даём бесконечно плодить метки на одно и то же приложение.
|
||||||
|
if appKey != "" {
|
||||||
|
kept := st.Items[:0]
|
||||||
|
for _, it := range st.Items {
|
||||||
|
if strings.ToLower(strings.TrimSpace(it.Target)) == target &&
|
||||||
|
strings.TrimSpace(it.AppKey) == appKey &&
|
||||||
|
it.ID != id {
|
||||||
|
_ = nftDeleteAppMarkRule(target, it.ID)
|
||||||
|
changed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kept = append(kept, it)
|
||||||
|
}
|
||||||
|
st.Items = kept
|
||||||
|
}
|
||||||
|
|
||||||
// Replace any existing rule/state for this (target,id).
|
// Replace any existing rule/state for this (target,id).
|
||||||
_ = nftDeleteAppMarkRule(target, id)
|
_ = nftDeleteAppMarkRule(target, id)
|
||||||
if err := nftInsertAppMarkRule(target, rel, level, id); err != nil {
|
if err := nftInsertAppMarkRule(target, rel, level, id); err != nil {
|
||||||
@@ -284,6 +311,9 @@ func appMarksAdd(target string, id uint64, cgAbs string, rel string, level int,
|
|||||||
Cgroup: cgAbs,
|
Cgroup: cgAbs,
|
||||||
CgroupRel: rel,
|
CgroupRel: rel,
|
||||||
Level: level,
|
Level: level,
|
||||||
|
Unit: unit,
|
||||||
|
Command: command,
|
||||||
|
AppKey: appKey,
|
||||||
AddedAt: now.Format(time.RFC3339),
|
AddedAt: now.Format(time.RFC3339),
|
||||||
ExpiresAt: now.Add(time.Duration(ttlSec) * time.Second).Format(time.RFC3339),
|
ExpiresAt: now.Add(time.Duration(ttlSec) * time.Second).Format(time.RFC3339),
|
||||||
}
|
}
|
||||||
@@ -613,6 +643,22 @@ func saveAppMarksState(st appMarksState) error {
|
|||||||
return os.Rename(tmp, trafficAppMarksPath)
|
return os.Rename(tmp, trafficAppMarksPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeAppKey(appKey string, command string) string {
|
||||||
|
key := strings.TrimSpace(appKey)
|
||||||
|
if key != "" {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
cmd := strings.TrimSpace(command)
|
||||||
|
if cmd == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
fields := strings.Fields(cmd)
|
||||||
|
if len(fields) > 0 {
|
||||||
|
return strings.TrimSpace(fields[0])
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
func isAllDigits(s string) bool {
|
func isAllDigits(s string) bool {
|
||||||
s = strings.TrimSpace(s)
|
s = strings.TrimSpace(s)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
|
|||||||
@@ -200,10 +200,15 @@ const (
|
|||||||
// EN: Runtime app marking request. Used by per-app launcher wrappers.
|
// EN: Runtime app marking request. Used by per-app launcher wrappers.
|
||||||
// RU: Runtime app marking запрос. Используется wrapper-лаунчером per-app.
|
// RU: Runtime app marking запрос. Используется wrapper-лаунчером per-app.
|
||||||
type TrafficAppMarksRequest struct {
|
type TrafficAppMarksRequest struct {
|
||||||
Op TrafficAppMarksOp `json:"op"`
|
Op TrafficAppMarksOp `json:"op"`
|
||||||
Target string `json:"target"` // vpn|direct
|
Target string `json:"target"` // vpn|direct
|
||||||
Cgroup string `json:"cgroup,omitempty"`
|
Cgroup string `json:"cgroup,omitempty"`
|
||||||
TimeoutSec int `json:"timeout_sec,omitempty"` // only for add
|
// EN: Optional metadata to deduplicate marks per-app across restarts / re-runs.
|
||||||
|
// RU: Опциональные метаданные, чтобы не плодить метки на одно и то же приложение.
|
||||||
|
Unit string `json:"unit,omitempty"`
|
||||||
|
Command string `json:"command,omitempty"`
|
||||||
|
AppKey string `json:"app_key,omitempty"`
|
||||||
|
TimeoutSec int `json:"timeout_sec,omitempty"` // only for add
|
||||||
}
|
}
|
||||||
|
|
||||||
type TrafficAppMarksResponse struct {
|
type TrafficAppMarksResponse struct {
|
||||||
|
|||||||
@@ -825,6 +825,9 @@ class ApiClient:
|
|||||||
op: str,
|
op: str,
|
||||||
target: str,
|
target: str,
|
||||||
cgroup: str = "",
|
cgroup: str = "",
|
||||||
|
unit: str = "",
|
||||||
|
command: str = "",
|
||||||
|
app_key: str = "",
|
||||||
timeout_sec: int = 0,
|
timeout_sec: int = 0,
|
||||||
) -> TrafficAppMarksResult:
|
) -> TrafficAppMarksResult:
|
||||||
payload: Dict[str, Any] = {
|
payload: Dict[str, Any] = {
|
||||||
@@ -833,6 +836,12 @@ class ApiClient:
|
|||||||
}
|
}
|
||||||
if cgroup:
|
if cgroup:
|
||||||
payload["cgroup"] = str(cgroup).strip()
|
payload["cgroup"] = str(cgroup).strip()
|
||||||
|
if unit:
|
||||||
|
payload["unit"] = str(unit).strip()
|
||||||
|
if command:
|
||||||
|
payload["command"] = str(command).strip()
|
||||||
|
if app_key:
|
||||||
|
payload["app_key"] = str(app_key).strip()
|
||||||
if int(timeout_sec or 0) > 0:
|
if int(timeout_sec or 0) > 0:
|
||||||
payload["timeout_sec"] = int(timeout_sec)
|
payload["timeout_sec"] = int(timeout_sec)
|
||||||
|
|
||||||
|
|||||||
@@ -716,12 +716,18 @@ class DashboardController:
|
|||||||
op: str,
|
op: str,
|
||||||
target: str,
|
target: str,
|
||||||
cgroup: str = "",
|
cgroup: str = "",
|
||||||
|
unit: str = "",
|
||||||
|
command: str = "",
|
||||||
|
app_key: str = "",
|
||||||
timeout_sec: int = 0,
|
timeout_sec: int = 0,
|
||||||
) -> TrafficAppMarksResult:
|
) -> TrafficAppMarksResult:
|
||||||
return self.client.traffic_appmarks_apply(
|
return self.client.traffic_appmarks_apply(
|
||||||
op=op,
|
op=op,
|
||||||
target=target,
|
target=target,
|
||||||
cgroup=cgroup,
|
cgroup=cgroup,
|
||||||
|
unit=unit,
|
||||||
|
command=command,
|
||||||
|
app_key=app_key,
|
||||||
timeout_sec=timeout_sec,
|
timeout_sec=timeout_sec,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,8 @@ class TrafficModeDialog(QDialog):
|
|||||||
self._settings = QtCore.QSettings("AdGuardVPN", "SelectiveVPNDashboardQt")
|
self._settings = QtCore.QSettings("AdGuardVPN", "SelectiveVPNDashboardQt")
|
||||||
self._last_app_unit: str = str(self._settings.value("traffic_app_last_unit", "") or "")
|
self._last_app_unit: str = str(self._settings.value("traffic_app_last_unit", "") or "")
|
||||||
self._last_app_target: str = str(self._settings.value("traffic_app_last_target", "") or "")
|
self._last_app_target: str = str(self._settings.value("traffic_app_last_target", "") or "")
|
||||||
|
self._last_app_key: str = str(self._settings.value("traffic_app_last_key", "") or "")
|
||||||
|
self._last_app_cmdline: str = str(self._settings.value("traffic_app_last_cmdline", "") or "")
|
||||||
try:
|
try:
|
||||||
self._last_app_cgroup_id: int = int(self._settings.value("traffic_app_last_cgroup_id", 0) or 0)
|
self._last_app_cgroup_id: int = int(self._settings.value("traffic_app_last_cgroup_id", 0) or 0)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -468,6 +470,23 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
QtCore.QTimer.singleShot(0, self.refresh_appmarks_counts)
|
QtCore.QTimer.singleShot(0, self.refresh_appmarks_counts)
|
||||||
QtCore.QTimer.singleShot(0, self.refresh_running_scopes)
|
QtCore.QTimer.singleShot(0, self.refresh_running_scopes)
|
||||||
|
|
||||||
|
# EN: Auto-refresh runtime marks/units while dialog is open.
|
||||||
|
# RU: Авто-обновление runtime меток/юнитов пока окно открыто.
|
||||||
|
self._runtime_auto_timer = QtCore.QTimer(self)
|
||||||
|
self._runtime_auto_timer.setInterval(5000)
|
||||||
|
self._runtime_auto_timer.timeout.connect(self._auto_refresh_runtime)
|
||||||
|
self._runtime_auto_timer.start()
|
||||||
|
|
||||||
|
def _auto_refresh_runtime(self) -> None:
|
||||||
|
# Keep this quiet: no modal popups.
|
||||||
|
self.refresh_appmarks_counts()
|
||||||
|
try:
|
||||||
|
# Only refresh units list when Apps(runtime) tab is visible.
|
||||||
|
if int(self.tabs.currentIndex() or 0) == 1:
|
||||||
|
self.refresh_running_scopes(quiet=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
def _is_operation_error(self, message: str) -> bool:
|
def _is_operation_error(self, message: str) -> bool:
|
||||||
low = (message or "").strip().lower()
|
low = (message or "").strip().lower()
|
||||||
return ("rolled back" in low) or ("apply failed" in low) or ("verification failed" in low)
|
return ("rolled back" in low) or ("apply failed" in low) or ("verification failed" in low)
|
||||||
@@ -864,6 +883,7 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
def _refresh_last_scope_ui(self) -> None:
|
def _refresh_last_scope_ui(self) -> None:
|
||||||
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()
|
||||||
|
app_key = (self._last_app_key or "").strip()
|
||||||
cg_id = int(self._last_app_cgroup_id or 0)
|
cg_id = int(self._last_app_cgroup_id or 0)
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
@@ -871,6 +891,8 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
parts.append(f"unit={unit}")
|
parts.append(f"unit={unit}")
|
||||||
if target in ("vpn", "direct"):
|
if target in ("vpn", "direct"):
|
||||||
parts.append(f"target={target}")
|
parts.append(f"target={target}")
|
||||||
|
if app_key:
|
||||||
|
parts.append(f"app={app_key}")
|
||||||
if cg_id > 0:
|
if cg_id > 0:
|
||||||
parts.append(f"cgroup_id={cg_id}")
|
parts.append(f"cgroup_id={cg_id}")
|
||||||
|
|
||||||
@@ -882,9 +904,19 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
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(bool(unit) and target in ("vpn", "direct") and cg_id > 0)
|
||||||
|
|
||||||
def _set_last_scope(self, *, unit: str = "", target: str = "", cgroup_id: int = 0) -> None:
|
def _set_last_scope(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
unit: str = "",
|
||||||
|
target: str = "",
|
||||||
|
app_key: str = "",
|
||||||
|
cmdline: str = "",
|
||||||
|
cgroup_id: int = 0,
|
||||||
|
) -> None:
|
||||||
self._last_app_unit = str(unit or "").strip()
|
self._last_app_unit = str(unit or "").strip()
|
||||||
self._last_app_target = str(target or "").strip().lower()
|
self._last_app_target = str(target or "").strip().lower()
|
||||||
|
self._last_app_key = str(app_key or "").strip()
|
||||||
|
self._last_app_cmdline = str(cmdline or "").strip()
|
||||||
try:
|
try:
|
||||||
self._last_app_cgroup_id = int(cgroup_id or 0)
|
self._last_app_cgroup_id = int(cgroup_id or 0)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -892,6 +924,8 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
|
|
||||||
self._settings.setValue("traffic_app_last_unit", self._last_app_unit)
|
self._settings.setValue("traffic_app_last_unit", self._last_app_unit)
|
||||||
self._settings.setValue("traffic_app_last_target", self._last_app_target)
|
self._settings.setValue("traffic_app_last_target", self._last_app_target)
|
||||||
|
self._settings.setValue("traffic_app_last_key", self._last_app_key)
|
||||||
|
self._settings.setValue("traffic_app_last_cmdline", self._last_app_cmdline)
|
||||||
self._settings.setValue("traffic_app_last_cgroup_id", int(self._last_app_cgroup_id or 0))
|
self._settings.setValue("traffic_app_last_cgroup_id", int(self._last_app_cgroup_id or 0))
|
||||||
self._refresh_last_scope_ui()
|
self._refresh_last_scope_ui()
|
||||||
|
|
||||||
@@ -1058,24 +1092,86 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
QMessageBox.warning(self, "Missing command", "Please enter a command to run.")
|
QMessageBox.warning(self, "Missing command", "Please enter a command to run.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
app_key = ""
|
||||||
|
try:
|
||||||
|
args = shlex.split(cmdline or "")
|
||||||
|
if args:
|
||||||
|
app_key = str(args[0] or "").strip()
|
||||||
|
except Exception:
|
||||||
|
app_key = ""
|
||||||
|
if not app_key:
|
||||||
|
# Fallback: best-effort first token.
|
||||||
|
app_key = (cmdline.split() or [""])[0].strip()
|
||||||
|
|
||||||
target = "vpn" if self.rad_app_vpn.isChecked() else "direct"
|
target = "vpn" if self.rad_app_vpn.isChecked() else "direct"
|
||||||
ttl_sec = int(self.spn_app_ttl.value()) * 3600
|
ttl_sec = int(self.spn_app_ttl.value()) * 3600
|
||||||
|
|
||||||
|
# EN: If the app is already running inside the last svpn unit, don't spawn a new instance.
|
||||||
|
# RU: Если приложение уже запущено в последнем svpn unit, не запускаем второй экземпляр.
|
||||||
|
last_unit = (self._last_app_unit or "").strip()
|
||||||
|
last_target = (self._last_app_target or "").strip().lower()
|
||||||
|
last_key = (self._last_app_key or "").strip()
|
||||||
|
if last_unit and last_target == target and last_key and last_key == app_key:
|
||||||
|
code, out = self._systemctl_user(["is-active", last_unit])
|
||||||
|
if code == 0 and (out or "").strip().lower() == "active":
|
||||||
|
cg = self._query_control_group_for_unit(last_unit)
|
||||||
|
self._append_app_log(
|
||||||
|
f"[app] already running: app={app_key} target={target} unit={last_unit} (refreshing mark)"
|
||||||
|
)
|
||||||
|
res = self.ctrl.traffic_appmarks_apply(
|
||||||
|
op="add",
|
||||||
|
target=target,
|
||||||
|
cgroup=cg,
|
||||||
|
unit=last_unit,
|
||||||
|
command=cmdline,
|
||||||
|
app_key=app_key,
|
||||||
|
timeout_sec=ttl_sec,
|
||||||
|
)
|
||||||
|
if res.ok:
|
||||||
|
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 refreshed: target={target} cgroup_id={res.cgroup_id}",
|
||||||
|
ok=True,
|
||||||
|
)
|
||||||
|
self._set_last_scope(
|
||||||
|
unit=last_unit,
|
||||||
|
target=target,
|
||||||
|
app_key=app_key,
|
||||||
|
cmdline=cmdline,
|
||||||
|
cgroup_id=int(res.cgroup_id or 0),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._append_app_log(f"[appmarks] ERROR: {res.message}")
|
||||||
|
self._set_action_status(
|
||||||
|
f"App mark refresh failed: target={target} ({res.message})",
|
||||||
|
ok=False,
|
||||||
|
)
|
||||||
|
QMessageBox.critical(self, "App mark error", res.message or "unknown error")
|
||||||
|
self.refresh_appmarks_counts()
|
||||||
|
self.refresh_running_scopes()
|
||||||
|
return
|
||||||
|
|
||||||
unit = f"svpn-{target}-{int(time.time())}.service"
|
unit = f"svpn-{target}-{int(time.time())}.service"
|
||||||
|
|
||||||
self._append_app_log(
|
self._append_app_log(
|
||||||
f"[app] launching: target={target} ttl={ttl_sec}s unit={unit}"
|
f"[app] launching: app={app_key or '-'} target={target} ttl={ttl_sec}s unit={unit}"
|
||||||
)
|
)
|
||||||
|
|
||||||
cg, out = self._run_systemd_unit(cmdline, unit=unit)
|
cg, out = self._run_systemd_unit(cmdline, unit=unit)
|
||||||
if out:
|
if out:
|
||||||
self._append_app_log(f"[app] systemd-run:\n{out}")
|
self._append_app_log(f"[app] systemd-run:\n{out}")
|
||||||
self._append_app_log(f"[app] ControlGroup: {cg}")
|
self._append_app_log(f"[app] ControlGroup: {cg}")
|
||||||
self._set_last_scope(unit=unit, target=target, cgroup_id=0)
|
self._set_last_scope(unit=unit, target=target, app_key=app_key, cmdline=cmdline, cgroup_id=0)
|
||||||
|
|
||||||
res = self.ctrl.traffic_appmarks_apply(
|
res = self.ctrl.traffic_appmarks_apply(
|
||||||
op="add",
|
op="add",
|
||||||
target=target,
|
target=target,
|
||||||
cgroup=cg,
|
cgroup=cg,
|
||||||
|
unit=unit,
|
||||||
|
command=cmdline,
|
||||||
|
app_key=app_key,
|
||||||
timeout_sec=ttl_sec,
|
timeout_sec=ttl_sec,
|
||||||
)
|
)
|
||||||
if res.ok:
|
if res.ok:
|
||||||
@@ -1086,20 +1182,40 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
f"App mark added: target={target} cgroup_id={res.cgroup_id}",
|
f"App mark added: target={target} cgroup_id={res.cgroup_id}",
|
||||||
ok=True,
|
ok=True,
|
||||||
)
|
)
|
||||||
self._set_last_scope(unit=unit, target=target, cgroup_id=int(res.cgroup_id or 0))
|
self._set_last_scope(
|
||||||
|
unit=unit,
|
||||||
|
target=target,
|
||||||
|
app_key=app_key,
|
||||||
|
cmdline=cmdline,
|
||||||
|
cgroup_id=int(res.cgroup_id or 0),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._append_app_log(f"[appmarks] ERROR: {res.message}")
|
self._append_app_log(f"[appmarks] ERROR: {res.message}")
|
||||||
self._set_action_status(
|
self._set_action_status(
|
||||||
f"App mark failed: target={target} ({res.message})",
|
f"App mark failed: target={target} ({res.message})",
|
||||||
ok=False,
|
ok=False,
|
||||||
)
|
)
|
||||||
QMessageBox.critical(
|
low = (res.message or "").lower()
|
||||||
self,
|
if "cgroupv2 path fails" in low or "no such file or directory" in low:
|
||||||
"App mark error",
|
QMessageBox.critical(
|
||||||
res.message or "unknown error",
|
self,
|
||||||
)
|
"App mark error",
|
||||||
|
(res.message or "unknown error")
|
||||||
|
+ "\n\n"
|
||||||
|
+ "EN: This usually means the app didn't stay inside the new systemd unit "
|
||||||
|
+ "(often because it was already running). Close the app completely and run again from here.\n"
|
||||||
|
+ "RU: Обычно это значит, что приложение не осталось в новом systemd unit "
|
||||||
|
+ "(часто потому что оно уже было запущено). Полностью закрой приложение и запусти снова отсюда.",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
QMessageBox.critical(
|
||||||
|
self,
|
||||||
|
"App mark error",
|
||||||
|
res.message or "unknown error",
|
||||||
|
)
|
||||||
|
|
||||||
self.refresh_appmarks_counts()
|
self.refresh_appmarks_counts()
|
||||||
|
self.refresh_running_scopes()
|
||||||
|
|
||||||
self._safe(work, title="Run app error")
|
self._safe(work, title="Run app error")
|
||||||
|
|
||||||
@@ -1292,7 +1408,7 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
except Exception:
|
except Exception:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def refresh_running_scopes(self) -> None:
|
def refresh_running_scopes(self, *, quiet: bool = False) -> None:
|
||||||
def work() -> None:
|
def work() -> None:
|
||||||
units = self._list_running_svpn_units()
|
units = self._list_running_svpn_units()
|
||||||
self.lst_scopes.clear()
|
self.lst_scopes.clear()
|
||||||
@@ -1347,6 +1463,14 @@ RU: Применяет policy-rules и проверяет health. При оши
|
|||||||
self.btn_scopes_stop_selected.setEnabled(has_any)
|
self.btn_scopes_stop_selected.setEnabled(has_any)
|
||||||
self.btn_scopes_cleanup.setEnabled(has_any)
|
self.btn_scopes_cleanup.setEnabled(has_any)
|
||||||
|
|
||||||
|
if quiet:
|
||||||
|
try:
|
||||||
|
work()
|
||||||
|
except Exception:
|
||||||
|
# Quiet mode: avoid modal popups on background refresh.
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
|
||||||
self._safe(work, title="Units refresh error")
|
self._safe(work, title="Units refresh error")
|
||||||
|
|
||||||
def _copy_scope_unit(self, it: QListWidgetItem) -> None:
|
def _copy_scope_unit(self, it: QListWidgetItem) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user