diff --git a/dns-switcher/README.md b/dns-switcher/README.md index 9380cbbb..ad4e9484 100644 --- a/dns-switcher/README.md +++ b/dns-switcher/README.md @@ -22,9 +22,10 @@ rebuilt on the v5 Luau plugin API. - **Detection**, not guessing — reads the connection's own `ipv4.dns` / `ipv4.ignore-auto-dns`, so a manually configured resolver (LAN ones included) shows as its provider, DHCP-assigned DNS shows as *Default (ISP)* -- **DNS lookup tester** at the bottom of the panel: resolve any name against - the currently active provider's own address with `dig`/`nslookup`, to - confirm a switch took effect or check whether a provider blocks a domain +- **DNS lookup tester** at the bottom of the panel: resolve a name with + `dig`/`nslookup` against the active provider, or against any other provider + from its row menu. Use it to confirm a switch, or to find out if a provider + blocks a domain before you switch to it - **Fully rebindable gestures** — left click, right click and scroll are declared in the manifest (`[widget.actions]`), so any of them can be remapped from the bar's own gesture settings; scroll cycles providers @@ -44,6 +45,18 @@ Add the `dns-switcher` widget from Noctalia's widget picker. Default gestures: | Scroll | Cycle to the next/previous configured provider | All three are bar-level defaults and can be remapped from *Settings → Bar*. + +In the panel, right-click a provider to open its row menu: + +| Entry | Effect | +| --- | --- | +| **Apply this provider** | Same as a left click. On the active row it applies the profile again. | +| **Copy these addresses** | Copies that provider's addresses to the clipboard. | +| **Look up *name* through this resolver** | Sends the hostname from the *DNS lookup* box to that provider. It does not change the system DNS. | + +The lookup entry needs a valid hostname in the box, and a provider that has its +own addresses. It is disabled for *Default (ISP)*. + The panel itself, and the plugin's settings page, also open from the CLI: ```sh @@ -75,9 +88,13 @@ neighbouring provider (what scroll sends). ## Requirements -- noctalia v5.0.0-beta.7 or newer (`plugin_api = 17`, for the `onExit` - lifecycle cleanup in `service.luau`) +- noctalia v5.0.0-beta.9 or newer — the first release that accepts + `plugin_api = 28`. The plugin needs 28 for the provider row menu + (`panel.openContextMenu`), and 24 for argv process execution: every command + it runs is an argument vector, so no shell parses a DNS address, a hostname + or the privilege command. On beta.8 the plugin store keeps serving 0.1.2 - NetworkManager (`networkmanager`, provides `nmcli`) with an active connection +- `env` (coreutils) — runs `nmcli` under `LC_ALL=C` - Permission to modify system connections (see *Privileges* below) - `dig` (bind-tools/dnsutils) or `nslookup`, optional — only the lookup tester needs one of them; the rest of the plugin works without either @@ -90,7 +107,10 @@ password. If you get a "not authorized" error, set it to `pkexec` (shows noctalia's own polkit prompt) or `sudo -n` with a matching sudoers rule. The privilege command is applied to the `nmcli con mod` and `nmcli device reapply` calls individually — never to a wrapping shell — so the sudoers -rule only ever needs to name `nmcli` itself: +rule only ever needs to name `nmcli` itself. It is split on whitespace into +separate arguments (`sudo -n` is two), and `nmcli` stays the program it is +asked to run, which is what the rule below matches on; a privilege command +whose own path contains spaces is not supported — use a wrapper script. ``` # /etc/sudoers.d/nmcli-dns diff --git a/dns-switcher/panel.luau b/dns-switcher/panel.luau index 03d5d36f..7381ec97 100644 --- a/dns-switcher/panel.luau +++ b/dns-switcher/panel.luau @@ -3,6 +3,11 @@ -- service entry (service.luau) publishes "dns_state" and executes the -- "apply_request" entries this panel emits. Picking a provider applies it -- immediately (one nmcli change, no reactivation). +-- +-- Right-clicking a provider raises a native context menu (plugin_api >= 28): +-- apply it, copy its addresses, or send the name in the lookup box THROUGH it +-- without switching to it — which is the question the panel could not answer +-- before, since the tester only ever queried the resolver already in use. local STATE_KEY = "dns_state" -- published by service.luau local REQUEST_KEY = "apply_request" -- consumed by service.luau @@ -43,10 +48,6 @@ local function trim(value) return (value:gsub("^%s+", ""):gsub("%s+$", "")) end -local function shellQuote(value) - return "'" .. value:gsub("'", "'\\''") .. "'" -end - -- The nonce is monotonic across writers (widget instances and the panel): -- each seeds from the last request already in the shared state. local function requestApply(entry) @@ -59,9 +60,10 @@ local function requestApply(entry) end -- A conservative hostname shape (letters/digits/dot/hyphen, no leading dot or --- hyphen, 253 chars max — the DNS wire-format limit). shellQuote() below is --- the actual safety net; this only keeps an obviously-wrong query from ever --- reaching a shell as a "valid enough" no-op. +-- hyphen, 253 chars max — the DNS wire-format limit). Since 0.2.0 the lookup +-- runs as an argv (plugin_api >= 24), so there is no shell to defend against +-- and no quoting to get right: the name is one argument, whatever is in it. +-- This check is now only about not sending an obviously-wrong query at all. local function isValidHostname(name) return name ~= "" and #name <= 253 and name:match("^[%w][%w%.%-]*$") ~= nil end @@ -97,7 +99,11 @@ end -- pointed at the active provider's own address when it has one, so the -- answer reflects that resolver specifically rather than whatever the system -- resolver layer (systemd-resolved, etc.) does with it. -local function runResolve() +-- +-- `serverOverride` ({ ip, label }) aims the query at a provider that is NOT +-- active — the row menu's lookup entry — which is a read-only question about +-- that resolver and changes no configuration at all. +local function runResolve(serverOverride) local name = trim(resolveQuery) if not isValidHostname(name) then resolveError = tr("resolve_invalid") @@ -106,7 +112,7 @@ local function runResolve() return end - local server = activeServerInfo() + local server = serverOverride or activeServerInfo() local useDig = noctalia.commandExists("dig") local useNslookup = not useDig and noctalia.commandExists("nslookup") if not useDig and not useNslookup then @@ -124,11 +130,16 @@ local function runResolve() local tool = useDig and "dig" or "nslookup" local cmd if useDig then - cmd = "dig +time=3 +tries=1 +short " - .. (server ~= nil and ("@" .. shellQuote(server.ip) .. " ") or "") - .. shellQuote(name) + cmd = { "dig", "+time=3", "+tries=1", "+short" } + if server ~= nil then + table.insert(cmd, "@" .. server.ip) + end + table.insert(cmd, name) else - cmd = "nslookup " .. shellQuote(name) .. (server ~= nil and (" " .. shellQuote(server.ip)) or "") + cmd = { "nslookup", name } + if server ~= nil then + table.insert(cmd, server.ip) + end end local ok = noctalia.runAsync(cmd, function(result) @@ -169,6 +180,39 @@ local function runResolve() end end +-- The provider row's right-click menu (plugin_api >= 28). Only a ui.button's +-- onRightClick reports the pointer serial openContextMenu needs, and the row IS +-- a button, so it raises this itself. `onActivate` must NAME a global +-- (openContextMenu is a raw binding, not a UI tree prop, so closures are not +-- registered for it); the provider id rides along in `context` and onProviderMenu +-- looks the entry up again in the current snapshot rather than capturing it. +-- +-- Apply stays enabled on the active row too: `nmcli con mod` + `device reapply` +-- is idempotent, so re-applying is a real action (it pushes the profile back +-- onto the live connection). That also guarantees the one enabled entry the +-- host requires, whatever the other two are doing. +local function openProviderMenu(entry, active, text) + local hasIp = type(entry.ip) == "string" and entry.ip ~= "" + local name = trim(resolveQuery) + panel.openContextMenu({ + onActivate = "onProviderMenu", + context = entry.id, + items = { + { kind = "header", label = text }, + { id = "apply", label = tr(active and "menu.reapply" or "menu.apply") }, + { id = "copy", label = tr("menu.copy"), enabled = hasIp }, + -- Named after the hostname it would send, so the entry says what + -- it does; with nothing usable in the box it turns into the hint + -- for how to make it work, disabled. + { + id = "test", + label = isValidHostname(name) and tr("menu.test", { name = name }) or tr("menu.test_hint"), + enabled = hasIp and isValidHostname(name), + }, + }, + }) +end + -- A row names its provider and the addresses it would set, so picking one is not -- a guess about what it does. The ISP default has no fixed addresses -- whatever -- the LAN hands out -- so it stays a bare label, and the footer shows what is @@ -191,9 +235,20 @@ local function providerRow(entry) requestApply(entry) end end, + onRightClick = function() + openProviderMenu(entry, active, text) + end, }) end +local function copyServers(text) + if text == nil or text == "" then + return + end + noctalia.copyToClipboard(text, "text/plain") + noctalia.notify(tr("title"), tr("copied", { ip = text })) +end + local function statusFooter() if snapshot.changing == true then return ui.label({ text = tr("status_switching"), fontSize = 11, color = "secondary" }) @@ -326,11 +381,36 @@ function onCopyServers() if (text == nil or text == "") and snapshot.current ~= nil then text = snapshot.current.ip end - if text == nil or text == "" then + copyServers(text) +end + +-- Dispatch for a provider row's context menu. `action` is the id of the entry +-- picked, `context` the provider id it was opened on — looked up again here, +-- since a poll may have replaced the whole list in between. +function onProviderMenu(action, context) + if snapshot == nil or type(snapshot.list) ~= "table" then return end - noctalia.copyToClipboard(text, "text/plain") - noctalia.notify(tr("title"), tr("copied", { ip = text })) + local entry = nil + for _, candidate in ipairs(snapshot.list) do + if candidate.id == context then + entry = candidate + break + end + end + if entry == nil then + return + end + if action == "apply" then + requestApply(entry) + elseif action == "copy" then + copyServers(entry.ip) + elseif action == "test" then + local ip = type(entry.ip) == "string" and entry.ip:match("%S+") or nil + if ip ~= nil then + runResolve({ ip = ip, label = entry.label }) + end + end end -- Opens the settings window on this plugin's own page (the host supplies the diff --git a/dns-switcher/plugin.toml b/dns-switcher/plugin.toml index a8c5a37b..1cd9214e 100644 --- a/dns-switcher/plugin.toml +++ b/dns-switcher/plugin.toml @@ -3,17 +3,25 @@ # widget shows the active provider and toggles a panel listing the configured # providers; picking one applies it immediately via `nmcli con mod` + # `nmcli device reapply` (no reactivation, the connection never drops). +# Right-click a provider to apply it, copy its addresses, or run the panel's +# DNS lookup through it without switching to it. id = "nightwatch75/dns-switcher" name = "DNS Switcher" -version = "0.1.2" -plugin_api = 17 +version = "0.3.0" +# 28 for panel.openContextMenu: the provider row's right-click menu. Also 24 +# for direct argv process execution — every command this plugin runs is an +# argument vector, so no shell ever parses a DNS address, a hostname or the +# privilege command. +plugin_api = 28 author = "nightwatch75" license = "MIT" # dig (bind-tools/dnsutils) is preferred for the panel's lookup tester; # nslookup is the fallback when dig is missing. Neither is required for the # core switch/apply feature, only for that one panel section. -dependencies = ["networkmanager", "dig", "nslookup"] +# `env` runs nmcli under LC_ALL=C: with no shell in the picture it is the only +# way left to fix the locale, and apply() reads nmcli's stderr. +dependencies = ["networkmanager", "env", "dig", "nslookup"] tags = ["bar", "panel", "service", "network", "privacy"] icon = "world" description = "Switch the system DNS between popular providers, custom servers, or the ISP default (NetworkManager)." diff --git a/dns-switcher/service.luau b/dns-switcher/service.luau index 20410ba5..79b88d20 100644 --- a/dns-switcher/service.luau +++ b/dns-switcher/service.luau @@ -18,6 +18,11 @@ -- of those two nmcli calls individually (never onto a wrapping shell), so a -- sudoers NOPASSWD rule naming the nmcli binary itself is enough — see -- apply() below and the README's Privileges section. +-- +-- Since 0.2.0 every command here is an argument vector rather than a shell +-- line (plugin_api >= 24), so there is no shell to quote for anywhere in this +-- plugin. The detection and apply pipelines that used to be one `sh -c` script +-- each are now chains of individual nmcli calls parsed in Luau. local STATE_KEY = "dns_state" -- published here, read by widget + panel local REQUEST_KEY = "apply_request" -- sent by widget/panel, consumed here @@ -177,41 +182,62 @@ local function publish() }) end --- Picks the active connection: prefer wifi/ethernet, else the first --- non-loopback entry. Emits KEY=value lines parsed by the poll callback; --- UUID (colon-free) identifies the connection, DEV drives the reapply. -local DETECT_CMD = [[ -ACT=$(LC_ALL=C nmcli -t -f TYPE,DEVICE,UUID,NAME connection show --active 2>/dev/null) -LINE=$(printf '%s\n' "$ACT" | grep -E '^(802-11-wireless|802-3-ethernet):' | head -n 1) -[ -n "$LINE" ] || LINE=$(printf '%s\n' "$ACT" | grep -v '^loopback:' | head -n 1) -[ -n "$LINE" ] || { echo 'ERR=noconn'; exit 0; } -DEV=$(printf '%s' "$LINE" | cut -d: -f2) -UUID=$(printf '%s' "$LINE" | cut -d: -f3) -echo "NAME=$(printf '%s' "$LINE" | cut -d: -f4-)" -echo "CFG=$(LC_ALL=C nmcli -g ipv4.dns connection show uuid "$UUID" 2>/dev/null)" -echo "AUTO=$(LC_ALL=C nmcli -g ipv4.ignore-auto-dns connection show uuid "$UUID" 2>/dev/null)" -echo "RUN=$(nmcli -g IP4.DNS device show "$DEV" 2>/dev/null | tr '\n' ' ')" -]] - -local function updateDnsState(stdout) - local fields = {} +-- Every command below is an argument vector, never a shell line: plugin_api +-- >= 24 lets noctalia.runAsync take the argv directly and exec it, so nothing +-- this plugin runs is ever parsed by a shell. That is what the privileged path +-- is really about — a sudoers NOPASSWD rule naming /usr/bin/nmcli authorizes +-- the binary, and there is now no `sh -c` anywhere for it to fail to cover. +-- `env LC_ALL=C` replaces what the old shell line set: nmcli's field values are +-- locale-independent, but its stderr is not, and apply() below reads it. +local function nmcli(...) + return { "env", "LC_ALL=C", "nmcli", ... } +end + +-- One row of `nmcli -t -f TYPE,DEVICE,UUID,NAME connection show --active`. +-- TYPE, DEVICE and UUID never contain a colon; NAME can, so it takes the rest +-- of the line — the same split the previous `cut -d: -f4-` did. +local function parseConnectionRow(line) + local kind, device, uuid, name = line:match("^([^:]*):([^:]*):([^:]*):(.*)$") + if uuid == nil or uuid == "" then + return nil + end + return { kind = kind, device = device, uuid = uuid, name = name } +end + +-- The connection to act on: prefer wifi/ethernet, else the first non-loopback +-- entry. Was a grep/head pipeline inside the detect script; in Luau it is the +-- one rule both detection and apply() go through, so the two can no longer +-- disagree about which connection is "the" one. +local function pickConnection(stdout) + local fallback = nil for line in stdout:gmatch("[^\n]+") do - local key, value = line:match("^(%u+)=(.*)$") - if key ~= nil then - fields[key] = value + local row = parseConnectionRow(line) + if row ~= nil then + if row.kind == "802-11-wireless" or row.kind == "802-3-ethernet" then + return row + end + if fallback == nil and row.kind ~= "loopback" then + fallback = row + end end end + return fallback +end - if fields.ERR == "noconn" then +-- `cfg` is the profile's ipv4.dns, `auto` its ipv4.ignore-auto-dns, `run` the +-- device's live IP4.DNS. Split out of the old KEY=value echo protocol: the +-- three now arrive as the plain stdout of three nmcli calls. +local function updateDnsState(con, cfgDns, auto, run) + if con == nil then current = nil errMsg = tr("err_no_connection") return end errMsg = nil - conName = fields.NAME or "" + conName = con.name or "" local runtime = {} - for token in (fields.RUN or ""):gmatch("%d+%.%d+%.%d+%.%d+") do + for token in (run or ""):gmatch("%d+%.%d+%.%d+%.%d+") do if isValidIp(token) then table.insert(runtime, token) end @@ -220,9 +246,9 @@ local function updateDnsState(stdout) -- Manual DNS lives in the profile (ipv4.dns + ignore-auto-dns yes); -- anything else is the connection default, whatever the LAN hands out. - local manual = (fields.AUTO == "yes") + local manual = (trim(auto or "") == "yes") local cfgIps = {} - for token in (fields.CFG or ""):gmatch("%d+%.%d+%.%d+%.%d+") do + for token in (cfgDns or ""):gmatch("%d+%.%d+%.%d+%.%d+") do if isValidIp(token) then table.insert(cfgIps, token) end @@ -259,23 +285,64 @@ local function updateDnsState(stdout) } end +-- Run one nmcli query and hand its stdout on, or nil when it could not be +-- asked. Every detection step below has the same shape, and none of them is +-- fatal on its own: a missing answer just leaves that field empty. +local function query(argv, done) + local ok = noctalia.runAsync(argv, function(result) + if result.exitCode == 0 and not result.timedOut then + done(result.stdout or "") + else + done(nil) + end + end, 4000) + if not ok then + done(nil) + end +end + +-- Detection, as three chained nmcli calls: the active connections, then the +-- chosen one's profile DNS settings, then the DNS its device is actually +-- using. It was one shell script with an echo protocol between the two halves; +-- chained argv calls cost two extra round trips on a poll that runs every +-- pollSeconds() and buys the shell being gone from the plugin entirely. +-- +-- `checkInFlight` spans the whole chain, so a poll tick landing mid-chain is +-- dropped rather than starting a second one. local function pollNow() if checkInFlight or changing or nmcliMissing then return end checkInFlight = true - local ok = noctalia.runAsync(DETECT_CMD, function(result) + local function finish() checkInFlight = false - if result.exitCode == 0 and not result.timedOut then - updateDnsState(result.stdout) - elseif current == nil then - errMsg = tr("status_no_nmcli") - end publish() - end, 4000) - if not ok then - checkInFlight = false end + query(nmcli("-t", "-f", "TYPE,DEVICE,UUID,NAME", "connection", "show", "--active"), function(active) + if active == nil then + -- nmcli itself did not answer; keep whatever was last detected + -- rather than claiming there is no connection. + if current == nil then + errMsg = tr("status_no_nmcli") + end + finish() + return + end + local con = pickConnection(active) + if con == nil then + updateDnsState(nil) + finish() + return + end + query(nmcli("-g", "ipv4.dns,ipv4.ignore-auto-dns", "connection", "show", "uuid", con.uuid), function(profile) + -- Two -g fields, one value per line and in the order asked for. + local cfgDns, auto = (profile or ""):match("^([^\n]*)\n([^\n]*)") + query(nmcli("-g", "IP4.DNS", "device", "show", con.device), function(run) + updateDnsState(con, cfgDns, auto, run) + finish() + end) + end) + end) end local function apply(provider) @@ -292,47 +359,60 @@ local function apply(provider) return end - -- Safety net mirroring the v4 plugin: the spec is already validated, the - -- gsub guarantees nothing shell-relevant ever reaches the command line. - local safeIp = provider.ip:gsub("[^%d%. ]", "") + -- The DNS list reaches nmcli as one argv element, so it is never split, + -- globbed or re-parsed on the way. validDnsSpec above already limits it to + -- one or two dotted quads; this is the value nmcli stores verbatim. local mods - if safeIp == "" then - mods = 'ipv4.dns "" ipv4.ignore-auto-dns no' + if provider.ip == "" then + mods = { "ipv4.dns", "", "ipv4.ignore-auto-dns", "no" } else - mods = 'ipv4.dns "' .. safeIp .. '" ipv4.ignore-auto-dns yes' + mods = { "ipv4.dns", provider.ip, "ipv4.ignore-auto-dns", "yes" } end + -- The privilege command becomes the leading argv elements of each mutating + -- call, split on whitespace: "sudo", "pkexec", "sudo -n" all work; a path + -- with spaces in it does not, and a wrapper script is the answer there. + -- Prefixed onto each nmcli invocation individually, never onto a wrapping + -- shell — the README's sudoers example authorizes the nmcli binary itself + -- (NOPASSWD: /usr/bin/nmcli), which would never cover a shell run under + -- sudo. Discovering the device/uuid stays unprivileged either way (it is a + -- plain read), so only the two mutating calls carry it. local priv = cfg("privilege_command") if type(priv) ~= "string" then priv = "" end - priv = trim(priv) - -- Prefixed onto each nmcli invocation individually, never onto a - -- wrapping `sh -c`: the README's sudoers example authorizes the nmcli - -- binary itself (NOPASSWD: /usr/bin/nmcli), which never covers a shell - -- run under sudo. Discovering the device/uuid stays unprivileged either - -- way (it's a plain read), so only the two mutating calls need it. - local privPrefix = priv ~= "" and (priv .. " ") or "" - - local cmd = 'ACT=$(LC_ALL=C nmcli -t -f TYPE,DEVICE,UUID connection show --active 2>/dev/null); ' - .. [[LINE=$(printf '%s\n' "$ACT" | grep -E '^(802-11-wireless|802-3-ethernet):' | head -n 1); ]] - .. [=[[ -n "$LINE" ] || LINE=$(printf '%s\n' "$ACT" | grep -v '^loopback:' | head -n 1); ]=] - .. [=[[ -n "$LINE" ] || exit 9; ]=] - .. 'DEV=$(printf \'%s\' "$LINE" | cut -d: -f2); ' - .. 'UUID=$(printf \'%s\' "$LINE" | cut -d: -f3); ' - .. privPrefix .. 'nmcli con mod "$UUID" ' .. mods .. ' && ' .. privPrefix .. 'nmcli device reapply "$DEV"' + local privArgv = {} + for word in priv:gmatch("%S+") do + table.insert(privArgv, word) + end + + -- `env LC_ALL=C` goes in FRONT of the privilege command, not between it and + -- nmcli: sudo and pkexec match their policy against the program they are + -- asked to run, so `sudo env … nmcli` is a request to run /usr/bin/env and + -- the README's `NOPASSWD: /usr/bin/nmcli` rule would stop matching. This + -- way the privileged program stays nmcli itself. The locale still reaches + -- it under sudo, whose default sudoers keeps LC_* (`env_keep`); pkexec + -- scrubs the environment and nmcli's stderr comes back localized there, + -- which only costs the message match below its shortcut — the raw stderr + -- is still shown. + local function privileged(...) + local argv = { "env", "LC_ALL=C" } + for _, word in ipairs(privArgv) do + table.insert(argv, word) + end + table.insert(argv, "nmcli") + for _, word in ipairs({ ... }) do + table.insert(argv, word) + end + return argv + end - changing = true - publish() - -- 60s budget so an eventual polkit password prompt can be answered. - local ok = noctalia.runAsync(cmd, function(result) + local function failed(result) changing = false - if result.exitCode == 0 and not result.timedOut then - noctalia.notify(tr("title"), tr("applied", { name = provider.label })) + if result == nil then + noctalia.notifyError(tr("title"), tr("err_spawn")) elseif result.timedOut then noctalia.notifyError(tr("title"), tr("err_timeout")) - elseif result.exitCode == 9 then - noctalia.notifyError(tr("title"), tr("err_no_connection")) elseif result.exitCode == 126 then noctalia.notifyError(tr("title"), tr("err_auth_dismissed")) else @@ -350,11 +430,65 @@ local function apply(provider) end publish() pollNow() - end, 60000) + end + + changing = true + publish() + + -- Three steps where there used to be one shell line: find the connection, + -- write the profile, push it onto the live device. The 60s budget is on + -- each of the two privileged calls, so a polkit password prompt has the + -- same room to be answered as before. + local ok = noctalia.runAsync( + nmcli("-t", "-f", "TYPE,DEVICE,UUID,NAME", "connection", "show", "--active"), + function(listed) + if listed.exitCode ~= 0 or listed.timedOut then + failed(listed) + return + end + local con = pickConnection(listed.stdout or "") + if con == nil then + changing = false + noctalia.notifyError(tr("title"), tr("err_no_connection")) + publish() + pollNow() + return + end + local modArgv = privileged("con", "mod", con.uuid) + for _, word in ipairs(mods) do + table.insert(modArgv, word) + end + local modOk = noctalia.runAsync(modArgv, function(modded) + if modded.exitCode ~= 0 or modded.timedOut then + failed(modded) + return + end + -- reapply pushes the profile onto the live connection without + -- reactivating it, so the network never drops. + local reapplyOk = noctalia.runAsync( + privileged("device", "reapply", con.device), + function(reapplied) + if reapplied.exitCode ~= 0 or reapplied.timedOut then + failed(reapplied) + return + end + changing = false + noctalia.notify(tr("title"), tr("applied", { name = provider.label })) + publish() + pollNow() + end, 60000 + ) + if not reapplyOk then + failed(nil) + end + end, 60000) + if not modOk then + failed(nil) + end + end, 4000 + ) if not ok then - changing = false - noctalia.notifyError(tr("title"), tr("err_spawn")) - publish() + failed(nil) end end diff --git a/dns-switcher/translations/en.json b/dns-switcher/translations/en.json index 7495c51d..e19102c3 100644 --- a/dns-switcher/translations/en.json +++ b/dns-switcher/translations/en.json @@ -10,6 +10,13 @@ "err_spawn": "Could not run nmcli", "err_timeout": "Timed out applying the DNS change (authorization prompt left unanswered?)", "err_unknown_provider": "Unknown provider id: {id}", + "menu": { + "apply": "Apply this provider", + "copy": "Copy these addresses", + "reapply": "Re-apply this provider", + "test": "Look up {name} through this resolver", + "test_hint": "Type a hostname below to look it up here" + }, "resolve_busy": "Resolving…", "resolve_empty": "No records found", "resolve_invalid": "Enter a valid hostname",