mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
freebsd: add initial support for running on FreeBSD, excluding Bluez,
ppd, and some things. Add a simple wpa_supplicant backend
This commit is contained in:
@@ -58,3 +58,31 @@ jobs:
|
||||
|
||||
- name: Build dankinstall
|
||||
run: go build -v ./cmd/dankinstall
|
||||
|
||||
build-freebsd:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: core
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Build all packages
|
||||
env:
|
||||
GOOS: freebsd
|
||||
CGO_ENABLED: 0
|
||||
run: go build -v ./...
|
||||
|
||||
- name: Build dms (distropkg, embedded shell)
|
||||
run: |
|
||||
make sync-shell
|
||||
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
|
||||
|
||||
@@ -27,7 +27,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
arch: [amd64, arm64]
|
||||
include:
|
||||
- { goos: linux, arch: amd64 }
|
||||
- { goos: linux, arch: arm64 }
|
||||
- { goos: freebsd, arch: amd64 }
|
||||
- { goos: freebsd, arch: arm64 }
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -35,6 +39,8 @@ jobs:
|
||||
|
||||
env:
|
||||
TAG: ${{ inputs.tag }}
|
||||
# linux assets keep their historical arch-only names
|
||||
ASSET: ${{ matrix.goos == 'linux' && matrix.arch || format('{0}-{1}', matrix.goos, matrix.arch) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -50,6 +56,7 @@ jobs:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Format check
|
||||
if: matrix.goos == 'linux'
|
||||
run: |
|
||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||
echo "The following files are not formatted:"
|
||||
@@ -58,41 +65,43 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run tests
|
||||
if: matrix.goos == 'linux'
|
||||
run: go test -v ./...
|
||||
|
||||
- name: Build dankinstall (${{ matrix.arch }})
|
||||
- name: Build dankinstall (${{ env.ASSET }})
|
||||
if: matrix.goos == 'linux'
|
||||
env:
|
||||
GOOS: linux
|
||||
GOOS: ${{ matrix.goos }}
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dankinstall
|
||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dankinstall-${{ matrix.arch }}
|
||||
-o ../../dankinstall-${{ env.ASSET }}
|
||||
cd ../..
|
||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
gzip -9 -k dankinstall-${{ env.ASSET }}
|
||||
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
|
||||
|
||||
- name: Sync embedded shell
|
||||
run: make sync-shell
|
||||
|
||||
- name: Build dms (${{ matrix.arch }})
|
||||
- name: Build dms (${{ env.ASSET }})
|
||||
env:
|
||||
GOOS: linux
|
||||
GOOS: ${{ matrix.goos }}
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-${{ matrix.arch }}
|
||||
-o ../../dms-${{ env.ASSET }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-${{ matrix.arch }}
|
||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
||||
gzip -9 -k dms-${{ env.ASSET }}
|
||||
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
|
||||
|
||||
- name: Generate shell completions
|
||||
if: matrix.arch == 'amd64'
|
||||
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||
run: |
|
||||
set -eux
|
||||
chmod +x dms-amd64
|
||||
@@ -100,46 +109,58 @@ jobs:
|
||||
./dms-amd64 completion fish > completion.fish
|
||||
./dms-amd64 completion zsh > completion.zsh
|
||||
|
||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
||||
- name: Build dms-distropkg (${{ env.ASSET }})
|
||||
env:
|
||||
GOOS: linux
|
||||
GOOS: ${{ matrix.goos }}
|
||||
CGO_ENABLED: 0
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
set -eux
|
||||
cd cmd/dms
|
||||
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
|
||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||
-o ../../dms-distropkg-${{ env.ASSET }}
|
||||
cd ../..
|
||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
gzip -9 -k dms-distropkg-${{ env.ASSET }}
|
||||
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||
|
||||
- name: Upload artifacts (${{ matrix.arch }})
|
||||
if: matrix.arch == 'arm64'
|
||||
- name: Upload artifacts (${{ env.ASSET }})
|
||||
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
name: core-assets-${{ env.ASSET }}
|
||||
path: |
|
||||
core/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
core/dankinstall-${{ env.ASSET }}.gz
|
||||
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||
core/dms-${{ env.ASSET }}.gz
|
||||
core/dms-${{ env.ASSET }}.gz.sha256
|
||||
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload artifacts (${{ env.ASSET }}, no installer)
|
||||
if: matrix.goos != 'linux'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ env.ASSET }}
|
||||
path: |
|
||||
core/dms-${{ env.ASSET }}.gz
|
||||
core/dms-${{ env.ASSET }}.gz.sha256
|
||||
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload artifacts with completions
|
||||
if: matrix.arch == 'amd64'
|
||||
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: core-assets-${{ matrix.arch }}
|
||||
name: core-assets-${{ env.ASSET }}
|
||||
path: |
|
||||
core/dankinstall-${{ matrix.arch }}.gz
|
||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-${{ matrix.arch }}.gz
|
||||
core/dms-${{ matrix.arch }}.gz.sha256
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
||||
core/dankinstall-${{ env.ASSET }}.gz
|
||||
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||
core/dms-${{ env.ASSET }}.gz
|
||||
core/dms-${{ env.ASSET }}.gz.sha256
|
||||
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||
core/completion.bash
|
||||
core/completion.fish
|
||||
core/completion.zsh
|
||||
@@ -248,12 +269,18 @@ jobs:
|
||||
### Complete Packages
|
||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||
- **`dms-full-freebsd-amd64.tar.gz`** - Complete package for FreeBSD x86_64 systems
|
||||
- **`dms-full-freebsd-arm64.tar.gz`** - Complete package for FreeBSD ARM64 systems
|
||||
|
||||
### Individual Components
|
||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||
- **`dms-cli-freebsd-amd64.gz`** - DMS CLI binary for FreeBSD x86_64 systems
|
||||
- **`dms-cli-freebsd-arm64.gz`** - DMS CLI binary for FreeBSD ARM64 systems
|
||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||
- **`dms-distropkg-freebsd-amd64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD x86_64 systems
|
||||
- **`dms-distropkg-freebsd-arm64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD ARM64 systems
|
||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||
@@ -338,8 +365,8 @@ jobs:
|
||||
# Generate checksum for QML package
|
||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||
|
||||
# Create full packages for each architecture
|
||||
for arch in amd64 arm64; do
|
||||
# Create full packages for each os/architecture
|
||||
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
|
||||
mkdir -p _temp_full/dms
|
||||
mkdir -p _temp_full/bin
|
||||
mkdir -p _temp_full/completions
|
||||
|
||||
@@ -72,6 +72,9 @@ install-completions:
|
||||
@echo "Shell completions installed"
|
||||
|
||||
install-systemd:
|
||||
ifneq ($(shell uname),Linux)
|
||||
@echo "Skipping systemd user service (non-Linux); start the shell from your compositor config with 'dms run'"
|
||||
else
|
||||
@echo "Installing systemd user service..."
|
||||
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
||||
@@ -79,6 +82,7 @@ install-systemd:
|
||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||
endif
|
||||
|
||||
install-icon:
|
||||
@echo "Installing icon..."
|
||||
|
||||
+20
-20
@@ -23,6 +23,14 @@ BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUI
|
||||
|
||||
# Architecture to build for dist target (amd64, arm64, or all)
|
||||
ARCH ?= all
|
||||
# Target OSes for dist builds
|
||||
DIST_OSES ?= linux freebsd
|
||||
|
||||
ifeq ($(ARCH),all)
|
||||
DIST_ARCHS = amd64 arm64
|
||||
else
|
||||
DIST_ARCHS = $(ARCH)
|
||||
endif
|
||||
|
||||
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
||||
|
||||
@@ -57,26 +65,18 @@ dankinstall:
|
||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
||||
|
||||
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
|
||||
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
|
||||
dist: sync-shell
|
||||
ifeq ($(ARCH),all)
|
||||
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
|
||||
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@echo "Building for linux/amd64..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
|
||||
@echo "Building for linux/arm64..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
|
||||
@echo "Distribution builds complete:"
|
||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
|
||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
|
||||
else
|
||||
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
|
||||
@mkdir -p $(BUILD_DIR)
|
||||
@echo "Building for linux/$(ARCH)..."
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
|
||||
@echo "Distribution build complete:"
|
||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
|
||||
endif
|
||||
@for os in $(DIST_OSES); do \
|
||||
for arch in $(DIST_ARCHS); do \
|
||||
echo "Building for $$os/$$arch..."; \
|
||||
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch ./$(SOURCE_DIR) || exit 1; \
|
||||
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
|
||||
done; \
|
||||
done
|
||||
@echo "Distribution builds complete"
|
||||
|
||||
build-all: build dankinstall
|
||||
|
||||
@@ -161,8 +161,8 @@ help:
|
||||
@echo " build - Build the main binary (dms) with the embedded UI"
|
||||
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
|
||||
@echo " dankinstall - Build dankinstall binary"
|
||||
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
||||
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
||||
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)"
|
||||
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
|
||||
@echo " build-all - Build both binaries"
|
||||
@echo " install - Install dms to $(INSTALL_DIR)"
|
||||
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
||||
|
||||
@@ -833,6 +833,8 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||
return "systemd-networkd"
|
||||
case network.BackendConnMan:
|
||||
return "ConnMan"
|
||||
case network.BackendWpaSupplicant:
|
||||
return "wpa_supplicant"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -980,26 +980,6 @@ func signalTerminals(opts *Options) {
|
||||
}
|
||||
}
|
||||
|
||||
func signalByName(name string, sig syscall.Signal) {
|
||||
entries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
pid, err := strconv.Atoi(entry.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(string(comm)) == name {
|
||||
syscall.Kill(pid, sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncColorScheme(mode ColorMode) {
|
||||
scheme := "prefer-dark"
|
||||
if mode == ColorModeLight {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// procfs(5) is optional on FreeBSD; pkill(1) from base queries the kernel
|
||||
// directly.
|
||||
func signalByName(name string, sig syscall.Signal) {
|
||||
signame := strings.TrimPrefix(unix.SignalName(sig), "SIG")
|
||||
exec.Command("pkill", "-"+signame, "-x", name).Run()
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func signalByName(name string, sig syscall.Signal) {
|
||||
entries, err := os.ReadDir("/proc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, entry := range entries {
|
||||
pid, err := strconv.Atoi(entry.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
comm, err := os.ReadFile(filepath.Join("/proc", entry.Name(), "comm"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if strings.TrimSpace(string(comm)) == name {
|
||||
syscall.Kill(pid, sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@ const (
|
||||
// rest cover distros (or minimal installs) with no /etc/pam.d/login.
|
||||
// lockscreenPamBaseDirs mirrors libpam's search order: /etc overrides, then the
|
||||
// vendor dir (/usr/lib) and the stateless-distro default (/usr/share).
|
||||
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d"}
|
||||
// /usr/local/etc/pam.d is OpenPAM's ports dir on FreeBSD (openpam_configure).
|
||||
var lockscreenPamBaseDirs = []string{"/etc/pam.d", "/usr/lib/pam.d", "/usr/share/pam.d", "/usr/local/etc/pam.d"}
|
||||
|
||||
// Standalone auth+account services, most universal first. login exists almost
|
||||
// everywhere (util-linux); system-* cover Fedora/Arch/Gentoo/SUSE-Leap.
|
||||
@@ -50,13 +51,15 @@ var lockscreenPamEntryCandidates = []string{
|
||||
}
|
||||
|
||||
// Fallback for distros with no standalone login service, only shared building
|
||||
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*).
|
||||
// blocks: openSUSE/Debian (common-*), Alpine/postmarketOS (base-*), FreeBSD
|
||||
// (system holds both stanzas, included by login).
|
||||
var lockscreenPamSharedIncludePairs = []struct {
|
||||
auth string
|
||||
account string
|
||||
}{
|
||||
{auth: "common-auth", account: "common-account"},
|
||||
{auth: "base-auth", account: "base-account"},
|
||||
{auth: "system", account: "system"},
|
||||
}
|
||||
|
||||
var includedPamAuthFiles = []string{
|
||||
@@ -67,6 +70,7 @@ var includedPamAuthFiles = []string{
|
||||
"system-local-login",
|
||||
"common-auth-pc",
|
||||
"login",
|
||||
"system",
|
||||
}
|
||||
|
||||
type AuthSettings struct {
|
||||
@@ -1268,6 +1272,9 @@ func pamModuleExists(module string) bool {
|
||||
"/usr/lib/aarch64-linux-gnu/security",
|
||||
"/run/current-system/sw/lib64/security",
|
||||
"/run/current-system/sw/lib/security",
|
||||
"/usr/local/lib/security",
|
||||
"/usr/local/lib",
|
||||
"/usr/lib",
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(libDir, module)); err == nil {
|
||||
return true
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/dankgo/syncmap"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// sys/sys/backlight.h: brightness is a 0-100 percent;
|
||||
// BACKLIGHTGETSTATUS/BACKLIGHTUPDATESTATUS = _IOWR('G', 0/1, struct
|
||||
// backlight_props{uint32 brightness; uint32 nlevels; uint32 levels[100]}).
|
||||
const (
|
||||
backlightDevDir = "/dev/backlight"
|
||||
backlightGetStatus = 0xc1984700
|
||||
backlightUpdateStatus = 0xc1984701
|
||||
)
|
||||
|
||||
type backlightProps struct {
|
||||
brightness uint32
|
||||
nlevels uint32
|
||||
levels [100]uint32
|
||||
}
|
||||
|
||||
type BacklightBackend struct {
|
||||
devices syncmap.Map[string, string]
|
||||
}
|
||||
|
||||
func NewBacklightBackend() (*BacklightBackend, error) {
|
||||
b := &BacklightBackend{}
|
||||
if err := b.scanDevices(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func isGenericBacklightName(name string) bool {
|
||||
rest, ok := strings.CutPrefix(name, "backlight")
|
||||
if !ok || rest == "" {
|
||||
return false
|
||||
}
|
||||
for _, r := range rest {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) scanDevices() error {
|
||||
entries, err := os.ReadDir(backlightDevDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read %s: %w", backlightDevDir, err)
|
||||
}
|
||||
|
||||
// backlight_register (sys/dev/backlight/backlight.c) publishes each unit
|
||||
// as backlight/backlightN plus a driver-named alias for the same cdev;
|
||||
// dedupe on the device number and keep the descriptive alias.
|
||||
names := make(map[uint64]string)
|
||||
for _, entry := range entries {
|
||||
var st unix.Stat_t
|
||||
if err := unix.Stat(filepath.Join(backlightDevDir, entry.Name()), &st); err != nil {
|
||||
continue
|
||||
}
|
||||
rdev := uint64(st.Rdev)
|
||||
current, exists := names[rdev]
|
||||
if exists && !isGenericBacklightName(current) {
|
||||
continue
|
||||
}
|
||||
names[rdev] = entry.Name()
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
b.devices.Store("backlight:"+name, filepath.Join(backlightDevDir, name))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func backlightIoctl(fd int, req uint, props *backlightProps) error {
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(unsafe.Pointer(props)))
|
||||
if errno != 0 {
|
||||
return errno
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readBrightness(path string) (int, error) {
|
||||
fd, err := unix.Open(path, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
var props backlightProps
|
||||
if err := backlightIoctl(fd, backlightGetStatus, &props); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(props.brightness), nil
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) Rescan() error {
|
||||
return b.scanDevices()
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) GetDevices() ([]Device, error) {
|
||||
devices := make([]Device, 0)
|
||||
|
||||
b.devices.Range(func(id, path string) bool {
|
||||
brightness, err := readBrightness(path)
|
||||
if err != nil {
|
||||
log.Debugf("failed to read brightness for %s: %v", id, err)
|
||||
return true
|
||||
}
|
||||
|
||||
devices = append(devices, Device{
|
||||
Class: ClassBacklight,
|
||||
ID: id,
|
||||
Name: strings.TrimPrefix(id, "backlight:"),
|
||||
Current: brightness,
|
||||
Max: 100,
|
||||
CurrentPercent: brightness,
|
||||
Backend: "backlight",
|
||||
})
|
||||
return true
|
||||
})
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
func (b *BacklightBackend) SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error {
|
||||
if percent < 0 || percent > 100 {
|
||||
return fmt.Errorf("percent out of range: %d", percent)
|
||||
}
|
||||
|
||||
path, ok := b.devices.Load(id)
|
||||
if !ok {
|
||||
return fmt.Errorf("device not found: %s", id)
|
||||
}
|
||||
|
||||
value := percent
|
||||
switch {
|
||||
case percent == 0:
|
||||
value = 1
|
||||
case exponential:
|
||||
value = 1 + int(math.Round(math.Pow(float64(percent-1)/99.0, exponent)*99.0))
|
||||
}
|
||||
|
||||
fd, err := unix.Open(path, unix.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %s: %w", path, err)
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
props := backlightProps{brightness: uint32(value)}
|
||||
if err := backlightIoctl(fd, backlightUpdateStatus, &props); err != nil {
|
||||
return fmt.Errorf("set brightness: %w", err)
|
||||
}
|
||||
|
||||
log.Debugf("set %s to %d%% (hw %d) via backlight(4)", id, percent, value)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
// devd(8) publishes device events on this SOCK_SEQPACKET socket, one
|
||||
// "!system=... subsystem=... type=..." record per packet.
|
||||
const devdSocketPath = "/var/run/devd.seqpacket.pipe"
|
||||
|
||||
const (
|
||||
devdMaxRetries = 5
|
||||
devdBaseDelay = 2 * time.Second
|
||||
devdMaxDelay = 60 * time.Second
|
||||
)
|
||||
|
||||
type DevdMonitor struct {
|
||||
stop chan struct{}
|
||||
rescanMutex sync.Mutex
|
||||
rescanTimer *time.Timer
|
||||
rescanPending bool
|
||||
}
|
||||
|
||||
func newDevdMonitor(manager *Manager) *DevdMonitor {
|
||||
m := &DevdMonitor{
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
|
||||
go m.run(manager)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) run(manager *Manager) {
|
||||
failures := 0
|
||||
for {
|
||||
if err := m.monitorLoop(manager); err != nil {
|
||||
log.Errorf("Devd monitor error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
failures++
|
||||
if failures > devdMaxRetries {
|
||||
log.Errorf("Devd monitor exceeded %d retries, giving up", devdMaxRetries)
|
||||
return
|
||||
}
|
||||
|
||||
delay := min(devdBaseDelay*time.Duration(1<<(failures-1)), devdMaxDelay)
|
||||
log.Infof("Devd monitor reconnecting in %v (attempt %d/%d)", delay, failures, devdMaxRetries)
|
||||
|
||||
select {
|
||||
case <-m.stop:
|
||||
return
|
||||
case <-time.After(delay):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) monitorLoop(manager *Manager) error {
|
||||
conn, err := net.Dial("unixpacket", devdSocketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-m.stop:
|
||||
conn.Close()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("Devd monitor started for backlight/drm events")
|
||||
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
select {
|
||||
case <-m.stop:
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.handleEvent(manager, string(buf[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) handleEvent(manager *Manager, event string) {
|
||||
notification, ok := strings.CutPrefix(event, "!")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
fields := parseDevdEvent(notification)
|
||||
switch fields["system"] {
|
||||
case "DRM":
|
||||
m.debouncedRescan(manager)
|
||||
case "DEVFS":
|
||||
if fields["subsystem"] != "CDEV" {
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(fields["cdev"], "backlight/") {
|
||||
return
|
||||
}
|
||||
m.debouncedRescan(manager)
|
||||
}
|
||||
}
|
||||
|
||||
func parseDevdEvent(s string) map[string]string {
|
||||
fields := make(map[string]string)
|
||||
for _, part := range strings.Fields(s) {
|
||||
k, v, ok := strings.Cut(part, "=")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fields[k] = v
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) debouncedRescan(manager *Manager) {
|
||||
m.rescanMutex.Lock()
|
||||
defer m.rescanMutex.Unlock()
|
||||
|
||||
m.rescanPending = true
|
||||
|
||||
if m.rescanTimer != nil {
|
||||
m.rescanTimer.Reset(2 * time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
m.rescanTimer = time.AfterFunc(2*time.Second, func() {
|
||||
m.rescanMutex.Lock()
|
||||
pending := m.rescanPending
|
||||
m.rescanPending = false
|
||||
m.rescanMutex.Unlock()
|
||||
|
||||
if !pending {
|
||||
return
|
||||
}
|
||||
|
||||
manager.Rescan()
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DevdMonitor) Close() {
|
||||
close(m.stop)
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func NewManagerWithOptions(exponential bool) (*Manager, error) {
|
||||
}
|
||||
|
||||
go m.initLogind()
|
||||
go m.initSysfs()
|
||||
go m.initNative()
|
||||
go m.initDDC()
|
||||
|
||||
return m, nil
|
||||
@@ -40,39 +40,6 @@ func (m *Manager) initLogind() {
|
||||
log.Info("Logind backend initialized - will use for brightness control")
|
||||
}
|
||||
|
||||
func (m *Manager) initSysfs() {
|
||||
log.Debug("Initializing sysfs backend...")
|
||||
sysfs, err := NewSysfsBackend()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize sysfs backend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := sysfs.GetDevices()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get initial sysfs devices: %v", err)
|
||||
m.sysfsBackend = sysfs
|
||||
m.sysfsReady = true
|
||||
m.updateState()
|
||||
m.initUdev()
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Sysfs backend initialized with %d devices", len(devices))
|
||||
for _, d := range devices {
|
||||
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
|
||||
}
|
||||
|
||||
m.sysfsBackend = sysfs
|
||||
m.sysfsReady = true
|
||||
m.updateState()
|
||||
m.initUdev()
|
||||
}
|
||||
|
||||
func (m *Manager) initUdev() {
|
||||
m.udevMonitor = NewUdevMonitor(m)
|
||||
}
|
||||
|
||||
func (m *Manager) initDDC() {
|
||||
ddc, err := NewDDCBackend()
|
||||
if err != nil {
|
||||
@@ -96,9 +63,9 @@ func (m *Manager) Rescan() {
|
||||
}
|
||||
}
|
||||
|
||||
if m.sysfsReady && m.sysfsBackend != nil {
|
||||
if err := m.sysfsBackend.Rescan(); err != nil {
|
||||
log.Debugf("Sysfs rescan failed: %v", err)
|
||||
if m.nativeReady && m.nativeBackend != nil {
|
||||
if err := m.nativeBackend.Rescan(); err != nil {
|
||||
log.Debugf("Native backend rescan failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,10 +117,10 @@ func stateChanged(old, new State) bool {
|
||||
func (m *Manager) updateState() {
|
||||
allDevices := make([]Device, 0)
|
||||
|
||||
if m.sysfsReady && m.sysfsBackend != nil {
|
||||
devices, err := m.sysfsBackend.GetDevices()
|
||||
if m.nativeReady && m.nativeBackend != nil {
|
||||
devices, err := m.nativeBackend.GetDevices()
|
||||
if err != nil {
|
||||
log.Debugf("Failed to get sysfs devices: %v", err)
|
||||
log.Debugf("Failed to get native backend devices: %v", err)
|
||||
}
|
||||
if err == nil {
|
||||
allDevices = append(allDevices, devices...)
|
||||
@@ -232,18 +199,21 @@ func (m *Manager) SetBrightnessWithExponent(deviceID string, percent int, expone
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
var err error
|
||||
if deviceClass == ClassDDC {
|
||||
switch {
|
||||
case deviceClass == ClassDDC:
|
||||
log.Debugf("Calling DDC backend for %s", deviceID)
|
||||
err = m.ddcBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, func() {
|
||||
m.updateState()
|
||||
m.debouncedBroadcast(deviceID)
|
||||
})
|
||||
} else if m.logindReady && m.logindBackend != nil {
|
||||
case m.logindReady && m.logindBackend != nil:
|
||||
log.Debugf("Calling logind backend for %s", deviceID)
|
||||
err = m.setViaSysfsWithLogindWithExponent(deviceID, percent, exponential, exponent)
|
||||
} else {
|
||||
log.Debugf("Calling sysfs backend for %s", deviceID)
|
||||
err = m.sysfsBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
|
||||
case m.nativeBackend != nil:
|
||||
log.Debugf("Calling native backend for %s", deviceID)
|
||||
err = m.nativeBackend.SetBrightnessWithExponent(deviceID, percent, exponential, exponent)
|
||||
default:
|
||||
err = fmt.Errorf("no brightness backend for %s", deviceID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func (m *Manager) initNative() {
|
||||
log.Debug("Initializing backlight backend...")
|
||||
backend, err := NewBacklightBackend()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize backlight backend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := backend.GetDevices()
|
||||
if err == nil {
|
||||
log.Infof("Backlight backend initialized with %d devices", len(devices))
|
||||
}
|
||||
|
||||
m.nativeBackend = backend
|
||||
m.nativeReady = true
|
||||
m.updateState()
|
||||
m.monitor = newDevdMonitor(m)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package brightness
|
||||
|
||||
import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func (m *Manager) initNative() {
|
||||
log.Debug("Initializing sysfs backend...")
|
||||
sysfs, err := NewSysfsBackend()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize sysfs backend: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
devices, err := sysfs.GetDevices()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get initial sysfs devices: %v", err)
|
||||
m.sysfsBackend = sysfs
|
||||
m.nativeBackend = sysfs
|
||||
m.nativeReady = true
|
||||
m.updateState()
|
||||
m.monitor = NewUdevMonitor(m)
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Sysfs backend initialized with %d devices", len(devices))
|
||||
for _, d := range devices {
|
||||
log.Debugf(" - %s: %s (%d%%)", d.ID, d.Name, d.CurrentPercent)
|
||||
}
|
||||
|
||||
m.sysfsBackend = sysfs
|
||||
m.nativeBackend = sysfs
|
||||
m.nativeReady = true
|
||||
m.updateState()
|
||||
m.monitor = NewUdevMonitor(m)
|
||||
}
|
||||
+5
-4
@@ -43,7 +43,7 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -180,8 +180,9 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
|
||||
m := &Manager{
|
||||
logindBackend: nil,
|
||||
sysfsBackend: sysfs,
|
||||
nativeBackend: sysfs,
|
||||
logindReady: false,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -243,7 +244,7 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
|
||||
logindBackend: mockLogind,
|
||||
sysfsBackend: sysfs,
|
||||
logindReady: true,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -33,14 +33,25 @@ type DeviceUpdate struct {
|
||||
Device Device `json:"device"`
|
||||
}
|
||||
|
||||
type Backend interface {
|
||||
Rescan() error
|
||||
GetDevices() ([]Device, error)
|
||||
SetBrightnessWithExponent(id string, percent int, exponential bool, exponent float64) error
|
||||
}
|
||||
|
||||
type deviceMonitor interface {
|
||||
Close()
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
logindBackend *LogindBackend
|
||||
sysfsBackend *SysfsBackend
|
||||
nativeBackend Backend
|
||||
ddcBackend *DDCBackend
|
||||
udevMonitor *UdevMonitor
|
||||
monitor deviceMonitor
|
||||
|
||||
logindReady bool
|
||||
sysfsReady bool
|
||||
nativeReady bool
|
||||
ddcReady bool
|
||||
|
||||
exponential bool
|
||||
@@ -170,8 +181,8 @@ func (m *Manager) Close() {
|
||||
return true
|
||||
})
|
||||
|
||||
if m.udevMonitor != nil {
|
||||
m.udevMonitor.Close()
|
||||
if m.monitor != nil {
|
||||
m.monitor.Close()
|
||||
}
|
||||
|
||||
if m.logindBackend != nil {
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ func setupTestManager(t *testing.T) (*Manager, string) {
|
||||
|
||||
m := &Manager{
|
||||
sysfsBackend: sysfs,
|
||||
sysfsReady: true,
|
||||
nativeReady: true,
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
// Default ctrl_interface directory on FreeBSD and minimal Linux setups; see
|
||||
// https://man.freebsd.org/cgi/man.cgi?query=wpa_supplicant.conf&sektion=5
|
||||
const wpaSupplicantCtrlDir = "/var/run/wpa_supplicant"
|
||||
|
||||
type wpaConnectAttempt struct {
|
||||
ssid string
|
||||
netID int
|
||||
saved bool
|
||||
newlyAdded bool
|
||||
prompted bool
|
||||
sawTempDisabled bool
|
||||
finalized bool
|
||||
deadline time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type WpaSupplicantBackend struct {
|
||||
ctrlDir string
|
||||
ifname string
|
||||
cmd *wpaCtrlConn
|
||||
monitor *wpaCtrlConn
|
||||
state *BackendState
|
||||
stateMutex sync.RWMutex
|
||||
promptBroker PromptBroker
|
||||
onStateChange func()
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
curAttempt *wpaConnectAttempt
|
||||
attemptMutex sync.RWMutex
|
||||
|
||||
savedIDs map[string]int
|
||||
savedIDsMu sync.Mutex
|
||||
|
||||
recentScans map[string]time.Time
|
||||
recentScansMu sync.Mutex
|
||||
}
|
||||
|
||||
func NewWpaSupplicantBackend() (*WpaSupplicantBackend, error) {
|
||||
return &WpaSupplicantBackend{
|
||||
ctrlDir: wpaSupplicantCtrlDir,
|
||||
state: &BackendState{
|
||||
Backend: "wpa_supplicant",
|
||||
WiFiNetworks: []WiFiNetwork{},
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
savedIDs: make(map[string]int),
|
||||
recentScans: make(map[string]time.Time),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func discoverWpaInterfaces(ctrlDir string) ([]string, error) {
|
||||
entries, err := os.ReadDir(ctrlDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", ctrlDir, err)
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
// wpa_supplicant creates an extra p2p-dev-<ifname> management socket
|
||||
// when built with CONFIG_P2P; it is not a station interface.
|
||||
if strings.HasPrefix(entry.Name(), "p2p-dev-") {
|
||||
continue
|
||||
}
|
||||
names = append(names, entry.Name())
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) Initialize() error {
|
||||
names, err := discoverWpaInterfaces(b.ctrlDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to discover wpa_supplicant interfaces: %w", err)
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return fmt.Errorf("no wpa_supplicant control sockets in %s", b.ctrlDir)
|
||||
}
|
||||
|
||||
b.ifname = names[0]
|
||||
if len(names) > 1 {
|
||||
log.Infof("wpa_supplicant manages %d interfaces; using %s", len(names), b.ifname)
|
||||
}
|
||||
|
||||
conn, err := newWpaCtrlConn(filepath.Join(b.ctrlDir, b.ifname))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open wpa_ctrl socket: %w", err)
|
||||
}
|
||||
b.cmd = conn
|
||||
|
||||
reply, err := b.cmd.request("PING")
|
||||
if err != nil {
|
||||
b.cmd.close()
|
||||
return fmt.Errorf("wpa_supplicant not responding on %s: %w", b.ifname, err)
|
||||
}
|
||||
if reply != "PONG" {
|
||||
b.cmd.close()
|
||||
return fmt.Errorf("unexpected PING reply from wpa_supplicant: %s", reply)
|
||||
}
|
||||
|
||||
b.stateMutex.Lock()
|
||||
b.state.WiFiDevice = b.ifname
|
||||
b.state.WiFiEnabled = true
|
||||
b.stateMutex.Unlock()
|
||||
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("Failed to get initial saved WiFi networks: %v", err)
|
||||
}
|
||||
|
||||
if err := b.updateState(); err != nil {
|
||||
b.cmd.close()
|
||||
return fmt.Errorf("failed to get initial state: %w", err)
|
||||
}
|
||||
|
||||
if _, err := b.updateWiFiNetworks(); err != nil {
|
||||
log.Warnf("Failed to get initial WiFi networks: %v", err)
|
||||
}
|
||||
|
||||
if err := b.ScanWiFi(); err != nil {
|
||||
log.Debugf("Initial WiFi scan not started: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) Close() {
|
||||
b.StopMonitoring()
|
||||
|
||||
if b.monitor != nil {
|
||||
_ = b.monitor.send("DETACH")
|
||||
b.monitor.close()
|
||||
}
|
||||
if b.cmd != nil {
|
||||
b.cmd.close()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetCurrentState() (*BackendState, error) {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
|
||||
state := *b.state
|
||||
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
|
||||
state.SavedWiFiNetworks = append([]WiFiNetwork(nil), b.state.SavedWiFiNetworks...)
|
||||
state.EthernetDevices = append([]EthernetDevice(nil), b.state.EthernetDevices...)
|
||||
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
|
||||
state.WiFiDevices = b.getWiFiDevicesLocked()
|
||||
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetPromptBroker() PromptBroker {
|
||||
return b.promptBroker
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) SetPromptBroker(broker PromptBroker) error {
|
||||
if broker == nil {
|
||||
return fmt.Errorf("broker cannot be nil")
|
||||
}
|
||||
|
||||
b.promptBroker = broker
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) SubmitCredentials(token string, secrets map[string]string, save bool) error {
|
||||
if b.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not initialized")
|
||||
}
|
||||
|
||||
return b.promptBroker.Resolve(token, PromptReply{
|
||||
Secrets: secrets,
|
||||
Save: save,
|
||||
Cancel: false,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) CancelCredentials(token string) error {
|
||||
if b.promptBroker == nil {
|
||||
return fmt.Errorf("prompt broker not initialized")
|
||||
}
|
||||
|
||||
return b.promptBroker.Resolve(token, PromptReply{
|
||||
Cancel: true,
|
||||
})
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) StopMonitoring() {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
default:
|
||||
close(b.stopChan)
|
||||
}
|
||||
b.sigWG.Wait()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) promptForPSK(ssid, reason string) (string, bool) {
|
||||
if b.promptBroker == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
|
||||
token, err := b.promptBroker.Ask(ctx, PromptRequest{
|
||||
SSID: ssid,
|
||||
SettingName: "802-11-wireless-security",
|
||||
Fields: []string{"psk"},
|
||||
Reason: reason,
|
||||
})
|
||||
if err != nil {
|
||||
log.Warnf("failed to request credentials for %s: %v", ssid, err)
|
||||
return "", false
|
||||
}
|
||||
|
||||
reply, err := b.promptBroker.Wait(ctx, token)
|
||||
if err != nil || reply.Cancel {
|
||||
return "", false
|
||||
}
|
||||
|
||||
psk, ok := reply.Secrets["psk"]
|
||||
if !ok || psk == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return psk, true
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) storeSavedIDs(ids map[string]int) {
|
||||
b.savedIDsMu.Lock()
|
||||
b.savedIDs = ids
|
||||
b.savedIDsMu.Unlock()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) savedNetworkID(ssid string) (int, bool) {
|
||||
b.savedIDsMu.Lock()
|
||||
defer b.savedIDsMu.Unlock()
|
||||
id, ok := b.savedIDs[ssid]
|
||||
return id, ok
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) seenInRecentScan(ssid string) bool {
|
||||
b.recentScansMu.Lock()
|
||||
defer b.recentScansMu.Unlock()
|
||||
lastSeen, ok := b.recentScans[ssid]
|
||||
return ok && time.Since(lastSeen) < 30*time.Second
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) setConnectError(code string) {
|
||||
b.stateMutex.Lock()
|
||||
b.state.IsConnecting = false
|
||||
b.state.ConnectingSSID = ""
|
||||
b.state.LastError = code
|
||||
b.stateMutex.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
wpaCtrlRequestTimeout = 5 * time.Second
|
||||
wpaCtrlReadBufferSize = 65536
|
||||
)
|
||||
|
||||
// Client side of the wpa_ctrl protocol: a unix datagram socket bound to a
|
||||
// local path, connected to wpa_supplicant's per-interface socket. See
|
||||
// https://w1.fi/wpa_supplicant/devel/ctrl_iface_page.html
|
||||
type wpaCtrlConn struct {
|
||||
sockPath string
|
||||
localDir string
|
||||
mu sync.Mutex
|
||||
conn *net.UnixConn
|
||||
seq int
|
||||
}
|
||||
|
||||
func wpaCtrlLocalSocketBase() string {
|
||||
if dir := os.Getenv("XDG_RUNTIME_DIR"); dir != "" {
|
||||
return dir
|
||||
}
|
||||
return os.TempDir()
|
||||
}
|
||||
|
||||
func newWpaCtrlConn(sockPath string) (*wpaCtrlConn, error) {
|
||||
localDir, err := os.MkdirTemp(wpaCtrlLocalSocketBase(), "dms-wpa-")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create wpa_ctrl socket dir: %w", err)
|
||||
}
|
||||
|
||||
c := &wpaCtrlConn{sockPath: sockPath, localDir: localDir}
|
||||
if err := c.dialLocked(); err != nil {
|
||||
os.RemoveAll(localDir)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) dialLocked() error {
|
||||
c.seq++
|
||||
localPath := filepath.Join(c.localDir, fmt.Sprintf("ctrl_%d", c.seq))
|
||||
os.Remove(localPath)
|
||||
|
||||
conn, err := net.DialUnix("unixgram",
|
||||
&net.UnixAddr{Name: localPath, Net: "unixgram"},
|
||||
&net.UnixAddr{Name: c.sockPath, Net: "unixgram"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial %s: %w", c.sockPath, err)
|
||||
}
|
||||
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
}
|
||||
c.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) reconnectLocked() error {
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
return c.dialLocked()
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) reconnect() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.reconnectLocked()
|
||||
}
|
||||
|
||||
// request sends a command and waits for its reply, retrying once on a fresh
|
||||
// socket so a wpa_supplicant restart between requests is transparent.
|
||||
func (c *wpaCtrlConn) request(cmd string) (string, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
reply, err := c.roundTripLocked(cmd)
|
||||
if err == nil {
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
if rerr := c.reconnectLocked(); rerr != nil {
|
||||
return "", err
|
||||
}
|
||||
return c.roundTripLocked(cmd)
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) roundTripLocked(cmd string) (string, error) {
|
||||
if c.conn == nil {
|
||||
if err := c.dialLocked(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
_ = c.conn.SetDeadline(time.Now().Add(wpaCtrlRequestTimeout))
|
||||
if _, err := c.conn.Write([]byte(cmd)); err != nil {
|
||||
return "", fmt.Errorf("write %q: %w", cmd, err)
|
||||
}
|
||||
|
||||
buf := make([]byte, wpaCtrlReadBufferSize)
|
||||
for {
|
||||
n, err := c.conn.Read(buf)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read reply for %q: %w", cmd, err)
|
||||
}
|
||||
// Unsolicited event datagrams carry a "<priority>" prefix, command
|
||||
// replies never do (ctrl_iface_page.html, "Control interface data").
|
||||
if n > 0 && buf[0] == '<' {
|
||||
continue
|
||||
}
|
||||
return strings.TrimRight(string(buf[:n]), "\n"), nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) send(cmd string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn == nil {
|
||||
if err := c.dialLocked(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = c.conn.SetWriteDeadline(time.Now().Add(wpaCtrlRequestTimeout))
|
||||
if _, err := c.conn.Write([]byte(cmd)); err != nil {
|
||||
return fmt.Errorf("write %q: %w", cmd, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) readDatagram(timeout time.Duration) (string, error) {
|
||||
c.mu.Lock()
|
||||
conn := c.conn
|
||||
c.mu.Unlock()
|
||||
|
||||
if conn == nil {
|
||||
return "", fmt.Errorf("wpa_ctrl socket closed")
|
||||
}
|
||||
|
||||
_ = conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
buf := make([]byte, wpaCtrlReadBufferSize)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimRight(string(buf[:n]), "\n"), nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) attach() error {
|
||||
reply, err := c.request("ATTACH")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reply != "OK" {
|
||||
return fmt.Errorf("ATTACH failed: %s", reply)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *wpaCtrlConn) close() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
os.RemoveAll(c.localDir)
|
||||
}
|
||||
|
||||
func isWpaCtrlTimeout(err error) bool {
|
||||
netErr, ok := err.(net.Error)
|
||||
return ok && netErr.Timeout()
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
func interfaceIPv4(ifname string) string {
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||
return ipv4.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func looksWirelessName(name string) bool {
|
||||
return strings.HasPrefix(name, "wlan") || strings.HasPrefix(name, "wlp")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) isWpaManaged(name string) bool {
|
||||
return name == b.ifname
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ethernetDevices() []EthernetDevice {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var devices []EthernetDevice
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue
|
||||
}
|
||||
if looksVirtual(iface.Name) || looksWirelessName(iface.Name) {
|
||||
continue
|
||||
}
|
||||
if b.isWpaManaged(iface.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
up := iface.Flags&net.FlagUp != 0
|
||||
running := iface.Flags&net.FlagRunning != 0
|
||||
ip := interfaceIPv4(iface.Name)
|
||||
|
||||
stateStr := "off"
|
||||
switch {
|
||||
case running && ip != "":
|
||||
stateStr = "routable"
|
||||
case running:
|
||||
stateStr = "carrier"
|
||||
case up:
|
||||
stateStr = "no-carrier"
|
||||
}
|
||||
|
||||
devices = append(devices, EthernetDevice{
|
||||
Name: iface.Name,
|
||||
HwAddress: iface.HardwareAddr.String(),
|
||||
State: stateStr,
|
||||
Connected: up && running,
|
||||
IP: ip,
|
||||
})
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
func wiredConnectionsFromEthernetDevices(devices []EthernetDevice) []WiredConnection {
|
||||
conns := make([]WiredConnection, 0, len(devices))
|
||||
for _, dev := range devices {
|
||||
conns = append(conns, WiredConnection{
|
||||
Path: dbus.ObjectPath("/" + dev.Name),
|
||||
ID: dev.Name,
|
||||
UUID: "wired:" + dev.Name,
|
||||
Type: "ethernet",
|
||||
IsActive: dev.Connected,
|
||||
})
|
||||
}
|
||||
return conns
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetEthernetDevices() []EthernetDevice {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
return append([]EthernetDevice(nil), b.state.EthernetDevices...)
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return wiredConnectionsFromEthernetDevices(b.ethernetDevices()), nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetWiredNetworkDetails(id string) (*WiredNetworkInfoResponse, error) {
|
||||
ifname := strings.TrimPrefix(id, "wired:")
|
||||
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get interface: %w", err)
|
||||
}
|
||||
|
||||
addrs, _ := iface.Addrs()
|
||||
var ipv4s, ipv6s []string
|
||||
for _, addr := range addrs {
|
||||
ipnet, ok := addr.(*net.IPNet)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ipv4 := ipnet.IP.To4(); ipv4 != nil {
|
||||
ipv4s = append(ipv4s, ipnet.String())
|
||||
} else if ipv6 := ipnet.IP.To16(); ipv6 != nil {
|
||||
ipv6s = append(ipv6s, ipnet.String())
|
||||
}
|
||||
}
|
||||
|
||||
return &WiredNetworkInfoResponse{
|
||||
UUID: id,
|
||||
IFace: ifname,
|
||||
HwAddr: iface.HardwareAddr.String(),
|
||||
IPv4: WiredIPConfig{
|
||||
IPs: ipv4s,
|
||||
},
|
||||
IPv6: WiredIPConfig{
|
||||
IPs: ipv6s,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
// Event strings per WPA_EVENT_* in contrib/wpa/src/common/wpa_ctrl.h
|
||||
// (freebsd-src).
|
||||
const (
|
||||
wpaEventConnected = "CTRL-EVENT-CONNECTED"
|
||||
wpaEventDisconnected = "CTRL-EVENT-DISCONNECTED"
|
||||
wpaEventScanResults = "CTRL-EVENT-SCAN-RESULTS"
|
||||
wpaEventTempDisabled = "CTRL-EVENT-SSID-TEMP-DISABLED"
|
||||
)
|
||||
|
||||
const (
|
||||
wpaMonitorReadTimeout = 5 * time.Second
|
||||
wpaMonitorPingInterval = 30 * time.Second
|
||||
wpaMonitorReconnectDelay = 2 * time.Second
|
||||
)
|
||||
|
||||
type wpaEvent struct {
|
||||
priority int
|
||||
name string
|
||||
args string
|
||||
}
|
||||
|
||||
func parseWpaEventLine(line string) (wpaEvent, bool) {
|
||||
if len(line) < 3 || line[0] != '<' {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
end := strings.IndexByte(line, '>')
|
||||
if end < 0 {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
priority, err := strconv.Atoi(line[1:end])
|
||||
if err != nil {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
rest := line[end+1:]
|
||||
name, args, _ := strings.Cut(rest, " ")
|
||||
if name == "" {
|
||||
return wpaEvent{}, false
|
||||
}
|
||||
|
||||
return wpaEvent{priority: priority, name: name, args: args}, true
|
||||
}
|
||||
|
||||
// Args format: id=%d ssid="%s" auth_failures=%u duration=%d reason=%s, per
|
||||
// wpas_auth_failed in contrib/wpa/wpa_supplicant/wpa_supplicant.c
|
||||
// (freebsd-src); the SSID is printf_encoded inside the quotes.
|
||||
func parseWpaTempDisabled(args string) (ssid string, reason string) {
|
||||
if start := strings.Index(args, `ssid="`); start >= 0 {
|
||||
raw := args[start+len(`ssid="`):]
|
||||
if end := indexUnescapedQuote(raw); end >= 0 {
|
||||
ssid = decodeWpaSSIDText(raw[:end])
|
||||
}
|
||||
}
|
||||
|
||||
for _, field := range strings.Fields(args) {
|
||||
if value, ok := strings.CutPrefix(field, "reason="); ok {
|
||||
reason = value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ssid, reason
|
||||
}
|
||||
|
||||
func indexUnescapedQuote(s string) int {
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\\':
|
||||
i++
|
||||
case '"':
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) StartMonitoring(onStateChange func()) error {
|
||||
b.onStateChange = onStateChange
|
||||
|
||||
monitor, err := newWpaCtrlConn(filepath.Join(b.ctrlDir, b.ifname))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open wpa_ctrl monitor socket: %w", err)
|
||||
}
|
||||
if err := monitor.attach(); err != nil {
|
||||
monitor.close()
|
||||
return fmt.Errorf("failed to attach wpa_ctrl monitor: %w", err)
|
||||
}
|
||||
b.monitor = monitor
|
||||
|
||||
b.sigWG.Add(1)
|
||||
go b.monitorLoop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) monitorLoop() {
|
||||
defer b.sigWG.Done()
|
||||
|
||||
lastActivity := time.Now()
|
||||
pingOutstanding := false
|
||||
var pingSent time.Time
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
msg, err := b.monitor.readDatagram(wpaMonitorReadTimeout)
|
||||
switch {
|
||||
case err == nil:
|
||||
lastActivity = time.Now()
|
||||
if strings.HasPrefix(msg, "<") {
|
||||
b.handleEvent(msg)
|
||||
continue
|
||||
}
|
||||
if msg == "PONG" {
|
||||
pingOutstanding = false
|
||||
}
|
||||
|
||||
case isWpaCtrlTimeout(err):
|
||||
if pingOutstanding && time.Since(pingSent) > wpaCtrlRequestTimeout {
|
||||
b.reattachMonitor()
|
||||
pingOutstanding = false
|
||||
lastActivity = time.Now()
|
||||
continue
|
||||
}
|
||||
if !pingOutstanding && time.Since(lastActivity) > wpaMonitorPingInterval {
|
||||
if b.monitor.send("PING") != nil {
|
||||
b.reattachMonitor()
|
||||
lastActivity = time.Now()
|
||||
continue
|
||||
}
|
||||
pingOutstanding = true
|
||||
pingSent = time.Now()
|
||||
}
|
||||
|
||||
default:
|
||||
b.reattachMonitor()
|
||||
pingOutstanding = false
|
||||
lastActivity = time.Now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) reattachMonitor() {
|
||||
for {
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err := b.monitor.reconnect(); err == nil {
|
||||
if err := b.monitor.attach(); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-b.stopChan:
|
||||
return
|
||||
case <-time.After(wpaMonitorReconnectDelay):
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("wpa_supplicant monitor reattached on %s", b.ifname)
|
||||
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to refresh saved networks after wpa reattach: %v", err)
|
||||
}
|
||||
if err := b.updateState(); err != nil {
|
||||
log.Warnf("failed to refresh state after wpa reattach: %v", err)
|
||||
}
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleEvent(raw string) {
|
||||
event, ok := parseWpaEventLine(raw)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch event.name {
|
||||
case wpaEventScanResults:
|
||||
b.handleScanResults()
|
||||
case wpaEventConnected:
|
||||
b.handleConnected()
|
||||
case wpaEventDisconnected:
|
||||
b.handleDisconnected()
|
||||
case wpaEventTempDisabled:
|
||||
b.handleTempDisabled(event.args)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleScanResults() {
|
||||
if _, err := b.updateWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to update WiFi networks after scan: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleConnected() {
|
||||
if err := b.updateState(); err != nil {
|
||||
log.Warnf("failed to update wpa state after connect event: %v", err)
|
||||
}
|
||||
|
||||
b.attemptMutex.RLock()
|
||||
att := b.curAttempt
|
||||
b.attemptMutex.RUnlock()
|
||||
|
||||
b.stateMutex.RLock()
|
||||
currentSSID := b.state.WiFiSSID
|
||||
b.stateMutex.RUnlock()
|
||||
|
||||
if att != nil && att.ssid == currentSSID {
|
||||
b.finalizeAttempt(att, "")
|
||||
b.attemptMutex.Lock()
|
||||
if b.curAttempt == att {
|
||||
b.curAttempt = nil
|
||||
}
|
||||
b.attemptMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to refresh saved networks after connect event: %v", err)
|
||||
}
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleDisconnected() {
|
||||
if err := b.updateState(); err != nil {
|
||||
log.Warnf("failed to update wpa state after disconnect event: %v", err)
|
||||
}
|
||||
if err := b.updateSavedWiFiNetworks(); err != nil {
|
||||
log.Warnf("failed to refresh saved networks after disconnect event: %v", err)
|
||||
}
|
||||
|
||||
if b.onStateChange != nil {
|
||||
b.onStateChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) handleTempDisabled(args string) {
|
||||
ssid, reason := parseWpaTempDisabled(args)
|
||||
|
||||
b.attemptMutex.RLock()
|
||||
att := b.curAttempt
|
||||
b.attemptMutex.RUnlock()
|
||||
|
||||
if att == nil || att.ssid != ssid {
|
||||
return
|
||||
}
|
||||
|
||||
att.mu.Lock()
|
||||
att.sawTempDisabled = true
|
||||
att.mu.Unlock()
|
||||
|
||||
code := errdefs.ErrConnectionFailed
|
||||
// WRONG_KEY is the reason wpas_auth_failed reports for a PSK mismatch
|
||||
// (could_be_psk_mismatch path in contrib/wpa/wpa_supplicant/events.c).
|
||||
if reason == "WRONG_KEY" {
|
||||
code = errdefs.ErrBadCredentials
|
||||
}
|
||||
|
||||
b.finalizeAttempt(att, code)
|
||||
|
||||
b.attemptMutex.Lock()
|
||||
if b.curAttempt == att {
|
||||
b.curAttempt = nil
|
||||
}
|
||||
b.attemptMutex.Unlock()
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"net"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseWpaScanResults(t *testing.T) {
|
||||
text := "bssid / frequency / signal level / flags / ssid\n" +
|
||||
"02:00:01:02:03:04\t2412\t-45\t[WPA2-PSK-CCMP][ESS]\tHome\n" +
|
||||
"02:00:01:02:03:05\t5180\t-60\t[WPA2-EAP-CCMP][ESS]\tOffice\n" +
|
||||
"02:00:01:02:03:06\t2437\t-70\t[ESS]\tCafe\n" +
|
||||
"02:00:01:02:03:07\t2462\t-80\t[WPA2-PSK-CCMP][ESS]\t\n" +
|
||||
"02:00:01:02:03:08\t2412\t-50\t[WEP][ESS]\tTab\\tNet\n" +
|
||||
"garbage line without tabs\n"
|
||||
|
||||
results := parseWpaScanResults(text)
|
||||
|
||||
assert.Len(t, results, 5)
|
||||
assert.Equal(t, "02:00:01:02:03:04", results[0].BSSID)
|
||||
assert.Equal(t, uint32(2412), results[0].Frequency)
|
||||
assert.Equal(t, -45, results[0].Level)
|
||||
assert.Equal(t, "[WPA2-PSK-CCMP][ESS]", results[0].Flags)
|
||||
assert.Equal(t, "Home", results[0].SSID)
|
||||
assert.Empty(t, results[3].SSID)
|
||||
assert.Equal(t, "Tab\tNet", results[4].SSID)
|
||||
}
|
||||
|
||||
func TestWpaWiFiNetworksFromScanResults(t *testing.T) {
|
||||
results := []wpaScanResult{
|
||||
{BSSID: "02:00:01:02:03:04", Frequency: 2412, Level: -45, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: "Home"},
|
||||
{BSSID: "02:00:01:02:03:05", Frequency: 5180, Level: -80, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: "Home"},
|
||||
{BSSID: "02:00:01:02:03:06", Frequency: 5200, Level: -60, Flags: "[WPA2-EAP-CCMP][ESS]", SSID: "Office"},
|
||||
{BSSID: "02:00:01:02:03:07", Frequency: 2437, Level: -70, Flags: "[ESS]", SSID: "Cafe"},
|
||||
{BSSID: "02:00:01:02:03:08", Frequency: 2462, Level: -50, Flags: "[WPA2-PSK-CCMP][ESS]", SSID: ""},
|
||||
}
|
||||
|
||||
networks := wpaWiFiNetworksFromScanResults(results)
|
||||
|
||||
byNet := make(map[string]WiFiNetwork, len(networks))
|
||||
for _, network := range networks {
|
||||
byNet[network.SSID] = network
|
||||
}
|
||||
|
||||
assert.Len(t, networks, 3)
|
||||
|
||||
home := byNet["Home"]
|
||||
assert.Equal(t, "02:00:01:02:03:04", home.BSSID, "strongest BSS should win")
|
||||
assert.Equal(t, uint8(55), home.Signal)
|
||||
assert.True(t, home.Secured)
|
||||
assert.False(t, home.Enterprise)
|
||||
assert.Equal(t, uint32(1), home.Channel)
|
||||
|
||||
office := byNet["Office"]
|
||||
assert.True(t, office.Secured)
|
||||
assert.True(t, office.Enterprise)
|
||||
|
||||
cafe := byNet["Cafe"]
|
||||
assert.False(t, cafe.Secured)
|
||||
assert.Equal(t, "infrastructure", cafe.Mode)
|
||||
}
|
||||
|
||||
func TestSignalPercentFromDbm(t *testing.T) {
|
||||
testCases := []struct {
|
||||
dbm int
|
||||
expected uint8
|
||||
}{
|
||||
{-30, 70},
|
||||
{-100, 0},
|
||||
{-110, 0},
|
||||
{0, 100},
|
||||
{10, 100},
|
||||
{-55, 45},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t, tc.expected, signalPercentFromDbm(tc.dbm), "dbm=%d", tc.dbm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWpaStatus(t *testing.T) {
|
||||
text := "bssid=02:00:01:02:03:04\n" +
|
||||
"freq=2412\n" +
|
||||
"ssid=Home Network\n" +
|
||||
"id=3\n" +
|
||||
"key_mgmt=WPA2-PSK\n" +
|
||||
"wpa_state=COMPLETED\n" +
|
||||
"ip_address=192.168.1.21\n"
|
||||
|
||||
st := parseWpaStatus(text)
|
||||
|
||||
assert.Equal(t, "COMPLETED", st.wpaState)
|
||||
assert.Equal(t, "Home Network", st.ssid)
|
||||
assert.Equal(t, "02:00:01:02:03:04", st.bssid)
|
||||
assert.Equal(t, "192.168.1.21", st.ipAddress)
|
||||
assert.Equal(t, "WPA2-PSK", st.keyMgmt)
|
||||
assert.Equal(t, 3, st.networkID)
|
||||
}
|
||||
|
||||
func TestParseWpaStatus_Disconnected(t *testing.T) {
|
||||
st := parseWpaStatus("wpa_state=DISCONNECTED\naddress=02:00:01:02:03:04\n")
|
||||
|
||||
assert.Equal(t, "DISCONNECTED", st.wpaState)
|
||||
assert.Empty(t, st.ssid)
|
||||
assert.Equal(t, -1, st.networkID)
|
||||
}
|
||||
|
||||
func TestParseWpaSignalPollRSSI(t *testing.T) {
|
||||
rssi, ok := parseWpaSignalPollRSSI("RSSI=-62\nLINKSPEED=433\nNOISE=9999\nFREQUENCY=5180\n")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, -62, rssi)
|
||||
|
||||
_, ok = parseWpaSignalPollRSSI("FAIL")
|
||||
assert.False(t, ok)
|
||||
}
|
||||
|
||||
func TestParseWpaListNetworks(t *testing.T) {
|
||||
text := "network id / ssid / bssid / flags\n" +
|
||||
"0\tHome\tany\t[CURRENT]\n" +
|
||||
"1\tOffice\tany\t[DISABLED]\n" +
|
||||
"2\tCafe\tany\t\n" +
|
||||
"3\tFlaky\tany\t[TEMP-DISABLED]\n" +
|
||||
"bogus\tline\tany\t\n"
|
||||
|
||||
networks := parseWpaListNetworks(text)
|
||||
|
||||
assert.Len(t, networks, 4)
|
||||
assert.Equal(t, 0, networks[0].id)
|
||||
assert.Equal(t, "Home", networks[0].ssid)
|
||||
assert.True(t, networks[0].current)
|
||||
assert.False(t, networks[0].disabled)
|
||||
|
||||
assert.True(t, networks[1].disabled)
|
||||
assert.False(t, networks[2].disabled)
|
||||
assert.False(t, networks[2].current)
|
||||
assert.True(t, networks[3].tempDisabled)
|
||||
}
|
||||
|
||||
func TestParseWpaEventLine(t *testing.T) {
|
||||
testCases := []struct {
|
||||
line string
|
||||
ok bool
|
||||
priority int
|
||||
name string
|
||||
args string
|
||||
}{
|
||||
{"<2>CTRL-EVENT-CONNECTED - Connection to 02:00:01:02:03:04 completed [id=0 id_str=]", true, 2, "CTRL-EVENT-CONNECTED", "- Connection to 02:00:01:02:03:04 completed [id=0 id_str=]"},
|
||||
{"<3>CTRL-EVENT-DISCONNECTED bssid=02:00:01:02:03:04 reason=3 locally_generated=1", true, 3, "CTRL-EVENT-DISCONNECTED", "bssid=02:00:01:02:03:04 reason=3 locally_generated=1"},
|
||||
{"<2>CTRL-EVENT-SCAN-RESULTS ", true, 2, "CTRL-EVENT-SCAN-RESULTS", ""},
|
||||
{"OK", false, 0, "", ""},
|
||||
{"", false, 0, "", ""},
|
||||
{"<x>BROKEN", false, 0, "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
event, ok := parseWpaEventLine(tc.line)
|
||||
assert.Equal(t, tc.ok, ok, "line=%q", tc.line)
|
||||
if !tc.ok {
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, tc.priority, event.priority, "line=%q", tc.line)
|
||||
assert.Equal(t, tc.name, event.name, "line=%q", tc.line)
|
||||
assert.Equal(t, tc.args, event.args, "line=%q", tc.line)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseWpaTempDisabled(t *testing.T) {
|
||||
ssid, reason := parseWpaTempDisabled(`id=1 ssid="Home Net" auth_failures=1 duration=10 reason=WRONG_KEY`)
|
||||
assert.Equal(t, "Home Net", ssid)
|
||||
assert.Equal(t, "WRONG_KEY", reason)
|
||||
|
||||
ssid, reason = parseWpaTempDisabled(`id=2 ssid="quo\"te" auth_failures=3 duration=20 reason=CONN_FAILED`)
|
||||
assert.Equal(t, `quo"te`, ssid)
|
||||
assert.Equal(t, "CONN_FAILED", reason)
|
||||
|
||||
ssid, reason = parseWpaTempDisabled("garbage")
|
||||
assert.Empty(t, ssid)
|
||||
assert.Empty(t, reason)
|
||||
}
|
||||
|
||||
func TestDecodeWpaSSIDText(t *testing.T) {
|
||||
testCases := []struct {
|
||||
encoded string
|
||||
expected string
|
||||
}{
|
||||
{"Plain", "Plain"},
|
||||
{`With\tTab`, "With\tTab"},
|
||||
{`With\\Backslash`, `With\Backslash`},
|
||||
{`With\"Quote`, `With"Quote`},
|
||||
{`Hex\xc3\xa9`, "Hexé"},
|
||||
{`Trailing\`, `Trailing\`},
|
||||
{`Bad\xzz`, `Bad\xzz`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
assert.Equal(t, tc.expected, decodeWpaSSIDText(tc.encoded), "encoded=%q", tc.encoded)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWpaQuotedString(t *testing.T) {
|
||||
quoted, err := wpaQuotedString("passphrase")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, `"passphrase"`, quoted)
|
||||
|
||||
_, err = wpaQuotedString(`with"quote`)
|
||||
assert.Error(t, err)
|
||||
|
||||
_, err = wpaQuotedString("with\nnewline")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestParseWpaConfigPassphrase(t *testing.T) {
|
||||
config := "ctrl_interface=/var/run/wpa_supplicant\n" +
|
||||
"update_config=1\n" +
|
||||
"\n" +
|
||||
"network={\n" +
|
||||
"\tssid=\"Home\"\n" +
|
||||
"\tpsk=\"secret pass\"\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"network={\n" +
|
||||
"\tssid=486578\n" +
|
||||
"\tpsk=\"hexpass\"\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"network={\n" +
|
||||
"\tssid=\"RawKey\"\n" +
|
||||
"\tpsk=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\n" +
|
||||
"}\n"
|
||||
|
||||
pass, err := parseWpaConfigPassphrase(config, "Home")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "secret pass", pass)
|
||||
|
||||
pass, err = parseWpaConfigPassphrase(config, "Hex")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hexpass", pass)
|
||||
|
||||
_, err = parseWpaConfigPassphrase(config, "RawKey")
|
||||
assert.Error(t, err, "raw hex PSK is not a passphrase")
|
||||
|
||||
_, err = parseWpaConfigPassphrase(config, "Unknown")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func startFakeWpaSocket(t *testing.T, handler func(cmd string) []string) string {
|
||||
t.Helper()
|
||||
|
||||
sockPath := filepath.Join(t.TempDir(), "wlan0")
|
||||
conn, err := net.ListenUnixgram("unixgram", &net.UnixAddr{Name: sockPath, Net: "unixgram"})
|
||||
if err != nil {
|
||||
t.Fatalf("listen fake wpa socket: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
|
||||
go func() {
|
||||
buf := make([]byte, 4096)
|
||||
for {
|
||||
n, addr, err := conn.ReadFromUnix(buf)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, reply := range handler(string(buf[:n])) {
|
||||
conn.WriteToUnix([]byte(reply), addr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return sockPath
|
||||
}
|
||||
|
||||
func TestWpaCtrlConn_Request(t *testing.T) {
|
||||
sockPath := startFakeWpaSocket(t, func(cmd string) []string {
|
||||
switch cmd {
|
||||
case "PING":
|
||||
return []string{"PONG\n"}
|
||||
case "SCAN":
|
||||
return []string{"<2>CTRL-EVENT-SCAN-STARTED ", "OK\n"}
|
||||
default:
|
||||
return []string{"FAIL\n"}
|
||||
}
|
||||
})
|
||||
|
||||
conn, err := newWpaCtrlConn(sockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open wpa_ctrl conn: %v", err)
|
||||
}
|
||||
defer conn.close()
|
||||
|
||||
reply, err := conn.request("PING")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "PONG", reply)
|
||||
|
||||
reply, err = conn.request("SCAN")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "OK", reply, "unsolicited event datagrams must be skipped")
|
||||
|
||||
reply, err = conn.request("BOGUS")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "FAIL", reply)
|
||||
}
|
||||
|
||||
func TestWpaSupplicantBackend_ClassifyAttempt(t *testing.T) {
|
||||
backend, _ := NewWpaSupplicantBackend()
|
||||
|
||||
att := &wpaConnectAttempt{ssid: "Home", sawTempDisabled: true}
|
||||
assert.Equal(t, "bad-credentials", backend.classifyAttempt(att))
|
||||
|
||||
att = &wpaConnectAttempt{ssid: "Home"}
|
||||
assert.Equal(t, "no-such-ssid", backend.classifyAttempt(att))
|
||||
|
||||
backend.recentScansMu.Lock()
|
||||
backend.recentScans["Home"] = time.Now()
|
||||
backend.recentScansMu.Unlock()
|
||||
assert.Equal(t, "assoc-timeout", backend.classifyAttempt(att))
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package network
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (b *WpaSupplicantBackend) ConnectEthernet() error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectEthernet() error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectEthernetDevice(device string) error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ActivateWiredConnection(uuid string) error {
|
||||
return fmt.Errorf("wired control not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ListVPNProfiles() ([]VPNProfile, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ListActiveVPN() ([]VPNActive, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ConnectVPN(uuidOrName string, singleActive bool) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectVPN(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectAllVPN() error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ClearVPNCredentials(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ListVPNPlugins() ([]VPNPlugin, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ImportVPN(filePath string, name string) (*VPNImportResult, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetVPNConfig(uuidOrName string) (*VPNConfig, error) {
|
||||
return nil, fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) UpdateVPNConfig(uuid string, updates map[string]any) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DeleteVPN(uuidOrName string) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) SetVPNCredentials(uuid, username, password string, save bool) error {
|
||||
return fmt.Errorf("VPN not supported by wpa_supplicant backend")
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) ScanWiFiDevice(device string) error {
|
||||
return b.ScanWiFi()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) DisconnectWiFiDevice(device string) error {
|
||||
return b.DisconnectWiFi()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) GetWiFiDevices() []WiFiDevice {
|
||||
b.stateMutex.RLock()
|
||||
defer b.stateMutex.RUnlock()
|
||||
return b.getWiFiDevicesLocked()
|
||||
}
|
||||
|
||||
func (b *WpaSupplicantBackend) getWiFiDevicesLocked() []WiFiDevice {
|
||||
if b.state.WiFiDevice == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
stateStr := "disconnected"
|
||||
if b.state.WiFiConnected {
|
||||
stateStr = "connected"
|
||||
}
|
||||
|
||||
return []WiFiDevice{{
|
||||
Name: b.state.WiFiDevice,
|
||||
State: stateStr,
|
||||
Connected: b.state.WiFiConnected,
|
||||
SSID: b.state.WiFiSSID,
|
||||
BSSID: b.state.WiFiBSSID,
|
||||
Signal: b.state.WiFiSignal,
|
||||
IP: b.state.WiFiIP,
|
||||
Networks: b.state.WiFiNetworks,
|
||||
}}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
@@ -14,6 +15,7 @@ const (
|
||||
BackendIwd
|
||||
BackendConnMan
|
||||
BackendNetworkd
|
||||
BackendWpaSupplicant
|
||||
)
|
||||
|
||||
func nameHasOwner(bus *dbus.Conn, name string) (bool, error) {
|
||||
@@ -35,11 +37,25 @@ type DetectResult struct {
|
||||
ChosenReason string
|
||||
}
|
||||
|
||||
func wpaSupplicantCtrlDirPresent() bool {
|
||||
info, err := os.Stat(wpaSupplicantCtrlDir)
|
||||
return err == nil && info.IsDir()
|
||||
}
|
||||
|
||||
func DetectNetworkStack() (*DetectResult, error) {
|
||||
bus, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
// FreeBSD and minimal Linux systems may run wpa_supplicant with no
|
||||
// system bus at all; the control directory is the only signal there.
|
||||
if !wpaSupplicantCtrlDirPresent() {
|
||||
return nil, fmt.Errorf("connect system bus: %w", err)
|
||||
}
|
||||
return &DetectResult{
|
||||
Backend: BackendWpaSupplicant,
|
||||
HasWpaSupp: true,
|
||||
ChosenReason: "System bus unreachable; wpa_supplicant control directory present. Using wpa_ctrl interface.",
|
||||
}, nil
|
||||
}
|
||||
defer bus.Close()
|
||||
|
||||
hasNM, _ := nameHasOwner(bus, "org.freedesktop.NetworkManager")
|
||||
@@ -76,10 +92,14 @@ func DetectNetworkStack() (*DetectResult, error) {
|
||||
case hasNetworkd:
|
||||
res.Backend = BackendNetworkd
|
||||
res.ChosenReason = "systemd-networkd detected (no NM/ConnMan). Using networkd for L3 and wired."
|
||||
case wpaSupplicantCtrlDirPresent():
|
||||
res.Backend = BackendWpaSupplicant
|
||||
res.HasWpaSupp = true
|
||||
res.ChosenReason = "No NM/ConnMan/iwd/networkd; wpa_supplicant control directory present. Using wpa_ctrl interface."
|
||||
default:
|
||||
res.Backend = BackendNone
|
||||
if hasWpa {
|
||||
res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant present. Consider a wpa_supplicant path."
|
||||
res.ChosenReason = "No NM/ConnMan/iwd; wpa_supplicant D-Bus name present but control directory missing."
|
||||
} else {
|
||||
res.ChosenReason = "No known network manager bus names found."
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ func TestBackendType_Constants(t *testing.T) {
|
||||
assert.Equal(t, BackendType(2), BackendIwd)
|
||||
assert.Equal(t, BackendType(3), BackendConnMan)
|
||||
assert.Equal(t, BackendType(4), BackendNetworkd)
|
||||
assert.Equal(t, BackendType(5), BackendWpaSupplicant)
|
||||
}
|
||||
|
||||
func TestDetectResult_HasNetworkdField(t *testing.T) {
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
)
|
||||
|
||||
// ErrNoNetworkBackend is returned when no supported network management daemon
|
||||
// (NetworkManager, iwd, systemd-networkd, ConnMan) is present on the system.
|
||||
// (NetworkManager, iwd, systemd-networkd, ConnMan, wpa_supplicant) is present
|
||||
// on the system.
|
||||
var ErrNoNetworkBackend = errors.New("no supported network backend found")
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
@@ -62,6 +63,13 @@ func NewManager() (*Manager, error) {
|
||||
backend = nd
|
||||
}
|
||||
|
||||
case BackendWpaSupplicant:
|
||||
wpa, err := NewWpaSupplicantBackend()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create wpa_supplicant backend: %w", err)
|
||||
}
|
||||
backend = wpa
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", ErrNoNetworkBackend, detection.ChosenReason)
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const resumeDelay = 3 * time.Second
|
||||
@@ -95,21 +94,3 @@ func (m *Manager) Close() {
|
||||
}
|
||||
log.Info("TrayRecovery manager closed")
|
||||
}
|
||||
|
||||
// timeSuspended returns how long the system has spent in suspend since boot.
|
||||
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
||||
// CLOCK_MONOTONIC (excludes suspend).
|
||||
func timeSuspended() time.Duration {
|
||||
var bt, mt unix.Timespec
|
||||
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
||||
return 0
|
||||
}
|
||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
||||
return 0
|
||||
}
|
||||
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
||||
if diff < 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(diff)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package trayrecovery
|
||||
|
||||
import "time"
|
||||
|
||||
// FreeBSD has no CLOCK_BOOTTIME (clock_gettime(2)), so suspend time since
|
||||
// boot cannot be measured; the startup recovery scan is skipped.
|
||||
func timeSuspended() time.Duration {
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package trayrecovery
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// timeSuspended returns how long the system has spent in suspend since boot.
|
||||
// It is the difference between CLOCK_BOOTTIME (includes suspend) and
|
||||
// CLOCK_MONOTONIC (excludes suspend).
|
||||
func timeSuspended() time.Duration {
|
||||
var bt, mt unix.Timespec
|
||||
if err := unix.ClockGettime(unix.CLOCK_BOOTTIME, &bt); err != nil {
|
||||
return 0
|
||||
}
|
||||
if err := unix.ClockGettime(unix.CLOCK_MONOTONIC, &mt); err != nil {
|
||||
return 0
|
||||
}
|
||||
diff := (bt.Sec-mt.Sec)*int64(time.Second) + (bt.Nsec - mt.Nsec)
|
||||
if diff < 0 {
|
||||
return 0
|
||||
}
|
||||
return time.Duration(diff)
|
||||
}
|
||||
@@ -13,12 +13,12 @@ import (
|
||||
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
||||
)
|
||||
|
||||
const animKelvinStep = 25
|
||||
@@ -920,7 +920,7 @@ func (m *Manager) setGammaBytes(out *outputState, data []byte) error {
|
||||
return fmt.Errorf("gamma control invalid")
|
||||
}
|
||||
|
||||
fd, err := MemfdCreate("gamma-ramp", 0)
|
||||
fd, err := shm.CreateAnonFd("gamma-ramp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1295,11 +1295,3 @@ func (m *Manager) Close() {
|
||||
m.dbusConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func MemfdCreate(name string, flags int) (int, error) {
|
||||
fd, err := unix.MemfdCreate(name, flags)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package trash
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// readMountPoints returns user-visible mount points via getfsstat(2),
|
||||
// skipping pseudo and system filesystems.
|
||||
func readMountPoints() []string {
|
||||
n, err := unix.Getfsstat(nil, unix.MNT_NOWAIT)
|
||||
if err != nil || n == 0 {
|
||||
return nil
|
||||
}
|
||||
stats := make([]unix.Statfs_t, n)
|
||||
n, err = unix.Getfsstat(stats, unix.MNT_NOWAIT)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for _, st := range stats[:n] {
|
||||
mp := unix.ByteSliceToString(st.Mntonname[:])
|
||||
if mp == "" || skipMountPoint(mp, seen) {
|
||||
continue
|
||||
}
|
||||
seen[mp] = true
|
||||
out = append(out, mp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package trash
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// readMountPoints returns user-visible mount points from /proc/self/mountinfo,
|
||||
// skipping pseudo and system filesystems.
|
||||
func readMountPoints() []string {
|
||||
data, err := os.ReadFile("/proc/self/mountinfo")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for line := range strings.SplitSeq(string(data), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
mp := fields[4]
|
||||
if skipMountPoint(mp, seen) {
|
||||
continue
|
||||
}
|
||||
seen[mp] = true
|
||||
out = append(out, mp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -275,39 +275,16 @@ func allTrashDirs() []string {
|
||||
return dirs
|
||||
}
|
||||
|
||||
// readMountPoints returns user-visible mount points from /proc/self/mountinfo,
|
||||
// skipping pseudo and system filesystems.
|
||||
func readMountPoints() []string {
|
||||
data, err := os.ReadFile("/proc/self/mountinfo")
|
||||
if err != nil {
|
||||
return nil
|
||||
func skipMountPoint(mp string, seen map[string]bool) bool {
|
||||
if mp == "/" || seen[mp] {
|
||||
return true
|
||||
}
|
||||
skipPrefixes := []string{"/proc", "/sys", "/dev"}
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for line := range strings.SplitSeq(string(data), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
continue
|
||||
}
|
||||
mp := fields[4]
|
||||
if mp == "/" {
|
||||
continue
|
||||
}
|
||||
skip := false
|
||||
for _, p := range skipPrefixes {
|
||||
for _, p := range []string{"/proc", "/sys", "/dev"} {
|
||||
if mp == p || strings.HasPrefix(mp, p+"/") {
|
||||
skip = true
|
||||
break
|
||||
return true
|
||||
}
|
||||
}
|
||||
if skip || seen[mp] {
|
||||
continue
|
||||
}
|
||||
seen[mp] = true
|
||||
out = append(out, mp)
|
||||
}
|
||||
return out
|
||||
return false
|
||||
}
|
||||
|
||||
func List() ([]Entry, error) {
|
||||
|
||||
@@ -43,9 +43,9 @@ type Buffer struct {
|
||||
func CreateBuffer(width, height, stride int) (*Buffer, error) {
|
||||
size := stride * height
|
||||
|
||||
fd, err := unix.MemfdCreate("dms-shm", 0)
|
||||
fd, err := CreateAnonFd("dms-shm")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("memfd_create: %w", err)
|
||||
return nil, fmt.Errorf("create shm fd: %w", err)
|
||||
}
|
||||
|
||||
if err := unix.Ftruncate(fd, int64(size)); err != nil {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package shm
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// Mirrors FreeBSD libc memfd_create (lib/libc/gen/memfd_create.c):
|
||||
// shm_open2(SHM_ANON, O_RDWR, 0, SHM_GROW_ON_WRITE, name).
|
||||
// x/sys/unix has no wrapper for SYS_shm_open2 (571, sys/sys/syscall.h).
|
||||
const (
|
||||
sysShmOpen2 = 571
|
||||
shmAnon = 1 // SHM_ANON ((char *)1), sys/sys/mman.h
|
||||
shmGrowOnWrite = 0x2 // SHM_GROW_ON_WRITE, sys/sys/mman.h
|
||||
)
|
||||
|
||||
func CreateAnonFd(name string) (int, error) {
|
||||
p, err := unix.BytePtrFromString(name)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
fd, _, errno := unix.Syscall6(sysShmOpen2, shmAnon, unix.O_RDWR, 0, shmGrowOnWrite, uintptr(unsafe.Pointer(p)), 0)
|
||||
if errno != 0 {
|
||||
return -1, errno
|
||||
}
|
||||
return int(fd), nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package shm
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func CreateAnonFd(name string) (int, error) {
|
||||
return unix.MemfdCreate(name, 0)
|
||||
}
|
||||
+1
-1
Submodule dank-qml-common updated: 4fa313766a...493b609cc0
Generated
+3
-3
@@ -3,11 +3,11 @@
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1784386873,
|
||||
"narHash": "sha256-n0LKLirPnNSXYxElK1IpnybEwU0nnyso4kpsEFX88U0=",
|
||||
"lastModified": 1784471445,
|
||||
"narHash": "sha256-hRTmcd3Ms9DF3X35ZiapXM15rG5X1DtIdlrQZwRINFE=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "4fa313766ab5cd2766be0f87a163305ca9af6faf",
|
||||
"rev": "493b609cc04659f1388f5738e8f65d6c531994fe",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -184,6 +184,8 @@ done
|
||||
}
|
||||
|
||||
Row {
|
||||
// charge_control_* live in Linux sysfs; no BSD equivalent
|
||||
visible: Qt.platform.os === "linux"
|
||||
width: parent.width
|
||||
height: applyButton.height
|
||||
layoutDirection: I18n.isRtl ? Qt.LeftToRight : Qt.RightToLeft
|
||||
|
||||
@@ -361,6 +361,16 @@ EOFCONFIG
|
||||
wireplumberReloading = true;
|
||||
wireplumberReloadStarted();
|
||||
|
||||
if (!SessionService.systemctlCommandAvailable) {
|
||||
Proc.runCommand("restartWireplumber", ["sh", "-c", "pkill -x wireplumber; sleep 1"], () => {
|
||||
Quickshell.execDetached(["wireplumber"]);
|
||||
wireplumberReloading = false;
|
||||
ToastService.showInfo(I18n.tr("Audio system restarted"), I18n.tr("Device names updated"));
|
||||
wireplumberReloadCompleted(true);
|
||||
}, 5000);
|
||||
return;
|
||||
}
|
||||
|
||||
Proc.runCommand("restartWireplumber", ["systemctl", "--user", "restart", "wireplumber"], (output, exitCode) => {
|
||||
wireplumberReloading = false;
|
||||
|
||||
|
||||
@@ -948,8 +948,12 @@ Singleton {
|
||||
// systemd user environment from previous sessions and lie. Unset
|
||||
// WAYLAND_DISPLAY falls back to "wayland-0", mirroring wl_display_connect.
|
||||
// /proc/net/unix: field 6 is state (01 = listening), 7 inode, 8 bound path.
|
||||
// The BSDs have no /proc/net; sockstat(1) -l -u lists listening unix
|
||||
// sockets as USER COMMAND PID FD PROTO LOCAL-ADDRESS.
|
||||
function detectCompositor() {
|
||||
const script = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$sock" ;; esac; inode=$(awk -v p="$sock" \'$8 == p && $6 == 1 {print $7; exit}\' /proc/net/unix); [ -n "$inode" ] || exit 1; fd=$(find /proc/[0-9]*/fd/ -mindepth 1 -maxdepth 1 -lname "socket:\\[$inode\\]" 2>/dev/null | head -n1); [ -n "$fd" ] || exit 1; pid="${fd#/proc/}"; cat "/proc/${pid%%/*}/comm"';
|
||||
const procScript = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$sock" ;; esac; inode=$(awk -v p="$sock" \'$8 == p && $6 == 1 {print $7; exit}\' /proc/net/unix); [ -n "$inode" ] || exit 1; fd=$(find /proc/[0-9]*/fd/ -mindepth 1 -maxdepth 1 -lname "socket:\\[$inode\\]" 2>/dev/null | head -n1); [ -n "$fd" ] || exit 1; pid="${fd#/proc/}"; cat "/proc/${pid%%/*}/comm"';
|
||||
const sockstatScript = 'sock="${WAYLAND_DISPLAY:-wayland-0}"; case "$sock" in /*) ;; *) sock="${XDG_RUNTIME_DIR:-/var/run/user/$(id -u)}/$sock" ;; esac; sockstat -l -u | awk -v p="$sock" \'$6 == p {print $2; exit}\'';
|
||||
const script = Qt.platform.os === "unix" ? sockstatScript : procScript;
|
||||
Proc.runCommand("waylandSocketOwner", ["sh", "-c", script], (output, exitCode) => {
|
||||
const comm = (exitCode === 0 && output) ? output.trim().toLowerCase() : "";
|
||||
const name = _compositorNameFromComm(comm);
|
||||
@@ -1037,7 +1041,7 @@ Singleton {
|
||||
{
|
||||
name: "labwc",
|
||||
present: !!labwcPid,
|
||||
test: ["sh", "-c", "[ \"$(cat /proc/\"$LABWC_PID\"/comm 2>/dev/null)\" = labwc ]"],
|
||||
test: ["sh", "-c", "[ \"$(ps -p \"$LABWC_PID\" -o comm= 2>/dev/null)\" = labwc ]"],
|
||||
detail: "LABWC_PID " + labwcPid
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,6 +12,8 @@ Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("SessionService")
|
||||
|
||||
// Qt.platform.os is "unix" on the BSDs (qtdeclarative qqmlplatform.cpp)
|
||||
readonly property bool isBSD: Qt.platform.os === "unix"
|
||||
property bool hasUwsm: false
|
||||
property bool isElogind: false
|
||||
property bool loginctlCommandAvailable: false
|
||||
@@ -351,10 +353,26 @@ Singleton {
|
||||
}
|
||||
|
||||
function powerManagerCommand(action) {
|
||||
if (isBSD)
|
||||
return bsdPowerCommand(action);
|
||||
const useLoginctl = isElogind || (loginctlCommandAvailable && !systemctlCommandAvailable);
|
||||
return [useLoginctl ? "loginctl" : "systemctl", action];
|
||||
}
|
||||
|
||||
function bsdPowerCommand(action) {
|
||||
switch (action) {
|
||||
case "suspend":
|
||||
case "suspend-then-hibernate":
|
||||
return ["acpiconf", "-s", "3"];
|
||||
case "hibernate":
|
||||
return ["acpiconf", "-s", "4"];
|
||||
case "reboot":
|
||||
return ["shutdown", "-r", "now"];
|
||||
default:
|
||||
return ["shutdown", "-p", "now"];
|
||||
}
|
||||
}
|
||||
|
||||
function suspend() {
|
||||
if (SettingsData.customPowerActionSuspend.length === 0) {
|
||||
Quickshell.execDetached(powerManagerCommand("suspend"));
|
||||
|
||||
@@ -10,6 +10,9 @@ Singleton {
|
||||
|
||||
readonly property var log: Log.scoped("UsersService")
|
||||
|
||||
// Qt.platform.os is "unix" on the BSDs; user management goes through pw(8)
|
||||
readonly property bool isBSD: Qt.platform.os === "unix"
|
||||
|
||||
property var users: []
|
||||
property string adminGroup: "wheel"
|
||||
property string greeterGroup: "greeter"
|
||||
@@ -286,7 +289,7 @@ Singleton {
|
||||
property string op: "passwd"
|
||||
property var cb: null
|
||||
property string capturedErr: ""
|
||||
command: ["pkexec", "sh", "-c", "head -n1 | chpasswd"]
|
||||
command: root.isBSD ? ["pkexec", "pw", "usermod", "-n", targetUser, "-h", "0"] : ["pkexec", "sh", "-c", "head -n1 | chpasswd"]
|
||||
stdinEnabled: true
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
@@ -294,6 +297,9 @@ Singleton {
|
||||
onStreamFinished: chpasswdProc.capturedErr = text || ""
|
||||
}
|
||||
onStarted: {
|
||||
if (root.isBSD)
|
||||
chpasswdProc.write(chpasswdProc.targetPassword + "\n");
|
||||
else
|
||||
chpasswdProc.write(chpasswdProc.targetUser + ":" + chpasswdProc.targetPassword + "\n");
|
||||
}
|
||||
onExited: exitCode => {
|
||||
@@ -414,7 +420,7 @@ Singleton {
|
||||
|
||||
function _runUseradd(username, password, addToAdmin, addToGreeter, callback) {
|
||||
const proc = useraddComp.createObject(root, {
|
||||
command: ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
|
||||
command: root.isBSD ? ["pkexec", "pw", "useradd", "-n", username, "-m", "-s", "/bin/sh"] : ["pkexec", "useradd", "-m", "-s", "/bin/bash", username],
|
||||
targetUser: username,
|
||||
targetPassword: password,
|
||||
addAdmin: addToAdmin,
|
||||
@@ -436,15 +442,21 @@ Singleton {
|
||||
|
||||
function _runUserdel(username, callback) {
|
||||
const proc = userdelComp.createObject(root, {
|
||||
command: ["pkexec", "userdel", "-r", username],
|
||||
command: root.isBSD ? ["pkexec", "pw", "userdel", "-n", username, "-r"] : ["pkexec", "userdel", "-r", username],
|
||||
targetUser: username,
|
||||
cb: callback
|
||||
});
|
||||
proc.running = true;
|
||||
}
|
||||
|
||||
function _groupMemberCmd(group, username, add) {
|
||||
if (root.isBSD)
|
||||
return ["pkexec", "pw", "groupmod", group, add ? "-m" : "-d", username];
|
||||
return add ? ["pkexec", "usermod", "-aG", group, username] : ["pkexec", "gpasswd", "-d", username, group];
|
||||
}
|
||||
|
||||
function _runAdminToggle(username, makeAdmin, callback) {
|
||||
const cmd = makeAdmin ? ["pkexec", "usermod", "-aG", root.adminGroup, username] : ["pkexec", "gpasswd", "-d", username, root.adminGroup];
|
||||
const cmd = _groupMemberCmd(root.adminGroup, username, makeAdmin);
|
||||
const proc = adminToggleComp.createObject(root, {
|
||||
command: cmd,
|
||||
targetUser: username,
|
||||
@@ -455,7 +467,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function _runGreeterToggle(username, enableGreeter, callback) {
|
||||
const cmd = enableGreeter ? ["pkexec", "usermod", "-aG", root.greeterGroup, username] : ["pkexec", "gpasswd", "-d", username, root.greeterGroup];
|
||||
const cmd = _groupMemberCmd(root.greeterGroup, username, enableGreeter);
|
||||
const proc = greeterToggleComp.createObject(root, {
|
||||
command: cmd,
|
||||
targetUser: username,
|
||||
|
||||
@@ -46,7 +46,8 @@ Singleton {
|
||||
property int _geocodeReqId: 0
|
||||
property var _pendingCoords: null
|
||||
|
||||
readonly property var lowPriorityCmd: ["nice", "-n", "19", "ionice", "-c3"]
|
||||
// ionice is util-linux only; the BSDs get plain nice
|
||||
readonly property var lowPriorityCmd: Qt.platform.os === "linux" ? ["nice", "-n", "19", "ionice", "-c3"] : ["nice", "-n", "19"]
|
||||
readonly property var curlBaseCmd: ["curl", "-sS", "--fail", "--connect-timeout", "3", "--max-time", "6", "--limit-rate", "100k", "--compressed"]
|
||||
|
||||
property var weatherIcons: ({
|
||||
|
||||
Reference in New Issue
Block a user