mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
129 lines
3.2 KiB
Go
129 lines
3.2 KiB
Go
// Package network wraps nmcli to read and control network state
|
|
// (Wi-Fi, Ethernet, VPN). Every exported function shells out to nmcli;
|
|
// there is no direct D-Bus/NetworkManager integration.
|
|
package network
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
ErrNotFound = errors.New("nmcli not found in PATH")
|
|
ErrNotRunning = errors.New("NetworkManager is not running")
|
|
)
|
|
|
|
const defaultTimeout = 15 * time.Second
|
|
|
|
// CheckAvailable reports whether nmcli is installed and NetworkManager is
|
|
// reachable. Call it before using the rest of the package.
|
|
func CheckAvailable() error {
|
|
if _, err := exec.LookPath("nmcli"); err != nil {
|
|
return ErrNotFound
|
|
}
|
|
out, err := run("-t", "-f", "RUNNING", "general")
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", ErrNotRunning, err)
|
|
}
|
|
if strings.TrimSpace(out) != "running" {
|
|
return ErrNotRunning
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Installed reports whether nmcli is present in PATH, without probing
|
|
// NetworkManager. Cheap enough to call when deciding whether to show the
|
|
// Network page in the sidebar.
|
|
func Installed() bool {
|
|
_, err := exec.LookPath("nmcli")
|
|
return err == nil
|
|
}
|
|
|
|
func run(args ...string) (string, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "nmcli", args...) // #nosec G204 -- args are fixed nmcli subcommands or user-supplied SSID/password, never shell-interpreted
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
if errors.Is(err, exec.ErrNotFound) {
|
|
return "", ErrNotFound
|
|
}
|
|
msg := strings.TrimSpace(stderr.String())
|
|
if msg == "" {
|
|
msg = err.Error()
|
|
}
|
|
return "", fmt.Errorf("nmcli %s: %s", strings.Join(args, " "), msg)
|
|
}
|
|
return stdout.String(), nil
|
|
}
|
|
|
|
// getField runs `nmcli -g <field> ...` and returns the single trimmed value.
|
|
func getField(field string, args ...string) (string, error) {
|
|
out, err := run(append([]string{"-g", field}, args...)...)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.TrimSpace(out), nil
|
|
}
|
|
|
|
// runTerse runs nmcli with -t -f <fields> and parses each output line into
|
|
// its colon-separated columns, unescaping nmcli's `\:` and `\\`.
|
|
func runTerse(fields string, args ...string) ([][]string, error) {
|
|
out, err := run(append([]string{"-t", "-f", fields}, args...)...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = strings.TrimRight(out, "\n")
|
|
if out == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
lines := strings.Split(out, "\n")
|
|
rows := make([][]string, 0, len(lines))
|
|
for _, line := range lines {
|
|
rows = append(rows, parseTerseLine(line))
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func parseTerseLine(line string) []string {
|
|
var fields []string
|
|
var cur strings.Builder
|
|
escaped := false
|
|
for _, r := range line {
|
|
if escaped {
|
|
cur.WriteRune(r)
|
|
escaped = false
|
|
continue
|
|
}
|
|
switch r {
|
|
case '\\':
|
|
escaped = true
|
|
case ':':
|
|
fields = append(fields, cur.String())
|
|
cur.Reset()
|
|
default:
|
|
cur.WriteRune(r)
|
|
}
|
|
}
|
|
fields = append(fields, cur.String())
|
|
return fields
|
|
}
|
|
|
|
// field is a small helper to safely index a parsed terse row.
|
|
func field(row []string, i int) string {
|
|
if i < 0 || i >= len(row) {
|
|
return ""
|
|
}
|
|
return row[i]
|
|
}
|