This document describes the internal design and data flow of the AdGuard VPN plugin.
Language / Idioma: English is primary. Portuguese (Brazil) translation follows the English architecture notes.
The plugin is organized into four layers, each with a single responsibility:
┌──────────────────────────────────────────────────────┐
│ DankBar / DMS │
├────────────────────────┬─────────────────────────────┤
│ AdGuardVpnWidget │ AdGuardVpnSettings │
│ (bar pill + popout) │ (DMS settings screen) │
├────────────────────────┴─────────────────────────────┤
│ AdGuardVpnService (singleton) │
│ polling · actions · state · buildArgs · parsers │
├──────────────────────────────────────────────────────┤
│ AdGuardVpnI18n (singleton) │
│ i18n/en.js · i18n/<locale>.js │
├──────────────────────────────────────────────────────┤
│ AdGuardVpnParsers.js (.pragma library) │
│ parseStatusOutput · parseConfigOutput · … │
└──────────────────────────────────────────────────────┘
↕ Proc.runCommand()
┌─────────────┐
│ adguardvpn- │
│ cli (local) │
└─────────────┘
| Layer | File(s) | Role |
|---|---|---|
| UI | AdGuardVpnWidget.qml |
Bar pill, popout controls, location list, config cards |
| Settings | AdGuardVpnSettings.qml |
Declarative DMS setting controls |
| Service | AdGuardVpnService.qml |
Singleton: settings lifecycle, CLI execution, polling, state management |
| Localization | AdGuardVpnI18n.qml + i18n/*.js |
Translation lookups with fallback chain |
| Parsers | AdGuardVpnParsers.js |
Pure functions: parse CLI output into structured data |
1. Startup
Component.onCompleted → loadSettings() → restartTimers()
→ checkCliAvailability()
→ refreshAll(true)
→ maybeAutoConnectOnStartup()
only after initial status + config are known
2. Polling (repeating timers)
statusTimer ──→ refreshStatus() ──→ runCli("status") ──→ parseStatus()
metadataTimer → refreshConfig() ──→ runCli("config") ──→ parseConfig()
refreshLicense() ──→ runCli("license") ──→ parseLicense()
locationsTimer → refreshLocations() → runCli("list-locations") → parseLocations()
3. User actions
Widget button → Service method (e.g. connectFastest())
→ suspendPolling() → runCli() → resumePolling()
→ recordLastCommand() → toast notification → refreshStatus()
Key principle: the Widget never runs CLI commands directly. It binds to Service properties and calls Service methods. The Service owns all state.
- Load and validate all settings from
PluginService.loadPluginData()on startup. - Each setting is normalized (type-checked, clamped, default-fallback).
- Saves individual settings with
saveSetting(key, value).
| Timer | Cadence | What it refreshes |
|---|---|---|
statusTimer |
refreshIntervalSec (default 8 s) |
VPN connection state |
metadataTimer |
refreshIntervalSec × 3 (min 15 s) |
Config + license info |
locationsTimer |
refreshIntervalSec × 60 (min 15 min, doubled per failed refresh up to 1 h) |
Ranked server locations |
During write actions (runAction), all timers are suspended to avoid conflicting reads, and resumed after completion.
locationsTimer is the slow one on purpose: the location list barely changes, while adguardvpn-cli list-locations segfaults intermittently — every crash costs a coredump and returns nothing. locationsPollIntervalMs() therefore keeps the base cadence in minutes and doubles it on each failed refresh (locationsFailureStreak), resetting on the first success. Failed refreshes never overwrite locations, so the picker keeps the last good list instead of blanking. The popout's manual refresh is unaffected.
- Connect:
connectFastest(),connectToLocation(text),connectWithStrategy() - Disconnect:
disconnect()(setssuppressReconnectOnceto avoid auto-reconnect loop) - Config writes:
setMode(),setProtocol(),setUpdateChannel(),setDns() - Utilities:
openTunnelLog(),toggleFavoriteLocation()
Connect actions use prepareDisconnectedRuntime() before invoking the CLI. The preflight checks for conflicting same-metric default routes in TUN mode, asks the configured adguardBinary to disconnect an existing runtime, verifies the control socket is not busy with lsof/fuser, and removes stale socket files when safe.
All connect actions use buildArgs() to append -y, --no-progress, and IP stack flags consistently.
When the opt-in useSystemdService setting is enabled, lifecycle operations use the fixed adguardvpn-dms-control helper through non-interactive sudo; all reads and configuration writes remain on adguardBinary. The helper owns the idempotency decision, not the Service: a connect action for the target the tunnel is already on must not reach systemctl restart, which would drop the interface — and every connection riding it — for a few seconds. See scripts/adguardvpn-dms-control for the reference implementation.
maybeScheduleReconnect(wasConnected, nowConnected)retries after 5 s, 15 s, and 45 s when the tunnel drops.- Suppressed after explicit
disconnect()to prevent unwanted reconnects.
All parsers are pure functions in a .pragma library module — no QML/state dependencies.
| Function | Input | Output |
|---|---|---|
parseStatusOutput(clean) |
CLI status text |
{ connected, disconnected, empty, connectedLocation, … } |
parseLicenseOutput(clean) |
CLI license text |
{ accountEmail, accountTier, maxDevices, subscriptionRenewDate } |
parseConfigOutput(clean, fallback) |
CLI config show text |
{ currentMode, currentProtocol, dnsUpstream, … } |
parseLocationsOutput(clean) |
CLI list-locations text |
{ locations: [...], parseFailed: bool } |
parseLocationLine(line) |
Single location line | { iso, country, city, ping, label } or null |
The location parser tries five column-splitting strategies in order: multi-space, tab, pipe, CSV, and dashed format — making it resilient to CLI output changes.
- Bar pill: icon (shield states) + optional location text.
- Popout sections:
- Status card (connection state, account, last sync, diagnostics)
- Quick actions (connect/disconnect, fastest, refresh, open log)
- Locations (search filter, per-location favorites, quick-connect by city/country target)
- Configuration (mode, protocol, update channel, DNS)
- All labels go through
AdGuardVpnI18n.tr(key, fallback, params).
- Declarative DMS settings (
SelectionSetting,SliderSetting,ToggleSetting,StringSetting). - Persists values that
AdGuardVpnService.loadSettings()picks up on change. - No direct CLI interaction.
| Scenario | What happens |
|---|---|
| Non-zero exit code | lastError updated, error toast emitted, status refresh triggered |
| Location not found | Contextual hint appended: "Try refreshing locations and using the ISO code" |
| CLI unavailable | All actions disabled, bar shows warning icon, status shows unavailable message |
| Empty/unparseable output | Graceful fallback to "Unknown" / "No output" with no crash |
| Permission | Why |
|---|---|
settings_read |
Load plugin settings from DMS storage |
settings_write |
Persist plugin settings (polling interval, strategy, favorites, etc.) |
process |
Execute local adguardvpn-cli commands via Proc.runCommand |
Este documento descreve o desenho interno e o fluxo de dados do plugin AdGuard VPN.
O plugin é organizado em quatro camadas com responsabilidades separadas:
| Camada | Arquivos | Responsabilidade |
|---|---|---|
| UI | AdGuardVpnWidget.qml |
Ícone/barra, popout, controles, lista de localizações e cards de configuração |
| Settings | AdGuardVpnSettings.qml |
Controles declarativos de configuração do DMS |
| Service | AdGuardVpnService.qml |
Singleton de settings, execução do CLI, polling e estado |
| Localização | AdGuardVpnI18n.qml + i18n/*.js |
Traduções com fallback |
| Parsers | AdGuardVpnParsers.js |
Funções puras para transformar saída do CLI em dados estruturados |
- No startup,
loadSettings()reinicia timers, valida o CLI, chamarefreshAll(true)e só tenta auto-conectar quando status e configuração iniciais já são conhecidos. - Timers chamam
refreshStatus(),refreshConfig(),refreshLicense()erefreshLocations()em intervalos derivados derefreshIntervalSec. - A UI nunca executa comandos diretamente. Ela chama métodos do
AdGuardVpnService, que suspende polling durante ações de escrita, executa o CLI e atualiza propriedades observáveis.
- Carregar e normalizar settings via
PluginService.loadPluginData(). - Persistir mudanças pontuais com
saveSetting(key, value). - Executar
adguardvpn-cliporProc.runCommand()usandoadguardBinaryconfigurado. - Montar argumentos de conexão com
buildArgs(). - Rodar
prepareDisconnectedRuntime()antes de conectar. - Controlar auto-reconnect e suppressão após disconnect explícito.
Antes de conectar, prepareDisconnectedRuntime() verifica conflitos de rota em modo TUN, chama disconnect pelo binário configurado, testa se o socket de controle está ocupado com lsof/fuser e remove socket obsoleto quando seguro.
Os parsers ficam em .pragma library e não dependem de estado QML. Eles interpretam status, licença, configuração e localizações. A saída ANSI é removida antes do parsing.
O widget mostra status, conta, diagnóstico, ações rápidas, localizações favoritas por cidade/país e controles de configuração. A tela de settings persiste valores; ela não chama o CLI diretamente.
Falhas de comando atualizam lastError, exibem toast, e disparam refresh de status quando necessário. Saída vazia ou formato inesperado cai para estados seguros como “Unknown” ou “No output”.
| Permissão | Motivo |
|---|---|
settings_read |
Ler configurações do DMS |
settings_write |
Persistir polling, estratégia, favoritos e preferências |
process |
Executar comandos locais do adguardvpn-cli |