Group Mullvad exit nodes by country
This commit is contained in:
@@ -35,12 +35,19 @@ function displayHostName(hostName, dnsName) {
|
||||
return shortDnsName(dnsName) || host || "Unknown"
|
||||
}
|
||||
|
||||
function isMullvadPeer(peer) {
|
||||
var hostName = String((peer && peer.HostName) || "").toLowerCase()
|
||||
var dnsName = cleanDnsName((peer && peer.DNSName) || "").toLowerCase()
|
||||
return dnsName.indexOf(".mullvad.ts.net") !== -1 || hostName.indexOf(".mullvad.ts.net") !== -1
|
||||
}
|
||||
|
||||
function osIcon(os) {
|
||||
var value = String(os || "").toLowerCase()
|
||||
if (value === "linux") return ""
|
||||
if (value === "macos" || value === "ios") return ""
|
||||
if (value === "windows") return ""
|
||||
if (value === "android") return ""
|
||||
if (value === "mullvad") return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -57,16 +64,113 @@ function peerFromStatus(id, peer) {
|
||||
id: id,
|
||||
HostName: displayHostName(peer.HostName, peer.DNSName),
|
||||
DNSName: cleanDnsName(peer.DNSName),
|
||||
DisplayName: displayHostName(peer.HostName, peer.DNSName),
|
||||
TailscaleIPs: filterIPv4(peer.TailscaleIPs || []),
|
||||
TailscaleIPv6: filterIPv6(peer.TailscaleIPs || []),
|
||||
Online: peer.Online === true,
|
||||
OS: String(peer.OS || ""),
|
||||
Tags: peer.Tags || [],
|
||||
ExitNodeOption: peer.ExitNodeOption === true,
|
||||
ExitNode: peer.ExitNode === true
|
||||
ExitNode: peer.ExitNode === true,
|
||||
Mullvad: isMullvadPeer(peer)
|
||||
}
|
||||
}
|
||||
|
||||
function sliceTableColumn(line, start, end) {
|
||||
var text = String(line || "")
|
||||
if (start < 0 || start >= text.length) return ""
|
||||
if (end < 0) return text.substring(start).trim()
|
||||
return text.substring(start, Math.min(end, text.length)).trim()
|
||||
}
|
||||
|
||||
function parseExitNodeList(raw) {
|
||||
var lines = String(raw || "").split(/\r?\n/)
|
||||
var header = ""
|
||||
var headerIndex = -1
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (/^\s*IP\s+HOSTNAME\s+COUNTRY\s+CITY\s+STATUS\s*$/.test(lines[i])) {
|
||||
header = lines[i]
|
||||
headerIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (headerIndex === -1) return []
|
||||
|
||||
var ipStart = header.indexOf("IP")
|
||||
var hostStart = header.indexOf("HOSTNAME")
|
||||
var countryStart = header.indexOf("COUNTRY")
|
||||
var cityStart = header.indexOf("CITY")
|
||||
var statusStart = header.indexOf("STATUS")
|
||||
var byHost = {}
|
||||
|
||||
for (var j = headerIndex + 1; j < lines.length; j++) {
|
||||
var line = lines[j]
|
||||
if (/^\s*$/.test(line) || /^\s*#/.test(line)) continue
|
||||
|
||||
var ip = sliceTableColumn(line, ipStart, hostStart)
|
||||
var host = sliceTableColumn(line, hostStart, countryStart)
|
||||
var country = sliceTableColumn(line, countryStart, cityStart)
|
||||
var city = sliceTableColumn(line, cityStart, statusStart)
|
||||
var status = sliceTableColumn(line, statusStart, -1)
|
||||
if (host.indexOf(".mullvad.ts.net") === -1) continue
|
||||
|
||||
byHost[host] = {
|
||||
id: "mullvad:" + host,
|
||||
HostName: host,
|
||||
DNSName: host,
|
||||
DisplayName: (city && city !== "Any" ? city + ", " : "") + country,
|
||||
TailscaleIPs: ip ? [ip] : [],
|
||||
TailscaleIPv6: [],
|
||||
Online: true,
|
||||
OS: "mullvad",
|
||||
Tags: [],
|
||||
ExitNodeOption: true,
|
||||
ExitNode: status !== "" && status !== "-",
|
||||
Mullvad: true,
|
||||
Country: country,
|
||||
City: city,
|
||||
Status: status
|
||||
}
|
||||
}
|
||||
|
||||
var result = []
|
||||
for (var hostName in byHost) result.push(byHost[hostName])
|
||||
result.sort(function(a, b) {
|
||||
var countryCompare = String(a.Country).localeCompare(String(b.Country))
|
||||
if (countryCompare !== 0) return countryCompare
|
||||
return String(a.DisplayName).localeCompare(String(b.DisplayName))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function mullvadCountryOptions(nodes) {
|
||||
var byCountry = {}
|
||||
var values = Array.isArray(nodes) ? nodes : []
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var node = values[i] || {}
|
||||
if (node.Mullvad !== true) continue
|
||||
var country = String(node.Country || "").trim()
|
||||
if (country === "") continue
|
||||
var current = byCountry[country]
|
||||
if (!current || node.City === "Any") {
|
||||
var option = {}
|
||||
for (var key in node) option[key] = node[key]
|
||||
option.id = "mullvad-country:" + country
|
||||
option.DisplayName = country
|
||||
option.Country = country
|
||||
option.MullvadCountry = true
|
||||
byCountry[country] = option
|
||||
}
|
||||
}
|
||||
|
||||
var result = []
|
||||
for (var name in byCountry) result.push(byCountry[name])
|
||||
result.sort(function(a, b) {
|
||||
return String(a.Country).localeCompare(String(b.Country))
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
function parseStatus(raw) {
|
||||
var text = String(raw || "").trim()
|
||||
if (text === "") return { ok: true, unavailable: true, message: "Disconnected" }
|
||||
@@ -83,6 +187,7 @@ function parseStatus(raw) {
|
||||
for (var id in rawPeers) {
|
||||
var peer = rawPeers[id] || {}
|
||||
var normalized = peerFromStatus(id, peer)
|
||||
if (normalized.Mullvad) continue
|
||||
if (normalized.Online) {
|
||||
peers.push(normalized)
|
||||
if (normalized.ExitNodeOption) exitNodes.push(normalized)
|
||||
@@ -155,7 +260,10 @@ if (typeof module !== "undefined") {
|
||||
displayHostName: displayHostName,
|
||||
osIcon: osIcon,
|
||||
accountLabel: accountLabel,
|
||||
isMullvadPeer: isMullvadPeer,
|
||||
peerFromStatus: peerFromStatus,
|
||||
parseExitNodeList: parseExitNodeList,
|
||||
mullvadCountryOptions: mullvadCountryOptions,
|
||||
parseStatus: parseStatus,
|
||||
parseAccounts: parseAccounts
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ Panel {
|
||||
property int exitNodeIndex: 0
|
||||
property bool cursorActive: false
|
||||
property bool copyMenuOpen: false
|
||||
property bool mullvadPickerOpen: false
|
||||
property string mullvadQuery: ""
|
||||
property int phraseIndex: 0
|
||||
readonly property var activePhrases: [
|
||||
"Encrypting connections",
|
||||
@@ -40,8 +42,11 @@ Panel {
|
||||
readonly property string fontFamily: bar ? bar.fontFamily : Style.font.family
|
||||
readonly property bool showConnections: tailscale.accounts.length > 1 || tailscale.accountsAccessDenied
|
||||
readonly property bool showPeers: tailscale.running && tailscale.peers.length > 0
|
||||
readonly property var exitNodes: tailscale.exitNodes
|
||||
readonly property bool showExitNodes: tailscale.running && exitNodes.length > 0
|
||||
readonly property var recentMullvadCountries: Array.isArray(settings.recentMullvadCountries) ? settings.recentMullvadCountries : []
|
||||
readonly property var recentMullvadExitNodes: recentMullvadNodes()
|
||||
readonly property var exitNodes: displayExitNodes()
|
||||
readonly property bool showExitNodes: tailscale.running && (exitNodes.length > 0 || tailscale.mullvadCountries.length > 0)
|
||||
readonly property var filteredMullvadCountries: filteredMullvadCountryNodes()
|
||||
readonly property color iconColor: tailscale.running ? foreground : dim
|
||||
readonly property color barIconColor: tailscale.running ? barForeground : Qt.darker(barForeground, 1.55)
|
||||
readonly property color hoverFill: bar ? Style.hoverFillFor(bar.foreground, Color.accent) : "transparent"
|
||||
@@ -57,6 +62,83 @@ Panel {
|
||||
return exitNodes[Math.max(0, Math.min(exitNodeIndex, exitNodes.length - 1))]
|
||||
}
|
||||
|
||||
function displayExitNodes() {
|
||||
var nodes = []
|
||||
for (var i = 0; i < tailscale.tailnetExitNodes.length; i++) nodes.push(tailscale.tailnetExitNodes[i])
|
||||
for (var j = 0; j < recentMullvadExitNodes.length; j++) nodes.push(recentMullvadExitNodes[j])
|
||||
if (tailscale.mullvadCountries.length > 0) nodes.push({ id: "mullvad:add", AddMullvad: true, DisplayName: "Choose Mullvad region" })
|
||||
return nodes
|
||||
}
|
||||
|
||||
function recentMullvadNodes() {
|
||||
var nodes = []
|
||||
var seen = {}
|
||||
for (var a = 0; a < tailscale.mullvadCountries.length && nodes.length < 5; a++) {
|
||||
var active = tailscale.mullvadCountries[a]
|
||||
var activeCountry = String(active.Country || "")
|
||||
if (active.ExitNode === true && activeCountry !== "" && !seen[activeCountry]) {
|
||||
nodes.push(active)
|
||||
seen[activeCountry] = true
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < recentMullvadCountries.length && nodes.length < 5; i++) {
|
||||
var country = String(recentMullvadCountries[i] || "")
|
||||
if (country === "" || seen[country]) continue
|
||||
var node = mullvadCountryNode(country)
|
||||
if (node) {
|
||||
nodes.push(node)
|
||||
seen[country] = true
|
||||
}
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
function mullvadCountryNode(country) {
|
||||
for (var i = 0; i < tailscale.mullvadCountries.length; i++) {
|
||||
var node = tailscale.mullvadCountries[i]
|
||||
if (String(node.Country || "") === String(country || "")) return node
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function filteredMullvadCountryNodes() {
|
||||
var query = String(mullvadQuery || "").trim().toLowerCase()
|
||||
var result = []
|
||||
for (var i = 0; i < tailscale.mullvadCountries.length; i++) {
|
||||
var node = tailscale.mullvadCountries[i]
|
||||
var label = String(node.DisplayName || node.Country || "").toLowerCase()
|
||||
if (query === "" || label.indexOf(query) !== -1) result.push(node)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function persistRecentMullvad(country) {
|
||||
var name = String(country || "")
|
||||
if (name === "") return
|
||||
var next = [name]
|
||||
for (var i = 0; i < recentMullvadCountries.length && next.length < 5; i++) {
|
||||
var existing = String(recentMullvadCountries[i] || "")
|
||||
if (existing !== "" && existing !== name && next.indexOf(existing) === -1) next.push(existing)
|
||||
}
|
||||
if (!root.bar || !root.bar.shell || typeof root.bar.shell.updateEntryInline !== "function") return
|
||||
var entry = { id: root.moduleName }
|
||||
for (var key in settings) if (key !== "id") entry[key] = settings[key]
|
||||
entry.recentMullvadCountries = next
|
||||
root.bar.shell.updateEntryInline(root.moduleName, entry)
|
||||
}
|
||||
|
||||
function chooseExitNode(peer) {
|
||||
if (!peer) return
|
||||
if (peer.AddMullvad === true) {
|
||||
mullvadPickerOpen = !mullvadPickerOpen
|
||||
if (mullvadPickerOpen) Qt.callLater(function() { if (mullvadSearch) mullvadSearch.forceActiveFocus() })
|
||||
return
|
||||
}
|
||||
if (peer.Mullvad === true) persistRecentMullvad(peer.Country)
|
||||
tailscale.setExitNode(peer)
|
||||
mullvadPickerOpen = false
|
||||
}
|
||||
|
||||
function selectedAccount() {
|
||||
if (tailscale.accounts.length === 0) return null
|
||||
return tailscale.accounts[Math.max(0, Math.min(accountIndex, tailscale.accounts.length - 1))]
|
||||
@@ -133,7 +215,7 @@ Panel {
|
||||
} else if (focusSection === "peers") {
|
||||
openSelectedPeerCopyMenu()
|
||||
} else if (focusSection === "exitNodes") {
|
||||
tailscale.setExitNode(selectedExitNode())
|
||||
chooseExitNode(selectedExitNode())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,6 +541,43 @@ Panel {
|
||||
rowIndex: index
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
visible: root.mullvadPickerOpen
|
||||
width: parent.width
|
||||
spacing: Style.space(6)
|
||||
|
||||
TextField {
|
||||
id: mullvadSearch
|
||||
width: parent.width
|
||||
foreground: root.foreground
|
||||
placeholderText: "Search countries"
|
||||
text: root.mullvadQuery
|
||||
onTextChanged: root.mullvadQuery = text
|
||||
onAccepted: {
|
||||
if (root.filteredMullvadCountries.length > 0) root.chooseExitNode(root.filteredMullvadCountries[0])
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.filteredMullvadCountries.length === 0
|
||||
width: parent.width
|
||||
text: "No Mullvad regions found."
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.filteredMullvadCountries
|
||||
MullvadCountryRow {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
peer: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,7 +790,7 @@ Panel {
|
||||
id: peerRow
|
||||
property var peer: null
|
||||
property int rowIndex: 0
|
||||
readonly property string peerName: peer ? String(peer.HostName || "Unknown") : "Unknown"
|
||||
readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || "Unknown") : "Unknown"
|
||||
readonly property string peerIp: peer && peer.TailscaleIPs && peer.TailscaleIPs.length > 0 ? String(peer.TailscaleIPs[0]) : ""
|
||||
readonly property string peerIpv6: {
|
||||
if (!peer || !peer.TailscaleIPv6 || peer.TailscaleIPv6.length === 0) return ""
|
||||
@@ -826,7 +945,7 @@ Panel {
|
||||
color: Color.background
|
||||
border.color: root.dim
|
||||
border.width: 1
|
||||
radius: Style.radius.md
|
||||
radius: Style.cornerRadius
|
||||
}
|
||||
|
||||
contentItem: Column {
|
||||
@@ -899,12 +1018,13 @@ Panel {
|
||||
id: exitNodeRow
|
||||
property var peer: null
|
||||
property int rowIndex: 0
|
||||
readonly property bool addMullvad: peer && peer.AddMullvad === true
|
||||
readonly property bool activeExitNode: peer && peer.ExitNode === true
|
||||
readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "")
|
||||
readonly property string peerName: peer ? String(peer.HostName || "Unknown") : "Unknown"
|
||||
readonly property string peerName: peer ? String(peer.DisplayName || peer.HostName || "Unknown") : "Unknown"
|
||||
|
||||
hasCursor: root.cursorActive && root.focusSection === "exitNodes" && root.exitNodeIndex === rowIndex
|
||||
current: activeExitNode || settingExitNode
|
||||
current: activeExitNode || settingExitNode || (addMullvad && root.mullvadPickerOpen)
|
||||
foreground: root.foreground
|
||||
fill: root.hoverFill
|
||||
currentFill: root.selectedFill
|
||||
@@ -922,8 +1042,8 @@ Panel {
|
||||
|
||||
Text {
|
||||
id: exitNodeGlyph
|
||||
text: ""
|
||||
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode ? root.foreground : root.dim
|
||||
text: exitNodeRow.addMullvad ? "+" : (peer && peer.Mullvad === true ? "" : "")
|
||||
color: exitNodeRow.activeExitNode || exitNodeRow.settingExitNode || exitNodeRow.addMullvad ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
width: Style.space(22)
|
||||
@@ -958,7 +1078,74 @@ Panel {
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: root.setExitNodeCursor(exitNodeRow.rowIndex)
|
||||
onClicked: if (exitNodeRow.peer) tailscale.setExitNode(exitNodeRow.peer)
|
||||
onClicked: root.chooseExitNode(exitNodeRow.peer)
|
||||
}
|
||||
}
|
||||
|
||||
component MullvadCountryRow: CursorSurface {
|
||||
id: countryRow
|
||||
|
||||
property var peer: null
|
||||
readonly property string countryName: peer ? String(peer.DisplayName || peer.Country || "Unknown") : "Unknown"
|
||||
readonly property bool activeExitNode: peer && peer.ExitNode === true
|
||||
readonly property bool settingExitNode: peer && tailscale.settingExitNodeId === String(peer.id || "")
|
||||
|
||||
foreground: root.foreground
|
||||
fill: root.hoverFill
|
||||
currentFill: root.selectedFill
|
||||
current: activeExitNode || settingExitNode
|
||||
implicitHeight: row.implicitHeight + Style.spacing.lg
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Style.space(6)
|
||||
anchors.rightMargin: Style.space(6)
|
||||
spacing: Style.space(8)
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: countryRow.current ? root.foreground : root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
width: Style.space(22)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - Style.space(30)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(1)
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: countryRow.countryName
|
||||
color: root.foreground
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.body
|
||||
font.bold: countryRow.activeExitNode
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: peer && peer.City === "Any" ? "Best available city" : String(peer && peer.City ? peer.City : "")
|
||||
color: root.dim
|
||||
font.family: root.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.chooseExitNode(countryRow.peer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ Item {
|
||||
property string authUrl: ""
|
||||
property var peers: []
|
||||
property var exitNodes: []
|
||||
property var tailnetExitNodes: []
|
||||
property var mullvadExitNodes: []
|
||||
property var mullvadCountries: []
|
||||
property var accounts: []
|
||||
property string selectedAccountId: ""
|
||||
property string selectedAccountLabel: ""
|
||||
@@ -31,13 +34,15 @@ Item {
|
||||
property string lastError: ""
|
||||
|
||||
readonly property int refreshIntervalSec: intSetting("refreshIntervalSec", 30, 5, 3600)
|
||||
readonly property bool busy: whichProcess.running || statusProcess.running || accountsProcess.running || actionProcess.running || loginProcess.running || switchProcess.running || operatorProcess.running || exitNodeProcess.running
|
||||
readonly property bool busy: whichProcess.running || statusProcess.running || mullvadExitNodesProcess.running || accountsProcess.running || actionProcess.running || loginProcess.running || switchProcess.running || operatorProcess.running || exitNodeProcess.running
|
||||
readonly property string userName: Quickshell.env("USER") || Quickshell.env("LOGNAME")
|
||||
|
||||
property string _statusOutput: ""
|
||||
property string _statusError: ""
|
||||
property string _accountsOutput: ""
|
||||
property string _accountsError: ""
|
||||
property string _mullvadExitNodesOutput: ""
|
||||
property string _mullvadExitNodesError: ""
|
||||
property string _actionOutput: ""
|
||||
property string _actionError: ""
|
||||
property string _loginOutput: ""
|
||||
@@ -133,6 +138,12 @@ Item {
|
||||
statusProcess.command = ["tailscale", "status", "--json"]
|
||||
statusProcess.running = true
|
||||
}
|
||||
if (!mullvadExitNodesProcess.running) {
|
||||
_mullvadExitNodesOutput = ""
|
||||
_mullvadExitNodesError = ""
|
||||
mullvadExitNodesProcess.command = ["tailscale", "exit-node", "list"]
|
||||
mullvadExitNodesProcess.running = true
|
||||
}
|
||||
var now = Date.now()
|
||||
var shouldRefreshAccounts = forceAccounts === true || accounts.length === 0 || now - _lastAccountsRefreshMs > 60000
|
||||
if (shouldRefreshAccounts && !accountsProcess.running) {
|
||||
@@ -160,6 +171,9 @@ Item {
|
||||
authUrl = ""
|
||||
peers = []
|
||||
exitNodes = []
|
||||
tailnetExitNodes = []
|
||||
mullvadExitNodes = []
|
||||
mullvadCountries = []
|
||||
accounts = []
|
||||
selectedAccountId = ""
|
||||
selectedAccountLabel = ""
|
||||
@@ -190,7 +204,8 @@ Item {
|
||||
selfDnsName = parsed.selfDnsName
|
||||
selfIp = parsed.selfIp
|
||||
peers = parsed.running ? parsed.peers : []
|
||||
exitNodes = parsed.running ? parsed.exitNodes : []
|
||||
tailnetExitNodes = parsed.running ? parsed.exitNodes : []
|
||||
exitNodes = parsed.running ? tailnetExitNodes.concat(mullvadCountries) : []
|
||||
|
||||
if (needsLogin) statusText = "Needs login"
|
||||
else if (running) {
|
||||
@@ -215,6 +230,12 @@ Item {
|
||||
accountsAccessDenied = false
|
||||
}
|
||||
|
||||
function parseMullvadExitNodes(raw) {
|
||||
mullvadExitNodes = Model.parseExitNodeList(raw)
|
||||
mullvadCountries = Model.mullvadCountryOptions(mullvadExitNodes)
|
||||
exitNodes = running ? tailnetExitNodes.concat(mullvadCountries) : []
|
||||
}
|
||||
|
||||
function toggleTailscale() {
|
||||
if (!installed) return
|
||||
if (running) runAction(["tailscale", "down"], "Turning Tailscale off…")
|
||||
@@ -397,6 +418,19 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: mullvadExitNodesProcess
|
||||
running: false
|
||||
command: []
|
||||
stdout: StdioCollector { id: mullvadExitNodesStdout; waitForEnd: true; onStreamFinished: root._mullvadExitNodesOutput = text }
|
||||
stderr: StdioCollector { id: mullvadExitNodesStderr; waitForEnd: true; onStreamFinished: root._mullvadExitNodesError = text }
|
||||
onExited: function(exitCode) {
|
||||
var stdout = String(mullvadExitNodesStdout.text || root._mullvadExitNodesOutput || "")
|
||||
if (exitCode === 0) root.parseMullvadExitNodes(stdout)
|
||||
else root.parseMullvadExitNodes("")
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: actionProcess
|
||||
running: false
|
||||
|
||||
Reference in New Issue
Block a user